30 минут | Средний-продвинутый уровень | Освойте принципы оценки позы по WiFi CSI, разверните версии на Python/Rust, подключите узел感知 ESP32 к сети и запустите сквозной конвейер
Целевая аудитория
- Разработчики, интересующиеся WiFi-сенсорикой и повсеместными вычислениями
- Инженеры со знанием Python, желающие изучить высокопроизводительные вычисления на Rust
- Исследователи, ищущие альтернативы системам распознавания позы на основе камер
Минимальные требования: умение читать классы Python и базовый синтаксис Rust, наличие среды Linux/macOS или WSL2.
Основные зависимости и окружение
| Зависимость | Требуемая версия | Назначение |
|---|---|---|
| Python | ≥ 3.9 | Основная кодовая база v1 |
| Rust | ≥ 1.75 | Высокопроизводительный порт для вывода |
| PyTorch | ≥ 2.1 | Вывод нейронных сетей |
| NumPy + SciPy | Последняя стабильная версия | Обработка сигналов CSI |
| OpenCV | ≥ 4.8 | Визуализация позы |
| Плата разработки ESP32-S3 | 8MB Flash | Аппаратный узел WiFi CSI (опционально) |
| Маршрутизатор с поддержкой AP | 802.11n и выше | Источник данных CSI |
WARNING
Обычный ESP32 (первое поколение) и ESP32-C3 не поддерживают сбор CSI. Для проекта явно требуется ESP32-S3 (двухъядерный Xtensa) или ESP32-C6 + 60-ГГц модуль mmWave.
Полная структура проекта
RuView/
├── v1/ # Основная кодовая база Python
│ ├── src/
│ │ ├── core/
│ │ │ ├── csi_processor.py # Структура данных CSI и обработка
│ │ │ ├── phase_sanitizer.py # Коррекция фазового шума
│ │ │ └── router_interface.py # Связь с маршрутизатором/ESP32
│ │ ├── hardware/
│ │ │ ├── csi_extractor.py # Извлечение данных из последовательного порта ESP32
│ │ │ └── router_interface.py # Захват кадров WiFi CSI
│ │ └── services/
│ │ └── ... # Конвейер обработки сигналов
│ ├── data/
│ │ └── proof/
│ │ ├── sample_csi_data.json # 1000 детерминированных синтезированных кадров CSI (seed=42)
│ │ └── verify.py # Скрипт проверки конвейера по SHA-256
│ └── tests/ # Набор тестов pytest
├── rust-port/wifi-densepose-rs/ # Портированная версия на Rust
│ └── crates/
│ ├── wifi-densepose-core/ # Основные типы, примитивы кадров CSI
│ ├── wifi-densepose-signal/ # Передовая обработка сигналов RuvSense (14 модулей)
│ ├── wifi-densepose-nn/ # Бэкенды вывода ONNX/PyTorch/Candle
│ ├── wifi-densepose-train/ # Конвейер обучения RuVector
│ ├── wifi-densepose-hardware/ # Протокол TDM для ESP32
│ └── wifi-densepose-api/ # REST API на Axum
├── firmware/esp32-csi-node/ # Исходный код прошивки для ESP32-S3
├── docs/adr/ # 43 записи архитектурных решений (ADR)
└── pyproject.toml # Определение зависимостей Python
Пошаговое руководство
Шаг 1: Понимание принципов WiFi CSI (сначала запустите Python-симуляцию без оборудования)
Суть WiFi CSI заключается в использовании изменений амплитуды и фазы радиосигнала, отраженного от тела человека в пространстве, для бесконтактного восприятия.
Когда между устройством WiFi (маршрутизатором) и приемником движется человек, в отчетах CSI регистрируются комплексные значения для каждой поднесущей, включая амплитуду (amplitude) и фазу (phase). Хотя эти изменения незначительны, их достаточно для восстановления позы и движений человека.
Сила RuView в том, что он преобразует сигнал CSI через полный конвейер (коррекция фазы → подавление многолучевости → вывод нейронной сети) в оценку позы, состоящую из 17 ключевых точек (keypoint).
Сначала запустите полный конвейер со встроенными синтезированными данными без подключения оборудования:
# Клонирование проекта
git clone https://github.com/ruvnet/RuView.git
cd RuView
# Установка зависимостей Python
cd v1
pip install -e ".[dev]"
# Запуск проверки детерминированного конвейера (оборудование не требуется)
python data/proof/verify.py
Если все работает правильно, вы увидите:
===== WiFi-DensePose Pipeline Verification =====
Generator: generate_reference_signal.py v1.0.0
Seed: 42
Frames: 1000 | Antennas: 3 | Subcarriers: 56
Loading sample CSI data... done
Running pipeline...
[1/4] Phase sanitization ............ OK
[2/4] Multistatic signal processing .. OK
[3/4] Neural network inference ....... OK
[4/4] Keypoint extraction ........... OK
Computing reference hash (SHA-256)...
VERDICT: PASS
TIP
Этот проверочный скрипт использует синтезированные данные CSI, сгенерированные с seed=42, и все результаты полностью воспроизводимы. Успешное выполнение этого шага означает, что ваше окружение Python настроено правильно, и можно переходить к следующему шагу.
Шаг 2: Подготовка окружения (Python + Rust + WSL2)
Окружение Python (рекомендуется venv)
# Рекомендуется использовать Python 3.10 или 3.11 для лучшей совместимости
python3.10 -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows PowerShell
pip install --upgrade pip
pip install -e ".[gpu]" # При наличии NVIDIA GPU
pip install -e ".[dev]" # Зависимости для разработки (включая pytest)
Окружение Rust (для высокопроизводительного порта вывода)
# Установка Rust (если еще не установлен)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Проверка
rustc --version # Должно быть rustc 1.75+
cargo --version # Должно быть cargo 1.75+
# Инструментарий Rust для поддержки ESP32 (опционально, требуется только при наличии оборудования)
rustup target add xtensa-esp32-elf riscv32imc-esp-elf
Пользователям Windows: WSL2 обязателен
# Запустите PowerShell от имени администратора
wsl --install
# После перезагрузки войдите в WSL
wsl -d Ubuntu-22.04
WARNING
ESP-IDF (инструментарий компиляции прошивки ESP32) имеет конфликты переменных окружения в родной Git Bash для Windows (переменная MSYSTEM мешает ESP-IDF v5.4). Для компиляции прошивки ESP32 на Windows рекомендуется использовать WSL2 или чистый PowerShell.
Шаг 3: Запуск конвейера оценки позы в Python
Теперь мы используем реальный код для передачи данных CSI в конвейер вывода.
3.1 Структура данных CSI
# v1/src/core/csi_processor.py
from __future__ import annotations
import numpy as np
from dataclasses import dataclass
from typing import List
@dataclass
class CSIFrame:
"""Один кадр CSI — соответствует одному наблюдению канала WiFi"""
timestamp_s: float
amplitude: np.ndarray # форма: (num_antennas, num_subcarriers)
phase: np.ndarray # форма: (num_antennas, num_subcarriers)
subcarrier_indices: np.ndarray # Номера поднесущих
@classmethod
def from_json(cls, data: dict) -> CSIFrame:
return cls(
timestamp_s=data["timestamp_s"],
amplitude=np.array(data["amplitude"], dtype=np.float32),
phase=np.zeros_like(data["amplitude"], dtype=np.float32), # JSON не содержит фазу, инициализируем вручную
subcarrier_indices=np.arange(56), # По умолчанию 56 поднесущих
)
@dataclass
class PoseKeypoints:
"""Результат позы из 17 ключевых точек COCO"""
keypoints: List[np.ndarray] # Каждая ключевая точка: (x, y, confidence)
pose_id: int
def get_skeleton(self) -> np.ndarray:
"""Возвращает массив координат (N, 2) всех ключевых точек для удобства рисования"""
return np.array([p[:2] for p in self.keypoints])
3.2 Коррекция фазы (устранение сдвига LO)
Исходная фаза CSI сильно загрязнена шумом LO (гетеродина), и ее необходимо скорректировать перед использованием:
# v1/src/core/phase_sanitizer.py
import numpy as np
def sanitize_phase(csi: np.ndarray) -> np.ndarray:
"""
Итеративная оценка сдвига фазы LO + коррекция кругового среднего
Ссылка: ADR-014 SOTA Signal Processing
Аргументы:
csi: комплексная матрица CSI формы (num_antennas, num_subcarriers)
Возвращает:
Скорректированную матрицу фазы той же формы
"""
num_antennas, num_subcarriers = csi.shape
phase = np.angle(csi) # Исходная фаза [-π, π]
# Итеративная оценка сдвига фазы (обычно 3 итерации для сходимости)
for _ in range(3):
# Оценка сдвига по разности фаз соседних антенн на каждой поднесущей
phase_offset = np.zeros(num_antennas)
for a in range(1, num_antennas):
delta = phase[a] - phase[0]
# Круговое среднее (с учетом разрыва фазы)
delta_mean = np.arctan2(
np.mean(np.sin(delta)),
np.mean(np.cos(delta))
)
phase_offset[a] = delta_mean
phase[a] -= phase_offset[a]
return phase
def extract_clean_amplitude(csi: np.ndarray) -> np.ndarray:
"""Извлечение чистой амплитуды, игнорируя поднесущие с сильным шумом"""
amplitude = np.abs(csi)
# Фильтрация по Z-оценке: отбрасываем поднесущие с аномально низкой амплитудой (возможные помехи)
z_scores = (amplitude - np.mean(amplitude, axis=1, keepdims=True)) \
/ (np.std(amplitude, axis=1, keepdims=True) + 1e-8)
mask = z_scores > -2.5
amplitude = np.where(mask, amplitude, 0)
return amplitude
3.3 Сквозной процесс вывода
# v1/demo_pipeline.py
import json
import numpy as np
from pathlib import Path
from src.core.csi_processor import CSIFrame, PoseKeypoints
from src.core.phase_sanitizer import sanitize_phase, extract_clean_amplitude
def load_csi_frames(json_path: str) -> list[CSIFrame]:
"""Загрузка последовательности кадров CSI из JSON-файла"""
with open(json_path) as f:
data = json.load(f)
return [CSIFrame.from_json(frame) for frame in data["frames"]]
def run_pipeline(csi_frame: CSIFrame) -> PoseKeypoints:
"""
Полный конвейер вывода CSI → Поза
Этапы:
1. Коррекция фазы (удаление шума LO)
2. Очистка амплитуды (фильтрация выбросов)
3. Подавление многолучевости (ядро обработки сигналов RuvSense)
4. Вывод нейронной сети (упрощенный вывод ключевых точек)
5. Фильтр Кальмана для отслеживания (трекер 17 ключевых точек)
"""
# Шаг 1: Коррекция фазы
# Здесь мы используем комплексные числа, заполненные единицами, для имитации исходного CSI (демонстрация)
csi = csi_frame.amplitude * np.exp(1j * csi_frame.phase)
clean_phase = sanitize_phase(csi)
# Шаг 2: Чистая амплитуда
clean_amplitude = extract_clean_amplitude(csi)
# Шаг 3: Подавление многолучевости (упрощенная версия — пространственное усреднение)
suppressed = clean_amplitude.mean(axis=0) # (56,) усреднение по антеннам
# Шаг 4: Упрощенный вывод позы (демонстрация)
# Реально здесь вызывается модель PyTorch: self.model(suppressed.unsqueeze(0))
# Здесь мы используем фиктивные выходные данные для имитации 17 ключевых точек (x, y, conf)
keypoints = []
for i in range(17):
x = 0.5 + 0.05 * np.sin(i * 0.5) + np.random.normal(0, 0.02)
y = 0.5 + 0.3 * np.cos(i * 0.3) + np.random.normal(0, 0.02)
conf = 0.9 + np.random.normal(0, 0.05)
keypoints.append(np.array([x, y, np.clip(conf, 0, 1)]))
return PoseKeypoints(keypoints=keypoints, pose_id=0)
def main():
csi_path = Path(__file__).parent / "data/proof/sample_csi_data.json"
frames = load_csi_frames(str(csi_path))
print(f"Загружено {len(frames)} кадров CSI")
# Запуск вывода для первых 30 кадров
for i, frame in enumerate(frames[:30]):
pose = run_pipeline(frame)
skeleton = pose.get_skeleton()
if i % 10 == 0:
print(f"Кадр {i}: {len(skeleton)} ключевых точек, "
f"средняя уверенность={np.mean([k[2] for k in pose.keypoints]):.3f}")
print("Выполнение конвейера завершено.")
if __name__ == "__main__":
main()
Запустите:
python demo_pipeline.py
# Пример вывода:
# Загружено 1000 кадров CSI
# Кадр 0: 17 ключевых точек, средняя уверенность=0.901
# Кадр 10: 17 ключевых точек, средняя уверенность=0.898
# ...
# Выполнение конвейера завершено.
Шаг 4: Компиляция и тестирование версии на Rust (сверхбыстрый вывод)
Python-версия подтвердила правильность логики. Теперь переключимся на версию на Rust — это производственный путь, обеспечивающий задержки на уровне миллисекунд.
cd rust-port/wifi-densepose-rs
# Проверка компиляции (без GPU, быстрая проверка отдельного крейта)
cargo check -p wifi-densepose-core --no-default-features
cargo check -p wifi-densepose-signal --no-default-features
# Запуск полного набора тестов (1031+ тестов, около 2 минут)
cargo test --workspace --no-default-features
TIP
Флаг --no-default-features пропускает код, связанный с GPU/CUDA, и позволяет запускать тесты на машинах без драйверов NVIDIA. Успешное прохождение тестов означает, что логика основного конвейера полностью корректна.
Структура основного кода Rust выглядит следующим образом — вы заметите, что ее архитектура полностью соответствует Python-версии:
// crates/wifi-densepose-core/src/types.rs
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
// Кадр WiFi CSI — версия на Rust использует строгую типизацию вместо dataclass Python
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CSIFrame {
pub timestamp_us: u64,
pub amplitude: Vec<Vec<f32>>, // (antennas, subcarriers)
pub phase_raw: Vec<Vec<f32>>, // Исходная фаза (нескорректированная)
pub subcarrier_idx: Vec<i32>,
}
// Ключевые точки позы после обработки RuvSense
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoseKeypoints {
pub keypoints: Vec<Keypoint>,
pub pose_id: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Keypoint {
pub x: f32,
pub y: f32,
pub confidence: f32,
pub label: &'static str, // "nose", "left_shoulder", и т.д.
}
// Стиль ошибок в Rust
#[derive(Debug, thiserror::Error)]
pub enum CSIPipelineError {
#[error("Недостаточно антенн: получено {0}, требуется >= 2")]
InsufficientAntennas(usize),
#[error("Обнаружено NaN в данных CSI")]
NaNInCSI,
#[error("Сбой коррекции фазы: {0}")]
PhaseSanitization(String),
}
// crates/wifi-densepose-signal/src/ruvsense/phase_align.rs
// Коррекция фазы — высокопроизводительная версия на Rust (с ускорением SIMD)
use std::f32::consts::PI;
pub fn circular_mean(samples: &[f32]) -> f32 {
// Круговое среднее = atan2(sum(sin), sum(cos))
let (sin_sum, cos_sum) = samples.iter()
.map(|&x| (x.sin(), x.cos()))
.fold((0.0_f32, 0.0_f32), |(s, c), (si, co)| (s + si, c + co));
sin_sum.atan2(cos_sum)
}
/// Итеративная коррекция сдвига фазы LO (соответствует sanitize_phase в Python)
pub fn align_phase(phase: &mut [[f32; 56]; 3]) {
for _iter in 0..3 {
for ant in 1..3 {
let delta: [f32; 56] = std::array::from_fn(|sc| phase[ant][sc] - phase[0][sc]);
let offset = circular_mean(&delta);
for sc in 0..56 {
phase[ant][sc] -= offset;
// Приведение к диапазону [-π, π]
phase[ant][sc] = ((phase[ant][sc] + PI) % (2.0 * PI)) - PI;
}
}
}
}
Шаг 5: Прошивка ESP32-S3 и подключение к WiFi
Этот шаг предназначен для тех, у кого есть оборудование. Если оборудования нет, можно пропустить и перейти к шагу 6.
5.1 Компиляция прошивки
WARNING
Приведенные ниже команды выполняются в WSL2 на Windows или в Linux/macOS. Не запускайте ESP-IDF в родной Git Bash для Windows из-за конфликта переменной окружения MSYSTEM.
cd firmware/esp32-csi-node
# Конфигурация — версия с Flash 8 МБ (стандартная)
cp sdkconfig.defaults.8mb sdkconfig.defaults
# Компиляция прошивки (полная команда компиляции ESP-IDF довольно длинная, здесь приведены ключевые шаги)
# Необходимо предварительно активировать окружение esp-idf
. /path/to/esp-idf/export.sh # Linux/macOS/WSL2
# C:\Espressif\esp-idf\export.ps1 # Windows PowerShell
idf.py build
TIP
Для ESP-IDF v5.4 + ESP32-S3 требуются определенные настройки Kconfig для WiFi CSI: необходимо включить CONFIG_ESP_WIFI_CSI_ENABLED=y. В шаблоне по умолчанию это уже настроено.
5.2 Прошивка ESP32-S3
# Определите последовательный порт (Linux/macOS: /dev/ttyUSB0, Windows: COM7)
idf.py -p /dev/ttyUSB0 flash monitor
5.3 Настройка подключения к WiFi
python firmware/esp32-csi-node/provision.py \
--port /dev/ttyUSB0 \
--ssid "ИмяВашейСетиWiFi" \
--password "ПарольВашейСетиWiFi" \
--target-ip 192.168.1.20 # IP-адрес, на который ESP32 будет отправлять собранные CSI
5.4 Проверка потока данных CSI
# Запуск монитора последовательного порта
python -m serial.tools.miniterm /dev/ttyUSB0 115200
Если вы видите непрерывный вывод, похожий на следующий, это означает, что данные CSI передаются нормально:
CSI: ts=1234567890 ant=0 sc=0 amp=1.23 phase=-0.45
CSI: ts=1234567890 ant=0 sc=1 amp=1.45 phase=-0.32
CSI: ts=1234567890 ant=1 sc=0 amp=0.98 phase=0.12
...
Шаг 6: Сквозная проверка — от сбора CSI до вывода позы
Подключите hardware/csi_extractor.py из Python к потоку данных ESP32 или используйте режим зеркалирования маршрутизатора для сбора реального CSI:
# Запуск службы сбора CSI (подключение к узлу ESP32)
cd v1
python -m src.hardware.csi_extractor --port /dev/ttyUSB0 --baud 115200
# В другом терминале запустите REST API (сервис Axum на Rust)
cd rust-port/wifi-densepose-rs
cargo run -p wifi-densepose-api
Эндпоинты API:
# Получить текущие обнаруженные позы
curl http://localhost:8080/api/v1/poses/current
# Получить координаты скелета за последние 10 кадров
curl http://localhost:8080/api/v1/poses/history?limit=10
Пример ответа:
{
"poses": [
{
"pose_id": 1,
"keypoints": [
{"label": "nose", "x": 0.52, "y": 0.15, "confidence": 0.94},
{"label": "left_shoulder", "x": 0.38, "y": 0.28, "confidence": 0.91},
{"label": "right_shoulder","x": 0.65, "y": 0.27, "confidence": 0.89},
...
],
"timestamp_us": 1712345678901234
}
]
}
Шаг 7: Запуск официальных тестов (опциональное расширение)
# Тесты Python
cd v1
python -m pytest tests/ -x -q
# Тесты Rust
cd rust-port/wifi-densepose-rs
cargo test --workspace --no-default-features
# Генерация полного проверочного пакета (стандарт ADR-028)
cd ../..
bash scripts/generate-witness-bundle.sh
cd dist/witness-bundle-ADR028-*/
bash VERIFY.sh
При успешном выполнении проверочный пакет выдаст 7/7 PASS, включая результаты тестов Rust, хэш проверки Python и SHA-256 файлов прошивки.
Устранение неполадок
Вопрос 1: ESP32 не передает данные CSI, экран пуст
WiFi CSI требует, чтобы и маршрутизатор, и ESP32 поддерживали 802.11n. Кроме того, на некоторых маршрутизаторах необходимо вручную включить функцию сбора CSI (некоторые модели ASUS/Netgear поддерживают родной CSI). Проверьте версию прошивки маршрутизатора или используйте ESP32 в режиме точки доступа в качестве источника данных.
Вопрос 2: Ошибка numpy.linalg.LinAlgError при выполнении python data/proof/verify.py
Обычно это связано с несовместимостью версий numpy/scipy. Убедитесь, что вы используете numpy >= 1.24, scipy >= 1.11, и не используйте старые образы conda. Рекомендуется использовать изолированное окружение venv:
pip install "numpy>=1.24" "scipy>=1.11"
python data/proof/verify.py
Вопрос 3: Ошибка компиляции cargo test: linker cc not found
В Windows отсутствует инструментарий MSVC. Установите Visual Studio Build Tools и выберите "C++ CMake tools for Windows".
Вопрос 4: После коррекции фазы амплитуда равна нулю или содержит NaN
Проверьте диапазон значений амплитуды во входном JSON-файле CSI. Если исходные данные представлены целыми числами (ненормализованными), их необходимо предварительно разделить на масштабный коэффициент. Обратитесь к диапазону значений (1.0~2.0) в файле v1/data/proof/sample_csi_data.json.
Вопрос 5: Прошивка ESP32 выполнена успешно, но подключение к WiFi не удается
Убедитесь, что на маршрутизаторе не включена изоляция AP (некоторые функции, такие как Airplay/HomeCast, иногда включают изоляцию). Кроме того, ESP32-S3 поддерживает только 2.4 ГГц WiFi. Убедитесь, что SSID маршрутизатора не работает только в диапазоне 5 ГГц.
Вопрос 6: Ключевые точки позы сильно колеблются, сигнал зашумлен
Это самая распространенная проблема, обычно вызванная многолучевыми помехами. Способы решения: расположите антенну ESP32 и антенну маршрутизатора под углом 45° друг к другу; или увеличьте параметры подавления многолучевости в RuvSense (SVD-шумоподавление в файле field_model.rs).
Дополнительные материалы / Направления для развития
1. Многовзглядное внимание RuvSense с слиянием
Одной из ключевых особенностей RuView является модуль RuvSense. Механизм CrossViewpointAttention в файле attention.rs объединяет перспективы CSI от нескольких узлов ESP32. Геометрическое смещение (GeometricBias) взвешивает вклад разных узлов, а анализ границы Крамера-Рао позволяет определить, достигло ли текущее развертывание теоретического предела точности.
2. Углубленное изучение архитектурных решений (ADR)
Проект включает 43 ADR. Рекомендуется начать с следующих:
- ADR-014 — Выбор передовых методов обработки сигналов
- ADR-024 — Контрастное встраивание CSI (основа для AETHER re-ID)
- ADR-028 — Аудит возможностей ESP32 (обязателен для выбора оборудования)
3. Обучение собственных моделей с помощью RuVector
Крейт wifi-densepose-train уже включает конвейер обучения RuVector v2.0.4. Вы можете дообучать модели ONNX на собственных наборах данных CSI в помещении (требуется сбор с нескольких точек зрения и ручная разметка ключевых точек). ADR-016 описывает полный процесс.
4. Создание mesh-сети из нескольких узлов RuvSense
ADR-029/ADR-032 описывают режим восприятия с несколькими узлами. Используя несколько ESP32-S3 + ESP32-C6 для создания сенсорной mesh-сети в сочетании с логикой слияния MultistaticArray из RuvSense, можно отслеживать позу в нескольких комнатах.
5. Принципы прошивки ESP32 CSI
Чтобы по-настоящему понять, откуда берутся данные CSI, прочитайте firmware/esp32-csi-node/main/wifi_csi.c. Основная логика заключается в функции обратного вызова wifi_csi_cb_t в ESP-IDF — каждый раз при получении подходящего WiFi-кадра запускается сбор CSI. Протокол TDM (временное мультиплексирование) реализован в hardware/src/esp32/tdm.rs и обеспечивает отсутствие взаимных помех при работе нескольких узлов.