| """ |
| CarDentIQ β Model Evaluation |
| Developer: Saksham Pathak (github.com/parthmax2) |
| |
| Runs YOLO validation on a chosen split and reports per-class metrics, |
| overall mAP, and an optional inference speed benchmark. |
| |
| Usage (run from project root): |
| python scripts/evaluate.py # test split, best.pt |
| python scripts/evaluate.py --model runs/phase2_full/weights/best.pt |
| python scripts/evaluate.py --split val |
| python scripts/evaluate.py --save-images # save annotated predictions |
| python scripts/evaluate.py --speed # add latency benchmark |
| python scripts/evaluate.py --conf 0.25 --iou 0.5 # custom NMS thresholds |
| """ |
|
|
| import argparse |
| import time |
| from pathlib import Path |
|
|
| from ultralytics import YOLO |
|
|
| |
| ROOT = Path(__file__).resolve().parent.parent |
| DATA_YAML = str(ROOT / "configs" / "data.yaml") |
| DEFAULT_MODEL = str(ROOT / "best.pt") |
| RUNS_DIR = str(ROOT / "runs") |
|
|
| CLASS_NAMES = [ |
| "no_damage", "lost_parts", "torn", "dent", |
| "paint_scratch", "hole", "broken_glass", "broken_lamp", |
| ] |
|
|
|
|
| |
| def run_validation( |
| model_path: str, |
| split: str, |
| conf: float, |
| iou: float, |
| save_images: bool, |
| ): |
| if not Path(model_path).exists(): |
| raise FileNotFoundError(f"Weights not found: {model_path}") |
| if not Path(DATA_YAML).exists(): |
| raise FileNotFoundError(f"Dataset config not found: {DATA_YAML}") |
|
|
| print(f"\n{'='*60}") |
| print(f" Model : {model_path}") |
| print(f" Split : {split}") |
| print(f" Conf : {conf} IoU: {iou}") |
| print(f"{'='*60}") |
|
|
| model = YOLO(model_path) |
| metrics = model.val( |
| data = DATA_YAML, |
| split = split, |
| conf = conf, |
| iou = iou, |
| save = save_images, |
| save_txt = False, |
| save_json = False, |
| plots = True, |
| project = RUNS_DIR, |
| name = f"eval_{split}", |
| exist_ok = True, |
| verbose = True, |
| ) |
|
|
| _print_results(metrics) |
| return metrics |
|
|
|
|
| |
| def _print_results(metrics) -> None: |
| box = metrics.box |
|
|
| print(f"\n{'='*60}") |
| print(" OVERALL METRICS") |
| print(f"{'='*60}") |
| print(f" mAP @ 0.5 : {box.map50:.4f}") |
| print(f" mAP @ 0.5:0.95 : {box.map:.4f}") |
| print(f" Precision (mean) : {box.mp:.4f}") |
| print(f" Recall (mean) : {box.mr:.4f}") |
|
|
| print(f"\n{'='*60}") |
| print(" PER-CLASS METRICS") |
| print(f"{'='*60}") |
| header = f" {'Class':<20} {'AP@0.5':>8} {'mAP':>8} {'P':>8} {'R':>8}" |
| print(header) |
| print(" " + "-" * (len(header) - 2)) |
|
|
| for i, name in enumerate(CLASS_NAMES): |
| ap50 = box.ap50[i] if i < len(box.ap50) else float("nan") |
| mAP = box.maps[i] if i < len(box.maps) else float("nan") |
| p = box.p[i] if i < len(box.p) else float("nan") |
| r = box.r[i] if i < len(box.r) else float("nan") |
| print(f" {name:<20} {ap50:>8.4f} {mAP:>8.4f} {p:>8.4f} {r:>8.4f}") |
|
|
| print(f"{'='*60}") |
| print(f" Confusion matrix and plots saved to: {RUNS_DIR}/eval_*/\n") |
|
|
|
|
| |
| def speed_benchmark(model_path: str, n: int = 100) -> None: |
| import torch |
| from PIL import Image |
|
|
| model = YOLO(model_path) |
| dummy = Image.new("RGB", (640, 640), color=(128, 128, 128)) |
|
|
| for _ in range(10): |
| model(dummy, verbose=False) |
|
|
| t0 = time.perf_counter() |
| for _ in range(n): |
| model(dummy, verbose=False) |
| elapsed = time.perf_counter() - t0 |
|
|
| device = "CUDA" if torch.cuda.is_available() else "CPU" |
| print(f"\n[speed] {n} Γ 640Γ640 inference on {device}") |
| print(f" Avg latency : {elapsed / n * 1000:.1f} ms / image") |
| print(f" Throughput : {n / elapsed:.1f} FPS") |
|
|
|
|
| |
| def main() -> None: |
| parser = argparse.ArgumentParser( |
| description="Evaluate a car-damage YOLO model", |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, |
| ) |
| parser.add_argument("--model", default=DEFAULT_MODEL, |
| help="Path to model weights (.pt)") |
| parser.add_argument("--split", default="test", |
| choices=["train", "val", "test"], |
| help="Dataset split to evaluate on") |
| parser.add_argument("--conf", type=float, default=0.001, |
| help="Confidence threshold (use 0.001 for full mAP curve)") |
| parser.add_argument("--iou", type=float, default=0.6, |
| help="IoU threshold for NMS") |
| parser.add_argument("--save-images", action="store_true", |
| help="Save annotated prediction images") |
| parser.add_argument("--speed", action="store_true", |
| help="Run inference speed benchmark after evaluation") |
| args = parser.parse_args() |
|
|
| run_validation( |
| model_path = args.model, |
| split = args.split, |
| conf = args.conf, |
| iou = args.iou, |
| save_images = args.save_images, |
| ) |
|
|
| if args.speed: |
| speed_benchmark(args.model) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|