pothole_video / utils.py
hardiksharma6555's picture
Create utils.py
18d55a0 verified
# ═══════════════════════════════════════════════════════════════════
# 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