编程 WiFi 穿墙姿态估计爆火解剖:wifi-densepose 如何用 8 美元 ESP32 与 Rust 流水线把 CSI 信号变成人体 DensePose

2026-07-26 02:43:50 +0800 CST views 6

前言:当 WiFi 路由器变成"透视眼"

2026 年 7 月,GitHub Trending 上出现了一个罕见的场面:一个叫 ruvnet/wifi-densepose 的 Rust 项目三天连冠,单日新增 star 突破 5000,总量迅速冲过 2 万。它的一句话简介足够震撼——用普通的商用 Mesh 路由器,实现穿墙的实时全身人体追踪

没有摄像头,没有激光雷达,没有毫米波模组。只有你家里那台天天在发 beacon 帧的 WiFi 路由器。

第一反应通常是两种:一种是"这不可能,营销噱头";另一种是"这也太可怕了"。而作为工程师,我的第一反应是第三种:它到底是怎么做到的?

这篇文章我会把 wifi-densepose(学术原型叫 InvisPose,源头是 CMU 那篇著名的 WiFi-DensePose 论文思路)整条技术链路拆开:从 WiFi 信道状态信息(CSI)的物理本质,到相位清洗(Phase Sanitization)的数学处理,到模态翻译网络(Modality Translation Network)如何把一维无线信号"翻译"成计算机视觉特征,再到 DensePose 的 UV 坐标回归,最后落到 ESP32 采集节点和 Rust 推理管线的工程实现。文末还会聊聊绕不开的话题:隐私。

全文较长,建议先收藏。

一、物理基础:CSI 为什么携带人体姿态信息

1.1 从 RSSI 到 CSI:粒度差了三个数量级

大多数人对 WiFi 信号质量的认知停留在"信号有几格",那个东西叫 RSSI(接收信号强度指示),是一个标量——整个信道的能量总和。用 RSSI 做人体感知,大概只能判断"有没有人走过",粒度太粗。

CSI(Channel State Information,信道状态信息)完全是另一个量级的东西。802.11n/ac 使用 OFDM 调制,把 20/40/80MHz 的信道切分成几十到上百个子载波(subcarrier),CSI 记录的是每一个子载波上信号的幅度和相位

数学上,接收信号和发射信号的关系是:

Y(f, t) = H(f, t) × X(f, t) + N(f, t)

其中 H(f, t) 就是 CSI——一个随频率 f 和时间 t 变化的复数矩阵。对于 3 根发射天线 × 3 根接收天线的 MIMO 配置、56 个子载波的场景,每一次测量得到的是一个 3 × 3 × 56 的复数张量。每秒采样 100~1000 次,你就得到了一条信息量极其丰富的时间序列。

1.2 人体是一面"会动的反射镜"

WiFi 信号在室内传播不是走直线的,它会经历多径效应:直射、墙面反射、家具散射……当一个人出现在传播路径中,情况发生质变:

  • 人体含有大量水分,对 2.4GHz/5GHz 电磁波有显著的吸收和反射
  • 人体的位置改变多径的路径长度,直接体现在相位变化上
  • 肢体的运动产生多普勒频移,体现在相位的时间导数上
  • 身体的轮廓和姿态改变散射的空间分布,体现在不同天线对、不同子载波之间的幅度差异上

一句话总结:人体的每一个姿态,都会在 CSI 的幅度/相位空间中留下一组独特的"指纹"。这就是 WiFi 感知的物理基础——问题只剩下:怎么把这个指纹解码回姿态。

这件事的难度在于,CSI 到姿态的映射是高度非线性、高度环境相关的。传统信号处理方法(FFT + 手工特征)只能做到粗粒度的活动识别(走路/坐下/跌倒),要还原每个像素级别的密集姿态,必须上深度学习。

二、信号预处理:Phase Sanitization 是整个系统的地基

拿到原始 CSI 后不能直接喂给神经网络,因为原始相位基本是垃圾。商用 WiFi 网卡的相位测量包含多种系统性误差:

  • CFO(载波频偏):收发双方晶振不同步,引入随时间线性增长的相位漂移
  • SFO(采样频偏):ADC 采样时钟偏差,引入随子载波索引线性变化的相位斜坡
  • PDD(包检测延迟):每个包的检测时刻抖动,引入随机相位偏置

如果不清洗,同一个姿态在两次测量中的相位可能完全对不上,网络学到的全是噪声。

wifi-densepose 采用的清洗方案是经典的线性拟合去斜坡:假设误差项相对子载波索引 k 是线性的,那么对每次测量的相位序列做一次线性回归,减去拟合出来的直线,剩下的就是(近似)真实的相位响应:

import numpy as np

def sanitize_phase(raw_phase: np.ndarray) -> np.ndarray:
    """
    raw_phase: shape (n_subcarriers,) 某一天线对的原始相位
    返回去除线性斜坡和常数偏置后的净化相位
    """
    # 1. 相位解缠绕:消除 ±π 跳变
    unwrapped = np.unwrap(raw_phase)

    n = len(unwrapped)
    k = np.arange(n)

    # 2. 估计线性斜率 a 和偏置 b
    a = (unwrapped[-1] - unwrapped[0]) / (n - 1)
    b = unwrapped.mean()

    # 3. 减去线性项,保留真实信道响应
    return unwrapped - a * k - b

幅度部分相对干净,但也要做两步处理:

  1. Hampel 滤波去除脉冲噪声(邻居设备突发流量造成的尖刺)
  2. 滑动窗口标准化,消除发射功率自动调整(TPC)带来的整体幅度漂移

工程上一个容易被忽略的细节:CSI 采样是不均匀的。WiFi 包的到达时间取决于信道竞争,不是严格等间隔。wifi-densepose 在预处理层做了线性插值重采样,把不规则时间序列对齐到固定 100Hz 网格上,这一步对后续时序建模至关重要——很多复现失败的人都是栽在这里。

三、核心架构:Modality Translation Network——无线信号到视觉特征的"翻译官"

这是整个系统最精彩的部分。问题的本质是一个模态翻译任务:输入是 3×3×56×T 的无线信号张量,输出要接入一个为图像设计的 DensePose 头。两者之间隔着一条巨大的模态鸿沟。

3.1 为什么不端到端从零训练?

直接训一个 CSI → 姿态的网络理论上可行,但有两个致命问题:

  1. 数据太贵。图像姿态数据集(COCO DensePose)有几十万标注,而 CSI-姿态配对数据只能自己采,量级差了三个数量级
  2. CSI 没有空间结构先验。CNN 之所以在图像上强大,是因为卷积假设了空间局部性,而 CSI 张量的"邻近子载波"之间并没有图像那种平移不变性

InvisPose 的解法非常聪明:不重造视觉轮子,而是把 CSI 翻译成"伪图像特征",然后复用整个成熟的 DensePose-RCNN 管线。这本质上是一种跨模态迁移学习。

3.2 翻译网络的三段式结构

CSI 张量 (3×3×56×T)
    │
    ▼
┌─────────────────┐
│ ① 幅度/相位编码器  │  两条独立分支,各自 1D卷积 + 时序建模
└─────────────────┘
    │ 融合 (concat + MLP)
    ▼
┌─────────────────┐
│ ② 空间上采样器    │  转置卷积,把特征"画"成 2D 特征图 (720×1280 对应的下采样网格)
└─────────────────┘
    │
    ▼
┌─────────────────┐
│ ③ 域对齐层       │  让输出特征分布逼近 ResNet-FPN 的中层特征分布
└─────────────────┘
    │
    ▼
DensePose-RCNN 头 (直接复用预训练权重)

关键设计决策有三个:

幅度与相位分开编码。两者的物理含义不同(幅度对应能量衰减,相位对应路径长度),统计分布也完全不同,共享编码器会互相污染。实验表明分支编码比联合编码 AP 高约 4 个点。

转置卷积做"信号成像"。第②段本质上是在做一件反直觉的事:把没有空间结构的特征向量上采样成一张 2D 特征图。这张"图"并不是人眼可看的图像,而是与视觉 backbone 中层特征同构的张量。网络在训练中自己学会了"CSI 的哪些模式对应特征图的哪些空间位置"。

教师-学生蒸馏做域对齐。训练时,同步采集的 RGB 视频经过预训练 ResNet-FPN 产生"教师特征",翻译网络的输出作为"学生特征",用 MSE 损失强迫两者对齐。这样 DensePose 头可以冻结不动,只训翻译网络——训练数据需求直接降了一个数量级。

3.3 DensePose:不止是骨架,是每一寸皮肤

很多人以为姿态估计就是 17 个关键点的火柴人。DensePose 的野心大得多:它把人体表面划分为 24 个区域(part),对每个前景像素回归其在对应区域上的 UV 坐标——也就是说,输出是"这个像素属于左前臂,位于左前臂展开面的 (0.3, 0.7) 处"。

这意味着系统输出的是一个连续的人体表面映射,可以直接驱动 3D 人体模型(SMPL),这也是为什么演示视频里能看到完整的"人形轮廓"而不是几根线段。

推理侧的简化调用大致长这样:

import torch

class WiFiDensePose(torch.nn.Module):
    def __init__(self, translator, densepose_head):
        super().__init__()
        self.translator = translator        # 模态翻译网络(可训练)
        self.head = densepose_head          # DensePose-RCNN 头(冻结)

    @torch.inference_mode()
    def forward(self, csi_window):
        # csi_window: (B, 3, 3, 56, T) 复数 -> 预处理后的实数特征
        amp, phase = csi_window.abs(), sanitize(csi_window.angle())
        pseudo_feat = self.translator(amp, phase)   # (B, 256, H/8, W/8)
        return self.head(pseudo_feat)
        # 输出: 每个检测框的 part 分类图 (25类) + UV 回归图 (24×2通道)

论文级指标供参考:在同源环境下,WiFi 方案的 [email protected] 能达到 87 左右,与 RGB 方案(94+)有差距但已经可用;跨环境(换一个房间)性能会明显下降,这是当前所有 WiFi 感知方案的共同软肋,后面优化章节细说。

四、工程实现:为什么是 Rust,以及 ESP32 集群怎么搭

4.1 Rust 重写的动机:实时流水线的确定性延迟

学术原型是 Python 的,但 wifi-densepose 的生产实现选择了 Rust,仓库语言标识也是 Rust。这不是跟风,而是实时信号处理的刚需:

  • CSI 流是 100~1000Hz 的持续数据流,Python 的 GC 停顿和 GIL 在高频小包场景下会造成采样抖动,直接污染时序特征
  • 相位清洗、插值重采样这类逐样本运算,Rust + SIMD 比 NumPy 快 5~10 倍,且延迟方差极小
  • 边缘部署目标是树莓派/迷你主机级别的设备,省下的每一毫秒都是帧率

核心管线的结构大致是三级异步流水:

// 简化后的管线骨架
use tokio::sync::mpsc;

async fn pipeline() {
    let (raw_tx, mut raw_rx) = mpsc::channel::<CsiPacket>(1024);
    let (feat_tx, mut feat_rx) = mpsc::channel::<FeatureFrame>(64);

    // Stage 1: UDP 采集 —— ESP32 节点推流至 5005 端口
    tokio::spawn(async move {
        let sock = tokio::net::UdpSocket::bind("0.0.0.0:5005").await.unwrap();
        let mut buf = [0u8; 2048];
        loop {
            let (len, _) = sock.recv_from(&mut buf).await.unwrap();
            if let Ok(pkt) = CsiPacket::parse(&buf[..len]) {
                let _ = raw_tx.try_send(pkt); // 满则丢弃,绝不阻塞采集
            }
        }
    });

    // Stage 2: 预处理 —— 相位清洗 + 100Hz 重采样 + 滑窗
    tokio::spawn(async move {
        let mut window = SlidingWindow::new(100 /* 1s @ 100Hz */);
        while let Some(pkt) = raw_rx.recv().await {
            window.push(sanitize(pkt));
            if let Some(frame) = window.try_emit() {
                let _ = feat_tx.try_send(frame);
            }
        }
    });

    // Stage 3: 推理 —— ONNX Runtime 执行翻译网络 + DensePose 头
    let session = ort::Session::builder().unwrap()
        .commit_from_file("invispose.onnx").unwrap();
    while let Some(frame) = feat_rx.recv().await {
        let poses = session.run(frame.into_inputs()).unwrap();
        broadcast(poses); // WebSocket 推送给前端可视化
    }
}

注意 try_send 的使用——采集线程永不阻塞是硬性原则。推理跟不上就丢帧,丢帧只影响输出帧率,阻塞采集则会破坏整条时间轴。

4.2 硬件方案:从 8 美元到研究级

wifi-densepose 生态支持三档硬件,丰俭由人:

档位硬件成本能力
入门任意 WiFi 设备(RSSI 模式)0存在检测、粗粒度活动
标准3~6 个 ESP32-S3 节点组 Mesh每节点约 8 美元CSI 采集、姿态估计、呼吸检测
研究Intel 5300 / Atheros AR9580 网卡数百元完整 3×3 MIMO CSI

标准方案的部署拓扑:路由器锁定 2.4GHz 信道 6(禁止自动切换,信道跳变会让 CSI 基准漂移),3 个 ESP32-S3 节点分布在房间不同角落,各自抓取 CSI 后通过 UDP 打到局域网内的推理主机。

ESP32 侧的采集核心用的是 ESP-IDF 的 CSI 回调 API:

// ESP-IDF: 开启 CSI 采集并注册回调
wifi_csi_config_t csi_cfg = {
    .lltf_en = true,           // Legacy Long Training Field
    .htltf_en = true,          // HT-LTF,n 模式主要数据来源
    .stbc_htltf2_en = true,
    .manu_scale = false,
};
esp_wifi_set_csi_config(&csi_cfg);
esp_wifi_set_csi_rx_cb(&csi_rx_callback, NULL);
esp_wifi_set_csi(true);

static void csi_rx_callback(void *ctx, wifi_csi_info_t *info) {
    // info->buf 为交错的 I/Q 原始数据,info->len 通常 128~384 字节
    // 打包节点 ID + 时间戳 + 原始 CSI,UDP 发往推理主机
    send_udp_packet(NODE_ID, esp_timer_get_time(), info->buf, info->len);
}

一个实测经验:ESP32 的 CSI 只有单天线、子载波数也少于 Intel 5300,单节点信息量不足,多节点空间分集是关键——3 个节点从不同角度"照射"同一空间,拼起来的信息量才够神经网络还原姿态。这也解释了为什么官方推荐最少 3 节点。

4.3 时间同步:分布式采集的隐形大坑

多节点方案立刻引入一个分布式系统经典问题:时钟同步。三个 ESP32 各自打时间戳,晶振漂移每小时能差出几十毫秒,而姿态估计的滑窗是 10ms 级别的。

wifi-densepose 的处理是软件层 NTP 校准 + 数据层对齐的双保险:节点每 60 秒与主机做一次简化 NTP 握手修正偏移;主机侧再用各节点收到的同一个 beacon 帧(路由器广播,所有节点都能听到)作为天然同步锚点做精对齐。这个设计相当漂亮——beacon 帧就是免费的全网时钟脉冲。

五、性能优化实战

5.1 推理侧:INT8 量化 + 算子融合

翻译网络 + DensePose 头的 FP32 模型在树莓派 5 上只能跑 3~4 FPS,不可用。三步优化后到 15+ FPS:

  1. ONNX 导出后做图优化:Conv+BN+ReLU 融合、消除冗余 Transpose(模态翻译网络里 NCHW/NHWC 转换特别多)
  2. INT8 动态量化:翻译网络对量化不敏感(输入本来就是重度归一化的信号特征),精度损失 <1 AP;DensePose 头的 UV 回归分支对量化敏感,保留 FP16
  3. 滑窗复用:相邻两帧的 CSI 窗口有 90% 重叠,预处理结果做增量更新而不是全量重算,预处理 CPU 占用降了 80%
# 量化命令示意
python -m onnxruntime.quantization.preprocess --input invispose.onnx --output prep.onnx
python -c "
from onnxruntime.quantization import quantize_dynamic, QuantType
quantize_dynamic('prep.onnx', 'invispose_int8.onnx',
                 weight_type=QuantType.QInt8,
                 nodes_to_exclude=['uv_head/*'])  # UV 回归分支保留高精度
"

5.2 精度侧:对抗跨环境退化

前面提到,换个房间性能就崩,这是 CSI 特征里混入了环境静态多径(墙、家具的反射)导致的。有效的缓解手段按性价比排序:

  • 背景减除:部署后先采集 30 秒无人 CSI 作为"环境底噪",运行时做复数域减法。最便宜,效果立竿见影,跨环境 AP 能追回一半
  • 数据增强:训练时对 CSI 做随机子载波丢弃、幅度缩放、时间抖动,模拟环境差异
  • 少样本微调:新环境采 5 分钟标注数据(用一台手机拍视频跑 RGB DensePose 自动生成伪标签),微调翻译网络最后两层。这是目前工程上最实用的方案——标注完全自动化,部署成本只有 5 分钟

5.3 多人场景的真实水平

必须泼一盆冷水:论文和项目宣传里"多人追踪"是有水分的。CSI 是全空间叠加信号,两个人的反射分量在信号域是混在一起的,不像图像天然可分割。实测中:

  • 1 人:稳定可用
  • 2 人(间距 >1.5m):基本可用,偶发身份互换
  • 3 人以上或近距离:姿态互相污染,只能退化为人数统计

这是物理层面的限制,短期内靠算法很难根治。评估这个项目落地时请务必按"单人/双人场景"来设定预期。

六、隐私:这项技术最锋利的双刃

不谈隐私的 WiFi 感知文章是不完整的。

支持者的叙事是"隐私友好":不采集图像,黑暗中工作,养老监护(跌倒检测、呼吸监测)不需要在卧室卫生间装摄像头。这个价值是真实的——跌倒检测 + 呼吸率监测恰恰是 wifi-densepose 官方主打的应用场景。

但反过来想:摄像头至少你看得见,WiFi 感知是完全无感的。穿墙特性意味着追踪者甚至不需要进入你的房间——理论上,楼道里的一台设备就能感知你在家里的活动。你无法用贴纸挡住它,无法通过关灯躲避它,甚至不知道它的存在。

技术上的对抗手段目前只有两类:物理层面的电磁屏蔽(不现实),以及信号层面的主动扰动——已有研究在探索用发射随机噪声帧的"CSI 干扰器"污染感知者的信道估计。可以预见,WiFi 感知与反感知会成为下一个猫鼠游戏。

立法层面几乎是空白。图像监控好歹有明确的法律框架,而"用无线信号推断室内人体活动"在绝大多数司法辖区处于灰色地带。这个项目冲上 Trending 引发的伦理争论,价值可能不亚于技术本身。

七、总结与展望

把这个项目拆完,我的评价是:学术上不算全新(CMU 论文珠玉在前),但工程化的完成度是里程碑级的。它做对了三件事:

  1. 把研究级硬件门槛打到 8 美元——ESP32 方案让任何人都能复现,这是它能病毒式传播的根本原因
  2. Rust 流水线证明了边缘实时推理的可行性——不依赖云端,数据不出局域网,反而部分回应了隐私质疑
  3. 教师-学生跨模态蒸馏的范式值得举一反三——"把新模态翻译成成熟模态的特征空间,复用整个下游生态",这个思路可以搬到毫米波、UWB、声学感知等一切新兴传感模态上

短板同样清晰:跨环境泛化差、多人场景受物理限制、隐私争议悬而未决。

我的判断是:两年内,WiFi 感知不会取代摄像头,但会在摄像头不被允许出现的场景——卧室跌倒检测、卫生间求救、睡眠呼吸监测——撕开一个真实的市场。而对开发者来说,现在花一个周末、100 块钱人民币搭一套 ESP32 感知集群,亲手摸一摸"无线信号里的人体",绝对是 2026 年性价比最高的技术探索之一。

墙,从来没有像今天这样透明过。

推荐文章

软件定制开发流程
2024-11-19 05:52:28 +0800 CST
企业官网案例-芊诺网络科技官网
2024-11-18 11:30:20 +0800 CST
Java环境中使用Elasticsearch
2024-11-18 22:46:32 +0800 CST
PHP设计模式:单例模式
2024-11-18 18:31:43 +0800 CST
【SQL注入】关于GORM的SQL注入问题
2024-11-19 06:54:57 +0800 CST
程序员茄子在线接单