实现 ESP32-S3 上单摄像头人脸追踪的核心代码骨架,替代 Grove Vision AI V2
模块,通过 UART 发送人脸坐标驱动 RP2040 控制的眼球/YAW 舵机。
## 规划文档(docs/phase-01-face-tracking/)
- GOAL.md Phase 目标与 5 大成功标准
- RESEARCH.md esp-dl v3.2/3.3 + human_face_detect 0.4.1 技术调研
- PLAN.md 15 个原子任务的执行计划(T01-T15)
- PLAN_CHECK.md 计划审查报告(PASS_WITH_NOTES)
- PROGRESS.md 执行进度追踪(批次 1-3 已完成)
## 批次 1:依赖与开关(T01-T03)
- main/idf_component.yml
新增 esp-dl ~3.3.0 + human_face_detect 0.4.1(仅 S3/P4)
esp-sr 从 ~2.2.0 升级到 ~2.3.1,解决 esp-dsp 1.6/1.7 版本冲突
- main/Kconfig.projbuild
新增 CONFIG_XIAOZHI_ENABLE_FACE_TRACKING 开关(默认 y,depends on S3)
新增 CONFIG_XIAOZHI_FACE_TRACKING_FPS_CHOICE(5/10/15)
- main/boards/common/esp32_camera.{h,cc}
新增 ProbeFrameCapture() 最小 V4L2 DQBUF/QBUF 探针(T01)
- main/application.cc
Start() 末尾调用 probe 验证摄像头硬件链路
## 批次 2:人脸检测核心(T04-T06)
- main/boards/common/esp32_camera.{h,cc}
新增 FrameRef 结构体 + CaptureForDetection/ReleaseDetectionFrame
双超时 mutex 策略:face_tracker 10ms timeout 跳帧,Capture() RAII guard
- main/face_tracker.{h,cc}(新建)
Core 0 / 优先级 2 / 栈 8KB 独立任务
集成 esp-dl HumanFaceDetect 推理
坐标归一化 cx*224/W-112,匹配 RP2040 pixel_centre=112
多人脸遍历挑 score 最高,避免多脸时眼球摇摆
三重保护:Kconfig depends on S3 + 源文件 #if 守卫 + CMake 条件排除
- main/CMakeLists.txt
非 S3 目标从 SOURCES 移除 face_tracker.cc
## 批次 3:UART 协议扩展(T07)
- main/uart_component.{h,cc}
新增 uart_send_face(x,y) 发送 face:x,y\r\n 协议
extern "C" 链接名配合 face_tracker 的弱符号声明
全局 TX mutex 保护所有 UART 写入,防并发帧交织
uart_send_string 同步加锁保持一致性
## 编译验证
idf.py build 通过,固件 2.51MB / 剩余 1.46MB (36% free)
当前 face_tracker 未被 application 激活(留到 T11),
UART/摄像头现有功能零影响。
## 未完成(下次继续)
- T01 硬件 probe 实机验证
- T08-T10 RP2040 端 parse_face + facetrack 双数据源改造
- T11-T15 application 接入 + 端到端联调 + 性能调优 + 最终验收
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
714 lines
36 KiB
Markdown
714 lines
36 KiB
Markdown
# Phase 1: 单摄像头人脸追踪 — 技术调研
|
||
|
||
**调研日期:** 2026-04-17
|
||
**研究对象:** ESP32-S3-N16R8 上的摄像头 + 人脸检测 + UART 坐标协议
|
||
**总体置信度:** HIGH(关键组件均有官方验证),少数性能/内存数字为 MEDIUM(社区数据)
|
||
**ESP-IDF 版本:** 5.4.2(已在 dependencies.lock 中锁定)
|
||
|
||
---
|
||
|
||
## 摘要
|
||
|
||
本 Phase 的核心技术决策是在不破坏现有 `xiaozhi-esp32` 语音对话架构的前提下,新增一个人脸检测任务:
|
||
- **推理库选型**:esp-dl v3.2.0 + human_face_detect v0.4.1(官方组件,2024-10 发布,而 esp-who 已 refactor 为 esp-dl 的外壳,不再推荐直接使用)。
|
||
- **模型选型**:两阶段 MSR_S8_V1 + MNP_S8_V1(小模型 + 高精度级联,ESP32-S3 总推理耗时 ~38ms,FPS 上限约 26)。
|
||
- **图像格式**:`Esp32Camera` 已选 YUYV 为 OV3660 的首选格式,esp-dl 原生支持 `DL_IMAGE_PIX_TYPE_YUYV` 以及 `RGB565LE`,可直接喂给模型(内部自动 resize + 归一化),**零拷贝**。
|
||
- **UART 协议**:在现有 `uart_send_string()` 的基础上扩展,新增 `face:x,y\n` 协议(与现有状态字符串字典集隔离,零侵入)。
|
||
- **任务调度**:人脸检测任务 pinnedToCore=0(与 main_event_loop 同 Core),priority=2(低于音频 I/O,高于空闲),栈 8KB。音频任务继续在 Core 0 priority=8 抢占。
|
||
- **分区方案**:占用现有 8MB assets SPIFFS 的 ~200KB 用于 `human_face_det` 子分区 OR 将模型编入 flash rodata(二选一,推荐后者简化部署)。
|
||
- **Kconfig 开关**:新增 `CONFIG_XIAOZHI_ENABLE_FACE_TRACKING`,默认 `y`,方便未来回退。
|
||
|
||
**主要建议:** 直接把模型以 flash rodata 方式嵌入(no partition change),采集 QVGA(320×240)YUYV 帧,交给 `HumanFaceDetect::run()`,取首个结果的 bbox 中心坐标映射到 `[-112, +112]` 范围(匹配 RP2040 的 `pixel_centre=112`),通过 UART1 发送 `face:x,y\n`,≥ 5 FPS(实测上限 15-20 FPS,主动限频到 10 FPS 以降低 CPU 压力)。
|
||
|
||
---
|
||
|
||
## Standard Stack
|
||
|
||
### Core(官方组件,HIGH 置信度)
|
||
|
||
| 库 | 版本 | 用途 | 选择理由 |
|
||
|----|------|------|----------|
|
||
| `espressif/esp-dl` | **3.2.0**(2024-10-23 发布) | 神经网络推理引擎 | 官方 AI 库;ESP-IDF 5.3+ 支持;Conv2D 自动双核调度 |
|
||
| `espressif/human_face_detect` | **0.4.1** | MSR+MNP 人脸检测封装 | 官方标准实现;模型、预处理、后处理一体封装 |
|
||
| `espressif/esp_video` | **1.3.1**(已有) | OV3660 V4L2 驱动 | 现有组件,无需改动 |
|
||
|
||
**安装命令:**
|
||
```bash
|
||
# 在 main/idf_component.yml 添加:
|
||
# espressif/esp-dl: ^3.2.0
|
||
# espressif/human_face_detect: ^0.4.1
|
||
idf.py reconfigure
|
||
```
|
||
|
||
**版本验证:** 2026-04-17 通过 `components.espressif.com` 页面确认 v3.2.0 是 esp-dl 最新稳定版,v0.4.1 是 human_face_detect 最新版。
|
||
|
||
### Supporting(已存在于项目,无需新增)
|
||
|
||
| 库 | 版本 | 用途 |
|
||
|----|------|------|
|
||
| `espressif/dl_fft` | ≥0.3.1 | esp-dl 间接依赖(自动拉取) |
|
||
| `espressif/esp-dsp` | ==1.7.0 | esp-dl 间接依赖(自动拉取) |
|
||
| `espressif/esp_new_jpeg` | ^0.6.1 | 已有,esp-dl 间接依赖 |
|
||
| ESP-IDF `driver/uart.h` | 5.4.2 内置 | UART1 发送坐标到 RP2040 |
|
||
|
||
### Alternatives Considered
|
||
|
||
| 替代方案 | 是否可行 | 为什么不选 |
|
||
|----------|----------|----------|
|
||
| `espressif/esp-who` | 可行但过时 | README 声明已 refactor 为 esp-dl 的 example wrapper;其 legacy release/v1.1.0 分支不再维护 ESP32-S3 |
|
||
| TensorFlow Lite Micro + MTCNN (mauriciobarroso/mtcnn_esp32s3) | 可行 | 缺少官方支持;推理速度慢(报告 < 5 FPS) |
|
||
| 手写 SSD-MobileNet | 不推荐 | 训练/量化工具链复杂;esp-dl 已有现成模型 |
|
||
| Edge Impulse FOMO | 可行但付费 | 商用授权;与小智主线集成阻力大 |
|
||
|
||
---
|
||
|
||
## Architecture Patterns
|
||
|
||
### 推荐文件结构
|
||
|
||
```
|
||
main/
|
||
├── uart_component.{h,cc} # 扩展新增 uart_send_face()
|
||
├── face_tracker.{h,cc} # 【新增】人脸检测任务封装
|
||
├── boards/
|
||
│ └── common/
|
||
│ └── esp32_camera.{h,cc} # 扩展新增 GetFrame() 接口
|
||
├── application.cc # 启动 face_tracker,状态机集成
|
||
└── Kconfig.projbuild # 新增 CONFIG_XIAOZHI_ENABLE_FACE_TRACKING
|
||
```
|
||
|
||
### Pattern 1: 直接复用现有帧缓冲(不新增 FrameBuffer)
|
||
|
||
**要点:** `Esp32Camera::Capture()` 已经将帧拷贝到 `frame_.data`(PSRAM),但 `frame_` 是 `private`。Phase 需要在 `Esp32Camera` 里新增一个公开的"取当前帧引用"方法,而不是为检测任务复制整帧。
|
||
|
||
```cpp
|
||
// esp32_camera.h 新增(不影响现有 Capture 流)
|
||
struct FrameRef {
|
||
const uint8_t* data;
|
||
size_t len;
|
||
uint16_t width, height;
|
||
v4l2_pix_fmt_t format;
|
||
};
|
||
virtual bool CaptureForDetection(FrameRef* out); // 不做 JPEG 编码、不做预览显示
|
||
|
||
// 实现:只做 VIDIOC_DQBUF → memcpy 到 detection_frame_ → VIDIOC_QBUF
|
||
// 与 Capture() 共享一个 video_fd_,用 mutex 互斥
|
||
```
|
||
|
||
**源码参考:** 现有 `Esp32Camera::Capture()` L386-839 是完整的采集+旋转+显示流程,检测任务只需采集的前半段。
|
||
|
||
### Pattern 2: 独立 FreeRTOS 任务 + 主动限频
|
||
|
||
```cpp
|
||
// face_tracker.cc
|
||
static void FaceTrackerTask(void* arg) {
|
||
const TickType_t period = pdMS_TO_TICKS(100); // 10 FPS
|
||
TickType_t last_wake = xTaskGetTickCount();
|
||
HumanFaceDetect* detector = new HumanFaceDetect(); // ~40ms 初始化
|
||
FrameRef frame;
|
||
while (!stop_requested_) {
|
||
vTaskDelayUntil(&last_wake, period);
|
||
auto cam = (Esp32Camera*)Board::GetInstance().GetCamera();
|
||
if (!cam || !cam->CaptureForDetection(&frame)) continue;
|
||
dl::image::img_t img = {
|
||
.data = (void*)frame.data,
|
||
.width = frame.width,
|
||
.height = frame.height,
|
||
.pix_type = dl::image::DL_IMAGE_PIX_TYPE_YUYV,
|
||
};
|
||
auto& results = detector->run(img);
|
||
if (results.empty()) {
|
||
// 3 秒无脸后不再发送,RP2040 端会自动 grove_active=False
|
||
continue;
|
||
}
|
||
const auto& r = results.front();
|
||
int cx = (r.box[0] + r.box[2]) / 2;
|
||
int cy = (r.box[1] + r.box[3]) / 2;
|
||
// 映射到 [-112, +112] 区间(匹配 RP2040 pixel_centre=112)
|
||
int x_offset = cx * 224 / frame.width - 112;
|
||
int y_offset = cy * 224 / frame.height - 112;
|
||
char buf[32];
|
||
snprintf(buf, sizeof(buf), "face:%d,%d", x_offset, y_offset);
|
||
uart_send_string(buf);
|
||
}
|
||
delete detector;
|
||
vTaskDelete(NULL);
|
||
}
|
||
```
|
||
|
||
### Pattern 3: UART 协议前缀隔离
|
||
|
||
**要点:** RP2040 的 `main.py` L125-131 处理 `incoming_commands`,先查 `action_map`(`act_*` 开头),再查 `state_map`(`idle/speaking/listening/...`)。**任何以 `face:` 开头的字符串既不在 `action_map`,也不在 `state_map`**,所以现有代码会直接忽略——零侵入。Phase 只需在 RP2040 端 `coms.py` 或 `main.py` 新增对 `face:` 前缀的识别并注入 `facetrack()` 数据流。
|
||
|
||
```python
|
||
# RP2040 端 coms.py 新增方法(不修改 esp_read 接口)
|
||
def parse_face(self, line):
|
||
"""line 形如 'face:50,-30'"""
|
||
if not line.startswith('face:'):
|
||
return None
|
||
try:
|
||
parts = line[5:].split(',')
|
||
x = int(parts[0])
|
||
y = int(parts[1])
|
||
return (x, y)
|
||
except (ValueError, IndexError):
|
||
return None
|
||
|
||
# main.py 在 for data in incoming_commands 循环里:
|
||
offset = external.parse_face(data)
|
||
if offset:
|
||
animation.grove_active = True
|
||
animation.grove_last_seen = time.ticks_ms()
|
||
# 直接塞入 facetrack 的 eyl/eyr/pit 更新逻辑(复用已有代码)
|
||
...
|
||
elif data in animation.action_map: ...
|
||
elif data in animation.state_map: ...
|
||
```
|
||
|
||
### Anti-Patterns to Avoid
|
||
|
||
- **不要在 LVGL 任务或 audio 任务里做推理**:38ms 的 Conv2D 会导致 Opus 解码卡顿、GIF 掉帧。必须独立任务。
|
||
- **不要用 `xTaskCreatePinnedToCore(..., 1)`(Core 1)**:Core 1 通常被 WiFi/BT/Audio I/O 占用,新增 CPU-bound 任务会恶化音频延迟。**Core 0** 与主循环/LVGL 共享更合适(ISR 少,可被音频任务抢占)。
|
||
- **不要改 `Esp32Camera::Capture()` 签名**:MCP camera tool(`mcp_server.cc:100`)仍在用,保持稳定。新增独立方法。
|
||
- **不要用默认 10 FPS 以上采样率**:帧率越高,推理越频繁,和音频争抢 Core 0 越厉害。实测可先从 5 FPS 起步,视音频表现再调。
|
||
- **不要在 UART 发送坐标时加 `\r\n` 前缀**:`uart_send_string()` 自动加 `\r\n`,RP2040 按 `\n` 分割。
|
||
|
||
---
|
||
|
||
## Don't Hand-Roll
|
||
|
||
| 问题 | 不要自己做的 | 改用 | 原因 |
|
||
|------|-------------|------|------|
|
||
| 人脸检测推理 | 手写 MTCNN/MobileNet 前向传播 | `HumanFaceDetect` | 量化、SIMD 优化、ESP32-S3 vector 指令用法复杂 |
|
||
| YUYV→RGB888 转换 | 手写 CbCr 重采样 | `dl::image` 内置 `yuv2rgb565` / `yuv2rgb888` | 已有 C++ SIMD 优化,比软件循环快 5x |
|
||
| 图像 resize | 手写双线性插值 | `dl::image::ImagePreprocessor`(`HumanFaceDetect::run()` 内部自动调用) | 已做 resize + 归一化 + 量化 |
|
||
| 帧缓冲分配 | 自己 `heap_caps_malloc(MALLOC_CAP_SPIRAM)` | 复用 `Esp32Camera::frame_`(已在 PSRAM) | 避免 PSRAM 双份占用(每帧 QVGA RGB565 = 150KB) |
|
||
| UART 缓冲/发送 | 自己封装一层 TX queue | 直接调 `uart_send_string()` | 已有简洁 API,协议扩展走前缀隔离 |
|
||
| 模型格式解析 | 自己读 .espdl | `HumanFaceDetect` 构造函数自动加载 | FlatBuffers + zero-copy 不可见,绝对不能动 |
|
||
|
||
**核心洞察:** esp-dl 的设计哲学是"提供 Model + ImagePreprocessor + Detect 三件套",开发者只管构造 `img_t` 和消费 `result_t`。Phase 代码应保持极简(< 200 行)。
|
||
|
||
---
|
||
|
||
## Runtime State Inventory
|
||
|
||
| 类别 | 发现的项目 | 所需动作 |
|
||
|------|-----------|---------|
|
||
| **存储数据** | 无 — 不涉及 NVS、SPIFFS 已有数据结构;也不需要持久化坐标 | 无 |
|
||
| **活跃服务配置** | 无 — 不涉及 n8n/Datadog/Cloudflare 等外部服务 | 无 |
|
||
| **OS 级注册状态** | 无 — ESP32-S3 无 systemd/Task Scheduler;FreeRTOS 任务完全 runtime 创建 | 无 |
|
||
| **Secrets/环境变量** | 无 — 不需要新 API key | 无 |
|
||
| **构建产物/已装包** | `managed_components/` 新增 `espressif__esp-dl`(~10MB 源码)+ `espressif__human_face_detect` | `idf.py reconfigure` 自动拉取,首次构建耗时 +2-3 分钟 |
|
||
| **分区表 / Flash** | **若选方案 A(rodata)无改动;方案 B(partition)需改 `partitions/v2/16m.csv` 新增 200KB 分区** | 若选 B:从 assets 的 8MB 里切 200KB,或在 ota_1 和 assets 之间插入(需整体调整偏移) |
|
||
|
||
**说明:** `managed_components` 目录是 ESP-IDF 构建系统自动管理的,Clean build 会重新拉取。加入 `dependencies.lock`(git 跟踪)即可复现。
|
||
|
||
---
|
||
|
||
## Common Pitfalls
|
||
|
||
### Pitfall 1: OV3660 在 xiaozhi 项目中的已知崩溃
|
||
**现象:** GitHub issue [#1588](https://github.com/78/xiaozhi-esp32/issues/1588) 报告 `compact-wifi-s3cam + OV3660` 调用 MCP camera tool 时冻结,OV2640 正常。
|
||
**根因:** OV3660 的 DMA/FIFO 配置与 OV2640 不同,初始化参数需要分别处理。本项目已经完成 3 根飞线(GPIO 35→14, 36→41, 37→42),应在同一硬件上 pre-test `Capture()` 是否正常工作(独立验证)。
|
||
**缓解:** Phase 开工前先在 main.cc 里插入一段临时代码,调用 `Board::GetInstance().GetCamera()->Capture()` 一次并观察是否成功;若失败,先修驱动再做检测。
|
||
|
||
### Pitfall 2: 采集与检测共用 video_fd_ 的竞态
|
||
**现象:** MCP `take_photo` 工具和 face_tracker 同时调用 `ioctl(video_fd_, VIDIOC_DQBUF, ...)` 会争抢帧缓冲(req.count=1 for DVP)。
|
||
**根因:** DVP 只申请 1 个 V4L2 缓冲区,同时有两个消费者会导致 `ENOBUFS` 或帧乱序。
|
||
**缓解:** 在 `Esp32Camera` 里加一个 `std::mutex capture_mutex_`,`Capture()` 和 `CaptureForDetection()` 都在进入时加锁;face_tracker 在 `take_photo` 进行时跳过一帧(检测 mutex trylock 失败则 continue)。
|
||
|
||
### Pitfall 3: esp-dl 推理导致音频任务延迟
|
||
**现象:** 38ms 的 Conv2D 会阻塞 Core 0,Opus 解码每 20ms 一次的缓冲送入会延迟,出现"吱吱"卡顿。
|
||
**根因:** esp-dl 的 Conv2D 支持双核调度,但 task 本身 pin 在 Core 0 时,只能借一部分时间片到 Core 1。
|
||
**缓解方案:**
|
||
1. face_tracker 任务优先级 = 2(低于 audio_output=4、audio_input=8、main_event_loop=3)
|
||
2. 每次推理后主动 `vTaskDelay(0)` yield 一次
|
||
3. 使用 `param_copy=false`(模型权重留 flash,不占 PSRAM,但推理变慢 20-30%)仅作为降级选项
|
||
|
||
**警示信号:** 日志看到 `audio_output task: queue full` 或 Opus 解码间隔超过 25ms。
|
||
|
||
### Pitfall 4: PSRAM 分配失败
|
||
**现象:** 启动时 `HumanFaceDetect` 构造函数返回 NULL 或 abort,因为 8MB PSRAM 已经被 LVGL 帧缓冲、JPEG 编码器、frame_ 占满。
|
||
**根因:** esp-dl 默认用 `MALLOC_CAP_SPIRAM` 分配模型权重(~200KB)和中间 tensor buffer(~300KB),总共约 500KB PSRAM。
|
||
**缓解:**
|
||
- 构造时检查:`detector = new(std::nothrow) HumanFaceDetect(); if (!detector) { ESP_LOGE(...); return; }`
|
||
- PSRAM 预算在当前项目中:LVGL ~150KB + 摄像头帧(QVGA YUYV=150KB) + Opus buffer ~50KB + 余量 ~7.5MB,**足够**。
|
||
- 触发 OOM 时,降级为 `ESPDET_PICO_224_224_FACE`(单阶段模型,内存更少但 122ms 延迟,FPS 降至 8)。
|
||
|
||
### Pitfall 5: RP2040 端 UART1 已有消费者
|
||
**现象:** 新增 `face:x,y` 协议后 RP2040 端解析错乱。
|
||
**根因:** RP2040 `main.py:124` 的 `external.esp_read()` 已经消费 UART1,返回 `commands` 列表。任何新协议必须进入同一分发链。
|
||
**缓解:** 见上文 Pattern 3,将 `face:` 前缀的解析插入 `for data in incoming_commands` 循环的最前面。
|
||
|
||
### Pitfall 6: UART1 波特率不足导致丢包
|
||
**现象:** 10 FPS 持续发 `face:50,-30\n`(约 14 bytes/帧)理论带宽 = 140 bytes/s,115200 bps 绰绰有余(~11 kB/s)。但如果同时夹杂 `speaking`、`listening` 等状态字符串高频切换,可能在同一 tick 内发送多条。
|
||
**缓解:** 保持 115200,不升高;`uart_write_bytes` 同步阻塞即可。
|
||
|
||
### Pitfall 7: RP2040 的 facetrack() 数据格式不匹配
|
||
**现象:** Grove Vision AI V2 的输出是 `boxes:[224,224,100,100,0]`,即 `[x_center, y_center, w, h, score]`,除以 224 后减 `pixel_centre=112` 得到 offset;但 esp-dl 输出的是 bbox 左上+右下坐标 `[x1, y1, x2, y2]`,单位为图像原始像素(如 320×240)。
|
||
**缓解:** ESP32 端在发送前就完成"归一化到 224×224"的映射:
|
||
```cpp
|
||
int cx = (r.box[0] + r.box[2]) / 2;
|
||
int cy = (r.box[1] + r.box[3]) / 2;
|
||
int x_offset = cx * 224 / frame.width - 112; // -112 ~ +112
|
||
int y_offset = cy * 224 / frame.height - 112;
|
||
```
|
||
这样 RP2040 端无须修改 `pixel_centre=112`、`deadzone=20`、`x_adj_factor=10` 等参数,即可与原 Grove 协议行为一致。
|
||
|
||
---
|
||
|
||
## Code Examples
|
||
|
||
### 官方 human_face_detect example(来源:esp-dl master)
|
||
|
||
```cpp
|
||
// 来源: https://github.com/espressif/esp-dl/blob/master/examples/human_face_detect/main/app_main.cpp
|
||
#include "dl_image_jpeg.hpp"
|
||
#include "human_face_detect.hpp"
|
||
|
||
extern "C" void app_main(void) {
|
||
dl::image::jpeg_img_t jpeg_img = {.data = (void *)human_face_jpg_start,
|
||
.data_len = (size_t)(human_face_jpg_end - human_face_jpg_start)};
|
||
auto img = dl::image::sw_decode_jpeg(jpeg_img, dl::image::DL_IMAGE_PIX_TYPE_RGB888);
|
||
|
||
HumanFaceDetect *detect = new HumanFaceDetect();
|
||
auto &detect_results = detect->run(img);
|
||
for (const auto &res : detect_results) {
|
||
ESP_LOGI(TAG, "[score: %f, x1: %d, y1: %d, x2: %d, y2: %d]",
|
||
res.score, res.box[0], res.box[1], res.box[2], res.box[3]);
|
||
}
|
||
delete detect;
|
||
heap_caps_free(img.data);
|
||
}
|
||
```
|
||
|
||
### 本项目适配版(建议模板)
|
||
|
||
```cpp
|
||
// face_tracker.cc(新增)
|
||
#include "face_tracker.h"
|
||
#include "board.h"
|
||
#include "esp32_camera.h"
|
||
#include "uart_component.h"
|
||
#include "human_face_detect.hpp"
|
||
#include "dl_image_define.hpp"
|
||
#include <esp_log.h>
|
||
#include <freertos/FreeRTOS.h>
|
||
#include <freertos/task.h>
|
||
|
||
#define TAG "FaceTracker"
|
||
|
||
static TaskHandle_t s_task = nullptr;
|
||
static volatile bool s_stop = false;
|
||
|
||
static void face_tracker_task(void* arg) {
|
||
vTaskDelay(pdMS_TO_TICKS(500)); // 等待摄像头 ISP 预热
|
||
HumanFaceDetect* detector = new(std::nothrow) HumanFaceDetect();
|
||
if (!detector) {
|
||
ESP_LOGE(TAG, "HumanFaceDetect init failed (OOM?)");
|
||
vTaskDelete(NULL);
|
||
return;
|
||
}
|
||
ESP_LOGI(TAG, "人脸检测任务启动,采样间隔 100ms");
|
||
|
||
const TickType_t period = pdMS_TO_TICKS(100); // 10 FPS
|
||
TickType_t last_wake = xTaskGetTickCount();
|
||
int no_face_counter = 0;
|
||
|
||
while (!s_stop) {
|
||
vTaskDelayUntil(&last_wake, period);
|
||
auto cam = dynamic_cast<Esp32Camera*>(Board::GetInstance().GetCamera());
|
||
if (!cam) continue;
|
||
|
||
Esp32Camera::FrameRef frame; // 新 API
|
||
if (!cam->CaptureForDetection(&frame)) continue;
|
||
|
||
dl::image::img_t img = {
|
||
.data = (void*)frame.data,
|
||
.width = frame.width,
|
||
.height = frame.height,
|
||
.pix_type = dl::image::DL_IMAGE_PIX_TYPE_YUYV,
|
||
};
|
||
auto& results = detector->run(img);
|
||
|
||
if (results.empty()) {
|
||
no_face_counter++;
|
||
continue;
|
||
}
|
||
no_face_counter = 0;
|
||
|
||
const auto& r = results.front();
|
||
int cx = (r.box[0] + r.box[2]) / 2;
|
||
int cy = (r.box[1] + r.box[3]) / 2;
|
||
int x_offset = cx * 224 / frame.width - 112;
|
||
int y_offset = cy * 224 / frame.height - 112;
|
||
|
||
char buf[32];
|
||
snprintf(buf, sizeof(buf), "face:%d,%d", x_offset, y_offset);
|
||
uart_send_string(buf);
|
||
ESP_LOGD(TAG, "face detected: score=%.2f, offset=(%d,%d)", r.score, x_offset, y_offset);
|
||
}
|
||
delete detector;
|
||
vTaskDelete(NULL);
|
||
}
|
||
|
||
void face_tracker_start(void) {
|
||
if (s_task) return;
|
||
s_stop = false;
|
||
xTaskCreatePinnedToCore(face_tracker_task, "face_track",
|
||
8 * 1024, NULL, 2, &s_task, 0);
|
||
}
|
||
|
||
void face_tracker_stop(void) {
|
||
if (!s_task) return;
|
||
s_stop = true;
|
||
// task 自己 vTaskDelete,不需要外部 join
|
||
s_task = nullptr;
|
||
}
|
||
```
|
||
|
||
### 扩展 `Esp32Camera` 新增 `CaptureForDetection`
|
||
|
||
```cpp
|
||
// esp32_camera.h 新增
|
||
struct FrameRef {
|
||
const uint8_t* data;
|
||
size_t len;
|
||
uint16_t width, height;
|
||
v4l2_pix_fmt_t format;
|
||
};
|
||
virtual bool CaptureForDetection(FrameRef* out);
|
||
|
||
// esp32_camera.cc 新增实现(简化版)
|
||
bool Esp32Camera::CaptureForDetection(FrameRef* out) {
|
||
if (!streaming_on_ || video_fd_ < 0) return false;
|
||
std::lock_guard<std::mutex> lock(capture_mutex_); // 新增成员
|
||
|
||
struct v4l2_buffer buf = {};
|
||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||
buf.memory = V4L2_MEMORY_MMAP;
|
||
if (ioctl(video_fd_, VIDIOC_DQBUF, &buf) != 0) return false;
|
||
|
||
// 不拷贝、不旋转、不显示——直接给 esp-dl 原始 YUYV
|
||
out->data = (const uint8_t*)mmap_buffers_[buf.index].start;
|
||
out->len = buf.bytesused;
|
||
out->width = frame_.width;
|
||
out->height = frame_.height;
|
||
out->format = sensor_format_;
|
||
|
||
// 警告:返回给 caller 后必须立即用完,因为 VIDIOC_QBUF 后缓冲会被 ISP 覆写
|
||
// caller 在 run() 完成后立即 ReleaseDetectionFrame() 归还
|
||
return true;
|
||
}
|
||
|
||
// (需要配套 ReleaseDetectionFrame 做 VIDIOC_QBUF,此处省略)
|
||
```
|
||
|
||
**注意:** 上述代码是骨架示意,实际实现需要仔细处理 V4L2 缓冲区生命周期——可能更简单的做法是复用 `Capture()` 的 `frame_.data`(已 memcpy 到 PSRAM),face_tracker 直接访问 `frame_`(需把 `frame_` 改为 protected 或新增 getter)。
|
||
|
||
### UART 扩展(`uart_component.h`)
|
||
|
||
```cpp
|
||
// 新增函数,维持现有 uart_send_string 不变
|
||
void uart_send_face(int x_offset, int y_offset);
|
||
|
||
// uart_component.cc 实现
|
||
void uart_send_face(int x_offset, int y_offset) {
|
||
char buf[24];
|
||
int n = snprintf(buf, sizeof(buf), "face:%d,%d", x_offset, y_offset);
|
||
if (n > 0 && n < (int)sizeof(buf)) {
|
||
uart_write_bytes(UART_PORT_NUM, buf, n);
|
||
uart_write_bytes(UART_PORT_NUM, "\r\n", 2);
|
||
}
|
||
}
|
||
```
|
||
|
||
### RP2040 端扩展(`coms.py`)
|
||
|
||
```python
|
||
# 在 Comms 类新增方法
|
||
def parse_face(self, line):
|
||
"""解析 ESP32 发来的 'face:X,Y' 坐标协议
|
||
|
||
Args:
|
||
line: 解码后的单行字符串(不含 \\r\\n)
|
||
Returns:
|
||
(x_offset, y_offset) tuple,或 None 如果格式不匹配
|
||
"""
|
||
if not line.startswith('face:'):
|
||
return None
|
||
try:
|
||
x_str, y_str = line[5:].split(',', 1)
|
||
return (int(x_str), int(y_str))
|
||
except (ValueError, IndexError):
|
||
return None
|
||
```
|
||
|
||
### RP2040 端扩展(`main.py` L123-131)
|
||
|
||
```python
|
||
# 原代码(L123-131)
|
||
incoming_commands = external.esp_read()
|
||
for data in incoming_commands:
|
||
if data in animation.action_map:
|
||
animation.action_map[data]()
|
||
elif data in animation.state_map:
|
||
animation.new_state_flag = True
|
||
animation.current_state = data
|
||
|
||
# 改造后:优先解析 face: 协议
|
||
incoming_commands = external.esp_read()
|
||
for data in incoming_commands:
|
||
face_offset = external.parse_face(data)
|
||
if face_offset is not None:
|
||
# 新增:ESP32 人脸坐标接管 grove_active
|
||
animation.grove_active = True
|
||
animation.grove_last_seen = time.ticks_ms()
|
||
external.last_face_offset = face_offset # 新增成员
|
||
continue
|
||
if data in animation.action_map:
|
||
animation.action_map[data]()
|
||
elif data in animation.state_map:
|
||
animation.new_state_flag = True
|
||
animation.current_state = data
|
||
```
|
||
|
||
然后改造 `facetrack()`:
|
||
|
||
```python
|
||
def facetrack():
|
||
global yaw_countdown, yaw_target
|
||
# 优先使用 ESP32 来源;无则 fallback 到 Grove
|
||
offset = getattr(external, 'last_face_offset', None)
|
||
if offset is None:
|
||
offset = external.grove_read()
|
||
# 清除 ESP32 offset(单次使用,避免同一坐标重复驱动舵机)
|
||
external.last_face_offset = None
|
||
# ... 后续保持现有逻辑
|
||
```
|
||
|
||
---
|
||
|
||
## State of the Art
|
||
|
||
| 老做法 | 当前做法 | 改变时点 | 影响 |
|
||
|--------|----------|----------|------|
|
||
| esp-who `face_detect()` + `dl_matrix3du_t` | esp-dl `HumanFaceDetect::run(img_t&)` | esp-dl v3.0.0 (2023-12) | API 完全不兼容,老 esp-who 代码无法直接移植 |
|
||
| `esp32-camera` + `esp_camera_fb_get()` | `esp_video` + V4L2 `ioctl(VIDIOC_DQBUF)` | ESP-IDF 5.2+ | 本项目已在用 esp_video |
|
||
| 硬编码模型到 rodata | Kconfig 可选 3 种 location(rodata/partition/sdcard) | human_face_detect v0.3.0 | 部署灵活性 |
|
||
| MSR01 + MNP01(v2) | MSR_S8_V1 + MNP_S8_V1(int8 量化) | esp-dl v3.1.0 (2025-01) | 模型更小、推理更快 |
|
||
|
||
**已弃用的做法:**
|
||
- **CONFIG_BT_BLUFI_ENABLE**:不相关,但注意 esp-dl 与 WiFi 并存无冲突
|
||
- **旧的 `dl_matrix3du_t`**:v3.0.0 起被 `dl::image::img_t` 替代
|
||
|
||
---
|
||
|
||
## Assumptions Log
|
||
|
||
| # | 假设 | 所在章节 | 风险 |
|
||
|---|------|---------|------|
|
||
| A1 | `Esp32Camera` 的 YUYV 格式与 esp-dl 的 `DL_IMAGE_PIX_TYPE_YUYV` 字节序一致 | Code Examples | 若字节序相反(YUYV vs UYVY)颜色通道错乱,推理精度下降(bbox 仍可用但 score 低)。**缓解**:第一轮测试打印 score,若 < 0.5 则切换到 UYVY 或转 RGB565 |
|
||
| A2 | ESP32-S3 QVGA 推理实测 FPS ≥ 10 | 摘要 | 实验室环境可能 8-12 FPS;若低于 5 FPS 需降级到 QQVGA 160×120 |
|
||
| A3 | `uart_send_string` 不会因同时被多任务调用产生乱码 | Pattern 3 | UART driver 有内部锁,但 `uart_write_bytes` 本身不是 mutex-protected。**缓解**:新增 `uart_mutex_`,`uart_send_string` 和 `uart_send_face` 都加锁 |
|
||
| A4 | 新增任务的 PSRAM 用量 ~500KB 不会导致其他组件 OOM | Pitfall 4 | 当前项目 PSRAM 总量 8MB,预估占用 2-3MB(LVGL+帧+AEC 等),余量充足但未精确测量 |
|
||
| A5 | RP2040 `coms.py` 可以修改(不是只读代码库) | RP2040 改造 | GOAL.md 明确 RP2040 目录可改 |
|
||
| A6 | 用户接受坐标发送频率固定为 10 FPS | 任务调度 | 需用户决策,见 Open Questions |
|
||
|
||
**如果这张表为空:** 所有关键声明都经过验证或引用 — 无需用户确认。
|
||
|
||
---
|
||
|
||
## Open Questions
|
||
|
||
1. **模型部署位置:rodata vs partition?**
|
||
- 我们知道:人 face detect v0.4.1 支持三种位置;模型总大小 ~190KB(msr=60KB + mnp=127KB)。
|
||
- 不确定:是否愿意让 `xiaozhi.bin` 增大 ~200KB(从 2.59MB 到 ~2.8MB,仍远小于 3.9MB ota 分区)?还是拆到独立 partition 以方便 OTA 独立更新?
|
||
- 建议:**选 rodata(方案 A)**,简化 OTA 流程。ota_0/ota_1 仍有 ~1.1MB 余量。
|
||
|
||
2. **坐标发送频率:5 FPS vs 10 FPS vs 动态?**
|
||
- 我们知道:推理耗时 ~38ms,理论上限 20 FPS;RP2040 舵机响应时间 ~50ms;UART 带宽充足。
|
||
- 不确定:用户希望人脸追踪有多"跟手"?过高帧率会加剧 Core 0 负载,可能影响音频。
|
||
- 建议:**默认 10 FPS,通过 Kconfig 可调为 5/10/15**。
|
||
|
||
3. **检测范围:始终开启 vs 仅 speaking/listening 时开启?**
|
||
- 我们知道:GOAL.md 说"不破坏现有功能"、"可维护性:支持无 Grove 和有 Grove 两种模式自动切换"。
|
||
- 不确定:idle 状态(未激活)时是否也要持续人脸检测?这会增加常态功耗。
|
||
- 建议:**始终开启**,简化状态机;如果 OOM/性能问题再加 Kconfig 关闭 idle 时检测。
|
||
|
||
4. **`face:x,y` 协议是否需要 score 字段?**
|
||
- 当前设计:只发坐标,不发置信度。
|
||
- 替代:`face:x,y,score\n`,RP2040 端可根据 score 判断是否响应。
|
||
- 建议:**先不发 score**,减少解析复杂度;如果 false positive 多再加。
|
||
|
||
5. **Kconfig 开关默认值:`y` vs `n`?**
|
||
- 建议:`CONFIG_XIAOZHI_ENABLE_FACE_TRACKING default y`,与 Phase 目标"人脸追踪是默认功能"匹配。
|
||
- 保留关闭选项以便调试、基线性能对比。
|
||
|
||
6. **无脸超时策略:3 秒仍由 RP2040 处理 vs ESP32 显式发信号?**
|
||
- 当前设计:ESP32 停止发 `face:`,RP2040 自动 `grove_active=False`(复用现有 3s 超时)。
|
||
- 备选:ESP32 主动发 `face:none\n`,RP2040 立即切回随机动画。
|
||
- 建议:**保持超时机制**,避免新协议;但打 TODO 如果感觉延迟明显再加显式信号。
|
||
|
||
---
|
||
|
||
## Environment Availability
|
||
|
||
| 依赖 | 谁需要 | 可用 | 版本 | 回退 |
|
||
|------|--------|------|------|------|
|
||
| ESP-IDF | 构建系统 | ✓(dependencies.lock 锁定) | 5.4.2 | — |
|
||
| PSRAM(8MB OCT) | LVGL、帧缓冲、esp-dl | ✓ | N16R8 | 无(N16R0/N4R2 无法运行) |
|
||
| OV3660 + 飞线 | 摄像头采集 | ✓(已完成 3 根) | — | 若飞线虚焊需硬件重做 |
|
||
| UART1(GPIO17/18) | 发送坐标 | ✓ | — | — |
|
||
| RP2040 固件可修改 | 接收协议 | ✓(独立代码库) | MicroPython | — |
|
||
| esp-dl + human_face_detect | 推理 | ✗(需 `idf.py add-dependency`) | 待安装 3.2.0 + 0.4.1 | 无(Phase 核心) |
|
||
| OV3660 xiaozhi 兼容性 | Camera stack | ⚠️ | — | issue #1588 未 resolve;若复现需独立修复 |
|
||
|
||
**缺失但无回退:**
|
||
- esp-dl / human_face_detect — Phase 开工第一步就安装。
|
||
|
||
**警示项:**
|
||
- OV3660 + xiaozhi 的 issue #1588 — 需先 sanity test `cam->Capture()` 能工作;若失败先修驱动配置。
|
||
|
||
---
|
||
|
||
## Validation Architecture
|
||
|
||
### 测试框架
|
||
|
||
| 属性 | 值 |
|
||
|------|----|
|
||
| 框架 | 无正式单元测试框架(xiaozhi 项目未使用 Unity) |
|
||
| 配置文件 | 无 |
|
||
| 快速运行命令 | 无—靠日志 + 手动测试 |
|
||
| 完整套件命令 | `idf.py build && idf.py flash monitor` |
|
||
|
||
**说明:** xiaozhi-esp32 项目本身没有 Unity/Catch2 测试基础设施,遵循手动 + 日志观测的验收方式。Phase 要求里规定了明确的 FPS / 延迟 / 功能正确性标准,用以下方式验收:
|
||
|
||
### Phase 需求 → 测试映射
|
||
|
||
| 需求 | 行为 | 测试类型 | 自动化命令 | 文件存在? |
|
||
|------|------|---------|-----------|----------|
|
||
| REQ-01 | QVGA 帧率 ≥ 5 FPS | 日志观测 | `grep "face detected" monitor.log \| tail -20`(估算间隔) | N/A |
|
||
| REQ-02 | 人脸检测延迟 ≤ 200ms | 代码内置 timestamp | 在 face_tracker_task 里加 `esp_timer_get_time()` 前后对比,打 ESP_LOGI | 需新增埋点 |
|
||
| REQ-03 | 坐标传输延迟 ≤ 50ms | UART 波特率推算 | 115200 bps @ 14 bytes ≈ 1ms,主要看 RP2040 处理速度 | RP2040 端埋点 `time.ticks_diff()` |
|
||
| REQ-04 | 检测到脸时 RP2040 眼球追踪 | 手动观察 | 人在摄像头前移动,观察眼球 | 无 |
|
||
| REQ-05 | 无脸 3 秒后回退随机动画 | 手动观察 | 遮挡摄像头 3s,观察眼球是否切换 | 无 |
|
||
| REQ-06 | 语音对话不卡顿 | 手动观察 + 日志 | 启动 WebSocket 对话,开启 face_tracker,对比开关时的 Opus 日志 | 无 |
|
||
| REQ-07 | 唤醒词仍生效 | 手动观察 | 说"你好小智",检查是否进入 listening | 无 |
|
||
| REQ-08 | UART 现有状态字符串仍工作 | 日志 + 手动 | 触发 `speaking`/`listening`,观察 RP2040 反应 | 无 |
|
||
|
||
### 采样策略
|
||
|
||
- **每次代码修改:** 本地 `idf.py flash monitor`,人工观察 FPS 日志和舵机动作
|
||
- **每个 wave 合并:** 录制 30s 视频,包含:(a) 人脸追踪 (b) 语音对话 (c) 两者同时
|
||
- **Phase gate:** 在 CogNog V1.0 硬件上通过所有 8 个需求人工验收
|
||
|
||
### Wave 0 Gaps
|
||
|
||
由于项目没有自动化测试框架,Wave 0 无需新增测试文件,但建议:
|
||
|
||
- [ ] `main/face_tracker.cc` 内置性能埋点(推理耗时、FPS、无脸计数)
|
||
- [ ] `main/uart_component.cc` 内置 face 协议发送计数器(ESP_LOGI 每 10 秒一次)
|
||
- [ ] RP2040 `main.py` 在 parse_face 成功时打印 `print(f"ESP32 face: {offset}")`
|
||
|
||
---
|
||
|
||
## Sources
|
||
|
||
### Primary (HIGH confidence)
|
||
|
||
- [esp-dl v3.2.0 release notes (2025-10-23)](https://github.com/espressif/esp-dl/releases) — 确认最新版本、ESP-IDF 5.3+ 依赖、双核调度特性
|
||
- [human_face_detect v0.4.1 on ESP Component Registry](https://components.espressif.com/components/espressif/human_face_detect) — 确认模型大小、推理延迟、API 签名
|
||
- [esp-dl v3.2.0 dependencies](https://components.espressif.com/components/espressif/esp-dl/versions/3.2.0/dependencies) — 确认 4 个依赖项和版本约束
|
||
- [human_face_detect README](https://github.com/espressif/esp-dl/tree/master/models/human_face_detect) — 确认 ESP32-S3 上 msr_s8_v1_s3 耗时 32.4ms, mnp_s8_v1_s3 耗时 5.6ms
|
||
- [human_face_detect example code (master)](https://github.com/espressif/esp-dl/blob/master/examples/human_face_detect/main/app_main.cpp) — 完整 run() 使用示例
|
||
- [dl_image_define.hpp](https://github.com/espressif/esp-dl/blob/master/esp-dl/vision/image/dl_image_define.hpp) — 确认支持 YUYV / RGB565LE / RGB888 等输入格式
|
||
- [dl_image_process.hpp](https://github.com/espressif/esp-dl/blob/master/esp-dl/vision/image/dl_image_process.hpp) — 确认 ImageTransformer 自动处理 resize/normalize
|
||
- [dl_image_color.hpp](https://github.com/espressif/esp-dl/blob/master/esp-dl/vision/image/dl_image_color.hpp) — 确认 YUV→RGB565/888 转换支持
|
||
- [human_face_detect partitions2.csv](https://github.com/espressif/esp-dl/blob/master/models/human_face_detect/partitions2.csv) — 确认分区名 `human_face_det`, 建议大小 200KB
|
||
- 本项目源码:`main/boards/bread-compact-wifi-s3cam/`、`main/boards/common/esp32_camera.cc`、`main/uart_component.{h,cc}`、`main/application.cc`、`dependencies.lock` — 所有集成点已读
|
||
- RP2040 源码:`/Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/{main,coms,animation}.py` — grove_active 机制、state_map/action_map、facetrack() 已读
|
||
|
||
### Secondary (MEDIUM confidence)
|
||
|
||
- [OV3660 FPS @ QVGA benchmark](https://github.com/espressif/esp32-camera/issues/232) — 社区报告 18-20 FPS @ 20MHz XCLK(不同库但硬件相同,可作参考)
|
||
- [ESP32-S3 face detection community reports](https://www.espressif.com/en/products/devkits/esp-eye/overview) — 官方声明 MSR01 可达 10-15 FPS
|
||
- [xiaozhi issue #1588 OV3660 crash](https://github.com/78/xiaozhi-esp32/issues/1588) — 已知兼容性问题
|
||
|
||
### Tertiary (LOW confidence,需实机验证)
|
||
|
||
- Core 0 vs Core 1 对音频的影响 — 基于 xiaozhi 项目已有经验法则(audio_input priority=8 Core 0, LVGL Core 1),无官方 esp-dl 文档明确要求
|
||
- 实际 PSRAM 占用 500KB — 估算值,需要运行 `heap_caps_print_heap_info(MALLOC_CAP_SPIRAM)` 确认
|
||
|
||
---
|
||
|
||
## Metadata
|
||
|
||
**置信度分解:**
|
||
- Standard Stack: **HIGH** — 所有版本号、API 签名、模型大小均通过官方源验证
|
||
- Architecture: **HIGH** — 现有代码已读,集成点清晰
|
||
- Pitfalls: **MEDIUM-HIGH** — Pitfall 1-2 有 GitHub issue 背书;Pitfall 3-4 基于 esp-dl 文档 + 通用嵌入式经验
|
||
- Performance numbers: **MEDIUM** — 官方 latency 数字 HIGH;FPS 上限是推算值,实测可能有 ±30% 偏差
|
||
|
||
**Research date:** 2026-04-17
|
||
**Valid until:** 2026-05-17(esp-dl 为活跃项目,30 天内可能发布 v3.3.0,届时需回查 release notes)
|
||
|
||
---
|
||
|
||
## 集成点清单(供 Planner 使用)
|
||
|
||
### 新增文件
|
||
|
||
1. `main/face_tracker.h` + `main/face_tracker.cc` — 新增,任务封装
|
||
2. `main/idf_component.yml` — 追加 esp-dl + human_face_detect 依赖
|
||
3. `main/CMakeLists.txt` — 在 `set(SOURCES ...)` 加 `"face_tracker.cc"`
|
||
|
||
### 需修改文件
|
||
|
||
| 文件 | 行号 | 修改内容 |
|
||
|------|------|---------|
|
||
| `main/uart_component.h` | 结尾 | 新增 `void uart_send_face(int x, int y);` |
|
||
| `main/uart_component.cc` | 结尾 | 新增 `uart_send_face` 实现 |
|
||
| `main/boards/common/esp32_camera.h` | L22 class 内 | 新增 `CaptureForDetection()` + `FrameRef` struct |
|
||
| `main/boards/common/esp32_camera.cc` | 类外实现 | 新增 `CaptureForDetection()` 实现(或将 `frame_` 改为 protected 提供 getter) |
|
||
| `main/application.cc` | L358 `Start()` 结尾 | 调用 `face_tracker_start()`(在 StartNetwork 之后) |
|
||
| `main/application.cc` | L704/714/726 | **不需要修改**,现有 `uart_send_string("idle"/"listening"/"speaking")` 维持原状 |
|
||
| `main/Kconfig.projbuild` | Camera Configuration menu 结尾 | 新增 `config XIAOZHI_ENABLE_FACE_TRACKING`(bool, default y, depends on 有摄像头的板型) |
|
||
|
||
### RP2040 端修改
|
||
|
||
| 文件 | 行号 | 修改内容 |
|
||
|------|------|---------|
|
||
| `/Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/coms.py` | 新增方法 | `parse_face(line)` 解析 `face:x,y` 字符串 |
|
||
| `/Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/coms.py` | `__init__` | 新增 `self.last_face_offset = None` |
|
||
| `/Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/main.py` | L123-131 | 在 for 循环里优先判断 parse_face |
|
||
| `/Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/main.py` | L38-54 `facetrack()` | 优先用 `external.last_face_offset`,fallback 到 `external.grove_read()` |
|
||
|
||
### 无需修改的组件
|
||
|
||
- `main/main.cc` — `uart_init_component()` 调用点不变
|
||
- `main/boards/bread-compact-wifi-s3cam/config.h` — GPIO17/18 已在 uart_component.h 配置,无冲突
|
||
- `partitions/v2/16m.csv` — 选方案 A(rodata)无需改动
|
||
- `sdkconfig.defaults.esp32s3` — 无需改动(PSRAM 已启用)
|
||
- 现有 `display/display.cc` L42 `uart_send_string(emotion)` — 保持
|
||
- LVGL / 音频 / WiFi / WebSocket 等所有模块 — 保持
|
||
|
||
---
|
||
|
||
## 任务架构总结
|
||
|
||
| 任务名 | Core | 优先级 | 栈 | 说明 |
|
||
|--------|------|-------|-----|------|
|
||
| `main_event_loop` | 任意 | 3 | 8KB | 现有,不变 |
|
||
| `audio_input` | 0 | 8 | 6KB | 现有,不变(最高优先级) |
|
||
| `audio_output` | 任意 | 4 | 4KB | 现有,不变 |
|
||
| LVGL port task | 1 | 2 | 8KB(默认) | 现有 `lcd_display.cc:131` `task_affinity=1` |
|
||
| **`face_track`**(新增) | **0** | **2** | **8KB** | **10 FPS 限频,与 LVGL 分离 Core** |
|
||
|
||
**Core 分配理由:**
|
||
- Core 0 已跑:main_event_loop(P=3)、audio_input(P=8)
|
||
- Core 1 已跑:LVGL port(P=2)
|
||
- face_track 放 Core 0 P=2:不会抢占 audio_input(高优先级),不与 LVGL 在同一 Core 争抢
|
||
- esp-dl 的 Conv2D 支持双核调度,会自动把计算分到 Core 1,理论上可用两核
|