首页
/ OpenCV 视频目标跟踪实战:Meanshift 与 Camshift 算法原理、源码解析与代码实现

OpenCV 视频目标跟踪实战:Meanshift 与 Camshift 算法原理、源码解析与代码实现

2026-09-07 11:51:55作者:魏侃纯Zoe

本文以 OpenCV 官方教程 doc/tutorials/others/meanshift.markdown 为主体脉络,系统讲解在视频序列中基于直方图反投影跟踪运动物体的 MeanshiftCamshift(Continuously Adaptive Meanshift,连续自适应均值漂移) 两种经典算法。文章不仅给出可直接运行的 C++/Python 示例,还深入 video 模块源码(modules/video/src/camshift.cpp)剖析二者的迭代收敛细节,帮助你既能跑通官方 Demo,又能理解 OpenCV 内部究竟在做什么。

读完本文,你将掌握:目标颜色直方图的建立与直方图反投影流程、cv.meanShift()cv.CamShift() 的完整调用链与参数语义、固定窗口跟踪与自适应旋转窗口跟踪的差异,以及如何把结果绘制到画面中形成实时跟踪效果。


一、Meanshift:把窗口挪向像素密度最大的地方

1.1 直觉与原理

Meanshift(均值漂移)的核心思想非常直观:假设你面对一组数据点(例如一幅直方图反投影图上的像素分布),并已经给定一个小窗口(通常视为圆形或矩形),任务就是把这个窗口移动到"像素密度最大"(即点数最集中)的区域。

迭代过程如下:

  1. 以初始窗口中心(记为 C1_o)圈定窗口内的所有点;
  2. 计算窗口内所有点的质心(centroid,记为 C1_r),它通常与窗口中心并不重合;
  3. 把窗口中心移动到刚才算出的质心位置,得到新窗口;
  4. 再次计算新窗口内的质心……如此反复迭代;
  5. 当窗口中心与其内部质心重合(或偏差小于允许误差)时停止——此时窗口便落在了像素分布最密集的位置(下图中的绿色圆 C2)。

该过程可用下面的示意图直观表达(原图出自官方教程插图):

Meanshift 窗口向最大像素密度区域收敛的示意图,蓝色窗口为迭代初始位置,绿色为收敛后的最终位置

在视频跟踪场景中,通常把直方图反投影图像目标的初始位置窗口作为输入。当物体移动时,运动会立即反映在反投影图像的变化上,Meanshift 因而能把窗口推向新的最大密度区域,实现逐帧跟踪。

从源码层面看,OpenCV 的 meanShift 正是"不断把窗口中心平移至其内部质心"这一过程的直接实现(见 modules/video/src/camshift.cpp#L44-L107):

  • 对窗口覆盖的子图像调用 moments() 计算零阶矩 m00、一阶矩 m10/m01
  • 得到质心偏移量 dx = m10/m00 - window.width*0.5dy = m01/m00 - window.height*0.5
  • 若质心与窗口中心的偏移平方和 dx*dx + dy*dy < eps 则提前终止,否则窗口中心平移后进入下一轮迭代;
  • 返回实际执行的迭代次数 i

需要说明的是,meanShift 只允许在单通道反投影图上运行(CV_Assert(cn == 1)),且要求窗口宽高为正数,否则会抛出参数错误。其中终止条件 eps 的语义值得注意:源码将 criteria.epsiloncvRound(eps*eps) 后用于判断"窗口中心位移平方"是否足够小——这正是官方示例把 TermCriteria 阈值设为 1(像素)即可取得良好效果的原因。

1.2 目标建模三件套:ROI、色相直方图与反投影

在 OpenCV 中使用 Meanshift 前,需要完成三步准备工作:

  1. 框选目标 ROI:从视频首帧手动给定初始跟踪窗口 (x, y, w, h)
  2. 统计目标色相直方图:把 ROI 转到 HSV 色彩空间,只取 H(Hue,色相)通道统计直方图,这样对光照强度变化相对鲁棒;
  3. 过滤低饱和度/低亮度像素:为规避暗光下颜色值失真,用 cv.inRange() 把低饱和、低亮度的像素置 0,只让"有颜色"的像素参与直方图统计。

官方示例中采用的色相过滤区间为 S ∈ [60, 255]V ∈ [32, 255](Python 示例见 samples/python/tutorial_code/video/meanshift/meanshift.py,C++ 对应版本见 samples/cpp/tutorial_code/video/meanshift/meanshift.cpp)。

完成 ROI 直方图建模后,每一帧的处理流程固定为:

  1. 当前帧转 HSV → 仅取 H 通道 → calcBackProject() 生成反投影图 dst(像素值越大表示越像目标);
  2. cv.meanShift(dst, track_window, term_crit) 获得新窗口位置;
  3. 绘制矩形框并显示。

下面的完整 Python 实现可以直接运行(需要传入一段交通视频,例如 slow_traffic_small.mp4):

import numpy as np
import cv2 as cv
import argparse

parser = argparse.ArgumentParser(description='This sample demonstrates the meanshift algorithm.')
parser.add_argument('image', type=str, help='path to image file')
args = parser.parse_args()

cap = cv.VideoCapture(args.image)

# take first frame of the video
ret, frame = cap.read()

# setup initial location of window
x, y, w, h = 300, 200, 100, 50  # simply hardcoded the values
track_window = (x, y, w, h)

# set up the ROI for tracking
roi = frame[y:y+h, x:x+w]
hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV)
mask = cv.inRange(hsv_roi, np.array((0., 60., 32.)), np.array((180., 255., 255.)))
roi_hist = cv.calcHist([hsv_roi], [0], mask, [180], [0, 180])
cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX)

# Setup the termination criteria, either 10 iteration or move by at least 1 pt
term_crit = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1)

while True:
    ret, frame = cap.read()
    if not ret:
        break
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
    dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)

    # apply meanshift to get the new location
    ret, track_window = cv.meanShift(dst, track_window, term_crit)

    # Draw it on image
    x, y, w, h = track_window
    img2 = cv.rectangle(frame, (x, y), (x + w, y + h), 255, 2)
    cv.imshow('img2', img2)

    k = cv.waitKey(30) & 0xff
    if k == 27:  # Esc 退出
        break

cv.destroyAllWindows()

C++ 版本结构完全相同,只是需要显式声明 Rect track_window(300, 200, 100, 50)TermCriteria term_crit(TermCriteria::EPS | TermCriteria::COUNT, 10, 1),并自行管理 calcHist/calcBackProject 的通道与区间参数(见 samples/cpp/tutorial_code/video/meanshift/meanshift.cpp#L45-L82)。Java 版本可参考 samples/java/tutorial_code/video/meanshift/MeanshiftDemo.java

官方示例在真实交通视频上的跟踪结果如下,矩形框会持续框住目标车辆:

Meanshift 在交通视频中跟踪目标车辆的三帧结果截图

运行提示:示例中 (x, y, w, h) = (300, 200, 100, 50) 是硬编码的初值。换用别的视频时,需要按目标实际位置重新框选,否则第一帧直方图建的是背景而非目标。


二、Camshift:让窗口大小与旋转跟着目标变

2.1 Meanshift 的局限

仔细看上面的结果会发现一个问题:无论汽车离镜头是远还是近,跟踪窗口的大小始终不变。目标靠近时窗口偏大、远离时窗口偏小,固定尺寸显然不是好的跟踪形态。理想的窗口应当随目标的尺度变化旋转角度自适应调整。

2.2 算法由来与更新规则

这一问题的解决方案出自 "OpenCV Labs",即 CAMshift(Continuously Adaptive Meanshift),由 Gary Bradsky 在 1998 年发表的论文 "Computer Vision Face Tracking for Use in a Perceptual User Interface" 中提出。

Camshift 的工作方式可以概括为三步循环:

  1. 先执行一次 Meanshift,使窗口收敛到目标的质心位置;
  2. 按公式

    s=2×M00256s = 2 \times \sqrt{\frac{M_{00}}{256}}

    依据窗口内反投影像素的零阶矩 M00 更新窗口尺寸,同时计算能够最佳拟合目标形状的旋转外接椭圆的方向;
  3. 用新尺度的搜索窗口 + 上一轮窗口位置再次执行 Meanshift……如此反复,直到达到所需精度。

这样得到的就不再是固定矩形,而是一个带旋转角度、可缩放的目标框。

从实现上看,OpenCV 的 cv::CamShift(见 modules/video/src/camshift.cpp#L110-L218)完整复现了上述思路:

  • 首先调用内部 meanShift() 收敛到质心(window 参数在此函数中会以输入输出方式被就地更新);
  • TOLERANCE = 10 像素对窗口做外扩后再计算二阶矩 mu11/mu20/mu02,进而用 atan2(2b, a-c+sqrt(...)) 求解椭圆主轴方向角 theta
  • 通过把二阶矩旋转到主轴坐标系,计算出椭圆的长半轴对应长度 length = sqrt(rotate_a/m00)*4 与短轴 width,并处理了 theta 为 0° 或约 90° 时长短轴互换的边界情形;
  • 返回一个 RotatedRect box,其成员包含目标中心 center、尺寸 size 与角度 angle(角度值最终规整到 [0, 180) 区间),随后调用方可用 RotatedRect::boundingRect() 得到下一轮所需的搜索窗口(该约定在 modules/video/include/opencv2/video/tracking.hpp#L64-L83 的 API 文档中有明确说明)。

下图为 Camshift 跟踪人脸时的收敛示意动画帧(出自官方教程插图):

Camshift 自适应调整窗口大小与方向以跟踪人脸的示意图

2.3 Camshift 在 OpenCV 中的使用

Camshift 的使用与 Meanshift 几乎相同——目标建模、直方图反投影、终止条件全部复用——唯一区别在于跟踪函数:

  • cv.meanShift() 返回窗口位置,绘制轴对齐矩形
  • cv.CamShift() 返回 RotatedRect(旋转矩形),绘制的是带倾斜角度的四边形外框,而窗口参数(track_window)仍由函数就地更新,可直接传给下一帧。

完整的 Python 示例见 samples/python/tutorial_code/video/meanshift/camshift.py,核心循环如下:

while True:
    ret, frame = cap.read()
    if not ret:
        break
    hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
    dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)

    # apply camshift to get the new location
    ret, track_window = cv.CamShift(dst, track_window, term_crit)

    # Draw rotated rectangle on image via its 4 corner points
    pts = cv.boxPoints(ret)
    pts = np.int0(pts)
    img2 = cv.polylines(frame, [pts], True, 255, 2)
    cv.imshow('img2', img2)

    k = cv.waitKey(30) & 0xff
    if k == 27:
        break

要点解读:

  • roi_hist 的建模代码与 Meanshift 版本完全一致(H 通道 180 级直方图 + NORM_MINMAX 归一化到 [0,255]);
  • cv.CamShift() 的返回值 retRotatedRect,其中 ret[0]/ret.center 为中心点、ret[1]/ret.size(宽, 高)ret[2]/ret.angle 为旋转角;
  • 绘制时不能直接用 rectangle,而应通过 cv.boxPoints(ret) 取出旋转矩形的 4 个角点,再用 polylines 连线成框。

C++ 对应示例在 samples/cpp/tutorial_code/video/meanshift/camshift.cpp#L64-L85 中,用 RotatedRect::points() 取出 4 个角点后逐边连线绘制;Java 版本见 samples/java/tutorial_code/video/meanshift/CamshiftDemo.java

Camshift 在交通视频上的实际跟踪效果如下,可以看到框随汽车尺度变化:

Camshift 跟踪结果三帧,旋转外接框随目标尺度与姿态自适应变化

提示:官方还额外提供了一个可交互的 Python 演示 camshift.py(位于 samples/python/snippets/camshift.py),允许用鼠标实时框选目标后观看跟踪过程,非常适合边改边理解算法行为。对应 C++ 交互版本可见 samples/cpp/snippets/camshiftdemo.cpp


三、两算法对比与适用边界

维度 Meanshift Camshift
返回结果 迭代后窗口 Rect(位置 + 固定宽高),及迭代次数 RotatedRect(中心 + 尺寸 + 旋转角),window 就地更新供下轮使用
窗口形态 轴对齐、尺寸恒定 旋转外接椭圆/矩形,尺寸与角度自适应
窗口更新 每轮只平移窗口中心到质心 收敛后再用零阶矩 M00 重算尺寸,并外扩 TOLERANCE=10 后重新估计
适用场景 目标尺度基本不变的粗跟踪 目标远近变化明显、存在姿态旋转的跟踪
源码位置 modules/video/src/camshift.cppcv::meanShift() 同文件中 cv::CamShift()(内部复用 meanShift
公共 API 声明 modules/video/include/opencv2/video/tracking.hpp modules/video/include/opencv2/video/tracking.hpp

需要如实说明的工程边界:

  • 两类算法都基于颜色直方图反投影,本质假设是"目标颜色显著区别于背景"。当目标与背景颜色接近、目标颜色剧烈变化,或存在大面积遮挡时,跟踪容易漂移,这是直方图外观模型的固有局限;
  • meanShiftCamShift 均为轻量级、CPU 友好的经典跟踪器,适合实时场景,但面对快速运动、形变剧烈的目标不如现代的检测式/判别式跟踪器鲁棒;
  • 官方教程在 doc/tutorials/others/table_of_content_other.markdown 中将其与背景减除、光流等内容并列,可结合背景减除(tutorial_background_subtraction)与光流(tutorial_optical_flow)对比不同跟踪思路的取舍。Bradski 1998 年的原始论文可作为进一步研究 Camshift 数学推导的参考文献。

四、小结与下一步建议

doc/tutorials/others/meanshift.markdown 为纲,可以快速搭起一套"目标建模 → 逐帧反投影 → 漂移搜索/自适应搜索"的完整链路:

  1. 取首帧 ROI,在 HSV 空间用 inRange 滤除低饱和低亮像素后统计 H 通道直方图并归一化;
  2. 每帧 cvtColor + calcBackProject 得到概率图;
  3. 需要固定框用 cv.meanShift,需要自适应旋转框用 cv.CamShift,终止条件统一交给 TERM_CRITERIA_EPS | TERM_CRITERIA_COUNT(迭代上限 10、位移 ≤1 像素);
  4. 读一遍 modules/video/src/camshift.cpp 对应实现,把"质心迭代 + 二阶矩求椭圆"的原理落实到代码行。

延伸练习:动手修改官方 camshift 演示代码(samples/python/snippets/camshift.py),尝试调整 TERM_CRITERIA 中的最大迭代次数与位移阈值、inRange 的 S/V 下界,以及直方图通道(例如改用 H+S 两通道建模),观察各自对跟踪稳定性与漂移速度的影响——这是理解参数语义最直接的方式。

登录后查看全文
热门项目推荐
相关项目推荐