civilscan-backend / detection.py
Amir Aziz
Initial
1670e42
Raw
History Blame Contribute Delete
6.19 kB
"""
detection.py — Core YOLO inference engine.
- run_detection_single() : one model on one image
- run_detection_combined() : ALL 6 models, keep only the single
highest-confidence box overall
- extract_video_frames() : samples frames every N seconds from a video
"""
import os
import cv2
import random
import logging
from pathlib import Path
from typing import Dict, List, Any
from model_loader import load_model, get_model_info, get_severity, MODEL_REGISTRY
logger = logging.getLogger(__name__)
SEVERITY_COLOR_BGR = {
"Critical": (0, 0, 220),
"High": (0, 100, 255),
"Medium": (0, 200, 255),
"Low": (0, 200, 0),
}
VIDEO_FRAME_INTERVAL_SECONDS = 2
def run_detection_single(image_path: str, model_key: str, output_dir: str, image_id: str, conf_threshold: float = 0.45) -> Dict[str, Any]:
model = load_model(model_key)
if model is None:
detections = _mock_detections(model_key, conf_threshold)
else:
detections = _run_yolo(model, model_key, image_path, conf_threshold)
return _finalize_result(image_path, detections, output_dir, image_id, model_key)
def run_detection_combined(image_path: str, output_dir: str, image_id: str, conf_threshold: float = 0.45) -> Dict[str, Any]:
all_detections: List[Dict] = []
for model_key in MODEL_REGISTRY.keys():
model = load_model(model_key)
if model is None:
dets = _mock_detections(model_key, conf_threshold)
else:
dets = _run_yolo(model, model_key, image_path, conf_threshold)
all_detections.extend(dets)
best = [max(all_detections, key=lambda d: d["confidence"])] if all_detections else []
return _finalize_result(image_path, best, output_dir, image_id, "combined (todos los modelos)")
def extract_video_frames(video_path: str, output_dir: str, video_id: str) -> List[Dict]:
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError(f"Cannot open video file: {video_path}")
fps = cap.get(cv2.CAP_PROP_FPS) or 25
frame_interval = max(1, int(fps * VIDEO_FRAME_INTERVAL_SECONDS))
frames_dir = Path(output_dir) / "video_frames"
frames_dir.mkdir(parents=True, exist_ok=True)
results = []
frame_idx = 0
saved_idx = 0
while True:
ret, frame = cap.read()
if not ret:
break
if frame_idx % frame_interval == 0:
timestamp_sec = round(frame_idx / fps, 1)
frame_id = f"{video_id}_frame{saved_idx:03d}"
frame_path = frames_dir / f"{frame_id}.jpg"
cv2.imwrite(str(frame_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
results.append({"frame_path": str(frame_path), "frame_id": frame_id, "timestamp_sec": timestamp_sec})
saved_idx += 1
frame_idx += 1
cap.release()
logger.info("Extracted %d frames from video %s", len(results), video_id)
return results
def _run_yolo(model, model_key: str, image_path: str, conf_threshold: float) -> List[Dict]:
results = model(image_path, conf=conf_threshold, iou=0.45, imgsz=640, device="cpu", verbose=False)
detections = []
for r in results:
boxes = r.boxes
if boxes is None:
continue
for box in boxes:
cls_id = int(box.cls[0])
cls_name = model.names.get(cls_id, f"class_{cls_id}")
conf = float(box.conf[0])
x1, y1, x2, y2 = box.xyxy[0].tolist()
detections.append({
"class": cls_name, "class_name": cls_name,
"confidence": round(conf, 4),
"severity": get_severity(model_key, cls_name),
"bbox": [round(x1), round(y1), round(x2), round(y2)],
"model_used": model_key,
})
return detections
def _mock_detections(model_key: str, conf_threshold: float) -> List[Dict]:
info = get_model_info(model_key) or {}
classes = info.get("classes", ["defect"])
n = random.randint(1, 3)
detections = []
for _ in range(n):
cls = random.choice(classes)
conf = round(random.uniform(max(conf_threshold, 0.4), 0.95), 3)
x1, y1 = random.randint(20, 200), random.randint(20, 200)
x2, y2 = x1 + random.randint(80, 250), y1 + random.randint(80, 250)
detections.append({
"class": cls, "class_name": cls, "confidence": conf,
"severity": get_severity(model_key, cls),
"bbox": [x1, y1, x2, y2], "model_used": model_key,
})
return detections
def _finalize_result(image_path: str, detections: List[Dict], output_dir: str, image_id: str, model_label: str) -> Dict[str, Any]:
img = cv2.imread(image_path)
if img is None:
raise ValueError(f"Cannot read image: {image_path}")
annotated = img.copy()
for det in detections:
x1, y1, x2, y2 = det["bbox"]
color = SEVERITY_COLOR_BGR.get(det["severity"], (255, 255, 255))
cv2.rectangle(annotated, (x1, y1), (x2, y2), color, 2)
label = f"{det['class_name'].replace('_', ' ')} {det['confidence']:.0%}"
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 2)
cv2.rectangle(annotated, (x1, y1 - th - 8), (x1 + tw + 6, y1), color, -1)
cv2.putText(annotated, label, (x1 + 3, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 2)
out_name = f"{image_id}_annotated.jpg"
out_path = Path(output_dir) / out_name
cv2.imwrite(str(out_path), annotated, [cv2.IMWRITE_JPEG_QUALITY, 92])
severity_summary = {"Critical": 0, "High": 0, "Medium": 0, "Low": 0}
for d in detections:
severity_summary[d["severity"]] = severity_summary.get(d["severity"], 0) + 1
overall_severity = "Low"
for sev in ["Critical", "High", "Medium", "Low"]:
if severity_summary.get(sev, 0) > 0:
overall_severity = sev
break
return {
"detections": detections,
"total_defects": len(detections),
"severity_summary": severity_summary,
"overall_severity": overall_severity,
"annotated_image": f"/outputs/{out_name}",
"model_used": model_label,
}