上一篇做了仪表读数,一个能真正落地的项目。这一篇换个玩法------让程序「盯住」一个运动的东西。目标跟踪是视频分析的第一步,工业现场到处都要用它:传送带上的零件要跟踪、机械臂抓取的目标要锁定。而 OpenCV 里最经典的颜色跟踪算法,就是 CamShift。
一、效果先行
先看跟踪效果。左边第一帧框选了一个绿色圆作为目标,中间是它移动中途,右边最后一帧------红色椭圆框始终死死咬住绿圆,两条轨迹线(绿色真实、红色跟踪)几乎重合,右上角标注平均误差 0.71 像素:


第二张是 CamShift 的「内功」------反向投影概率图(左)和目标的颜色签名(右)。左图暖色代表「颜色像目标」的像素,绿色圆处一个亮斑;右图是目标 H 通道的直方图,尖峰就是它标志性的绿色:

两张图记住两件事:CamShift 的跟踪精度能做到亚像素;它靠的不是形状,是颜色。
二、CamShift 是什么:从 MeanShift 讲起
CamShift 的全称是 Continuously Adaptive MeanShift(连续自适应均值漂移),它是 MeanShift 的升级版。先理解 MeanShift:在一张「概率图」上,从当前窗口出发,反复把窗口中心移到窗口内概率的质心,直到收敛------所以叫「均值漂移」。
MeanShift 只能定位,窗口大小是死的。目标走近变大、走远变小,它就框不准了。CamShift 的改进就在名字里:定位之后,再根据概率的二阶矩自适应调整窗口的大小和角度,返回一个可旋转矩形(RotatedRect)。目标缩放、旋转都能框住。这是它相对 MeanShift 最关键的一步。
三、反向投影:理解 CamShift 的钥匙
CamShift 不找形状,找颜色。整个流程围绕一个核心概念------反向投影(back projection)。
先说正向:从目标框里算出它 H 通道的颜色直方图,这是目标的「颜色签名」------比如一个绿球,签名就是在绿色 H 值处有个尖峰。再说反向:拿这个签名去扫描整幅图,每个像素的 H 值换成「它在签名里出现的频数」,得到一张概率图。亮的地方 = 颜色像目标,暗的地方 = 不像。
CamShift 本质上就是在这张概率图上跑 MeanShift。理解了反向投影,就理解了 CamShift。
四、五步 pipeline
官方 camshiftdemo 依赖摄像头和鼠标框选,我改成了静态合成图加程序化平移(模拟运动),核心五步不变:
cpp
// ① 转 HSV:把色相(H)和亮度/饱和度分离,颜色跟踪只看 H
cvtColor(frame, hsv, COLOR_BGR2HSV);
// ② inRange:饱和度+亮度阈值 → mask,过滤掉低饱和的背景
inRange(hsv, Scalar(0, smin, min(vmin,vmax)),
Scalar(180, 256, max(vmin,vmax)), mask);
// ③ 提取 H 通道(0~180)
extractChannel(hsv, hue, 0);
// ④ 从初始 ROI 建目标颜色签名(只建一次)
calcHist({roi}, {0}, mask_roi, hist, {hsize}, {0,180});
normalize(hist, hist, 0, 255, NORM_MINMAX);
// ⑤ 反向投影得概率图 → CamShift 求质心+自适应窗口
calcBackProject({hue}, {0}, hist, backproj, {0,180}, 1.0);
backproj &= mask;
RotatedRect box = CamShift(backproj, trackWindow,
TermCriteria(TermCriteria::EPS | TermCriteria::COUNT, 10, 1));
每一步环环相扣:mask 过滤背景干扰,颜色签名锁定目标特征,反向投影把「找颜色」变成「找概率峰值」,CamShift 在概率峰值上自适应跟踪。
五、实测对账:亚像素精度
合成场景是深灰背景加一个绿色圆,让它匀速直线运动 30 帧(每帧平移 8px 横向、5px 纵向)。程序化指定初始框,逐帧 CamShift,把「真实轨迹 vs 跟踪轨迹」画出来对账:
- 平均误差 0.707 像素,逐帧误差都在 0.5px 的亚像素精度内
- 因为 CamShift 返回的是 RotatedRect 的中心(浮点坐标),精度天然比整数像素高
用已知运动对账,比肉眼看图靠谱得多------这是本系列一直在用的验证方法。
六、踩坑记录
| 坑 | 现象 | 原因 | 正确姿势 |
|---|---|---|---|
| mixChannels 段错误 | 程序直接崩 | 官方用 Mat* 重载,4.14 有 bug | 用 extractChannel 提取单通道 |
| calcBackProject 编译不过 | expects 6 arguments | vector 重载的 scale 参数无默认值 | 显式传 6 个参数,末尾加 1.0 |
| hconcat 抛异常 | rows 不一致 | 反投影图 640×480 拼 320×200 直方图 | 先 resize 直方图到同高 |
| 红色目标难跟踪 | 直方图裂成两半 | HSV 里红色 H 在 0 和 180 两端 | 示例用绿/蓝,避开 H 环绕 |
最后一条是 CamShift 的固有软肋:它「找颜色」,所以目标撞色、光照剧变导致 H 漂移时就会失效。这是它相对光流、特征点法的取舍------便宜、快、简单,但挑场景。
七、AI 与 LLM Wiki:这一课沉淀了什么
LLM Wiki 现在 20 页了,这一课新增「目标跟踪:CamShift反向投影」概念页,沉淀了 Meanshift→CamShift 的演进逻辑、反向投影的本质、五步 pipeline、以及 0.707px 的实测对账数据。两张截图归档进素材目录,进度表上 camshiftdemo 勾选完成,从 25 个变成 26 个,进度 27%。

这一课最值得进知识库的,是「反向投影」这个思维转换------把「找颜色」变成「找概率峰值」。想跟踪一个绿球,不用教程序「圆长什么样」,而是给它「绿色长什么样」,它在概率图上一钻就找到了。这种「换一个空间解决问题」的思路,比 CamShift 本身的 API 更值钱。
写在最后
97 个实例,今天完成第 26 个,进度 27%。这一课是系列第一次碰「目标跟踪」,CamShift 便宜、快、简单,是颜色跟踪的入门经典,但挑场景。后面特征检测阶段还会见到更强的跟踪方法(光流、特征点匹配),到时候能对比出各自的边界。
下一篇做图像分割------GrabCut 图割和 inpaint 图像修复,一个抠图、一个去水印。到时候见。