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
@@ -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 图像并返回)。
|
||||
|
||||
Reference in New Issue
Block a user