第二章:目标表示与特征提取——让算法“看懂”目标
做目标跟踪这些年,我最大的感触是:你用什么方式描述目标,直接决定了跟踪能走多远。就像你让我描述一个人——说“穿红衣服的”和说“身高175、左眉有颗痣、走路外八字”,信息量完全不同。目标表示和特征提取,就是给算法一双“看得懂”的眼睛。
核心逻辑链: 目标表示(怎么框住它)→ 特征提取(它长什么样)→ 相似度度量(下一帧它还在不在)
2.1 目标表示方法——你打算怎么“框”住它?
我个人习惯把目标表示分成四类,从简单到复杂:
2.1.1 点表示
最朴素的方式。只用目标的中心点坐标 (x, y) 来描述。我在做早期雷达目标跟踪时,用的就是点表示——因为雷达回波本身就是一个点云。优点是计算量极小,缺点嘛……你想想看,一个点能告诉你目标有多大、旋转了多少吗?显然不能。
2.1.2 矩形框(Bounding Box)
这是工业界最常用的方式。用 (x, y, w, h) 四个参数搞定——中心坐标加宽高。我参与的第一个安防项目,行人检测全部用矩形框。为什么?简单、直观、标注成本低。但要注意:矩形框天然包含背景信息,框越大,背景干扰越严重。
避坑指南: 我曾经在密集人群跟踪中吃过亏——两个行人挨着走,矩形框大面积重叠,导致跟踪ID频繁跳变。后来改用旋转框(带角度θ的矩形),情况好了很多。
2.1.3 轮廓/分割掩码
像素级的精确表示。每个像素标记为“属于目标”或“不属于目标”。精度最高,但计算量也最大。我记得2018年做视频分割时,一张1080p图像的轮廓就要处理207万个像素点。实时性?嗯,当时只能在高端GPU上跑。
2.1.4 骨架/关键点
用一组关键点描述目标的结构。比如人体骨架用17个关键点(COCO标准):鼻子、肩膀、手肘、膝盖……这种表示对姿态变化非常鲁棒。我做过一个手势识别项目,用21个手部关键点,旋转、缩放都不怕。
| 表示方法 | 参数数量 | 精度 | 计算开销 | 典型场景 |
|---|---|---|---|---|
| 点 | 2 | 低 | 极低 | 雷达跟踪、稀疏目标 |
| 矩形框 | 4 | 中 | 低 | 行人检测、车辆跟踪 |
| 轮廓 | 可变 | 高 | 高 | 视频分割、精细跟踪 |
| 骨架 | 17~21 | 中高 | 中 | 人体姿态、手势识别 |
2.2 特征提取——目标到底长什么样?
有了表示方式,接下来要回答:这个目标有什么“辨识度”?说白了,就是提取特征。
2.2.1 颜色直方图
最经典的特征之一。统计图像中每种颜色出现的频率。我习惯用HSV空间做直方图——对光照变化更鲁棒。举个例子:一个红色球在RGB空间里,光照一变红色可能变成暗红;但在HSV里,H(色调)分量基本不变。
# 计算HSV颜色直方图(Python风格伪代码)
def calc_hsv_histogram(image, bins=32):
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1], None, [bins, bins], [0, 180, 0, 256])
return cv2.normalize(hist, hist).flatten()
注意: 颜色直方图丢失了空间信息。一个红蓝各半的球,和一个红蓝棋盘格,直方图一模一样!所以它常和其他特征配合使用。
2.2.2 HOG(方向梯度直方图)
HOG是我个人非常喜欢的一个特征。它统计图像局部区域的梯度方向分布。为什么有效?因为物体的边缘和纹理,本质上就是梯度的剧烈变化。HOG把图像分成8×8的小块(cell),每个块统计9个方向的梯度,然后归一化。
我记得在行人检测项目中,HOG+SVM的组合简直是黄金搭档。一个64×128的行人图像,提取HOG特征后得到3780维向量。嗯,这个数字我到现在都记得——因为调参时反复看到它。
2.2.3 LBP(局部二值模式)
LBP擅长描述纹理。它的思路很巧妙:对每个像素,比较它和周围8个邻居的灰度值。比它大记1,比它小记0,得到一个8位二进制数。这个数就是该像素的LBP值。
我曾经用LBP做织物瑕疵检测——布料的纹理非常规律,一旦出现瑕疵,LBP直方图就会明显变化。均匀模式(uniform LBP)把维度从256降到59,计算效率大幅提升。
2.2.4 深度特征
这是近几年的主流。用卷积神经网络(CNN)提取特征,比如VGG、ResNet的中间层输出。深度特征的优势是:自动学习最有效的特征表达,不需要人工设计。
但要注意:深度特征计算量大,而且需要大量数据训练。我在一个小样本项目里试过直接用预训练模型的特征,效果居然还不错——说明深度特征的泛化能力确实强。
| 特征类型 | 维度 | 光照鲁棒性 | 旋转鲁棒性 | 计算速度 |
|---|---|---|---|---|
| 颜色直方图 | 96~768 | 中(HSV较好) | 好 | 极快 |
| HOG | 3780(典型) | 好 | 差 | 快 |
| LBP | 59(均匀模式) | 好 | 中 | 快 |
| 深度特征 | 512~4096 | 极好 | 好(数据增强后) | 慢(需GPU) |
2.3 相似度度量——怎么判断“还是它”?
特征提取完了,下一帧来了,怎么判断这个目标还是不是原来那个?这就需要相似度度量。
2.3.1 相关系数
衡量两个向量之间的线性相关程度。值域[-1, 1],1表示完全正相关,-1表示完全负相关。我常用在模板匹配中——把第一帧的目标作为模板,后续帧滑动窗口计算相关系数。
相关系数对光照变化有一定鲁棒性,因为它做了去均值处理。但要注意:如果目标发生非刚性变形(比如人弯腰),相关系数会急剧下降。
2.3.2 欧氏距离
最直观的距离度量。两个特征向量对应位置相减,平方求和再开方。值越小越相似。我习惯在特征维度不高时用欧氏距离——比如颜色直方图(几百维)或者LBP(几十维)。
但欧氏距离有个坑:它对特征尺度敏感。如果一个特征的数值范围是0~255,另一个是0~1,前者会主导距离计算。所以用之前一定要做归一化。
# 欧氏距离计算
def euclidean_distance(feat1, feat2):
return np.sqrt(np.sum((feat1 - feat2) ** 2))
# 归一化后再计算
feat1_norm = feat1 / np.linalg.norm(feat1)
feat2_norm = feat2 / np.linalg.norm(feat2)
dist = euclidean_distance(feat1_norm, feat2_norm)
2.3.3 余弦相似度
衡量两个向量的方向一致性,忽略模长。值域[-1, 1],1表示方向完全相同。我特别喜欢在深度特征上用余弦相似度——因为深度特征的模长往往包含目标尺度信息,而我们更关心“它是什么”而不是“它有多大”。
举个例子:同一个行人,走近时特征向量模长变大,走远时模长变小。但方向基本不变。用余弦相似度,就能排除尺度干扰,专注于内容匹配。
实战建议: 我一般会组合使用多种度量。比如在KCF跟踪器中,用相关系数做快速匹配;在深度特征重检测时,用余弦相似度做精确匹配。取长补短,效果更好。
2.4 小结
目标表示、特征提取、相似度度量,这三者环环相扣。你选择矩形框表示,就要考虑框内背景的干扰;你选择颜色直方图特征,就要接受它丢失空间信息的缺陷;你选择欧氏距离,就别忘了先做归一化。
没有完美的方案,只有最适合场景的方案。我这些年最大的体会就是:理解每个方法的优缺点,比记住一堆公式更重要。遇到实际问题时,多问自己一句:这个场景下,什么表示最合适?什么特征最鲁棒?什么度量最可靠?想清楚了,代码写起来自然顺手。
个人经验: 刚开始做跟踪时,我总想用最复杂的方法——深度特征+轮廓表示+余弦相似度,结果跑起来卡成PPT。后来学乖了:先上简单的(矩形框+颜色直方图+欧氏距离),跑通了再逐步升级。这个“从简到繁”的思路,帮我省了大量调试时间。