2. 目标跟踪基础理论
大家好,我是你们的老朋友。今天咱们聊聊目标跟踪里最核心的三个模块——目标怎么表示、怎么算相似、怎么去搜索。这三个东西,说白了就是跟踪器的灵魂。我当年刚入行时,觉得跟踪就是框个框、算个特征、找找位置,结果被现实狠狠教育了一顿。嗯,咱们一个一个来拆解。
一、目标表示方法:你拿什么来描述目标?
目标表示,就是给目标拍一张“身份证照片”。这张照片要足够独特,还得能应对光照、形变、遮挡这些幺蛾子。我个人习惯把表示方法分成三代:手工特征、统计特征、深度特征。
1. 颜色直方图
颜色直方图是最朴素的方法。它统计图像中每种颜色出现的次数,形成一个分布。说白了,就是数一数目标区域里红色多少、蓝色多少。
优点:计算快、对旋转不敏感、实现简单。
缺点:完全丢失空间信息。举个例子,一张红底白字的图片和一张白底红字的图片,直方图可能一模一样。我在项目中遇到过这种情况——跟踪一个穿红衣服的人,背景里出现红色广告牌,直接跟丢。
# 颜色直方图计算示例(OpenCV)
import cv2
import numpy as np
def calc_color_hist(image, bins=32):
# 将图像转换到HSV空间,对光照更鲁棒
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1], None, [bins, bins], [0, 180, 0, 256])
# 归一化
cv2.normalize(hist, hist, 0, 1, cv2.NORM_MINMAX)
return hist.flatten()
2. HOG特征
HOG(方向梯度直方图)就聪明多了。它不光看颜色,还看边缘和纹理。怎么做的?把图像切成小格子,每个格子统计梯度方向,最后拼成一个特征向量。
你想想看,HOG为什么比颜色直方图强?因为它抓住了目标的“形状”。比如跟踪行人,HOG能捕捉到人的轮廓——肩膀、腿、头的梯度分布。我曾经用HOG做行人检测,效果比颜色直方图好一个档次。
我的经验:HOG对光照变化有一定鲁棒性,但计算量比颜色直方图大。如果目标形变严重(比如跟踪一只奔跑的狗),HOG的格子划分会出问题。我建议在目标形变不大的场景优先用HOG。
# HOG特征提取示例
from skimage.feature import hog
def extract_hog(image):
# image需为灰度图,尺寸建议64x128
features = hog(image,
orientations=9, # 梯度方向数
pixels_per_cell=(8, 8),
cells_per_block=(2, 2),
block_norm='L2-Hys')
return features
3. 深度特征
深度特征,就是让神经网络自己去学什么特征有用。比如用预训练的VGG、ResNet提取特征。这玩意儿强在哪?它能学到高层语义信息——比如“这是一个人的脸”而不是“这里有128个像素点”。
但注意,深度特征不是万能的。我踩过一个坑:用深度特征做跟踪,模型在训练集上表现很好,换到真实场景就崩了。为什么?因为训练数据里没有遮挡、没有运动模糊。所以,深度特征需要配合数据增强和微调。
避坑指南:我曾经在嵌入式设备上用深度特征做实时跟踪,结果帧率只有5fps。深度特征计算量大,不适合所有场景。如果追求实时性,建议用轻量级网络(如MobileNet)或手工特征。
二、相似度度量:怎么判断两个目标像不像?
有了特征,下一步就是比较。相似度度量就是一把尺子,量一量当前帧的目标和模板有多像。我常用的有三种尺子。
1. 巴氏距离
巴氏距离专门用来比较两个概率分布。颜色直方图本身就是分布,所以巴氏距离是颜色直方图的天然搭档。
公式不复杂:d = sqrt(1 - sum(sqrt(p_i * q_i)))。值越小越相似。我在做MeanShift跟踪时,用的就是巴氏距离。效果嘛,在背景简单时很稳,背景一复杂就露怯。
def bhattacharyya_distance(hist1, hist2):
# hist1和hist2需为归一化直方图
return np.sqrt(1 - np.sum(np.sqrt(hist1 * hist2)))
2. 余弦相似度
余弦相似度看的是两个向量的夹角。夹角越小,相似度越高。它不关心向量的长度,只关心方向。什么意思?比如两个特征向量,一个很大一个很小,但只要方向一致,余弦相似度就高。
我个人习惯在深度特征上用余弦相似度。因为深度特征通常维度很高(比如4096维),欧氏距离在高维空间容易失效(维度灾难),余弦相似度反而更稳定。
def cosine_similarity(vec1, vec2):
dot = np.dot(vec1, vec2)
norm = np.linalg.norm(vec1) * np.linalg.norm(vec2)
return dot / norm if norm != 0 else 0
3. 欧氏距离
欧氏距离就是直线距离,最直观。但注意,它要求特征向量的每个维度量纲一致。比如HOG特征,不同bin的值范围可能差很多,直接算欧氏距离会出问题。我建议先做标准化。
| 度量方法 | 适用场景 | 注意事项 |
|---|---|---|
| 巴氏距离 | 颜色直方图、概率分布 | 对零值敏感,需平滑处理 |
| 余弦相似度 | 深度特征、高维向量 | 忽略向量长度信息 |
| 欧氏距离 | 低维特征、标准化数据 | 需先做归一化或标准化 |
三、搜索策略:怎么找到目标的位置?
特征有了,尺子有了,下一步就是去图像里找目标。搜索策略决定了你怎么“逛”完整张图。我主要讲两种:滑动窗口和粒子滤波。
1. 滑动窗口
滑动窗口,说白了就是暴力搜索。用一个固定大小的窗口,在图像上从左到右、从上到下扫一遍,每个位置都算一次相似度,取最高的那个。
这方法简单粗暴,但效率低。一张1080p的图像,窗口步长设8像素,要算几万个位置。我当年做实时跟踪时,滑动窗口根本跑不动。后来用了图像金字塔+多尺度窗口,稍微好一点,但还是慢。
优化技巧:可以用上一帧的目标位置作为先验,只在附近区域搜索。这叫“局部滑动窗口”,能减少90%的计算量。我建议在目标运动速度不快时使用。
2. 粒子滤波
粒子滤波就聪明多了。它用一堆“粒子”来代表目标可能的位置。每个粒子有一个权重,权重高的粒子说明这个位置更像目标。然后根据权重重新采样,粒子会逐渐聚集到目标附近。
你想想看,粒子滤波的好处是什么?它不需要遍历整张图,而且能处理多模态分布——比如目标被遮挡时,粒子会分散,遮挡解除后又能重新聚集。我在做复杂场景跟踪时,粒子滤波比滑动窗口稳得多。
# 粒子滤波核心步骤(伪代码)
def particle_filter(frame, particles, weights):
# 1. 预测:根据运动模型移动粒子
particles = predict(particles)
# 2. 更新:计算每个粒子的权重
for i, p in enumerate(particles):
feature = extract_feature(frame, p)
weights[i] = similarity(feature, template)
# 3. 归一化权重
weights /= np.sum(weights)
# 4. 重采样:根据权重重新抽取粒子
particles = resample(particles, weights)
# 5. 估计目标位置(加权平均)
position = np.average(particles, weights=weights, axis=0)
return position, particles, weights
避坑指南:粒子滤波的粒子数量很关键。粒子太少容易跟丢,太多又跑不动。我建议根据目标运动速度动态调整粒子数——运动快时多加点粒子,运动慢时可以少一些。另外,重采样时要注意“粒子贫化”问题,就是所有粒子都挤到一起,失去多样性。可以加一点随机扰动来缓解。
知识体系总览
下面这张图,是我自己画的本章知识结构。你可以把它当作一张地图,随时回来看看。
好了,这一章的内容就到这里。目标表示、相似度度量、搜索策略,这三个东西是跟踪器的三根支柱。你可以在实际项目中灵活组合——比如用颜色直方图配巴氏距离做快速跟踪,用深度特征配余弦相似度做高精度跟踪。记住,没有最好的方法,只有最合适的组合。
可可精品资料库