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
@@ -140,6 +140,46 @@ def _inside(inner: Box, outer: Box) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def filter_overlapping_outputs(
|
||||
outputs: list[dict], max_iou: float = 0.0, contained: bool = True
|
||||
) -> list[dict]:
|
||||
"""给 ultralytics 验证器 postprocess 的输出补上"框不重叠"约束。
|
||||
|
||||
每个元素是一张图的结果:`bboxes`(xyxy)、`conf`、`cls` 等张量。坐标是不是归一化的
|
||||
无所谓,IoU 与尺度无关。
|
||||
|
||||
为什么要在这里补:yolo26 这类**端到端模型不走 IoU 抑制**。ultralytics 的
|
||||
`non_max_suppression` 对它们早退(`utils/nms.py` 里 `prediction.shape[-1] == 6 or end2end`
|
||||
的分支),只按置信度与 `max_det` 截断,传入的 `iou` 和 `agnostic` 都不生效;
|
||||
`agnostic_nms` 只作用于检测头的 top-k(每个 anchor 取一个类别),压不掉不同 anchor
|
||||
产生的重叠框。不补这一步,训练期的验证指标就和最终推理对不上。
|
||||
"""
|
||||
filtered = []
|
||||
for output in outputs:
|
||||
boxes = [
|
||||
Box(int(class_id), *(float(v) for v in xyxy), float(score))
|
||||
for xyxy, score, class_id in zip(
|
||||
output["bboxes"].tolist(), output["conf"].tolist(), output["cls"].tolist(),
|
||||
strict=True,
|
||||
)
|
||||
]
|
||||
kept = suppress_overlaps(boxes, max_iou=max_iou, contained=contained)
|
||||
if len(kept) == len(boxes):
|
||||
filtered.append(output)
|
||||
continue
|
||||
# 张量用列表下标筛选;空结果不能用 [] 索引,改用零长切片
|
||||
filtered.append({
|
||||
key: (value[kept] if kept else value[:0]) if _is_per_box(value, len(boxes)) else value
|
||||
for key, value in output.items()
|
||||
})
|
||||
return filtered
|
||||
|
||||
|
||||
def _is_per_box(value, count: int) -> bool:
|
||||
"""这个张量是不是"每个框一行"——只有这种才跟着筛选。"""
|
||||
return hasattr(value, "shape") and len(getattr(value, "shape", ())) >= 1 and len(value) == count
|
||||
|
||||
|
||||
def draw_boxes(image, ground_truth: Sequence[Box], predictions: Sequence[Box],
|
||||
classes: Sequence[str], match: Match, width: int = 2):
|
||||
"""把真值与预测画在图上(就地修改传入的 PIL 图像并返回)。
|
||||
|
||||
Reference in New Issue
Block a user