| """SAHI 切片推理评估 — 对比不同切片参数的mAP50-95 |
| |
| 原理: |
| 大图 (3200x1800) 切成重叠小块 → 每块独立检测 → WBF/NMS融合 |
| 小目标在切片中变大 → 定位精度显著提升 |
| |
| 测试配置: |
| 1. 基线: 全图推理 (当前最佳方式) |
| 2. SAHI 640切片 |
| 3. SAHI 960切片 |
| 4. SAHI 1280切片 |
| """ |
| import sys, os, time, json |
| import numpy as np |
| from pathlib import Path |
|
|
| os.chdir("/home/user/goat") |
| sys.path.insert(0, "/home/user/goat") |
|
|
| from ultralytics import YOLO |
| from sahi import AutoDetectionModel |
| from sahi.predict import get_sliced_prediction, get_prediction |
| from sahi.utils.coco import Coco, CocoAnnotation, CocoImage, CocoPrediction |
| import glob |
| from PIL import Image |
|
|
|
|
| def load_gt_boxes(lbl_path, img_w, img_h): |
| """读取YOLO格式GT, 返回xyxy列表""" |
| boxes = [] |
| if not os.path.exists(lbl_path): |
| return boxes |
| with open(lbl_path) as f: |
| for line in f: |
| parts = line.strip().split() |
| if len(parts) < 5: |
| continue |
| cls, cx, cy, w, h = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) |
| x1 = (cx - w/2) * img_w |
| y1 = (cy - h/2) * img_h |
| x2 = (cx + w/2) * img_w |
| y2 = (cy + h/2) * img_h |
| boxes.append([x1, y1, x2, y2, cls]) |
| return boxes |
|
|
|
|
| def compute_iou(box1, box2): |
| x1 = max(box1[0], box2[0]); y1 = max(box1[1], box2[1]) |
| x2 = min(box1[2], box2[2]); y2 = min(box1[3], box2[3]) |
| inter = max(0, x2-x1) * max(0, y2-y1) |
| a1 = (box1[2]-box1[0]) * (box1[3]-box1[1]) |
| a2 = (box2[2]-box2[0]) * (box2[3]-box2[1]) |
| return inter / (a1 + a2 - inter + 1e-7) |
|
|
|
|
| def compute_ap_at_iou(gt_boxes_all, pred_boxes_all, iou_threshold): |
| """计算单个IoU阈值下的AP""" |
| all_preds = [] |
| n_gt_total = 0 |
| |
| for img_idx, (gts, preds) in enumerate(zip(gt_boxes_all, pred_boxes_all)): |
| n_gt_total += len(gts) |
| gt_matched = [False] * len(gts) |
| |
| for pred in preds: |
| best_iou, best_gt = 0, -1 |
| for gi, gt in enumerate(gts): |
| iou = compute_iou(pred[:4], gt[:4]) |
| if iou > best_iou: |
| best_iou = iou |
| best_gt = gi |
| |
| if best_iou >= iou_threshold and best_gt >= 0 and not gt_matched[best_gt]: |
| all_preds.append((pred[4], 1, img_idx)) |
| gt_matched[best_gt] = True |
| else: |
| all_preds.append((pred[4], 0, img_idx)) |
| |
| if n_gt_total == 0: |
| return 0.0 |
| |
| |
| all_preds.sort(key=lambda x: -x[0]) |
| |
| tp_cum, fp_cum = 0, 0 |
| precisions, recalls = [], [] |
| |
| for conf, is_tp, _ in all_preds: |
| if is_tp: |
| tp_cum += 1 |
| else: |
| fp_cum += 1 |
| precisions.append(tp_cum / (tp_cum + fp_cum)) |
| recalls.append(tp_cum / n_gt_total) |
| |
| |
| ap = 0 |
| for t in np.linspace(0, 1, 101): |
| p_at_r = 0 |
| for p, r in zip(precisions, recalls): |
| if r >= t: |
| p_at_r = max(p_at_r, p) |
| ap += p_at_r / 101 |
| |
| return ap |
|
|
|
|
| def eval_config(model_path, val_img_dir, val_lbl_dir, slice_size=None, overlap_ratio=0.2, imgsz=1536, conf=0.25): |
| """评估一种推理配置""" |
| detection_model = AutoDetectionModel.from_pretrained( |
| model_type="ultralytics", |
| model_path=model_path, |
| confidence_threshold=conf, |
| device="cuda:0", |
| ) |
| |
| imgs = sorted(glob.glob(os.path.join(val_img_dir, "*"))) |
| |
| gt_boxes_all = [] |
| pred_boxes_all = [] |
| |
| t0 = time.time() |
| for idx, img_path in enumerate(imgs): |
| basename = os.path.splitext(os.path.basename(img_path))[0] |
| lbl_path = os.path.join(val_lbl_dir, basename + ".txt") |
| |
| img = Image.open(img_path) |
| W, H = img.size |
| |
| gts = load_gt_boxes(lbl_path, W, H) |
| gt_boxes_all.append(gts) |
| |
| if slice_size: |
| result = get_sliced_prediction( |
| img_path, |
| detection_model, |
| slice_height=slice_size, |
| slice_width=slice_size, |
| overlap_height_ratio=overlap_ratio, |
| overlap_width_ratio=overlap_ratio, |
| perform_standard_pred=True, |
| postprocess_type="NMS", |
| postprocess_match_threshold=0.5, |
| verbose=0, |
| ) |
| else: |
| result = get_prediction( |
| img_path, |
| detection_model, |
| verbose=0, |
| ) |
| |
| preds = [] |
| for pred in result.object_prediction_list: |
| bb = pred.bbox |
| preds.append([bb.minx, bb.miny, bb.maxx, bb.maxy, pred.score.value]) |
| pred_boxes_all.append(preds) |
| |
| if (idx+1) % 20 == 0: |
| print(f" [{idx+1}/{len(imgs)}]...") |
| |
| elapsed = time.time() - t0 |
| |
| |
| iou_thresholds = np.arange(0.5, 1.0, 0.05) |
| aps = [] |
| ap_details = {} |
| for iou_t in iou_thresholds: |
| ap = compute_ap_at_iou(gt_boxes_all, pred_boxes_all, iou_t) |
| aps.append(ap) |
| ap_details[f"AP@{int(iou_t*100)}"] = ap |
| |
| map50 = aps[0] |
| map5095 = np.mean(aps) |
| |
| return map50, map5095, ap_details, elapsed |
|
|
|
|
| def main(): |
| model_path = "runs/detect/Detection_experiments/v6_1_s_refined/weights/best.pt" |
| val_img_dir = "Data/Detection_dataset/images/val" |
| val_lbl_dir = "Data/Detection_dataset/labels/val" |
| |
| configs = [ |
| ("全图推理 (baseline)", None, 0.2), |
| ("SAHI 切片 640", 640, 0.25), |
| ("SAHI 切片 960", 960, 0.25), |
| ("SAHI 切片 1280", 1280, 0.2), |
| ] |
| |
| results = [] |
| for name, slice_sz, overlap in configs: |
| print(f"\n{'='*50}") |
| print(f" 测试: {name}") |
| print(f"{'='*50}") |
| |
| map50, map5095, details, elapsed = eval_config( |
| model_path, val_img_dir, val_lbl_dir, |
| slice_size=slice_sz, overlap_ratio=overlap, |
| ) |
| |
| results.append((name, map50, map5095, details, elapsed)) |
| print(f" mAP50={map50:.4f} mAP50-95={map5095:.4f} 用时={elapsed:.1f}s") |
| for k in ["AP@50","AP@75","AP@80","AP@90","AP@95"]: |
| print(f" {k} = {details[k]:.4f}") |
| |
| print(f"\n\n{'='*70}") |
| print(f"{'配置':<25} {'mAP50':>8} {'mAP50-95':>10} {'AP@75':>8} {'AP@90':>8} {'耗时':>8}") |
| print(f"{'='*70}") |
| for name, m50, m5095, det, t in results: |
| print(f"{name:<25} {m50:>8.4f} {m5095:>10.4f} {det['AP@75']:>8.4f} {det['AP@90']:>8.4f} {t:>7.1f}s") |
| print(f"{'='*70}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|