ATA / src /accident /transforms.py
vishanth11
Deploy AI Traffic Adaptation System with Git LFS binaries to Hugging Face Spaces
f234112
Raw History Blame Contribute Delete
4.28 kB
"""Bounding-box and image transforms for accident detection.
The original Parquet annotations are COCO-style xywh in absolute pixels for
640x640 images. This module provides conversions to the xyxy format required
by torchvision detection models, plus resize-aware box rescaling.
"""
from __future__ import annotations
from typing import Any
import numpy as np
import torch
import torchvision.transforms.functional as F
from PIL import Image
class AccidentObject:
"""Container for one annotation with conversion helpers."""
def __init__(self, bbox_xywh: list[float], category_id: int):
self.bbox_xywh = [float(v) for v in bbox_xywh]
self.category_id = int(category_id)
def to_xyxy(self) -> list[float]:
"""Convert COCO [x, y, w, h] to [x_min, y_min, x_max, y_max]."""
x, y, w, h = self.bbox_xywh
return [x, y, x + w, y + h]
@property
def area(self) -> float:
return self.bbox_xywh[2] * self.bbox_xywh[3]
def is_valid(self, image_width: int, image_height: int) -> bool:
"""Check that the box has positive area and lies inside the image."""
x, y, w, h = self.bbox_xywh
if w <= 0 or h <= 0:
return False
if x < 0 or y < 0:
return False
if x + w > image_width or y + h > image_height:
return False
return True
def to_dict(self) -> dict[str, Any]:
return {
"bbox_xywh": self.bbox_xywh,
"bbox_xyxy": self.to_xyxy(),
"category_id": self.category_id,
"area": self.area,
}
def xywh_to_xyxy(boxes_xywh: np.ndarray) -> np.ndarray:
"""Vectorized COCO xywh -> xyxy conversion."""
boxes_xywh = np.asarray(boxes_xywh, dtype=np.float32)
if boxes_xywh.size == 0:
return np.zeros((0, 4), dtype=np.float32)
xyxy = np.copy(boxes_xywh)
xyxy[:, 2] = boxes_xywh[:, 0] + boxes_xywh[:, 2]
xyxy[:, 3] = boxes_xywh[:, 1] + boxes_xywh[:, 3]
return xyxy
def xyxy_to_xywh(boxes_xyxy: np.ndarray) -> np.ndarray:
"""Vectorized xyxy -> COCO xywh conversion."""
boxes_xyxy = np.asarray(boxes_xyxy, dtype=np.float32)
if boxes_xyxy.size == 0:
return np.zeros((0, 4), dtype=np.float32)
xywh = np.copy(boxes_xyxy)
xywh[:, 2] = boxes_xyxy[:, 2] - boxes_xyxy[:, 0]
xywh[:, 3] = boxes_xyxy[:, 3] - boxes_xyxy[:, 1]
return xywh
def scale_boxes(
boxes_xyxy: np.ndarray,
orig_size: tuple[int, int],
new_size: tuple[int, int],
) -> np.ndarray:
"""Rescale absolute xyxy boxes after image resize.
Args:
boxes_xyxy: array of shape (N, 4) in original absolute coordinates.
orig_size: (width, height) of the source image.
new_size: (width, height) of the resized image.
Returns:
Rescaled boxes in the new image coordinate system.
"""
boxes_xyxy = np.asarray(boxes_xyxy, dtype=np.float32)
if boxes_xyxy.size == 0:
return boxes_xyxy
orig_w, orig_h = orig_size
new_w, new_h = new_size
sx = new_w / orig_w
sy = new_h / orig_h
scaled = np.copy(boxes_xyxy)
scaled[:, [0, 2]] *= sx
scaled[:, [1, 3]] *= sy
return scaled
class ResizeTransform:
"""Resize image and rescale bounding boxes deterministically."""
def __init__(self, target_size: tuple[int, int] = (320, 320)):
self.target_size = target_size # (width, height)
def __call__(
self,
image: Image.Image,
boxes_xyxy: np.ndarray,
) -> tuple[torch.Tensor, np.ndarray]:
orig_size = image.size # (width, height)
resized = F.resize(image, self.target_size[::-1]) # PIL expects (h, w)
tensor = F.pil_to_tensor(resized).float() / 255.0
scaled = scale_boxes(boxes_xyxy, orig_size, self.target_size)
return tensor, scaled
def clamp_boxes(boxes_xyxy: np.ndarray, width: int, height: int) -> np.ndarray:
"""Clamp xyxy boxes to image bounds and ensure x2 > x1, y2 > y1."""
boxes = np.copy(boxes_xyxy)
boxes[:, [0, 2]] = np.clip(boxes[:, [0, 2]], 0, width)
boxes[:, [1, 3]] = np.clip(boxes[:, [1, 3]], 0, height)
boxes[:, 2] = np.maximum(boxes[:, 2], boxes[:, 0] + 1e-3)
boxes[:, 3] = np.maximum(boxes[:, 3], boxes[:, 1] + 1e-3)
return boxes