Files
code-test-drone/utils/tests/target_tracker/tracklet_test.py
T
2026-09-02 15:13:13 +08:00

160 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""tracklet_test.py —— Tracklet 与几何工具单元测试。
移植自 reference-c++/target_tracker/tests/tracklet_test.cpp。
覆盖(视觉追踪技术路线 §3.2/§5.2/§6.1/§7):框互转、IoU 边界、
IsDead/IsCoasting 判定(严格大于)、Predict/Update/MarkMiss 计数语义。
"""
import pytest
from target_tracker.tracklet import (BoxXYXY, BoxCXCYWH, Tracklet, compute_iou,
to_center_size, to_xyxy)
# ---- 框表示互转(§7) ----
def test_round_trip_preserves_values():
box = BoxXYXY(10.0, 20.0, 50.0, 80.0)
c = to_center_size(box)
assert c.cx == 30.0 # (10+50)/2
assert c.cy == 50.0 # (20+80)/2
assert c.w == 40.0 # 50−10
assert c.h == 60.0 # 80−20
back = to_xyxy(c)
assert back.x1 == box.x1
assert back.y1 == box.y1
assert back.x2 == box.x2
assert back.y2 == box.y2
# ---- IoU 边界(§5.2) ----
def test_identical_boxes_return_one():
a = BoxXYXY(10.0, 10.0, 30.0, 30.0)
assert compute_iou(a, a) == pytest.approx(1.0, abs=1e-12) # 完全重合 = 1
def test_disjoint_boxes_return_zero():
a = BoxXYXY(0.0, 0.0, 10.0, 10.0)
b = BoxXYXY(20.0, 20.0, 30.0, 30.0)
assert compute_iou(a, b) == 0.0 # 零交集
def test_touching_edges_return_zero():
# 仅边/角相接:交集宽高为 0 → 交集面积 ≤ 0 返回 0(§5.2)
a = BoxXYXY(0.0, 0.0, 10.0, 10.0)
b = BoxXYXY(10.0, 0.0, 20.0, 10.0) # 右边缘相接
assert compute_iou(a, b) == 0.0
c = BoxXYXY(10.0, 10.0, 20.0, 20.0) # 角点相接
assert compute_iou(a, c) == 0.0
def test_empty_box_returns_zero():
normal = BoxXYXY(0.0, 0.0, 10.0, 10.0)
zero_area = BoxXYXY(5.0, 5.0, 5.0, 5.0) # 零面积
inverted = BoxXYXY(10.0, 10.0, 0.0, 0.0) # 反向(负宽高)
assert compute_iou(normal, zero_area) == 0.0
assert compute_iou(zero_area, normal) == 0.0
assert compute_iou(normal, inverted) == 0.0
assert compute_iou(zero_area, zero_area) == 0.0
def test_partial_overlap_is_correct():
# A = [0,0,10,10](面积 100),B = [5,5,15,15](面积 100),交集 25
a = BoxXYXY(0.0, 0.0, 10.0, 10.0)
b = BoxXYXY(5.0, 5.0, 15.0, 15.0)
assert compute_iou(a, b) == pytest.approx(25.0 / 175.0, abs=1e-12)
assert compute_iou(b, a) == pytest.approx(25.0 / 175.0, abs=1e-12) # 对称性
# ---- 构造与派生状态(§3.2 / §6.1) ----
def test_construction_initial_fields():
t = Tracklet(7, BoxCXCYWH(100.0, 100.0, 20.0, 20.0), 0.9, 1, 8)
assert t.track_id == 7
assert t.lost_count == 0
assert t.max_lost == 8
assert t.confidence == 0.9
assert t.cls_id == 1
assert t.age == 1 # 创建时为 1(§3.2)
assert t.hits == 1 # 创建时为 1(§3.2)
assert t.last_center == (100.0, 100.0)
assert t.last_box is not None # 创建当帧即赋 last_box(§7 步骤 5)
assert t.last_box.x1 == 90.0
assert t.last_box.y1 == 90.0
assert t.last_box.x2 == 110.0
assert t.last_box.y2 == 110.0
assert not t.is_dead
assert not t.is_coasting
def test_is_dead_uses_strictly_greater():
# IsDead 使用严格大于:lost_count == max_lost 仍存活(§6.1 注记 2)
t = Tracklet(0, BoxCXCYWH(100.0, 100.0, 20.0, 20.0), 0.9, 0, 8)
for _ in range(8):
t.mark_miss()
assert not t.is_dead, f"lost_count={t.lost_count} 未超过 max_lost 不应判死"
assert t.lost_count == 8
t.mark_miss() # lost_count = 9 > 8
assert t.is_dead
def test_is_coasting_when_lost_positive():
t = Tracklet(0, BoxCXCYWH(100.0, 100.0, 20.0, 20.0), 0.9, 0, 8)
assert not t.is_coasting
t.mark_miss()
assert t.is_coasting # lost_count > 0 即 Coast(§3.2)
# ---- 生命周期方法计数语义(§7) ----
def test_predict_advances_age_and_updates_last_state():
t = Tracklet(0, BoxCXCYWH(100.0, 100.0, 20.0, 20.0), 0.9, 0, 8)
age_before = t.age
t.predict() # 速度为零:位置不变,但 age+1、last_box 更新为预测框
assert t.age == age_before + 1
assert t.last_box is not None
assert t.last_center[0] == pytest.approx(100.0, abs=1e-9)
assert t.last_center[1] == pytest.approx(100.0, abs=1e-9)
# hits / lost_count / confidence 不受预测影响
assert t.hits == 1
assert t.lost_count == 0
assert t.confidence == 0.9
def test_update_resets_lost_and_increments_hits():
t = Tracklet(0, BoxCXCYWH(100.0, 100.0, 20.0, 20.0), 0.9, 0, 8)
t.mark_miss()
t.mark_miss()
assert t.lost_count == 2
assert t.is_coasting
age_before = t.age
box = BoxXYXY(150.0, 160.0, 170.0, 180.0)
t.update(box, 0.75, 2)
assert t.lost_count == 0 # 匹配成功清零(§7)
assert t.hits == 2 # hits+1
assert t.age == age_before + 1 # age+1
assert t.confidence == 0.75
assert t.cls_id == 2
assert t.last_center == (160.0, 170.0) # ((150+170)/2, (160+180)/2)
assert t.last_box is not None
assert t.last_box.x1 == box.x1
assert t.last_raw_box is not None # 实测框同步记录
assert t.last_raw_box.x2 == box.x2
assert not t.is_coasting
def test_mark_miss_increments_lost_and_age():
t = Tracklet(0, BoxCXCYWH(100.0, 100.0, 20.0, 20.0), 0.9, 0, 8)
age_before = t.age
t.mark_miss()
assert t.lost_count == 1
assert t.age == age_before + 1
# MarkMiss 不影响 hits / confidence
assert t.hits == 1
assert t.confidence == 0.9