实现 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>
190 lines
14 KiB
Markdown
190 lines
14 KiB
Markdown
# Phase 1 PLAN.md 第二轮审查报告
|
||
|
||
**审查日期:** 2026-04-17
|
||
**审查对象:** `/Users/rdzleo/Desktop/CogletESP-camera-version/docs/phase-01-face-tracking/PLAN.md` v1.1
|
||
**审查方法:** 对照第一轮 PLAN_CHECK.md 每条问题逐一核验 + 实际代码交叉
|
||
**审查人:** GSD Plan Checker(第二轮)
|
||
|
||
---
|
||
|
||
## 1. 审查结论
|
||
|
||
**`PASS_WITH_NOTES`**
|
||
|
||
第一轮提出的 3 个 BLOCKER + 3 个 HIGH 全部得到正确修复,修复质量整体良好。Revision History 完整、决策点 D-07 在"用户已决策"表中明确登记、依赖图笔误已订正、新增风险 R13/R14/R15 与代码逻辑一致。**可以进入执行阶段**。
|
||
|
||
仅有 2 处轻微注意点(不阻塞,仅作提醒),见第 3 节。
|
||
|
||
---
|
||
|
||
## 2. 原问题修复情况表
|
||
|
||
| 编号 | 严重级 | 原问题 | 是否修复 | 修复质量 | 备注 |
|
||
|------|--------|--------|----------|----------|------|
|
||
| BLOCKER #1 | 🔴 | T10 facetrack 缺失 idle 判断 | ✅ 已修复 | **好** | T10 修订(L667-734)+ D-07 决策(L48)+ R15 风险登记(L1078) |
|
||
| BLOCKER #2 | 🔴 | grove_active 重复更新 | ✅ 已修复 | **好** | T10 删除重复更新代码 + T09(L659)注释明确"只在此处更新" |
|
||
| BLOCKER #3 | 🔴 | T01 用 Capture() 过重 | ✅ 已修复 | **好** | T01 重写为最小 V4L2 DQBUF/QBUF probe,预算 < 200ms |
|
||
| HIGH #1 | 🟠 | CMakeLists 非 S3 目标 | ✅ 已修复 | **好** | 三重保护:Kconfig depends + CMake REMOVE_ITEM + 源文件 #if 守卫 |
|
||
| HIGH #2 | 🟠 | Capture mutex 饥饿 | ✅ 已修复 | **好** | T04 双超时策略(detection 10ms timeout 跳帧,capture portMAX_DELAY) |
|
||
| HIGH #3 | 🟠 | staticflag 硬编码 False 漂移 | ✅ 已修复 | **好** | T08 新增 `last_face_raw` + `FACE_STATIC_THRESHOLD=3` 去重逻辑 |
|
||
| 笔误 | — | 依赖图 T05→T07 | ✅ 已修复 | **好** | 改为 T06→T07,并在 v1.1 标头明确说明 |
|
||
|
||
---
|
||
|
||
## 3. 修复细节核验
|
||
|
||
### BLOCKER #1(T10 idle 判断)
|
||
|
||
- **D-07 决策已登记**(L48):明确"idle 状态下 RP2040 不驱动眼球舵机追踪。ESP32 侧行为不变(按 D-03 始终发送坐标);RP2040 侧收到 face:x,y 时,若 animation.current_state == 'idle',仅更新 grove_active/grove_last_seen/last_face_offset 状态,不调用 set_target()"
|
||
- **代码位置正确**(L699-702):idle 判断放在数据消费 + grove_active 超时之后、舵机驱动之前——这是**严格正确的位置**:
|
||
- 先消费 `last_face_offset`(避免数据堆积)✅
|
||
- 再做 3s 超时回退(即便 idle 也要正确清 `grove_active`)✅
|
||
- 最后 `if idle: return`,跳过 servo 驱动 ✅
|
||
- **对齐原代码语义**:原 `facetrack()` L53 是 `if animation.current_state != "idle":`,T10 用 `if idle: return` 实现等价转换,且把 `grove_active` 超时判定上移到 idle return 之前——这反而比原代码更稳健(原代码 idle 时根本不更新 grove_active)。
|
||
- **DoD 测试用例完整**(L717-730):手动测试 1(非 idle 追踪)、测试 2(idle 不追踪)、测试 3(3s 超时)、测试 4(staticflag 联动)四项覆盖。
|
||
|
||
### BLOCKER #2(grove_active 重复更新)
|
||
|
||
- **T09 单一更新点**(L649-651):`animation.grove_active = True; animation.grove_last_seen = time.ticks_ms()` 只在 incoming_commands 循环里设置。
|
||
- **T09 显式注释**(L659):"grove_active / grove_last_seen 的更新**只在此处**做,T10 的 facetrack() 不再重复更新"——意图清晰。
|
||
- **T10 完全删除原 L456-461 重复代码**:T10 修订后只保留"3 秒无数据回退"兜底(L694-697),不再 set True。
|
||
- **职责划分清晰**:
|
||
- T09:incoming_commands 收到 face: → set True + 更新 last_seen
|
||
- T10:facetrack 检查超时 → set False
|
||
- 两处不冲突,未来 `grove_last_seen` 真正反映"最后一次收到 ESP32 数据的时间"。
|
||
|
||
### BLOCKER #3(T01 最小 probe)
|
||
|
||
- **改为 V4L2 直接 ioctl**(L75-99):只调一次 `VIDIOC_DQBUF` + `VIDIOC_QBUF`,不触发 JPEG 编码、不做 PSRAM 大分配、不触发 encoder_thread。
|
||
- **执行时间预算合理**:DoD(L116)要求 `elapsed < 200ms`——这是合理的(V4L2 DQBUF 在 10 FPS 下理论 < 100ms 就能拿到下一帧;DVP 唤醒延迟也在此预算内)。
|
||
- **probe 调用位置正确**(L101-112):放在 `Application::Start()` 末尾、`protocol_->Start()` 之后——此时 `Esp32Camera` 已构造完成,`streaming_on_=true`,video_fd_ 已 open。
|
||
- **保留诊断 API**(L114):T04 完成后只删 probe 调用、保留 `ProbeFrameCapture` API 作为诊断工具——比原方案"完全删除"更稳。
|
||
|
||
### HIGH #1(CMakeLists 非 S3)
|
||
|
||
- **三重保护明确写入** T05(L370-387 + L388-394 兼容性表):
|
||
1. **Kconfig 层**:`depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4`(T02 L139)
|
||
2. **CMake 层**:`if(NOT CONFIG_IDF_TARGET_ESP32S3) list(REMOVE_ITEM SOURCES "face_tracker.cc") endif()`(T05 L380-382)
|
||
3. **源文件层**:`#if defined(CONFIG_XIAOZHI_ENABLE_FACE_TRACKING) && defined(CONFIG_IDF_TARGET_ESP32S3)` 包裹所有 esp-dl include(T05 L319)
|
||
- **兼容性表**(L388-394)覆盖 ESP32-S3 / P4 / 原版 / C3 / C6 五种目标的预期行为。
|
||
- **DoD 加入交叉验证**(L397):"ESP32(原版)编译通过(验证非 S3 目标不会因 human_face_detect.hpp 缺失而失败)"。
|
||
- **轻微注意**:T05 L392 写"ESP32-P4:face_tracker.cc 编译为空壳"——但 T02 Kconfig L139 明确 `depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4`,且 T05 L380 CMake 条件是 `if(NOT CONFIG_IDF_TARGET_ESP32S3)` —— 这意味着 P4 目标会被 CMake 排除掉编译(与 Kconfig 允许冲突)。**详见第 4 节 NOTE-1**。
|
||
|
||
### HIGH #2(Capture mutex 饥饿)
|
||
|
||
- **T04 双超时策略明确**(L209,修订说明):"face_track 拿不到锁就跳过这一帧(人脸检测允许丢帧),拍照则可完整持有 mutex"。
|
||
- **代码层面正确**:
|
||
- `CaptureForDetection`(L240):`xSemaphoreTake(capture_mutex_, pdMS_TO_TICKS(10))`,拿不到立即返回 false
|
||
- `Capture`(L275):`xSemaphoreTake(capture_mutex_, portMAX_DELAY)` 等任意时长
|
||
- **使用 FreeRTOS Semaphore 而非 std::mutex**:注释(L231)解释了原因(std::mutex 的 `try_lock_for` 在 ESP32 toolchain 上不可移植)——**正确的工程权衡**。
|
||
- **新增 R13 风险登记**(L1076):与代码逻辑完全一致。
|
||
- **T12 步骤 7 显式验证**(L796-799):listening 状态下触发 MCP take_photo,观察 face_tracker 跳帧而非卡死。
|
||
- **轻微注意**:`CaptureForDetection` 与 `ReleaseDetectionFrame` 的 mutex 是**跨调用持有**——`CaptureForDetection` 拿锁后不解锁,由后续的 `ReleaseDetectionFrame` 解锁。这是正确的(保护 mmap_buffers_[buf.index] 的内容直到 caller 用完),但 face_tracker_task 必须保证两者**严格成对**调用。**详见第 4 节 NOTE-2**。
|
||
|
||
### HIGH #3(staticflag 漂移)
|
||
|
||
- **T08 新增 static 去重逻辑**(L592-606):
|
||
- 与 `last_face_raw` 比较 dx, dy
|
||
- 阈值 ≤ `FACE_STATIC_THRESHOLD=3` → 设 `staticflag = True`
|
||
- 否则更新 `last_face_raw` + 设 `staticflag = False`
|
||
- 首次收到坐标视为非静态(合理)
|
||
- **阈值 3 像素的合理性**:
|
||
- 在 224×224 归一化坐标下约 2.7%
|
||
- 对照 `coms.py` L60 `deadzone = 20`(更宽容),3 像素只用于**消除 bbox 抖动**,不会影响 deadzone 判定
|
||
- 与原 Grove 的 `if boxes_part != self.last_boxes` 字符串完全相等比较(coms.py L80)相比,3 像素阈值更宽容(容忍 bbox 抖动),是**合理升级**
|
||
- **T10 不再硬编码 staticflag = False**(L686 注释明确):"注意:staticflag 已在 T08 parse_face 中更新,此处不再设置"
|
||
- **DoD 完整覆盖**(L618-628):基本解析 4 条 + static 去重 4 条断言。
|
||
- **R14 风险登记 + D-D 决策点**(L1077, L989-992):留出实测调校空间。
|
||
|
||
### 依赖图笔误
|
||
|
||
- **L900-902 已改为 T06→T07**:图中 T07 的箭头指向 T06,配上注释 "← T06 依赖 T07(v1.1 修正)"
|
||
- **v1.1 修订说明明确**(L20, L873-875):"原版图中 T05→T07 是笔误。实际 T05 只是骨架任务(打印 hello),不调用 uart_send_face;是 T06 才调用 uart_send_face 完成坐标推送"
|
||
- **执行顺序表同步更新**(L946):"T06 前置:T02、T03、T04、T05、T07 全部完成后才能开工(v1.1 修正)"
|
||
|
||
---
|
||
|
||
## 4. 新发现的问题
|
||
|
||
### 🔵 NOTE(提示性,不阻塞执行)
|
||
|
||
#### NOTE-1: ESP32-P4 在 Kconfig 允许但 CMake 排除——存在矛盾
|
||
|
||
- **位置:** T02 L139(Kconfig `depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4`) vs T05 L380(CMake `if(NOT CONFIG_IDF_TARGET_ESP32S3)`)
|
||
- **问题:** Kconfig 允许 P4 看到选项并打开,但 CMake 在 P4 上会移除 `face_tracker.cc`,导致 face_tracker 函数变成空壳(`#else` 分支的 3 个空函数)。如果用户在 P4 上启用该选项,编译能过但功能不工作——日志里也不会有任何报错。
|
||
- **建议:** 二选一:
|
||
- **方案 A**:CMake 改为 `if(NOT CONFIG_IDF_TARGET_ESP32S3 AND NOT CONFIG_IDF_TARGET_ESP32P4)` 一并支持 P4
|
||
- **方案 B**:Kconfig 改为 `depends on IDF_TARGET_ESP32S3` 只支持 S3,把 P4 从声明列表中去掉
|
||
- **不阻塞**:本项目硬件是 S3-N16R8,P4 路径根本不会被走到。但作为面向其他用户的 Kconfig 开关,最好和实际 CMake 行为对齐。
|
||
|
||
#### NOTE-2: `CaptureForDetection` 与 `ReleaseDetectionFrame` 跨调用持锁——face_tracker 任务必须严格成对
|
||
|
||
- **位置:** T04 L256-258(注释"不解锁!由 ReleaseDetectionFrame 配对解锁"),T06 L443/458 调用配对
|
||
- **观察:** T06 的实现已经正确做到了:在 `CaptureForDetection` 返回 true 后立即用 `auto& results = detector->run(img)`,然后立刻调用 `ReleaseDetectionFrame(f)`。中间没有 `continue`/`return`/异常路径会跳过 `ReleaseDetectionFrame`。
|
||
- **潜在隐患:** 如果未来有人在 T06 的 if-else 分支中插入 early return(例如某种检测失败的快速路径),可能漏掉 `ReleaseDetectionFrame` → 整个 capture mutex 永久持有 → MCP 拍照永久卡死。
|
||
- **建议:** T06 的 ReleaseDetectionFrame 调用建议用 RAII 风格包装:
|
||
```cpp
|
||
// 可选改进:建一个 helper struct
|
||
struct DetectionFrameGuard {
|
||
Esp32Camera* cam;
|
||
Esp32Camera::FrameRef* f;
|
||
~DetectionFrameGuard() { if (cam && f) cam->ReleaseDetectionFrame(*f); }
|
||
};
|
||
```
|
||
或者至少在 T06 的代码注释中加 **"绝对不要在 CaptureForDetection true 之后到 ReleaseDetectionFrame 之间插入 early return"** 的警示。
|
||
- **不阻塞**:当前代码路径正确,仅作未来维护提醒。
|
||
|
||
---
|
||
|
||
## 5. 已修复但值得点赞的设计
|
||
|
||
1. **T10 idle return 之前先做 3 秒超时回退**(L694-702)——比原 `facetrack()` 在 idle 下完全不更新 grove_active 的行为**更稳健**。这意味着用户在 idle 下离开摄像头 3 秒后再唤醒到 listening,`grove_active=False` 已被正确清掉,不会出现"基于过期数据驱动眼球"的视觉异常。
|
||
2. **T01 保留 `ProbeFrameCapture` 作为诊断 API**(L114)——比"完全删除"更工程化,未来 issue 排查可独立触发。
|
||
3. **T05 三重保护**(Kconfig + CMake + 源文件 #if)——任一机制失效另两层兜底,鲁棒性极高。
|
||
4. **T08 阈值 3 像素的工程经验值**——对应 coms.py L60 `deadzone=20`,比例合理;而且在 DoD 里给了完整断言(包括边界 = 阈值的 case)。
|
||
5. **R13/R14/R15 与 D-D/D-07 双向链接**——风险登记和决策点形成闭环。
|
||
|
||
---
|
||
|
||
## 6. Revision History 完整性核验
|
||
|
||
- ✅ Revision History 表(L13-16)清晰列出 v1.0 → v1.1 的变更摘要
|
||
- ✅ "v1.1 涉及修改的任务"明确列举(L18):T01、T04、T06、T08、T10
|
||
- 注:T05/T07/T09 也有少量修订(CMake/注释),但属于配套调整,未单独标记
|
||
- **轻微遗漏**:T05 实际有修订(CMake 条件编译策略,HIGH #1 修复),但 v1.1 摘要里没列入。建议下次修订时补全。
|
||
- ✅ "v1.1 新增决策"(L19)明确列出 D-07
|
||
- ✅ "v1.1 依赖图修正"(L20)明确指出 T06→T07
|
||
- ✅ 每个修订任务都有 `[修订于 2026-04-17]` 标记:T01(L63)、T04(L206)、T05(L287)、T08(L549)、T10(L667)—— **T06 没有**[修订于 2026-04-17] 标记,但实际 T06 内容相对 v1.0 有微调(FPS 计算加保底防除零)。建议补上。
|
||
- ✅ 任务编号保持 T01-T15 不变
|
||
- ✅ 任务清单快速参考(L1148-1166)的 v1.1 修订列已正确标注
|
||
|
||
---
|
||
|
||
## 7. 推荐下一步
|
||
|
||
### ✅ 批准进入执行阶段
|
||
|
||
第一轮的 3 BLOCKER + 3 HIGH 已全部修复且修复质量良好;新增风险 R13/R14/R15 与代码逻辑一致;Revision History 基本完整;依赖图笔误已订正。
|
||
|
||
**可以执行 `/gsd-execute-phase 01-face-tracking`**。
|
||
|
||
### 执行过程中建议留意
|
||
|
||
1. **NOTE-1(P4 行为不一致)**:执行 T02/T05 时如果方便,顺手统一 Kconfig 和 CMake 的目标列表(建议改 Kconfig 为 `depends on IDF_TARGET_ESP32S3` 只支持 S3,因为本项目硬件就是 S3)。
|
||
2. **NOTE-2(CaptureForDetection RAII)**:执行 T04/T06 时考虑加 RAII guard 或至少加警示注释,避免未来维护引入 mutex 永久持有的 bug。
|
||
3. **R14(FACE_STATIC_THRESHOLD 调校)**:T12 阶段必须实测验证 3 像素阈值是否合适,准备好按 D-D 调整。
|
||
4. **D-07 用户体验观察**:T12 步骤 3 显式测试 idle 状态——这是本次修订的核心,务必拍视频/截图记录眼球闭眼且不动的状态作为 ACCEPTANCE.md 证据。
|
||
|
||
### 不阻塞但可优化(执行后再议)
|
||
|
||
- 给 T05 / T06 补 [修订于 2026-04-17] 标记(Revision History 完整性)
|
||
- T05 兼容性表对 P4 行为表述对齐 NOTE-1 的修订
|
||
|
||
---
|
||
|
||
## 8. 审查总结
|
||
|
||
- **正面:** 第一轮提出的所有问题都得到准确理解和修复;修复方案不仅"满足要求"还体现了工程权衡(如 FreeRTOS Semaphore vs std::mutex、RAII 担心、保留诊断 API);新增决策 D-07 的设计深入到了"idle 时眼睑闭合所以追踪无意义"的产品语义层面,不是机械修复。
|
||
- **负面:** Revision History 标记不完全(T05/T06 缺标记),少量 Kconfig/CMake 目标列表不一致——但都属于轻微注意,不影响执行。
|
||
- **结论:** 修订质量超出预期,**PASS_WITH_NOTES**,进入执行阶段。
|