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 很容易漂走。有一次做行人跟踪,那人穿着灰色衣服走在水泥地上,算法直接“叛变”去跟踪地面了。嗯,这里要注意——颜色特征在复杂背景下非常脆弱。
4.2 连续自适应均值漂移(CAMShift)算法
CAMShift 是 Mean Shift 的升级版。它解决了 Mean Shift 的一个大问题:窗口大小固定。你想想看,目标靠近摄像头时会变大,远离时会变小,固定窗口肯定不行。
CAMShift 的做法很聪明:每次迭代后,根据目标的零阶矩(其实就是像素质量分布)来调整窗口尺寸和方向。说白了,就是让窗口能“呼吸”。
实现步骤:
- 初始化搜索窗口(位置和大小)
- 计算窗口内的颜色概率分布(反向投影)
- 执行 Mean Shift 找到新中心
- 根据二阶矩计算窗口的宽度、高度和旋转角度
- 用新窗口重复步骤2-4
我个人习惯用 CAMShift 做手势跟踪。记得有一次做智能家居项目,用户用手势控制灯光,CAMShift 在 640x480 分辨率下能跑到 30fps,效果相当不错。但有个坑:当目标快速运动时,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 矩阵会奇异,解出来的光流全是噪声。我在做无人机视觉导航时遇到过这个问题——飞过一片水面,光流完全乱跳。
代码示例(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 的轻量级优势就体现出来了。
好了,这一章的内容就到这里。三种算法各有千秋,没有银弹。关键是要理解它们的假设条件,然后在实际场景中灵活组合。下一章我们会继续聊其他经典跟踪算法,到时候再对比着看。