feat(detect): 约束预测框互不重叠

人工标注的真值里一处缺陷只有一个框:11211 张图、151 张多框图,重叠框对为 0。
而模型预测在验证集上有 6.9% 的图存在重叠框——NMS 只在同类内、按固定 IoU 阈值抑制,
压不掉两类情况:同一处缺陷被两个类别各框一次(实测 IoU 高达 0.99),以及 IoU 低于
NMS 阈值的同类重叠(grease 66 对、wrinkle 45 对)。

- engine/detection.suppress_overlaps:按置信度贪心保留互不重叠的框,可设允许的
  最大 IoU;另可丢弃被大框完全包住的小框(面积悬殊时 IoU 很小,但显然是重复表达)。
- visualize_detections 默认 --max-overlap 0(与真值一致),并放出 --nms-iou 与
  类别无关 NMS 开关;--max-overlap -1 可看模型原始输出。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-18 11:19:47 +08:00
co-authored by Claude Opus 5
parent 43c8c4a179
commit eba533a5b2
3 changed files with 140 additions and 3 deletions
+38
View File
@@ -102,6 +102,44 @@ 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 draw_boxes(image, ground_truth: Sequence[Box], predictions: Sequence[Box],
classes: Sequence[str], match: Match, width: int = 2):
"""把真值与预测画在图上(就地修改传入的 PIL 图像并返回)。