1. 从标题说起:不用摄像头和传感器,WiFi 凭什么能“看见”你
第一次看到“通过 WiFi 实现监控你在家的一举一动,不用摄像头、不用传感器、更不需要什么特殊硬件”这个说法,我的第一反应是:这标题党也太敢写了。但仔细琢磨之后,我发现它背后其实站着一个非常扎实的技术方向——WiFi 感知(WiFi Sensing),也有人叫它无线感知或信道状态信息感知。它的核心逻辑并不玄乎:WiFi 信号在室内传播时,会因为你走动、坐下、挥手甚至呼吸,产生反射、折射、散射的变化。这些变化被网卡记录下来,经过算法处理,就能反推出你在干什么。
换句话说,WiFi 信号本身就是一种“看不见的雷达”。它不需要额外的摄像头,不需要穿戴设备,也不需要红外传感器。你家里那台路由器、你手机里的 WiFi 芯片、你笔记本的无线网卡,就已经是现成的“感知硬件”了。这件事之所以最近又被翻出来讨论,是因为**信道状态信息(CSI, Channel State Information)**的获取门槛在降低,一些开源工具和消费级网卡已经能比较方便地拿到细粒度的物理层数据,让个人开发者在家里做原型验证成为可能。
这篇文章适合谁看?如果你是做物联网、智能家居、边缘计算方向的开发者,或者你对“用无线信号做人体感知”这件事有好奇,想自己动手跑一个最小可用原型,那这篇内容就是写给你的。我会从整体设计思路讲起,拆到 CSI 数据怎么拿、怎么清洗、怎么提取特征、怎么做一个能区分“有人走动”和“没人”的分类器,最后再聊几个我实际踩过的坑。全程不涉及任何特殊硬件,用的都是市面上能买到的普通网卡和一台跑 Linux 的机器。
注意:本文讨论的是基于无线信号物理层信息的感知技术,所有实验均在合法合规的本地环境中进行,不涉及任何网络入侵或隐私侵犯行为。实际部署时请务必遵守当地法律法规,尊重他人隐私。
2. 整体设计思路:为什么选 CSI 而不是 RSSI
2.1 核心需求拆解:从“有没有人”到“人在做什么”
这个项目的目标可以拆成三个层次。第一层是存在检测:房间里有没有人。第二层是动作识别:人在走动、坐下、挥手还是摔倒。第三层是身份与状态推断:是谁、呼吸频率多少、睡姿如何。标题里说的“一举一动”听起来很夸张,但落到工程上,绝大多数可复现的方案都集中在第一层和第二层。第三层不是不能做,而是对数据采集的稳定性和标注量要求高得多,个人开发者很难在短时间内做出可靠结果。
所以我的整体设计思路是:先做存在检测,再做动作分类,最后尝试呼吸监测。这三个任务共用同一套 CSI 采集链路,区别只在于特征提取和模型选择。这样做的好处是,你不需要一开始就追求“全知全能”,而是可以逐层验证,每一步都有可观测的输出。
2.2 为什么不用 RSSI:粒度太粗,信息量不够
很多人第一次想到的是用 RSSI(Received Signal Strength Indicator,接收信号强度)来做感知。RSSI 确实容易拿,几乎所有网卡都支持,但它的问题在于粒度太粗。RSSI 是整条链路上所有子载波信号强度的聚合值,一个人走动带来的 RSSI 波动,可能和空调风吹动窗帘带来的波动差不多。你很难从一维的强度序列里区分出“人”和“环境噪声”。
CSI 则完全不同。在 OFDM(正交频分复用)系统里,WiFi 信号被分成几十甚至上百个子载波。CSI 记录的是每个子载波上的幅度和相位。当人体在空间中移动时,不同子载波受到的多径影响不同,有的增强、有的减弱,形成一组高维的、随时间和频率变化的矩阵。这就像从“一个温度计”升级到“一张热力图”,信息量差了几个数量级。
| 对比维度 | RSSI | CSI |
|---|---|---|
| 数据维度 | 单值(每包一个强度) | 复数矩阵(子载波 × 天线对) |
| 时间分辨率 | 低,通常秒级 | 高,可达毫秒级 |
| 对微小动作敏感度 | 差 | 好,能捕捉呼吸级起伏 |
| 获取难度 | 极低 | 中等,需要特定网卡和工具 |
| 适用任务 | 粗略存在检测 | 动作识别、呼吸监测、定位 |
2.3 方案选型:消费级网卡 + 开源工具链
硬件方面,我选择的是支持 CSI 提取的消费级 WiFi 网卡。市面上有几款常见的芯片方案被开源社区广泛支持,比如某些基于 Intel 5300、Atheros 9k 系列的网卡。这些网卡价格不贵,二手市场很容易买到。一台普通的路由器作为信号发射端,一台装有 Linux 的笔记本作为接收端,就构成了最基本的采集环境。
软件方面,Linux 下有几个成熟的开源工具可以拿到 CSI 数据。它们的工作原理是修改网卡驱动,在收到每个数据包时把物理层信息导出到用户空间。你不需要自己写驱动,只需要编译安装对应的工具,然后用脚本读取输出即可。采集到的数据通常是文本格式,每行包含时间戳、子载波索引、幅度、相位等信息。
提示:不同网卡型号和驱动版本对 CSI 的支持程度差异很大。建议先确认你手头的网卡是否在开源工具的支持列表里,再决定是否继续。如果网卡不支持,换一块支持的网卡比折腾驱动要省事得多。
3. 核心细节解析:CSI 数据里到底藏着什么
3.1 幅度与相位:两个维度的信息各有用途
CSI 数据最核心的两个量是幅度和相位。幅度反映的是信号经过多径传播后的衰减和增强情况。当你走动时,身体会遮挡某些路径、反射某些路径,导致不同子载波的幅度出现有规律的起伏。幅度信息比较稳定,受硬件相位噪声影响小,所以大多数入门方案都先从幅度入手。
相位则更敏感,但也更“脏”。相位里包含了载波频偏(CFO)、**采样频偏(SFO)**以及硬件本身的相位噪声。如果不做校准,原始相位几乎没法直接用。但相位的好处是对微小位移极其敏感,呼吸时胸腔的起伏在相位上会有清晰体现。所以做呼吸监测时,相位是绕不开的。
我的建议是:存在检测和动作分类先用幅度,呼吸监测再处理相位。这样你可以先把链路跑通,再逐步增加难度。
3.2 子载波选择:不是越多越好
一条 WiFi 链路通常有 30 个或 56 个子载波(取决于带宽和协议)。理论上子载波越多,信息越丰富。但实际中,边缘子载波的信噪比往往很差,而且有些子载波是保护频带,不携带有效信息。如果你把所有子载波都塞进模型,不仅计算量大,还会引入大量噪声。
我通常的做法是:先画出所有子载波的幅度随时间变化的曲线,肉眼观察哪些子载波对动作响应明显、哪些基本是平的。然后选择响应明显且彼此相关性较低的一组子载波。一般选 10 到 20 个就足够了。这一步没有固定公式,更多是靠观察和经验。
3.3 时间窗口:多长才合适
CSI 采集是连续的,但模型需要固定长度的输入。时间窗口的选择直接影响识别效果。窗口太短,动作还没完成,特征不完整;窗口太长,会混入无关动作,而且延迟增加。
对于存在检测,窗口可以长一些,比如 2 到 5 秒,因为“有人”这个状态是持续的。对于动作识别,窗口通常取 0.5 到 2 秒,具体取决于动作的持续时间。比如“挥手”可能 0.5 秒就结束了,“从走到坐”可能需要 2 秒。你可以先录一批数据,统计每个动作的实际时长,再取一个覆盖大多数动作的窗口长度。
注意:窗口之间可以有重叠,这叫滑动窗口。重叠比例通常取 50%,既能增加样本量,又能保证不会漏掉窗口边界处的动作。
4. 实操过程:从零搭建一个 WiFi 感知原型
4.1 环境准备与网卡配置
先说一下我的实验环境。发射端是一台普通家用路由器,工作在 5GHz 频段,带宽 20MHz 或 40MHz。接收端是一台装了 Ubuntu 的笔记本,网卡是某款支持 CSI 提取的型号。两台设备之间没有遮挡,距离大约 3 到 5 米。
第一步是确认网卡驱动和固件版本。不同版本的驱动对 CSI 输出的格式有影响,建议按照开源工具的文档,把驱动和固件固定在一个已知可用的版本上。然后关闭网卡的各种节能和自动调整功能,因为这些功能会改变物理层参数,导致 CSI 数据出现莫名其妙的跳变。
# 关闭网卡的节能模式(示例,具体接口名需替换) sudo iw dev wlan0 set power_save off # 固定信道和带宽(示例) sudo iw dev wlan0 set channel 36 HT20第二步是配置发射端。路由器不需要特殊设置,但建议关闭波束成形和 MU-MIMO,因为这些技术会动态调整天线权重,让 CSI 变得不稳定。固定信道、固定带宽、固定发射功率,能让采集到的数据更可复现。
4.2 CSI 数据采集与格式解析
工具跑起来之后,你会看到类似这样的输出:
timestamp, subcarrier_index, amplitude, phase 0.001, 0, 12.3, -0.45 0.001, 1, 11.8, 0.32 ...每一行对应一个子载波在某个时刻的测量值。实际数据里还会有天线索引、RSSI 等字段。你需要写一个解析脚本,把原始文本转换成三维数组:时间 × 子载波 × 天线。然后按时间窗口切片,得到一个个样本。
采集时要注意标注。你需要在采集的同时记录“当前在做什么”。我的做法是:一个人负责做动作,另一个人在旁边用手机秒表记录每个动作的起止时间,事后根据时间戳对齐。更省事的办法是写一个简单的标注脚本,按快捷键打标签,比如按 1 表示“走动”,按 2 表示“坐下”,按 3 表示“无人”。
import numpy as np import pandas as pd # 读取原始 CSI 数据 raw = pd.read_csv('csi_raw.csv') # 按时间戳和子载波索引重塑为矩阵 csi_matrix = raw.pivot_table( index='timestamp', columns='subcarrier_index', values='amplitude' ).values # 滑动窗口切片 def sliding_window(data, window_size, step): samples = [] for start in range(0, len(data) - window_size, step): samples.append(data[start:start + window_size]) return np.array(samples) windows = sliding_window(csi_matrix, window_size=100, step=50) print(windows.shape) # (样本数, 时间步, 子载波数)4.3 特征提取:从原始波形到可分类的特征
原始 CSI 窗口不能直接丢给分类器,因为维度太高、噪声太大。你需要提取有物理意义的特征。我常用的特征分三类:
第一类是统计特征:均值、方差、偏度、峰度、最大值、最小值。这些特征计算简单,对动作引起的幅度变化很敏感。比如“无人”时方差很小,“走动”时方差明显增大。
第二类是频域特征:对每个子载波的时间序列做 FFT,取低频段的能量。人体动作的频率通常在 0.5 到 5 Hz 之间,呼吸在 0.2 到 0.5 Hz 之间。通过带通滤波和频谱分析,可以把动作和呼吸分开。
第三类是相关性特征:不同子载波之间的相关系数。当有人走动时,多径结构改变,子载波之间的相关性会发生变化。这个特征对区分“有人但静止”和“无人”特别有用。
from scipy.stats import skew, kurtosis from scipy.fft import fft def extract_features(window): features = [] # 统计特征 features.extend([ np.mean(window), np.std(window), skew(window.flatten()), kurtosis(window.flatten()) ]) # 频域特征 spectrum = np.abs(fft(window, axis=0)) low_freq_energy = np.sum(spectrum[1:10], axis=0) features.extend(low_freq_energy[:5]) # 相关性特征 corr = np.corrcoef(window.T) features.append(np.mean(corr[np.triu_indices_from(corr, k=1)])) return np.array(features)4.4 模型训练与评估:从逻辑回归到轻量级神经网络
特征提取完之后,分类器的选择就比较自由了。我的建议是先从逻辑回归或随机森林开始,因为这两个模型训练快、可解释性强,能帮你快速判断特征是否有效。如果随机森林的准确率能到 80% 以上,说明特征提取方向是对的,再考虑上更复杂的模型。
数据划分要特别注意:不能随机划分,因为相邻窗口之间有重叠,随机划分会导致训练集和测试集信息泄漏。正确做法是按时间段划分:前 70% 的时间段做训练,后 30% 做测试。这样测试集里的动作和训练集里的动作在时间上完全不重叠,评估结果更真实。
| 模型 | 训练速度 | 可解释性 | 适合阶段 |
|---|---|---|---|
| 逻辑回归 | 极快 | 强 | 特征验证 |
| 随机森林 | 快 | 中等 | 基线模型 |
| SVM | 中等 | 弱 | 小样本 |
| 一维 CNN | 慢 | 弱 | 数据量大时 |
如果要做实时推理,模型不能太重。我试过在树莓派上跑随机森林,单次推理在毫秒级,完全跟得上 CSI 的包速率。一维 CNN 虽然准确率可能高几个点,但在边缘设备上跑起来比较吃力,需要做量化或剪枝。
5. 常见问题与排查技巧实录
5.1 数据波动大、无法复现怎么办
这是最常见的问题。同样的动作,今天采的数据和明天采的数据分布不一样。原因通常有三个:环境变化(家具挪动、门窗开关)、设备状态变化(网卡温度漂移、路由器自动调整)、人员位置变化(发射端和接收端的位置动了)。
解决办法是:每次采集前重新校准基线。具体做法是:先采集 30 秒“无人”状态的数据,计算每个子载波的均值和方差,作为归一化的参考。后续所有数据都减去这个均值、除以这个标准差。这样可以在一定程度上消除环境漂移的影响。
5.2 相位数据完全不可用怎么办
原始相位确实没法直接用,但可以通过线性变换做校准。常用的方法是:对每个数据包的所有子载波相位做线性拟合,减去拟合出的斜率和截距。这个操作的物理意义是消除载波频偏和采样频偏带来的线性相位旋转。校准之后,相位的变化就主要来自人体运动了。
def calibrate_phase(phase): # phase: 形状为 (子载波数,) 的原始相位 indices = np.arange(len(phase)) slope, intercept = np.polyfit(indices, phase, 1) calibrated = phase - (slope * indices + intercept) return calibrated注意:相位校准只能在单个数据包内做。跨数据包的相位关系需要更复杂的同步机制,入门阶段可以先不处理。
5.3 如何区分“人静止”和“无人”
这是存在检测里最难的场景。人坐在那里不动,CSI 幅度可能和无人时差不多。这时候相位信息和呼吸引起的微动就派上用场了。即使人静止,呼吸也会导致胸腔起伏,在相位上产生 0.2 到 0.5 Hz 的周期性变化。你可以对相位序列做带通滤波,如果在这个频段有明显的能量峰值,就说明有人。
另一个技巧是利用多天线。如果接收端有多根天线,不同天线接收到的信号经历了不同的多径。人静止时,不同天线之间的相位差仍然会因为呼吸而微小变化。这个差分信号比单天线信号更干净。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 数据全是零或缺失 | 网卡不支持 CSI / 驱动未加载 | 检查工具输出和驱动日志 |
| 幅度跳变剧烈 | 网卡节能模式未关 / 信道切换 | 关闭节能,固定信道 |
| 动作识别准确率低 | 窗口长度不合适 / 特征不足 | 调整窗口,增加频域特征 |
| 相位杂乱无章 | 未做相位校准 | 应用线性校准 |
| 实时推理延迟高 | 模型太重 / 采集频率过高 | 换轻量模型,降采样 |
| 不同天数据不可比 | 环境漂移 | 每次采集前重新校准基线 |
5.5 几个我踩过的坑
第一个坑是网卡过热。连续采集半小时后,网卡温度升高,CSI 的幅度基线会缓慢漂移。解决办法是加散热片,或者每采集 10 分钟休息 2 分钟。
第二个坑是路由器自动信道选择。有些路由器会在检测到干扰时自动换信道,换信道瞬间 CSI 会完全断掉。一定要在路由器设置里锁定信道。
第三个坑是标注不同步。手动按秒表标注的误差可能达到几百毫秒,对于短动作来说这是致命的。后来我改用键盘快捷键标注,采集脚本同时记录按键时间戳,误差降到了几十毫秒以内。
第四个坑是过度依赖幅度。一开始我只用幅度做呼吸监测,效果很差。后来加入校准后的相位,呼吸检测的准确率明显提升。所以不要怕麻烦,相位该处理还是要处理。
6. 进阶方向:从动作识别到呼吸与身份推断
6.1 呼吸监测:把相位用起来
呼吸监测的核心是从相位序列中提取 0.2 到 0.5 Hz 的周期性成分。具体步骤是:先对相位做校准,然后带通滤波,再用峰值检测或 FFT 找主频。主频乘以 60 就是每分钟呼吸次数。我实测下来,在安静环境下,呼吸频率的误差可以控制在每分钟 1 到 2 次。
难点在于身体大动作的干扰。如果人在呼吸的同时还在看手机、翻身,相位里会混入低频运动成分。这时候需要先做动作检测,只在“静止”片段上做呼吸分析。
6.2 身份识别:步态作为生物特征
每个人走路的姿态、步幅、频率都有细微差异,这些差异会在 CSI 中留下痕迹。理论上可以用步态做身份识别。但实际做起来,数据量和标注成本是最大的瓶颈。你需要同一个人在不同时间、不同状态下走很多次,还要采集多个人做对比。个人开发者很难凑齐足够的数据。我的建议是把这个方向当作长期目标,先把手势识别和呼吸监测做扎实。
6.3 多房间与穿墙场景
WiFi 信号可以穿墙,这意味着理论上一个发射端可以覆盖多个房间。但穿墙之后信号衰减严重,CSI 的信噪比会大幅下降。而且多房间场景下,不同房间的动作会混在一起,很难分离。目前比较可行的方案是每个房间放一个接收端,做空间分集,而不是指望一个接收端搞定全屋。
7. 隐私与合规:技术之外的必修课
这个项目最敏感的地方在于“监控”两个字。技术本身是中性的,但用在什么地方、怎么用,决定了它是便利还是侵犯。我在做这个原型的时候,给自己定了三条规矩:只在自己家里做、不采集任何可识别个人身份的信息、数据本地处理不上传。
如果你要把这个技术用到实际产品里,比如独居老人跌倒检测,那必须在知情同意的前提下进行,并且要提供明确的关闭方式。WiFi 感知的一个“优势”是隐蔽性强,但隐蔽性用在监控上就是灾难。技术开发者应该主动把透明度做进设计里,而不是等出了问题再补救。
提示:任何涉及人体感知的项目,在部署前都应该做隐私影响评估。即使是在自己家里,如果有多人居住,也应该告知并征得同意。
8. 写在最后:一些个人体会
这个项目我从头到尾跑了一遍,最大的感受是:WiFi 感知的门槛在“数据”上,不在“算法”上。网上有很多论文讲各种复杂的神经网络,但你真正动手之后会发现,最难的是采到干净、可复现、标注准确的数据。算法反而不是瓶颈,随机森林加好的特征,效果往往不比深度学习差多少。
另一个体会是不要追求一步到位。我一开始就想做“全屋动作识别”,结果数据采了一堆,标注乱七八糟,模型怎么调都不对。后来退回到“有人/无人”二分类,把链路跑通,再逐步增加动作类别,反而顺利得多。做原型就是这样,先让系统跑起来,再让它跑得准。
最后分享一个小技巧:采集数据时放一个手机在旁邊录像。事后如果发现某段数据有问题,可以回看录像确认当时到底发生了什么。这个习惯帮我省了很多排查时间。视频不用保留太久,确认完数据就可以删掉,既方便又不涉及长期存储问题。
这个方向后续还可以往多模态融合走,比如把 WiFi CSI 和麦克风阵列的声学信号结合,做更鲁棒的动作识别。也可以往低功耗边缘部署走,把模型量化后跑在嵌入式设备上,做成一个不用联网的本地感知模块。这些我都还在摸索,有新的进展再聊。