broadleaf-weed-detector / scripts /tiled_infer_eval.py
rgthelen's picture
Broadleaf weed detector: yolo11n/s + Hailo-10H HEFs + full dataset + repro scripts
33475e9 verified
Raw
History Blame Contribute Delete
4.17 kB
#!/usr/bin/env python3
"""Compare full-frame vs TILED inference for the fine-tuned footage model.
Tiled: run the detector on each tile of a grid (weeds appear bigger → in the
model's resolvable range), map boxes back, NMS. Report per-frame detection
rate + recall vs the tiled GT (fraction of GT weeds with an overlapping pred).
"""
import argparse
import glob
from pathlib import Path
from collections import defaultdict
from ultralytics import YOLO
from PIL import Image
GT = Path("/opt/weeds/footage/gt_tiled")
def iou(a, b):
ix1, iy1 = max(a[0], b[0]), max(a[1], b[1])
ix2, iy2 = min(a[2], b[2]), min(a[3], b[3])
iw, ih = max(0, ix2-ix1), max(0, iy2-iy1)
inter = iw*ih
ua = (a[2]-a[0])*(a[3]-a[1]) + (b[2]-b[0])*(b[3]-b[1]) - inter
return inter/ua if ua > 0 else 0
def nms(boxes, thr=0.5):
boxes = sorted(boxes, key=lambda b: -b[4])
keep = []
for b in boxes:
if all(iou(b[:4], k[:4]) < thr for k in keep):
keep.append(b)
return keep
def full_preds(model, img, conf, imgsz=1280):
r = model.predict(img, imgsz=imgsz, conf=conf, verbose=False)[0]
return [[*map(float, b.xyxy[0].tolist()), float(b.conf[0])] for b in r.boxes]
def tiled_preds(model, img_path, conf, grid=(3, 2), imgsz=640, overlap=0.12):
im = Image.open(img_path); W, H = im.size
gx, gy = (grid[1], grid[0]) if H > W else grid
tw, th = W//gx, H//gy
ox, oy = int(tw*overlap), int(th*overlap)
boxes = []
for j in range(gy):
for i in range(gx):
x0 = max(0, i*tw-ox); y0 = max(0, j*th-oy)
x1 = min(W, (i+1)*tw+ox); y1 = min(H, (j+1)*th+oy)
tile = im.crop((x0, y0, x1, y1))
tmp = Path("/tmp")/f"ti_{Path(img_path).stem}_{i}_{j}.jpg"
tile.convert("RGB").save(tmp, quality=90)
r = model.predict(str(tmp), imgsz=imgsz, conf=conf, verbose=False)[0]
tmp.unlink(missing_ok=True)
for b in r.boxes:
bx = b.xyxy[0].tolist()
boxes.append([x0+bx[0], y0+bx[1], x0+bx[2], y0+bx[3], float(b.conf[0])])
return nms(boxes, 0.5)
def gt_boxes(stem):
W = H = None
ip = GT/"images"/f"{stem}.jpg"
with Image.open(ip) as im:
W, H = im.size
out = []
for ln in (GT/"labels"/f"{stem}.txt").read_text().splitlines():
if not ln.strip():
continue
_, cx, cy, bw, bh = map(float, ln.split())
out.append([(cx-bw/2)*W, (cy-bh/2)*H, (cx+bw/2)*W, (cy+bh/2)*H])
return out
def recall(preds, gts, thr=0.3):
if not gts:
return None
hit = 0
for g in gts:
if any(iou(p[:4], g) >= thr for p in preds):
hit += 1
return hit/len(gts)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model", required=True)
ap.add_argument("--imgsz-full", type=int, default=1280)
ap.add_argument("--imgsz-tile", type=int, default=640)
ap.add_argument("--clip", default="", help="restrict to one clip prefix; blank = all frames")
ap.add_argument("--grid", default="3x2")
args = ap.parse_args()
gx, gy = (int(v) for v in args.grid.lower().split("x"))
model = YOLO(args.model)
pat = f"{args.clip}*.txt" if args.clip else "*.txt"
stems = [Path(p).stem for p in glob.glob(str(GT/"labels"/pat))]
agg = defaultdict(lambda: [0.0, 0])
frames_hit = defaultdict(int)
for stem in stems:
gts = gt_boxes(stem)
ip = str(GT/'images'/f'{stem}.jpg')
for mode, preds in (("full", full_preds(model, ip, 0.25, args.imgsz_full)),
("tiled", tiled_preds(model, ip, 0.25, (gx, gy), args.imgsz_tile))):
r = recall(preds, gts)
if r is not None:
agg[mode][0] += r; agg[mode][1] += 1
if preds:
frames_hit[mode] += 1
n = len(stems)
for mode in ("full", "tiled"):
mr = agg[mode][0]/agg[mode][1] if agg[mode][1] else 0
print(f"{mode:6s} inference: mean per-frame weed-recall={mr:.3f} "
f"frame-detection-rate={frames_hit[mode]/n:.3f} (n={n})")
print("TILED_EVAL_DONE")
if __name__ == "__main__":
main()