4. 经典跟踪算法(二):连续自适应均值漂移(CamShift)算法原理、与Mean Shift的区别、应用场景
4.1 从Mean Shift到CamShift:我踩过的坑
说起CamShift,我得先聊聊Mean Shift。很多教材喜欢把CamShift说成是Mean Shift的“升级版”,这个说法没错,但不够具体。
我当年第一次用Mean Shift做目标跟踪时,遇到一个很头疼的问题:目标在画面里忽大忽小。比如一个人朝镜头走来,他的脸在画面中会逐渐变大。Mean Shift的搜索窗口是固定大小的,目标一变大,窗口就框不住了,跟踪直接漂移。
后来我查资料才发现,CamShift就是专门解决这个问题的。它全称是Continuously Adaptive Mean Shift,翻译过来就是“连续自适应均值漂移”。说白了,它能在每一帧里自动调整搜索窗口的大小和方向。
嗯,这里要注意:CamShift不是凭空造出来的新算法,它是在Mean Shift的基础上加了一个“自适应”的步骤。这个步骤很关键,我后面会细讲。
4.2 CamShift的核心原理
CamShift的流程,我习惯用四个步骤来概括:
- 初始化:选定目标区域,计算其颜色直方图作为模板
- 反向投影:将当前帧转换为概率图,每个像素值代表它属于目标的概率
- Mean Shift迭代:在概率图上用Mean Shift找到目标的新位置
- 自适应调整:根据概率图的二阶矩,更新搜索窗口的大小和方向
你想想看,第3步和第4步是循环执行的。每一帧里,CamShift会反复做Mean Shift迭代,直到收敛,然后根据收敛后的概率分布重新计算窗口尺寸。这就是“连续自适应”的含义。
核心公式(二阶矩计算窗口尺寸):
M20 = ∑∑ x² * I(x,y)
M02 = ∑∑ y² * I(x,y)
M11 = ∑∑ x * y * I(x,y)
窗口长轴 a = √((M20 + M02 + √((M20 - M02)² + 4*M11²)) / 2)
窗口短轴 b = √((M20 + M02 - √((M20 - M02)² + 4*M11²)) / 2)
窗口方向 θ = 0.5 * arctan(2*M11 / (M20 - M02))
这些公式看着复杂,其实逻辑很简单:用概率图的二阶矩来拟合一个椭圆,这个椭圆的长轴、短轴和方向,就是新的搜索窗口。
4.3 CamShift与Mean Shift的区别
我整理了一个表格,方便你对比:
| 对比维度 | Mean Shift | CamShift |
|---|---|---|
| 窗口大小 | 固定不变 | 每帧自适应调整 |
| 窗口方向 | 始终水平 | 可旋转(拟合椭圆) |
| 迭代方式 | 每帧只做一次Mean Shift | 每帧多次迭代直到收敛 |
| 适用场景 | 目标尺度变化小 | 目标尺度变化大 |
| 计算开销 | 较低 | 稍高(但可接受) |
我曾经在一个项目中用Mean Shift跟踪行人,目标走远时窗口太大,引入了大量背景噪声;走近时窗口太小,只框住了目标的一部分。换成CamShift后,窗口能自动缩放到合适大小,跟踪稳定性提升了一个档次。
我的经验:CamShift的窗口自适应不是万能的。如果目标快速形变(比如奔跑的人),窗口调整会滞后。这时候可以适当降低自适应更新的学习率,让窗口变化更平滑。
4.4 应用场景
CamShift最经典的应用场景有三个:
- 人脸跟踪:人脸颜色分布相对稳定,且尺度变化频繁(走近走远)。我做过一个会议摄像头自动跟踪演讲者的Demo,用的就是CamShift,效果还不错。
- 手势跟踪:手部肤色在HSV空间的色调分量比较集中,适合用颜色直方图建模。CamShift能适应手部在画面中的大小变化。
- 运动目标跟踪:如果目标颜色特征明显(比如红色球衣的运动员),CamShift可以快速跟踪,计算量小,适合嵌入式设备。
避坑指南:我曾经在一个户外监控项目里用CamShift跟踪车辆,结果翻车了。原因是车辆颜色和路面颜色太接近,反向投影图几乎全是噪声。CamShift对颜色区分度要求很高,背景和目标颜色相近时,效果会急剧下降。
4.5 知识体系结构图
下面这张图展示了CamShift在整个目标跟踪知识体系中的位置,以及它内部的核心逻辑:
4.6 代码实战:OpenCV中的CamShift
OpenCV已经封装好了CamShift,用起来很方便。我贴一段核心代码,注释写得很详细:
import cv2
import numpy as np
# 初始化:选定目标区域
cap = cv2.VideoCapture('test.mp4')
ret, frame = cap.read()
x, y, w, h = 100, 100, 80, 80 # 手动选定目标框
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)
# 设置终止条件:迭代10次或移动距离小于1像素
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()
调参建议:term_crit中的迭代次数和最小移动距离需要根据帧率调整。30fps的视频,迭代5-8次就够了;低帧率视频可以适当增加迭代次数。我习惯设成(10, 1),大部分场景都能用。
4.7 局限性
CamShift不是银弹。我总结了几点局限性:
- 颜色敏感:背景和目标颜色相近时,反向投影图会失效
- 光照敏感:光照变化会导致颜色直方图偏移,需要定期更新模板
- 快速运动:目标移动速度过快时,窗口可能跟不上
- 遮挡问题:目标被部分遮挡时,概率图会变得不可靠
我曾经在一个仓库监控项目里用CamShift跟踪叉车,结果叉车经过阴影区域时直接跟丢了。后来我加了光照补偿和模板更新策略,才勉强能用。所以,CamShift适合做入门级的跟踪,或者作为复杂跟踪系统的一个组件。
好了,CamShift的核心内容就这些。它不算复杂,但“自适应窗口”这个思想很实用。你可以在自己的项目里试试,看看效果如何。
可可精品资料库