4、经典跟踪算法(上):均值漂移(Mean Shift)算法原理、连续自适应均值漂移(CAMShift)算法、光流法(Lucas-Kanade)原理与实现

各位同学,今天我们来啃三块硬骨头:Mean Shift、CAMShift 和光流法。这三兄弟是传统目标跟踪里的经典套路,虽然现在深度学习满天飞,但它们在特定场景下依然能打。我个人习惯把这类算法叫做“轻量级狙击手”——资源消耗低,响应快,但需要你懂它的脾气。

4.1 均值漂移(Mean Shift)算法原理

先说说 Mean Shift。这名字听起来玄乎,说白了就是“找密度中心”。你想象一下:一堆点散落在空间里,算法会不断往点最密集的地方挪动,直到站稳脚跟。用在目标跟踪上,就是拿目标区域的颜色直方图当模板,然后在下一帧里找最像的地方。

核心思想: 用核函数给像素点加权,离中心越近权重越大。然后计算偏移向量,让窗口中心往密度梯度上升的方向移动。

数学上其实不复杂:

1. 计算目标区域的颜色直方图(通常是RGB或HSV的量化)
2. 在候选区域计算相似度(常用Bhattacharyya系数)
3. 用Mean Shift公式更新窗口中心:
   y_new = (Σ x_i * w_i * g(||x_i - y||²)) / (Σ w_i * g(||x_i - y||²))
   其中w_i是权重,g是核函数的导数
4. 迭代直到收敛(移动距离小于阈值)

我在项目中遇到过一个问题:当目标颜色和背景颜色相近时,Mean Shift 很容易漂走。有一次做行人跟踪,那人穿着灰色衣服走在水泥地上,算法直接“叛变”去跟踪地面了。嗯,这里要注意——颜色特征在复杂背景下非常脆弱。

避坑指南: 我曾经在户外光照变化剧烈的场景下用Mean Shift,结果跟踪框疯狂抖动。后来发现是颜色直方图没做光照归一化。建议用HSV空间的H通道,或者做直方图均衡化预处理。

4.2 连续自适应均值漂移(CAMShift)算法

CAMShift 是 Mean Shift 的升级版。它解决了 Mean Shift 的一个大问题:窗口大小固定。你想想看,目标靠近摄像头时会变大,远离时会变小,固定窗口肯定不行。

CAMShift 的做法很聪明:每次迭代后,根据目标的零阶矩(其实就是像素质量分布)来调整窗口尺寸和方向。说白了,就是让窗口能“呼吸”。

实现步骤:

  1. 初始化搜索窗口(位置和大小)
  2. 计算窗口内的颜色概率分布(反向投影)
  3. 执行 Mean Shift 找到新中心
  4. 根据二阶矩计算窗口的宽度、高度和旋转角度
  5. 用新窗口重复步骤2-4

我个人习惯用 CAMShift 做手势跟踪。记得有一次做智能家居项目,用户用手势控制灯光,CAMShift 在 640x480 分辨率下能跑到 30fps,效果相当不错。但有个坑:当目标快速运动时,CAMShift 容易跟丢,因为它每帧只迭代几次,运动幅度大了就追不上。

经验之谈: 如果你用 CAMShift 做实时跟踪,建议把搜索窗口的初始位置用运动预测(比如卡尔曼滤波)来修正。这样即使目标突然加速,也不至于完全丢失。

4.3 光流法(Lucas-Kanade)原理与实现

光流法走的是另一条路。它不依赖颜色,而是靠像素点的运动。核心假设是:同一个点在相邻帧之间,亮度保持不变。然后通过求解光流方程,算出每个像素的移动向量。

Lucas-Kanade 方法(简称LK)做了一个关键简化:假设一个小窗口内所有像素的运动一致。这样就把一个欠定问题变成了超定问题,用最小二乘法求解。

数学形式:

对于窗口内的每个像素,有:
Ix * u + Iy * v + It = 0

写成矩阵形式:
[Ix1 Iy1] [u]   [It1]
[Ix2 Iy2] [v] = [It2]
[... ...]       [...  ]

用最小二乘解:
[u; v] = (A^T * A)^(-1) * A^T * b

你想想看,这个方程其实很脆弱。如果窗口内纹理太少(比如一面白墙),A^T*A 矩阵会奇异,解出来的光流全是噪声。我在做无人机视觉导航时遇到过这个问题——飞过一片水面,光流完全乱跳。

实用技巧: LK光流通常配合金字塔使用(金字塔LK)。先在高层的低分辨率图像上算大运动,再在底层精调。这样能处理较大的位移,而且鲁棒性更好。OpenCV 里的 calcOpticalFlowPyrLK 就是干这个的。

代码示例(OpenCV Python):

import cv2
import numpy as np

# 读取两帧图像
prev = cv2.imread('frame1.jpg', 0)
next = cv2.imread('frame2.jpg', 0)

# 设置LK参数
lk_params = dict(winSize=(15, 15),
                 maxLevel=2,
                 criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

# 检测角点作为跟踪点
p0 = cv2.goodFeaturesToTrack(prev, mask=None, maxCorners=100,
                             qualityLevel=0.3, minDistance=7)

# 计算光流
p1, st, err = cv2.calcOpticalFlowPyrLK(prev, next, p0, None, **lk_params)

# 筛选有效点
good_new = p1[st == 1]
good_old = p0[st == 1]

这段代码我用了很多次。注意 winSize 这个参数——窗口太小容易受噪声影响,太大又可能违背运动一致性假设。我一般设 15x15 到 21x21 之间,具体要看图像分辨率和运动速度。

4.4 三种算法的对比与选型

说了这么多,到底该用哪个?我整理了一张表,方便你快速决策:

算法 核心原理 优点 缺点 适用场景
Mean Shift 颜色密度梯度上升 简单、快速、无参数 窗口固定、颜色敏感 目标颜色独特、尺度变化小
CAMShift 自适应窗口的Mean Shift 尺度自适应、旋转不变 快速运动易丢失 手势跟踪、人脸跟踪
LK光流 亮度恒定+运动一致性 不依赖颜色、精度高 纹理要求高、计算量大 特征点跟踪、运动估计

我个人建议:如果目标颜色特征明显且场景简单,用 CAMShift 最省事;如果场景纹理丰富且需要精确运动,用 LK 光流;如果资源受限(比如嵌入式设备),Mean Shift 的轻量级优势就体现出来了。

重要提醒: 这三种算法都是基于“目标外观不变”的假设。一旦目标发生形变、遮挡或光照突变,它们都会失效。所以实际项目中,我通常把它们作为底层模块,上层再加一个重检测机制(比如每隔N帧用检测器确认一下)。

好了,这一章的内容就到这里。三种算法各有千秋,没有银弹。关键是要理解它们的假设条件,然后在实际场景中灵活组合。下一章我们会继续聊其他经典跟踪算法,到时候再对比着看。

经典跟踪算法(上)知识体系 目标跟踪算法 Mean Shift CAMShift LK光流法 颜色密度梯度 窗口固定 自适应窗口 旋转不变性 快速运动易丢 亮度恒定假设 运动一致性 纹理要求高 选型建议:场景决定算法,没有银弹

可可精品资料库