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:
co-authored by
Claude Opus 5
parent
36fff984b7
commit
154677032f
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user