diff --git a/src/perception/yolo/yolov8_urc_with_new_roboflow.py b/src/perception/yolo/yolov8_urc_with_new_roboflow.py new file mode 100644 index 0000000..9925fbf --- /dev/null +++ b/src/perception/yolo/yolov8_urc_with_new_roboflow.py @@ -0,0 +1,687 @@ +# -*- coding: utf-8 -*- +"""YOLOv8_URC_With_new_Roboflow.ipynb + +Automatically generated by Colab. + +Original file is located at + https://colab.research.google.com/drive/1tcFsIVcY6PlxC-Ks2JMHscw5n-eUQN5F +""" + +# === Cell A: CLEAN & INSTALL (run this first; it will restart the runtime) === +# This setup targets Python 3.12 + NumPy 2.x + recent Ultralytics. +!pip -q uninstall -y numpy ultralytics albumentations opencv-python roboflow numba jax jaxlib pytensor thinc opencv-contrib-python || true +!rm -rf /usr/local/lib/python3.12/dist-packages/numpy* /usr/local/lib/python3.12/dist-packages/__pycache__/numpy* + +!pip -q install --upgrade pip setuptools wheel +!pip -q install --no-cache-dir "numpy>=2.0,<2.3" "opencv-python>=4.10.0.84" "albumentations>=1.4.7" "ultralytics>=8.3.0" "roboflow>=1.1.36" "pyyaml>=6.0.1" + +# HARD RESTART so NumPy C-extensions reload cleanly +import os; os.kill(os.getpid(), 9) + +# === Cell B: Verify versions (run after the runtime restarts) === +import sys, numpy as np, cv2, albumentations, ultralytics +print("python:", sys.version) +print("numpy:", np.__version__, "@", np.__file__) +print("opencv:", cv2.__version__) +print("albumentations:", albumentations.__version__) +print("ultralytics:", ultralytics.__version__) + +# === Cell C: Config & Device === +import os, platform, torch + +# Optional: disable Weights & Biases prompt +os.environ["WANDB_MODE"] = "disabled" + +# TODO: set your PRIVATE Roboflow key + slugs here +os.environ["ROBOFLOW_API_KEY"] = "X4W9a8Imqz7AmOIFq3Xt" +WORKSPACE = "just-with-a-hammer" +PROJECT = "sdgdghdfhegwetb-g3xrx" +VERSION = 1 + +DEV = 0 if torch.cuda.is_available() else "cpu" +WORKERS = 2 if (DEV != "cpu" and platform.system() != "Windows") else 0 +print("[INFO] device:", DEV, "| workers:", WORKERS) +if DEV != "cpu": + print("[INFO] GPU:", torch.cuda.get_device_name(0)) + +# === Cell D: Download dataset (YOLOv8 format) === +from roboflow import Roboflow +rf = Roboflow(api_key=os.environ["ROBOFLOW_API_KEY"]) +ds = rf.workspace(WORKSPACE).project(PROJECT).version(VERSION).download("yolov8") +data_yaml = ds.location + "/data.yaml" +print("data.yaml →", data_yaml) + +# === Cell E (optional): Quick counts === +import glob, os, yaml +n_train = len(glob.glob(os.path.join(ds.location, "train/images/*"))) +n_val = len(glob.glob(os.path.join(ds.location, "valid/images/*"))) +n_test = len(glob.glob(os.path.join(ds.location, "test/images/*"))) +print(f"[INFO] images: train={n_train}, val={n_val}, test={n_test}") + +with open(data_yaml) as f: + cfg = yaml.safe_load(f) +print("classes:", cfg.get("names")) + +# === Cell F: Albumentations pre-aug (padding lines + input clamp) === +import os, glob, re, shutil +import cv2 +import albumentations as A + +# If ds isn't defined (e.g., after a restart), re-download quickly +try: + _ = ds.location +except NameError: + from roboflow import Roboflow + rf = Roboflow(api_key=os.environ["ROBOFLOW_API_KEY"]) + ds = rf.workspace(WORKSPACE).project(PROJECT).version(VERSION).download("yolov8") + +print("albumentations:", A.__version__) + +src_root = ds.location +train_src = os.path.join(src_root, "train") +val_src = os.path.join(src_root, "valid") +test_src = os.path.join(src_root, "test") + +AUG_ROOT = "/content/urc_aug_dataset" +N_AUG_PER_IMAGE = 1 # start with 1; try 2 later if results benefit + +def list_images(folder): + exts = ('.jpg','.jpeg','.png','.bmp','.tif','.tiff') + return [p for p in glob.glob(os.path.join(folder,'*')) if p.lower().endswith(exts)] + +def read_yolo_labels(lbl_path): + boxes, classes = [], [] + if not os.path.exists(lbl_path): return boxes, classes + for line in open(lbl_path): + parts = line.strip().split() + if len(parts)==5: + c,x,y,w,h = parts + boxes.append([float(x),float(y),float(w),float(h)]) + classes.append(int(c)) + return boxes, classes + +def write_yolo_labels(path, boxes, classes): + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path,'w') as f: + for b,c in zip(boxes,classes): + f.write(f"{c} {b[0]:.6f} {b[1]:.6f} {b[2]:.6f} {b[3]:.6f}\n") + +def copy_split(split_src, split_out): + os.makedirs(os.path.join(split_out,'images'), exist_ok=True) + os.makedirs(os.path.join(split_out,'labels'), exist_ok=True) + for ip in list_images(os.path.join(split_src,'images')): + base,ext = os.path.splitext(os.path.basename(ip)) + lp = os.path.join(split_src,'labels', base+'.txt') + shutil.copy2(ip, os.path.join(split_out,'images', base+ext)) + if os.path.exists(lp): + shutil.copy2(lp, os.path.join(split_out,'labels', base+'.txt')) + +# --- clamp helpers (prevents tiny negatives like -7.8e-06) --- +def _clip01(v): + return 0.0 if v < 0.0 else (1.0 if v > 1.0 else v) + +def clip_yolo_box(box_xywh): + # box in YOLO [x_center, y_center, w, h] (normalized) + x, y, w, h = box_xywh + x1 = _clip01(x - w/2.0); y1 = _clip01(y - h/2.0) + x2 = _clip01(x + w/2.0); y2 = _clip01(y + h/2.0) + if x2 <= x1 or y2 <= y1: + return None + cx = (x1 + x2) / 2.0; cy = (y1 + y2) / 2.0 + w2 = x2 - x1; h2 = y2 - y1 + return [cx, cy, w2, h2] + +def sanitize_boxes(boxes, classes): + """Clamp all boxes to [0,1] and drop degenerate/tiny ones.""" + out_b, out_c = [], [] + for b,c in zip(boxes, classes): + bb = clip_yolo_box(b) + if bb is not None and (bb[2]*bb[3]) > 1e-8: + out_b.append(bb); out_c.append(c) + return out_b, out_c + +# --- Albumentations pipeline --- +geom = A.Affine( + scale=(0.92, 1.08), + translate_percent=(-0.08, 0.08), + rotate=(-7, 7), + shear=(-4, 4), + p=0.6 +) + +photometric = A.OneOf([ + A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.25, p=1.0), + A.HueSaturationValue(hue_shift_limit=8, sat_shift_limit=12, val_shift_limit=12, p=1.0), +], p=0.6) + +compression = A.ImageCompression(quality_range=(35, 55), p=0.35) + +motion_noise = A.OneOf([ + A.MotionBlur(blur_limit=(7, 13), p=1.0), + A.GaussianBlur(blur_limit=(3, 7), p=1.0), + A.GaussNoise(var_limit=(10.0, 30.0), p=1.0), # if this errors on v2, ping me and I’ll swap to std_range +], p=0.35) + +occlusion = A.CoarseDropout( + max_holes=6, # if this errors on v2, ping me and I’ll swap to *_range + max_height=0.18, + max_width=0.18, + p=0.35 +) + +try: + persp = A.Perspective(scale=(0.05, 0.10), keep_size=True, p=0.30) +except AttributeError: + persp = A.NoOp() + +# === padding lines (visible border) === +PAD_PX = 2 # thickness in pixels +PAD_COLOR = 0 # 0=black, 255=white, or BGR tuple +padding = A.CropAndPad( + px=(PAD_PX, PAD_PX, PAD_PX, PAD_PX), # top, right, bottom, left + pad_cval=PAD_COLOR, + keep_size=False, # keep the larger image so border is visible + p=1.0 +) + +aug = A.Compose( + [padding, geom, photometric, motion_noise, compression, occlusion, persp], + bbox_params=A.BboxParams( + format='yolo', + label_fields=['class_labels'], + min_visibility=0.12 + ) +) + +def augment_split(split_src, split_out, n_aug=1): + os.makedirs(os.path.join(split_out,'images'), exist_ok=True) + os.makedirs(os.path.join(split_out,'labels'), exist_ok=True) + imgs = list_images(os.path.join(split_src,'images')) + print(f"Augmenting {os.path.basename(split_src)}: {len(imgs)} images") + + for ip in imgs: + base,ext = os.path.splitext(os.path.basename(ip)) + lp = os.path.join(split_src,'labels', base+'.txt') + img = cv2.cvtColor(cv2.imread(ip), cv2.COLOR_BGR2RGB) + boxes, classes = read_yolo_labels(lp) + + # NEW: sanitize input boxes BEFORE calling aug (this fixes your error) + boxes, classes = sanitize_boxes(boxes, classes) + + # copy originals (optional to also write sanitized original labels) + shutil.copy2(ip, os.path.join(split_out,'images', base+ext)) + if os.path.exists(lp): + write_yolo_labels(os.path.join(split_out,'labels', base+'.txt'), boxes, classes) + + for k in range(n_aug): + if boxes: + t = aug(image=img, bboxes=boxes, class_labels=classes) + img2, b2_raw, c2_raw = t['image'], t['bboxes'], t['class_labels'] + + # (Optional) sanitize again after transforms + b2, c2 = sanitize_boxes(b2_raw, c2_raw) + else: + t = (A.Compose([photometric, motion_noise, compression]))(image=img) + img2, b2, c2 = t['image'], [], [] + + out_ip = os.path.join(split_out,'images', f"{base}_aug{k}{ext}") + cv2.imwrite(out_ip, cv2.cvtColor(img2, cv2.COLOR_RGB2BGR)) + out_lp = os.path.join(split_out,'labels', f"{base}_aug{k}.txt") + write_yolo_labels(out_lp, b2, c2) + +# Build augmented dataset +if os.path.exists(AUG_ROOT): shutil.rmtree(AUG_ROOT) +augment_split(train_src, os.path.join(AUG_ROOT,'train'), n_aug=N_AUG_PER_IMAGE) +copy_split(val_src, os.path.join(AUG_ROOT,'valid')) # keep clean +copy_split(test_src, os.path.join(AUG_ROOT,'test')) # keep clean +print("Augmented dataset at:", AUG_ROOT) + +# === Cell G: Rebuild /content/urc_augmented.yaml safely === +import yaml, ast, re + +orig_yaml = os.path.join(src_root, "data.yaml") # from Roboflow download +with open(orig_yaml, "r") as f: + try: + base_cfg = yaml.safe_load(f) + except Exception: + base_cfg = None + +names = None +if isinstance(base_cfg, dict) and "names" in base_cfg: + names = base_cfg["names"] + +if names is None: + text = open(orig_yaml, "r").read() + m = re.search(r"names\s*:\s*(\[.*?\]|\{.*?\}|[^\n]+)", text, re.S) + if m: + chunk = m.group(1).strip() + try: + names = ast.literal_eval(chunk) + except Exception: + names = [chunk.replace("-", "").strip()] + +if isinstance(names, dict): + names = [names[k] for k in sorted(names.keys(), key=lambda x: int(x))] +if not isinstance(names, (list, tuple)): + names = [str(names)] + +new_cfg = { + "path": AUG_ROOT, + "train": "train/images", + "val": "valid/images", + "test": "test/images", + "names": list(names) +} + +new_yaml_path = "/content/urc_augmented.yaml" +with open(new_yaml_path, "w") as f: + yaml.safe_dump(new_cfg, f, sort_keys=False) + +print("Wrote:", new_yaml_path) +print(yaml.safe_dump(new_cfg, sort_keys=False)) + +# === Cell H: Train YOLOv8 (auto backoff) === +from ultralytics import YOLO + +MODEL = "yolov8s.pt" # use yolov8n.pt if VRAM is tight +IMGSZ = 640 +EPOCHS = 60 +BATCH_TRY = [8, 12, 16, 4] + +def train_with_backoff(): + for b in BATCH_TRY: + try: + print(f"[INFO] Trying batch={b}") + model = YOLO(MODEL) + return model.train( + data=new_yaml_path, + imgsz=IMGSZ, + epochs=EPOCHS, + batch=b, + device=DEV, + workers=WORKERS, + project="urc_runs", + name="yolov8s_aug_disk", + mosaic=0.15, mixup=0.0, + degrees=3.0, translate=0.06, scale=0.4, shear=1.5, perspective=0.0005, + hsv_h=0.01, hsv_s=0.4, hsv_v=0.25, fliplr=0.5, flipud=0.0 + ) + except RuntimeError as e: + if "out of memory" in str(e).lower() and DEV != "cpu": + import torch; torch.cuda.empty_cache() + print(f"[WARN] OOM at batch={b}, reducing…") + continue + raise + raise RuntimeError("All batch sizes failed. Try imgsz=512 or yolov8n.pt.") + +_ = train_with_backoff() +!ls -lah urc_runs/yolov8s_aug_disk/weights || true + +# === Cell I: Validate + quick visual check === +from ultralytics import YOLO +BEST = "urc_runs/yolov8s_aug_disk/weights/best.pt" +model = YOLO(BEST) +_ = model.val(data=new_yaml_path, imgsz=IMGSZ, device=DEV) +_ = model.predict(source=os.path.join(AUG_ROOT,"valid/images"), + imgsz=IMGSZ, conf=0.25, device=DEV, save=True) + +from IPython.display import display, Image +import glob, os + +pred_dir = "/content/runs/detect/predict" # change if you used a different name +imgs = sorted(glob.glob(os.path.join(pred_dir, "*.*"))) # jpg/png/jpeg +for p in imgs[:24]: # show first 24; tweak as you like + display(Image(filename=p)) + +# Commented out IPython magic to ensure Python compatibility. +# --- Install + verify Ultralytics in THIS notebook kernel --- +# %pip install -U --quiet ultralytics opencv-python + +import sys, site, importlib +print("Python:", sys.executable) +import pkgutil +print("ultralytics installed?", pkgutil.find_loader("ultralytics") is not None) + +# optional: show version +from ultralytics import __version__ as uver +print("Ultralytics version:", uver) + +import glob, os, time +from pathlib import Path + +def list_found(pats): + hits = [] + for pat in pats: + hits += glob.glob(pat, recursive=True) + hits = [h for h in hits if os.path.isfile(h)] + hits.sort(key=lambda p: os.path.getmtime(p)) + return hits + +pats = [ + "/content/**/weights/best.pt", + "/content/**/weights/last.pt", + "/content/**/weights/best.engine", + "/content/**/weights/best.onnx", + "/content/**/best.pt", # just in case + "/content/**/last.pt" +] +hits = list_found(pats) + +if not hits: + print("Nothing found under /content. Your runtime may have reset.") +else: + print("Found (oldest → newest):") + for h in hits: + print(time.ctime(os.path.getmtime(h)), " | ", h) + + # pick the newest as BEST and continue + BEST = hits[-1] + print("\nUsing:", BEST) + +from ultralytics import YOLO +from IPython.display import display, Image +import glob, os + +BEST = "/content/urc_runs/yolov8s_aug_disk/weights/best.pt" # <- change this to your model’s best.pt path +IMGS = "/content/urc_aug_dataset/valid/images" + +model = YOLO(BEST) +outdir = "/content/runs/detect/smoke" +_ = model.predict( + source=IMGS, imgsz=640, conf=0.35, iou=0.5, + save=True, save_conf=True, line_thickness=2, + project="/content/runs/detect", name="smoke", exist_ok=True +) + +for p in sorted(glob.glob(os.path.join(outdir, "*.*")))[:12]: + display(Image(filename=p)) + +# === Build a CSV of all detections from a YOLO run === +from pathlib import Path +import csv, re +from PIL import Image +import numpy as np + +RUN_DIR = Path("/content/runs/detect/predict") # change if your run is elsewhere +LABEL_DIR = RUN_DIR / "labels" +OUT_CSV = Path("/content/detections.csv") + +def find_images(root: Path): + exts = (".jpg",".jpeg",".png",".bmp",".tif",".tiff",".webp") + return sorted([p for p in root.iterdir() if p.suffix.lower() in exts]) + +def yolo_line_to_row(img_path: Path, img_size, names_map, parts): + # parts: class xc yc w h [conf] + has_conf = len(parts) == 6 + cls = int(float(parts[0])) + xc, yc, w, h = map(float, parts[1:5]) + W, H = img_size + # normalized -> pixel corners + x1 = (xc - w/2) * W + y1 = (yc - h/2) * H + x2 = (xc + w/2) * W + y2 = (yc + h/2) * H + return { + "image": str(img_path), + "class_id": cls, + "class_name": names_map.get(cls, str(cls)), + "conf": float(parts[5]) if has_conf else None, + "x1": x1, "y1": y1, "x2": x2, "y2": y2, + "w_px": x2-x1, "h_px": y2-y1, + "w_norm": w, "h_norm": h, + "img_w": W, "img_h": H + } + +rows = [] +names_map = {} # will fill if we have Results; otherwise stays numeric +label_files = sorted(LABEL_DIR.glob("*.txt")) +img_files = find_images(RUN_DIR) + +if label_files: + # --- Preferred: parse saved labels (normalized) and convert to pixels --- + # Try to match each label to its image (jpg/png) + for lab in label_files: + # Match image with same stem, try common extensions + candidates = [RUN_DIR / f"{lab.stem}{ext}" for ext in [".jpg",".png",".jpeg",".bmp",".tif",".tiff",".webp"]] + img_path = next((p for p in candidates if p.exists()), None) + if not img_path: + # Sometimes Ultralytics copies original extension; try to find by stem among present images + img_path = next((p for p in img_files if p.stem == lab.stem), None) + if not img_path: + print(f"[warn] No image found for label {lab.name}; skipping.") + continue + + W, H = Image.open(img_path).size + with open(lab, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + parts = re.split(r"\s+", line) + if len(parts) not in (5,6): + print(f"[warn] Unexpected label format in {lab.name}: {line}") + continue + rows.append(yolo_line_to_row(img_path, (W,H), names_map, parts)) + +else: + # --- Fallback: re-run YOLO on the images in RUN_DIR (annotated images work fine) --- + print("[info] No label files found in:", LABEL_DIR) + print("[info] Re-running YOLO on images in", RUN_DIR) + from ultralytics import YOLO + model = YOLO("yolov8n.pt") # use your custom weights here if needed + # Batch process all images in folder + results = model(str(RUN_DIR), conf=0.25) + for res in results: + names_map = res.names + W, H = res.orig_shape[1], res.orig_shape[0] + img_path = Path(res.path) + if res.boxes is None or len(res.boxes) == 0: + continue + xyxy = res.boxes.xyxy.cpu().numpy() + conf = res.boxes.conf.cpu().numpy() + clsid = res.boxes.cls.cpu().numpy() + for i in range(len(res.boxes)): + x1,y1,x2,y2 = map(float, xyxy[i]) + w_px = x2-x1; h_px = y2-y1 + rows.append({ + "image": str(img_path), + "class_id": int(clsid[i]), + "class_name": names_map.get(int(clsid[i]), str(int(clsid[i]))), + "conf": float(conf[i]), + "x1": x1, "y1": y1, "x2": x2, "y2": y2, + "w_px": w_px, "h_px": h_px, + "w_norm": w_px / W, "h_norm": h_px / H, + "img_w": W, "img_h": H + }) + +if rows: + # Write CSV + fieldnames = [ + "image","class_id","class_name","conf", + "x1","y1","x2","y2","w_px","h_px", + "w_norm","h_norm","img_w","img_h" + ] + with open(OUT_CSV, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + print(f"[ok] Wrote {len(rows)} rows to {OUT_CSV}") +else: + print("[info] No detections found. If you want labels, re-run prediction with:") + print(" save_txt=True, save_conf=True") + +# === One-shot: locate weights -> patch YAML -> validate dataset -> run YOLO val() === +import os, glob, time, re, yaml +from pathlib import Path +from PIL import Image +import numpy as np + +# ---------------------- CONFIG ---------------------- +YAML_IN = Path("/content/urc_augmented.yaml") # your dataset YAML +FALLBACK_WEIGHTS = "yolov8n.pt" # used only if no .pt found +IMGSZ = 640 +CONF = 0.25 +SPLIT = "val" # or "test" +# ---------------------------------------------------- + +# 1) Find the newest .pt weights +candidates = [] +patterns = [ + "/content/runs/**/weights/best.pt", + "/content/runs/**/weights/last.pt", + "/content/**/best.pt", + "/content/**/last.pt", + "/content/**/*.pt", # fallback +] +for pat in patterns: + for p in glob.glob(pat, recursive=True): + try: + st = os.stat(p) + candidates.append((p, st.st_mtime)) + except FileNotFoundError: + pass + +# Look in Drive if mounted +if os.path.exists("/content/drive"): + for p in glob.glob("/content/drive/MyDrive/**/*.pt", recursive=True): + try: + st = os.stat(p) + candidates.append((p, st.st_mtime)) + except FileNotFoundError: + pass + +if candidates: + candidates.sort(key=lambda x: x[1], reverse=True) + WEIGHTS = candidates[0][0] + print("Using weights:", WEIGHTS) +else: + WEIGHTS = FALLBACK_WEIGHTS + print(f"No local .pt found. Falling back to pretrained: {WEIGHTS}") + +# 2) Load + patch YAML to absolute paths +assert YAML_IN.exists(), f"{YAML_IN} not found" +cfg = yaml.safe_load(open(YAML_IN, "r")) +for k in ["train","val","names"]: + assert k in cfg, f"Missing '{k}' in {YAML_IN}" + +root = Path(cfg.get("path", YAML_IN.parent)) +def abs_path(p): + p = Path(p) + return p if p.is_absolute() else (root / p) + +train_img = abs_path(cfg["train"]).resolve() +val_img = abs_path(cfg["val"]).resolve() +test_img = abs_path(cfg.get("test")).resolve() if cfg.get("test") else None +names = list(cfg["names"]) + +def infer_labels_dir(img_dir: Path): + parts = list(img_dir.parts) + if "images" in parts: + i = parts.index("images") + parts[i] = "labels" + return Path(*parts) + # fallback guess: ../labels/ + return img_dir.parent.parent / "labels" / img_dir.name + +train_lbl = infer_labels_dir(train_img) +val_lbl = infer_labels_dir(val_img) +test_lbl = infer_labels_dir(test_img) if test_img else None + +def chk(p, tag): + ok = bool(p and Path(p).exists()) + print(("OK " if ok else "MISS"), f"{tag}: {p}") + return ok + +print("\n--- PATH CHECKS ---") +ok_all = True +ok_all &= chk(train_img, "train images") +ok_all &= chk(val_img, "val images") +ok_all &= chk(train_lbl, "train labels") +ok_all &= chk(val_lbl, "val labels") +if test_img: + ok_all &= chk(test_img, "test images") + ok_all &= chk(test_lbl, "test labels") + +# 3) Pairing & class-ID scan +IMG_EXT = (".jpg",".jpeg",".png",".bmp",".tif",".tiff",".webp") +def list_imgs(d: Path): + return [p for p in Path(d).glob("*") if p.suffix.lower() in IMG_EXT] + +def label_for(img: Path, lbl_dir: Path): + return Path(lbl_dir) / (img.stem + ".txt") + +def pairing_report(img_dir, lbl_dir, name): + imgs = list_imgs(img_dir) + miss = [p.name for p in imgs if not label_for(p, lbl_dir).exists()] + print(f"{name}: {len(imgs)} images; {len(miss)} missing labels") + if miss[:5]: + print(" sample missing:", miss[:5]) + +def scan_ids(lbl_dir, max_id): + bad = [] + for p in sorted(Path(lbl_dir).glob("*.txt")): + try: + arr = np.loadtxt(p, ndmin=2) + if arr.size == 0: + continue + cls = arr[:,0].astype(int) + if (cls < 0).any() or (cls > max_id).any(): + bad.append(p.name) + except Exception: + bad.append(p.name) + if bad: + print(f"[WARN] {len(bad)} label files have class IDs outside 0..{max_id}. e.g.:", bad[:5]) + else: + print(f"Class IDs OK (0..{max_id}).") + +if ok_all: + print("\n--- DATASET SANITY ---") + pairing_report(train_img, train_lbl, "train") + pairing_report(val_img, val_lbl, "val") + if test_img and test_lbl: + pairing_report(test_img, test_lbl, "test") + scan_ids(train_lbl, len(names)-1) + scan_ids(val_lbl, len(names)-1) + +# Write patched YAML with absolute paths +PATCHED = YAML_IN.with_name(YAML_IN.stem + "_ABS.yaml") +patched = dict(cfg) +patched["path"] = "/" # force absolute +patched["train"] = str(train_img) +patched["val"] = str(val_img) +if test_img: + patched["test"] = str(test_img) +patched["names"] = names +with open(PATCHED, "w") as f: + yaml.safe_dump(patched, f, sort_keys=False) +print("\nWrote patched YAML ->", PATCHED) + +# 4) Run validation +from ultralytics import YOLO +model = YOLO(WEIGHTS) +metrics = model.val(data=str(PATCHED), split=SPLIT, imgsz=IMGSZ, conf=CONF) + +m = metrics.results_dict +print("\n--- SUMMARY METRICS ---") +for k in ["metrics/precision","metrics/recall","metrics/f1","metrics/mAP50","metrics/mAP50-95", + "speed/preprocess","speed/inference","speed/postprocess"]: + if k in m: + print(f"{k:>20}: {m[k]:.4f}") + +print("\nArtifacts (PR curves, confusion matrix, per-class CSV) saved in:") +print(metrics.save_dir) +print("Per-class table is usually at: runs/val/exp*/results.csv") + +from google.colab import files +files.download("/content/detections.csv") + +from google.colab import drive +drive.mount('/content/drive') + +# === Cell J: Export for Jetson/ONNX (run if you want artifacts) === +from ultralytics import YOLO +BEST = "urc_runs/yolov8s_aug_disk/weights/best.pt" +m = YOLO(BEST) +m.export(format="pt") +m.export(format="onnx", imgsz=IMGSZ, opset=12) +print("Exported best.pt and best.onnx") \ No newline at end of file