| """ |
| CarDentIQ β inference engine |
| Developer: Saksham Pathak (github.com/parthmax2) |
| |
| Exposes: |
| CLASS_NAMES dict[int, str] |
| CLASS_COLORS dict[int, tuple] |
| DEFAULT_THRESHOLDS dict[int, float] |
| load_model() β YOLO |
| detect() β dict |
| """ |
|
|
| import cv2 |
| import numpy as np |
| from ultralytics import YOLO |
|
|
| |
| CLASS_NAMES: dict[int, str] = { |
| 0: "no damage", |
| 1: "lost parts", |
| 2: "torn", |
| 3: "dent", |
| 4: "paint scratch", |
| 5: "hole", |
| 6: "broken glass", |
| 7: "broken lamp", |
| } |
|
|
| CLASS_COLORS: dict[int, tuple] = { |
| 0: (128, 128, 128), |
| 1: (0, 128, 255), |
| 2: (255, 0, 255), |
| 3: (0, 255, 0), |
| 4: (0, 140, 255), |
| 5: (0, 255, 255), |
| 6: (0, 0, 255), |
| 7: (255, 165, 0), |
| } |
|
|
| DEFAULT_THRESHOLDS: dict[int, float] = { |
| 0: 0.90, 1: 0.26, 2: 0.05, 3: 0.05, |
| 4: 0.05, 5: 0.16, 6: 0.33, 7: 0.05, |
| } |
|
|
| |
| CLASS_SEVERITY_WEIGHT: dict[int, float] = { |
| 0: 0, |
| 4: 1, |
| 3: 2, |
| 7: 3, |
| 6: 3, |
| 5: 4, |
| 2: 4, |
| 1: 5, |
| } |
|
|
| _model: YOLO | None = None |
|
|
|
|
| |
| def load_model(weights: str = "best.pt") -> YOLO: |
| global _model |
| if _model is None: |
| print(f"[detector] Loading model from {weights} β¦") |
| _model = YOLO(weights) |
| return _model |
|
|
|
|
| |
| def _xyxy_to_yolo(x1: float, y1: float, x2: float, y2: float, |
| img_w: int, img_h: int) -> tuple: |
| cx = (x1 + x2) / 2.0 / img_w |
| cy = (y1 + y2) / 2.0 / img_h |
| w = (x2 - x1) / img_w |
| h = (y2 - y1) / img_h |
| return cx, cy, w, h |
|
|
|
|
| def _draw_box(img: np.ndarray, x1: int, y1: int, x2: int, y2: int, |
| label: str, color: tuple) -> None: |
| cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) |
| cv2.putText(img, label, (x1, max(y1 - 10, 10)), |
| cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 2, cv2.LINE_AA) |
|
|
|
|
| def _compute_summary(detections: list[dict]) -> dict: |
| by_class: dict[int, dict] = {} |
| raw_score = 0.0 |
| total_defects = 0 |
|
|
| for d in detections: |
| cid = d["class_id"] |
| if cid == 0: |
| continue |
| total_defects += 1 |
| raw_score += CLASS_SEVERITY_WEIGHT.get(cid, 2) * d["confidence"] |
|
|
| entry = by_class.setdefault(cid, { |
| "class_id": cid, "class_name": d["class_name"], |
| "color": CLASS_COLORS.get(cid, (255, 255, 255)), |
| "count": 0, "confidence_sum": 0.0, |
| }) |
| entry["count"] += 1 |
| entry["confidence_sum"] += d["confidence"] |
|
|
| by_class_list = [ |
| { |
| "class_id": e["class_id"], "class_name": e["class_name"], "color": e["color"], |
| "count": e["count"], "avg_confidence": round(e["confidence_sum"] / e["count"], 3), |
| } |
| for e in by_class.values() |
| ] |
| by_class_list.sort(key=lambda x: x["count"], reverse=True) |
|
|
| score = round(min(raw_score, 10), 1) |
| if total_defects == 0: |
| label = "No Damage Detected" |
| elif score <= 3: |
| label = "Minor" |
| elif score <= 7: |
| label = "Moderate" |
| else: |
| label = "Severe" |
|
|
| return { |
| "severity_label": label, |
| "severity_score": score, |
| "total_defects": total_defects, |
| "by_class": by_class_list, |
| } |
|
|
|
|
| |
| def detect( |
| image_rgb: np.ndarray, |
| resize: bool, |
| thresholds: dict[int, float], |
| weights: str = "best.pt", |
| ) -> dict: |
| """ |
| Parameters |
| ---------- |
| image_rgb : np.ndarray (H, W, 3) β RGB image |
| resize : bool β cap image at 1024 px before inference |
| thresholds : per-class confidence floor {class_id: min_conf} |
| weights : path to model weights file |
| |
| Returns |
| ------- |
| dict with keys: |
| annotated : np.ndarray (H, W, 3) RGB β image with boxes drawn |
| original : np.ndarray (H, W, 3) RGB β image at inference resolution, no boxes |
| detections : list[dict] β class_id, class_name, confidence, box_xyxy, box_yolo |
| summary : dict β severity_label, severity_score, total_defects, by_class |
| """ |
| img = image_rgb |
|
|
| if resize and max(img.shape[:2]) > 1024: |
| img = cv2.resize(img, (1024, 1024)) |
|
|
| model = load_model(weights) |
| preds = model(img, augment=True) |
|
|
| raw_boxes, confs, class_ids = [], [], [] |
| for r in preds: |
| raw_boxes.extend(r.boxes.xyxy.cpu().numpy().tolist()) |
| confs.extend(r.boxes.conf.cpu().numpy().tolist()) |
| class_ids.extend(r.boxes.cls.cpu().numpy().astype(int).tolist()) |
|
|
| img_h, img_w = img.shape[:2] |
| annotated = img.copy() |
| detections = [] |
|
|
| for box, conf, cls in zip(raw_boxes, confs, class_ids): |
| if conf < thresholds.get(cls, 0.25): |
| continue |
|
|
| x1, y1, x2, y2 = map(int, box) |
| color = CLASS_COLORS.get(cls, (255, 255, 255)) |
| label = f"{CLASS_NAMES[cls]} {conf:.2f}" |
| _draw_box(annotated, x1, y1, x2, y2, label, color) |
|
|
| cx, cy, w, h = _xyxy_to_yolo(*box, img_w, img_h) |
| detections.append({ |
| "class_id": cls, |
| "class_name": CLASS_NAMES[cls], |
| "confidence": round(conf, 3), |
| "box_xyxy": [x1, y1, x2, y2], |
| "box_yolo": [round(cx, 6), round(cy, 6), round(w, 6), round(h, 6)], |
| }) |
|
|
| return { |
| "annotated": annotated, |
| "original": img, |
| "detections": detections, |
| "summary": _compute_summary(detections), |
| } |
|
|