ATA / src /accident /inference.py
vishanth11
Deploy AI Traffic Adaptation System with Git LFS binaries to Hugging Face Spaces
f234112
Raw History Blame Contribute Delete
4.77 kB
"""Inference interface for the accident detector.
Defines the Phase 5 output contract. Detector confidence is returned as
`accident_detection_confidence` and is NOT claimed to be a calibrated probability.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import numpy as np
import torch
from PIL import Image
from . import CLASS_NAMES
from .labels import MODEL_LABEL_BACKGROUND, model_to_dataset_labels
from .model import build_detector
from .transforms import ResizeTransform, clamp_boxes
from .utils import get_device
class AccidentDetector:
"""Callable detector wrapper that implements the Phase 5 inference contract.
The checkpoint stores a 3-class model head (background + accident +
non_accident in model label space); predictions are mapped back to dataset
semantics (0 = accident, 1 = non_accident) before output.
"""
def __init__(
self,
checkpoint_path: str | Path,
num_classes: int | None = None,
image_size: tuple[int, int] = (320, 320),
confidence_threshold: float = 0.5,
device: torch.device | None = None,
):
self.image_size = image_size
self.confidence_threshold = confidence_threshold
self.device = device if device is not None else get_device()
self.resize = ResizeTransform(target_size=image_size)
state_dict = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
# The 2-class Phase 5 baseline and the 3-class Phase 5B checkpoints
# differ only in the box head width; infer it from the checkpoint.
inferred = state_dict["roi_heads.box_predictor.cls_score.bias"].shape[0]
self.num_classes = num_classes if num_classes is not None else inferred
self.model = build_detector(num_classes=self.num_classes, pretrained=False)
self.model.load_state_dict(state_dict)
self.model.to(self.device)
self.model.eval()
@torch.no_grad()
def predict(
self,
image: Image.Image | np.ndarray | torch.Tensor,
score_threshold: float | None = None,
) -> dict[str, Any]:
"""Run inference on a single image and return the contract output.
Output fields:
- detector_version: static version string
- image_size: (width, height)
- detections: list of {bbox, category, confidence}
- accident_detection_confidence: aggregated detector confidence
"""
if isinstance(image, np.ndarray):
image = Image.fromarray(image).convert("RGB")
elif isinstance(image, torch.Tensor):
image = Image.fromarray(
(image.permute(1, 2, 0).cpu().numpy() * 255).astype(np.uint8)
).convert("RGB")
orig_size = image.size
tensor, _ = self.resize(image, np.zeros((0, 4), dtype=np.float32))
tensor = tensor.to(self.device)
outputs = self.model([tensor])
output = outputs[0]
thr = score_threshold if score_threshold is not None else self.confidence_threshold
keep = (output["scores"] >= thr) & (output["labels"] != MODEL_LABEL_BACKGROUND)
boxes_xyxy = output["boxes"][keep].cpu().numpy()
# Model labels (1/2) -> dataset semantics (0/1) for category naming.
labels = model_to_dataset_labels(output["labels"][keep].cpu().numpy())
scores = output["scores"][keep].cpu().numpy()
# Clamp boxes to image bounds for safety.
boxes_xyxy = clamp_boxes(boxes_xyxy, self.image_size[0], self.image_size[1])
detections = [
{
"bbox": [round(float(v), 2) for v in box],
"category": CLASS_NAMES.get(int(label), str(label)),
"confidence": round(float(score), 4),
}
for box, label, score in zip(boxes_xyxy, labels, scores)
]
accident_scores = [
d["confidence"] for d in detections if d["category"] == "accident"
]
accident_detection_confidence = float(max(accident_scores)) if accident_scores else 0.0
return {
"detector_version": "accident_detector_v1",
"image_size": [orig_size[0], orig_size[1]],
"detections": detections,
"accident_detection_confidence": accident_detection_confidence,
}
def __call__(self, *args, **kwargs) -> dict[str, Any]:
return self.predict(*args, **kwargs)
def infer_probability(
image: Image.Image | np.ndarray | torch.Tensor,
checkpoint_path: str | Path,
**kwargs: Any,
) -> dict[str, Any]:
"""Convenience helper to run a single-image prediction."""
detector = AccidentDetector(checkpoint_path, **kwargs)
return detector.predict(image)