第1章:数据关联基础——最近邻(NN)与全局最近邻(GNN)
各位同学,咱们今天聊聊数据关联里最基础、也最直观的方法——最近邻算法。说实话,我刚入行那会儿,第一次接触多目标跟踪,看到一堆乱七八糟的检测框和轨迹,脑子都是懵的。后来师傅丢给我一句话:「先学会最近邻,其他的再说。」
嗯,今天我就把当年踩过的坑、总结的经验,一次性讲清楚。
1.1 最近邻(NN)算法:直觉上的第一选择
最近邻(Nearest Neighbor,NN)的思路特别简单——说白了就是「找最近的」。你有一个新检测到的目标,跟已有的每条轨迹算个距离,谁离得最近,就把它关联给谁。
这个「距离」怎么算?我项目中常用的有三种:
| 距离度量 | 适用场景 | 我的经验 |
|---|---|---|
| 欧氏距离 | 位置相近的目标 | 最简单,但容易受尺度影响 |
| 马氏距离 | 考虑协方差的场景 | 卡尔曼滤波后配合使用效果不错 |
| 余弦距离 | 特征向量匹配 | ReID特征常用,我习惯用这个 |
核心思想:每个检测只找一条最近的轨迹,不考虑其他检测的影响。
你想想看,这样做有什么问题?
举个例子:两个目标靠得很近,检测框A离轨迹1最近,检测框B也离轨迹1最近。NN算法会直接把两个检测都塞给轨迹1——这不就乱套了吗?
我在项目中遇到过这种情况,当时用的是行人跟踪,两个人擦肩而过,NN直接给搞混了。后来我换成了全局最近邻,才解决了这个问题。
1.2 全局最近邻(GNN):让分配更合理
全局最近邻(Global Nearest Neighbor,GNN)的思路是:不只看单个检测,而是把所有检测和所有轨迹放在一起,找一个「整体最优」的匹配方案。
这其实就是一个二分图匹配问题。左边是轨迹,右边是检测,边上的权重就是距离。我们要找一组匹配,让总距离最小。
常用的求解算法是匈牙利算法(Hungarian Algorithm)。
我的建议:别自己手写匈牙利算法,直接用scipy.optimize.linear_sum_assignment,稳定又高效。
GNN相比NN的优势很明显:
- 避免了一个轨迹被多个检测抢占的情况
- 整体匹配更合理,不容易出现冲突
- 在密集场景下效果明显更好
但GNN也有代价——计算量上去了。轨迹数和检测数都是N的话,匈牙利算法的时间复杂度是O(N³)。不过别担心,实际场景中N一般不会太大,几十到几百个目标,跑起来还是很快的。
1.3 核心逻辑流程图
下面这张图是我自己画的,把NN和GNN的核心逻辑放在一起对比,你看一眼就明白了:
1.4 代码实现:从零写一个GNN
光说不练假把式。咱们直接上代码,我带你手写一个GNN匹配器。
import numpy as np
from scipy.optimize import linear_sum_assignment
class GNNTracker:
def __init__(self, max_distance=10.0):
self.tracks = [] # 已有的轨迹列表
self.max_distance = max_distance # 最大关联距离
def compute_distance_matrix(self, detections):
"""计算检测和轨迹之间的距离矩阵"""
n_tracks = len(self.tracks)
n_dets = len(detections)
if n_tracks == 0 or n_dets == 0:
return np.array([])
dist_matrix = np.zeros((n_tracks, n_dets))
for i, track in enumerate(self.tracks):
for j, det in enumerate(detections):
# 欧氏距离,你也可以换成马氏距离
dist = np.linalg.norm(track - det)
dist_matrix[i, j] = dist
return dist_matrix
def match(self, detections):
"""执行GNN匹配"""
if len(self.tracks) == 0:
# 没有轨迹,所有检测都作为新目标
return [], list(range(len(detections)))
dist_matrix = self.compute_distance_matrix(detections)
# 用匈牙利算法找全局最优匹配
track_indices, det_indices = linear_sum_assignment(dist_matrix)
matches = []
unmatched_tracks = list(range(len(self.tracks)))
unmatched_dets = list(range(len(detections)))
for t_idx, d_idx in zip(track_indices, det_indices):
if dist_matrix[t_idx, d_idx] < self.max_distance:
matches.append((t_idx, d_idx))
unmatched_tracks.remove(t_idx)
unmatched_dets.remove(d_idx)
return matches, unmatched_dets
def update(self, detections):
"""更新轨迹"""
matches, unmatched_dets = self.match(detections)
# 更新匹配上的轨迹
for t_idx, d_idx in matches:
self.tracks[t_idx] = detections[d_idx] # 简单更新,实际会用卡尔曼滤波
# 新增未匹配的检测作为新轨迹
for d_idx in unmatched_dets:
self.tracks.append(detections[d_idx])
return matches
注意:上面的代码是简化版,实际项目中你还需要处理轨迹的创建、删除、生命周期管理。我曾经在项目里忘了处理长时间未匹配的轨迹,结果内存直接爆了……
1.5 实战中的避坑指南
讲几个我踩过的坑,你遇到了能少走弯路:
- 距离阈值别设太死。我曾经设了个固定阈值,结果目标稍微跑快一点就匹配不上。后来改成自适应阈值,效果好多了。
- 匈牙利算法不是万能的。它假设所有检测都能匹配,但实际场景中会有虚警和漏检。记得加一个距离门限做过滤。
- 特征匹配比位置匹配更鲁棒。如果条件允许,用ReID特征代替位置做距离计算,能大幅减少ID Switch。
小技巧:GNN匹配完之后,我习惯再加一个「二次确认」——对匹配结果做一次卡方检验,把置信度低的匹配剔除掉。虽然多了一步计算,但能有效减少误匹配。
好了,NN和GNN的基础就讲到这里。这两种方法虽然简单,但却是很多高级关联算法(比如JPDA、MHT)的基石。你把这个搞懂了,后面学起来会轻松很多。
可可精品资料库