# ═══════════════════════════════════════════════════════════════════ # 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 "

No measurements available

" html = """
🕳️ Pothole Detection Results
""" for m in measurements: severity_class = f"severity-{m['severity']}" html += f"""
{m['severity_color']} Pothole #{m['pothole_id']} {m['severity']}
Confidence: {m['confidence']*100:.1f}%
📏 Max Depth
{m['max_depth_cm']:.2f} cm
📦 Area
{m['area_m2']:.4f} m²
💧 Volume
{m['volume_liters']:.2f} L
📍 Centroid
({m['centroid'][0]}, {m['centroid'][1]})
""" total_area = sum(m['area_m2'] for m in measurements) total_volume = sum(m['volume_liters'] for m in measurements) avg_depth = np.mean([m['max_depth_cm'] for m in measurements]) html += f"""

📊 Overall Summary

Total Potholes: {len(measurements)}
Average Depth: {avg_depth:.2f} cm
Total Area: {total_area:.4f} m²
Total Volume: {total_volume:.2f} L
""" return html def generate_video_metrics_html(stats, total_frames, fps): """Generate HTML metrics for video""" if stats['total_potholes'] == 0: return "

⚠️ No potholes detected

" duration_str = str(timedelta(seconds=int(total_frames / fps))) html = f"""
🎥 Video Report

📊 Summary

Duration: {duration_str} | Potholes: {stats['total_potholes']}

""" for pothole in sorted(stats['potholes'], key=lambda x: x['track_id']): severity_class = f"severity-{pothole['severity']}" html += f"""
🕳️ ID: {pothole['track_id']} {pothole['severity']}

Frames: {pothole['frames_detected']} | Max Depth: {pothole['max_depth_cm']:.2f} cm | Max Volume: {pothole['max_volume_liters']:.2f} L

""" html += "
" return html def generate_summary_text(measurements): """Generate text summary for images""" if not measurements: return "No potholes detected." summary = f"🔍 DETECTION SUMMARY\n{'='*50}\n\n" summary += f"Total Potholes: {len(measurements)}\n\n" for m in measurements: summary += f"{m['severity_color']} Pothole #{m['pothole_id']} - {m['severity']}\n" summary += f" Confidence: {m['confidence']*100:.1f}%\n" summary += f" Depth: {m['max_depth_cm']:.2f} cm\n" summary += f" Area: {m['area_m2']:.4f} m²\n" summary += f" Volume: {m['volume_liters']:.2f} L\n" summary += f" Centroid: ({m['centroid'][0]}, {m['centroid'][1]})\n\n" return summary def generate_video_summary_text(stats, total_frames, fps): """Generate text summary for video""" if stats['total_potholes'] == 0: return "No potholes detected in video." duration_str = str(timedelta(seconds=int(total_frames / fps))) summary = f"🎥 VIDEO REPORT\n{'='*70}\n\n" summary += f"Duration: {duration_str} | Frames: {total_frames:,} | FPS: {fps}\n" summary += f"Unique Potholes: {stats['total_potholes']}\n\n" for pothole in sorted(stats['potholes'], key=lambda x: x['track_id']): summary += f"🕳️ ID {pothole['track_id']} - {pothole['severity']}\n" summary += f" Frames: {pothole['frames_detected']}\n" summary += f" First: Frame {pothole['first_frame']} ({pothole['first_timestamp']})\n" summary += f" Max Depth: {pothole['max_depth_cm']:.2f} cm\n" summary += f" Max Volume: {pothole['max_volume_liters']:.2f} L\n\n" return summary