# ═══════════════════════════════════════════════════════════════════ # utils.py - Measurements, HTML Generation, and Utilities # ═══════════════════════════════════════════════════════════════════ import cv2 import numpy as np from datetime import timedelta # ═══════════════════════════════════════════════════════════════════ # MEASUREMENT CALCULATOR # ═══════════════════════════════════════════════════════════════════ class PotholeMeasurementSystem: """Calculate physical measurements from segmentation masks""" def __init__(self): self.fx = 384.0 self.fy = 384.0 self.cx = 320.0 self.cy = 240.0 def calculate_measurements(self, mask, depth_map=None): """Calculate all physical measurements from mask""" mask_bool = mask > 0 if not np.any(mask_bool): return None contours, _ = cv2.findContours( mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE ) if len(contours) == 0: return None contour = max(contours, key=cv2.contourArea) # Get bounding box and centroid x, y, w, h = cv2.boundingRect(contour) M = cv2.moments(contour) if M["m00"] != 0: cx = int(M["m10"] / M["m00"]) cy = int(M["m01"] / M["m00"]) else: cx, cy = x + w//2, y + h//2 pixel_area = np.sum(mask_bool) perimeter_pixels = cv2.arcLength(contour, True) pixel_to_mm = 0.5 if depth_map is not None: h_c = np.median(depth_map[~mask_bool & (depth_map > 0)]) pothole_depths = depth_map[mask_bool] pothole_depths = pothole_depths[pothole_depths > 0] if len(pothole_depths) > 0: actual_depths = pothole_depths - h_c max_depth_mm = float(actual_depths.max()) if len(actual_depths) > 0 else 0 mean_depth_mm = float(actual_depths.mean()) if len(actual_depths) > 0 else 0 depth_m = h_c / 1000.0 s_x = depth_m / self.fx s_y = depth_m / self.fy pixel_to_mm = (s_x + s_y) / 2 * 1000 else: max_depth_mm = 0 mean_depth_mm = 0 else: estimated_depth_cm = min(15, (pixel_area / 1000) * 2) max_depth_mm = estimated_depth_cm * 10 mean_depth_mm = max_depth_mm * 0.7 perimeter_cm = (perimeter_pixels * pixel_to_mm) / 10 area_cm2 = (pixel_area * pixel_to_mm * pixel_to_mm) / 100 area_m2 = area_cm2 / 10000 volume_liters = (area_m2 * (max_depth_mm / 1000) / 3) * 1000 depth_cm = max_depth_mm / 10 if depth_cm > 10 or area_m2 > 0.5: severity = 'CRITICAL' severity_color = '🔴' elif depth_cm > 5 or area_m2 > 0.2: severity = 'HIGH' severity_color = '🟠' elif depth_cm > 3: severity = 'MEDIUM' severity_color = '🟡' else: severity = 'LOW' severity_color = '🟢' return { 'max_depth_mm': max_depth_mm, 'max_depth_cm': max_depth_mm / 10, 'mean_depth_mm': mean_depth_mm, 'mean_depth_cm': mean_depth_mm / 10, 'perimeter_cm': perimeter_cm, 'perimeter_m': perimeter_cm / 100, 'area_cm2': area_cm2, 'area_m2': area_m2, 'volume_liters': volume_liters, 'volume_m3': volume_liters / 1000, 'num_pixels': int(pixel_area), 'severity': severity, 'severity_color': severity_color, 'contour': contour, 'bbox': (x, y, w, h), 'centroid': (cx, cy) } # ═══════════════════════════════════════════════════════════════════ # HTML GENERATION # ═══════════════════════════════════════════════════════════════════ def generate_metrics_html(measurements): """Generate HTML metrics for images""" if not measurements: return "
Duration: {duration_str} | Potholes: {stats['total_potholes']}
Frames: {pothole['frames_detected']} | Max Depth: {pothole['max_depth_cm']:.2f} cm | Max Volume: {pothole['max_volume_liters']:.2f} L