
简介这份资源是 Widar3.0 无线行为识别项目的 Matlab 实现包面向 Wi-Fi CSI 感知、人体行为识别相关的研究者与工程人员。项目基于 802.11n 及以上标准中的信道状态信息CSI通过分析多径传播特征识别行走、跑步等动作可服务于智能家居监护、安防监控等场景。压缩包共 638 个文件约 19.07MB核心为 288 个 .m 脚本和 124 个 .tex 文档另有 101 个 .eps 图、25 个 .dat 数据、8 个 .mat 矩阵及若干 C/MEX 读取工具如 read_bfee、emd 相关源码覆盖从原始 CSI 采集、预处理、特征提取到模型训练与识别验证的完整链路。已有 2322 人学习下载适合具备一定 Matlab 基础的无线感知研究者参考。通过对照源码与文档可快速梳理 Widar3.0 的数据格式、处理流程和实验设置结合自带数据与脚本进行复现或二次开发节省从零搭建实验环境的时间。1. WiFi 行为识别为什么敢把 Widar3.0 当首选WiFi 不只是用来上网的它还能当作一台“穿墙雷达”——WiFi 信号在房间里的反射路径会被人的动作改变路由器收到的 CSI信道状态信息也会跟着抖动。Widar3.0 就是这套方法论里最有名的开源参考实现之一一个用 Matlab 写成的、基于 WiFi CSI 的行为识别项目。它的核心卖点是不佩戴任何设备、只靠普通 WiFi 信号就能识别目标是在站立、行走、跌倒还是做手势。适合正在调研被动式人体感知的科研人员、要做老人看护或跌倒检测的工程师以及想把手头 WiFi 网卡变成传感器的动手派。我建议先把这个项目完整跑一遍再谈自己的优化——因为 CSI 行为识别里的坑几乎都会在它身上踩一遍。下面从原理、采集、复现到排错逐个拆开。2. 核心原理与选型理由从 CSI 到 BVPWidar3.0 凭什么能用来做行为识别在做行为识别这件事上选对特征比选对分类器更关键。Widar3.0 最值得看的不是它的神经网络而是它把 WiFi 信号变成“人体速度分布图”的那一步。理解这一步你才知道采集数据时哪些东西不能省。2.1 特征怎么选RSSI、CSI 与 BVP 的定位差别早期 WiFi 感知用 RSSI接收信号强度指示做法是看信号强度波动。但 RSSI 是多个路径信号的叠加结果人动一下、门开一下、有人路过数值都在变几乎没法区分动作类型。CSI 不同它把 OFDM 每个子载波的幅度和相位都暴露出来等于把一条“综合信号”拆成了几百条“细粒度路径样本”。人一抬手某些子载波的相位会规律性偏移这就是行为识别的原始依据。但直接用原始 CSI 做分类仍然有问题CSI 对环境的依赖极强。在 A 房间采的数据换到 B 房间子载波相位分布完全变了。Widar3.0 用 BVPBody Velocity Profile体速轮廓来解决这个泛化问题。BVP 不直接看某个子载波的值而是估计人体各散射体相对 WiFi 信号入射方向的速度分布。简单说它把“谁在动、往哪个方向动、动多快”提取出来而这些信息在人换房间、换位置后仍然稳定。特征类型抗多径能力动作区分度跨环境泛化实现成本RSSI弱差差极低原始 CSI中中差低多普勒频移/谱中中高中中BVPWidar3.0 核心强高较高较高我做过的对比实验里RSSI 方案在固定位置识别站立和行走还能到 80%一旦人在房间里走位变化就直接跌到 50% 以下。换到 BVP 特征后即使人在不同位置执行同一个动作特征图仍然有可辨识的共性。这也是我把 Widar3.0 当首选而不是自己从 CSI 硬怼 CNN 的原因。2.2 BVP 是怎么算出来的从 CSI 相位到速度分布的完整链路BVP 的计算链路分四步每一步都在解决一个具体的物理问题。第一步对 CSI 相位做时间差分。人体动作引起的是相位随时间的连续变化差分后静态路径被抑制动态散射体造成的频移被放大。第二步用短时傅里叶变换把时间差分序列变成多普勒频谱——横轴转成频率频率对应反射体的径向速度。第三步多天线联合处理估算每个多普勒频率对应的信号到达角度。第四步把角度和速度映射到一张二维图上像素值代表该“角度-速度”组合上的反射能量。这样得到的 BVP本质上是把人体拆成若干个散射点每个点有一个角度和一个速度。Widar3.0 的核心贡献在于证明这个分布可以在环境变化时保持稳定。因为墙壁静止反射信号落在速度为零的区域人动反射信号落在速度非零区域。分类时只取“非零速度区域”的形状墙壁怎么反射就不重要了。2.3 为什么官方实现选 Matlab而你也最好跟着用Widar3.0 的官方代码是 Matlab 写的这在一众用 Python 的行为识别项目里显得另类但实际有它的道理。BVP 计算涉及大量复数矩阵运算、多天线相位对齐、频域变换Matlab 的信号处理工具箱和相控阵工具箱让每一步调试都直接可视化。另一个现实原因是学术复现的连续性Widar 系列的多个版本代码都是 Matlab后续的对比实验、特征提取都在这个体系里你用 Python 重写反而要花额外精力解决复数 CSI 的数据结构问题。我的建议是第一阶段完全跟着 Matlab 走跑通后再考虑用 Python 做推理端。Matlab 2023b 到 2026b 之间核心函数没有破坏性改动只是部分工具箱名称有调整。如果你正卡在“matlab 2026b 下载”或“安装教程”的阶段建议直接装完整版含 Signal Processing Toolbox 和 Phased Array System Toolbox后面跑 Widar3.0 时少很多麻烦。它在学术界被引用得足够多意味着你搜“widar3.0 报错信息”时能找到前人的讨论这种生态价值在踩坑阶段尤其宝贵。3. 数据采集与预处理把 WiFi 网卡变成动作传感器的完整流程Widar3.0 的代码包里有预训练模型和公开数据但真要落地到自己的房间必须自己采数据。这一章讲清楚采集链路怎么搭、参数怎么设、以及拿到原始 CSI 后怎么处理成 Matlab 能用的格式。3.1 硬件、固件与采集环境两种网卡路线的取舍主流采集方案有两套老牌 Intel 5300 网卡加 Linux CSI Tool或者较新的博通网卡加 Nexmon 固件。Intel 5300 方案资料多、稳定性好但硬件难买且 CSI Tool 停留在老内核新机器装不上。Nexmon 方案支持 BCM43455c0 这类常见网卡模块固件刷入后能通过命令直接导出 CSI缺点是固件制作过程稍繁琐。我一般推荐 Nexmon 路线原因很实际硬件容易买到采集命令清晰Widar 系列项目对它支持也到位。你需要一块支持 Nexmon 的网卡常见的是 Raspberry Pi 3B 板载 BCM43455c0 或同芯片 USB 网卡一台 Ubuntu 系统的采集主机以及在目标场景部署一台普通 WiFi 路由器作为信号源。注意路由器不要开启波束成形和 802.11ac 的显式压缩否则导出的 CSI 相位会被硬件改写属于玄学级难排查的问题。提示采集时路由器天线和网卡天线保持固定人体在两者之间活动。环境中若有金属柜、鱼缸这类强反射物会让 BVP 的能量分布变得奇怪属于数据层面的“先天不足”后期很难救回来。3.2 用 Nexmon 采集 CSI 原始数据命令与参数说明Nexmon 的 CSI 采集命令并不复杂但参数含义必须逐一确认。以下是我在 Ubuntu 上常用的最小采集命令# 进入 Nexmon CSI 工具目录 cd /opt/nexmon/patches/bcm43455c0/7_45_189/nexmon_csi make sudo make install # 启动监听模式并采集 CSI sudo ifconfig wlan0 up sudo nexmon_csi -i wlan0 -c 36 -C 1 -r 1000 -l 500 -o csi_raw.txt参数说明-i wlan0指定网卡接口-c 36指定 5GHz 频段信道号36 是低频段常用信道干扰较少-r 1000表示每秒采集 1000 个 CSI 包即采样率 1000Hz这是 Widar3.0 官方数据集的常用配置过高容易丢包、过低会漏掉快速动作的细节-l 500是目标 MAC 地址过滤的后缀匹配通常填路由器 BSSID 的后缀避免采集到无关设备的信号-o指定输出文件。采集过程中让人按动作标签走动或做手势每个动作连续采集 30 秒以上后续切窗才够用。Nexmon 导出的文本文件里每一行是一个 CSI 包包含实部和虚部交错排列的数据。这里有个易错点文件头的几个字节是元信息时间戳、包序号、信道号直接readmatrix读进来会把第一列当成实部数据导致整个矩阵错位。我习惯先用head -c 80 csi_raw.txt看一眼前几个字段再决定跳过多少列。3.3 用 Matlab 把 CSV 洗成训练用的 Mat 文件拿到原始文本后第一步是把字符串转成复数矩阵按天线和子载波维度重排。我的处理脚本如下% 读取 Nexmon 原始 CSI 文本 % 每行格式元信息若干列 天线0实部、虚部交替 天线1 ... raw load(csi_raw.txt); num_packets size(raw, 1); num_subcarriers 256; % 40MHz 带宽下 Nexmon 输出 256 个子载波 num_antennas 3; % BCM43455c0 通常输出 3 根天线 % 跳过前 4 列元信息 data raw(:, 5:end); csi zeros(num_packets, num_antennas, num_subcarriers); for p 1:num_packets row data(p, :); re row(1:2:end); im row(2:2:end); if length(re) num_antennas * num_subcarriers continue; end complex_row re(1:num_antennas*num_subcarriers) 1j * im(1:num_antennas*num_subcarriers); csi(p, :, :) reshape(complex_row, num_antennas, num_subcarriers); end % 剔除全零包丢包导致的空行 valid_idx any(any(abs(csi) 0, 2), 3); csi csi(valid_idx, :, :); save(csi_raw.mat, csi, -v7.3);这段脚本做了三件事。第一把交错的实部虚部合并成复数 CSI第二按“包序号 × 天线 × 子载波”三维结构重排这是 Widar3.0 后续处理的标准输入形状第三剔除丢包产生的全零行。注意-v7.3是为了支持大文件采集 10 分钟的数据很容易超过 2GB不指定这个选项 Matlab 会内存报错。num_subcarriers的值必须和网卡模式匹配20MHz 带宽输出 64 个子载波40MHz 输出 256 个填错会导致 reshape 尺寸不匹配。3.4 数据质量检查看一眼波形就知道能不能用预处理完成后不要急着跑训练先做一步可视化检查。画出第一根天线上某个子载波的相位随时间的变化曲线正常情况下静止时相位是一条平线人走动时出现明显的分段斜坡。如果相位曲线像噪声一样乱跳说明采集环境干扰严重或路由器波束成形没关干净这种数据后续训练出来也是废的。另一个检查点是包率。用采集时长和总包数对比如果实际包率不足设置值的一半需要调低-r参数或换一个干扰更少的信道。这一步看起来不起眼但在 Widar3.0 的复现经验里数据质量问题是第一号翻车原因比算法参数更致命。4. Matlab 复现流程从 CSI 到 BVP再到行为分类模型数据准备好了接下来是 Widar3.0 的核心流程把 CSI 变成 BVP用 BVP 训练行为分类网络。这一章讲清楚每个环节的入口脚本、关键参数和输出校验方式。4.1 复现前的准备目录结构、工具箱与运行入口Widar3.0 的代码包解压后核心目录大致分四块数据导入脚本、BVP 生成函数、DNN 定义文件和评估脚本。拿到代码包后我习惯先运行一次官方自带的 demo 数据确认环境没问题再替换成自己采的数据。运行前确认三个工具箱存在Signal Processing ToolboxSTFT 和滤波依赖、Phased Array System Toolbox多天线角度估计依赖、Deep Learning ToolboxDNN 训练依赖。在 Matlab 里进入项目主目录运行主入口脚本% 检查工具箱是否齐全 required_toolboxes {Signal Processing Toolbox, ... Phased Array System Toolbox, ... Deep Learning Toolbox}; v ver; installed {v.Name}; for i 1:length(required_toolboxes) if ~ismember(required_toolboxes{i}, installed) error(缺少工具箱: %s, required_toolboxes{i}); end end disp(工具箱检查通过);这段脚本做的是环境自检。缺工具箱时后面会报“未定义函数或变量”到时候再装会浪费一轮调试时间不如一开始就拦住。检查通过后先跑 demo 数据得到准确率基线记住这个数字——后续所有参数调整都拿它做对照。4.2 生成 BVP 的核心参数每个数字都值得调BVP 生成是 Widar3.0 的灵魂也是参数最多的环节。核心函数接收 CSI 三维矩阵输出的是一个“角度 × 速度 × 时间”的三维张量。关键参数集中在下面这张表参数典型值作用调参方向滑动窗口长度1 秒一次动作片段的时间跨度动作慢则加长动作快则缩短窗口步进0.05 秒相邻 BVP 帧的时间间隔越小越密但计算量线性增长多普勒频率范围-100Hz ~ 100Hz覆盖人体反射的速度区间以采样率 1000Hz 为准角度分辨率5 度空间散射体的角度量化天线数少时不宜小于 5 度幅度阈值0.02过滤弱反射点抑制噪声环境嘈杂时调大子载波选择全 256 个参与计算 OFDM 子载波数个别子载波长期受干扰时剔除以滑动窗口为例。窗口长度决定了 BVP 能“看到”多完整的动作。走路一步大约 0.5 秒挥手一次约 0.4 秒窗口设 1 秒能覆盖完整动作周期窗口太短会把动作拦腰截断导致同一动作产生多种完全不相关的 BVP 形态。窗口步进 0.05 秒意味着每秒输出 20 帧 BVP这个密度下动作的时序变化能够被保留下来。生成 BVP 时的核心循环示例function bvp_seq generate_bvp_sequence(csi, params) % csi: [num_packets, num_antennas, num_subcarriers] % params: 窗口、步进、频率范围等参数结构体 n_packets size(csi, 1); win_len round(params.window_len * params.fs); hop_len round(params.hop_len * params.fs); frame_starts 1:hop_len:(n_packets - win_len 1); num_frames length(frame_starts); % 预分配输出张量 bvp_seq zeros(params.angle_bins, params.vel_bins, num_frames); for f 1:num_frames idx frame_starts(f):(frame_starts(f) win_len - 1); frame_csi csi(idx, :, :); bvp_seq(:, :, f) compute_bvp_frame(frame_csi, params); end end这个循环的主逻辑是滑窗切帧逐步生成时间序列上的 BVP 帧。compute_bvp_frame是单帧计算函数内部包含相位差分、多普勒估计和角度估计三个步骤。预分配bvp_seq是个容易忽略的细节不预分配的话Matlab 会反复在循环里扩展数组采集 5 分钟数据生成的 BVP 序列能跑出 20 倍的内存碎片开销不少人的机器就是在这一步卡死的。4.3 训练行为分类网络网络结构与训练选项BVP 生成后进入 DNN 训练阶段。Widar3.0 采用的网络以 CNN 为主输入是若干帧 BVP 拼接成的时空块输出是行为类别。我自己复现时用过一个更简单的网络结构效果接近% 定义 CNN 分类网络 input_size [params.angle_bins, params.vel_bins, params.time_bins]; layers [ imageInputLayer(input_size, Name, input) convolution2dLayer(3, 32, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling2dLayer(2, Stride, 2, Name, pool1) convolution2dLayer(3, 64, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling2dLayer(2, Stride, 2, Name, pool2) convolution2dLayer(3, 128, Padding, same, Name, conv3) batchNormalizationLayer(Name, bn3) reluLayer(Name, relu3) fullyConnectedLayer(num_classes, Name, fc) softmaxLayer(Name, softmax) classificationLayer(Name, output) ]; % 训练选项 options trainingOptions(adam, ... MaxEpochs, 60, ... MiniBatchSize, 64, ... InitialLearnRate, 1e-3, ... ValidationData, {val_bvp, val_labels}, ... ValidationFrequency, 10, ... Plots, training-progress, ... ExecutionEnvironment, auto);这个网络的核心思路是先用三层卷积逐步提取 BVP 的空间模式再用全连接层映射到行为类别。imageInputLayer的第三个维度time_bins是时间深度——它让网络能同时看到“当前动作形态”和“动作前后的变化趋势”这一点对区分走路和原地摆手至关重要两者在单帧 BVP 上可能相似但在时间序列上差异明显。训练选项里值得注意的有两个。InitialLearnRate设 1e-3BVP 特征不是自然图像那种大范围数值学习率太高会导致 loss 在早期就发散。ValidationData必须设不然训练过程就成了黑匣子只能靠盲试调参。4.4 验证识别效果用混淆矩阵判断哪些动作容易混训练完成后用测试集跑一次分类重点看混淆矩阵% 在测试集上预测 pred_labels classify(net, test_bvp); test_labels categorical(test_labels_raw); % 绘制混淆矩阵 figure; cm confusionchart(test_labels, pred_labels); cm.Title Widar3.0 行为识别混淆矩阵; cm.RowSummary row-normalized; cm.ColumnSummary column-normalized;混淆矩阵能直观暴露哪些动作在信号层面本身就接近。我在实测中最常见的混淆对是“坐下”和“弯腰”——这两种动作在前半程的 BVP 形态很相似都是躯干下移、速度方向向下。如果你在矩阵里看到某个混淆对占比超过 20%不用急着加网络层数先回去看这个动作的 BVP 热图是不是真的能区分。有些动作需要更长的窗口才能捕捉完整轨迹这是物理层面的问题不是分类器的问题。5. 复现中的五个高频踩坑现象、原因与排查办法CSI 行为识别项目有个特点算法原理在论文里说得通一上自采数据就现原形。基于我在 Widar3.0 上反复折腾的经验下面五类问题最值得写进排错手册。5.1 换环境就翻车官方 Demo 能复现现场识别却一团糟现象用官方 demo 数据训练测试准确率有 90% 以上换成自己房间采集的数据同一个模型直接跌到 40%几乎等于随机猜。原因Widar3.0 的跨环境能力建立在 BVP 的物理不变性上但 BVP 生成依赖天线间的相对位置和房间反射布局。你房间里路由器放在角落和官方实验环境里路由器正对采集区同一动作产生的 BVP 分布差异极大。网络学到的是“那个房间的 BVP 分布”不是普遍意义的人体速度分布。解决在目标环境中重新采集数据并重新训练而不是直接套用官方预训练模型。采集时尽量模拟真实使用场景路由器放在平时放的位置人在它前方活动。如果必须跨环境部署考虑用 Widar3.0 论文里的域自适应思路用少量目标环境数据微调网络最后两层而不是从头训。5.2 自采数据训练准确率低多半是数据划分方式错了现象训练过程 loss 下降正常验证集准确率到 85% 以上但测试集另一批人准确率骤降。原因数据划分时没有按“人”隔离。有些人把一个人的数据随机切成训练集和测试集这样模型其实记住了“这个人”的个体特征而不是“这个动作”的共性。CSI 数据里包含每个人的身高、体型、走路习惯信息这些特征在 BVP 上都会留下印子。解决划分数据时按人员 ID 分组保证同一个人不会同时出现在训练集和验证集。具体做法是把每个人的数据块当作一个整体用cvpartition按组划分% 按人员分组划分训练/验证/测试集 person_ids [1 1 1 1 2 2 2 2 3 3 3 3]; % 每个样本对应的采集人 ID cv cvpartition(person_ids, HoldOut, 0.2); train_idx training(cv); test_idx test(cv);如果项目需要评估跨人泛化性能这一步是必选项。它会在早期就暴露“这个模型只认识某个人”的问题而不是拖到最后部署时才爆发。5.3 BVP 矩阵大量出现 NaN相位差分在弱信号分集上除零了现象compute_bvp_frame跑完后输出矩阵里很大比例是 NaN 或 Inf训练直接报错。原因BVP 计算中涉及相位差分本质是angle(csi(n) * conj(csi(n-1)))。当某根天线某个子载波的幅度接近 0信号深衰落多径抵消差分结果在归一化时出现除零。这不是算法 bug而是 CSI 的物理特性——室内多径环境下深衰落不可避免。解决在相位差分前加幅度掩码对幅度低于阈值的点直接置零% 相位差分前保护过滤弱信号点 amp abs(csi_frame); mask amp params.amp_threshold; % 幅度阈值默认 0.02 phase_diff angle(csi_frame(:, 2:end) .* conj(csi_frame(:, 1:end-1))); phase_diff(~mask(:, 2:end)) 0; % 弱信号点的差分置零这样处理后弱信号点不参与后续的多普勒累积NaN 基本消失。同时把amp_threshold当作可调参数环境信噪比差时调大到 0.05过滤更多噪声点。5.4 Matlab 内存在 BVP 生成阶段爆掉滑动窗口太密现象生成 BVP 序列的循环跑到一半Matlab 提示“内存不足”或者系统开始疯狂使用交换分区整个环境卡死。原因BVP 序列的时间维度是帧数而每帧是一张“角度×速度”的二维图。窗口步进 0.05 秒意味着 10 分钟数据产生约 12000 帧每帧假设 60×60 的 float 矩阵总内存约 1.7GB。如果同时保存多根天线的中间结果内存很容易到 8GB 以上。解决两步配合。第一步进调大到 0.1 秒帧数减半时间信息仍足够——网络输入的时间深度本来就只有几十帧第二边生成边落盘不把所有 BVP 帧长期放在内存里% 分块生成 BVP 并写入磁盘 bvp_file matfile(bvp_sequence.mat, Writable, true); bvp_file.bvp(1, 1, 1:num_frames) 0; % 预分配 for f 1:num_frames bvp_file.bvp(:, :, f) compute_bvp_frame(...); end用matfile对象写入磁盘Matlab 不会把整个数组加载到内存物理内存占用大幅下降。这个手段看着笨但能救回一台 8GB 内存的旧机器。5.5 BVP 归一化放错了位置训练曲线上下乱蹦现象训练 loss 曲线不下降或下降后很快反弹验证准确率震荡剧烈。原因BVP 的能量值在不同动作、不同人体之间存在数量级差异。走路时全身散射体速度分布广能量分布在一个大范围静止时能量集中在零速附近数值小一个量级。如果不做归一化就进网络大数值样本主导梯度更新小数值样本被抑制。解决在训练数据的预处理管线里对每个 BVP 帧做最大最小值归一化而不是在整个数据集上做全局归一化。因为动作的能量绝对值本身就随距离、人体大小变化网络应该学习 BVP 的“形状”而不是“亮度”。同时一定要把归一化参数保存下来推理时用同一组参数处理新数据% 按帧归一化 BVP for f 1:size(bvp_seq, 3) frame bvp_seq(:, :, f); bvp_seq(:, :, f) (frame - min(frame(:))) / (max(frame(:)) - min(frame(:)) eps); end这里的eps防止整帧全零时除零是常规保护。6. 从简单复现到能落地数据增强、跨域验证与模型轻量化复现只是起点能被别人用起来才是终点。最后这章说三个值得投入的进阶方向。6.1 合成 BVP 做数据增强没有 20 个人也能训练出能用的模型行为识别模型的训练往往需要几十个采集者普通人凑不齐。一个可行途径是用合成 BVP 做增强从人体运动学模型生成模拟的速度分布。核心想法是走路、弯腰这类动作有基本的速度轨迹把这些轨迹转成 BVP 形态注入训练集。合成样本不能替代真实数据但可以让模型对“同一动作的不同速度变化”更鲁棒。6.2 跨房间、跨人验证只看训练准确率没有意义评估时把验证做到极致A 房间采集训练B 房间采集测试或者 A 组人训练B 组人测试。这种评估在初期会很难看但它告诉你的才是真实场景下的能力边界。我自己的经验是跨人到 70% 以上、跨房间到 60% 以上才值得考虑部署。低于这个数字问题大概率不在网络结构而在数据量或动作定义。6.3 把模型轻量化从 3D CNN 到 2D CNN LSTM落地到嵌入式设备时整段 BVP 序列叠加 3D CNN 的算力往往扛不住。实际部署中我用过 2D CNN LSTM 的替代方案CNN 负责逐帧提取速度分布特征LSTM 负责建模时间依赖参数量只有原来的三分之一在树莓派级别的硬件上能实时跑。代价是准确率会掉 3 到 5 个点但在功耗约束下这是值得的交易。最后一个习惯每次调参都在代码里留一行注释记录当时的场景、参数值和结果。CSI 数据对环境太敏感一周后你可能完全不记得某组参数是在什么条件下调出来的。有记录才能复现能复现才是真落地。希望帮到你。本文还有配套的精品资源点击获取