// [T05/T06] 人脸追踪任务 // 只有 ESP32-S3 + CONFIG_XIAOZHI_ENABLE_FACE_TRACKING=y 才编译完整实现 // 其他情况编译 3 个空函数,保证链接通过 #include "face_tracker.h" #include "sdkconfig.h" #if defined(CONFIG_XIAOZHI_ENABLE_FACE_TRACKING) && defined(CONFIG_IDF_TARGET_ESP32S3) #include "human_face_detect.hpp" #include "dl_image_define.hpp" #include "dl_detect_define.hpp" #include "board.h" #include "esp32_camera.h" #include #include #include #include #include #include #include static const char* TAG = "FaceTracker"; static TaskHandle_t s_handle = nullptr; static volatile bool s_stop = false; static float s_last_fps = 0.0f; // T06: uart_send_face 由 T07 在 uart_component.{h,cc} 中提供 // 此处用前向声明 + 弱符号,让 T07 完成前 face_tracker.cc 仍能通过编译 // T07 完成后该弱符号被真实实现覆盖,无需改动本文件 extern "C" __attribute__((weak)) void uart_send_face(int x_offset, int y_offset); static void face_tracker_task(void* arg) { (void)arg; // 等待摄像头 ISP 预热 + 视频流启动稳定 vTaskDelay(pdMS_TO_TICKS(500)); ESP_LOGI(TAG, "face_tracker task started on core %d", xPortGetCoreID()); // 构造检测器:默认 model_type 由 CONFIG_DEFAULT_HUMAN_FACE_DETECT_MODEL 决定 // lazy_load=true(默认)以减少启动期内存瞬时占用 auto* detector = new(std::nothrow) HumanFaceDetect(); if (!detector) { ESP_LOGE(TAG, "HumanFaceDetect 构造失败(PSRAM 不足?)"); multi_heap_info_t info; heap_caps_get_info(&info, MALLOC_CAP_SPIRAM); ESP_LOGE(TAG, "PSRAM free=%u total_allocated=%u", (unsigned)info.total_free_bytes, (unsigned)info.total_allocated_bytes); s_handle = nullptr; vTaskDelete(NULL); return; } // 一次性打印启动时 PSRAM 占用供诊断(RESEARCH R2 风险跟踪) { multi_heap_info_t info; heap_caps_get_info(&info, MALLOC_CAP_SPIRAM); ESP_LOGI(TAG, "PSRAM after detector init: free=%u allocated=%u", (unsigned)info.total_free_bytes, (unsigned)info.total_allocated_bytes); } // 按 Kconfig 配置的 FPS 计算节拍 const TickType_t period = pdMS_TO_TICKS(1000 / CONFIG_XIAOZHI_FACE_TRACKING_FPS); TickType_t last_wake = xTaskGetTickCount(); int hit = 0, miss = 0; int64_t last_report_us = esp_timer_get_time(); while (!s_stop) { vTaskDelayUntil(&last_wake, period); auto* cam = dynamic_cast(Board::GetInstance().GetCamera()); if (!cam) { continue; } Esp32Camera::FrameRef f; if (!cam->CaptureForDetection(&f)) { // [T04 策略] 拿不到 mutex(MCP 拍照中)或 DQBUF 失败 → 正常跳帧 continue; } // 组装 esp-dl 图像描述符 // RESEARCH Pitfall A1:先假定 YUYV;若首轮 score 低于 0.5 可改 RGB565LE(决策点 D-B) dl::image::img_t img{}; img.data = (void*)f.data; img.width = f.width; img.height = f.height; img.pix_type = dl::image::DL_IMAGE_PIX_TYPE_YUYV; int64_t t0 = esp_timer_get_time(); auto& results = detector->run(img); int64_t t1 = esp_timer_get_time(); // 立即归还 V4L2 缓冲,避免 face_track 占用时间长 cam->ReleaseDetectionFrame(f); if (results.empty()) { miss++; } else { hit++; // PLAN 未明确排序策略,esp-dl 内部 nms 后 list 顺序不稳定 // 为健壮性,挑 score 最高的那个(避免多脸时摇摆) const dl::detect::result_t* best = nullptr; for (const auto& r : results) { if (best == nullptr || r.score > best->score) { best = &r; } } // box: [left_up_x, left_up_y, right_down_x, right_down_y] int cx = (best->box[0] + best->box[2]) / 2; int cy = (best->box[1] + best->box[3]) / 2; // 坐标映射(RESEARCH Pitfall 7):严格保持 cx * 224 / width - 112 // 对齐 RP2040 端 deadzone=20 / x_adj_factor=10 的基准 int x_offset = (f.width > 0) ? (cx * 224 / f.width - 112) : 0; int y_offset = (f.height > 0) ? (cy * 224 / f.height - 112) : 0; // T07 完成后,uart_send_face 弱符号会被真实实现覆盖 if (uart_send_face != nullptr) { uart_send_face(x_offset, y_offset); } ESP_LOGD(TAG, "face score=%.2f offset=(%d,%d) infer=%lldus", best->score, x_offset, y_offset, (long long)(t1 - t0)); } // 每 10 秒汇报一次统计(加保底避免除零) int64_t now = esp_timer_get_time(); if (now - last_report_us > 10000000LL) { float elapsed_s = (now - last_report_us) / 1e6f; if (elapsed_s > 0.1f) { s_last_fps = (hit + miss) / elapsed_s; ESP_LOGI(TAG, "face stats: hit=%d miss=%d fps=%.1f", hit, miss, s_last_fps); } hit = miss = 0; last_report_us = now; } } delete detector; ESP_LOGI(TAG, "face_tracker task exiting"); s_handle = nullptr; vTaskDelete(NULL); } extern "C" void face_tracker_start(void) { if (s_handle != nullptr) { ESP_LOGW(TAG, "face_tracker already running, ignore start"); return; } s_stop = false; // Core 0 + 优先级 2:低于 LVGL / 音频,避免抢占主路径 // 栈 8KB:给 esp-dl 推理留充足空间 BaseType_t ok = xTaskCreatePinnedToCore( face_tracker_task, "face_track", 8 * 1024, nullptr, 2, &s_handle, 0); if (ok != pdPASS) { ESP_LOGE(TAG, "xTaskCreatePinnedToCore failed"); s_handle = nullptr; } } extern "C" void face_tracker_stop(void) { s_stop = true; } extern "C" float face_tracker_get_fps(void) { return s_last_fps; } #else // 非 S3 或功能未启用:提供空壳,保证链接通过 extern "C" void face_tracker_start(void) {} extern "C" void face_tracker_stop(void) {} extern "C" float face_tracker_get_fps(void) { return 0.0f; } #endif // CONFIG_XIAOZHI_ENABLE_FACE_TRACKING && CONFIG_IDF_TARGET_ESP32S3