第二章:目标表示与特征提取——让算法“看懂”目标

做目标跟踪这些年,我最大的感触是:你用什么方式描述目标,直接决定了跟踪能走多远。就像你让我描述一个人——说“穿红衣服的”和说“身高175、左眉有颗痣、走路外八字”,信息量完全不同。目标表示和特征提取,就是给算法一双“看得懂”的眼睛。

核心逻辑链: 目标表示(怎么框住它)→ 特征提取(它长什么样)→ 相似度度量(下一帧它还在不在)

目标跟踪基础:表示 → 特征 → 度量 目标表示方法 点 / 框 / 轮廓 / 骨架 特征提取 颜色直方图 / HOG / LBP / 深度特征 相似度度量 相关系数 / 欧氏距离 / 余弦相似度 表示方式对比 • 点:简单,丢失尺寸 • 框:最常用,含宽高 • 轮廓:精确,计算大 • 骨架:结构信息 特征维度对比 • 颜色直方图:256维/通道 • HOG:3780维(典型) • LBP:59维(均匀模式) • 深度特征:512~4096维 度量适用场景 • 相关系数:模板匹配 • 欧氏距离:特征向量 • 余弦相似度:方向敏感 • 结合使用效果更佳

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。后来学乖了:先上简单的(矩形框+颜色直方图+欧氏距离),跑通了再逐步升级。这个“从简到繁”的思路,帮我省了大量调试时间。

可可精品资料库