Rdzleo e95d0c414e Phase 01 批次 1-3: 单摄像头人脸追踪基础设施
实现 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>
2026-04-17 18:24:27 +08:00

14 KiB
Raw Blame History

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 删除重复更新代码 + T09L659注释明确"只在此处更新"
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 #1T10 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-702idle 判断放在数据消费 + 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 追踪)、测试 2idle 不追踪)、测试 33s 超时)、测试 4staticflag 联动)四项覆盖。

BLOCKER #2grove_active 重复更新)

  • T09 单一更新点L649-651animation.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。
  • 职责划分清晰
    • T09incoming_commands 收到 face: → set True + 更新 last_seen
    • T10facetrack 检查超时 → set False
    • 两处不冲突,未来 grove_last_seen 真正反映"最后一次收到 ESP32 数据的时间"。

BLOCKER #3T01 最小 probe

  • 改为 V4L2 直接 ioctlL75-99只调一次 VIDIOC_DQBUF + VIDIOC_QBUF,不触发 JPEG 编码、不做 PSRAM 大分配、不触发 encoder_thread。
  • 执行时间预算合理DoDL116要求 elapsed < 200ms——这是合理的V4L2 DQBUF 在 10 FPS 下理论 < 100ms 就能拿到下一帧DVP 唤醒延迟也在此预算内)。
  • probe 调用位置正确L101-112放在 Application::Start() 末尾、protocol_->Start() 之后——此时 Esp32Camera 已构造完成,streaming_on_=truevideo_fd_ 已 open。
  • 保留诊断 APIL114T04 完成后只删 probe 调用、保留 ProbeFrameCapture API 作为诊断工具——比原方案"完全删除"更稳。

HIGH #1CMakeLists 非 S3

  • 三重保护明确写入 T05L370-387 + L388-394 兼容性表):
    1. Kconfig 层depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4T02 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 includeT05 L319
  • 兼容性表L388-394覆盖 ESP32-S3 / P4 / 原版 / C3 / C6 五种目标的预期行为。
  • DoD 加入交叉验证L397"ESP32原版编译通过验证非 S3 目标不会因 human_face_detect.hpp 缺失而失败)"。
  • 轻微注意T05 L392 写"ESP32-P4face_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 #2Capture mutex 饥饿)

  • T04 双超时策略明确L209修订说明"face_track 拿不到锁就跳过这一帧(人脸检测允许丢帧),拍照则可完整持有 mutex"。
  • 代码层面正确
    • CaptureForDetectionL240xSemaphoreTake(capture_mutex_, pdMS_TO_TICKS(10)),拿不到立即返回 false
    • CaptureL275xSemaphoreTake(capture_mutex_, portMAX_DELAY) 等任意时长
  • 使用 FreeRTOS Semaphore 而非 std::mutex注释L231解释了原因std::mutex 的 try_lock_for 在 ESP32 toolchain 上不可移植)——正确的工程权衡
  • 新增 R13 风险登记L1076与代码逻辑完全一致。
  • T12 步骤 7 显式验证L796-799listening 状态下触发 MCP take_photo观察 face_tracker 跳帧而非卡死。
  • 轻微注意CaptureForDetectionReleaseDetectionFrame 的 mutex 是跨调用持有——CaptureForDetection 拿锁后不解锁,由后续的 ReleaseDetectionFrame 解锁。这是正确的(保护 mmap_buffers_[buf.index] 的内容直到 caller 用完),但 face_tracker_task 必须保证两者严格成对调用。详见第 4 节 NOTE-2

HIGH #3staticflag 漂移)

  • 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 = FalseL686 注释明确):"注意staticflag 已在 T08 parse_face 中更新,此处不再设置"
  • DoD 完整覆盖L618-628基本解析 4 条 + static 去重 4 条断言。
  • R14 风险登记 + D-D 决策点L1077, L989-992留出实测调校空间。

依赖图笔误

  • L900-902 已改为 T06→T07:图中 T07 的箭头指向 T06配上注释 "← T06 依赖 T07v1.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 L139Kconfig depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4 vs T05 L380CMake if(NOT CONFIG_IDF_TARGET_ESP32S3)
  • 问题: Kconfig 允许 P4 看到选项并打开,但 CMake 在 P4 上会移除 face_tracker.cc,导致 face_tracker 函数变成空壳(#else 分支的 3 个空函数)。如果用户在 P4 上启用该选项,编译能过但功能不工作——日志里也不会有任何报错。
  • 建议: 二选一:
    • 方案 ACMake 改为 if(NOT CONFIG_IDF_TARGET_ESP32S3 AND NOT CONFIG_IDF_TARGET_ESP32P4) 一并支持 P4
    • 方案 BKconfig 改为 depends on IDF_TARGET_ESP32S3 只支持 S3把 P4 从声明列表中去掉
  • 不阻塞:本项目硬件是 S3-N16R8P4 路径根本不会被走到。但作为面向其他用户的 Kconfig 开关,最好和实际 CMake 行为对齐。

NOTE-2: CaptureForDetectionReleaseDetectionFrame 跨调用持锁——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 风格包装:
    // 可选改进:建一个 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 秒后再唤醒到 listeninggrove_active=False 已被正确清掉,不会出现"基于过期数据驱动眼球"的视觉异常。
  2. T01 保留 ProbeFrameCapture 作为诊断 APIL114——比"完全删除"更工程化,未来 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 涉及修改的任务"明确列举L18T01、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] 标记T01L63、T04L206、T05L287、T08L549、T10L667—— 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-1P4 行为不一致):执行 T02/T05 时如果方便,顺手统一 Kconfig 和 CMake 的目标列表(建议改 Kconfig 为 depends on IDF_TARGET_ESP32S3 只支持 S3因为本项目硬件就是 S3
  2. NOTE-2CaptureForDetection RAII:执行 T04/T06 时考虑加 RAII guard 或至少加警示注释,避免未来维护引入 mutex 永久持有的 bug。
  3. R14FACE_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,进入执行阶段。