Revert "约束预测框互不重叠"(eba533a、9439cbb)

去掉强制非重叠:detect.py 不再给 DetectionValidator.postprocess 打补丁,
配置里移除 train.max_overlap 与 agnostic_nms,可视化脚本恢复为直接使用模型原始输出,
engine/detection 里的 suppress_overlaps / filter_overlapping_outputs 一并移除。

约束会把 mAP50 从 0.555 压到 0.483(precision 0.536->0.612),代价是按置信度抑制时
高分错框会压掉本可命中的低分框。需要时用 git revert 这次提交即可取回。

按类别分目录的可视化产出结构(36fff98)保留。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-18 15:34:13 +08:00
co-authored by Claude Opus 5
parent 36fff984b7
commit 154677032f
5 changed files with 5 additions and 273 deletions
-116
View File
@@ -10,12 +10,10 @@ from engine.detection import ( # noqa: E402
COLOR_UNMATCHED,
Box,
draw_boxes,
filter_overlapping_outputs,
from_yolo,
iou,
match_boxes,
parse_label,
suppress_overlaps,
)
@@ -131,117 +129,3 @@ 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]
# ---------------- 验证器输出的过滤 ----------------
def test_filter_overlapping_outputs_drops_the_lower_score():
"""ultralytics 验证器的输出是张量字典,筛选要对每个"每框一行"的张量同步生效。"""
torch = pytest.importorskip("torch")
output = {
"bboxes": torch.tensor([[0.0, 0.0, 10.0, 10.0], [1.0, 1.0, 11.0, 11.0],
[50.0, 50.0, 60.0, 60.0]]),
"conf": torch.tensor([0.9, 0.5, 0.8]),
"cls": torch.tensor([0.0, 1.0, 2.0]),
"extra": torch.zeros((3, 0)),
}
filtered = filter_overlapping_outputs([output])[0]
assert filtered["conf"].tolist() == pytest.approx([0.9, 0.8]) # 中间那个与第一个重叠
assert filtered["cls"].tolist() == [0.0, 2.0]
assert filtered["bboxes"].shape == (2, 4)
assert filtered["extra"].shape == (2, 0)
def test_filter_keeps_the_object_untouched_when_nothing_overlaps():
torch = pytest.importorskip("torch")
output = {
"bboxes": torch.tensor([[0.0, 0.0, 1.0, 1.0], [5.0, 5.0, 6.0, 6.0]]),
"conf": torch.tensor([0.9, 0.8]),
"cls": torch.tensor([0.0, 0.0]),
}
filtered = filter_overlapping_outputs([output])
assert filtered[0] is output # 没有变化时原样返回,不做无谓的拷贝
def test_filter_handles_images_with_no_detection():
torch = pytest.importorskip("torch")
empty = {"bboxes": torch.zeros((0, 4)), "conf": torch.zeros(0), "cls": torch.zeros(0)}
assert filter_overlapping_outputs([empty])[0]["conf"].numel() == 0
def test_filter_can_empty_an_image_entirely():
"""全部互相重叠时只留一个;这里构造两个几乎重合的框。"""
torch = pytest.importorskip("torch")
output = {
"bboxes": torch.tensor([[0.0, 0.0, 10.0, 10.0], [0.1, 0.1, 10.1, 10.1]]),
"conf": torch.tensor([0.3, 0.9]),
"cls": torch.tensor([1.0, 1.0]),
}
filtered = filter_overlapping_outputs([output])[0]
assert filtered["conf"].tolist() == pytest.approx([0.9])