2026.8.20 视觉追踪器优化

This commit is contained in:
2026-08-20 09:09:20 +08:00
parent 7d6b29939c
commit 861a5981a6
+79 -19
View File
@@ -3,11 +3,11 @@ target_tracker.py — 轻量级多目标追踪过滤器
【解决的问题】 【解决的问题】
1. 目标短暂丢帧(2-5帧)导致无人机抽搐 → 卡尔曼滤波 coast/predict 平滑过渡 1. 目标短暂丢帧(2-5帧)导致无人机抽搐 → 卡尔曼滤波 coast/predict 平滑过渡
2. 多目标 ID 飘移导致甩头 → 最邻近欧氏距离关联锁定 ID 不变 2. 多目标 ID 飘移导致甩头 → 中心距离 + IoU 加权关联锁定 ID 不变
【核心设计】 【核心设计】
- 恒定速度卡尔曼滤波器(Kalman Filter × 8状态:cx, cy, w, h, vx, vy, vw, vh) - 恒定速度卡尔曼滤波器(Kalman Filter × 8状态:cx, cy, w, h, vx, vy, vw, vh)
- 最邻近贪婪关联,阈值 200px 内视为同一目标 - 最邻近贪婪关联(中心距离 + IoU 加权代价),提高交叉/靠近时的 ID 稳定性
- 死区预测(Coast/Predict):连续丢失 ≤ max_lost_frames 帧时用滤波器预测值 - 死区预测(Coast/Predict):连续丢失 ≤ max_lost_frames 帧时用滤波器预测值
- 超过 max_lost_frames → 释放轨道,对外判丢失 - 超过 max_lost_frames → 释放轨道,对外判丢失
@@ -297,8 +297,10 @@ class TargetTracker:
| 参数 | 默认值 | 说明 | | 参数 | 默认值 | 说明 |
|------|--------|------| |------|--------|------|
| max_lost_frames | 8 | 目标连续消失帧数上限(~250ms @ 30fps) | | max_lost_frames | 8 | 目标连续消失帧数上限(~250ms @ 30fps) |
| max_association_dist | 200 | 关联距离阈值(像素),超过视为新目标 | | max_association_dist | 200 | 中心距离软阈值(像素),用于归一化代价 |
| min_hits | 3 | 新轨道确认前需要连续匹配帧数 | | min_hits | 3 | 新轨道确认前需要连续匹配帧数 |
| dist_weight | 0.5 | 关联代价中中心距离的权重 |
| iou_weight | 0.5 | 关联代价中 (1-IoU) 的权重 |
update() 返回值: update() 返回值:
{ {
@@ -322,16 +324,20 @@ class TargetTracker:
""" """
def __init__(self, max_lost_frames=8, max_association_dist=200.0, def __init__(self, max_lost_frames=8, max_association_dist=200.0,
min_hits=3): min_hits=3, dist_weight=0.5, iou_weight=0.5):
""" """
Args: Args:
max_lost_frames: 最大连续丢失帧数(默认 8 ≈ 250-300ms) max_lost_frames: 最大连续丢失帧数(默认 8 ≈ 250-300ms)
max_association_dist:关联距离阈值(像素,默认 200px) max_association_dist: 中心距离软阈值(像素,默认 200px),用于归一化
min_hits: 新轨道确认前最少连续匹配帧数(默认 3) min_hits: 新轨道确认前最少连续匹配帧数(默认 3)
dist_weight: 关联代价中中心距离项的权重(默认 0.5)
iou_weight: 关联代价中 (1 - IoU) 项的权重(默认 0.5)
""" """
self.max_lost_frames = max_lost_frames self.max_lost_frames = max_lost_frames
self.max_association_dist = max_association_dist self.max_association_dist = max_association_dist
self.min_hits = min_hits self.min_hits = min_hits
self.dist_weight = dist_weight
self.iou_weight = iou_weight
self._next_id = 0 self._next_id = 0
self._tracks = {} # {track_id: _Tracklet} self._tracks = {} # {track_id: _Tracklet}
@@ -469,9 +475,52 @@ class TargetTracker:
# 内部方法 # 内部方法
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@staticmethod
def _iou(box_a, box_b):
"""
计算两个轴对齐框的 IoU
Args:
box_a, box_b: [x1, y1, x2, y2]
Returns:
float — IoU ∈ [0, 1]
"""
if box_a is None or box_b is None:
return 0.0
ax1, ay1, ax2, ay2 = box_a
bx1, by1, bx2, by2 = box_b
inter_x1 = max(ax1, bx1)
inter_y1 = max(ay1, by1)
inter_x2 = min(ax2, bx2)
inter_y2 = min(ay2, by2)
inter_w = max(0.0, inter_x2 - inter_x1)
inter_h = max(0.0, inter_y2 - inter_y1)
inter_area = inter_w * inter_h
if inter_area <= 0.0:
return 0.0
area_a = max(0.0, (ax2 - ax1) * (ay2 - ay1))
area_b = max(0.0, (bx2 - bx1) * (by2 - by1))
union = area_a + area_b - inter_area
if union <= 0.0:
return 0.0
return inter_area / union
def _associate(self, det_centers): def _associate(self, det_centers):
""" """
最邻近贪婪关联 中心距离 + IoU 加权的贪婪关联
代价公式:
cost = dist_weight * (center_dist / max_association_dist)
+ iou_weight * (1 - IoU)
仅当 cost < 1.0 时才视为有效匹配。
Args: Args:
det_centers: [(idx, cx, cy, w, h, conf, cls_id), ...] det_centers: [(idx, cx, cy, w, h, conf, cls_id), ...]
@@ -482,37 +531,48 @@ class TargetTracker:
if not self._tracks or not det_centers: if not self._tracks or not det_centers:
return [], list(self._tracks.keys()), list(range(len(det_centers))) return [], list(self._tracks.keys()), list(range(len(det_centers)))
# 提取所有轨道的预测位置 # 预先计算每个检测的框
track_preds = {} # {tid: (cx, cy)} det_boxes = []
for tid, track in self._tracks.items(): for _, cx, cy, w, h, _, _ in det_centers:
cx, cy = track.last_center det_boxes.append([cx - w / 2.0, cy - h / 2.0,
track_preds[tid] = (cx, cy) cx + w / 2.0, cy + h / 2.0])
# 贪婪匹配:每条轨道找最近的未分配检测
matches = [] matches = []
assigned_dets = set() assigned_dets = set()
assigned_tracks = set() assigned_tracks = set()
# 先将轨道按置信度排序(更可靠的轨道优先匹配) # 置信度高的轨道优先匹配
sorted_tracks = sorted( sorted_tracks = sorted(
self._tracks.items(), self._tracks.items(),
key=lambda kv: kv[1].confidence, key=lambda kv: kv[1].confidence,
reverse=True reverse=True
) )
max_dist = max(self.max_association_dist, 1e-3)
for tid, track in sorted_tracks: for tid, track in sorted_tracks:
if tid in assigned_tracks: if tid in assigned_tracks:
continue continue
pred_cx, pred_cy = track_preds[tid]
best_dist = self.max_association_dist pred_cx, pred_cy = track.last_center
pred_box = track.last_box
best_cost = 1.0
best_did = None best_did = None
for did, _, d_cx, d_cy, _, _, _ in det_centers: for did, _, d_cx, d_cy, _, _, _ in det_centers:
if did in assigned_dets: if did in assigned_dets:
continue continue
dist = np.hypot(pred_cx - d_cx, pred_cy - d_cy) dist = np.hypot(pred_cx - d_cx, pred_cy - d_cy)
if dist < best_dist: norm_dist = dist / max_dist
best_dist = dist iou_val = self._iou(pred_box, det_boxes[did])
cost = (self.dist_weight * norm_dist +
self.iou_weight * (1.0 - iou_val))
if cost < best_cost:
best_cost = cost
best_did = did best_did = did
if best_did is not None: if best_did is not None: