CarDentIQ / src /detector.py
parthmax24's picture
added the latest updates
81b4246
Raw
History Blame Contribute Delete
6.29 kB
"""
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 registry ───────────────────────────────────────────────
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), # grey
1: (0, 128, 255), # orange
2: (255, 0, 255), # magenta
3: (0, 255, 0), # green
4: (0, 140, 255), # deep orange
5: (0, 255, 255), # yellow
6: (0, 0, 255), # red
7: (255, 165, 0), # blue-ish
}
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,
}
# relative repair-impact weight per class, used only for the severity summary
CLASS_SEVERITY_WEIGHT: dict[int, float] = {
0: 0, # no_damage
4: 1, # paint_scratch β€” cosmetic
3: 2, # dent
7: 3, # broken_lamp
6: 3, # broken_glass
5: 4, # hole
2: 4, # torn
1: 5, # lost_parts β€” missing component, most severe
}
_model: YOLO | None = None
# ── model loader (singleton) ─────────────────────────────────────
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
# ── helpers ──────────────────────────────────────────────────────
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 # no_damage is not a defect
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,
}
# ── main inference function ───────────────────────────────────────
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),
}