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
+3 -41
View File
@@ -25,10 +25,6 @@ train / val 不分目录,但 `index.csv` 里有 `split` 列——训练集的
同样地,"命中 / 漏检 / 误检"记在 `index.csv` 的 `outcome` 列里,按它排序筛选即可。
`--only errors` 只输出有漏检或误检的图,扫起来快得多。
**预测框默认约束为互不重叠**(`--max-overlap 0`)。人工标注的真值里一处缺陷只有一个框、
零重叠,而 NMS 只在同类内按固定阈值抑制,留得下跨类重复和 IoU 低于阈值的重叠框。
`--max-overlap -1` 可关掉这层约束,看模型的原始输出。
"""
from __future__ import annotations
@@ -46,13 +42,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import yaml # noqa: E402
from datasets.yolo import IMAGE_SUFFIXES # noqa: E402
from engine.detection import ( # noqa: E402
Box,
draw_boxes,
match_boxes,
parse_label,
suppress_overlaps,
)
from engine.detection import Box, draw_boxes, match_boxes, parse_label # noqa: E402
from utils import setup_logging # noqa: E402
from utils.device import DeviceNotAvailableError, describe_device, resolve_device # noqa: E402
@@ -133,16 +123,6 @@ def main() -> int:
parser.add_argument("--imgsz", type=int, default=640)
parser.add_argument("--conf", type=float, default=0.25, help="置信度阈值")
parser.add_argument("--iou-threshold", type=float, default=0.5, help="判定命中的 IoU")
parser.add_argument(
"--max-overlap", type=float, default=0.0,
help="预测框之间允许的最大 IoU,超出的丢低分框。0=完全不允许重叠(默认,与真值一致),"
"-1=不约束,看模型原始输出",
)
parser.add_argument("--nms-iou", type=float, default=0.7, help="NMS 的 IoU 阈值")
parser.add_argument(
"--no-agnostic-nms", action="store_true",
help="关掉类别无关 NMS。默认开启,用来压掉同一处缺陷被两个类别各框一次的重复",
)
parser.add_argument("--batch", type=int, default=16)
parser.add_argument(
"--only", choices=("all", "errors"), default="all",
@@ -170,12 +150,7 @@ def main() -> int:
"数据集: %s(%d 类,%s)", args.data, len(classes),
",".join(f"{name} {len(images)} 张" for name, images in splits.items()),
)
logger.info(
"conf=%.2f 命中 IoU=%.2f imgsz=%d NMS IoU=%.2f 类别无关 NMS=%s 框间最大重叠=%s",
args.conf, args.iou_threshold, args.imgsz, args.nms_iou,
not args.no_agnostic_nms,
"不约束" if args.max_overlap < 0 else f"{args.max_overlap:.2f}",
)
logger.info("conf=%.2f IoU=%.2f imgsz=%d", args.conf, args.iou_threshold, args.imgsz)
if out_dir.exists() and any(out_dir.iterdir()):
if not args.overwrite:
@@ -196,7 +171,6 @@ def main() -> int:
scores: dict[str, list[float]] = defaultdict(list)
rows = []
processed = 0
suppressed = 0
def predict_in_batches(images):
"""自己按批切分:ultralytics 的 predict 不理会 batch 参数,
@@ -205,8 +179,7 @@ def main() -> int:
chunk = images[start:start + args.batch]
results = model.predict(
source=[str(p) for p in chunk], imgsz=args.imgsz, conf=args.conf,
device=device_arg, verbose=False, iou=args.nms_iou,
agnostic_nms=not args.no_agnostic_nms,
device=device_arg, verbose=False,
)
yield from zip(chunk, results, strict=True)
@@ -219,11 +192,6 @@ def main() -> int:
result.boxes.conf.tolist(), strict=True,
)
]
if args.max_overlap >= 0:
kept = suppress_overlaps(predictions, max_iou=args.max_overlap)
suppressed += len(predictions) - len(kept)
predictions = [predictions[i] for i in kept]
label_file = label_path_for(image_path)
truth = parse_label(label_file.read_text(encoding="utf-8")) if label_file.exists() else []
match = match_boxes(predictions, truth, args.iou_threshold)
@@ -273,10 +241,6 @@ def main() -> int:
"conf": args.conf,
"iou_threshold": args.iou_threshold,
"imgsz": args.imgsz,
"nms_iou": args.nms_iou,
"agnostic_nms": not args.no_agnostic_nms,
"max_overlap": args.max_overlap,
"suppressed_overlapping_boxes": suppressed,
"outcomes": {split: dict(counter) for split, counter in outcomes.items()},
"per_class": {
split: {
@@ -293,8 +257,6 @@ def main() -> int:
json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
)
if suppressed:
logger.info("因与更高分的框重叠而丢弃: %d 个", suppressed)
for split, counter in outcomes.items():
shown = sum(counter[k] for k in ("missed", "extra"))
logger.info(