03 · 人脸识别管线优化:灰图之谜、ROI 转色与 IoU 追踪(7.6 → 29 fps) 本系列第 3 篇 | 2026-05 ~ 07 | 标签:GStreamer、OpenCV、NPU 推理、性能优化算法对齐 ST 官方参考工程:BlazeFace 128×128 检测 FaceNet512 160×160 特征 余弦距离匹配人脸库。跑通不难,难的是在双核 A35 上把它做快:识别循环从7.6 fps到29 fps(顶满相机帧率),CPU 从吃满一个核到只花BlazeFace 的 ~7ms/帧。这一篇复盘三次关键优化和两个不报错只是不对的坑。管线总览:一条相机,三个消费者libcamerasrc ── view-finder pad(640×480 RGB16)──┬─ tee ─→ 显示 sink(RGB16 直进,零转换) └──────→ appsink_rec(识别:裁 ROI) ── still-capture pad(DCMIPP 硬缩 128×128 BGR888)──→ appsink_det(检测)两个关键设计:检测输入走 still-capture 路:DCMIPP 第二条 pipe硬件缩放出 128×128,正好是 BlazeFace 的模型输入 —— 缩放在硬件里完成,CPU ≈ 0。显示与识别吃相机原生 RGB16:不经任何软件转换。--rotate/--disp_width这类选项只在真的用到时才插 videoflip/videoscale,不影响默认零转换路径。坑一:检测图全变灰 —— caps 协商退化现象:det 路落盘的 128×128 小图全是灰度。没有报错、没有 warning,画面就是灰的。定位方法值得记住:appsink 的 caps 被强成 RGB 会掩盖真相,要看转换元件(videoconvert)的 sink pad输入侧实际协商到了什么 —— 在回调里把 conv 元件的输入 caps 打出来看。实测结论:DCMIPP 第二条 pipe(still-capture)上游协商退化成了 GRAY8,后面的 videoconvert 把它升成 RGB 后 RGB,所以落盘是灰度 —— 不是代码 bug,是链路上游就没给彩色。后续重构时的闭环验证更有意思:still-capture 本来直出 BGR888 彩色,但链路里一插 videoconvert,libcamera 协商就退化成 GRAY8 再软件升色;去掉转换元件、queue 直连 appsink 钉 RGB,立刻回到彩色,还省每帧一次软转。教训:加一个 videoconvert 保平安在 libcamera/DCMIPP 上恰恰相反——它可能把 caps 协商引向退化路径。能用硬件直出的就不要插转换元件。坑二:整帧 videoconvert 吃满一个核识别路原本:queue → videoconvert(整帧 RGB565→RGB888)→ appsink,每帧软转一次 640×480;加上显示路的转换,一个 A35 核直接吃满(~100%)。改法:appsink 收相机原生 RGB16,回调里整帧只当CV_8UC2包住不转;等裁出人脸 ROI(通常几十×几十像素)后才cvtColor(BGR5652RGB) resize 到160×160 喂 FaceNet:cv::Matframe(cv::Size(w,h),CV_8UC2,map.data);// 包住,零拷贝cv::Mat roiframe(box);// 只裁人脸区域cv::cvtColor(roi,rgb,cv::COLOR_BGR5652RGB);// 转换量小两个数量级cv::resize(rgb,face160,cv::Size(160,160));实测:CPU 100% → 29%,识别相似度无退化。原则:颜色转换跟着 ROI 走,永远不要为了一小块区域转换整个帧。优化一:去掉检测/识别 ping-pong(7.6 → 11.8/s)原逻辑:检测出框后置标志,坐等下一个 view-finder 帧到达才裁剪 ——那一等平均半个帧周期,纯属浪费。改法:识别回调退化成只gst_sample_ref最新帧(零拷贝、零阻塞),识别在检测回调里就地跑,用手头最新那帧。一个反直觉的实测细节:两条 pad 的 buffer PTS 多数时候完全相同(同一次曝光分出的两路),偶尔差一个帧周期;而改前用的是检测帧之后到达的帧,反而更不同步 —— 新做法不但没变差,同步性还更好了。顺带修掉参考工程里一套危险写法:检测回调 lock、识别回调 unlock的跨线程 mutex——POSIX 下非持有线程 unlock 普通 mutex 是未定义行为,glibc 上碰巧能跑而已。重构后 faces 只被检测线程访问,唯一跨线程共享用 lock_guard 保护,68ms 的推理在锁外。优化二:按 IoU 追踪,同一张脸不重跑 FaceNet(11.8 → 29/s)洞察:人脸身份不逐帧变。本轮框和上轮框按 IoU 配对,配上、身份已确认、距上次推理不到rec_period_ms,就沿用 label,跳过 68ms 的 FaceNet。真跑只剩三种情况,按优先级:新框(prio 0)仍是 unknown(prio 1)到期复核(prio 2)commit 级别的细节有三个,少一个都会出错:全局 IoU 配对:所有 (新框,旧框) 组合按 IoU 降序依次认领,一个旧框只能被认领一次。让每个新框各自挑最优 → 两人并排时会同时配到同一个旧框、贴同一个名字;按检测顺序贪心 → 两人交错走过时标签对调。每轮最多跑一次 FaceNet(rec_max_per_cycle1):给单轮耗时封顶。没有它:5 个陌生人 → 每轮 5×68348ms → 循环掉到 2.9/s → 两轮之间人已走出旧框→ IoU 全失配 → 下轮重跑全部 → 恶性循环。有了它地板恒为 ~13/s 与人数无关,5 人 5 轮(~380ms)内认完。配上旧框的脸无条件继承身份(含 unknown):否则排不上推理名额的脸这轮会闪回unknown,UI 标签闪烁。这套设计的失败方向分析也值得学:IoU 失配(人动快了/框抖大了)只会当成新面孔重新识别 —— 安全;IoU 误配(框套到别人身上)才会贴错标签 —— 由全局配对 强制复核兜底,错误标签窗口上限即 rec_period_ms。让系统往安全的失败方向倾斜。阈值:int8 量化下的 0.40 是标定值,不是拍脑袋相似度阈值默认 0.40(对齐官方参考工程的标定值),比较语义是similarity threshold(距离越小越像)。注意板载.nb是int8 量化模型,会把余弦距离压扁:同一张脸实测相似度在0.27~0.55波动 —— 这个宽度远超 fp32模型的直觉。调参口诀:误认了调小、漏认调大;但先确认你在 int8 模型上量,别拿 fp32 经验套。显示 sink:kms 还是 wayland选择元件条件kmskmssink 直接 DRM 全屏必须先停 weston,否则拿不到 DRM master静默失败waylandwaylandsinkweston 客户端窗口,可与其他应用共存;要固定位置用 wp_viewport 等比缩放 黑边居中显示管线给 bus 加看护:这类运行时错误打一行明确的 ERROR 并自动停上屏,而不是让整条识别链陪着挂。静默失败是最贵的失败方式 —— 宁可吵。复现要点检测路:确认 appsink 收到的是 DCMIPP 直出彩色(RGB24/BGR888),不是 GRAY8 升上去的识别循环应顶满订阅帧率(~30fps),日志里 FaceNet 仅在新面孔/到期复核时出现CPU 观测:face_rec 的开销应只在 BlazeFace ~7ms/帧 量级下一篇:04 · WebRTC 低延迟优化实录—— 从自建信令的第一版开始,出画面 11.5 秒;一路压到首帧百毫秒级,每一步都有实测数字。