简介:一套基于C/C++的人脸检测与跟踪源码工程,能够在视频图像中实时定位并框出人脸,适合计算机视觉初学者和有一定C/C++基础的开发者学习。资源以Haar特征级联分类器为核心,包含图像预处理、特征提取、滑动窗口扫描、级联分类判定、人脸框绘制与视频流跟踪等完整流程,并提供肤色模型、运动检测和神经网络分类等扩展模块。压缩包共有五十五个文件,其中头文件二十一个、C++源文件二十个,另有八个神经网络配置、一个SVM模型、资源描述文件及Visual Studio工程文件,整体仅约190KB,目录结构清晰,便于快速阅读和编译。代码模块划分明确,适合对照源码理解Haar特征计算、AdaBoost训练原理及图像金字塔等关键算法,也可作为课堂项目或毕业设计的起点。目前已有189人学习下载,对于希望深入掌握传统人脸检测实现细节与C/C++图像处理流程的读者是值得参考的实战样例。
1. 用 C 代码在视频里实时抓人脸:比想象中更接近工程落地
摄像头前的实时人脸检测,很多人的第一反应是调 OpenCV 的 detectMultiScale,直到把这段 C 代码移植到嵌入式板子上才发现事情没这么简单。这套基于视频图像实时检测的资源,核心并不只在检测算法本身,而在“实时”两个字——它把视频采集、图像预处理、人脸定位、结果绘制串成了一条完整流水线。如果你在做桌面监控、门禁闸机或者毕业设计里“摄像头抓人脸”的需求,这份代码能让你少走大半年的弯路。适合谁看?写过点 C/C++、知道帧率是什么、正被“检测框乱跳和 CPU 爆表”折磨的人。我拆这套代码的时候,先翻算法,再跑编译,最后啃性能,下面按这个顺序来。
2. 算法选型与代码结构:从肤色模型到 Haar 级联
2.1 两种“C 语言人脸检测”方案的差异
首先要做一个决策:用纯 C 还是 C++ 调库。资源解压后能看到两个入口文件,一个是纯 C 版本的主流程,另一个是基于 OpenCV 的 C++ 版本,作者刻意把两条路都留下来了。纯 C 版只依赖标准 C 库和摄像头驱动接口,把每一帧拆成三步:颜色空间转换、肤色阈值分割、连通域外接矩形。肤色检测的核心是把 RGB 转成 YCrCb 色彩空间,Cr 和 Cb 两个分量在肤色区域聚集得比较集中,所以用区间判断就能筛出皮肤像素。这段代码零依赖,拿到任何带 GCC 的板子上编译都能过,但它对“像肤色”的背景太宽容。我之前拿纯 C 版测一段室内访谈视频,穿橙色上衣的人整体都被圈进了人脸框,因为橙色在 Cr 分量上的分布和肤色重叠,阈值怎么压都压不住。
C++ 版走的是 OpenCV 的 Haar 级联分类器,模型用的是haarcascade_frontalface_alt2.xml。Haar 特征本质上是多个矩形的黑白像素差值,描述眼睛、眉毛、鼻梁这些局部的明暗对比。分类器在训练阶段见过大量正脸样本,检测时用固定窗口在图像上滑动,每个窗口经过几十级弱分类器,全部通过才判定为人脸。模型文件大概 900KB,内存占用比肤色方案大一些,但检测可信度高一个量级。背景里有书架、窗帘、树叶这些高频纹理时,肤色方案会刷出一堆候选框,Haar 的误检率却能压到个位数百分比。
我的建议是两条路径都保留,主用 C++,把纯 C 的肤色分割当作 Haar 检测前的粗筛。这种两阶段检测在工程里很常见:先用便宜的计算锁住候选区域,再对候选区域跑准确但耗时的分类器。资源包里的skin_seg.c提供第一段,face_detect_cpp.cpp是第二段,两者通过config.h里的宏切换。这个设计在嵌入式场景尤其有意义,因为 Haar 在某些 MCU 上根本跑不动,而肤色分割用纯整数运算就能完成,至少可以先把疑似区域框出来。
2.2 这份资源包里的核心文件与数据流
目录结构是很多人拿到手第一眼被劝退的地方,一堆.c、.h和一个.xml平铺在一起。我整理了一张文件职责表:
| 文件 | 职责 | 关键细节 |
|---|---|---|
face_detect.c | 纯 C 主流程 | 使用 V4L2 接口读/dev/video0 |
face_detect_cpp.cpp | OpenCV 主流程 | 支持摄像头索引和视频文件两种输入 |
skin_seg.c | 肤色分割预筛 | 输出候选矩形数组,供 Haar 二次确认 |
tracker.c | 简易框平滑 | 对连续帧的检测框做移动平均 |
config.h | 全局配置 | 缩放比、阈值、最大人脸数、ROI |
model/haarcascade_frontalface_alt2.xml | Haar 模型 | 训练参数固定,不可改 |
build.sh | 编译脚本 | 自动检测 OpenCV 头文件路径 |
主循环的数据流可以简化成这样:先把读到的原始帧缩小处理面,再做灰度转换或肤色分割,然后执行检测,最后把检测框坐标映射回原始分辨率并绘制。注意缩放操作要放在灰度转换之前,720P 的帧缩到 640×360 后像素量只剩四分之一,后续所有步骤的耗时都会明显下降,代价是小人脸更难看清,所以缩放比例必须和最小人脸尺寸联动。
while (1) { // 从摄像头读一帧,V4L2 的 mmap 模式只做指针映射 ret = read_frame(fd, &frame_buf); if (ret < 0) break; // 1) 缩小处理面,减少后续计算量 resize_to(frame_buf, &proc, config.scale_ratio); // 2) 肤色预筛或灰度转换 if (config.use_skin_first) { skin_seg(proc, &candidates); // 只对候选区域做 Haar 检测 for (int i = 0; i < candidates.count; i++) { detect_on_roi(proc, candidates.rect[i], &faces); } } else { to_gray(proc, &gray); haar_detect(gray, &faces); } // 3) 框坐标映射回原分辨率并绘制 map_back_and_draw(frame_buf, faces); write_display(frame_buf); }这段代码里read_frame用的是 V4L2 的 mmap 模式,缓冲区由驱动分配,用户态只做零拷贝映射,这是摄像头读取性能的关键。纯 C 版最容易在这步翻车的是VIDIOC_S_FMT没设置对像素格式,默认 YUYV,如果直接当 RGB 解析,画面会整体偏色,检测自然全部失效。OpenCV 版对应的cv::cvtColor和cv::resize内部有 SIMD 加速,放循环里问题不大,但要注意灰度图gray应该提前分配好,不要在每帧循环里反复 new Mat。
2.3 Haar 级联的检测原理与参数是怎么传进去的
Haar 检测不是黑匣子,它本质上是一堆弱分类器的级联投票。detectMultiScale会把输入图像按scaleFactor逐层缩小,每层用固定窗口扫一遍,窗口内计算 Haar 特征并交给多级分类器判断。前几级分类器特征简单、计算快,能快速丢弃大量非人脸区域;越往后的级数特征越复杂,也越慢。这个设计决定了scaleFactor不能太小:1.1 意味着每层缩小 10%,扫描层数多,检测细致但耗时翻倍;1.3 每层缩小 30%,速度快,但可能跳过中间尺寸的人脸。
三个关键参数的推荐组合,我整理在表里,可以直接抄:
| 场景 | scaleFactor | minNeighbors | minSize | 典型耗时 |
|---|---|---|---|---|
| 720P 室内单人坐姿 | 1.1 | 3 | 80×80 | 约 45ms |
| 720P 室内多人走动 | 1.2 | 4 | 60×60 | 约 30ms |
| 1080P 出入口 | 1.3 | 5 | 120×120 | 约 28ms |
| 嵌入式 320×240 | 1.2 | 3 | 40×40 | 约 15ms |
参数书写成代码是这样的:
std::vector<cv::Rect> faces; cascade.detectMultiScale( gray, // 输入:必须是 8 位灰度图 faces, 1.1, // 每层图像缩小比例,越大越快 3, // 邻居矩形阈值,越大越难通过 0, // flags 在新版 OpenCV 里已废除 cv::Size(40, 40), // 最小人脸尺寸 cv::Size(300, 300) // 最大人脸尺寸 );检测前做直方图均衡化是资源包里的一个细节:equalizeHist重新分配灰度分布,让暗部细节露出来。这个操作对单通道灰度图很快,耗时约 1ms,除非画面灯光极其稳定,否则不建议省。另一个容易忽略的点是,detectMultiScale返回的矩形是按照检测窗口尺寸给的,直接画出来会比真实人脸略微偏大,代码里用了一个小幅收缩修正,让框边缘更贴合人脸轮廓。别小看这 8%,跟踪场景里框是否贴合直接决定下一步特征提取的稳定性。
3. 编译与运行:从源码到实时画面
3.1 环境准备:依赖库与编译器选型
先别急着编译,依赖决定你能不能在一小时内跑起来。OpenCV 版需要 3.10 以上的版本,我这边用的是 4.5.5,3.x 和 4.x 在 CascadeClassifier 的 API 上基本兼容,唯一要留意的是头文件路径不同。资源包的 build.sh 里做了两套路径的兜底,优先用pkg-config --cflags --libs opencv4,找不到再去找旧版的opencv。这套写法在 Ubuntu 和多数 Linux 发行版上都能生效,但如果你是从源码编译安装的 OpenCV,pkg-config 文件可能没注册到系统路径里,那就得自己在脚本里硬编码头文件目录。
纯 C 版没有第三方库依赖,但需要 Linux 头文件里的videodev2.h。绝大多数发行版默认装了,如果缺,装上 libv4l-dev 即可。编译器我用 GCC 9.4,C 标准-std=c99足够。有个容易被忽略的链接参数是-pthread,实时检测主循环里检测和显示分成了两个线程,不加这个选项,运行到pthread_create会直接报段错误,不是代码逻辑问题,纯粹是链接选项缺失。
编译脚本的核心片段是这样的:
#!/bin/bash # build.sh 核心片段 CXXFLAGS="-O3 -Wall -std=c++11 -march=native" PKG_OPENCV=$(pkg-config --cflags --libs opencv4 2>/dev/null) if [ -z "$PKG_OPENCV" ]; then PKG_OPENCV=$(pkg-config --cflags --libs opencv 2>/dev/null) fi g++ $CXXFLAGS face_detect_cpp.cpp -o build/face_detect_cpp $PKG_OPENCV -lpthread这里-O3和-march=native不是可有可无的优化选项,Haar 特征计算对循环展开和 SIMD 指令集很敏感,同一台机器上-O2和-O3的检测耗时能差 20% 以上。-O3会加大生成的可执行文件体积,但对桌面和嵌入式 Linux 来说,这点体积换帧率是划算的。
3.2 编译命令与目录结构
解压后先跑一次全量编译,验证工具链没断:
cd face_detect_c_code chmod +x build.sh ./build.sh ls -lh build/ # 输出里应看到 face_detect_c 和 face_detect_cpp 两个可执行文件如果pkg-config找不到 OpenCV,属于环境配置问题,不是代码问题。定位手段是三条命令:
pkg-config --modversion opencv4 # 看版本 find /usr -name "opencv4" -type d 2>/dev/null # 找头文件目录 ldconfig -p | grep libopencv_core # 看动态库是否注册得到头文件路径后,可以在 build.sh 里直接写死,例如CXXFLAGS="-I/usr/local/include/opencv4"。动态库路径如果不对,编译会报fatal error: opencv2/opencv.hpp: No such file or directory,这时去/usr/local/lib下确认存在libopencv_core.so,并把该目录加进LD_LIBRARY_PATH。这些都是老问题,但每个新手都会撞一次,加个环境变量就好了,不用重装 OpenCV。
编译成功后,build目录里还会生成一个preprocess工具,用来把摄像头帧转成灰度图并保存。这个工具我一般用来做输入排查:先截一帧,打开看灰度和对比度是否正常,再决定要不要改直方图均衡化参数。很多检测不到人脸的问题,根源不在算法,而在输入图像本身太暗或太亮。
3.3 命令行参数与默认值解析
程序运行起来需要喂的最小参数是输入源。资源包支持三种:-c /dev/video0表示摄像头设备,-v video.mp4表示视频文件,-n 0表示 OpenCV 的摄像头编号。其他参数都有默认值:
| 参数 | 默认值 | 说明 |
|---|---|---|
--width/--height | 1280 / 720 | 摄像头采集分辨率 |
--scale | 0.5 | 处理帧缩放比例 |
--roi | 0,0,1280,720 | 只检测指定区域 |
--skip | 0 | 每 N 帧检测一次 |
--min-size | 40 | 最小人脸尺寸 |
--model | model/haarcascade_frontalface_alt2.xml | 模型路径 |
--out | null | 保存结果视频 |
一条典型启动命令:
./build/face_detect_cpp -c /dev/video0 --width 640 --height 480 --scale 0.5 \ --min-size 60 --skip 1 --roi 0,80,640,400--roi是我最常调的参数。摄像头如果是固定机位,人脸只会出现在画面中下区域,把上方天空和背景裁掉,检测耗时能降 30% 以上。--skip 1的含义是每隔一帧检测一次,中间那帧直接用上一帧的结果画框,这个技巧在下一章会展开。这些参数全部在config.h里声明为全局变量,写死在代码里也行,但命令行传参能让你在评测不同阈值时不重新编译,性能调优期特别好用。
运行时会打印每帧的耗时和当前 FPS:
[INFO] frame=128, faces=1, detect_ms=32.4, total_ms=41.8, fps=23.9 [INFO] frame=129, faces=1, detect_ms=5.2, total_ms=35.1, fps=28.5第一行显示的是正常检测帧,第二行是被 skip 的帧,detect_ms 很低。如果你看到连续几行 detect_ms 都超过 50ms,说明当前参数对处理分辨率来说太重了,优先调--scale而不是改算法。
4. 调优实时检测:帧率、ROI 与多线程
4.1 帧率上不去的三个原因
先说最容易被忽略的:分辨率。很多人把摄像头设为 1920×1080,然后抱怨 Haar 检测慢。实时检测的思路是“采集高、处理低、显示高”。1280×720 的原始帧供显示和保存,内部处理缩小到 640×360 甚至 320×240,检测耗时跟像素数近似成正比,缩到四分之一像素量,窗口扫描次数大幅减少,这是性价比最高的一步。
第二个原因是逐帧检测。detectMultiScale 在 640×360 帧上通常要 20 到 40ms,摄像头 30FPS 的帧间隔是 33ms,理论上刚够,但加上采集、灰度转换、显示,单线程总耗时往往会超过 50ms,帧率会掉到 20 以下。解决思路是跳帧检测:奇数帧跑检测,偶数帧只把上一帧的矩形覆盖上去,帧率立刻能回到 28FPS 左右。代价是检测框滞后一帧,人快速转头时框子跟不上,但对门禁、监控、考勤这类场景完全够用。
第三个原因是矩形过多。minNeighbors 调太低时,一张脸周围会冒出三四个重叠框,增加绘制开销,更严重的是这些冗余框如果送进跟踪器,会让状态更新剧烈抖动。所以我在资源包里加了一个后处理:先对检测框按面积排序,再按重叠度合并,重叠面积超过 60% 的两个框合并成一个,最多保留 10 个框。这个逻辑放在post_process.c,代码很短,但对输出质量提升非常明显。
4.2 ROI 裁剪与缩放策略
ROI 不是玄学,它直接决定检测器的工作范围。固定摄像头场景下,把画面顶端 20% 的天空、底部 10% 的桌面裁掉,既减少了扫描行数,又避免了把风扇、灭火器认成人脸的尴尬。代码实现里不用新建图像,直接用cv::Mat roi = gray(cv::Rect(x, y, w, h)),ROI 只是原图的一个视图,内存零复制,检测完把坐标加回偏移量即可:
cv::Rect roi(0, 80, proc.cols, proc.rows - 100); cv::Mat gray_roi = gray(roi); std::vector<cv::Rect> faces; cascade.detectMultiScale(gray_roi, faces, scale_factor, min_neighbors, 0, cv::Size(40, 40), cv::Size(300, 300)); for (auto& r : faces) { r.x += roi.x; r.y += roi.y; // 坐标映射回原图 }这里有个隐蔽的边界问题:如果 ROI 的宽高小于minSize,检测器会直接返回空向量,不报错也不提示。另一个常见错误是忘了做r.x += roi.x的坐标补偿,结果框子全部画偏,排查起来很费时间。我的习惯是每帧检测前打印一次 ROI 值,确认阈值生效,稳定后再去掉日志。
缩放策略上,scale 不是越小越好。0.25 倍缩放(1280×720 到 320×180)虽然快,但一张 40×40 的人脸被缩到 10×10,低于 minSize 后直接漏检。我一般让缩放后的人脸尺寸保持在 20 像素以上,也就是 0.5 倍缩放加 minSize=40 的组合,在多数室内监控位置表现均衡。如果你检测的是远景行人脸,还要再往上提 minSize,否则远处的小脸会带进来大量背景误检。
4.3 多线程与双缓冲实现
单线程循环做“采集→检测→绘制”时,检测耗时直接卡住采集,摄像头内部缓冲区会丢帧。把采集和检测拆成两个线程,中间用双缓冲衔接,是最经典的做法:
#define BUF_NUM 2 // 生产者线程:持续读帧到 buf[p],并通知消费者 void* capture_thread(void* arg) { while (running) { read_frame(fd, &buf[p]); p = (p + 1) % BUF_NUM; usleep(2000); } } // 消费者线程:从另一个缓冲取帧做 Haar 检测 void* detect_thread(void* arg) { while (running) { if (p != c) { haar_detect(buf[c]); c = (c + 1) % BUF_NUM; } } }这个简化版的同步逻辑不够严谨,但能看出核心思路:p 和 c 分别记录生产者和消费者位置,两者不相等说明有新帧。真正的代码里要加互斥锁保护这两个变量,否则 CPU 缓存不一致会导致偶发看到同一帧两次。资源包里的 sync.h 把这个逻辑封装成了 frame_queue,我建议队列深度不要超过 2,深度越大,延迟越大,人脸框会明显滞后于画面。
真正让我在性能调优期栽跟头的是显示这一环。imshow 和 waitKey 是阻塞的,waitKey(1) 看起来只等 1ms,实际内部会连带窗口事件处理,在 Linux 上处理慢的时候能卡 10ms 以上。如果没有显示器需求,直接注释掉 imshow,只保留检测和输出,帧率能提高一大截。测算法性能的时候别开着窗口看,那是给自己增加干扰。另外线程优先级可以调整:采集线程设高优先级,检测线程设普通优先级,让摄像头缓冲区尽量不被占满,可有效减少帧间隔抖动。优先级设置在 Linux 下要用pthread_setschedparam,需要 root 权限,普通用户跑不起来,这个限制在文档里没写,我踩了才发现。
5. 避坑与常见问题排查:实测过的 5 个坑
5.1 现象:检测框大量误报,把衣服、玩偶认成人脸
现象:对着白墙或穿纯色衣服的人,画面上冒出五六个绿框,框还不完全重叠。
原因:大多数是 scaleFactor=1.1 加 minNeighbors=1 的组合太敏感。Haar 分类器本身存在一定误检率,背景纹理稍丰富就会触发;普通摄像头的自动增益会让某几帧偏亮,误检进一步放大。
解决:先把 minNeighbors 提到 4 或 5,再把 scaleFactor 调到 1.15,误检能下降一半以上。如果还有,说明背景里存在高亮或高频纹理的区域,开启肤色预筛模式,把肤色阈值收紧到 Cr [140,175]、Cb [90,130],可以滤掉一部分非肤色背景。我调试时习惯打开debug_show_candidates开关,先看肤色分割输出,如果肤色候选框本身就离谱,调肤色阈值;如果肤色框对但 Haar 最终框偏了,调 minNeighbors。还有一次误检源头是摄像头自动对焦没关,背景沙发纹理在失焦状态下产生高频噪声,Haar 把噪声当成特征,关掉自动对焦后现象消失。
5.2 现象:CPU 占用高,风扇狂转
现象:四核 i7 跑 720P 输入,CPU 占用超过 150%,风扇声音明显。
原因:负载主要不在 detectMultiScale,而在每帧的 cvtColor 和 resize 没有用 SIMD 优化,也没有先缩小处理面。另一个隐藏点是编译参数用-O0或-O2,Haar 特征计算的循环展开不足,速度差异能到 20%。
解决:编译参数改成-O3 -march=native,让编译器针对当前 CPU 生成指令集。同时确认输入帧先 resize 到 640×360 再做直方图均衡和检测。用getCPUTickCount打印每段耗时,先看哪一段最大再动刀,不要盲目换算法。有一次我把--scale 0.5错写成 2.0,图像被放大四倍,CPU 直接拉满,这种参数方向反了的问题值得写进自查清单。另外 OpenCV 的imshow窗口在开启状态下会频繁触发 GUI 事件,占用不少 CPU,嵌入式环境直接关掉预览。
5.3 现象:画面卡顿,帧率只有 8FPS
现象:摄像头画面像幻灯片,人脸框一波一波地跳。
原因:不是检测慢,是采集和显示互相锁死。单线程模型下 imshow 阻塞消耗大量时间;或者摄像头设置的像素格式是 YUYV,而代码里按 RGB 解析,导致每个通道都做了无意义的转换。V4L2 的 mmap 缓冲如果只申请了一帧,采集线程会被 read 卡住,表现为帧率上不去。
解决:切换到双缓冲采集,VIDIOC_REQBUFS至少申请 4 个缓冲区;显示线程独立出来或直接关闭预览。如果摄像头支持 MJPG 格式,用VIDIOC_S_FMT把像素格式切到V4L2_PIX_FMT_MJPEG,数据量能降一半,1080P 才能跑得动。还有一次调试时开着录屏软件又开预览窗口,本地 USB 摄像头带宽不够,帧率自然上不去,断开预览后一切正常。这类问题排查顺序是:先关掉所有无关的取流进程,再看像素格式,最后才怀疑检测参数。
5.4 现象:检测不到人脸
现象:画面里有人,但一个框都没有。
原因:最大嫌疑是 minSize 设得太大。当人脸在画面里只有 30×30 像素时,minSize=80 会把所有候选窗口全部筛掉。另一个是模型路径问题,加载失败时 OpenCV 会打一行警告,但程序不退出,调用 detectMultiScale 后返回空向量。
解决:先用--debug参数显示灰度图,估算人脸的像素尺寸。实测人脸只有 40 像素,就把 minSize 降到 30 并接受一定误检。检查模型是否真的加载成功,代码里务必加判断:
if (cascade.empty()) { fprintf(stderr, "model load fail: %s\n", model_path); return -1; }还有就是 Haar 模型对角度敏感,正脸和轻微偏转有效,超过 30 度的侧脸基本失效。这时要么要求用户看向镜头,要么换带旋转补偿的检测策略。我见过一位熟手排查了半天,最后发现是--model参数被单引号扩起来,路径里有空格,加载失败又被吞掉,这类问题不打印返回值根本查不出来。
5.5 现象:断断续续,框忽大忽小
现象:人脸框在同一张脸上来回缩放,甚至出现两个框轮流亮。
原因:检测器对每帧独立输出,没有历史信息;人离摄像头远近变化时,检测窗口尺寸跳变;同一个人脸被检测成两个相邻框,代码里没有执行合并。
解决:把 minNeighbors 提到 5,让检测本身的输出更收敛;再把 tracker.c 集成进主循环,用移动平均对框中心和宽高做平滑。新框与旧框重合度大于 70% 时,沿用旧框的参数做加权更新,而不是直接替换。这套逻辑本质上是跟踪预处理,不加它,框抖动在人靠近摄像头时尤其明显。还有一个容易忽略的点:USB 摄像头默认开启连续自动变焦,人脸靠近时焦距变化导致检测尺寸跳变,关掉自动对焦以后,框子立刻变得平稳。
6. 进阶:把检测框送给跟踪:卡尔曼滤波与人脸 ID 分配
6.1 为什么检测不够,要跟踪
检测是对单帧做判断,跟踪是对时间序列做状态估计。人脸快速移动时,跳帧检测的间隔会变成 66ms,此时单纯框平滑已经不够,需要引入预测模型。卡尔曼滤波用上一帧的位置和速度预测当前帧位置,再用检测结果修正,两级互补后,短暂遮挡和漏检也能继续输出框。我一般把策略定为:每 3 帧做一次全帧检测,中间两帧用卡尔曼预测的坐标画框,这样帧率能维持在 25FPS 以上,同时“丢框”的现象被预测掩盖,观感上完全察觉不到检测跳帧。
6.2 卡尔曼滤波的 C 实现要点与代码
用一个六维状态向量跟踪人脸:中心 x、中心 y、面积 s,以及各自的速度。对应代码里把滤波器拆成三个独立的一维滤波器更简单,因为 x、y、s 在短时间内互不耦合。下面是资源包里的 Kalman 结构体与更新函数:
typedef struct { float x, vx; // 位置和速度 float p, q, r; // 误差方差、过程噪声、观测噪声 float k; // 卡尔曼增益 } Kalman1D; float kalman_update(Kalman1D *kf, float z) { // 预测 float x_pred = kf->x + kf->vx; float p_pred = kf->p + kf->q; // 更新 kf->k = p_pred / (p_pred + kf->r); kf->x = x_pred + kf->k * (z - x_pred); kf->vx += kf->k * (z - x_pred); kf->p = (1 - kf->k) * p_pred; return kf->x; }参数 q 控制对运动模型的信任程度,r 控制对观测值的信任程度。人脸匀速移动时,q=0.01, r=0.5效果好;快速甩头时把 q 调到 0.1,让预测更激进,否则框子会粘在人脸后面。这个参数需要实测调整,我一般先在日志里记录预测值和实测值的偏差,偏差大于 20 像素就增大 q,直到偏差均值回到 5 像素以内。
把 kalman_update 接入主循环的做法是:每帧先跑一次预测得到预测坐标,如果检测线程返回了新的人脸框,就用距离最近的那个框的中心和面积去更新滤波器;如果没有检测结果,直接输出预测值。这个“预测优先、检测修正”的模式,让跳过检测的帧依然有框输出,而且框子的运动轨迹比纯检测结果平滑得多。
6.3 结果保存与串口输出
跟踪不只是画框。资源包里有一个on_detect回调,每当某个人脸连续出现超过 20 帧,就把它写入 CSV 或者通过串口输出。我给某跨平台系统做原型时,这个输出直接接到下位机的 UART,波特率 115200,每帧输出一次ID,x,y,w,h,下位机据此控制云台跟随人脸。串口发送要加环形缓冲,否则 write 在系统忙时会阻塞主线程,这个坑我改了三版才确定不是数据格式问题。
从那以后我每次跑检测都强制走一遍完整路径:先查参数表,再用--debug确认输入清晰度,最后才谈优化。每次准备动手改算法之前,先问自己一句“输入对不对”,能少走无数弯路。希望这些整理能帮到你,代码拿回去从编译命令开始跑,有问题在参数和 ROI 上找答案,大概率比改模型要快得多。
本文还有配套的精品资源,点击获取