Spaces:
Sleeping
Sleeping
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 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 "<h3 style='color: orange;'>No measurements available</h3>" | |
| html = """ | |
| <style> | |
| .metrics-container { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| padding: 20px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 15px; | |
| color: white; | |
| } | |
| .metrics-header { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| font-size: 24px; | |
| font-weight: bold; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| } | |
| .pothole-card { | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin: 10px 0; | |
| color: #333; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| } | |
| .pothole-header { | |
| font-size: 18px; | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .severity-badge { | |
| padding: 5px 15px; | |
| border-radius: 20px; | |
| font-weight: bold; | |
| font-size: 14px; | |
| } | |
| .severity-CRITICAL { background: #ff4444; color: white; } | |
| .severity-HIGH { background: #ff9800; color: white; } | |
| .severity-MEDIUM { background: #ffeb3b; color: #333; } | |
| .severity-LOW { background: #4caf50; color: white; } | |
| .metrics-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 10px; | |
| margin-top: 10px; | |
| } | |
| .metric-item { | |
| background: #f5f5f5; | |
| padding: 10px; | |
| border-radius: 5px; | |
| border-left: 4px solid #667eea; | |
| } | |
| .metric-label { | |
| font-size: 12px; | |
| color: #666; | |
| margin-bottom: 3px; | |
| } | |
| .metric-value { | |
| font-size: 18px; | |
| font-weight: bold; | |
| color: #333; | |
| } | |
| .summary-section { | |
| margin-top: 20px; | |
| padding: 15px; | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 10px; | |
| color: #333; | |
| } | |
| </style> | |
| <div class="metrics-container"> | |
| <div class="metrics-header">π³οΈ Pothole Detection Results</div> | |
| """ | |
| for m in measurements: | |
| severity_class = f"severity-{m['severity']}" | |
| html += f""" | |
| <div class="pothole-card"> | |
| <div class="pothole-header"> | |
| <span>{m['severity_color']} Pothole #{m['pothole_id']}</span> | |
| <span class="severity-badge {severity_class}">{m['severity']}</span> | |
| </div> | |
| <div style="margin-bottom: 10px;"><strong>Confidence:</strong> {m['confidence']*100:.1f}%</div> | |
| <div class="metrics-grid"> | |
| <div class="metric-item"> | |
| <div class="metric-label">π Max Depth</div> | |
| <div class="metric-value">{m['max_depth_cm']:.2f} cm</div> | |
| </div> | |
| <div class="metric-item"> | |
| <div class="metric-label">π¦ Area</div> | |
| <div class="metric-value">{m['area_m2']:.4f} mΒ²</div> | |
| </div> | |
| <div class="metric-item"> | |
| <div class="metric-label">π§ Volume</div> | |
| <div class="metric-value">{m['volume_liters']:.2f} L</div> | |
| </div> | |
| <div class="metric-item"> | |
| <div class="metric-label">π Centroid</div> | |
| <div class="metric-value">({m['centroid'][0]}, {m['centroid'][1]})</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| 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""" | |
| <div class="summary-section"> | |
| <h3>π Overall Summary</h3> | |
| <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;"> | |
| <div><strong>Total Potholes:</strong> {len(measurements)}</div> | |
| <div><strong>Average Depth:</strong> {avg_depth:.2f} cm</div> | |
| <div><strong>Total Area:</strong> {total_area:.4f} mΒ²</div> | |
| <div><strong>Total Volume:</strong> {total_volume:.2f} L</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| return html | |
| def generate_video_metrics_html(stats, total_frames, fps): | |
| """Generate HTML metrics for video""" | |
| if stats['total_potholes'] == 0: | |
| return "<h3 style='color: orange;'>β οΈ No potholes detected</h3>" | |
| duration_str = str(timedelta(seconds=int(total_frames / fps))) | |
| html = f""" | |
| <style> | |
| .video-metrics {{ | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| padding: 20px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 15px; | |
| color: white; | |
| }} | |
| .video-summary {{ | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin: 10px 0; | |
| color: #333; | |
| }} | |
| .pothole-track {{ | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin: 10px 0; | |
| color: #333; | |
| }} | |
| .severity-badge {{ | |
| padding: 5px 15px; | |
| border-radius: 20px; | |
| font-weight: bold; | |
| }} | |
| .severity-CRITICAL {{ background: #ff4444; color: white; }} | |
| .severity-HIGH {{ background: #ff9800; color: white; }} | |
| .severity-MEDIUM {{ background: #ffeb3b; color: #333; }} | |
| .severity-LOW {{ background: #4caf50; color: white; }} | |
| </style> | |
| <div class="video-metrics"> | |
| <div style="text-align: center; margin-bottom: 20px; font-size: 24px; font-weight: bold;"> | |
| π₯ Video Report | |
| </div> | |
| <div class="video-summary"> | |
| <h3>π Summary</h3> | |
| <p><strong>Duration:</strong> {duration_str} | <strong>Potholes:</strong> {stats['total_potholes']}</p> | |
| </div> | |
| """ | |
| for pothole in sorted(stats['potholes'], key=lambda x: x['track_id']): | |
| severity_class = f"severity-{pothole['severity']}" | |
| html += f""" | |
| <div class="pothole-track"> | |
| <div style="display: flex; justify-content: space-between;"> | |
| <span>π³οΈ ID: {pothole['track_id']}</span> | |
| <span class="severity-badge {severity_class}">{pothole['severity']}</span> | |
| </div> | |
| <p><strong>Frames:</strong> {pothole['frames_detected']} | | |
| <strong>Max Depth:</strong> {pothole['max_depth_cm']:.2f} cm | | |
| <strong>Max Volume:</strong> {pothole['max_volume_liters']:.2f} L</p> | |
| </div> | |
| """ | |
| html += "</div>" | |
| 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 |