从 8 个分片到热图:10 分钟 CSI 数据集解析实战

作者 Wu Jun · 更新于 2026-10-29 · Wi-Fi CSI, 数据集, Python, 解析, 热图 · 约 6 分钟

这份数据集不是"下载完就结束"的压缩包:它是 8 个二进制分片,需要先校验、再重组、再按记录格式解析。 本文用一段可以照着敲的 Python 代码,把 Zenodo 上的 10 分钟数据变成幅度矩阵和热图,并验证记录数、 时长与整包 SHA-256。所有数字都来自本项目实测 [1],数据与脚本已公开 [2] [3]。

1. 准备:下载与依赖

从 Zenodo 记录 https://zenodo.org/records/23434789 下载 8 个分片、manifest.json 与 REASSEMBLE.md,放到同一个目录。Python 侧只需要标准库 + NumPy + Matplotlib:

python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install numpy matplotlib

2. 先校验,再分析

不要跳过校验。分片缺失或下载损坏时,后面的解析结果会"看起来正常但数值错误":

cat csi-10m.part-*.bin > csi_10m.bin
shasum -a 256 csi_10m.bin
# 期望:527466ceb8b5f835e55ff348f02808736b947db39e7baac1f34e375de146a440

也可以逐个分片与 manifest.json 里的 sha256 对比;整包校验通过后再进入下一步。

3. 读取记录:外层容器与内层头

文件是"记录流",每条记录由两部分组成:

这份数据集使用 v1 的 24 字节头。读取时同时保留 chain_info,因为 bit15 才是"本次测量 的最后一条链"这一边界信号:

import struct
from collections import Counter

import numpy as np

REC = struct.Struct("<dH")             # 外层:文件时间戳 + payload 长度
HDR = struct.Struct("<I6sbBBBBBHHHH")  # 内层 24 字节头

def parse(path):
    records, measurements, pending = [], [], []
    with open(path, "rb") as f:
        while True:
            raw = f.read(REC.size)
            if not raw:
                break
            if len(raw) != REC.size:
                raise EOFError("truncated outer record")

            file_ts, length = REC.unpack(raw)
            payload = f.read(length)
            if len(payload) != length:
                raise EOFError(f"truncated payload: want {length}, got {len(payload)}")
            if length < HDR.size:
                raise ValueError("payload smaller than CSI header")

            ts, ta, rssi, snr, bw, ch, mode, _pad, n, tx, rx, chain = (
                HDR.unpack_from(payload)
            )
            iq = np.frombuffer(
                payload, dtype="<i2", count=n * 2, offset=HDR.size
            )
            item = {
                "file_ts": file_ts, "ts": ts, "ta": ta, "rssi": rssi,
                "snr": snr, "n": n, "tx": tx, "rx": rx,
                "chain": chain, "iq": iq,
            }
            records.append(item)
            pending.append(item)

            if chain & 0x8000:
                measurements.append(pending)
                pending = []

    if pending:
        print("warning: incomplete tail records:", len(pending))
    return records, measurements

records, measurements = parse("csi_10m.bin")
print("records:", len(records))
print("complete measurements:", len(measurements))
print("chains per group:", Counter(map(len, measurements)))
print("first header:", {k: v for k, v in records[0].items() if k != "iq"})

预期结果:

records: 57293
complete measurements: 7609
chains per group: Counter({8: 6714, 4: 895})
warning: incomplete tail records: 1

ts 是微秒;主数据有 256 个子载波,另有 4 条 64 子载波记录。不要假设每次测量都必然 有完整的 8 条链。这份抓包里既有 8 链组,也有 4 链组,末尾还有 1 条未闭合记录。

4. 每次测量只选一条有效链

同一次测量的多条链共享接近相同的时间点,不能把 57,293 条记录当成 57,293 个独立采样。 下面的代码优先选择 tx0/rx0;若它为空,再选择平均幅度最大的有效链:

def amplitude(iq):
    return np.hypot(iq[0::2].astype(float), iq[1::2].astype(float))

times, rows = [], []
for group in measurements:
    valid = [item for item in group if item["n"] == 256 and item["iq"].any()]
    if not valid:
        continue

    chosen = next(
        (item for item in valid if (item["tx"], item["rx"]) == (0, 0)),
        None,
    )
    if chosen is None:
        chosen = max(valid, key=lambda item: amplitude(item["iq"]).mean())

    times.append(chosen["file_ts"])
    rows.append(amplitude(chosen["iq"]))

t = np.asarray(times, dtype=float)
t -= t[0]
H = np.vstack(rows)
print("usable measurements:", H.shape)
print("measurement rate:", len(t) / (t[-1] - t[0]), "Hz")

按这个简单筛选得到约 7,608 × 256 的矩阵,时间跨度约 599.92 秒,独立测量率约 12.68 Hz。原分析脚本还会剔除 25 个零值占比过高的异常组,最终使用 7,583 次测量。 把这些差异保留下来,比强行宣称"每组一定有 8 条链"更符合真实数据。

不要把这个平均值直接当成均匀时钟。CSI 质量审计显示: 组间隔中位数为 41ms、p95 为 322ms,且有 57 个间隔超过 1 秒。做低频频谱估计前必须屏蔽长空档, 或只在连续窗口内分析。

5. 生成幅度热图

把每次测量选中的 256 个子载波按时间堆成矩阵:

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 5))
plt.imshow(
    H.T, aspect="auto", origin="lower", cmap="viridis",
    extent=[t[0], t[-1], 0, H.shape[1]],
)
plt.xlabel("time (s)")
plt.ylabel("subcarrier")
plt.title("GL-MT6000 CSI amplitude (one valid chain per measurement)")
plt.colorbar(label="amplitude")
plt.tight_layout()
plt.savefig("csi_heatmap.png", dpi=150)

这张图应该与数据集页中的热图形态一致;如果全是噪声或常数,优先检查分片顺序、24 字节头 偏移和 I/Q 字节序,而不是先调颜色范围。

6. 计算 1 秒滚动运动指标

原始测量间隔并不完全均匀。下面的代码先按时间戳插值到 20 Hz,再对相邻快照的幅度差做 1 秒滑动平均。插值只是统一时间网格,不会凭空增加信息:

FS = 20.0
uniform_t = np.arange(0.0, t[-1], 1.0 / FS)
uniform_H = np.column_stack([
    np.interp(uniform_t, t, H[:, i]) for i in range(H.shape[1])
])

delta = np.abs(np.diff(uniform_H, axis=0)).mean(axis=1)
window = int(FS)
motion = np.convolve(delta, np.ones(window) / window, mode="valid")
motion_t = uniform_t[1 + window // 2: 1 + window // 2 + len(motion)]

plt.figure(figsize=(12, 3))
plt.plot(motion_t, motion)
plt.xlabel("time (s)")
plt.ylabel("1-second mean |Δamplitude|")
plt.tight_layout()
plt.savefig("csi_motion.png", dpi=150)

在真实数据里,人走动时会看到明显的尖峰;静止时曲线接近基线。这个指标只是最便宜的 "有人活动"特征,不等于跌倒检测;呼吸和心率也不能仅靠这个幅度差指标可靠估计。

7. 常见错误

现象 原因 处理
记录数不是 57,293 缺分片或合并顺序错 按文件名排序合并,重算 SHA-256
时间轴差 1000 倍 把 ts 当毫秒 本项目 ts 是微秒;绘图前统一换算
256 个子载波变成乱码 偏移或字节序错误 使用 <i2 小端,偏移从 24 字节开始
分组数接近记录数 没有按 chain_info bit15 切分测量 以 bit15 为一个测量组的结束标记
分组数不是 7,162 误把 57,293 条链记录都除以 8 真实完整组数为 7,609,允许 4 链组和末尾残片
热图全黑/全白 未做类型转换或归一化 先转 float,再按分位数设置色标范围

8. 下一步

9. 复现验收清单

在继续做算法之前,至少让解析器满足以下条件:

如果这些数字不一致,先修正解析和分组,不要用深度学习模型或调色参数掩盖数据问题。

参考与数据来源

本文的每条外部结论都标注来源与证据等级(分级标准);标注 E 级的是本项目可复现的实测数据。

  1. E 级 · 本项目实测 本项目 10 分钟实测抓包(57,293 条 / 600.0 s,SHA-256 00373f83…95e2)
    Router CSI Lab · 2026
    okwujun.pw/zh/datasets/ · 核实: 分片重组逐字节校验通过(2026-10-11) · 本地原始母片 SHA-256 为 00373f83…95e2;公开下载的是去标识派生文件,SHA-256 为 527466ce…a440,两者不可混用。
  2. E 级 · 本项目实测 Router CSI Lab — GL-MT6000 (MT7986) Wi-Fi CSI dataset, 10 s sample + 10 min capture
    Router CSI Lab · 2026
    doi:10.5281/zenodo.23434789 · 核实: CC BY 4.0;SHA-256 已发布;Zenodo DOI 10.5281/zenodo.23434789 · 数据集已获 Zenodo DOI(10.5281/zenodo.23434789),可直接引用。
  3. E 级 · 本项目实测 Router CSI Toolkit — 采集/分析/守护脚本(MIT,公开仓库)
    Router CSI Lab · 2026
    github.com/jjjwu/router-csi-toolkit · 核实: GitHub API 于 2026-11-02 核实公开仓库、抓包质量预检、成功 CI 与 v0.1.3 Release;归档 SHA-256 为 59f144c0…adea7 · 公开版含无第三方依赖的抓包质量预检;网页端检查器使用同一容器定义和边界口径。