在之前的数据处理环节中,用CSI Tool收集到的原始数据信号,经历了数据解析、降噪、插值的处理步骤,变成了干净、完整的信号片段,这是后续做更进一步分析的基础。
在开始阅读本篇博客前,需要说明两个重要的点:
- 在先前的代码中,为了能够一次性提取出所有重要的信息,振幅和相位数据都进行了计算,被存储到result_matrix这个数组中,但是从本篇博客开始,将仅围绕着振幅值进行分析。具体提以什么值进行分析,请结合自己的研究对象以及相关文献。
- 后续的特征提取以及研究思路都是基于人类活动识别,CSI感知所涵盖的领域较多,而特征的提取与识别对象本身息息相关,因此需要对比自己所研究的领域,同本博客的是否有相似性。
本环节是特征提取环节,起承上启下的作用:对上,应着数据处理完毕的信号,但是还不能直接进行分析,因为此时信号片段过长,对于活动识别而言,存在非许多目标片段(例如失败不规范的动作、休息间歇等);对下,具体采取何种活动片段提取策略,需要配合后续采取的活动识别算法进行(深度学习/机器学习,CNN/RNN/LSTM等等),不同的算法适配不同的数据形式,例如CNN一般用于识图,不需要关注时间顺序,LSTM则要求保留时间关系等等,所以本博客后续仅介绍几种分类思路。
以CNN识图这种非常简单粗暴的一种识别方法为例,由于最后是需要图像形式的数据集,因此本环节,经历了以下几个步骤:
①标记活动点→②切割信号片段→③筛选活动片段→④转化为图像帧
具体怎么实现上述步骤呢,这里参考了下面博客的做法。这个github是我做CSI识别时候的救星,虽然是LSTM算法,但是里面的代码很全,只要提供信号数据文件和标注文件,就可以自动去进行一系列后续的操作。
GitHub - ermongroup/Wifi_Activity_Recognition: Code for IEEE Communication Magazine (A Survey on Behaviour Recognition Using WiFi Channle State Information)
在上面这个博客中,一共有三个重要的Python文件,其顺序和作用是:
cross_vali_data_convert_merge.py:从指定路径中的CSV文件中导入数据,并根据一定的条件将数据处理成滑动窗口的形式,然后将处理后的数据保存到新的CSV文件中。
cross_vali_input_data.py:读取前述代码处理好的CSV文件,对数据进行降采样、预处理,根据活动阈值进行筛选,并将处理后的数据存储到字典中。
cross_vali_recurrent_network_wifi_activity.py:LSTM算法部分。
接下来列出的代码源于上述博客,作出了一定修改。下面将按照代码,详细介绍每一步需要准备的文件和处理流程,实验细节以之前博客所设定的参数为例。
① 标记活动点
在之前的环节中,得到的是一段信号片段result_matrix,由于只需要振幅数值,那么现在result_matrix这个数组的维度是(N,91),N为数据包的个数。图像化展示一段信号的振幅图像如下:
可以看到圈起来的片段波动较大,对应着志愿者实际发生活动,而在活动的间歇间,振幅波动则很小,因此振幅的方差可以作为是否发生活动的一种判定方式,有相关文献是根据此进行自动化的活动片段提取。但是并不是所有的活动数据都能采用这个思路,不怕麻烦的话,人工对照视频监控对数据文件进行标注是最准确的方法,可以手动淘汰不标准的动作。不过改方式要求在先前做实验的时候,记录每次采集数据开始的时间,因为CSI信号数据本身并未存储现实生活中的时间,只有相对时间信息。举例,一段长度为10000的CSI信号,采样频率是1000Hz,可知该段信号记录了10s的活动,如果采集开始时刻已知,即可知道这段信号对应的真实时间段。
首先,将数据处理环节得到的振幅值以csv格式进行存储,每个实验样本的CSI振幅为一个csv文件,期内有90*N个数据,90表示子载波数量,N为该段实验样本的数据包个数,命名为input系列文件。与之相对的,每个实验样本还需要有一个标注文件,命名为annotation系列文件,其内数据维度为1*N,每行的内容即是对应信号数据文件的标注内容。强烈建议从一开始就对所有文件进行规范命名,如下图所示(input/annotation_志愿者代号_活动类型代号_数字编号):
②&③ 切割并筛选信号片段
如代码所示,整理好的input系列文件和annotation系列文件被放在filepath1和filepath2中,然后依次被送进dataimport函数处理。先统一用滑动窗口的方式把一段长的实验样本切割为若干个等长的小样本,代码中设置的小样本长度为0.5s。同时为了便于管理,每种分类的类别都以数字编码。统计每一样本内部各个类别出现的频次,若有类别频次大于设定的活动阈值,则标记该样本为相应类别。被标记为未发生活动的样本,将会从信号文件中剔除。输出的文件为xx和yy的csv文件,前者是重整筛选后的信号数据,维度是(M,90*500),90*500即一个样本的所有振幅数据,M是切割后的样本数量;后者是标记文件,维度是(M,10),在这个矩阵中,发生活动的时间点,对应列数会被标记为“1”,否则第一列为“2”。由于经历了剔除环节,所以可以看到在不同yy文件中,相应列下都是清一色的"1"。后续仅关注xx系列文件即可。
需要注意的是,由于CNN算法不关注时间顺序,所以这里对实验样本的顺序并没有做出严格管理,只是简单粗暴的按照类别把实验样本放在一起。
import numpy as np,numpy
import csv
import glob
import os
window_size = 500 # 滑动窗口大小
threshold = 80 # 活动阈值
slide_size = 400 # 滑动窗口每次滑动长度,建议设置大小不超过window_size
# 定义数据导入函数dataimport
# 三个输入参数:filepath1 表示输入文件路径模式,filepath2 表示注释文件路径模式,slide_size 表示滑动窗口的大小。
def dataimport(filepath1, filepath2, slide_size):
xx = np.empty([0, window_size, 90], dtype=np.float16)
yy = np.empty([0, 10], float)
###Input data###
# data import from csv
input_csv_files = sorted(glob.glob(filepath1))
for f in input_csv_files:
print("input_file_name=", f)
data = [[float(elm) for elm in v] for v in csv.reader(open(f, "r"))]
tmp1 = np.array(data, dtype=np.float16)
x2 = np.empty([0, window_size, 90], dtype=np.float16)
# data import by slide window
k = 0
while k <= (len(tmp1) + 1 - 2 * window_size):
x = np.dstack(np.array(tmp1[k:k + window_size, 0:90]).T)
x2 = np.concatenate((x2, x), axis=0)
k += slide_size
xx = np.concatenate((xx, x2), axis=0)
xx = xx.reshape(len(xx), -1)
xx = xx.astype(np.float16)
###Annotation data###
# data import from csv
annotation_csv_files = sorted(glob.glob(filepath2))
for ff in annotation_csv_files:
print("annotation_file_name=", ff)
ano_data = [[str(elm) for elm in v] for v in csv.reader(open(ff, "r"))]
tmp2 = np.array(ano_data)
# data import by slide window
y = np.zeros(((len(tmp2) + 1 - 2 * window_size) // slide_size + 1, 10))
k = 0
while k <= (len(tmp2) + 1 - 2 * window_size):
y_pre = np.stack(np.array(tmp2[k:k + window_size]))
liedown = 0
write = 0
read = 0
walk = 0
clean = 0
armtrain = 0
squat = 0
run = 0
jump = 0
noactivity = 0
for j in range(window_size):
if y_pre[j] == "LieDown":
liedown += 1
elif y_pre[j] == "Write":
write += 1
elif y_pre[j] == "Read":
read += 1
elif y_pre[j] == "Walk":
walk += 1
elif y_pre[j] == "Clean":
clean += 1
elif y_pre[j] == "ArmTrain":
armtrain += 1
elif y_pre[j] == "Squat":
squat += 1
elif y_pre[j] == "Run":
run += 1
elif y_pre[j] == "Jump":
jump += 1
else:
noactivity += 1
# 对样本进行标注
if liedown > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
elif write > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])
elif read > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 0])
elif walk > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 0, 0, 0, 1, 0, 0, 0, 0, 0])
elif clean > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 0, 0, 0, 0, 1, 0, 0, 0, 0])
elif armtrain > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 0, 0, 0, 0, 0, 1, 0, 0, 0])
elif squat > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0])
elif run > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 0, 0, 0, 0, 0, 0, 0, 1, 0])
elif jump > window_size * threshold / 100:
y[int(k / slide_size), :] = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
else:
y[int(k / slide_size), :] = np.array([2, 0, 0, 0, 0, 0, 0, 0, 0, 0])
k += slide_size
yy = np.concatenate((yy, y), axis=0)
# 剔除被标记为 "no activity"的样本
xx = xx[yy[:, 0] != 2]
yy = yy[yy[:, 0] != 2]
print(xx.shape, yy.shape)
return (xx, yy)
#### Main ####
if not os.path.exists(r"D:\DATA\最终版本\input_files"):
os.makedirs(r"D:\DATA\最终版本\input_files")
clients = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O']
activities = ["LieDown", "Write", "Read", "Walk", "Clean", "ArmTrain", "Squat", "Run", "Jump"]
for client in clients:
for activity in activities:
filepath1 = fr"D:\DATA\最终版本\annotated_CSI_data\INPUT\input_{client}_*{activity}*.csv"
filepath2 = fr"D:\DATA\最终版本\annotated_CSI_data\ANNOTATION\annotation_{client}_*{activity}*.csv"
if glob.glob(filepath1) and glob.glob(filepath2):
outputfilename1 = fr"D:\DATA\最终版本\input_files\xx_{client}_{window_size}_{threshold}_{slide_size}_{activity}.csv"
outputfilename2 = fr"D:\DATA\最终版本\input_files\yy_{client}_{window_size}_{threshold}_{slide_size}_{activity}.csv"
x, y = dataimport(filepath1, filepath2, slide_size)
with open(outputfilename1, "w") as f:
writer = csv.writer(f, lineterminator="\n")
writer.writerows(x)
with open(outputfilename2, "w") as f:
writer = csv.writer(f, lineterminator="\n")
writer.writerows(y)
print(f"{client}_{activity} finish!")
else:
print(f"No files found for {client}_{activity}, skipping.")
Jupyter中运行如下图所示:
友情提示,如果采样频率很高,输出文件会很大,注意内存管理。
输出得到的文件如下图所示。这份代码的作用简单来说,就是把长的信号段切割为小的样本,再根据标签频次,判定小样本属于哪一类活动,最后剔除哪些被未发生活动的样本。
④ 转化为图像帧
在上一环节中,根据采集数据时候的标签,筛选得到了若干个活动信号片段,并存储在xx系列csv文件。这一部分则需要把每个样本转化为图像,便于后续深度学习。
我又换到了Matlab,折腾过来是因为这边更熟悉,并行池一开出图非常快,因为输出大量图片是一个比较耗时的工作。实际上也可以用Python写,逻辑很简单。如果想要更快,可以先把csv文件统一转化为matlab的数据文件,代码读取就更快了。
Matlab代码如下。转化逻辑如代码所示,因为每一张图像的维度都是500*90,所以经过归一化后直接被映射为了灰度图像。
clc
clear
close all
% 定义文件路径和活动种类
input_path = 'D:\DATA\最终版本\input_files';
output_path = 'D:\DATA\最终版本\images';
if ~exist(output_path, 'dir')
mkdir(output_path);
end
% 读取文件名
files = dir(fullfile(input_path, 'xx*.csv'));
file_names = {files.name};
% 初始化cell数组来存放提取的活动类型
activity_types = {};
% 遍历所有的文件名并提取活动类型
for i = 1:length(file_names)
% 使用"_"来分割文件名
[~, file_name_no_ext, ~] = fileparts(file_names{i});
split_name = strsplit(file_name_no_ext, '_');
activity_name = split_name{end};
% 如果这个活动类型尚未添加到cell数组中,则添加它
if ~ismember(activity_name, activity_types)
activity_types{end+1} = activity_name;
end
end
% 检查并创建输出文件夹
if ~exist(output_path, 'dir')
mkdir(output_path);
end
% 为每一种活动创建文件夹
for act = activity_types
if ~exist(fullfile(output_path, act{1}), 'dir')
mkdir(fullfile(output_path, act{1}));
end
end
% 初始化并行池
if isempty(gcp('nocreate'))
parpool;
end
for i = 1:length(files)
% 加载数据
data_file = fullfile(input_path, files(i).name);
data = single(load(data_file));% 改为single类型节省内存
% 从文件名中获取活动标签
[~, file_name, ~] = fileparts(data_file);
parts = strsplit(file_name, '_');
activity_label = parts{end};
volunteer = parts{2};
% 对每张图像进行处理
parfor j = 1:size(data, 1)
% 取出单张图像的数据
single_data = squeeze(data(j, :, :));
reshaped_data = reshape(single_data, [90, 500]);
% 生成图像
% 使用极值归一化
min_val = min(reshaped_data(:));
max_val = max(reshaped_data(:));
img = (reshaped_data - min_val) / (max_val - min_val);
img_filename = sprintf('%s_%s_%d_%d.png', volunteer, activity_label, i, j);
imwrite(img, fullfile(output_path, activity_label, img_filename));
end
disp(['文件 ' data_file ' 的所有图片已生成完毕!']);
end
所有图像都被分门别类的放在对应的文件夹中,如下所示: