feat(detect): 训练期的验证也约束预测框互不重叠
ultralytics 没有可用的开关:yolo26 是端到端模型,non_max_suppression 对它早退 (utils/nms.py 里 end2end 分支),只按置信度与 max_det 截断,传入的 iou 和 agnostic 都不参与 IoU 抑制;agnostic_nms 只作用于检测头 top-k(每个 anchor 取一个类别), 压不掉不同 anchor 产生的重叠框。 所以给 DetectionValidator.postprocess 包一层 filter_overlapping_outputs。训练期验证、 早停、best.pt 的选择都随之遵守这条约束,指标与最终推理一致。配置项 train.max_overlap (默认 0,负数关闭),由 detect.py 摘出后自行实现,不传给 ultralytics。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eba533a5b2
commit
9439cbb56a
@@ -10,6 +10,7 @@ from engine.detection import ( # noqa: E402
|
||||
COLOR_UNMATCHED,
|
||||
Box,
|
||||
draw_boxes,
|
||||
filter_overlapping_outputs,
|
||||
from_yolo,
|
||||
iou,
|
||||
match_boxes,
|
||||
@@ -190,3 +191,57 @@ def test_max_iou_allows_slight_overlap():
|
||||
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