Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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.""" | |
| 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 | |
| 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 | |
| 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 | |