844 lines
28 KiB
Python
844 lines
28 KiB
Python
# Helper functions for drawing, IoU, and other utilities
|
|
|
|
import cv2
|
|
import numpy as np
|
|
import pandas as pd
|
|
import time
|
|
import os
|
|
import base64
|
|
from typing import Dict, List, Tuple, Optional, Any
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
import io
|
|
from PIL import Image
|
|
from red_light_violation_pipeline import RedLightViolationPipeline
|
|
|
|
def bbox_iou(box1, box2):
|
|
"""
|
|
Calculate IoU (Intersection over Union) between two bounding boxes
|
|
|
|
Args:
|
|
box1: First bounding box in format [x1, y1, x2, y2]
|
|
box2: Second bounding box in format [x1, y1, x2, y2]
|
|
|
|
Returns:
|
|
IoU score between 0 and 1
|
|
"""
|
|
# Ensure boxes are in [x1, y1, x2, y2] format and have valid dimensions
|
|
if len(box1) < 4 or len(box2) < 4:
|
|
return 0.0
|
|
|
|
# Convert to float and ensure x2 > x1 and y2 > y1
|
|
x1_1, y1_1, x2_1, y2_1 = map(float, box1[:4])
|
|
x1_2, y1_2, x2_2, y2_2 = map(float, box2[:4])
|
|
|
|
if x2_1 <= x1_1 or y2_1 <= y1_1 or x2_2 <= x1_2 or y2_2 <= y1_2:
|
|
return 0.0
|
|
|
|
# Calculate area of each box
|
|
area1 = (x2_1 - x1_1) * (y2_1 - y1_1)
|
|
area2 = (x2_2 - x1_2) * (y2_2 - y1_2)
|
|
|
|
if area1 <= 0 or area2 <= 0:
|
|
return 0.0
|
|
|
|
# Calculate intersection area
|
|
x1_i = max(x1_1, x1_2)
|
|
y1_i = max(y1_1, y1_2)
|
|
x2_i = min(x2_1, x2_2)
|
|
y2_i = min(y2_1, y2_2)
|
|
|
|
if x2_i <= x1_i or y2_i <= y1_i:
|
|
return 0.0 # No intersection
|
|
|
|
intersection_area = (x2_i - x1_i) * (y2_i - y1_i)
|
|
|
|
# Calculate IoU
|
|
union_area = area1 + area2 - intersection_area
|
|
|
|
if union_area <= 0:
|
|
return 0.0
|
|
|
|
iou = intersection_area / union_area
|
|
return iou
|
|
|
|
# Color mapping for traffic-related classes only
|
|
COLORS = {
|
|
'person': (255, 165, 0), # Orange
|
|
'bicycle': (255, 0, 255), # Magenta
|
|
'car': (0, 255, 0), # Green
|
|
'motorcycle': (255, 255, 0), # Cyan
|
|
'bus': (0, 0, 255), # Red
|
|
'truck': (0, 128, 255), # Orange-Blue
|
|
'traffic light': (0, 165, 255), # Orange
|
|
'stop sign': (0, 0, 139), # Dark Red
|
|
'parking meter': (128, 0, 128), # Purple
|
|
'default': (0, 255, 255) # Yellow as default
|
|
}
|
|
|
|
VIOLATION_COLORS = {
|
|
'red_light_violation': (0, 0, 255), # Red
|
|
'stop_sign_violation': (0, 100, 255), # Orange-Red
|
|
'speed_violation': (0, 255, 255), # Yellow
|
|
'lane_violation': (255, 0, 255), # Magenta
|
|
}
|
|
|
|
def draw_detections(frame: np.ndarray, detections: List[Dict],
|
|
draw_labels: bool = True, draw_confidence: bool = True) -> np.ndarray:
|
|
"""
|
|
Draw detection bounding boxes and labels on frame with enhanced robustness
|
|
|
|
Args:
|
|
frame: Input frame
|
|
detections: List of detection dictionaries
|
|
draw_labels: Whether to draw class labels
|
|
draw_confidence: Whether to draw confidence scores
|
|
|
|
Returns:
|
|
Annotated frame
|
|
"""
|
|
if frame is None or not isinstance(frame, np.ndarray) or frame.size == 0:
|
|
print("Warning: Invalid frame provided to draw_detections")
|
|
return np.zeros((300, 300, 3), dtype=np.uint8) # Return blank frame as fallback
|
|
|
|
annotated_frame = frame.copy()
|
|
|
|
# Handle case when detections is None or empty
|
|
if detections is None or len(detections) == 0:
|
|
return annotated_frame
|
|
|
|
# Get frame dimensions for validation
|
|
h, w = frame.shape[:2]
|
|
|
|
for detection in detections:
|
|
if not isinstance(detection, dict):
|
|
continue
|
|
|
|
try:
|
|
# Skip detection if it doesn't have bbox or has invalid confidence
|
|
if 'bbox' not in detection:
|
|
continue
|
|
|
|
# Skip if confidence is below threshold (don't rely on external filtering)
|
|
confidence = detection.get('confidence', 0.0)
|
|
if confidence < 0.01: # Apply a minimal threshold to ensure we're not drawing noise
|
|
continue
|
|
|
|
bbox = detection['bbox']
|
|
class_name = detection.get('class_name', 'unknown')
|
|
class_id = detection.get('class_id', -1)
|
|
|
|
# Get color for class
|
|
color = get_enhanced_class_color(class_name, class_id)
|
|
|
|
# Ensure bbox has enough coordinates and they are numeric values
|
|
if len(bbox) < 4 or not all(isinstance(coord, (int, float)) for coord in bbox[:4]):
|
|
continue
|
|
|
|
# Convert coordinates to integers
|
|
try:
|
|
x1, y1, x2, y2 = map(int, bbox[:4])
|
|
except (ValueError, TypeError):
|
|
print(f"Warning: Invalid bbox format: {bbox}")
|
|
continue
|
|
|
|
# Validate coordinates are within frame bounds
|
|
x1 = max(0, min(x1, w-1))
|
|
y1 = max(0, min(y1, h-1))
|
|
x2 = max(0, min(x2, w))
|
|
y2 = max(0, min(y2, h))
|
|
|
|
# Ensure x2 > x1 and y2 > y1 (at least 1 pixel width/height)
|
|
if x2 <= x1 or y2 <= y1:
|
|
# Instead of skipping, fix the coordinates to ensure at least 1 pixel width/height
|
|
x2 = max(x1 + 1, x2)
|
|
y2 = max(y1 + 1, y2)
|
|
|
|
# Draw bounding box
|
|
cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), color, 2)
|
|
|
|
# Prepare label text
|
|
label_parts = []
|
|
if draw_labels:
|
|
# Display proper class name
|
|
display_name = class_name.replace('_', ' ').title()
|
|
label_parts.append(display_name)
|
|
if draw_confidence:
|
|
label_parts.append(f"{confidence:.2f}")
|
|
|
|
# Add category indicator for clarity
|
|
category = detection.get('category', 'other')
|
|
if category != 'other':
|
|
label_parts.append(f"[{category.upper()}]")
|
|
|
|
# Draw license plate if available
|
|
if 'license_plate' in detection and detection['license_plate']:
|
|
plate_text = detection['license_plate'].get('text', 'Unknown')
|
|
label_parts.append(f"Plate: {plate_text}")
|
|
|
|
# Handle traffic light detection specially
|
|
if detection.get('type') == 'traffic_sign' and detection.get('sign_type') == 'traffic_light':
|
|
light_color = detection.get('color', 'unknown')
|
|
|
|
# Add traffic light color to label
|
|
if light_color != 'unknown':
|
|
# Set color indicator based on traffic light state
|
|
if light_color == 'red':
|
|
color_indicator = (0, 0, 255) # Red
|
|
label_parts.append("🔴 RED")
|
|
elif light_color == 'yellow':
|
|
color_indicator = (0, 255, 255) # Yellow
|
|
label_parts.append("🟡 YELLOW")
|
|
elif light_color == 'green':
|
|
color_indicator = (0, 255, 0) # Green
|
|
label_parts.append("🟢 GREEN")
|
|
|
|
# Draw traffic light visual indicator (circle with detected color)
|
|
circle_y = y1 - 15
|
|
circle_x = x1 + 10
|
|
circle_radius = 10
|
|
|
|
if light_color == 'red':
|
|
cv2.circle(annotated_frame, (circle_x, circle_y), circle_radius, (0, 0, 255), -1)
|
|
elif light_color == 'yellow':
|
|
cv2.circle(annotated_frame, (circle_x, circle_y), circle_radius, (0, 255, 255), -1)
|
|
elif light_color == 'green':
|
|
cv2.circle(annotated_frame, (circle_x, circle_y), circle_radius, (0, 255, 0), -1)
|
|
else:
|
|
cv2.circle(annotated_frame, (circle_x, circle_y), circle_radius, (128, 128, 128), -1)
|
|
|
|
# Draw label if we have any text
|
|
if label_parts:
|
|
label = " ".join(label_parts)
|
|
|
|
try:
|
|
# Get text size for background
|
|
(text_width, text_height), baseline = cv2.getTextSize(
|
|
label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2
|
|
)
|
|
|
|
# Ensure label position is within frame
|
|
text_x = max(0, min(x1, w - text_width))
|
|
text_y = max(text_height + 10, y1)
|
|
|
|
# Draw label background (use colored background)
|
|
bg_color = tuple(int(c * 0.8) for c in color) # Darker version of box color
|
|
cv2.rectangle(
|
|
annotated_frame,
|
|
(text_x, text_y - text_height - 10),
|
|
(text_x + text_width + 5, text_y),
|
|
bg_color,
|
|
-1
|
|
)
|
|
# Draw label text (white text on colored background)
|
|
cv2.putText(
|
|
annotated_frame,
|
|
label,
|
|
(text_x + 2, text_y - 5),
|
|
cv2.FONT_HERSHEY_SIMPLEX,
|
|
0.6,
|
|
(255, 255, 255), # White text
|
|
2
|
|
)
|
|
except Exception as e:
|
|
print(f"Error drawing label: {e}")
|
|
|
|
except Exception as e:
|
|
print(f"Error drawing detection: {e}")
|
|
continue
|
|
|
|
return annotated_frame
|
|
|
|
def draw_violations(frame: np.ndarray, violations: List[Dict]) -> np.ndarray:
|
|
"""
|
|
Draw violation indicators on frame with enhanced robustness
|
|
|
|
Args:
|
|
frame: Input frame
|
|
violations: List of violation dictionaries
|
|
|
|
Returns:
|
|
Annotated frame with violations
|
|
"""
|
|
if frame is None or not isinstance(frame, np.ndarray) or frame.size == 0:
|
|
print("Warning: Invalid frame provided to draw_violations")
|
|
return np.zeros((300, 300, 3), dtype=np.uint8) # Return blank frame as fallback
|
|
|
|
# Handle case when violations is None or empty
|
|
if violations is None or len(violations) == 0:
|
|
return frame.copy()
|
|
|
|
annotated_frame = frame.copy()
|
|
h, w = frame.shape[:2]
|
|
|
|
for violation in violations:
|
|
if not isinstance(violation, dict):
|
|
continue
|
|
|
|
try:
|
|
violation_type = violation.get('type', 'unknown')
|
|
color = VIOLATION_COLORS.get(violation_type, (0, 0, 255)) # Default to red
|
|
|
|
# Draw vehicle bbox if available
|
|
bbox = None
|
|
if 'vehicle_bbox' in violation:
|
|
bbox = violation['vehicle_bbox']
|
|
elif 'bbox' in violation:
|
|
bbox = violation['bbox']
|
|
|
|
if bbox and len(bbox) >= 4:
|
|
# Ensure bbox coordinates are numeric
|
|
if not all(isinstance(coord, (int, float)) for coord in bbox[:4]):
|
|
continue
|
|
|
|
try:
|
|
# Convert bbox coordinates to integers
|
|
x1, y1, x2, y2 = map(int, bbox[:4])
|
|
except (ValueError, TypeError):
|
|
print(f"Warning: Invalid violation bbox format: {bbox}")
|
|
continue
|
|
|
|
# Validate coordinates are within frame bounds
|
|
x1 = max(0, min(x1, w-1))
|
|
y1 = max(0, min(y1, h-1))
|
|
x2 = max(0, min(x2, w))
|
|
y2 = max(0, min(y2, h))
|
|
|
|
# Ensure x2 > x1 and y2 > y1 (at least 1 pixel width/height)
|
|
if x2 <= x1 or y2 <= y1:
|
|
# Instead of skipping, fix the coordinates
|
|
x2 = max(x1 + 1, x2)
|
|
y2 = max(y1 + 1, y2)
|
|
|
|
# Draw thicker red border for violations
|
|
cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), color, 4)
|
|
|
|
# Add violation warning icon
|
|
icon_x = max(0, min(x1 - 30, w-30))
|
|
icon_y = max(30, min(y1 + 30, h-10))
|
|
cv2.putText(annotated_frame, "⚠", (icon_x, icon_y),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 1.5, color, 3)
|
|
|
|
# Draw violation description
|
|
description = violation.get('description', violation_type)
|
|
severity = violation.get('severity', 'medium')
|
|
|
|
# Position for violation text - ensure it's visible
|
|
y_position = min(50 + (violations.index(violation) * 30), h - 40)
|
|
|
|
# Draw violation text with background
|
|
text = f"VIOLATION: {description} ({severity.upper()})"
|
|
draw_text_with_background(annotated_frame, text, (10, y_position), color)
|
|
except Exception as e:
|
|
print(f"Error drawing violation: {e}")
|
|
|
|
return annotated_frame
|
|
|
|
def get_enhanced_class_color(class_name: str, class_id: int) -> Tuple[int, int, int]:
|
|
"""
|
|
Get color for class with enhanced mapping (traffic classes only)
|
|
|
|
Args:
|
|
class_name: Name of the detected class
|
|
class_id: COCO class ID
|
|
|
|
Returns:
|
|
BGR color tuple
|
|
"""
|
|
# Only traffic class IDs/colors
|
|
enhanced_colors = {
|
|
0: (255, 165, 0), # person - Orange
|
|
1: (255, 0, 255), # bicycle - Magenta
|
|
2: (0, 255, 0), # car - Green
|
|
3: (255, 255, 0), # motorcycle - Cyan
|
|
4: (0, 0, 255), # bus - Red
|
|
5: (0, 128, 255), # truck - Orange-Blue
|
|
6: (0, 165, 255), # traffic light - Orange
|
|
7: (0, 0, 139), # stop sign - Dark Red
|
|
8: (128, 0, 128), # parking meter - Purple
|
|
}
|
|
|
|
# Get color from class name if available
|
|
if class_name.lower() in COLORS:
|
|
return COLORS[class_name.lower()]
|
|
|
|
# Get color from class ID if available
|
|
if class_id in enhanced_colors:
|
|
return enhanced_colors[class_id]
|
|
|
|
# Default color
|
|
return COLORS['default']
|
|
|
|
def draw_text_with_background(frame: np.ndarray, text: str, position: Tuple[int, int],
|
|
color: Tuple[int, int, int], alpha: float = 0.7) -> np.ndarray:
|
|
"""
|
|
Draw text with semi-transparent background
|
|
|
|
Args:
|
|
frame: Input frame
|
|
text: Text to display
|
|
position: Position (x, y) to display text
|
|
color: Color for text and border
|
|
alpha: Background transparency (0-1)
|
|
|
|
Returns:
|
|
Frame with text
|
|
"""
|
|
x, y = position
|
|
|
|
# Get text size
|
|
(text_width, text_height), baseline = cv2.getTextSize(
|
|
text, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2
|
|
)
|
|
|
|
# Create background
|
|
bg_color = (30, 30, 30) # Dark background
|
|
cv2.rectangle(
|
|
frame,
|
|
(x, y - text_height - 10),
|
|
(x + text_width + 10, y + 10),
|
|
bg_color,
|
|
-1
|
|
)
|
|
|
|
# Add colored border
|
|
cv2.rectangle(
|
|
frame,
|
|
(x, y - text_height - 10),
|
|
(x + text_width + 10, y + 10),
|
|
color,
|
|
2
|
|
)
|
|
|
|
# Add text
|
|
cv2.putText(
|
|
frame,
|
|
text,
|
|
(x + 5, y),
|
|
cv2.FONT_HERSHEY_SIMPLEX,
|
|
0.7,
|
|
color,
|
|
2
|
|
)
|
|
|
|
return frame
|
|
|
|
def create_detection_summary(detections: List[Dict]) -> Dict[str, int]:
|
|
"""
|
|
Create summary of detections
|
|
|
|
Args:
|
|
detections: List of detection dictionaries
|
|
|
|
Returns:
|
|
Dictionary with detection counts by type
|
|
"""
|
|
summary = {
|
|
'total': len(detections),
|
|
'vehicles': 0,
|
|
'pedestrians': 0,
|
|
'traffic_signs': 0,
|
|
'bicycles': 0,
|
|
'motorcycles': 0,
|
|
'license_plates': 0
|
|
}
|
|
|
|
vehicle_types = {}
|
|
|
|
for detection in detections:
|
|
detection_type = detection.get('type', '')
|
|
class_name = detection.get('class_name', '').lower()
|
|
|
|
if detection_type == 'vehicle':
|
|
summary['vehicles'] += 1
|
|
vehicle_type = detection.get('vehicle_type', 'unknown')
|
|
vehicle_types[vehicle_type] = vehicle_types.get(vehicle_type, 0) + 1
|
|
|
|
# Count license plates
|
|
if detection.get('license_plate'):
|
|
summary['license_plates'] += 1
|
|
|
|
elif 'person' in class_name:
|
|
summary['pedestrians'] += 1
|
|
elif detection_type == 'traffic_sign':
|
|
summary['traffic_signs'] += 1
|
|
elif 'bicycle' in class_name:
|
|
summary['bicycles'] += 1
|
|
elif 'motorcycle' in class_name:
|
|
summary['motorcycles'] += 1
|
|
|
|
# Add vehicle type breakdowns
|
|
for vehicle_type, count in vehicle_types.items():
|
|
summary[f"vehicle_{vehicle_type}"] = count
|
|
|
|
return summary
|
|
|
|
def create_performance_metrics(detector, violation_detector) -> Dict[str, Any]:
|
|
"""
|
|
Create performance metrics
|
|
|
|
Args:
|
|
detector: Vehicle detector instance
|
|
violation_detector: Violation detector instance
|
|
|
|
Returns:
|
|
Dictionary with performance metrics
|
|
"""
|
|
metrics = {}
|
|
|
|
# Detection metrics
|
|
if detector:
|
|
try:
|
|
# Try to get detector metrics
|
|
if hasattr(detector, 'get_performance_stats'):
|
|
stats = detector.get_performance_stats()
|
|
metrics.update({
|
|
'fps': stats.get('fps', 0),
|
|
'inference_time': stats.get('avg_inference_time', 0) * 1000,
|
|
'frames_processed': stats.get('frames_processed', 0)
|
|
})
|
|
|
|
# Add detection count
|
|
metrics['detection_count'] = getattr(detector, 'detection_count', 0)
|
|
except Exception:
|
|
pass
|
|
|
|
# Violation metrics
|
|
if violation_detector:
|
|
try:
|
|
# Count violations
|
|
violation_count = len(violation_detector.violation_history)
|
|
metrics['violation_count'] = violation_count
|
|
except Exception:
|
|
pass
|
|
|
|
# Add session metrics
|
|
try:
|
|
import streamlit as st
|
|
if 'start_time' in st.session_state:
|
|
uptime = time.time() - st.session_state.start_time
|
|
metrics['uptime'] = f"{uptime/3600:.1f}h"
|
|
|
|
if 'processed_frames' in st.session_state:
|
|
metrics['processed_frames'] = st.session_state.processed_frames
|
|
except ImportError:
|
|
# Streamlit not available
|
|
pass
|
|
|
|
return metrics
|
|
|
|
def export_detections_to_csv(detection_history: List[List[Dict]]) -> str:
|
|
"""
|
|
Export detection history to CSV
|
|
|
|
Args:
|
|
detection_history: List of frame detection lists
|
|
|
|
Returns:
|
|
CSV string
|
|
"""
|
|
records = []
|
|
|
|
for frame_idx, frame_detections in enumerate(detection_history):
|
|
for detection in frame_detections:
|
|
record = {
|
|
'frame_id': frame_idx,
|
|
'timestamp': detection.get('timestamp', ''),
|
|
'class_name': detection.get('class_name', ''),
|
|
'confidence': detection.get('confidence', 0),
|
|
'bbox_x1': detection['bbox'][0] if 'bbox' in detection else 0,
|
|
'bbox_y1': detection['bbox'][1] if 'bbox' in detection else 0,
|
|
'bbox_x2': detection['bbox'][2] if 'bbox' in detection else 0,
|
|
'bbox_y2': detection['bbox'][3] if 'bbox' in detection else 0,
|
|
'type': detection.get('type', ''),
|
|
'vehicle_type': detection.get('vehicle_type', ''),
|
|
'license_plate': detection.get('license_plate', {}).get('text', '') if detection.get('license_plate') else ''
|
|
}
|
|
records.append(record)
|
|
|
|
# Convert to DataFrame and then CSV
|
|
if records:
|
|
df = pd.DataFrame(records)
|
|
return df.to_csv(index=False)
|
|
else:
|
|
return "No data available"
|
|
|
|
def save_annotated_frame(frame: np.ndarray, suffix: str = None) -> str:
|
|
"""
|
|
Save annotated frame to temp file
|
|
|
|
Args:
|
|
frame: Frame to save
|
|
suffix: Optional filename suffix
|
|
|
|
Returns:
|
|
Path to saved file
|
|
"""
|
|
import tempfile
|
|
|
|
timestamp = int(time.time())
|
|
suffix = f"_{suffix}" if suffix else ""
|
|
filename = f"traffic_frame_{timestamp}{suffix}.jpg"
|
|
filepath = os.path.join(tempfile.gettempdir(), filename)
|
|
|
|
cv2.imwrite(filepath, frame)
|
|
return filepath
|
|
|
|
def resize_frame_for_display(frame: np.ndarray, max_width: int = 800) -> np.ndarray:
|
|
"""
|
|
Resize frame for display while maintaining aspect ratio
|
|
|
|
Args:
|
|
frame: Input frame
|
|
max_width: Maximum display width
|
|
|
|
Returns:
|
|
Resized frame
|
|
"""
|
|
height, width = frame.shape[:2]
|
|
|
|
# Only resize if width exceeds max_width
|
|
if width > max_width:
|
|
ratio = max_width / width
|
|
new_height = int(height * ratio)
|
|
return cv2.resize(frame, (max_width, new_height))
|
|
|
|
return frame
|
|
|
|
def load_configuration(config_file: str = "config.json") -> Dict:
|
|
"""
|
|
Load application configuration
|
|
|
|
Args:
|
|
config_file: Configuration file path
|
|
|
|
Returns:
|
|
Configuration dictionary
|
|
"""
|
|
default_config = {
|
|
"detection": {
|
|
"confidence_threshold": 0.4,
|
|
"enable_ocr": True,
|
|
"enable_tracking": True
|
|
},
|
|
"violations": {
|
|
"red_light_grace_period": 2.0,
|
|
"stop_sign_duration": 3.0,
|
|
"speed_tolerance": 10
|
|
},
|
|
"display": {
|
|
"show_confidence": True,
|
|
"show_labels": True,
|
|
"show_license_plates": True,
|
|
"max_display_width": 800
|
|
}
|
|
}
|
|
|
|
# Try to load existing configuration
|
|
try:
|
|
with open(config_file, 'r') as f:
|
|
config = json.load(f)
|
|
return config
|
|
except Exception:
|
|
# Return default if loading fails
|
|
return default_config
|
|
|
|
def save_configuration(config: Dict, config_file: str = "config.json"):
|
|
"""
|
|
Save application configuration
|
|
|
|
Args:
|
|
config: Configuration dictionary
|
|
config_file: Configuration file path
|
|
"""
|
|
with open(config_file, 'w') as f:
|
|
json.dump(config, f, indent=4)
|
|
|
|
class StreamlitUtils:
|
|
"""Utility methods for Streamlit UI"""
|
|
|
|
@staticmethod
|
|
def display_metrics(metrics: Dict, col1, col2, col3, col4):
|
|
"""
|
|
Display metrics in columns
|
|
|
|
Args:
|
|
metrics: Dictionary of metrics
|
|
col1, col2, col3, col4: Streamlit columns
|
|
"""
|
|
try:
|
|
import streamlit as st
|
|
|
|
# First column - Detection counts
|
|
with col1:
|
|
if 'detection_count' in metrics:
|
|
st.metric("Detections", metrics['detection_count'])
|
|
elif 'total_detections' in metrics:
|
|
st.metric("Detections", metrics['total_detections'])
|
|
|
|
# Second column - Violation counts
|
|
with col2:
|
|
if 'violation_count' in metrics:
|
|
st.metric("Violations", metrics['violation_count'])
|
|
|
|
# Third column - Performance
|
|
with col3:
|
|
if 'fps' in metrics:
|
|
st.metric("FPS", f"{metrics['fps']:.2f}")
|
|
elif 'processing_fps' in metrics:
|
|
st.metric("Processing FPS", f"{metrics['processing_fps']:.2f}")
|
|
|
|
# Fourth column - Status
|
|
with col4:
|
|
if 'uptime' in metrics:
|
|
st.metric("Uptime", metrics['uptime'])
|
|
elif 'frames_processed' in metrics:
|
|
st.metric("Frames", metrics['frames_processed'])
|
|
except ImportError:
|
|
# Streamlit not available
|
|
pass
|
|
|
|
@staticmethod
|
|
def display_detection_summary(summary: Dict):
|
|
"""
|
|
Display detection summary
|
|
|
|
Args:
|
|
summary: Detection summary dictionary
|
|
"""
|
|
try:
|
|
import streamlit as st
|
|
|
|
if not summary or summary.get('total', 0) == 0:
|
|
st.info("No detections to display.")
|
|
return
|
|
|
|
# Create summary table
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
st.metric("Total Objects", summary['total'])
|
|
st.metric("Vehicles", summary['vehicles'])
|
|
st.metric("Pedestrians", summary['pedestrians'])
|
|
|
|
with col2:
|
|
st.metric("Traffic Signs", summary['traffic_signs'])
|
|
st.metric("License Plates", summary['license_plates'])
|
|
|
|
# Performance indicator
|
|
if 'vehicles' in summary and summary['vehicles'] > 0:
|
|
license_rate = summary['license_plates'] / summary['vehicles'] * 100
|
|
st.metric("Plate Detection", f"{license_rate:.1f}%")
|
|
except ImportError:
|
|
# Streamlit not available
|
|
pass
|
|
|
|
@staticmethod
|
|
def display_violation_alerts(violations: List[Dict]):
|
|
"""
|
|
Display violation alerts
|
|
|
|
Args:
|
|
violations: List of violation dictionaries
|
|
"""
|
|
try:
|
|
import streamlit as st
|
|
|
|
if not violations:
|
|
st.info("No violations detected.")
|
|
return
|
|
|
|
for violation in violations:
|
|
violation_type = violation['type']
|
|
severity = violation.get('severity', 'medium')
|
|
description = violation.get('description', violation_type)
|
|
|
|
# Format violation alert based on severity
|
|
if severity == 'high':
|
|
alert_icon = "🔴"
|
|
alert_color = "red"
|
|
elif severity == 'medium':
|
|
alert_icon = "🟡"
|
|
alert_color = "orange"
|
|
else:
|
|
alert_icon = "🟢"
|
|
alert_color = "green"
|
|
|
|
# Display alert
|
|
st.markdown(
|
|
f"<div class='violation-alert' style='border-color: {alert_color};'>"
|
|
f"<h3>{alert_icon} {violation_type.replace('_', ' ').title()}</h3>"
|
|
f"<p>{description}</p>"
|
|
f"<p><strong>Severity:</strong> {severity.upper()}</p>"
|
|
f"</div>",
|
|
unsafe_allow_html=True
|
|
)
|
|
except ImportError:
|
|
# Streamlit not available
|
|
pass
|
|
|
|
@staticmethod
|
|
def create_download_button(data: Any, file_name: str, button_text: str, mime_type: str = "text/csv"):
|
|
"""
|
|
Create file download button
|
|
|
|
Args:
|
|
data: File data
|
|
file_name: Download file name
|
|
button_text: Button label text
|
|
mime_type: MIME type of file
|
|
"""
|
|
try:
|
|
import streamlit as st
|
|
|
|
if isinstance(data, str):
|
|
data = data.encode()
|
|
|
|
b64 = base64.b64encode(data).decode()
|
|
href = f'data:{mime_type};base64,{b64}'
|
|
|
|
st.download_button(
|
|
label=button_text,
|
|
data=data,
|
|
file_name=file_name,
|
|
mime=mime_type
|
|
)
|
|
except ImportError:
|
|
# Streamlit not available
|
|
pass
|
|
|
|
def bbox_iou(boxA, boxB):
|
|
"""
|
|
Calculate the Intersection over Union (IoU) of two bounding boxes.
|
|
|
|
Args:
|
|
boxA: First bounding box (x1, y1, x2, y2)
|
|
boxB: Second bounding box (x1, y1, x2, y2)
|
|
|
|
Returns:
|
|
IoU value between 0 and 1
|
|
"""
|
|
# Determine the coordinates of the intersection rectangle
|
|
xA = max(boxA[0], boxB[0])
|
|
yA = max(boxA[1], boxB[1])
|
|
xB = min(boxA[2], boxB[2])
|
|
yB = min(boxA[3], boxB[3])
|
|
|
|
# Compute the area of intersection
|
|
interArea = max(0, xB - xA + 1) * max(0, yB - yA + 1)
|
|
|
|
# Compute the area of both bounding boxes
|
|
boxAArea = (boxA[2] - boxA[0] + 1) * (boxA[3] - boxA[1] + 1)
|
|
boxBArea = (boxB[2] - boxB[0] + 1) * (boxB[3] - boxB[1] + 1)
|
|
|
|
# Compute the IoU
|
|
iou = interArea / float(boxAArea + boxBArea - interArea + 1e-6)
|
|
|
|
return iou
|
|
|
|
__all__ = [
|
|
"draw_detections", "draw_violations", "create_performance_metrics",
|
|
"load_configuration", "save_configuration", "StreamlitUtils",
|
|
"bbox_iou"
|
|
]
|