归档 legacy/:stereo_yolo/stereo_ranger/target_tracker 三包 + 175 条测试 + demo/配置/权重/三份规格;运行链路不再依赖,默认 pytest 446 条,归档测试按需运行。 目标有效性第二判据改为 LOS 运动学一致性(pod_bridge/los_consistency.py),删除 require_vision;pod_locked() 叠加该判据,状态机随之后退重搜。 自定义返航(task_runner/return_home.py):保持转场高度回起飞点 → 恢复初始航向 → 垂直降落,不再爬 RTL_RETURN_ALT;未起飞/无起飞点/超时/异常仍退回 PX4 原生 RTL。 任务链安全兜底:主循环异常→紧急返航、stop 幂等、landing_stop 保留遥测、in_air 流复用+超时、解锁/起飞与吊舱识别仅执行一次、rtl_timeout 上限。 入口共享件拆到 utils/sitl_support.py;pod_sitl_test.py→l1_sitl_test.py;删除旧架构 test.py;相机脚本移入 tools/。 新增 .gitignore 并剔除历史误跟踪的 59 个 .pyc;requirements 按实际依赖重写;文档全面对齐,新增 legacy/README.md 与 tools/README.md。
88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
"""匹配器测试(规格 §6.2/§11.4:硬约束各一例、贪心排序、一一配对、cost ≥ 1 拒绝)。"""
|
||
|
||
import pytest
|
||
|
||
from stereo_ranger import Detection, GreedyMatcher, SamplePoint, StereoConfig, sample_point
|
||
|
||
CFG = StereoConfig()
|
||
MATCHER = GreedyMatcher()
|
||
|
||
|
||
def _point(cx: float, cy: float, *, w: float = 60.0, h: float = 40.0, class_id: int = 0) -> SamplePoint:
|
||
return sample_point(
|
||
Detection(cx - w / 2.0, cy - h / 2.0, cx + w / 2.0, cy + h / 2.0, class_id=class_id)
|
||
)
|
||
|
||
|
||
def test_class_mismatch_rejected():
|
||
assert MATCHER.match([_point(1000.0, 500.0)], [_point(1000.0, 500.0, class_id=1)], CFG) == []
|
||
|
||
|
||
def test_dy_beyond_tau_rejected():
|
||
assert MATCHER.match([_point(1000.0, 500.0)], [_point(1000.0, 502.1)], CFG) == []
|
||
|
||
|
||
def test_dy_within_tau_accepted_with_cost():
|
||
pairs = MATCHER.match([_point(1000.0, 500.0)], [_point(1000.0, 501.9)], CFG)
|
||
assert len(pairs) == 1
|
||
assert pairs[0].cost == pytest.approx(1.9 / CFG.tau_y_px)
|
||
|
||
|
||
def test_height_ratio_beyond_tau_rejected():
|
||
assert MATCHER.match([_point(1000.0, 500.0, h=100.0)], [_point(1000.0, 500.0, h=140.0)], CFG) == []
|
||
|
||
|
||
def test_height_ratio_near_tau_accepted():
|
||
pairs = MATCHER.match([_point(1000.0, 500.0, h=120.0)], [_point(1000.0, 500.0, h=159.0)], CFG)
|
||
assert len(pairs) == 1
|
||
|
||
|
||
def test_cost_at_one_rejected():
|
||
# dy = tau_y ⇒ cost = 1.0,严格 cost < 1 才接受(§6.2)
|
||
assert MATCHER.match([_point(1000.0, 500.0)], [_point(1000.0, 502.0)], CFG) == []
|
||
|
||
|
||
def test_combined_cost_at_one_rejected():
|
||
# dy/tau = 0.5 与 Δh/(tau_h·h_max) = 0.5 相加恰为 1.0 → 拒绝
|
||
assert MATCHER.match([_point(1000.0, 500.0, h=112.0)], [_point(1000.0, 501.0, h=128.0)], CFG) == []
|
||
|
||
|
||
def test_greedy_pairs_cheapest_first_and_one_to_one():
|
||
left = [_point(1000.0, 500.0), _point(1000.0, 501.0)]
|
||
right = [_point(1000.0, 500.0), _point(1000.0, 501.0)]
|
||
pairs = MATCHER.match(left, right, CFG)
|
||
assert [(p.left_index, p.right_index) for p in pairs] == [(0, 0), (1, 1)]
|
||
assert pairs[0].cost == pytest.approx(0.0)
|
||
|
||
|
||
def test_one_left_claims_cheapest_right():
|
||
pairs = MATCHER.match([_point(1000.0, 500.0)], [_point(1000.0, 500.0), _point(1000.0, 500.5)], CFG)
|
||
assert [(p.left_index, p.right_index) for p in pairs] == [(0, 0)]
|
||
|
||
|
||
def test_two_lefts_cannot_share_one_right():
|
||
left = [_point(1000.0, 500.0), _point(1000.0, 500.2)]
|
||
right = [_point(1000.0, 500.0)]
|
||
pairs = MATCHER.match(left, right, CFG)
|
||
assert len(pairs) == 1
|
||
assert pairs[0].left_index == 0
|
||
|
||
|
||
def test_unmatched_right_is_not_paired():
|
||
pairs = MATCHER.match([_point(1000.0, 500.0)], [_point(1000.0, 500.0), _point(1400.0, 500.0)], CFG)
|
||
assert len(pairs) == 1
|
||
assert pairs[0].right_index == 0
|
||
|
||
|
||
def test_non_finite_points_excluded():
|
||
bad = sample_point(Detection(float("nan"), 500.0, 1060.0, 540.0))
|
||
good = _point(1000.0, 500.0)
|
||
assert MATCHER.match([bad], [good], CFG) == []
|
||
assert MATCHER.match([good], [bad], CFG) == []
|
||
|
||
|
||
def test_zero_height_box_rejected():
|
||
left = sample_point(Detection(970.0, 500.0, 1030.0, 500.0))
|
||
right = sample_point(Detection(970.0, 500.0, 1030.0, 500.0))
|
||
assert MATCHER.match([left], [right], CFG) == []
|