4. 经典跟踪算法(二):连续自适应均值漂移(CAMShift)算法
上一节我们聊了 Mean Shift,说白了它就是个「找密度中心」的算法。但有个问题——目标在运动过程中,大小会变啊!你想想看,一个人朝你走过来,他在画面里会越来越大。如果跟踪窗口固定不变,那肯定要出问题。
CAMShift 就是来解决这个问题的。全称是 Continuously Adaptive Mean Shift,连续自适应均值漂移。我当年第一次在项目中用这个算法时,心里还嘀咕:这不就是 Mean Shift 加了个自适应窗口吗?后来踩了不少坑才明白,这个「自适应」三个字,学问大着呢。
4.1 从 Mean Shift 到 CAMShift
先说说核心区别。Mean Shift 的窗口大小是固定的,你初始化时设多大,它就多大。CAMShift 呢?每一帧都会根据目标的「形状」重新计算窗口大小和方向。
怎么做到的?靠的是二阶矩。嗯,这里我稍微展开一下。
CAMShift 的工作流程是这样的:
- 用 Mean Shift 找到当前帧的目标中心位置
- 计算目标区域的二阶矩(说白了就是统计像素分布)
- 根据二阶矩算出新的窗口大小和旋转角度
- 用新窗口作为下一帧的初始窗口
你看,每一帧都在「自适应」。我刚开始做的时候觉得这步可有可无,直到有一次跟踪一辆转弯的车——固定窗口直接跟丢了,CAMShift 却稳稳地锁住了车身的朝向。
4.2 核心公式:二阶矩与窗口更新
这里我直接给出计算方式,不绕弯子。
假设目标区域的概率图是 \(I(x,y)\),那么二阶矩就是:
M20 = Σ Σ x² * I(x,y)
M02 = Σ Σ y² * I(x,y)
M11 = Σ Σ x * y * I(x,y)
然后我们可以算出窗口的长轴、短轴和方向角:
a = M20 / M00 - xc²
b = 2 * (M11 / M00 - xc * yc)
c = M02 / M00 - yc²
长轴 = sqrt( (a+c) + sqrt(b² + (a-c)²) ) / 2
短轴 = sqrt( (a+c) - sqrt(b² + (a-c)²) ) / 2
方向角 = 0.5 * atan2(b, a-c)
看着有点复杂对吧?其实说白了,就是在算一个椭圆,让它能最好地「包裹」住目标区域。
重要提醒: 这里的 M00 就是目标区域的零阶矩,也就是所有像素值的总和。如果目标区域太暗或者太亮,M00 会出问题。我建议你在计算前先做归一化。
4.3 CAMShift 与 Mean Shift 的对比
我整理了一个表格,方便你对比:
| 特性 | Mean Shift | CAMShift |
|---|---|---|
| 窗口大小 | 固定 | 自适应变化 |
| 窗口方向 | 始终水平 | 可旋转 |
| 适用场景 | 目标大小基本不变 | 目标有缩放、旋转 |
| 计算量 | 较低 | 稍高(多了一步矩计算) |
| 鲁棒性 | 对尺度变化敏感 | 对尺度变化不敏感 |
我个人习惯是:如果目标在画面中大小变化不超过 20%,用 Mean Shift 就够了。但如果目标会靠近或远离摄像头,或者有旋转,那必须上 CAMShift。
4.4 实战应用:用 OpenCV 实现 CAMShift
OpenCV 里已经封装好了 CAMShift,用起来很方便。我给你写个完整的例子:
import cv2
import numpy as np
# 读取视频
cap = cv2.VideoCapture('test_video.mp4')
ret, frame = cap.read()
# 选择初始跟踪区域
x, y, w, h = cv2.selectROI('Select Target', frame, False)
track_window = (x, y, w, h)
# 提取目标区域的HSV直方图
roi = frame[y:y+h, x:x+w]
hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv_roi, np.array((0., 60., 32.)), np.array((180., 255., 255.)))
roi_hist = cv2.calcHist([hsv_roi], [0], mask, [180], [0, 180])
cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX)
# 设置终止条件
term_crit = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1)
while True:
ret, frame = cap.read()
if not ret:
break
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
dst = cv2.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
# CAMShift 核心调用
ret, track_window = cv2.CamShift(dst, track_window, term_crit)
# 绘制结果
pts = cv2.boxPoints(ret)
pts = np.int0(pts)
img = cv2.polylines(frame, [pts], True, (0, 255, 0), 2)
cv2.imshow('CAMShift Tracking', img)
if cv2.waitKey(30) & 0xFF == 27:
break
cap.release()
cv2.destroyAllWindows()
避坑指南: 我曾经在 HSV 的 range 参数上吃过亏。如果你跟踪的目标颜色偏暗,记得把 S 和 V 的下限调低。默认的 (0., 60., 32.) 对暗色目标不太友好。
4.5 知识体系:CAMShift 核心逻辑
下面我用一张 SVG 图来展示 CAMShift 的完整流程:
4.6 实际项目中的经验总结
我在几个实际项目中用过 CAMShift,这里分享一些经验:
- 颜色空间选择: HSV 比 RGB 好得多。RGB 对光照变化太敏感,而 HSV 的 H 通道基本不受光照影响。
- 直方图更新: 如果目标外观变化不大,可以固定直方图。但如果目标会变色(比如车灯亮起),建议每隔几帧更新一次直方图。
- 遮挡处理: CAMShift 本身不处理遮挡。我一般会加一个置信度判断——如果反向投影的最大值低于某个阈值,就认为目标被遮挡了,暂停更新。
注意: CAMShift 对背景颜色敏感。如果背景颜色和目标颜色相近,跟踪会漂移。我曾经在跟踪一个穿红衣服的人时,背景里出现了一面红旗,结果窗口直接飘到红旗上去了。这种情况建议结合运动信息(比如帧差法)来辅助判断。
好了,CAMShift 的核心内容就这些。说白了,它就是 Mean Shift 的升级版,解决了窗口自适应的问题。虽然现在深度学习跟踪算法很火,但 CAMShift 在简单场景下依然是个轻量级的好选择——不需要 GPU,不需要训练,几行代码就能跑起来。
可可精品资料库