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
-78
View File
@@ -102,84 +102,6 @@ def match_boxes(
)
def suppress_overlaps(
boxes: Sequence[Box], max_iou: float = 0.0, contained: bool = True
) -> list[int]:
"""贪心地挑出互不重叠的框,返回保留下来的下标(按输入顺序)。
按置信度从高到低考察,与任一已保留框重叠超过 `max_iou` 就丢弃。
为什么需要这一步:NMS 只在**同类**内、且按固定 IoU 阈值抑制,所以同一处缺陷被两个
类别各框一次(跨类重复),或两个框 IoU 低于 NMS 阈值时,都会留下重叠框。而人工标注的
真值里一处缺陷只有一个框,零重叠——推理侧不加这层约束,指标和图上都会出现真值里
不可能有的形态。
Args:
max_iou: 允许的最大 IoU。0 表示碰到就算重叠,完全不允许。
contained: 是否额外丢弃被已保留框完全包住的框。整图大框套小框时 IoU 可能很小
(面积悬殊),但那显然是同一处目标的重复表达。
"""
order = sorted(range(len(boxes)), key=lambda i: -(boxes[i].score or 0.0))
kept: list[int] = []
for index in order:
box = boxes[index]
if any(
iou(box, boxes[other]) > max_iou or (contained and _inside(box, boxes[other]))
for other in kept
):
continue
kept.append(index)
return sorted(kept)
def _inside(inner: Box, outer: Box) -> bool:
"""inner 是否完全落在 outer 里(含边界)。"""
return (
outer.x1 <= inner.x1 and outer.y1 <= inner.y1
and outer.x2 >= inner.x2 and outer.y2 >= inner.y2
)
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 图像并返回)。