feat(detect): 推理侧改为切片,与固定 160 输入一致

训练数据已经是固定边长的片,推理还把整图交给 ultralytics 就会被 letterbox 缩放,
与模型见过的形态不符。三个调用方(可视化、批量推送预测、ML backend)统一改成
切片推理再拼回原图。

- engine/tiling.merge_tiles / tiled_predict:映射回原图、裁掉镜像填充区的框、跨片去重。
  推理框架留在调用方,几何部分可单测。
- engine/detection.nms:标准 NMS,另加按包含比例抑制。缺陷被切割线切开时,相邻片
  只看到一角,给出的小残框与完整框 IoU 很小(面积悬殊),光靠 IoU 压不掉。
- engine/tiled_inference:ultralytics 的接线,内部分批,避免片数多时 OOM。
- 三处调用点加 --tile-size / --tile-overlap(后端为 YOLO_TILE_SIZE / YOLO_TILE_OVERLAP),
  默认 160 / 32;设 0 退回整图推理,仅用于和旧模型对比。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Shaun Zhou
2026-09-23 15:12:35 +08:00
co-authored by Claude Opus 5
parent 2a92275317
commit 474b7b50a0
9 changed files with 412 additions and 75 deletions
+47
View File
@@ -102,6 +102,53 @@ def match_boxes(
)
def containment(inner: Box, outer: Box) -> float:
"""inner 有多大比例落在 outer 里(0~1)。面积悬殊时 IoU 会很小,但这个值接近 1。"""
left, top = max(inner.x1, outer.x1), max(inner.y1, outer.y1)
right, bottom = min(inner.x2, outer.x2), min(inner.y2, outer.y2)
if right <= left or bottom <= top:
return 0.0
area = max(inner.x2 - inner.x1, 0) * max(inner.y2 - inner.y1, 0)
return (right - left) * (bottom - top) / area if area > 0 else 0.0
def nms(
boxes: Sequence[Box],
iou_threshold: float = 0.5,
per_class: bool = True,
contained_threshold: float = 0.0,
) -> list[int]:
"""非极大值抑制,返回保留下来的下标(按输入顺序)。
用于把切片推理的结果拼回整图:相邻片在重叠区会把同一处缺陷各检出一次,
合并后必须去重。注意这与"禁止框重叠"是两回事——这里只压掉重复,
真正分开的两个缺陷即使轻微相邻也都保留。
Args:
per_class: 只在同类之间抑制。不同类别的框重叠往往是有意义的分歧,不该互相压制。
contained_threshold: 额外按**包含比例**抑制,0 = 关闭。切片时缺陷被切割线切开,
相邻片只看到一角,会给出一个小残框;它与完整框的 IoU 很小(面积悬殊),
光靠 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]
duplicate = False
for other in kept:
if per_class and boxes[other].class_id != box.class_id:
continue
if iou(box, boxes[other]) > iou_threshold:
duplicate = True
break
if contained_threshold and containment(box, boxes[other]) >= contained_threshold:
duplicate = True
break
if not duplicate:
kept.append(index)
return sorted(kept)
def draw_boxes(image, ground_truth: Sequence[Box], predictions: Sequence[Box],
classes: Sequence[str], match: Match, width: int = 2):
"""把真值与预测画在图上(就地修改传入的 PIL 图像并返回)。