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:
co-authored by
Claude Opus 5
parent
2a92275317
commit
474b7b50a0
@@ -25,6 +25,9 @@ train / val 不分目录,但 `index.csv` 里有 `split` 列——训练集的
|
||||
同样地,"命中 / 漏检 / 误检"记在 `index.csv` 的 `outcome` 列里,按它排序筛选即可。
|
||||
|
||||
`--only errors` 只输出有漏检或误检的图,扫起来快得多。
|
||||
|
||||
模型的输入固定为 `--tile-size`(默认 160,与训练一致)时,图会按同样规则切片推理再拼回,
|
||||
全程不缩放。`--tile-size 0` 退回整图推理(ultralytics 自己 letterbox),用于和旧模型对比。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -43,6 +46,7 @@ import yaml # noqa: E402
|
||||
|
||||
from datasets.yolo import IMAGE_SUFFIXES # noqa: E402
|
||||
from engine.detection import Box, draw_boxes, match_boxes, parse_label # noqa: E402
|
||||
from engine.tiled_inference import load_image, predict_image # noqa: E402
|
||||
from utils import setup_logging # noqa: E402
|
||||
from utils.device import DeviceNotAvailableError, describe_device, resolve_device # noqa: E402
|
||||
|
||||
@@ -124,6 +128,11 @@ def main() -> int:
|
||||
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("--batch", type=int, default=16)
|
||||
parser.add_argument(
|
||||
"--tile-size", type=int, default=160,
|
||||
help="模型输入边长;按它切片推理再拼回,全程不缩放。0=整图推理(会 letterbox)",
|
||||
)
|
||||
parser.add_argument("--tile-overlap", type=int, default=32, help="切片重叠像素")
|
||||
parser.add_argument(
|
||||
"--only", choices=("all", "errors"), default="all",
|
||||
help="errors=只输出有漏检或误检的图,统计仍按全量计",
|
||||
@@ -150,7 +159,16 @@ 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", args.conf, args.iou_threshold, args.imgsz)
|
||||
if args.tile_size > 0:
|
||||
logger.info(
|
||||
"conf=%.2f 命中 IoU=%.2f 切片 %dx%d 重叠 %d(不缩放)",
|
||||
args.conf, args.iou_threshold, args.tile_size, args.tile_size, args.tile_overlap,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"conf=%.2f 命中 IoU=%.2f 整图推理 imgsz=%d —— ultralytics 会 letterbox 缩放",
|
||||
args.conf, args.iou_threshold, args.imgsz,
|
||||
)
|
||||
|
||||
if out_dir.exists() and any(out_dir.iterdir()):
|
||||
if not args.overwrite:
|
||||
@@ -172,26 +190,13 @@ def main() -> int:
|
||||
rows = []
|
||||
processed = 0
|
||||
|
||||
def predict_in_batches(images):
|
||||
"""自己按批切分:ultralytics 的 predict 不理会 batch 参数,
|
||||
一次性把整个列表当成一个 batch,上万张图必然 OOM。"""
|
||||
for start in range(0, len(images), args.batch):
|
||||
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,
|
||||
)
|
||||
yield from zip(chunk, results, strict=True)
|
||||
|
||||
for split, images in splits.items():
|
||||
for image_path, result in predict_in_batches(images):
|
||||
predictions = [
|
||||
Box(int(cls), *(float(v) for v in xyxy), float(score))
|
||||
for xyxy, cls, score in zip(
|
||||
result.boxes.xyxyn.tolist(), result.boxes.cls.tolist(),
|
||||
result.boxes.conf.tolist(), strict=True,
|
||||
)
|
||||
]
|
||||
for image_path in images:
|
||||
predictions = predict_image(
|
||||
model, load_image(image_path), size=args.tile_size, overlap=args.tile_overlap,
|
||||
conf=args.conf, device=device_arg, batch=args.batch,
|
||||
whole_image_imgsz=args.imgsz,
|
||||
)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user