fridge-vision / utils /image_utils.py
NoNameWrath's picture
Deploy to HuggingFace Spaces
c02c6ce
Raw
History Blame Contribute Delete
7.08 kB
"""
Image preprocessing and postprocessing utilities.
Handles image loading, resizing, and results conversion.
"""
import cv2
import numpy as np
import logging
from pathlib import Path
from typing import Tuple, List, Dict, Optional
logger = logging.getLogger(__name__)
class ImagePreprocessor:
"""Handle image preprocessing for model inference."""
MAX_SIZE = 640 # Standard YOLO input size
@staticmethod
def load_image(image_path: str) -> np.ndarray:
"""
Load image from file.
Args:
image_path: Path to image file
Returns:
Image as numpy array in BGR format
"""
if not Path(image_path).exists():
raise FileNotFoundError(f"Image file not found: {image_path}")
image = cv2.imread(image_path)
if image is None:
raise ValueError(f"Failed to load image: {image_path}")
logger.info(f"Loaded image from {image_path}, shape: {image.shape}")
return image
@staticmethod
def load_image_from_bytes(image_bytes: bytes) -> np.ndarray:
"""
Load image from bytes.
Args:
image_bytes: Image data as bytes
Returns:
Image as numpy array in BGR format
"""
nparr = np.frombuffer(image_bytes, np.uint8)
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if image is None:
raise ValueError("Failed to decode image from bytes")
logger.info(f"Loaded image from bytes, shape: {image.shape}")
return image
@staticmethod
def resize_image(image: np.ndarray, max_size: int = MAX_SIZE) -> np.ndarray:
"""
Resize image while maintaining aspect ratio.
Args:
image: Input image
max_size: Maximum size for longest dimension
Returns:
Resized image
"""
height, width = image.shape[:2]
scale = min(max_size / max(height, width), 1.0)
new_width = int(width * scale)
new_height = int(height * scale)
resized = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
logger.info(f"Resized image from {image.shape} to {resized.shape}")
return resized
@staticmethod
def get_image_info(image: np.ndarray) -> Dict:
"""Get basic image information."""
height, width = image.shape[:2]
return {
"width": width,
"height": height,
"channels": image.shape[2] if len(image.shape) > 2 else 1,
"size_mb": (image.nbytes / (1024 * 1024))
}
class ResultPostprocessor:
"""Convert model predictions to clean output format."""
@staticmethod
def process_detections(
predictions,
class_names: List[str],
conf_threshold: float = 0.5
) -> List[Dict]:
"""
Convert YOLO predictions to structured format.
Args:
predictions: YOLOv5 predictions object
class_names: List of class names
conf_threshold: Confidence threshold for filtering
Returns:
List of detection dictionaries
"""
detections = []
# Extract predictions
xyxy = predictions.xyxy[0].cpu().numpy() # Bounding boxes
confs = predictions.conf[0].cpu().numpy() # Confidences
classes = predictions.cls[0].cpu().numpy().astype(int) # Class indices
for bbox, conf, cls_idx in zip(xyxy, confs, classes):
if conf >= conf_threshold:
x1, y1, x2, y2 = bbox
detection = {
"class_id": int(cls_idx),
"class_name": class_names[cls_idx] if cls_idx < len(class_names) else "unknown",
"confidence": float(conf),
"bbox": {
"x1": float(x1),
"y1": float(y1),
"x2": float(x2),
"y2": float(y2)
},
"center": {
"x": float((x1 + x2) / 2),
"y": float((y1 + y2) / 2)
},
"width": float(x2 - x1),
"height": float(y2 - y1)
}
detections.append(detection)
logger.info(f"Processed {len(detections)} detections")
return detections
@staticmethod
def merge_overlapping_detections(
detections: List[Dict],
iou_threshold: float = 0.5
) -> List[Dict]:
"""
Merge nearby detections of the same class using IoU.
Args:
detections: List of detection dictionaries
iou_threshold: IoU threshold for merging
Returns:
Merged detections
"""
if not detections:
return detections
# Sort by confidence (descending)
sorted_dets = sorted(detections, key=lambda x: x["confidence"], reverse=True)
merged = []
used = set()
for i, det in enumerate(sorted_dets):
if i in used:
continue
current_class = det["class_name"]
bbox1 = det["bbox"]
for j, other in enumerate(sorted_dets[i+1:], start=i+1):
if j in used:
continue
# Only merge same class
if other["class_name"] != current_class:
continue
bbox2 = other["bbox"]
iou = ResultPostprocessor._calculate_iou(bbox1, bbox2)
if iou >= iou_threshold:
used.add(j)
merged.append(det)
logger.info(f"Merged overlapping detections: {len(detections)} -> {len(merged)}")
return merged
@staticmethod
def _calculate_iou(bbox1: Dict, bbox2: Dict) -> float:
"""Calculate Intersection over Union of two bboxes."""
x1_min, y1_min = bbox1["x1"], bbox1["y1"]
x1_max, y1_max = bbox1["x2"], bbox1["y2"]
x2_min, y2_min = bbox2["x1"], bbox2["y1"]
x2_max, y2_max = bbox2["x2"], bbox2["y2"]
# Intersection area
xi_min = max(x1_min, x2_min)
yi_min = max(y1_min, y2_min)
xi_max = min(x1_max, x2_max)
yi_max = min(y1_max, y2_max)
if xi_max < xi_min or yi_max < yi_min:
return 0.0
intersection = (xi_max - xi_min) * (yi_max - yi_min)
# Union area
area1 = (x1_max - x1_min) * (y1_max - y1_min)
area2 = (x2_max - x2_min) * (y2_max - y2_min)
union = area1 + area2 - intersection
return intersection / union if union > 0 else 0.0