feat(detect): 约束预测框互不重叠
人工标注的真值里一处缺陷只有一个框:11211 张图、151 张多框图,重叠框对为 0。 而模型预测在验证集上有 6.9% 的图存在重叠框——NMS 只在同类内、按固定 IoU 阈值抑制, 压不掉两类情况:同一处缺陷被两个类别各框一次(实测 IoU 高达 0.99),以及 IoU 低于 NMS 阈值的同类重叠(grease 66 对、wrinkle 45 对)。 - engine/detection.suppress_overlaps:按置信度贪心保留互不重叠的框,可设允许的 最大 IoU;另可丢弃被大框完全包住的小框(面积悬殊时 IoU 很小,但显然是重复表达)。 - visualize_detections 默认 --max-overlap 0(与真值一致),并放出 --nms-iou 与 类别无关 NMS 开关;--max-overlap -1 可看模型原始输出。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43c8c4a179
commit
eba533a5b2
@@ -14,6 +14,7 @@ from engine.detection import ( # noqa: E402
|
||||
iou,
|
||||
match_boxes,
|
||||
parse_label,
|
||||
suppress_overlaps,
|
||||
)
|
||||
|
||||
|
||||
@@ -129,3 +130,63 @@ def test_draw_handles_boxes_at_the_border():
|
||||
prediction = Box(0, 0.0, 0.0, 0.5, 0.05, 0.9)
|
||||
draw_boxes(image, [], [prediction], ["blackdot"], match_boxes([prediction], []))
|
||||
assert COLOR_UNMATCHED in _colors(image)
|
||||
|
||||
|
||||
# ---------------- 不重叠约束 ----------------
|
||||
|
||||
|
||||
def test_suppress_keeps_the_highest_score_of_an_overlapping_pair():
|
||||
"""同一处缺陷被框两次时,留置信度高的那个。"""
|
||||
strong = Box(0, 0.3, 0.3, 0.7, 0.7, 0.9)
|
||||
weak = Box(1, 0.35, 0.35, 0.75, 0.75, 0.4)
|
||||
assert suppress_overlaps([weak, strong]) == [1] # 下标 1 = strong
|
||||
|
||||
|
||||
def test_suppress_is_cross_class():
|
||||
"""NMS 只在同类内抑制;跨类重复要靠这一步。"""
|
||||
a = Box(0, 0.3, 0.3, 0.7, 0.7, 0.9)
|
||||
b = Box(5, 0.31, 0.31, 0.69, 0.69, 0.8)
|
||||
assert suppress_overlaps([a, b]) == [0]
|
||||
|
||||
|
||||
def test_suppress_keeps_disjoint_boxes():
|
||||
"""真正分开的多个缺陷不能被误伤。"""
|
||||
boxes = [
|
||||
Box(0, 0.0, 0.0, 0.2, 0.2, 0.9),
|
||||
Box(0, 0.5, 0.5, 0.7, 0.7, 0.8),
|
||||
Box(1, 0.8, 0.1, 0.95, 0.3, 0.7),
|
||||
]
|
||||
assert suppress_overlaps(boxes) == [0, 1, 2]
|
||||
|
||||
|
||||
def test_touching_edges_are_not_overlap():
|
||||
"""边缘相接、面积不相交,IoU=0,应当都保留。"""
|
||||
left = Box(0, 0.0, 0.0, 0.5, 1.0, 0.9)
|
||||
right = Box(0, 0.5, 0.0, 1.0, 1.0, 0.8)
|
||||
assert suppress_overlaps([left, right]) == [0, 1]
|
||||
|
||||
|
||||
def test_contained_box_is_dropped_even_when_iou_is_below_the_threshold():
|
||||
"""整图大框套一个小框:面积悬殊导致 IoU 很小,但显然是同一处目标的重复表达。
|
||||
|
||||
这正是 `contained` 的用武之地——只在允许轻微重叠(max_iou>0)时才有区别,
|
||||
因为 max_iou=0 时任何包含都已经因 IoU>0 被抑制了。
|
||||
"""
|
||||
big = Box(0, 0.0, 0.0, 1.0, 1.0, 0.9)
|
||||
small = Box(1, 0.45, 0.45, 0.55, 0.55, 0.8)
|
||||
assert iou(big, small) < 0.02
|
||||
|
||||
assert suppress_overlaps([big, small], max_iou=0.1) == [0]
|
||||
assert suppress_overlaps([big, small], max_iou=0.1, contained=False) == [0, 1]
|
||||
|
||||
|
||||
def test_max_iou_allows_slight_overlap():
|
||||
a = Box(0, 0.0, 0.0, 0.5, 0.5, 0.9)
|
||||
b = Box(0, 0.4, 0.4, 0.9, 0.9, 0.8) # IoU 约 0.02
|
||||
assert suppress_overlaps([a, b]) == [0]
|
||||
assert suppress_overlaps([a, b], max_iou=0.1) == [0, 1]
|
||||
|
||||
|
||||
def test_suppress_handles_empty_and_single():
|
||||
assert suppress_overlaps([]) == []
|
||||
assert suppress_overlaps([Box(0, 0.1, 0.1, 0.2, 0.2, 0.5)]) == [0]
|
||||
|
||||
Reference in New Issue
Block a user