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的流程,我习惯用四个步骤来概括:

  1. 初始化:选定目标区域,计算其颜色直方图作为模板
  2. 反向投影:将当前帧转换为概率图,每个像素值代表它属于目标的概率
  3. Mean Shift迭代:在概率图上用Mean Shift找到目标的新位置
  4. 自适应调整:根据概率图的二阶矩,更新搜索窗口的大小和方向

你想想看,第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在整个目标跟踪知识体系中的位置,以及它内部的核心逻辑:

CamShift 算法知识体系 输入:视频帧 + 初始目标框 预处理:颜色空间转换 (RGB→HSV) 核心迭代流程(每帧执行) ① 计算反向投影图 ② Mean Shift迭代 ③ 自适应调整窗口 收敛? 输出:更新后的目标框 (位置+尺寸+方向) 进入下一帧,重复流程

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的核心内容就这些。它不算复杂,但“自适应窗口”这个思想很实用。你可以在自己的项目里试试,看看效果如何。

可可精品资料库