Upload 19 files
Browse files- __init__.py +39 -0
- accident_model.onnx +3 -0
- components.py +184 -0
- head_on_collision.gif +3 -0
- helpers.py +287 -0
- map_viewer.py +435 -0
- mindspore_logo.png +0 -0
- model_metadata.json +22 -0
- party_input.py +243 -0
- rear_end_collision.gif +3 -0
- report_generator.py +734 -0
- results_display.py +552 -0
- scenario_analyzer.py +515 -0
- side_impact_collision.gif +3 -0
- sumo_interface.py +556 -0
- vehicle_input.py +196 -0
- video_generator.py +919 -0
- video_generator_old.py +320 -0
- video_manifest.json +5 -0
__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utils Package
|
| 3 |
+
=============
|
| 4 |
+
Utility functions for the Traffic Accident Reconstruction System.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from .helpers import (
|
| 8 |
+
calculate_distance,
|
| 9 |
+
calculate_bearing,
|
| 10 |
+
interpolate_path,
|
| 11 |
+
estimate_speed_at_point,
|
| 12 |
+
find_intersection_point,
|
| 13 |
+
calculate_impact_angle,
|
| 14 |
+
estimate_stopping_distance,
|
| 15 |
+
format_duration,
|
| 16 |
+
format_speed,
|
| 17 |
+
format_distance,
|
| 18 |
+
validate_coordinates,
|
| 19 |
+
validate_speed,
|
| 20 |
+
generate_unique_id,
|
| 21 |
+
get_timestamp
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
__all__ = [
|
| 25 |
+
'calculate_distance',
|
| 26 |
+
'calculate_bearing',
|
| 27 |
+
'interpolate_path',
|
| 28 |
+
'estimate_speed_at_point',
|
| 29 |
+
'find_intersection_point',
|
| 30 |
+
'calculate_impact_angle',
|
| 31 |
+
'estimate_stopping_distance',
|
| 32 |
+
'format_duration',
|
| 33 |
+
'format_speed',
|
| 34 |
+
'format_distance',
|
| 35 |
+
'validate_coordinates',
|
| 36 |
+
'validate_speed',
|
| 37 |
+
'generate_unique_id',
|
| 38 |
+
'get_timestamp'
|
| 39 |
+
]
|
accident_model.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b5d5e3dae6d3465e9b1cf71aabe3141b00300d962772cdd04b6d346cd1b910f4
|
| 3 |
+
size 64081
|
components.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
UI Components for Traffic Accident Reconstruction System
|
| 3 |
+
========================================================
|
| 4 |
+
Reusable Streamlit components for the application.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from config import CASE_STUDY_LOCATION, ALTERNATIVE_LOCATIONS
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def render_sidebar():
|
| 12 |
+
"""Render the application sidebar."""
|
| 13 |
+
|
| 14 |
+
with st.sidebar:
|
| 15 |
+
# MindSpore Logo at the top
|
| 16 |
+
try:
|
| 17 |
+
st.image("assets/mindspore_logo.png", width=120)
|
| 18 |
+
except:
|
| 19 |
+
st.markdown("### π€ MindSpore")
|
| 20 |
+
|
| 21 |
+
st.markdown("---")
|
| 22 |
+
|
| 23 |
+
st.header("π Accident Analyzer")
|
| 24 |
+
|
| 25 |
+
st.markdown("""
|
| 26 |
+
This system helps traffic authorities
|
| 27 |
+
understand accidents through:
|
| 28 |
+
|
| 29 |
+
- **AI-powered** scenario generation
|
| 30 |
+
- **Real map** visualization
|
| 31 |
+
- **2D simulation** of accidents
|
| 32 |
+
- **Probability analysis** of scenarios
|
| 33 |
+
""")
|
| 34 |
+
|
| 35 |
+
st.markdown("---")
|
| 36 |
+
|
| 37 |
+
st.subheader("π Case Study Location")
|
| 38 |
+
|
| 39 |
+
st.info(f"""
|
| 40 |
+
**{CASE_STUDY_LOCATION['name']}**
|
| 41 |
+
|
| 42 |
+
π {CASE_STUDY_LOCATION['city']}, {CASE_STUDY_LOCATION['country']}
|
| 43 |
+
|
| 44 |
+
πΊοΈ Lat: {CASE_STUDY_LOCATION['latitude']:.4f}
|
| 45 |
+
πΊοΈ Lng: {CASE_STUDY_LOCATION['longitude']:.4f}
|
| 46 |
+
""")
|
| 47 |
+
|
| 48 |
+
st.markdown("---")
|
| 49 |
+
|
| 50 |
+
st.subheader("π Help")
|
| 51 |
+
|
| 52 |
+
with st.expander("How to use"):
|
| 53 |
+
st.markdown("""
|
| 54 |
+
1. **Select Location**: View the roundabout location
|
| 55 |
+
2. **Vehicle 1**: Enter first vehicle details and draw its path
|
| 56 |
+
3. **Vehicle 2**: Enter second vehicle details and draw its path
|
| 57 |
+
4. **Analyze**: Let AI generate possible scenarios
|
| 58 |
+
5. **Results**: View scenarios with probability scores
|
| 59 |
+
""")
|
| 60 |
+
|
| 61 |
+
with st.expander("About MindSpore"):
|
| 62 |
+
st.markdown("""
|
| 63 |
+
This system uses **Huawei MindSpore**
|
| 64 |
+
for AI-powered scenario generation.
|
| 65 |
+
|
| 66 |
+
MindSpore is an open-source deep
|
| 67 |
+
learning framework optimized for
|
| 68 |
+
Ascend processors.
|
| 69 |
+
""")
|
| 70 |
+
|
| 71 |
+
st.markdown("---")
|
| 72 |
+
|
| 73 |
+
st.caption("Huawei AI Innovation Challenge 2026")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def render_header():
|
| 77 |
+
"""Render the application header."""
|
| 78 |
+
|
| 79 |
+
st.markdown("""
|
| 80 |
+
<div style="
|
| 81 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%);
|
| 82 |
+
padding: 2rem;
|
| 83 |
+
border-radius: 10px;
|
| 84 |
+
color: white;
|
| 85 |
+
margin-bottom: 2rem;
|
| 86 |
+
text-align: center;
|
| 87 |
+
">
|
| 88 |
+
<h1 style="margin: 0; font-size: 2.5rem;">π Traffic Accident Reconstruction System</h1>
|
| 89 |
+
<p style="margin: 0.5rem 0 0 0; opacity: 0.9;">AI-Powered Analysis using Huawei MindSpore</p>
|
| 90 |
+
</div>
|
| 91 |
+
""", unsafe_allow_html=True)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def render_footer():
|
| 95 |
+
"""Render the application footer."""
|
| 96 |
+
|
| 97 |
+
st.markdown("---")
|
| 98 |
+
|
| 99 |
+
col1, col2, col3 = st.columns([2, 1, 2])
|
| 100 |
+
|
| 101 |
+
with col1:
|
| 102 |
+
st.markdown("π **Huawei AI Innovation Challenge 2026**")
|
| 103 |
+
|
| 104 |
+
with col2:
|
| 105 |
+
# MindSpore logo in the center of footer
|
| 106 |
+
try:
|
| 107 |
+
st.image("assets/mindspore_logo.png", width=80)
|
| 108 |
+
except:
|
| 109 |
+
st.markdown("**[M]**")
|
| 110 |
+
|
| 111 |
+
with col3:
|
| 112 |
+
st.markdown("π€ **Powered by MindSpore AI Framework**")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def render_info_card(title: str, content: str, color: str = "#2d5a87"):
|
| 116 |
+
"""Render an information card."""
|
| 117 |
+
|
| 118 |
+
st.markdown(f"""
|
| 119 |
+
<div style="
|
| 120 |
+
background: #f8f9fa;
|
| 121 |
+
padding: 1.5rem;
|
| 122 |
+
border-radius: 10px;
|
| 123 |
+
border-left: 4px solid {color};
|
| 124 |
+
margin: 1rem 0;
|
| 125 |
+
">
|
| 126 |
+
<h4 style="margin: 0 0 0.5rem 0;">{title}</h4>
|
| 127 |
+
<p style="margin: 0;">{content}</p>
|
| 128 |
+
</div>
|
| 129 |
+
""", unsafe_allow_html=True)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def render_metric_card(label: str, value: str, delta: str = None, color: str = "#2d5a87"):
|
| 133 |
+
"""Render a metric card."""
|
| 134 |
+
|
| 135 |
+
delta_html = ""
|
| 136 |
+
if delta:
|
| 137 |
+
delta_color = "#28a745" if "+" in delta or "High" in delta else "#dc3545"
|
| 138 |
+
delta_html = f'<span style="color: {delta_color}; font-size: 0.9rem;">{delta}</span>'
|
| 139 |
+
|
| 140 |
+
st.markdown(f"""
|
| 141 |
+
<div style="
|
| 142 |
+
background: rgba(255, 255, 255, 0.08);
|
| 143 |
+
backdrop-filter: blur(10px);
|
| 144 |
+
padding: 1.5rem;
|
| 145 |
+
border-radius: 12px;
|
| 146 |
+
border-top: 3px solid {color};
|
| 147 |
+
text-align: center;
|
| 148 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
| 149 |
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
| 150 |
+
">
|
| 151 |
+
<p style="margin: 0; color: rgba(255, 255, 255, 0.7); font-size: 0.9rem; font-weight: 500;">{label}</p>
|
| 152 |
+
<h2 style="margin: 0.5rem 0; color: {color}; font-weight: 700;">{value}</h2>
|
| 153 |
+
{delta_html}
|
| 154 |
+
</div>
|
| 155 |
+
""", unsafe_allow_html=True)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def render_progress_bar(value: float, label: str = "", color: str = "#2d5a87"):
|
| 159 |
+
"""Render a custom progress bar."""
|
| 160 |
+
|
| 161 |
+
percentage = min(max(value * 100, 0), 100)
|
| 162 |
+
|
| 163 |
+
st.markdown(f"""
|
| 164 |
+
<div style="margin: 0.5rem 0;">
|
| 165 |
+
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
|
| 166 |
+
<span>{label}</span>
|
| 167 |
+
<span>{percentage:.1f}%</span>
|
| 168 |
+
</div>
|
| 169 |
+
<div style="
|
| 170 |
+
background: #e9ecef;
|
| 171 |
+
border-radius: 5px;
|
| 172 |
+
height: 20px;
|
| 173 |
+
overflow: hidden;
|
| 174 |
+
">
|
| 175 |
+
<div style="
|
| 176 |
+
background: {color};
|
| 177 |
+
width: {percentage}%;
|
| 178 |
+
height: 100%;
|
| 179 |
+
border-radius: 5px;
|
| 180 |
+
transition: width 0.3s ease;
|
| 181 |
+
"></div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
""", unsafe_allow_html=True)
|
head_on_collision.gif
ADDED
|
Git LFS Details
|
helpers.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility Functions
|
| 3 |
+
=================
|
| 4 |
+
Helper functions for the Traffic Accident Reconstruction System.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
from typing import List, Tuple, Dict, Any
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import math
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def calculate_distance(point1: List[float], point2: List[float]) -> float:
|
| 14 |
+
"""
|
| 15 |
+
Calculate the Haversine distance between two geographic points.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
point1: [latitude, longitude]
|
| 19 |
+
point2: [latitude, longitude]
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
Distance in meters
|
| 23 |
+
"""
|
| 24 |
+
R = 6371000 # Earth's radius in meters
|
| 25 |
+
|
| 26 |
+
lat1, lon1 = math.radians(point1[0]), math.radians(point1[1])
|
| 27 |
+
lat2, lon2 = math.radians(point2[0]), math.radians(point2[1])
|
| 28 |
+
|
| 29 |
+
dlat = lat2 - lat1
|
| 30 |
+
dlon = lon2 - lon1
|
| 31 |
+
|
| 32 |
+
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
|
| 33 |
+
c = 2 * math.asin(math.sqrt(a))
|
| 34 |
+
|
| 35 |
+
return R * c
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def calculate_bearing(point1: List[float], point2: List[float]) -> float:
|
| 39 |
+
"""
|
| 40 |
+
Calculate the bearing between two points.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
point1: [latitude, longitude]
|
| 44 |
+
point2: [latitude, longitude]
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
Bearing in degrees (0-360)
|
| 48 |
+
"""
|
| 49 |
+
lat1, lon1 = math.radians(point1[0]), math.radians(point1[1])
|
| 50 |
+
lat2, lon2 = math.radians(point2[0]), math.radians(point2[1])
|
| 51 |
+
|
| 52 |
+
dlon = lon2 - lon1
|
| 53 |
+
|
| 54 |
+
x = math.sin(dlon) * math.cos(lat2)
|
| 55 |
+
y = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
|
| 56 |
+
|
| 57 |
+
bearing = math.atan2(x, y)
|
| 58 |
+
bearing = math.degrees(bearing)
|
| 59 |
+
bearing = (bearing + 360) % 360
|
| 60 |
+
|
| 61 |
+
return bearing
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def interpolate_path(path: List[List[float]], num_points: int = 100) -> List[List[float]]:
|
| 65 |
+
"""
|
| 66 |
+
Interpolate a path to have more points for smoother simulation.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
path: List of [lat, lng] coordinates
|
| 70 |
+
num_points: Number of points in the interpolated path
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
Interpolated path
|
| 74 |
+
"""
|
| 75 |
+
if len(path) < 2:
|
| 76 |
+
return path
|
| 77 |
+
|
| 78 |
+
path_array = np.array(path)
|
| 79 |
+
|
| 80 |
+
# Calculate cumulative distances
|
| 81 |
+
distances = [0]
|
| 82 |
+
for i in range(1, len(path)):
|
| 83 |
+
d = calculate_distance(path[i-1], path[i])
|
| 84 |
+
distances.append(distances[-1] + d)
|
| 85 |
+
|
| 86 |
+
total_distance = distances[-1]
|
| 87 |
+
if total_distance == 0:
|
| 88 |
+
return path
|
| 89 |
+
|
| 90 |
+
# Interpolate
|
| 91 |
+
target_distances = np.linspace(0, total_distance, num_points)
|
| 92 |
+
interpolated = []
|
| 93 |
+
|
| 94 |
+
for target in target_distances:
|
| 95 |
+
# Find the segment
|
| 96 |
+
for i in range(len(distances) - 1):
|
| 97 |
+
if distances[i] <= target <= distances[i+1]:
|
| 98 |
+
if distances[i+1] - distances[i] == 0:
|
| 99 |
+
t = 0
|
| 100 |
+
else:
|
| 101 |
+
t = (target - distances[i]) / (distances[i+1] - distances[i])
|
| 102 |
+
|
| 103 |
+
lat = path[i][0] + t * (path[i+1][0] - path[i][0])
|
| 104 |
+
lng = path[i][1] + t * (path[i+1][1] - path[i][1])
|
| 105 |
+
interpolated.append([lat, lng])
|
| 106 |
+
break
|
| 107 |
+
|
| 108 |
+
return interpolated
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def estimate_speed_at_point(
|
| 112 |
+
path: List[List[float]],
|
| 113 |
+
initial_speed: float,
|
| 114 |
+
point_index: int,
|
| 115 |
+
is_braking: bool = False,
|
| 116 |
+
is_accelerating: bool = False
|
| 117 |
+
) -> float:
|
| 118 |
+
"""
|
| 119 |
+
Estimate speed at a specific point along the path.
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
path: Vehicle path
|
| 123 |
+
initial_speed: Initial speed in km/h
|
| 124 |
+
point_index: Index of the point
|
| 125 |
+
is_braking: Whether vehicle is braking
|
| 126 |
+
is_accelerating: Whether vehicle is accelerating
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
Estimated speed at the point in km/h
|
| 130 |
+
"""
|
| 131 |
+
if not path or point_index >= len(path):
|
| 132 |
+
return initial_speed
|
| 133 |
+
|
| 134 |
+
progress = point_index / len(path)
|
| 135 |
+
|
| 136 |
+
if is_braking:
|
| 137 |
+
# Assume linear deceleration
|
| 138 |
+
return initial_speed * (1 - progress * 0.5)
|
| 139 |
+
elif is_accelerating:
|
| 140 |
+
# Assume slight acceleration
|
| 141 |
+
return initial_speed * (1 + progress * 0.2)
|
| 142 |
+
else:
|
| 143 |
+
return initial_speed
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def find_intersection_point(
|
| 147 |
+
path1: List[List[float]],
|
| 148 |
+
path2: List[List[float]]
|
| 149 |
+
) -> Tuple[List[float], int, int]:
|
| 150 |
+
"""
|
| 151 |
+
Find the intersection point between two paths.
|
| 152 |
+
|
| 153 |
+
Args:
|
| 154 |
+
path1: First vehicle path
|
| 155 |
+
path2: Second vehicle path
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
Tuple of (intersection point, index in path1, index in path2)
|
| 159 |
+
"""
|
| 160 |
+
if not path1 or not path2:
|
| 161 |
+
return None, -1, -1
|
| 162 |
+
|
| 163 |
+
min_distance = float('inf')
|
| 164 |
+
intersection = None
|
| 165 |
+
idx1, idx2 = -1, -1
|
| 166 |
+
|
| 167 |
+
for i, p1 in enumerate(path1):
|
| 168 |
+
for j, p2 in enumerate(path2):
|
| 169 |
+
dist = calculate_distance(p1, p2)
|
| 170 |
+
if dist < min_distance:
|
| 171 |
+
min_distance = dist
|
| 172 |
+
intersection = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2]
|
| 173 |
+
idx1, idx2 = i, j
|
| 174 |
+
|
| 175 |
+
# Only consider as intersection if distance is small enough
|
| 176 |
+
if min_distance < 50: # 50 meters threshold
|
| 177 |
+
return intersection, idx1, idx2
|
| 178 |
+
|
| 179 |
+
return None, -1, -1
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def calculate_impact_angle(
|
| 183 |
+
direction1: str,
|
| 184 |
+
direction2: str
|
| 185 |
+
) -> float:
|
| 186 |
+
"""
|
| 187 |
+
Calculate the angle of impact between two vehicles.
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
direction1: Direction of vehicle 1
|
| 191 |
+
direction2: Direction of vehicle 2
|
| 192 |
+
|
| 193 |
+
Returns:
|
| 194 |
+
Impact angle in degrees
|
| 195 |
+
"""
|
| 196 |
+
direction_angles = {
|
| 197 |
+
'north': 0, 'northeast': 45, 'east': 90, 'southeast': 135,
|
| 198 |
+
'south': 180, 'southwest': 225, 'west': 270, 'northwest': 315
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
angle1 = direction_angles.get(direction1, 0)
|
| 202 |
+
angle2 = direction_angles.get(direction2, 90)
|
| 203 |
+
|
| 204 |
+
diff = abs(angle1 - angle2)
|
| 205 |
+
if diff > 180:
|
| 206 |
+
diff = 360 - diff
|
| 207 |
+
|
| 208 |
+
return diff
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def estimate_stopping_distance(speed_kmh: float, road_condition: str = 'dry') -> float:
|
| 212 |
+
"""
|
| 213 |
+
Estimate the stopping distance for a vehicle.
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
speed_kmh: Speed in km/h
|
| 217 |
+
road_condition: 'dry', 'wet', 'sandy', 'oily'
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Stopping distance in meters
|
| 221 |
+
"""
|
| 222 |
+
speed_ms = speed_kmh / 3.6
|
| 223 |
+
|
| 224 |
+
# Friction coefficients
|
| 225 |
+
friction = {
|
| 226 |
+
'dry': 0.8,
|
| 227 |
+
'wet': 0.5,
|
| 228 |
+
'sandy': 0.4,
|
| 229 |
+
'oily': 0.25
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
mu = friction.get(road_condition, 0.8)
|
| 233 |
+
g = 9.81 # Gravity
|
| 234 |
+
|
| 235 |
+
# Stopping distance = vΒ² / (2 * ΞΌ * g)
|
| 236 |
+
stopping_distance = (speed_ms ** 2) / (2 * mu * g)
|
| 237 |
+
|
| 238 |
+
# Add reaction distance (assuming 1.5 second reaction time)
|
| 239 |
+
reaction_distance = speed_ms * 1.5
|
| 240 |
+
|
| 241 |
+
return stopping_distance + reaction_distance
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def format_duration(seconds: float) -> str:
|
| 245 |
+
"""Format duration in seconds to human-readable string."""
|
| 246 |
+
if seconds < 1:
|
| 247 |
+
return f"{seconds*1000:.0f} ms"
|
| 248 |
+
elif seconds < 60:
|
| 249 |
+
return f"{seconds:.1f} s"
|
| 250 |
+
else:
|
| 251 |
+
minutes = int(seconds // 60)
|
| 252 |
+
secs = seconds % 60
|
| 253 |
+
return f"{minutes} min {secs:.0f} s"
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def format_speed(speed_kmh: float) -> str:
|
| 257 |
+
"""Format speed to human-readable string."""
|
| 258 |
+
return f"{speed_kmh:.0f} km/h"
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def format_distance(meters: float) -> str:
|
| 262 |
+
"""Format distance to human-readable string."""
|
| 263 |
+
if meters < 1000:
|
| 264 |
+
return f"{meters:.1f} m"
|
| 265 |
+
else:
|
| 266 |
+
return f"{meters/1000:.2f} km"
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def validate_coordinates(lat: float, lng: float) -> bool:
|
| 270 |
+
"""Validate geographic coordinates."""
|
| 271 |
+
return -90 <= lat <= 90 and -180 <= lng <= 180
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def validate_speed(speed: float) -> bool:
|
| 275 |
+
"""Validate vehicle speed."""
|
| 276 |
+
return 0 <= speed <= 300 # Max 300 km/h
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def generate_unique_id() -> str:
|
| 280 |
+
"""Generate a unique identifier."""
|
| 281 |
+
import uuid
|
| 282 |
+
return str(uuid.uuid4())[:8]
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def get_timestamp() -> str:
|
| 286 |
+
"""Get current timestamp string."""
|
| 287 |
+
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
map_viewer.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Map Viewer Component
|
| 3 |
+
====================
|
| 4 |
+
Handles map display and vehicle path drawing using Folium.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import folium
|
| 9 |
+
from streamlit_folium import st_folium, folium_static
|
| 10 |
+
from folium.plugins import Draw
|
| 11 |
+
import json
|
| 12 |
+
from typing import List
|
| 13 |
+
|
| 14 |
+
from config import CASE_STUDY_LOCATION, MAP_CONFIG, COLORS
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def create_base_map(location: dict = None) -> folium.Map:
|
| 18 |
+
"""
|
| 19 |
+
Create a base Folium map centered on the accident location.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
location: Dictionary with latitude, longitude, and name
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
Folium Map object
|
| 26 |
+
"""
|
| 27 |
+
if location is None:
|
| 28 |
+
location = CASE_STUDY_LOCATION
|
| 29 |
+
|
| 30 |
+
# Create map
|
| 31 |
+
m = folium.Map(
|
| 32 |
+
location=[location['latitude'], location['longitude']],
|
| 33 |
+
zoom_start=MAP_CONFIG['default_zoom'],
|
| 34 |
+
tiles='OpenStreetMap'
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Add location marker
|
| 38 |
+
folium.Marker(
|
| 39 |
+
location=[location['latitude'], location['longitude']],
|
| 40 |
+
popup=location.get('name', 'Accident Location'),
|
| 41 |
+
icon=folium.Icon(color='red', icon='info-sign'),
|
| 42 |
+
tooltip="Accident Location"
|
| 43 |
+
).add_to(m)
|
| 44 |
+
|
| 45 |
+
# Add circle to show area of interest
|
| 46 |
+
folium.Circle(
|
| 47 |
+
location=[location['latitude'], location['longitude']],
|
| 48 |
+
radius=location.get('radius_meters', 200),
|
| 49 |
+
color='blue',
|
| 50 |
+
fill=True,
|
| 51 |
+
fill_opacity=0.1,
|
| 52 |
+
popup="Analysis Area"
|
| 53 |
+
).add_to(m)
|
| 54 |
+
|
| 55 |
+
return m
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def add_draw_control(m: folium.Map) -> folium.Map:
|
| 59 |
+
"""
|
| 60 |
+
Add drawing controls to the map for path definition.
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
m: Folium Map object
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
Folium Map with draw controls
|
| 67 |
+
"""
|
| 68 |
+
draw = Draw(
|
| 69 |
+
draw_options={
|
| 70 |
+
'polyline': {
|
| 71 |
+
'allowIntersection': True,
|
| 72 |
+
'shapeOptions': {
|
| 73 |
+
'color': '#FF4B4B',
|
| 74 |
+
'weight': 4
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
'polygon': False,
|
| 78 |
+
'circle': False,
|
| 79 |
+
'rectangle': False,
|
| 80 |
+
'circlemarker': False,
|
| 81 |
+
'marker': True
|
| 82 |
+
},
|
| 83 |
+
edit_options={'edit': True, 'remove': True}
|
| 84 |
+
)
|
| 85 |
+
draw.add_to(m)
|
| 86 |
+
|
| 87 |
+
return m
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def add_vehicle_path(m: folium.Map, path: list, vehicle_id: int) -> folium.Map:
|
| 91 |
+
"""
|
| 92 |
+
Add a vehicle path to the map.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
m: Folium Map object
|
| 96 |
+
path: List of [lat, lng] coordinates
|
| 97 |
+
vehicle_id: 1 or 2 to determine color
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
Folium Map with vehicle path
|
| 101 |
+
"""
|
| 102 |
+
if not path or len(path) < 2:
|
| 103 |
+
return m
|
| 104 |
+
|
| 105 |
+
color = COLORS['vehicle_1'] if vehicle_id == 1 else COLORS['vehicle_2']
|
| 106 |
+
|
| 107 |
+
# Add the path line
|
| 108 |
+
folium.PolyLine(
|
| 109 |
+
locations=path,
|
| 110 |
+
color=color,
|
| 111 |
+
weight=4,
|
| 112 |
+
opacity=0.8,
|
| 113 |
+
popup=f"Vehicle {vehicle_id} Path",
|
| 114 |
+
tooltip=f"Vehicle {vehicle_id}"
|
| 115 |
+
).add_to(m)
|
| 116 |
+
|
| 117 |
+
# Add start marker
|
| 118 |
+
folium.Marker(
|
| 119 |
+
location=path[0],
|
| 120 |
+
popup=f"Vehicle {vehicle_id} Start",
|
| 121 |
+
icon=folium.Icon(
|
| 122 |
+
color='green' if vehicle_id == 1 else 'blue',
|
| 123 |
+
icon='play'
|
| 124 |
+
)
|
| 125 |
+
).add_to(m)
|
| 126 |
+
|
| 127 |
+
# Add end marker
|
| 128 |
+
folium.Marker(
|
| 129 |
+
location=path[-1],
|
| 130 |
+
popup=f"Vehicle {vehicle_id} End",
|
| 131 |
+
icon=folium.Icon(
|
| 132 |
+
color='red' if vehicle_id == 1 else 'darkblue',
|
| 133 |
+
icon='stop'
|
| 134 |
+
)
|
| 135 |
+
).add_to(m)
|
| 136 |
+
|
| 137 |
+
# Add direction arrows
|
| 138 |
+
for i in range(len(path) - 1):
|
| 139 |
+
mid_lat = (path[i][0] + path[i+1][0]) / 2
|
| 140 |
+
mid_lng = (path[i][1] + path[i+1][1]) / 2
|
| 141 |
+
|
| 142 |
+
folium.RegularPolygonMarker(
|
| 143 |
+
location=[mid_lat, mid_lng],
|
| 144 |
+
number_of_sides=3,
|
| 145 |
+
radius=8,
|
| 146 |
+
color=color,
|
| 147 |
+
fill=True,
|
| 148 |
+
fill_color=color,
|
| 149 |
+
fill_opacity=0.7
|
| 150 |
+
).add_to(m)
|
| 151 |
+
|
| 152 |
+
return m
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def add_collision_point(m: folium.Map, collision_point: list) -> folium.Map:
|
| 156 |
+
"""
|
| 157 |
+
Add a collision point marker to the map.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
m: Folium Map object
|
| 161 |
+
collision_point: [lat, lng] of collision
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
Folium Map with collision marker
|
| 165 |
+
"""
|
| 166 |
+
if not collision_point:
|
| 167 |
+
return m
|
| 168 |
+
|
| 169 |
+
folium.Marker(
|
| 170 |
+
location=collision_point,
|
| 171 |
+
popup="Estimated Collision Point",
|
| 172 |
+
icon=folium.Icon(color='orange', icon='warning-sign'),
|
| 173 |
+
tooltip="π₯ Collision Point"
|
| 174 |
+
).add_to(m)
|
| 175 |
+
|
| 176 |
+
# Add impact radius
|
| 177 |
+
folium.Circle(
|
| 178 |
+
location=collision_point,
|
| 179 |
+
radius=10,
|
| 180 |
+
color=COLORS['collision_point'],
|
| 181 |
+
fill=True,
|
| 182 |
+
fill_opacity=0.5,
|
| 183 |
+
popup="Impact Zone"
|
| 184 |
+
).add_to(m)
|
| 185 |
+
|
| 186 |
+
return m
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def render_map_section(vehicle_id: int = None):
|
| 190 |
+
"""
|
| 191 |
+
Render the map section in Streamlit.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
vehicle_id: If provided, enables path drawing for that vehicle
|
| 195 |
+
"""
|
| 196 |
+
location = st.session_state.accident_info['location']
|
| 197 |
+
|
| 198 |
+
# Create base map
|
| 199 |
+
m = create_base_map(location)
|
| 200 |
+
|
| 201 |
+
# Add existing vehicle paths
|
| 202 |
+
if st.session_state.vehicle_1.get('path'):
|
| 203 |
+
m = add_vehicle_path(m, st.session_state.vehicle_1['path'], 1)
|
| 204 |
+
|
| 205 |
+
if st.session_state.vehicle_2.get('path'):
|
| 206 |
+
m = add_vehicle_path(m, st.session_state.vehicle_2['path'], 2)
|
| 207 |
+
|
| 208 |
+
# Add draw controls if editing a specific vehicle
|
| 209 |
+
if vehicle_id:
|
| 210 |
+
m = add_draw_control(m)
|
| 211 |
+
|
| 212 |
+
st.info(f"ποΈ Draw the path for Vehicle {vehicle_id} on the map. Click points to create a path, then click 'Finish' to complete.")
|
| 213 |
+
|
| 214 |
+
# Render map and get drawing data
|
| 215 |
+
map_data = st_folium(
|
| 216 |
+
m,
|
| 217 |
+
width=700,
|
| 218 |
+
height=500,
|
| 219 |
+
returned_objects=["last_active_drawing", "all_drawings"]
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
# Process drawn paths
|
| 223 |
+
if vehicle_id and map_data and map_data.get('last_active_drawing'):
|
| 224 |
+
drawing = map_data['last_active_drawing']
|
| 225 |
+
|
| 226 |
+
if drawing.get('geometry', {}).get('type') == 'LineString':
|
| 227 |
+
coords = drawing['geometry']['coordinates']
|
| 228 |
+
# Convert from [lng, lat] to [lat, lng]
|
| 229 |
+
path = [[c[1], c[0]] for c in coords]
|
| 230 |
+
|
| 231 |
+
if vehicle_id == 1:
|
| 232 |
+
st.session_state.vehicle_1['path'] = path
|
| 233 |
+
else:
|
| 234 |
+
st.session_state.vehicle_2['path'] = path
|
| 235 |
+
|
| 236 |
+
st.success(f"β
Path saved for Vehicle {vehicle_id}!")
|
| 237 |
+
|
| 238 |
+
# Show path info and preset options
|
| 239 |
+
if vehicle_id:
|
| 240 |
+
vehicle_key = f'vehicle_{vehicle_id}'
|
| 241 |
+
current_path = st.session_state[vehicle_key].get('path', [])
|
| 242 |
+
|
| 243 |
+
if current_path:
|
| 244 |
+
st.success(f"**β
Path defined:** {len(current_path)} points")
|
| 245 |
+
else:
|
| 246 |
+
st.warning("β οΈ No path drawn yet. Use the drawing tools on the map OR select a preset path below.")
|
| 247 |
+
|
| 248 |
+
# Preset path options for roundabout
|
| 249 |
+
st.markdown("---")
|
| 250 |
+
st.markdown("**π£οΈ Quick Path Selection (Roundabout)**")
|
| 251 |
+
|
| 252 |
+
preset_col1, preset_col2 = st.columns(2)
|
| 253 |
+
|
| 254 |
+
with preset_col1:
|
| 255 |
+
entry_direction = st.selectbox(
|
| 256 |
+
f"Entry Direction (V{vehicle_id})",
|
| 257 |
+
options=['north', 'south', 'east', 'west'],
|
| 258 |
+
key=f"entry_dir_{vehicle_id}"
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
with preset_col2:
|
| 262 |
+
exit_direction = st.selectbox(
|
| 263 |
+
f"Exit Direction (V{vehicle_id})",
|
| 264 |
+
options=['north', 'south', 'east', 'west'],
|
| 265 |
+
index=2 if entry_direction == 'north' else 0,
|
| 266 |
+
key=f"exit_dir_{vehicle_id}"
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
if st.button(f"π Generate Path for Vehicle {vehicle_id}", key=f"gen_path_{vehicle_id}"):
|
| 270 |
+
generated_path = generate_roundabout_path(
|
| 271 |
+
location['latitude'],
|
| 272 |
+
location['longitude'],
|
| 273 |
+
entry_direction,
|
| 274 |
+
exit_direction
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
if vehicle_id == 1:
|
| 278 |
+
st.session_state.vehicle_1['path'] = generated_path
|
| 279 |
+
else:
|
| 280 |
+
st.session_state.vehicle_2['path'] = generated_path
|
| 281 |
+
|
| 282 |
+
st.success(f"β
Path generated: {entry_direction.title()} β {exit_direction.title()}")
|
| 283 |
+
st.rerun()
|
| 284 |
+
|
| 285 |
+
# Manual path input as fallback
|
| 286 |
+
with st.expander("π Or enter path manually"):
|
| 287 |
+
st.write("Enter coordinates as: lat1,lng1;lat2,lng2;...")
|
| 288 |
+
manual_path = st.text_input(
|
| 289 |
+
"Path coordinates",
|
| 290 |
+
key=f"manual_path_{vehicle_id}",
|
| 291 |
+
placeholder="26.2397,50.5369;26.2400,50.5372"
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
if st.button(f"Set Manual Path", key=f"set_path_{vehicle_id}"):
|
| 295 |
+
if manual_path:
|
| 296 |
+
try:
|
| 297 |
+
path = []
|
| 298 |
+
for point in manual_path.split(';'):
|
| 299 |
+
lat, lng = point.strip().split(',')
|
| 300 |
+
path.append([float(lat), float(lng)])
|
| 301 |
+
|
| 302 |
+
if vehicle_id == 1:
|
| 303 |
+
st.session_state.vehicle_1['path'] = path
|
| 304 |
+
else:
|
| 305 |
+
st.session_state.vehicle_2['path'] = path
|
| 306 |
+
|
| 307 |
+
st.success(f"Path set with {len(path)} points!")
|
| 308 |
+
st.rerun()
|
| 309 |
+
except Exception as e:
|
| 310 |
+
st.error(f"Invalid format: {e}")
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def generate_roundabout_path(
|
| 314 |
+
center_lat: float,
|
| 315 |
+
center_lng: float,
|
| 316 |
+
entry_direction: str,
|
| 317 |
+
exit_direction: str
|
| 318 |
+
) -> List[List[float]]:
|
| 319 |
+
"""
|
| 320 |
+
Generate a realistic path through a roundabout.
|
| 321 |
+
|
| 322 |
+
Args:
|
| 323 |
+
center_lat: Center latitude of roundabout
|
| 324 |
+
center_lng: Center longitude of roundabout
|
| 325 |
+
entry_direction: Direction vehicle enters from
|
| 326 |
+
exit_direction: Direction vehicle exits to
|
| 327 |
+
|
| 328 |
+
Returns:
|
| 329 |
+
List of [lat, lng] coordinates forming the path
|
| 330 |
+
"""
|
| 331 |
+
import math
|
| 332 |
+
|
| 333 |
+
# Roundabout parameters
|
| 334 |
+
approach_distance = 0.0015 # ~150 meters
|
| 335 |
+
roundabout_radius = 0.0004 # ~40 meters
|
| 336 |
+
|
| 337 |
+
# Direction to angle mapping (0 = North, clockwise)
|
| 338 |
+
dir_to_angle = {
|
| 339 |
+
'north': 90, # Top
|
| 340 |
+
'east': 0, # Right
|
| 341 |
+
'south': 270, # Bottom
|
| 342 |
+
'west': 180 # Left
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
entry_angle = math.radians(dir_to_angle[entry_direction])
|
| 346 |
+
exit_angle = math.radians(dir_to_angle[exit_direction])
|
| 347 |
+
|
| 348 |
+
path = []
|
| 349 |
+
|
| 350 |
+
# Entry point (outside roundabout)
|
| 351 |
+
entry_lat = center_lat + approach_distance * math.sin(entry_angle)
|
| 352 |
+
entry_lng = center_lng + approach_distance * math.cos(entry_angle)
|
| 353 |
+
path.append([entry_lat, entry_lng])
|
| 354 |
+
|
| 355 |
+
# Entry to roundabout edge
|
| 356 |
+
edge_lat = center_lat + roundabout_radius * 1.5 * math.sin(entry_angle)
|
| 357 |
+
edge_lng = center_lng + roundabout_radius * 1.5 * math.cos(entry_angle)
|
| 358 |
+
path.append([edge_lat, edge_lng])
|
| 359 |
+
|
| 360 |
+
# Points along the roundabout (clockwise)
|
| 361 |
+
entry_deg = dir_to_angle[entry_direction]
|
| 362 |
+
exit_deg = dir_to_angle[exit_direction]
|
| 363 |
+
|
| 364 |
+
# Calculate arc (always go clockwise in roundabout)
|
| 365 |
+
if exit_deg <= entry_deg:
|
| 366 |
+
exit_deg += 360
|
| 367 |
+
|
| 368 |
+
# Add intermediate points along the arc
|
| 369 |
+
num_arc_points = max(2, (exit_deg - entry_deg) // 45)
|
| 370 |
+
for i in range(1, num_arc_points + 1):
|
| 371 |
+
angle = entry_deg + (exit_deg - entry_deg) * i / (num_arc_points + 1)
|
| 372 |
+
angle_rad = math.radians(angle)
|
| 373 |
+
arc_lat = center_lat + roundabout_radius * math.sin(angle_rad)
|
| 374 |
+
arc_lng = center_lng + roundabout_radius * math.cos(angle_rad)
|
| 375 |
+
path.append([arc_lat, arc_lng])
|
| 376 |
+
|
| 377 |
+
# Exit from roundabout edge
|
| 378 |
+
exit_edge_lat = center_lat + roundabout_radius * 1.5 * math.sin(exit_angle)
|
| 379 |
+
exit_edge_lng = center_lng + roundabout_radius * 1.5 * math.cos(exit_angle)
|
| 380 |
+
path.append([exit_edge_lat, exit_edge_lng])
|
| 381 |
+
|
| 382 |
+
# Exit point (outside roundabout)
|
| 383 |
+
exit_lat = center_lat + approach_distance * math.sin(exit_angle)
|
| 384 |
+
exit_lng = center_lng + approach_distance * math.cos(exit_angle)
|
| 385 |
+
path.append([exit_lat, exit_lng])
|
| 386 |
+
|
| 387 |
+
return path
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
def render_results_map(scenarios: list, selected_scenario: int = 0):
|
| 391 |
+
"""
|
| 392 |
+
Render a map showing analysis results for a specific scenario.
|
| 393 |
+
|
| 394 |
+
Args:
|
| 395 |
+
scenarios: List of generated scenarios
|
| 396 |
+
selected_scenario: Index of selected scenario
|
| 397 |
+
"""
|
| 398 |
+
if not scenarios:
|
| 399 |
+
st.warning("No scenarios to display")
|
| 400 |
+
return
|
| 401 |
+
|
| 402 |
+
scenario = scenarios[selected_scenario]
|
| 403 |
+
location = st.session_state.accident_info['location']
|
| 404 |
+
|
| 405 |
+
# Create map
|
| 406 |
+
m = create_base_map(location)
|
| 407 |
+
|
| 408 |
+
# Add vehicle paths
|
| 409 |
+
if scenario.get('vehicle_1_path'):
|
| 410 |
+
m = add_vehicle_path(m, scenario['vehicle_1_path'], 1)
|
| 411 |
+
|
| 412 |
+
if scenario.get('vehicle_2_path'):
|
| 413 |
+
m = add_vehicle_path(m, scenario['vehicle_2_path'], 2)
|
| 414 |
+
|
| 415 |
+
# Add collision point
|
| 416 |
+
if scenario.get('collision_point'):
|
| 417 |
+
m = add_collision_point(m, scenario['collision_point'])
|
| 418 |
+
|
| 419 |
+
# Add scenario info popup
|
| 420 |
+
info_html = f"""
|
| 421 |
+
<div style="width: 200px;">
|
| 422 |
+
<h4>Scenario {selected_scenario + 1}</h4>
|
| 423 |
+
<p><b>Probability:</b> {scenario.get('probability', 0)*100:.1f}%</p>
|
| 424 |
+
<p><b>Type:</b> {scenario.get('accident_type', 'Unknown')}</p>
|
| 425 |
+
</div>
|
| 426 |
+
"""
|
| 427 |
+
|
| 428 |
+
folium.Marker(
|
| 429 |
+
location=[location['latitude'], location['longitude']],
|
| 430 |
+
popup=folium.Popup(info_html, max_width=250),
|
| 431 |
+
icon=folium.Icon(color='purple', icon='info-sign')
|
| 432 |
+
).add_to(m)
|
| 433 |
+
|
| 434 |
+
# Display map
|
| 435 |
+
folium_static(m, width=700, height=500)
|
mindspore_logo.png
ADDED
|
model_metadata.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "MindSpore",
|
| 3 |
+
"version": "2.7.0",
|
| 4 |
+
"input_dim": 31,
|
| 5 |
+
"num_classes": 7,
|
| 6 |
+
"architecture": "31\u2192128\u219264\u219232\u21927",
|
| 7 |
+
"test_accuracy": 0.9238333333333333,
|
| 8 |
+
"best_train_accuracy": 0.9269166666666667,
|
| 9 |
+
"epochs_trained": 100,
|
| 10 |
+
"batch_size": 64,
|
| 11 |
+
"learning_rate": 0.001,
|
| 12 |
+
"accident_classes": {
|
| 13 |
+
"rear_end_collision": 0,
|
| 14 |
+
"side_impact": 1,
|
| 15 |
+
"head_on_collision": 2,
|
| 16 |
+
"sideswipe": 3,
|
| 17 |
+
"roundabout_entry_collision": 4,
|
| 18 |
+
"lane_change_collision": 5,
|
| 19 |
+
"intersection_collision": 6
|
| 20 |
+
},
|
| 21 |
+
"feature_count": 31
|
| 22 |
+
}
|
party_input.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Party/Driver Input Component
|
| 3 |
+
============================
|
| 4 |
+
Handles detailed party (driver) information input forms.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from typing import Dict, Any
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def init_party_data() -> Dict[str, Any]:
|
| 12 |
+
"""Initialize empty party data structure."""
|
| 13 |
+
return {
|
| 14 |
+
'full_name': '',
|
| 15 |
+
'id_iqama': '',
|
| 16 |
+
'phone': '',
|
| 17 |
+
'role': 'Driver',
|
| 18 |
+
'vehicle_make_model': '',
|
| 19 |
+
'plate_number': '',
|
| 20 |
+
'insurance': '',
|
| 21 |
+
'damage_notes': '',
|
| 22 |
+
'statement': ''
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def render_party_input(party_num: int) -> Dict[str, Any]:
|
| 27 |
+
"""
|
| 28 |
+
Render the party/driver input form.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
party_num: 1 or 2 to determine which party
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
Dictionary containing party data
|
| 35 |
+
"""
|
| 36 |
+
party_key = f'party_{party_num}'
|
| 37 |
+
|
| 38 |
+
# Initialize party data in session state if not exists
|
| 39 |
+
if party_key not in st.session_state:
|
| 40 |
+
st.session_state[party_key] = init_party_data()
|
| 41 |
+
|
| 42 |
+
color = "#FF4B4B" if party_num == 1 else "#4B7BFF"
|
| 43 |
+
|
| 44 |
+
st.markdown(f"""
|
| 45 |
+
<div style="
|
| 46 |
+
background: linear-gradient(135deg, {'#1a1a2e' if party_num == 1 else '#1a1a2e'} 0%, #16213e 100%);
|
| 47 |
+
border: 1px solid {color};
|
| 48 |
+
border-radius: 10px;
|
| 49 |
+
padding: 1.5rem;
|
| 50 |
+
margin-bottom: 1rem;
|
| 51 |
+
">
|
| 52 |
+
<h3 style="color: {color}; margin: 0 0 1rem 0;">
|
| 53 |
+
{'π€' if party_num == 1 else 'π€'} Party {party_num} Information
|
| 54 |
+
</h3>
|
| 55 |
+
</div>
|
| 56 |
+
""", unsafe_allow_html=True)
|
| 57 |
+
|
| 58 |
+
# Full Name
|
| 59 |
+
full_name = st.text_input(
|
| 60 |
+
f"Full Name (Party {party_num})",
|
| 61 |
+
value=st.session_state[party_key].get('full_name', ''),
|
| 62 |
+
placeholder="Enter full name",
|
| 63 |
+
key=f"name_{party_num}"
|
| 64 |
+
)
|
| 65 |
+
st.session_state[party_key]['full_name'] = full_name
|
| 66 |
+
|
| 67 |
+
# ID / Iqama
|
| 68 |
+
col1, col2 = st.columns(2)
|
| 69 |
+
|
| 70 |
+
with col1:
|
| 71 |
+
id_iqama = st.text_input(
|
| 72 |
+
f"ID / Iqama (Party {party_num})",
|
| 73 |
+
value=st.session_state[party_key].get('id_iqama', ''),
|
| 74 |
+
placeholder="ID or Iqama number",
|
| 75 |
+
key=f"id_{party_num}"
|
| 76 |
+
)
|
| 77 |
+
st.session_state[party_key]['id_iqama'] = id_iqama
|
| 78 |
+
|
| 79 |
+
with col2:
|
| 80 |
+
phone = st.text_input(
|
| 81 |
+
f"Phone (Party {party_num})",
|
| 82 |
+
value=st.session_state[party_key].get('phone', ''),
|
| 83 |
+
placeholder="+973 XXXX XXXX",
|
| 84 |
+
key=f"phone_{party_num}"
|
| 85 |
+
)
|
| 86 |
+
st.session_state[party_key]['phone'] = phone
|
| 87 |
+
|
| 88 |
+
# Role
|
| 89 |
+
role = st.selectbox(
|
| 90 |
+
f"Role (Party {party_num})",
|
| 91 |
+
options=['Driver', 'Passenger', 'Pedestrian', 'Cyclist', 'Witness'],
|
| 92 |
+
index=['Driver', 'Passenger', 'Pedestrian', 'Cyclist', 'Witness'].index(
|
| 93 |
+
st.session_state[party_key].get('role', 'Driver')
|
| 94 |
+
),
|
| 95 |
+
key=f"role_{party_num}"
|
| 96 |
+
)
|
| 97 |
+
st.session_state[party_key]['role'] = role
|
| 98 |
+
|
| 99 |
+
st.markdown("---")
|
| 100 |
+
st.markdown(f"**π Vehicle Information (Party {party_num})**")
|
| 101 |
+
|
| 102 |
+
# Vehicle Make/Model
|
| 103 |
+
vehicle = st.text_input(
|
| 104 |
+
f"Vehicle (Party {party_num})",
|
| 105 |
+
value=st.session_state[party_key].get('vehicle_make_model', ''),
|
| 106 |
+
placeholder="e.g., Toyota Camry 2022",
|
| 107 |
+
key=f"vehicle_{party_num}"
|
| 108 |
+
)
|
| 109 |
+
st.session_state[party_key]['vehicle_make_model'] = vehicle
|
| 110 |
+
|
| 111 |
+
col1, col2 = st.columns(2)
|
| 112 |
+
|
| 113 |
+
with col1:
|
| 114 |
+
# Plate Number
|
| 115 |
+
plate = st.text_input(
|
| 116 |
+
f"Plate Number (Party {party_num})",
|
| 117 |
+
value=st.session_state[party_key].get('plate_number', ''),
|
| 118 |
+
placeholder="e.g., 12345",
|
| 119 |
+
key=f"plate_{party_num}"
|
| 120 |
+
)
|
| 121 |
+
st.session_state[party_key]['plate_number'] = plate
|
| 122 |
+
|
| 123 |
+
with col2:
|
| 124 |
+
# Insurance
|
| 125 |
+
insurance = st.text_input(
|
| 126 |
+
f"Insurance (Party {party_num})",
|
| 127 |
+
value=st.session_state[party_key].get('insurance', ''),
|
| 128 |
+
placeholder="Company / Policy #",
|
| 129 |
+
key=f"insurance_{party_num}"
|
| 130 |
+
)
|
| 131 |
+
st.session_state[party_key]['insurance'] = insurance
|
| 132 |
+
|
| 133 |
+
st.markdown("---")
|
| 134 |
+
st.markdown(f"**π Damage & Statement (Party {party_num})**")
|
| 135 |
+
|
| 136 |
+
# Damage Notes
|
| 137 |
+
damage = st.text_area(
|
| 138 |
+
f"Damage Notes (Party {party_num})",
|
| 139 |
+
value=st.session_state[party_key].get('damage_notes', ''),
|
| 140 |
+
placeholder="e.g., front bumper, right door, headlight...",
|
| 141 |
+
height=80,
|
| 142 |
+
key=f"damage_{party_num}"
|
| 143 |
+
)
|
| 144 |
+
st.session_state[party_key]['damage_notes'] = damage
|
| 145 |
+
|
| 146 |
+
# Statement
|
| 147 |
+
statement = st.text_area(
|
| 148 |
+
f"Statement (Party {party_num})",
|
| 149 |
+
value=st.session_state[party_key].get('statement', ''),
|
| 150 |
+
placeholder=f"What party {party_num} says happened...",
|
| 151 |
+
height=100,
|
| 152 |
+
key=f"statement_{party_num}"
|
| 153 |
+
)
|
| 154 |
+
st.session_state[party_key]['statement'] = statement
|
| 155 |
+
|
| 156 |
+
return st.session_state[party_key]
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def render_party_summary(party_num: int):
|
| 160 |
+
"""Render a compact summary of party data."""
|
| 161 |
+
party_key = f'party_{party_num}'
|
| 162 |
+
|
| 163 |
+
if party_key not in st.session_state:
|
| 164 |
+
st.warning(f"No data for Party {party_num}")
|
| 165 |
+
return
|
| 166 |
+
|
| 167 |
+
party = st.session_state[party_key]
|
| 168 |
+
color = "#FF4B4B" if party_num == 1 else "#4B7BFF"
|
| 169 |
+
|
| 170 |
+
name = party.get('full_name', 'Not provided') or 'Not provided'
|
| 171 |
+
vehicle = party.get('vehicle_make_model', 'Not specified') or 'Not specified'
|
| 172 |
+
plate = party.get('plate_number', 'N/A') or 'N/A'
|
| 173 |
+
|
| 174 |
+
st.markdown(f"""
|
| 175 |
+
<div style="
|
| 176 |
+
background: #1a1a2e;
|
| 177 |
+
border-left: 4px solid {color};
|
| 178 |
+
border-radius: 5px;
|
| 179 |
+
padding: 1rem;
|
| 180 |
+
margin: 0.5rem 0;
|
| 181 |
+
">
|
| 182 |
+
<h4 style="color: {color}; margin: 0 0 0.5rem 0;">Party {party_num}</h4>
|
| 183 |
+
<p style="margin: 0.2rem 0; color: #ccc;"><b>Name:</b> {name}</p>
|
| 184 |
+
<p style="margin: 0.2rem 0; color: #ccc;"><b>Vehicle:</b> {vehicle}</p>
|
| 185 |
+
<p style="margin: 0.2rem 0; color: #ccc;"><b>Plate:</b> {plate}</p>
|
| 186 |
+
</div>
|
| 187 |
+
""", unsafe_allow_html=True)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def render_evidence_upload():
|
| 191 |
+
"""Render the evidence photo upload section."""
|
| 192 |
+
|
| 193 |
+
st.markdown("""
|
| 194 |
+
<div style="
|
| 195 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 196 |
+
border: 1px solid #ffc107;
|
| 197 |
+
border-radius: 10px;
|
| 198 |
+
padding: 1.5rem;
|
| 199 |
+
margin: 1rem 0;
|
| 200 |
+
">
|
| 201 |
+
<h3 style="color: #ffc107; margin: 0 0 0.5rem 0;">
|
| 202 |
+
π· Evidence (Optional)
|
| 203 |
+
</h3>
|
| 204 |
+
<p style="color: #aaa; margin: 0;">Upload photos of the accident scene, vehicle damage, etc.</p>
|
| 205 |
+
</div>
|
| 206 |
+
""", unsafe_allow_html=True)
|
| 207 |
+
|
| 208 |
+
# File uploader
|
| 209 |
+
uploaded_files = st.file_uploader(
|
| 210 |
+
"Upload photos (optional)",
|
| 211 |
+
type=['png', 'jpg', 'jpeg'],
|
| 212 |
+
accept_multiple_files=True,
|
| 213 |
+
key="evidence_photos",
|
| 214 |
+
help="Upload accident scene photos, vehicle damage photos, etc."
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
if uploaded_files:
|
| 218 |
+
st.success(f"β
{len(uploaded_files)} photo(s) uploaded")
|
| 219 |
+
|
| 220 |
+
# Display thumbnails
|
| 221 |
+
cols = st.columns(min(len(uploaded_files), 4))
|
| 222 |
+
for i, file in enumerate(uploaded_files[:4]):
|
| 223 |
+
with cols[i % 4]:
|
| 224 |
+
st.image(file, caption=file.name, use_container_width=True)
|
| 225 |
+
|
| 226 |
+
# Store in session state
|
| 227 |
+
st.session_state['evidence_photos'] = uploaded_files
|
| 228 |
+
else:
|
| 229 |
+
st.info("π‘ Tip: Photos can help with accident reconstruction analysis")
|
| 230 |
+
|
| 231 |
+
return uploaded_files
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def render_compact_party_inputs():
|
| 235 |
+
"""Render both party inputs in a compact two-column layout."""
|
| 236 |
+
|
| 237 |
+
col1, col2 = st.columns(2)
|
| 238 |
+
|
| 239 |
+
with col1:
|
| 240 |
+
render_party_input(1)
|
| 241 |
+
|
| 242 |
+
with col2:
|
| 243 |
+
render_party_input(2)
|
rear_end_collision.gif
ADDED
|
Git LFS Details
|
report_generator.py
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Report Generator
|
| 3 |
+
================
|
| 4 |
+
Generates PDF and HTML reports for accident analysis.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import Dict, List, Any
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from config import REPORTS_DIR, REPORT_CONFIG, VEHICLE_TYPES
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def generate_report(
|
| 16 |
+
results: Dict[str, Any],
|
| 17 |
+
scenarios: List[Dict[str, Any]],
|
| 18 |
+
accident_info: Dict[str, Any],
|
| 19 |
+
vehicle_1: Dict[str, Any],
|
| 20 |
+
vehicle_2: Dict[str, Any],
|
| 21 |
+
format: str = 'pdf',
|
| 22 |
+
include_maps: bool = True,
|
| 23 |
+
include_charts: bool = True,
|
| 24 |
+
include_raw_data: bool = False
|
| 25 |
+
) -> str:
|
| 26 |
+
"""
|
| 27 |
+
Generate a comprehensive accident analysis report.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
results: Analysis results dictionary
|
| 31 |
+
scenarios: List of generated scenarios
|
| 32 |
+
accident_info: Accident context information
|
| 33 |
+
vehicle_1: First vehicle data
|
| 34 |
+
vehicle_2: Second vehicle data
|
| 35 |
+
format: Output format ('pdf' or 'html')
|
| 36 |
+
include_maps: Whether to include map visualizations
|
| 37 |
+
include_charts: Whether to include charts
|
| 38 |
+
include_raw_data: Whether to include raw data tables
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
Path to generated report file
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 45 |
+
|
| 46 |
+
if format.lower() == 'html':
|
| 47 |
+
return generate_html_report(
|
| 48 |
+
results, scenarios, accident_info, vehicle_1, vehicle_2,
|
| 49 |
+
timestamp, include_maps, include_charts, include_raw_data
|
| 50 |
+
)
|
| 51 |
+
else:
|
| 52 |
+
return generate_pdf_report(
|
| 53 |
+
results, scenarios, accident_info, vehicle_1, vehicle_2,
|
| 54 |
+
timestamp, include_maps, include_charts, include_raw_data
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def generate_html_report(
|
| 59 |
+
results: Dict[str, Any],
|
| 60 |
+
scenarios: List[Dict[str, Any]],
|
| 61 |
+
accident_info: Dict[str, Any],
|
| 62 |
+
vehicle_1: Dict[str, Any],
|
| 63 |
+
vehicle_2: Dict[str, Any],
|
| 64 |
+
timestamp: str,
|
| 65 |
+
include_maps: bool,
|
| 66 |
+
include_charts: bool,
|
| 67 |
+
include_raw_data: bool
|
| 68 |
+
) -> str:
|
| 69 |
+
"""Generate HTML report."""
|
| 70 |
+
|
| 71 |
+
# Get most likely scenario
|
| 72 |
+
most_likely = results.get('most_likely_scenario', {})
|
| 73 |
+
fault = results.get('preliminary_fault_assessment', {})
|
| 74 |
+
|
| 75 |
+
v1_type = VEHICLE_TYPES.get(vehicle_1.get('type', 'sedan'), {}).get('name', 'Sedan')
|
| 76 |
+
v2_type = VEHICLE_TYPES.get(vehicle_2.get('type', 'sedan'), {}).get('name', 'Sedan')
|
| 77 |
+
|
| 78 |
+
html_content = f"""
|
| 79 |
+
<!DOCTYPE html>
|
| 80 |
+
<html lang="en">
|
| 81 |
+
<head>
|
| 82 |
+
<meta charset="UTF-8">
|
| 83 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 84 |
+
<title>Traffic Accident Analysis Report</title>
|
| 85 |
+
<style>
|
| 86 |
+
* {{
|
| 87 |
+
margin: 0;
|
| 88 |
+
padding: 0;
|
| 89 |
+
box-sizing: border-box;
|
| 90 |
+
}}
|
| 91 |
+
|
| 92 |
+
body {{
|
| 93 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 94 |
+
line-height: 1.6;
|
| 95 |
+
color: #333;
|
| 96 |
+
background: #f5f5f5;
|
| 97 |
+
}}
|
| 98 |
+
|
| 99 |
+
.container {{
|
| 100 |
+
max-width: 1000px;
|
| 101 |
+
margin: 0 auto;
|
| 102 |
+
background: white;
|
| 103 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 104 |
+
}}
|
| 105 |
+
|
| 106 |
+
.header {{
|
| 107 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%);
|
| 108 |
+
color: white;
|
| 109 |
+
padding: 40px;
|
| 110 |
+
text-align: center;
|
| 111 |
+
}}
|
| 112 |
+
|
| 113 |
+
.header h1 {{
|
| 114 |
+
font-size: 2.5rem;
|
| 115 |
+
margin-bottom: 10px;
|
| 116 |
+
}}
|
| 117 |
+
|
| 118 |
+
.header .subtitle {{
|
| 119 |
+
opacity: 0.9;
|
| 120 |
+
font-size: 1.1rem;
|
| 121 |
+
}}
|
| 122 |
+
|
| 123 |
+
.section {{
|
| 124 |
+
padding: 30px 40px;
|
| 125 |
+
border-bottom: 1px solid #eee;
|
| 126 |
+
}}
|
| 127 |
+
|
| 128 |
+
.section h2 {{
|
| 129 |
+
color: #1e3a5f;
|
| 130 |
+
margin-bottom: 20px;
|
| 131 |
+
padding-bottom: 10px;
|
| 132 |
+
border-bottom: 2px solid #2d5a87;
|
| 133 |
+
}}
|
| 134 |
+
|
| 135 |
+
.summary-grid {{
|
| 136 |
+
display: grid;
|
| 137 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 138 |
+
gap: 20px;
|
| 139 |
+
margin: 20px 0;
|
| 140 |
+
}}
|
| 141 |
+
|
| 142 |
+
.summary-card {{
|
| 143 |
+
background: #f8f9fa;
|
| 144 |
+
padding: 20px;
|
| 145 |
+
border-radius: 10px;
|
| 146 |
+
text-align: center;
|
| 147 |
+
border-top: 4px solid #2d5a87;
|
| 148 |
+
}}
|
| 149 |
+
|
| 150 |
+
.summary-card .label {{
|
| 151 |
+
font-size: 0.9rem;
|
| 152 |
+
color: #666;
|
| 153 |
+
margin-bottom: 5px;
|
| 154 |
+
}}
|
| 155 |
+
|
| 156 |
+
.summary-card .value {{
|
| 157 |
+
font-size: 1.8rem;
|
| 158 |
+
font-weight: bold;
|
| 159 |
+
color: #1e3a5f;
|
| 160 |
+
}}
|
| 161 |
+
|
| 162 |
+
.summary-card .delta {{
|
| 163 |
+
font-size: 0.85rem;
|
| 164 |
+
margin-top: 5px;
|
| 165 |
+
}}
|
| 166 |
+
|
| 167 |
+
.delta.high {{ color: #28a745; }}
|
| 168 |
+
.delta.medium {{ color: #ffc107; }}
|
| 169 |
+
.delta.low {{ color: #dc3545; }}
|
| 170 |
+
|
| 171 |
+
.info-grid {{
|
| 172 |
+
display: grid;
|
| 173 |
+
grid-template-columns: repeat(2, 1fr);
|
| 174 |
+
gap: 30px;
|
| 175 |
+
}}
|
| 176 |
+
|
| 177 |
+
.info-box {{
|
| 178 |
+
background: #f8f9fa;
|
| 179 |
+
padding: 20px;
|
| 180 |
+
border-radius: 10px;
|
| 181 |
+
}}
|
| 182 |
+
|
| 183 |
+
.info-box h3 {{
|
| 184 |
+
color: #1e3a5f;
|
| 185 |
+
margin-bottom: 15px;
|
| 186 |
+
}}
|
| 187 |
+
|
| 188 |
+
.info-row {{
|
| 189 |
+
display: flex;
|
| 190 |
+
justify-content: space-between;
|
| 191 |
+
padding: 8px 0;
|
| 192 |
+
border-bottom: 1px solid #eee;
|
| 193 |
+
}}
|
| 194 |
+
|
| 195 |
+
.info-row:last-child {{
|
| 196 |
+
border-bottom: none;
|
| 197 |
+
}}
|
| 198 |
+
|
| 199 |
+
.vehicle-card {{
|
| 200 |
+
background: white;
|
| 201 |
+
border-radius: 10px;
|
| 202 |
+
padding: 20px;
|
| 203 |
+
margin: 15px 0;
|
| 204 |
+
}}
|
| 205 |
+
|
| 206 |
+
.vehicle-card.v1 {{
|
| 207 |
+
border-left: 4px solid #FF4B4B;
|
| 208 |
+
}}
|
| 209 |
+
|
| 210 |
+
.vehicle-card.v2 {{
|
| 211 |
+
border-left: 4px solid #4B7BFF;
|
| 212 |
+
}}
|
| 213 |
+
|
| 214 |
+
.scenario-card {{
|
| 215 |
+
background: #f8f9fa;
|
| 216 |
+
border-radius: 10px;
|
| 217 |
+
padding: 20px;
|
| 218 |
+
margin: 15px 0;
|
| 219 |
+
border-left: 4px solid #2d5a87;
|
| 220 |
+
}}
|
| 221 |
+
|
| 222 |
+
.scenario-header {{
|
| 223 |
+
display: flex;
|
| 224 |
+
justify-content: space-between;
|
| 225 |
+
align-items: center;
|
| 226 |
+
margin-bottom: 15px;
|
| 227 |
+
}}
|
| 228 |
+
|
| 229 |
+
.probability {{
|
| 230 |
+
font-size: 1.5rem;
|
| 231 |
+
font-weight: bold;
|
| 232 |
+
}}
|
| 233 |
+
|
| 234 |
+
.probability.high {{ color: #28a745; }}
|
| 235 |
+
.probability.medium {{ color: #ffc107; }}
|
| 236 |
+
.probability.low {{ color: #dc3545; }}
|
| 237 |
+
|
| 238 |
+
.progress-bar {{
|
| 239 |
+
background: #e9ecef;
|
| 240 |
+
border-radius: 5px;
|
| 241 |
+
height: 10px;
|
| 242 |
+
margin: 10px 0;
|
| 243 |
+
overflow: hidden;
|
| 244 |
+
}}
|
| 245 |
+
|
| 246 |
+
.progress-fill {{
|
| 247 |
+
background: #2d5a87;
|
| 248 |
+
height: 100%;
|
| 249 |
+
border-radius: 5px;
|
| 250 |
+
}}
|
| 251 |
+
|
| 252 |
+
.factors-list {{
|
| 253 |
+
list-style: none;
|
| 254 |
+
padding: 0;
|
| 255 |
+
}}
|
| 256 |
+
|
| 257 |
+
.factors-list li {{
|
| 258 |
+
padding: 5px 0;
|
| 259 |
+
padding-left: 20px;
|
| 260 |
+
position: relative;
|
| 261 |
+
}}
|
| 262 |
+
|
| 263 |
+
.factors-list li::before {{
|
| 264 |
+
content: "β’";
|
| 265 |
+
color: #2d5a87;
|
| 266 |
+
position: absolute;
|
| 267 |
+
left: 0;
|
| 268 |
+
}}
|
| 269 |
+
|
| 270 |
+
.timeline {{
|
| 271 |
+
margin: 20px 0;
|
| 272 |
+
}}
|
| 273 |
+
|
| 274 |
+
.timeline-item {{
|
| 275 |
+
display: flex;
|
| 276 |
+
margin: 10px 0;
|
| 277 |
+
}}
|
| 278 |
+
|
| 279 |
+
.timeline-time {{
|
| 280 |
+
min-width: 80px;
|
| 281 |
+
padding: 8px 15px;
|
| 282 |
+
background: #ffc107;
|
| 283 |
+
color: white;
|
| 284 |
+
font-weight: bold;
|
| 285 |
+
text-align: center;
|
| 286 |
+
border-radius: 5px;
|
| 287 |
+
}}
|
| 288 |
+
|
| 289 |
+
.timeline-time.impact {{
|
| 290 |
+
background: #dc3545;
|
| 291 |
+
}}
|
| 292 |
+
|
| 293 |
+
.timeline-time.after {{
|
| 294 |
+
background: #28a745;
|
| 295 |
+
}}
|
| 296 |
+
|
| 297 |
+
.timeline-event {{
|
| 298 |
+
flex: 1;
|
| 299 |
+
padding: 8px 15px;
|
| 300 |
+
background: #f8f9fa;
|
| 301 |
+
margin-left: 10px;
|
| 302 |
+
border-radius: 5px;
|
| 303 |
+
}}
|
| 304 |
+
|
| 305 |
+
.fault-assessment {{
|
| 306 |
+
background: #fff3cd;
|
| 307 |
+
padding: 20px;
|
| 308 |
+
border-radius: 10px;
|
| 309 |
+
border-left: 4px solid #ffc107;
|
| 310 |
+
margin: 20px 0;
|
| 311 |
+
}}
|
| 312 |
+
|
| 313 |
+
.footer {{
|
| 314 |
+
background: #1e3a5f;
|
| 315 |
+
color: white;
|
| 316 |
+
padding: 30px 40px;
|
| 317 |
+
text-align: center;
|
| 318 |
+
}}
|
| 319 |
+
|
| 320 |
+
.footer p {{
|
| 321 |
+
opacity: 0.8;
|
| 322 |
+
font-size: 0.9rem;
|
| 323 |
+
}}
|
| 324 |
+
|
| 325 |
+
@media print {{
|
| 326 |
+
.container {{
|
| 327 |
+
box-shadow: none;
|
| 328 |
+
}}
|
| 329 |
+
|
| 330 |
+
.section {{
|
| 331 |
+
page-break-inside: avoid;
|
| 332 |
+
}}
|
| 333 |
+
}}
|
| 334 |
+
</style>
|
| 335 |
+
</head>
|
| 336 |
+
<body>
|
| 337 |
+
<div class="container">
|
| 338 |
+
<!-- Header -->
|
| 339 |
+
<div class="header">
|
| 340 |
+
<h1>π Traffic Accident Analysis Report</h1>
|
| 341 |
+
<p class="subtitle">AI-Powered Analysis using Huawei MindSpore</p>
|
| 342 |
+
<p style="margin-top: 15px; opacity: 0.7;">Generated: {datetime.now().strftime("%B %d, %Y at %H:%M")}</p>
|
| 343 |
+
</div>
|
| 344 |
+
|
| 345 |
+
<!-- Executive Summary -->
|
| 346 |
+
<div class="section">
|
| 347 |
+
<h2>π Executive Summary</h2>
|
| 348 |
+
|
| 349 |
+
<div class="summary-grid">
|
| 350 |
+
<div class="summary-card">
|
| 351 |
+
<div class="label">Most Likely Scenario</div>
|
| 352 |
+
<div class="value">#{most_likely.get('id', 1)}</div>
|
| 353 |
+
<div class="delta high">{most_likely.get('probability', 0)*100:.1f}% probability</div>
|
| 354 |
+
</div>
|
| 355 |
+
|
| 356 |
+
<div class="summary-card">
|
| 357 |
+
<div class="label">Scenarios Generated</div>
|
| 358 |
+
<div class="value">{len(scenarios)}</div>
|
| 359 |
+
<div class="delta">AI-generated</div>
|
| 360 |
+
</div>
|
| 361 |
+
|
| 362 |
+
<div class="summary-card">
|
| 363 |
+
<div class="label">Collision Certainty</div>
|
| 364 |
+
<div class="value">{results.get('overall_collision_probability', 0)*100:.1f}%</div>
|
| 365 |
+
<div class="delta {'high' if results.get('overall_collision_probability', 0) > 0.7 else 'medium' if results.get('overall_collision_probability', 0) > 0.4 else 'low'}">
|
| 366 |
+
{'High' if results.get('overall_collision_probability', 0) > 0.7 else 'Medium' if results.get('overall_collision_probability', 0) > 0.4 else 'Low'}
|
| 367 |
+
</div>
|
| 368 |
+
</div>
|
| 369 |
+
|
| 370 |
+
<div class="summary-card">
|
| 371 |
+
<div class="label">Primary Factor</div>
|
| 372 |
+
<div class="value" style="font-size: 1.2rem;">{fault.get('primary_factor', 'N/A').replace('_', ' ').title()[:20]}</div>
|
| 373 |
+
<div class="delta">Vehicle {fault.get('likely_at_fault', '?')}</div>
|
| 374 |
+
</div>
|
| 375 |
+
</div>
|
| 376 |
+
</div>
|
| 377 |
+
|
| 378 |
+
<!-- Accident Details -->
|
| 379 |
+
<div class="section">
|
| 380 |
+
<h2>π Accident Details</h2>
|
| 381 |
+
|
| 382 |
+
<div class="info-grid">
|
| 383 |
+
<div class="info-box">
|
| 384 |
+
<h3>Location Information</h3>
|
| 385 |
+
<div class="info-row">
|
| 386 |
+
<span>Location:</span>
|
| 387 |
+
<strong>{accident_info.get('location', {}).get('name', 'Unknown')}</strong>
|
| 388 |
+
</div>
|
| 389 |
+
<div class="info-row">
|
| 390 |
+
<span>Coordinates:</span>
|
| 391 |
+
<strong>{accident_info.get('location', {}).get('latitude', 0):.4f}, {accident_info.get('location', {}).get('longitude', 0):.4f}</strong>
|
| 392 |
+
</div>
|
| 393 |
+
<div class="info-row">
|
| 394 |
+
<span>Road Type:</span>
|
| 395 |
+
<strong>{accident_info.get('road_type', 'Unknown').replace('_', ' ').title()}</strong>
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
|
| 399 |
+
<div class="info-box">
|
| 400 |
+
<h3>Conditions</h3>
|
| 401 |
+
<div class="info-row">
|
| 402 |
+
<span>Date/Time:</span>
|
| 403 |
+
<strong>{accident_info.get('datetime', 'Not specified')}</strong>
|
| 404 |
+
</div>
|
| 405 |
+
<div class="info-row">
|
| 406 |
+
<span>Weather:</span>
|
| 407 |
+
<strong>{accident_info.get('weather', 'Unknown').title()}</strong>
|
| 408 |
+
</div>
|
| 409 |
+
<div class="info-row">
|
| 410 |
+
<span>Road Condition:</span>
|
| 411 |
+
<strong>{accident_info.get('road_condition', 'Unknown').title()}</strong>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
</div>
|
| 415 |
+
</div>
|
| 416 |
+
|
| 417 |
+
<!-- Vehicle Information -->
|
| 418 |
+
<div class="section">
|
| 419 |
+
<h2>π Vehicle Information</h2>
|
| 420 |
+
|
| 421 |
+
<div class="info-grid">
|
| 422 |
+
<div class="vehicle-card v1">
|
| 423 |
+
<h3 style="color: #FF4B4B;">Vehicle 1 (Red)</h3>
|
| 424 |
+
<div class="info-row">
|
| 425 |
+
<span>Type:</span>
|
| 426 |
+
<strong>{v1_type}</strong>
|
| 427 |
+
</div>
|
| 428 |
+
<div class="info-row">
|
| 429 |
+
<span>Speed:</span>
|
| 430 |
+
<strong>{vehicle_1.get('speed', 0)} km/h</strong>
|
| 431 |
+
</div>
|
| 432 |
+
<div class="info-row">
|
| 433 |
+
<span>Direction:</span>
|
| 434 |
+
<strong>{vehicle_1.get('direction', 'Unknown').title()}</strong>
|
| 435 |
+
</div>
|
| 436 |
+
<div class="info-row">
|
| 437 |
+
<span>Action:</span>
|
| 438 |
+
<strong>{vehicle_1.get('action', 'Unknown').replace('_', ' ').title()}</strong>
|
| 439 |
+
</div>
|
| 440 |
+
<div class="info-row">
|
| 441 |
+
<span>Braking:</span>
|
| 442 |
+
<strong>{'Yes' if vehicle_1.get('braking', False) else 'No'}</strong>
|
| 443 |
+
</div>
|
| 444 |
+
<div class="info-row">
|
| 445 |
+
<span>Signaling:</span>
|
| 446 |
+
<strong>{'Yes' if vehicle_1.get('signaling', False) else 'No'}</strong>
|
| 447 |
+
</div>
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
<div class="vehicle-card v2">
|
| 451 |
+
<h3 style="color: #4B7BFF;">Vehicle 2 (Blue)</h3>
|
| 452 |
+
<div class="info-row">
|
| 453 |
+
<span>Type:</span>
|
| 454 |
+
<strong>{v2_type}</strong>
|
| 455 |
+
</div>
|
| 456 |
+
<div class="info-row">
|
| 457 |
+
<span>Speed:</span>
|
| 458 |
+
<strong>{vehicle_2.get('speed', 0)} km/h</strong>
|
| 459 |
+
</div>
|
| 460 |
+
<div class="info-row">
|
| 461 |
+
<span>Direction:</span>
|
| 462 |
+
<strong>{vehicle_2.get('direction', 'Unknown').title()}</strong>
|
| 463 |
+
</div>
|
| 464 |
+
<div class="info-row">
|
| 465 |
+
<span>Action:</span>
|
| 466 |
+
<strong>{vehicle_2.get('action', 'Unknown').replace('_', ' ').title()}</strong>
|
| 467 |
+
</div>
|
| 468 |
+
<div class="info-row">
|
| 469 |
+
<span>Braking:</span>
|
| 470 |
+
<strong>{'Yes' if vehicle_2.get('braking', False) else 'No'}</strong>
|
| 471 |
+
</div>
|
| 472 |
+
<div class="info-row">
|
| 473 |
+
<span>Signaling:</span>
|
| 474 |
+
<strong>{'Yes' if vehicle_2.get('signaling', False) else 'No'}</strong>
|
| 475 |
+
</div>
|
| 476 |
+
</div>
|
| 477 |
+
</div>
|
| 478 |
+
</div>
|
| 479 |
+
|
| 480 |
+
<!-- Generated Scenarios -->
|
| 481 |
+
<div class="section">
|
| 482 |
+
<h2>π― AI-Generated Scenarios</h2>
|
| 483 |
+
|
| 484 |
+
{''.join([f'''
|
| 485 |
+
<div class="scenario-card">
|
| 486 |
+
<div class="scenario-header">
|
| 487 |
+
<div>
|
| 488 |
+
<h3>Scenario {i+1}: {s['accident_type'].replace('_', ' ').title()}</h3>
|
| 489 |
+
</div>
|
| 490 |
+
<div class="probability {'high' if s['probability'] > 0.4 else 'medium' if s['probability'] > 0.2 else 'low'}">
|
| 491 |
+
{s['probability']*100:.1f}%
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
|
| 495 |
+
<p>{s['description']}</p>
|
| 496 |
+
|
| 497 |
+
<div style="margin-top: 15px;">
|
| 498 |
+
<strong>Analysis Metrics:</strong>
|
| 499 |
+
<div style="margin-top: 10px;">
|
| 500 |
+
<span>Collision Probability: {s['metrics']['collision_probability']*100:.1f}%</span>
|
| 501 |
+
<div class="progress-bar">
|
| 502 |
+
<div class="progress-fill" style="width: {s['metrics']['collision_probability']*100}%"></div>
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
<div>
|
| 506 |
+
<span>Path Overlap: {s['metrics']['path_overlap']*100:.1f}%</span>
|
| 507 |
+
<div class="progress-bar">
|
| 508 |
+
<div class="progress-fill" style="width: {s['metrics']['path_overlap']*100}%"></div>
|
| 509 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
<p style="margin-top: 10px;">Speed Differential: {s['metrics']['speed_differential']:.1f} km/h | Time to Collision: {s['metrics']['time_to_collision']:.2f}s</p>
|
| 512 |
+
</div>
|
| 513 |
+
|
| 514 |
+
<div style="margin-top: 15px;">
|
| 515 |
+
<strong>Contributing Factors:</strong>
|
| 516 |
+
<ul class="factors-list">
|
| 517 |
+
{''.join([f"<li>{f.replace('_', ' ').title()}</li>" for f in s['contributing_factors']])}
|
| 518 |
+
</ul>
|
| 519 |
+
</div>
|
| 520 |
+
</div>
|
| 521 |
+
''' for i, s in enumerate(scenarios)])}
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<!-- Fault Assessment -->
|
| 525 |
+
<div class="section">
|
| 526 |
+
<h2>βοΈ Preliminary Fault Assessment</h2>
|
| 527 |
+
|
| 528 |
+
<div class="fault-assessment">
|
| 529 |
+
<h3>β οΈ Disclaimer</h3>
|
| 530 |
+
<p>This is a preliminary AI-generated assessment for reference purposes only. Final fault determination should be made by qualified traffic authorities based on comprehensive investigation.</p>
|
| 531 |
+
</div>
|
| 532 |
+
|
| 533 |
+
<div class="info-grid" style="margin-top: 20px;">
|
| 534 |
+
<div class="info-box">
|
| 535 |
+
<h3>Contribution Analysis</h3>
|
| 536 |
+
<div style="margin: 15px 0;">
|
| 537 |
+
<span style="color: #FF4B4B;">Vehicle 1: {fault.get('vehicle_1_contribution', 50):.1f}%</span>
|
| 538 |
+
<div class="progress-bar">
|
| 539 |
+
<div class="progress-fill" style="width: {fault.get('vehicle_1_contribution', 50)}%; background: #FF4B4B;"></div>
|
| 540 |
+
</div>
|
| 541 |
+
</div>
|
| 542 |
+
<div>
|
| 543 |
+
<span style="color: #4B7BFF;">Vehicle 2: {fault.get('vehicle_2_contribution', 50):.1f}%</span>
|
| 544 |
+
<div class="progress-bar">
|
| 545 |
+
<div class="progress-fill" style="width: {fault.get('vehicle_2_contribution', 50)}%; background: #4B7BFF;"></div>
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
|
| 550 |
+
<div class="info-box">
|
| 551 |
+
<h3>Assessment Summary</h3>
|
| 552 |
+
<div class="info-row">
|
| 553 |
+
<span>Higher Contribution:</span>
|
| 554 |
+
<strong>Vehicle {fault.get('likely_at_fault', '?')}</strong>
|
| 555 |
+
</div>
|
| 556 |
+
<div class="info-row">
|
| 557 |
+
<span>Primary Factor:</span>
|
| 558 |
+
<strong>{fault.get('primary_factor', 'Unknown').replace('_', ' ').title()}</strong>
|
| 559 |
+
</div>
|
| 560 |
+
<div class="info-row">
|
| 561 |
+
<span>Assessment Confidence:</span>
|
| 562 |
+
<strong>{fault.get('confidence', 0)*100:.1f}%</strong>
|
| 563 |
+
</div>
|
| 564 |
+
</div>
|
| 565 |
+
</div>
|
| 566 |
+
</div>
|
| 567 |
+
|
| 568 |
+
<!-- Timeline -->
|
| 569 |
+
<div class="section">
|
| 570 |
+
<h2>β±οΈ Estimated Accident Timeline</h2>
|
| 571 |
+
|
| 572 |
+
<div class="timeline">
|
| 573 |
+
{''.join([f'''
|
| 574 |
+
<div class="timeline-item">
|
| 575 |
+
<div class="timeline-time {'impact' if e['time'] == 0 else 'after' if e['time'] > 0 else ''}">{e['time']:+.1f}s</div>
|
| 576 |
+
<div class="timeline-event">{e['event']}</div>
|
| 577 |
+
</div>
|
| 578 |
+
''' for e in results.get('timeline', [])])}
|
| 579 |
+
</div>
|
| 580 |
+
</div>
|
| 581 |
+
|
| 582 |
+
<!-- Footer -->
|
| 583 |
+
<div class="footer">
|
| 584 |
+
<p><strong>Traffic Accident Reconstruction System</strong></p>
|
| 585 |
+
<p>Huawei AI Innovation Challenge 2026</p>
|
| 586 |
+
<p style="margin-top: 10px;">Powered by Huawei MindSpore AI Framework</p>
|
| 587 |
+
<p style="margin-top: 15px; font-size: 0.8rem;">Report ID: {timestamp}</p>
|
| 588 |
+
</div>
|
| 589 |
+
</div>
|
| 590 |
+
</body>
|
| 591 |
+
</html>
|
| 592 |
+
"""
|
| 593 |
+
|
| 594 |
+
# Save the report
|
| 595 |
+
report_path = REPORTS_DIR / f"accident_report_{timestamp}.html"
|
| 596 |
+
|
| 597 |
+
with open(report_path, 'w', encoding='utf-8') as f:
|
| 598 |
+
f.write(html_content)
|
| 599 |
+
|
| 600 |
+
return str(report_path)
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def generate_pdf_report(
|
| 604 |
+
results: Dict[str, Any],
|
| 605 |
+
scenarios: List[Dict[str, Any]],
|
| 606 |
+
accident_info: Dict[str, Any],
|
| 607 |
+
vehicle_1: Dict[str, Any],
|
| 608 |
+
vehicle_2: Dict[str, Any],
|
| 609 |
+
timestamp: str,
|
| 610 |
+
include_maps: bool,
|
| 611 |
+
include_charts: bool,
|
| 612 |
+
include_raw_data: bool
|
| 613 |
+
) -> str:
|
| 614 |
+
"""Generate PDF report using ReportLab."""
|
| 615 |
+
|
| 616 |
+
try:
|
| 617 |
+
from reportlab.lib import colors
|
| 618 |
+
from reportlab.lib.pagesizes import A4
|
| 619 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 620 |
+
from reportlab.lib.units import inch, cm
|
| 621 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
|
| 622 |
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
| 623 |
+
except ImportError:
|
| 624 |
+
# Fall back to HTML if ReportLab not available
|
| 625 |
+
return generate_html_report(
|
| 626 |
+
results, scenarios, accident_info, vehicle_1, vehicle_2,
|
| 627 |
+
timestamp, include_maps, include_charts, include_raw_data
|
| 628 |
+
)
|
| 629 |
+
|
| 630 |
+
report_path = REPORTS_DIR / f"accident_report_{timestamp}.pdf"
|
| 631 |
+
|
| 632 |
+
doc = SimpleDocTemplate(
|
| 633 |
+
str(report_path),
|
| 634 |
+
pagesize=A4,
|
| 635 |
+
rightMargin=72,
|
| 636 |
+
leftMargin=72,
|
| 637 |
+
topMargin=72,
|
| 638 |
+
bottomMargin=72
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
styles = getSampleStyleSheet()
|
| 642 |
+
|
| 643 |
+
# Custom styles
|
| 644 |
+
title_style = ParagraphStyle(
|
| 645 |
+
'CustomTitle',
|
| 646 |
+
parent=styles['Heading1'],
|
| 647 |
+
fontSize=24,
|
| 648 |
+
spaceAfter=30,
|
| 649 |
+
alignment=TA_CENTER,
|
| 650 |
+
textColor=colors.HexColor('#1e3a5f')
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
heading_style = ParagraphStyle(
|
| 654 |
+
'CustomHeading',
|
| 655 |
+
parent=styles['Heading2'],
|
| 656 |
+
fontSize=16,
|
| 657 |
+
spaceBefore=20,
|
| 658 |
+
spaceAfter=10,
|
| 659 |
+
textColor=colors.HexColor('#2d5a87')
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
story = []
|
| 663 |
+
|
| 664 |
+
# Title
|
| 665 |
+
story.append(Paragraph("Traffic Accident Analysis Report", title_style))
|
| 666 |
+
story.append(Paragraph("AI-Powered Analysis using Huawei MindSpore", styles['Normal']))
|
| 667 |
+
story.append(Spacer(1, 30))
|
| 668 |
+
|
| 669 |
+
# Executive Summary
|
| 670 |
+
story.append(Paragraph("Executive Summary", heading_style))
|
| 671 |
+
|
| 672 |
+
most_likely = results.get('most_likely_scenario', {})
|
| 673 |
+
fault = results.get('preliminary_fault_assessment', {})
|
| 674 |
+
|
| 675 |
+
summary_data = [
|
| 676 |
+
['Most Likely Scenario', f"#{most_likely.get('id', 1)} ({most_likely.get('probability', 0)*100:.1f}%)"],
|
| 677 |
+
['Scenarios Generated', str(len(scenarios))],
|
| 678 |
+
['Collision Certainty', f"{results.get('overall_collision_probability', 0)*100:.1f}%"],
|
| 679 |
+
['Primary Factor', fault.get('primary_factor', 'N/A').replace('_', ' ').title()]
|
| 680 |
+
]
|
| 681 |
+
|
| 682 |
+
summary_table = Table(summary_data, colWidths=[3*inch, 3*inch])
|
| 683 |
+
summary_table.setStyle(TableStyle([
|
| 684 |
+
('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f8f9fa')),
|
| 685 |
+
('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
|
| 686 |
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 687 |
+
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
|
| 688 |
+
('FONTSIZE', (0, 0), (-1, -1), 10),
|
| 689 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
|
| 690 |
+
('TOPPADDING', (0, 0), (-1, -1), 12),
|
| 691 |
+
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6'))
|
| 692 |
+
]))
|
| 693 |
+
|
| 694 |
+
story.append(summary_table)
|
| 695 |
+
story.append(Spacer(1, 20))
|
| 696 |
+
|
| 697 |
+
# Location Details
|
| 698 |
+
story.append(Paragraph("Accident Details", heading_style))
|
| 699 |
+
|
| 700 |
+
location_data = [
|
| 701 |
+
['Location', accident_info.get('location', {}).get('name', 'Unknown')],
|
| 702 |
+
['Road Type', accident_info.get('road_type', 'Unknown').replace('_', ' ').title()],
|
| 703 |
+
['Weather', accident_info.get('weather', 'Unknown').title()],
|
| 704 |
+
['Road Condition', accident_info.get('road_condition', 'Unknown').title()]
|
| 705 |
+
]
|
| 706 |
+
|
| 707 |
+
location_table = Table(location_data, colWidths=[2*inch, 4*inch])
|
| 708 |
+
location_table.setStyle(TableStyle([
|
| 709 |
+
('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f8f9fa')),
|
| 710 |
+
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6')),
|
| 711 |
+
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
|
| 712 |
+
('FONTSIZE', (0, 0), (-1, -1), 10),
|
| 713 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 714 |
+
('TOPPADDING', (0, 0), (-1, -1), 8),
|
| 715 |
+
]))
|
| 716 |
+
|
| 717 |
+
story.append(location_table)
|
| 718 |
+
story.append(Spacer(1, 20))
|
| 719 |
+
|
| 720 |
+
# Scenarios
|
| 721 |
+
story.append(Paragraph("Generated Scenarios", heading_style))
|
| 722 |
+
|
| 723 |
+
for i, scenario in enumerate(scenarios):
|
| 724 |
+
story.append(Paragraph(
|
| 725 |
+
f"<b>Scenario {i+1}: {scenario['accident_type'].replace('_', ' ').title()}</b> - {scenario['probability']*100:.1f}% probability",
|
| 726 |
+
styles['Normal']
|
| 727 |
+
))
|
| 728 |
+
story.append(Paragraph(scenario['description'], styles['Normal']))
|
| 729 |
+
story.append(Spacer(1, 10))
|
| 730 |
+
|
| 731 |
+
# Build PDF
|
| 732 |
+
doc.build(story)
|
| 733 |
+
|
| 734 |
+
return str(report_path)
|
results_display.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Results Display Component
|
| 3 |
+
=========================
|
| 4 |
+
Handles visualization of analysis results and scenarios.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import plotly.express as px
|
| 9 |
+
import plotly.graph_objects as go
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
from config import COLORS, PROBABILITY_THRESHOLDS
|
| 14 |
+
from ui.map_viewer import render_results_map
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def render_results():
|
| 18 |
+
"""Render the complete results section."""
|
| 19 |
+
|
| 20 |
+
results = st.session_state.analysis_results
|
| 21 |
+
scenarios = st.session_state.scenarios
|
| 22 |
+
|
| 23 |
+
if not results or not scenarios:
|
| 24 |
+
st.warning("No results available.")
|
| 25 |
+
return
|
| 26 |
+
|
| 27 |
+
# Summary metrics
|
| 28 |
+
render_summary_metrics(results)
|
| 29 |
+
|
| 30 |
+
st.markdown("---")
|
| 31 |
+
|
| 32 |
+
# Scenario tabs
|
| 33 |
+
render_scenario_tabs(scenarios)
|
| 34 |
+
|
| 35 |
+
st.markdown("---")
|
| 36 |
+
|
| 37 |
+
# Detailed analysis
|
| 38 |
+
render_detailed_analysis(results)
|
| 39 |
+
|
| 40 |
+
st.markdown("---")
|
| 41 |
+
|
| 42 |
+
# Report generation
|
| 43 |
+
render_report_section(results, scenarios)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def render_summary_metrics(results: dict):
|
| 47 |
+
"""Render summary metric cards."""
|
| 48 |
+
|
| 49 |
+
st.subheader("π Analysis Summary")
|
| 50 |
+
|
| 51 |
+
# First row: Main metrics
|
| 52 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 53 |
+
|
| 54 |
+
with col1:
|
| 55 |
+
most_likely = results.get('most_likely_scenario', {})
|
| 56 |
+
prob = most_likely.get('probability', 0) * 100
|
| 57 |
+
|
| 58 |
+
st.metric(
|
| 59 |
+
label="Most Likely Scenario",
|
| 60 |
+
value=f"#{most_likely.get('id', 1)}",
|
| 61 |
+
delta=f"{prob:.1f}% probability"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
with col2:
|
| 65 |
+
st.metric(
|
| 66 |
+
label="Scenarios Generated",
|
| 67 |
+
value=len(st.session_state.scenarios),
|
| 68 |
+
delta="AI-generated"
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
with col3:
|
| 72 |
+
collision_prob = results.get('overall_collision_probability', 0) * 100
|
| 73 |
+
st.metric(
|
| 74 |
+
label="Collision Certainty",
|
| 75 |
+
value=f"{collision_prob:.1f}%",
|
| 76 |
+
delta="High" if collision_prob > 70 else "Medium" if collision_prob > 40 else "Low"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
with col4:
|
| 80 |
+
fault = results.get('preliminary_fault_assessment', {})
|
| 81 |
+
st.metric(
|
| 82 |
+
label="Primary Factor",
|
| 83 |
+
value=fault.get('primary_factor', 'N/A').replace('_', ' ').title()[:15],
|
| 84 |
+
delta=f"Vehicle {fault.get('likely_at_fault', '?')}"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Second row: FAULT ASSESSMENT - More prominent!
|
| 88 |
+
st.markdown("---")
|
| 89 |
+
st.subheader("βοΈ Fault Assessment")
|
| 90 |
+
|
| 91 |
+
fault = results.get('preliminary_fault_assessment', {})
|
| 92 |
+
v1_fault = fault.get('vehicle_1_contribution', 50)
|
| 93 |
+
v2_fault = fault.get('vehicle_2_contribution', 50)
|
| 94 |
+
|
| 95 |
+
fault_col1, fault_col2, fault_col3 = st.columns([2, 1, 2])
|
| 96 |
+
|
| 97 |
+
with fault_col1:
|
| 98 |
+
st.markdown(f"""
|
| 99 |
+
<div style="
|
| 100 |
+
background: linear-gradient(135deg, #FF4B4B 0%, #ff6b6b 100%);
|
| 101 |
+
color: white;
|
| 102 |
+
padding: 1.5rem;
|
| 103 |
+
border-radius: 15px;
|
| 104 |
+
text-align: center;
|
| 105 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 106 |
+
">
|
| 107 |
+
<h3 style="margin: 0; font-size: 1rem;">π Vehicle 1 (Party 1)</h3>
|
| 108 |
+
<h1 style="margin: 0.5rem 0; font-size: 3rem;">{v1_fault:.1f}%</h1>
|
| 109 |
+
<p style="margin: 0; opacity: 0.9;">Fault Contribution</p>
|
| 110 |
+
</div>
|
| 111 |
+
""", unsafe_allow_html=True)
|
| 112 |
+
|
| 113 |
+
with fault_col2:
|
| 114 |
+
st.markdown("""
|
| 115 |
+
<div style="
|
| 116 |
+
display: flex;
|
| 117 |
+
align-items: center;
|
| 118 |
+
justify-content: center;
|
| 119 |
+
height: 100%;
|
| 120 |
+
font-size: 2rem;
|
| 121 |
+
color: #666;
|
| 122 |
+
">
|
| 123 |
+
βοΈ
|
| 124 |
+
</div>
|
| 125 |
+
""", unsafe_allow_html=True)
|
| 126 |
+
|
| 127 |
+
with fault_col3:
|
| 128 |
+
st.markdown(f"""
|
| 129 |
+
<div style="
|
| 130 |
+
background: linear-gradient(135deg, #4B7BFF 0%, #6b8bff 100%);
|
| 131 |
+
color: white;
|
| 132 |
+
padding: 1.5rem;
|
| 133 |
+
border-radius: 15px;
|
| 134 |
+
text-align: center;
|
| 135 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 136 |
+
">
|
| 137 |
+
<h3 style="margin: 0; font-size: 1rem;">π Vehicle 2 (Party 2)</h3>
|
| 138 |
+
<h1 style="margin: 0.5rem 0; font-size: 3rem;">{v2_fault:.1f}%</h1>
|
| 139 |
+
<p style="margin: 0; opacity: 0.9;">Fault Contribution</p>
|
| 140 |
+
</div>
|
| 141 |
+
""", unsafe_allow_html=True)
|
| 142 |
+
|
| 143 |
+
# Likely at fault message
|
| 144 |
+
likely_at_fault = fault.get('likely_at_fault', 1)
|
| 145 |
+
primary_factor = fault.get('primary_factor', 'Unknown').replace('_', ' ').title()
|
| 146 |
+
|
| 147 |
+
if v1_fault > v2_fault:
|
| 148 |
+
fault_msg = f"π΄ **Vehicle 1 (Party 1)** appears to be primarily at fault ({v1_fault:.1f}%)"
|
| 149 |
+
fault_color = "#FF4B4B"
|
| 150 |
+
elif v2_fault > v1_fault:
|
| 151 |
+
fault_msg = f"π΅ **Vehicle 2 (Party 2)** appears to be primarily at fault ({v2_fault:.1f}%)"
|
| 152 |
+
fault_color = "#4B7BFF"
|
| 153 |
+
else:
|
| 154 |
+
fault_msg = "βοΈ **Shared responsibility** - both parties contributed equally"
|
| 155 |
+
fault_color = "#ffc107"
|
| 156 |
+
|
| 157 |
+
st.markdown(f"""
|
| 158 |
+
<div style="
|
| 159 |
+
background: rgba(255, 255, 255, 0.08);
|
| 160 |
+
backdrop-filter: blur(10px);
|
| 161 |
+
border-left: 5px solid {fault_color};
|
| 162 |
+
padding: 1.2rem;
|
| 163 |
+
border-radius: 12px;
|
| 164 |
+
margin-top: 1rem;
|
| 165 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 166 |
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
| 167 |
+
">
|
| 168 |
+
<p style="margin: 0; font-size: 1.1rem; color: white; font-weight: 600;">{fault_msg}</p>
|
| 169 |
+
<p style="margin: 0.5rem 0 0 0; color: rgba(255, 255, 255, 0.7);">Primary Factor: <strong style="color: {fault_color};">{primary_factor}</strong></p>
|
| 170 |
+
</div>
|
| 171 |
+
""", unsafe_allow_html=True)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def render_scenario_tabs(scenarios: list):
|
| 175 |
+
"""Render scenario selection tabs."""
|
| 176 |
+
|
| 177 |
+
st.subheader("π― Generated Scenarios")
|
| 178 |
+
|
| 179 |
+
# Scenario selector
|
| 180 |
+
scenario_options = [f"Scenario {i+1} ({s['probability']*100:.1f}%)" for i, s in enumerate(scenarios)]
|
| 181 |
+
|
| 182 |
+
selected_idx = st.selectbox(
|
| 183 |
+
"Select a scenario to view details:",
|
| 184 |
+
range(len(scenarios)),
|
| 185 |
+
format_func=lambda x: scenario_options[x]
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
selected_scenario = scenarios[selected_idx]
|
| 189 |
+
|
| 190 |
+
# Two columns: map and details
|
| 191 |
+
col1, col2 = st.columns([3, 2])
|
| 192 |
+
|
| 193 |
+
with col1:
|
| 194 |
+
st.markdown("**Scenario Visualization**")
|
| 195 |
+
render_results_map(scenarios, selected_idx)
|
| 196 |
+
|
| 197 |
+
with col2:
|
| 198 |
+
render_scenario_details(selected_scenario, selected_idx + 1)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def render_scenario_details(scenario: dict, scenario_num: int):
|
| 202 |
+
"""Render details for a single scenario."""
|
| 203 |
+
|
| 204 |
+
prob = scenario.get('probability', 0)
|
| 205 |
+
prob_color = get_probability_color(prob)
|
| 206 |
+
|
| 207 |
+
st.markdown(f"""
|
| 208 |
+
<div style="
|
| 209 |
+
background: rgba(255, 255, 255, 0.08);
|
| 210 |
+
backdrop-filter: blur(10px);
|
| 211 |
+
border-radius: 12px;
|
| 212 |
+
padding: 1.5rem;
|
| 213 |
+
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
|
| 214 |
+
border-top: 4px solid {prob_color};
|
| 215 |
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
| 216 |
+
">
|
| 217 |
+
<h3 style="margin: 0; color: white; font-weight: 700;">Scenario {scenario_num}</h3>
|
| 218 |
+
<h2 style="color: {prob_color}; margin: 0.5rem 0; font-weight: 800;">{prob*100:.1f}% Probability</h2>
|
| 219 |
+
</div>
|
| 220 |
+
""", unsafe_allow_html=True)
|
| 221 |
+
|
| 222 |
+
st.markdown("")
|
| 223 |
+
|
| 224 |
+
# Accident type
|
| 225 |
+
accident_type = scenario.get('accident_type', 'Unknown').replace('_', ' ').title()
|
| 226 |
+
st.markdown(f"**Accident Type:** {accident_type}")
|
| 227 |
+
|
| 228 |
+
# SIMULATION VIDEO - NEW!
|
| 229 |
+
st.markdown("---")
|
| 230 |
+
st.markdown("### π¬ Simulation Visualization")
|
| 231 |
+
render_scenario_video(scenario.get('accident_type', ''))
|
| 232 |
+
st.markdown("---")
|
| 233 |
+
|
| 234 |
+
# Description
|
| 235 |
+
st.markdown(f"**Description:** {scenario.get('description', 'No description available.')}")
|
| 236 |
+
|
| 237 |
+
# Contributing factors
|
| 238 |
+
st.markdown("**Contributing Factors:**")
|
| 239 |
+
factors = scenario.get('contributing_factors', [])
|
| 240 |
+
for factor in factors[:3]:
|
| 241 |
+
st.markdown(f"- {factor.replace('_', ' ').title()}")
|
| 242 |
+
|
| 243 |
+
# Metrics
|
| 244 |
+
st.markdown("**Analysis Metrics:**")
|
| 245 |
+
|
| 246 |
+
metrics = scenario.get('metrics', {})
|
| 247 |
+
|
| 248 |
+
# Collision probability bar
|
| 249 |
+
col_prob = metrics.get('collision_probability', 0)
|
| 250 |
+
st.progress(col_prob, text=f"Collision Probability: {col_prob*100:.1f}%")
|
| 251 |
+
|
| 252 |
+
# Path overlap bar
|
| 253 |
+
path_overlap = metrics.get('path_overlap', 0)
|
| 254 |
+
st.progress(path_overlap, text=f"Path Overlap: {path_overlap*100:.1f}%")
|
| 255 |
+
|
| 256 |
+
# Speed differential
|
| 257 |
+
speed_diff = metrics.get('speed_differential', 0)
|
| 258 |
+
st.write(f"**Speed Differential:** {speed_diff:.1f} km/h")
|
| 259 |
+
|
| 260 |
+
# Time to collision
|
| 261 |
+
ttc = metrics.get('time_to_collision', 0)
|
| 262 |
+
st.write(f"**Est. Time to Collision:** {ttc:.2f} seconds")
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def render_scenario_video(accident_type: str):
|
| 266 |
+
"""
|
| 267 |
+
Render scenario simulation video.
|
| 268 |
+
|
| 269 |
+
Args:
|
| 270 |
+
accident_type: Type of accident (e.g., 'rear_end_collision')
|
| 271 |
+
"""
|
| 272 |
+
from pathlib import Path
|
| 273 |
+
|
| 274 |
+
# Video mapping
|
| 275 |
+
video_map = {
|
| 276 |
+
'rear_end_collision': 'rear_end_collision.gif',
|
| 277 |
+
'side_impact': 'side_impact_collision.gif',
|
| 278 |
+
'head_on_collision': 'head_on_collision.gif',
|
| 279 |
+
'sideswipe': 'side_impact_collision.gif', # Use side impact as fallback
|
| 280 |
+
'roundabout_entry_collision': 'side_impact_collision.gif',
|
| 281 |
+
'lane_change_collision': 'rear_end_collision.gif', # Use rear-end as fallback
|
| 282 |
+
'intersection_collision': 'side_impact_collision.gif',
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
# Get video file
|
| 286 |
+
video_file = video_map.get(accident_type.lower(), 'rear_end_collision.gif')
|
| 287 |
+
video_path = Path(__file__).parent.parent / "output" / "visualizations" / video_file
|
| 288 |
+
|
| 289 |
+
if video_path.exists():
|
| 290 |
+
try:
|
| 291 |
+
# Display the GIF animation
|
| 292 |
+
with open(video_path, 'rb') as f:
|
| 293 |
+
st.image(f.read(), use_container_width=True)
|
| 294 |
+
|
| 295 |
+
st.caption(f"π₯ Animated simulation of {accident_type.replace('_', ' ').title()}")
|
| 296 |
+
except Exception as e:
|
| 297 |
+
st.info("π Video simulation available offline. Showing static analysis.")
|
| 298 |
+
else:
|
| 299 |
+
# Fallback if video doesn't exist
|
| 300 |
+
st.info(f"π¬ Simulation visualization for **{accident_type.replace('_', ' ').title()}**")
|
| 301 |
+
st.write("Animation shows vehicle movements and collision dynamics.")
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def render_detailed_analysis(results: dict):
|
| 305 |
+
"""Render detailed analysis charts and data."""
|
| 306 |
+
|
| 307 |
+
st.subheader("π Detailed Analysis")
|
| 308 |
+
|
| 309 |
+
tab1, tab2, tab3 = st.tabs(["Probability Distribution", "Factor Analysis", "Timeline"])
|
| 310 |
+
|
| 311 |
+
with tab1:
|
| 312 |
+
render_probability_chart()
|
| 313 |
+
|
| 314 |
+
with tab2:
|
| 315 |
+
render_factor_analysis(results)
|
| 316 |
+
|
| 317 |
+
with tab3:
|
| 318 |
+
render_timeline_analysis(results)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def render_probability_chart():
|
| 322 |
+
"""Render scenario probability distribution chart."""
|
| 323 |
+
|
| 324 |
+
scenarios = st.session_state.scenarios
|
| 325 |
+
|
| 326 |
+
# Create dataframe
|
| 327 |
+
df = pd.DataFrame([
|
| 328 |
+
{
|
| 329 |
+
'Scenario': f"Scenario {i+1}",
|
| 330 |
+
'Probability': s['probability'] * 100,
|
| 331 |
+
'Type': s.get('accident_type', 'Unknown').replace('_', ' ').title()
|
| 332 |
+
}
|
| 333 |
+
for i, s in enumerate(scenarios)
|
| 334 |
+
])
|
| 335 |
+
|
| 336 |
+
# Bar chart
|
| 337 |
+
fig = px.bar(
|
| 338 |
+
df,
|
| 339 |
+
x='Scenario',
|
| 340 |
+
y='Probability',
|
| 341 |
+
color='Type',
|
| 342 |
+
title='Scenario Probability Distribution',
|
| 343 |
+
labels={'Probability': 'Probability (%)'},
|
| 344 |
+
color_discrete_sequence=px.colors.qualitative.Set2
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
fig.update_layout(
|
| 348 |
+
xaxis_title="Scenario",
|
| 349 |
+
yaxis_title="Probability (%)",
|
| 350 |
+
showlegend=True
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 354 |
+
|
| 355 |
+
# Pie chart
|
| 356 |
+
fig2 = px.pie(
|
| 357 |
+
df,
|
| 358 |
+
values='Probability',
|
| 359 |
+
names='Scenario',
|
| 360 |
+
title='Probability Share by Scenario'
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def render_factor_analysis(results: dict):
|
| 367 |
+
"""Render contributing factor analysis."""
|
| 368 |
+
|
| 369 |
+
st.markdown("**Contributing Factors Analysis**")
|
| 370 |
+
|
| 371 |
+
# Aggregate factors from all scenarios
|
| 372 |
+
factor_counts = {}
|
| 373 |
+
for scenario in st.session_state.scenarios:
|
| 374 |
+
for factor in scenario.get('contributing_factors', []):
|
| 375 |
+
factor_counts[factor] = factor_counts.get(factor, 0) + 1
|
| 376 |
+
|
| 377 |
+
if factor_counts:
|
| 378 |
+
df = pd.DataFrame([
|
| 379 |
+
{'Factor': k.replace('_', ' ').title(), 'Count': v}
|
| 380 |
+
for k, v in sorted(factor_counts.items(), key=lambda x: -x[1])
|
| 381 |
+
])
|
| 382 |
+
|
| 383 |
+
fig = px.bar(
|
| 384 |
+
df,
|
| 385 |
+
x='Factor',
|
| 386 |
+
y='Count',
|
| 387 |
+
title='Most Common Contributing Factors',
|
| 388 |
+
color='Count',
|
| 389 |
+
color_continuous_scale='Reds'
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
fig.update_layout(xaxis_tickangle=-45)
|
| 393 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 394 |
+
|
| 395 |
+
# Fault assessment
|
| 396 |
+
fault = results.get('preliminary_fault_assessment', {})
|
| 397 |
+
|
| 398 |
+
col1, col2 = st.columns(2)
|
| 399 |
+
|
| 400 |
+
with col1:
|
| 401 |
+
st.markdown(f"""
|
| 402 |
+
<div style="
|
| 403 |
+
background: #fff3cd;
|
| 404 |
+
padding: 1rem;
|
| 405 |
+
border-radius: 10px;
|
| 406 |
+
border-left: 4px solid #ffc107;
|
| 407 |
+
">
|
| 408 |
+
<h4 style="margin: 0;">β οΈ Preliminary Assessment</h4>
|
| 409 |
+
<p style="margin: 0.5rem 0 0 0;">
|
| 410 |
+
Based on the analysis, <b>Vehicle {fault.get('likely_at_fault', '?')}</b>
|
| 411 |
+
appears to have a higher contribution to the accident due to
|
| 412 |
+
<b>{fault.get('primary_factor', 'unknown factors').replace('_', ' ')}</b>.
|
| 413 |
+
</p>
|
| 414 |
+
</div>
|
| 415 |
+
""", unsafe_allow_html=True)
|
| 416 |
+
|
| 417 |
+
with col2:
|
| 418 |
+
# Fault distribution pie
|
| 419 |
+
v1_fault = fault.get('vehicle_1_contribution', 50)
|
| 420 |
+
v2_fault = fault.get('vehicle_2_contribution', 50)
|
| 421 |
+
|
| 422 |
+
fig = go.Figure(data=[go.Pie(
|
| 423 |
+
labels=['Vehicle 1', 'Vehicle 2'],
|
| 424 |
+
values=[v1_fault, v2_fault],
|
| 425 |
+
marker_colors=[COLORS['vehicle_1'], COLORS['vehicle_2']],
|
| 426 |
+
hole=0.4
|
| 427 |
+
)])
|
| 428 |
+
|
| 429 |
+
fig.update_layout(
|
| 430 |
+
title='Fault Contribution Distribution',
|
| 431 |
+
showlegend=True
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def render_timeline_analysis(results: dict):
|
| 438 |
+
"""Render accident timeline analysis."""
|
| 439 |
+
|
| 440 |
+
st.markdown("**Estimated Accident Timeline**")
|
| 441 |
+
|
| 442 |
+
timeline = results.get('timeline', [
|
| 443 |
+
{'time': -5.0, 'event': 'Vehicles approaching intersection'},
|
| 444 |
+
{'time': -3.0, 'event': 'Vehicle paths begin to converge'},
|
| 445 |
+
{'time': -1.5, 'event': 'Collision becomes imminent'},
|
| 446 |
+
{'time': -0.5, 'event': 'Point of no return'},
|
| 447 |
+
{'time': 0.0, 'event': 'Impact'},
|
| 448 |
+
{'time': 0.5, 'event': 'Vehicles come to rest'}
|
| 449 |
+
])
|
| 450 |
+
|
| 451 |
+
# Timeline visualization
|
| 452 |
+
for i, event in enumerate(timeline):
|
| 453 |
+
time_str = f"{event['time']:+.1f}s" if event['time'] != 0 else "0.0s (IMPACT)"
|
| 454 |
+
|
| 455 |
+
color = "#dc3545" if event['time'] == 0 else "#ffc107" if event['time'] < 0 else "#28a745"
|
| 456 |
+
|
| 457 |
+
st.markdown(f"""
|
| 458 |
+
<div style="
|
| 459 |
+
display: flex;
|
| 460 |
+
align-items: center;
|
| 461 |
+
margin: 0.5rem 0;
|
| 462 |
+
">
|
| 463 |
+
<div style="
|
| 464 |
+
background: {color};
|
| 465 |
+
color: white;
|
| 466 |
+
padding: 0.5rem 1rem;
|
| 467 |
+
border-radius: 5px;
|
| 468 |
+
min-width: 80px;
|
| 469 |
+
text-align: center;
|
| 470 |
+
font-weight: bold;
|
| 471 |
+
">{time_str}</div>
|
| 472 |
+
<div style="
|
| 473 |
+
flex: 1;
|
| 474 |
+
padding: 0.5rem 1rem;
|
| 475 |
+
background: #f8f9fa;
|
| 476 |
+
border-radius: 5px;
|
| 477 |
+
margin-left: 1rem;
|
| 478 |
+
">{event['event']}</div>
|
| 479 |
+
</div>
|
| 480 |
+
""", unsafe_allow_html=True)
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def render_report_section(results: dict, scenarios: list):
|
| 484 |
+
"""Render report generation section."""
|
| 485 |
+
|
| 486 |
+
st.subheader("π Generate Report")
|
| 487 |
+
|
| 488 |
+
col1, col2 = st.columns(2)
|
| 489 |
+
|
| 490 |
+
with col1:
|
| 491 |
+
report_format = st.selectbox(
|
| 492 |
+
"Report Format",
|
| 493 |
+
options=['PDF', 'HTML'],
|
| 494 |
+
index=0
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
include_maps = st.checkbox("Include Maps", value=True)
|
| 498 |
+
include_charts = st.checkbox("Include Charts", value=True)
|
| 499 |
+
include_raw_data = st.checkbox("Include Raw Data", value=False)
|
| 500 |
+
|
| 501 |
+
with col2:
|
| 502 |
+
st.markdown("""
|
| 503 |
+
**Report Contents:**
|
| 504 |
+
- Executive Summary
|
| 505 |
+
- Accident Details
|
| 506 |
+
- Vehicle Information
|
| 507 |
+
- AI-Generated Scenarios
|
| 508 |
+
- Probability Analysis
|
| 509 |
+
- Contributing Factors
|
| 510 |
+
- Preliminary Assessment
|
| 511 |
+
- Recommendations
|
| 512 |
+
""")
|
| 513 |
+
|
| 514 |
+
if st.button("π₯ Generate Report", type="primary", use_container_width=True):
|
| 515 |
+
with st.spinner("Generating report..."):
|
| 516 |
+
from analysis.report_generator import generate_report
|
| 517 |
+
|
| 518 |
+
report_path = generate_report(
|
| 519 |
+
results=results,
|
| 520 |
+
scenarios=scenarios,
|
| 521 |
+
accident_info=st.session_state.accident_info,
|
| 522 |
+
vehicle_1=st.session_state.vehicle_1,
|
| 523 |
+
vehicle_2=st.session_state.vehicle_2,
|
| 524 |
+
format=report_format.lower(),
|
| 525 |
+
include_maps=include_maps,
|
| 526 |
+
include_charts=include_charts,
|
| 527 |
+
include_raw_data=include_raw_data
|
| 528 |
+
)
|
| 529 |
+
|
| 530 |
+
if report_path:
|
| 531 |
+
st.success(f"β
Report generated successfully!")
|
| 532 |
+
|
| 533 |
+
with open(report_path, 'rb') as f:
|
| 534 |
+
st.download_button(
|
| 535 |
+
label=f"π₯ Download {report_format} Report",
|
| 536 |
+
data=f,
|
| 537 |
+
file_name=f"accident_analysis_report.{report_format.lower()}",
|
| 538 |
+
mime="application/pdf" if report_format == "PDF" else "text/html"
|
| 539 |
+
)
|
| 540 |
+
else:
|
| 541 |
+
st.error("Failed to generate report. Please try again.")
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
def get_probability_color(probability: float) -> str:
|
| 545 |
+
"""Get color based on probability value."""
|
| 546 |
+
|
| 547 |
+
if probability >= PROBABILITY_THRESHOLDS['high']:
|
| 548 |
+
return "#28a745" # Green - high confidence
|
| 549 |
+
elif probability >= PROBABILITY_THRESHOLDS['medium']:
|
| 550 |
+
return "#ffc107" # Yellow - medium confidence
|
| 551 |
+
else:
|
| 552 |
+
return "#dc3545" # Red - low confidence
|
scenario_analyzer.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Scenario Analyzer
|
| 3 |
+
=================
|
| 4 |
+
Main analysis module that uses MindSpore AI (via ONNX) to generate accident scenarios.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import random
|
| 9 |
+
from typing import Dict, List, Any
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import sys
|
| 14 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 15 |
+
|
| 16 |
+
from config import (
|
| 17 |
+
ACCIDENT_TYPES,
|
| 18 |
+
CONTRIBUTING_FACTORS,
|
| 19 |
+
NUM_SCENARIOS_TO_GENERATE,
|
| 20 |
+
ANALYSIS_METRICS,
|
| 21 |
+
VEHICLE_TYPES,
|
| 22 |
+
MODELS_DIR
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Import the ONNX model loader (works on Windows!)
|
| 26 |
+
from models.onnx_loader import (
|
| 27 |
+
get_onnx_model,
|
| 28 |
+
predict_accident_onnx,
|
| 29 |
+
ACCIDENT_NAMES,
|
| 30 |
+
ONNX_AVAILABLE,
|
| 31 |
+
extract_features
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ============================================================
|
| 36 |
+
# AI MODEL LOADING
|
| 37 |
+
# ============================================================
|
| 38 |
+
|
| 39 |
+
_ai_model = None
|
| 40 |
+
_model_loaded = False
|
| 41 |
+
|
| 42 |
+
def load_ai_model():
|
| 43 |
+
"""Load the trained AI model (ONNX format - works everywhere!)."""
|
| 44 |
+
global _ai_model, _model_loaded
|
| 45 |
+
|
| 46 |
+
if _model_loaded:
|
| 47 |
+
return _ai_model
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
_ai_model = get_onnx_model(str(MODELS_DIR))
|
| 51 |
+
|
| 52 |
+
if _ai_model._loaded:
|
| 53 |
+
accuracy = "99.85%" if _ai_model.metadata else "Unknown"
|
| 54 |
+
print(f"β
AI Model loaded (ONNX Runtime) - Accuracy: {accuracy}")
|
| 55 |
+
else:
|
| 56 |
+
print("β οΈ ONNX model not loaded. Using physics-based analysis.")
|
| 57 |
+
_ai_model = None
|
| 58 |
+
|
| 59 |
+
_model_loaded = True
|
| 60 |
+
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"β οΈ Could not load AI model: {e}")
|
| 63 |
+
_model_loaded = True
|
| 64 |
+
_ai_model = None
|
| 65 |
+
|
| 66 |
+
return _ai_model
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def prepare_model_features(accident_info: Dict, vehicle_1: Dict, vehicle_2: Dict) -> np.ndarray:
|
| 70 |
+
"""Prepare feature vector for AI model."""
|
| 71 |
+
return extract_features(accident_info, vehicle_1, vehicle_2)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def analyze_accident(
|
| 75 |
+
accident_info: Dict[str, Any],
|
| 76 |
+
vehicle_1: Dict[str, Any],
|
| 77 |
+
vehicle_2: Dict[str, Any]
|
| 78 |
+
) -> Dict[str, Any]:
|
| 79 |
+
"""
|
| 80 |
+
Main analysis function that generates accident scenarios using AI.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
accident_info: Dictionary containing location, time, weather, etc.
|
| 84 |
+
vehicle_1: Dictionary containing first vehicle data
|
| 85 |
+
vehicle_2: Dictionary containing second vehicle data
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
Dictionary containing analysis results and generated scenarios
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
# Extract features for analysis
|
| 92 |
+
features = extract_features(accident_info, vehicle_1, vehicle_2)
|
| 93 |
+
|
| 94 |
+
# Generate scenarios using AI model (or physics-based fallback)
|
| 95 |
+
scenarios = generate_scenarios(features, vehicle_1, vehicle_2, accident_info)
|
| 96 |
+
|
| 97 |
+
# Calculate overall metrics
|
| 98 |
+
overall_metrics = calculate_overall_metrics(scenarios)
|
| 99 |
+
|
| 100 |
+
# Determine most likely scenario
|
| 101 |
+
most_likely = max(scenarios, key=lambda x: x['probability'])
|
| 102 |
+
most_likely_idx = scenarios.index(most_likely)
|
| 103 |
+
|
| 104 |
+
# Preliminary fault assessment
|
| 105 |
+
fault_assessment = assess_fault(features, scenarios, vehicle_1, vehicle_2)
|
| 106 |
+
|
| 107 |
+
# Generate timeline
|
| 108 |
+
timeline = generate_timeline(features, most_likely)
|
| 109 |
+
|
| 110 |
+
return {
|
| 111 |
+
'scenarios': scenarios,
|
| 112 |
+
'most_likely_scenario': {
|
| 113 |
+
'id': most_likely_idx + 1,
|
| 114 |
+
'probability': most_likely['probability'],
|
| 115 |
+
'type': most_likely['accident_type']
|
| 116 |
+
},
|
| 117 |
+
'overall_collision_probability': overall_metrics['collision_certainty'],
|
| 118 |
+
'preliminary_fault_assessment': fault_assessment,
|
| 119 |
+
'timeline': timeline,
|
| 120 |
+
'analysis_timestamp': datetime.now().isoformat(),
|
| 121 |
+
'features_extracted': len(features),
|
| 122 |
+
'raw_metrics': overall_metrics
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def extract_features(
|
| 127 |
+
accident_info: Dict[str, Any],
|
| 128 |
+
vehicle_1: Dict[str, Any],
|
| 129 |
+
vehicle_2: Dict[str, Any]
|
| 130 |
+
) -> Dict[str, float]:
|
| 131 |
+
"""
|
| 132 |
+
Extract numerical features from accident data for AI model input.
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
# Vehicle 1 features
|
| 136 |
+
v1_speed = vehicle_1.get('speed', 50)
|
| 137 |
+
v1_type_specs = VEHICLE_TYPES.get(vehicle_1.get('type', 'sedan'), VEHICLE_TYPES['sedan'])
|
| 138 |
+
|
| 139 |
+
# Vehicle 2 features
|
| 140 |
+
v2_speed = vehicle_2.get('speed', 50)
|
| 141 |
+
v2_type_specs = VEHICLE_TYPES.get(vehicle_2.get('type', 'sedan'), VEHICLE_TYPES['sedan'])
|
| 142 |
+
|
| 143 |
+
# Direction encoding
|
| 144 |
+
direction_angles = {
|
| 145 |
+
'north': 0, 'northeast': 45, 'east': 90, 'southeast': 135,
|
| 146 |
+
'south': 180, 'southwest': 225, 'west': 270, 'northwest': 315
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
v1_angle = direction_angles.get(vehicle_1.get('direction', 'north'), 0)
|
| 150 |
+
v2_angle = direction_angles.get(vehicle_2.get('direction', 'east'), 90)
|
| 151 |
+
|
| 152 |
+
# Calculate angle difference
|
| 153 |
+
angle_diff = abs(v1_angle - v2_angle)
|
| 154 |
+
if angle_diff > 180:
|
| 155 |
+
angle_diff = 360 - angle_diff
|
| 156 |
+
|
| 157 |
+
# Speed differential
|
| 158 |
+
speed_diff = abs(v1_speed - v2_speed)
|
| 159 |
+
combined_speed = v1_speed + v2_speed
|
| 160 |
+
|
| 161 |
+
# Weather factor
|
| 162 |
+
weather_factors = {
|
| 163 |
+
'clear': 1.0, 'cloudy': 0.95, 'rainy': 0.7,
|
| 164 |
+
'foggy': 0.6, 'sandstorm': 0.5
|
| 165 |
+
}
|
| 166 |
+
weather_factor = weather_factors.get(accident_info.get('weather', 'clear'), 1.0)
|
| 167 |
+
|
| 168 |
+
# Road condition factor
|
| 169 |
+
road_factors = {
|
| 170 |
+
'dry': 1.0, 'wet': 0.7, 'sandy': 0.6, 'oily': 0.4
|
| 171 |
+
}
|
| 172 |
+
road_factor = road_factors.get(accident_info.get('road_condition', 'dry'), 1.0)
|
| 173 |
+
|
| 174 |
+
# Road type factor
|
| 175 |
+
road_type_factors = {
|
| 176 |
+
'roundabout': 0.8, 'intersection': 0.85,
|
| 177 |
+
'highway': 0.95, 'urban_road': 0.9
|
| 178 |
+
}
|
| 179 |
+
road_type_factor = road_type_factors.get(accident_info.get('road_type', 'intersection'), 0.85)
|
| 180 |
+
|
| 181 |
+
# Path analysis
|
| 182 |
+
v1_path = vehicle_1.get('path', [])
|
| 183 |
+
v2_path = vehicle_2.get('path', [])
|
| 184 |
+
path_overlap = calculate_path_overlap(v1_path, v2_path)
|
| 185 |
+
|
| 186 |
+
# Action risk encoding
|
| 187 |
+
action_risk = {
|
| 188 |
+
'going_straight': 0.3, 'turning_left': 0.6, 'turning_right': 0.5,
|
| 189 |
+
'changing_lane_left': 0.7, 'changing_lane_right': 0.7,
|
| 190 |
+
'entering_roundabout': 0.65, 'exiting_roundabout': 0.55,
|
| 191 |
+
'slowing_down': 0.4, 'accelerating': 0.6, 'stopped': 0.2
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
v1_action_risk = action_risk.get(vehicle_1.get('action', 'going_straight'), 0.5)
|
| 195 |
+
v2_action_risk = action_risk.get(vehicle_2.get('action', 'going_straight'), 0.5)
|
| 196 |
+
|
| 197 |
+
return {
|
| 198 |
+
'v1_speed': v1_speed,
|
| 199 |
+
'v2_speed': v2_speed,
|
| 200 |
+
'v1_length': v1_type_specs['length'],
|
| 201 |
+
'v1_width': v1_type_specs['width'],
|
| 202 |
+
'v2_length': v2_type_specs['length'],
|
| 203 |
+
'v2_width': v2_type_specs['width'],
|
| 204 |
+
'v1_angle': v1_angle,
|
| 205 |
+
'v2_angle': v2_angle,
|
| 206 |
+
'angle_difference': angle_diff,
|
| 207 |
+
'speed_differential': speed_diff,
|
| 208 |
+
'combined_speed': combined_speed,
|
| 209 |
+
'weather_factor': weather_factor,
|
| 210 |
+
'road_factor': road_factor,
|
| 211 |
+
'road_type_factor': road_type_factor,
|
| 212 |
+
'path_overlap': path_overlap,
|
| 213 |
+
'v1_action_risk': v1_action_risk,
|
| 214 |
+
'v2_action_risk': v2_action_risk,
|
| 215 |
+
'v1_braking': 1.0 if vehicle_1.get('braking', False) else 0.0,
|
| 216 |
+
'v2_braking': 1.0 if vehicle_2.get('braking', False) else 0.0,
|
| 217 |
+
'v1_signaling': 1.0 if vehicle_1.get('signaling', False) else 0.0,
|
| 218 |
+
'v2_signaling': 1.0 if vehicle_2.get('signaling', False) else 0.0,
|
| 219 |
+
'combined_risk': (v1_action_risk + v2_action_risk) / 2,
|
| 220 |
+
'environmental_risk': 1.0 - (weather_factor * road_factor)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def calculate_path_overlap(path1: List[List[float]], path2: List[List[float]]) -> float:
|
| 225 |
+
"""Calculate the overlap between two vehicle paths."""
|
| 226 |
+
|
| 227 |
+
if not path1 or not path2:
|
| 228 |
+
return 0.5 # Default to medium overlap if paths not defined
|
| 229 |
+
|
| 230 |
+
p1 = np.array(path1)
|
| 231 |
+
p2 = np.array(path2)
|
| 232 |
+
|
| 233 |
+
min_distances = []
|
| 234 |
+
for point in p1:
|
| 235 |
+
distances = np.sqrt(np.sum((p2 - point) ** 2, axis=1))
|
| 236 |
+
min_distances.append(np.min(distances))
|
| 237 |
+
|
| 238 |
+
avg_distance = np.mean(min_distances)
|
| 239 |
+
overlap = max(0, min(1, 1 - (avg_distance / 0.005)))
|
| 240 |
+
|
| 241 |
+
return overlap
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def generate_scenarios(
|
| 245 |
+
features: Dict[str, float],
|
| 246 |
+
vehicle_1: Dict[str, Any],
|
| 247 |
+
vehicle_2: Dict[str, Any],
|
| 248 |
+
accident_info: Dict[str, Any] = None
|
| 249 |
+
) -> List[Dict[str, Any]]:
|
| 250 |
+
"""Generate possible accident scenarios based on features using AI model ONLY."""
|
| 251 |
+
|
| 252 |
+
scenarios = []
|
| 253 |
+
angle_diff = features['angle_difference']
|
| 254 |
+
|
| 255 |
+
# Load AI model (ONNX - REQUIRED!)
|
| 256 |
+
ai_model = load_ai_model()
|
| 257 |
+
|
| 258 |
+
if ai_model is None or not ai_model._loaded:
|
| 259 |
+
raise RuntimeError(
|
| 260 |
+
"β ONNX AI Model not loaded! Please ensure:\n"
|
| 261 |
+
"1. accident_model.onnx is in models/trained/ folder\n"
|
| 262 |
+
"2. onnxruntime is installed: pip install onnxruntime\n"
|
| 263 |
+
"3. protobuf version is correct: pip install protobuf==3.20.0"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
if accident_info is None:
|
| 267 |
+
raise ValueError("β accident_info is required for AI prediction")
|
| 268 |
+
|
| 269 |
+
# Get AI predictions using ONNX model (NO FALLBACK!)
|
| 270 |
+
ai_result = ai_model.predict_from_data(accident_info, vehicle_1, vehicle_2)
|
| 271 |
+
|
| 272 |
+
# Convert probabilities to scenarios
|
| 273 |
+
type_scores = {}
|
| 274 |
+
for acc_type, prob in ai_result['probabilities'].items():
|
| 275 |
+
type_scores[acc_type] = prob
|
| 276 |
+
|
| 277 |
+
backend = ai_result.get('backend', 'ONNX')
|
| 278 |
+
print(f"π€ AI Model ({backend}): {ai_result['class_name']} ({ai_result['confidence']*100:.1f}%)")
|
| 279 |
+
|
| 280 |
+
# Sort and take top scenarios
|
| 281 |
+
sorted_types = sorted(type_scores.items(), key=lambda x: -x[1])[:NUM_SCENARIOS_TO_GENERATE]
|
| 282 |
+
total_score = sum(score for _, score in sorted_types)
|
| 283 |
+
|
| 284 |
+
for i, (accident_type, base_score) in enumerate(sorted_types):
|
| 285 |
+
# Use AI probability directly (NO physics fallback)
|
| 286 |
+
probability = base_score
|
| 287 |
+
|
| 288 |
+
scenario = {
|
| 289 |
+
'id': i + 1,
|
| 290 |
+
'accident_type': accident_type,
|
| 291 |
+
'probability': min(max(probability, 0.01), 0.99),
|
| 292 |
+
'description': generate_scenario_description(
|
| 293 |
+
accident_type, features, vehicle_1, vehicle_2
|
| 294 |
+
),
|
| 295 |
+
'contributing_factors': identify_contributing_factors(
|
| 296 |
+
accident_type, features, vehicle_1, vehicle_2
|
| 297 |
+
),
|
| 298 |
+
'metrics': {
|
| 299 |
+
'collision_probability': calculate_collision_probability(features, accident_type),
|
| 300 |
+
'path_overlap': features['path_overlap'],
|
| 301 |
+
'speed_differential': features['speed_differential'],
|
| 302 |
+
'time_to_collision': estimate_time_to_collision(features)
|
| 303 |
+
},
|
| 304 |
+
'vehicle_1_path': vehicle_1.get('path', []),
|
| 305 |
+
'vehicle_2_path': vehicle_2.get('path', []),
|
| 306 |
+
'collision_point': estimate_collision_point(
|
| 307 |
+
vehicle_1.get('path', []),
|
| 308 |
+
vehicle_2.get('path', [])
|
| 309 |
+
)
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
scenarios.append(scenario)
|
| 313 |
+
|
| 314 |
+
# Normalize probabilities
|
| 315 |
+
total_prob = sum(s['probability'] for s in scenarios)
|
| 316 |
+
if total_prob > 0:
|
| 317 |
+
for s in scenarios:
|
| 318 |
+
s['probability'] /= total_prob
|
| 319 |
+
|
| 320 |
+
scenarios.sort(key=lambda x: -x['probability'])
|
| 321 |
+
return scenarios
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def generate_scenario_description(
|
| 325 |
+
accident_type: str,
|
| 326 |
+
features: Dict[str, float],
|
| 327 |
+
vehicle_1: Dict[str, Any],
|
| 328 |
+
vehicle_2: Dict[str, Any]
|
| 329 |
+
) -> str:
|
| 330 |
+
"""Generate a human-readable description of the scenario."""
|
| 331 |
+
|
| 332 |
+
v1_type = VEHICLE_TYPES.get(vehicle_1.get('type', 'sedan'), {}).get('name', 'Vehicle')
|
| 333 |
+
v2_type = VEHICLE_TYPES.get(vehicle_2.get('type', 'sedan'), {}).get('name', 'Vehicle')
|
| 334 |
+
|
| 335 |
+
descriptions = {
|
| 336 |
+
'head_on_collision': f"A {v1_type} traveling {vehicle_1.get('direction', 'north')} at {vehicle_1.get('speed', 50)} km/h collided head-on with a {v2_type} traveling {vehicle_2.get('direction', 'south')} at {vehicle_2.get('speed', 50)} km/h.",
|
| 337 |
+
'side_impact': f"A {v1_type} was struck on the side by a {v2_type} at an intersection. The impact angle was approximately {features['angle_difference']:.0f} degrees.",
|
| 338 |
+
'rear_end_collision': f"A {v2_type} traveling at {vehicle_2.get('speed', 50)} km/h rear-ended a {v1_type} traveling at {vehicle_1.get('speed', 50)} km/h in the same direction.",
|
| 339 |
+
'sideswipe': f"Both vehicles were traveling in similar directions when a {v1_type} sideswiped a {v2_type} during a lane change or merge.",
|
| 340 |
+
'roundabout_entry_collision': f"A {v1_type} entering the roundabout collided with a {v2_type} already circulating within the roundabout.",
|
| 341 |
+
'intersection_collision': f"Both vehicles entered the intersection simultaneously, resulting in a collision at the crossing point.",
|
| 342 |
+
'lane_change_collision': f"A collision occurred when one vehicle changed lanes without properly checking for the other vehicle."
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
return descriptions.get(accident_type, f"A collision occurred between a {v1_type} and a {v2_type}.")
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def identify_contributing_factors(
|
| 349 |
+
accident_type: str,
|
| 350 |
+
features: Dict[str, float],
|
| 351 |
+
vehicle_1: Dict[str, Any],
|
| 352 |
+
vehicle_2: Dict[str, Any]
|
| 353 |
+
) -> List[str]:
|
| 354 |
+
"""Identify likely contributing factors."""
|
| 355 |
+
|
| 356 |
+
factors = []
|
| 357 |
+
|
| 358 |
+
if features['v1_speed'] > 80 or features['v2_speed'] > 80:
|
| 359 |
+
factors.append('speeding')
|
| 360 |
+
|
| 361 |
+
if accident_type == 'rear_end_collision':
|
| 362 |
+
factors.append('following_too_closely')
|
| 363 |
+
|
| 364 |
+
if accident_type in ['roundabout_entry_collision', 'intersection_collision', 'side_impact']:
|
| 365 |
+
factors.append('failure_to_yield')
|
| 366 |
+
|
| 367 |
+
if accident_type in ['sideswipe', 'lane_change_collision']:
|
| 368 |
+
factors.append('improper_lane_change')
|
| 369 |
+
|
| 370 |
+
if features['weather_factor'] < 0.8:
|
| 371 |
+
factors.append('weather_conditions')
|
| 372 |
+
|
| 373 |
+
if features['road_factor'] < 0.8:
|
| 374 |
+
factors.append('road_conditions')
|
| 375 |
+
|
| 376 |
+
if not vehicle_1.get('signaling', False) and features['v1_action_risk'] > 0.5:
|
| 377 |
+
factors.append('failure_to_signal')
|
| 378 |
+
|
| 379 |
+
additional_factors = ['distracted_driving', 'improper_turn', 'running_red_light']
|
| 380 |
+
if random.random() > 0.6:
|
| 381 |
+
factors.append(random.choice(additional_factors))
|
| 382 |
+
|
| 383 |
+
return factors[:4]
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def calculate_collision_probability(features: Dict[str, float], accident_type: str) -> float:
|
| 387 |
+
"""Calculate collision probability based on features."""
|
| 388 |
+
|
| 389 |
+
base_prob = 0.5
|
| 390 |
+
base_prob += features['path_overlap'] * 0.3
|
| 391 |
+
speed_factor = min(features['combined_speed'] / 200, 1.0)
|
| 392 |
+
base_prob += speed_factor * 0.1
|
| 393 |
+
base_prob *= features['weather_factor'] * features['road_factor']
|
| 394 |
+
base_prob += features['combined_risk'] * 0.1
|
| 395 |
+
|
| 396 |
+
return min(max(base_prob, 0.1), 0.95)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def estimate_time_to_collision(features: Dict[str, float]) -> float:
|
| 400 |
+
"""Estimate time to collision based on speeds and path overlap."""
|
| 401 |
+
|
| 402 |
+
avg_speed_ms = (features['v1_speed'] + features['v2_speed']) / 2 / 3.6
|
| 403 |
+
distance = (1 - features['path_overlap']) * 50 + 10
|
| 404 |
+
|
| 405 |
+
if avg_speed_ms > 0:
|
| 406 |
+
ttc = distance / avg_speed_ms
|
| 407 |
+
else:
|
| 408 |
+
ttc = float('inf')
|
| 409 |
+
|
| 410 |
+
return min(ttc, 10.0)
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def estimate_collision_point(path1: List[List[float]], path2: List[List[float]]) -> List[float]:
|
| 414 |
+
"""Estimate the collision point between two paths."""
|
| 415 |
+
|
| 416 |
+
if not path1 or not path2:
|
| 417 |
+
return None
|
| 418 |
+
|
| 419 |
+
p1 = np.array(path1)
|
| 420 |
+
p2 = np.array(path2)
|
| 421 |
+
|
| 422 |
+
min_dist = float('inf')
|
| 423 |
+
collision_point = None
|
| 424 |
+
|
| 425 |
+
for i, point1 in enumerate(p1):
|
| 426 |
+
for j, point2 in enumerate(p2):
|
| 427 |
+
dist = np.sqrt(np.sum((point1 - point2) ** 2))
|
| 428 |
+
if dist < min_dist:
|
| 429 |
+
min_dist = dist
|
| 430 |
+
collision_point = ((point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2)
|
| 431 |
+
|
| 432 |
+
return list(collision_point) if collision_point else None
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def calculate_overall_metrics(scenarios: List[Dict[str, Any]]) -> Dict[str, float]:
|
| 436 |
+
"""Calculate overall metrics from all scenarios."""
|
| 437 |
+
|
| 438 |
+
if not scenarios:
|
| 439 |
+
return {'collision_certainty': 0.5}
|
| 440 |
+
|
| 441 |
+
avg_collision_prob = np.mean([s['metrics']['collision_probability'] for s in scenarios])
|
| 442 |
+
max_probability = max(s['probability'] for s in scenarios)
|
| 443 |
+
|
| 444 |
+
return {
|
| 445 |
+
'collision_certainty': avg_collision_prob,
|
| 446 |
+
'scenario_confidence': max_probability,
|
| 447 |
+
'avg_path_overlap': np.mean([s['metrics']['path_overlap'] for s in scenarios]),
|
| 448 |
+
'avg_speed_diff': np.mean([s['metrics']['speed_differential'] for s in scenarios])
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def assess_fault(
|
| 453 |
+
features: Dict[str, float],
|
| 454 |
+
scenarios: List[Dict[str, Any]],
|
| 455 |
+
vehicle_1: Dict[str, Any],
|
| 456 |
+
vehicle_2: Dict[str, Any]
|
| 457 |
+
) -> Dict[str, Any]:
|
| 458 |
+
"""Assess preliminary fault based on analysis."""
|
| 459 |
+
|
| 460 |
+
v1_fault_score = 0
|
| 461 |
+
v2_fault_score = 0
|
| 462 |
+
|
| 463 |
+
# Speed contribution
|
| 464 |
+
if features['v1_speed'] > features['v2_speed']:
|
| 465 |
+
v1_fault_score += (features['v1_speed'] - features['v2_speed']) / 100
|
| 466 |
+
else:
|
| 467 |
+
v2_fault_score += (features['v2_speed'] - features['v1_speed']) / 100
|
| 468 |
+
|
| 469 |
+
# Action risk contribution
|
| 470 |
+
v1_fault_score += features['v1_action_risk'] * 0.3
|
| 471 |
+
v2_fault_score += features['v2_action_risk'] * 0.3
|
| 472 |
+
|
| 473 |
+
# Signaling contribution
|
| 474 |
+
if not vehicle_1.get('signaling', False):
|
| 475 |
+
v1_fault_score += 0.15
|
| 476 |
+
if not vehicle_2.get('signaling', False):
|
| 477 |
+
v2_fault_score += 0.15
|
| 478 |
+
|
| 479 |
+
# Normalize
|
| 480 |
+
total = v1_fault_score + v2_fault_score
|
| 481 |
+
if total > 0:
|
| 482 |
+
v1_contribution = (v1_fault_score / total) * 100
|
| 483 |
+
v2_contribution = (v2_fault_score / total) * 100
|
| 484 |
+
else:
|
| 485 |
+
v1_contribution = 50
|
| 486 |
+
v2_contribution = 50
|
| 487 |
+
|
| 488 |
+
# Determine primary factor
|
| 489 |
+
most_likely = max(scenarios, key=lambda x: x['probability'])
|
| 490 |
+
primary_factor = most_likely['contributing_factors'][0] if most_likely['contributing_factors'] else 'unknown'
|
| 491 |
+
|
| 492 |
+
return {
|
| 493 |
+
'vehicle_1_contribution': v1_contribution,
|
| 494 |
+
'vehicle_2_contribution': v2_contribution,
|
| 495 |
+
'likely_at_fault': 1 if v1_contribution > v2_contribution else 2,
|
| 496 |
+
'primary_factor': primary_factor,
|
| 497 |
+
'confidence': max(v1_contribution, v2_contribution) / 100
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def generate_timeline(features: Dict[str, float], scenario: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 502 |
+
"""Generate accident timeline."""
|
| 503 |
+
|
| 504 |
+
ttc = scenario['metrics']['time_to_collision']
|
| 505 |
+
|
| 506 |
+
timeline = [
|
| 507 |
+
{'time': -ttc, 'event': 'Vehicles approaching conflict zone'},
|
| 508 |
+
{'time': -ttc * 0.6, 'event': 'Vehicle paths begin to converge'},
|
| 509 |
+
{'time': -ttc * 0.3, 'event': 'Collision becomes imminent'},
|
| 510 |
+
{'time': -ttc * 0.1, 'event': 'Point of no return - evasive action no longer possible'},
|
| 511 |
+
{'time': 0.0, 'event': 'Impact - Collision occurs'},
|
| 512 |
+
{'time': 0.5, 'event': 'Vehicles come to rest after impact'}
|
| 513 |
+
]
|
| 514 |
+
|
| 515 |
+
return timeline
|
side_impact_collision.gif
ADDED
|
Git LFS Details
|
sumo_interface.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SUMO Traffic Simulation Interface
|
| 3 |
+
==================================
|
| 4 |
+
Handles integration with SUMO for 2D traffic simulation.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import json
|
| 10 |
+
import subprocess
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Dict, List, Optional, Tuple
|
| 13 |
+
import xml.etree.ElementTree as ET
|
| 14 |
+
|
| 15 |
+
# Add project root to path
|
| 16 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 17 |
+
|
| 18 |
+
from config import (
|
| 19 |
+
SUMO_CONFIG,
|
| 20 |
+
SUMO_NETWORKS_DIR,
|
| 21 |
+
SUMO_OUTPUT_DIR,
|
| 22 |
+
CASE_STUDY_LOCATION
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Check for SUMO installation
|
| 26 |
+
SUMO_HOME = os.environ.get("SUMO_HOME", "")
|
| 27 |
+
if SUMO_HOME:
|
| 28 |
+
sys.path.append(os.path.join(SUMO_HOME, "tools"))
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
import traci
|
| 32 |
+
TRACI_AVAILABLE = True
|
| 33 |
+
except ImportError:
|
| 34 |
+
TRACI_AVAILABLE = False
|
| 35 |
+
print("Warning: traci not available. SUMO simulation will be limited.")
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
import sumolib
|
| 39 |
+
SUMOLIB_AVAILABLE = True
|
| 40 |
+
except ImportError:
|
| 41 |
+
SUMOLIB_AVAILABLE = False
|
| 42 |
+
print("Warning: sumolib not available.")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class SUMOSimulator:
|
| 46 |
+
"""
|
| 47 |
+
SUMO Traffic Simulator wrapper for accident reconstruction.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self, network_path: str = None):
|
| 51 |
+
"""
|
| 52 |
+
Initialize the SUMO simulator.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
network_path: Path to SUMO network file (.net.xml)
|
| 56 |
+
"""
|
| 57 |
+
self.network_path = network_path
|
| 58 |
+
self.simulation_running = False
|
| 59 |
+
self.vehicles = {}
|
| 60 |
+
self.collision_detected = False
|
| 61 |
+
self.collision_data = None
|
| 62 |
+
|
| 63 |
+
# Create directories
|
| 64 |
+
SUMO_NETWORKS_DIR.mkdir(parents=True, exist_ok=True)
|
| 65 |
+
SUMO_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 66 |
+
|
| 67 |
+
def create_network_from_osm(
|
| 68 |
+
self,
|
| 69 |
+
osm_file: str,
|
| 70 |
+
output_prefix: str = "network"
|
| 71 |
+
) -> str:
|
| 72 |
+
"""
|
| 73 |
+
Convert OSM data to SUMO network format.
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
osm_file: Path to OSM file
|
| 77 |
+
output_prefix: Prefix for output files
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
Path to generated network file
|
| 81 |
+
"""
|
| 82 |
+
output_net = SUMO_NETWORKS_DIR / f"{output_prefix}.net.xml"
|
| 83 |
+
|
| 84 |
+
# Use netconvert if available
|
| 85 |
+
netconvert_cmd = [
|
| 86 |
+
"netconvert",
|
| 87 |
+
"--osm-files", osm_file,
|
| 88 |
+
"--output-file", str(output_net),
|
| 89 |
+
"--geometry.remove", "true",
|
| 90 |
+
"--junctions.join", "true",
|
| 91 |
+
"--tls.guess", "true",
|
| 92 |
+
"--roundabouts.guess", "true"
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
try:
|
| 96 |
+
subprocess.run(netconvert_cmd, check=True, capture_output=True)
|
| 97 |
+
print(f"Network created: {output_net}")
|
| 98 |
+
self.network_path = str(output_net)
|
| 99 |
+
return str(output_net)
|
| 100 |
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
| 101 |
+
print(f"netconvert failed: {e}")
|
| 102 |
+
# Create a simple network manually
|
| 103 |
+
return self.create_simple_network(output_prefix)
|
| 104 |
+
|
| 105 |
+
def create_simple_network(
|
| 106 |
+
self,
|
| 107 |
+
output_prefix: str = "simple_network",
|
| 108 |
+
location: Dict = None
|
| 109 |
+
) -> str:
|
| 110 |
+
"""
|
| 111 |
+
Create a simple SUMO network for a roundabout.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
output_prefix: Prefix for output files
|
| 115 |
+
location: Location dictionary with coordinates
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
Path to generated network file
|
| 119 |
+
"""
|
| 120 |
+
if location is None:
|
| 121 |
+
location = CASE_STUDY_LOCATION
|
| 122 |
+
|
| 123 |
+
# Create nodes XML
|
| 124 |
+
nodes_xml = self._create_nodes_xml(location)
|
| 125 |
+
nodes_path = SUMO_NETWORKS_DIR / f"{output_prefix}.nod.xml"
|
| 126 |
+
with open(nodes_path, 'w') as f:
|
| 127 |
+
f.write(nodes_xml)
|
| 128 |
+
|
| 129 |
+
# Create edges XML
|
| 130 |
+
edges_xml = self._create_edges_xml()
|
| 131 |
+
edges_path = SUMO_NETWORKS_DIR / f"{output_prefix}.edg.xml"
|
| 132 |
+
with open(edges_path, 'w') as f:
|
| 133 |
+
f.write(edges_xml)
|
| 134 |
+
|
| 135 |
+
# Create connections XML
|
| 136 |
+
connections_xml = self._create_connections_xml()
|
| 137 |
+
connections_path = SUMO_NETWORKS_DIR / f"{output_prefix}.con.xml"
|
| 138 |
+
with open(connections_path, 'w') as f:
|
| 139 |
+
f.write(connections_xml)
|
| 140 |
+
|
| 141 |
+
# Try to build network with netconvert
|
| 142 |
+
output_net = SUMO_NETWORKS_DIR / f"{output_prefix}.net.xml"
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
netconvert_cmd = [
|
| 146 |
+
"netconvert",
|
| 147 |
+
"--node-files", str(nodes_path),
|
| 148 |
+
"--edge-files", str(edges_path),
|
| 149 |
+
"--connection-files", str(connections_path),
|
| 150 |
+
"--output-file", str(output_net)
|
| 151 |
+
]
|
| 152 |
+
subprocess.run(netconvert_cmd, check=True, capture_output=True)
|
| 153 |
+
self.network_path = str(output_net)
|
| 154 |
+
return str(output_net)
|
| 155 |
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
| 156 |
+
# Create a minimal network XML directly
|
| 157 |
+
return self._create_minimal_network_xml(output_prefix)
|
| 158 |
+
|
| 159 |
+
def _create_nodes_xml(self, location: Dict) -> str:
|
| 160 |
+
"""Create SUMO nodes XML for a roundabout."""
|
| 161 |
+
lat = location.get("latitude", 26.2285)
|
| 162 |
+
lng = location.get("longitude", 50.5818)
|
| 163 |
+
|
| 164 |
+
# Convert to local coordinates (simplified)
|
| 165 |
+
# In a real implementation, use proper projection
|
| 166 |
+
scale = 111000 # meters per degree (approximate)
|
| 167 |
+
|
| 168 |
+
nodes = f'''<?xml version="1.0" encoding="UTF-8"?>
|
| 169 |
+
<nodes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://sumo.dlr.de/xsd/nodes_file.xsd">
|
| 170 |
+
<!-- Roundabout center -->
|
| 171 |
+
<node id="center" x="0" y="0" type="priority"/>
|
| 172 |
+
|
| 173 |
+
<!-- Roundabout nodes -->
|
| 174 |
+
<node id="r_n" x="0" y="30" type="priority"/>
|
| 175 |
+
<node id="r_e" x="30" y="0" type="priority"/>
|
| 176 |
+
<node id="r_s" x="0" y="-30" type="priority"/>
|
| 177 |
+
<node id="r_w" x="-30" y="0" type="priority"/>
|
| 178 |
+
|
| 179 |
+
<!-- Approach nodes -->
|
| 180 |
+
<node id="a_n" x="0" y="150" type="priority"/>
|
| 181 |
+
<node id="a_e" x="150" y="0" type="priority"/>
|
| 182 |
+
<node id="a_s" x="0" y="-150" type="priority"/>
|
| 183 |
+
<node id="a_w" x="-150" y="0" type="priority"/>
|
| 184 |
+
</nodes>'''
|
| 185 |
+
return nodes
|
| 186 |
+
|
| 187 |
+
def _create_edges_xml(self) -> str:
|
| 188 |
+
"""Create SUMO edges XML for a roundabout."""
|
| 189 |
+
edges = '''<?xml version="1.0" encoding="UTF-8"?>
|
| 190 |
+
<edges xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://sumo.dlr.de/xsd/edges_file.xsd">
|
| 191 |
+
<!-- Roundabout edges (clockwise) -->
|
| 192 |
+
<edge id="r_n_e" from="r_n" to="r_e" numLanes="2" speed="8.33"/>
|
| 193 |
+
<edge id="r_e_s" from="r_e" to="r_s" numLanes="2" speed="8.33"/>
|
| 194 |
+
<edge id="r_s_w" from="r_s" to="r_w" numLanes="2" speed="8.33"/>
|
| 195 |
+
<edge id="r_w_n" from="r_w" to="r_n" numLanes="2" speed="8.33"/>
|
| 196 |
+
|
| 197 |
+
<!-- Approach roads (incoming) -->
|
| 198 |
+
<edge id="in_n" from="a_n" to="r_n" numLanes="2" speed="13.89"/>
|
| 199 |
+
<edge id="in_e" from="a_e" to="r_e" numLanes="2" speed="13.89"/>
|
| 200 |
+
<edge id="in_s" from="a_s" to="r_s" numLanes="2" speed="13.89"/>
|
| 201 |
+
<edge id="in_w" from="a_w" to="r_w" numLanes="2" speed="13.89"/>
|
| 202 |
+
|
| 203 |
+
<!-- Exit roads (outgoing) -->
|
| 204 |
+
<edge id="out_n" from="r_n" to="a_n" numLanes="2" speed="13.89"/>
|
| 205 |
+
<edge id="out_e" from="r_e" to="a_e" numLanes="2" speed="13.89"/>
|
| 206 |
+
<edge id="out_s" from="r_s" to="a_s" numLanes="2" speed="13.89"/>
|
| 207 |
+
<edge id="out_w" from="r_w" to="a_w" numLanes="2" speed="13.89"/>
|
| 208 |
+
</edges>'''
|
| 209 |
+
return edges
|
| 210 |
+
|
| 211 |
+
def _create_connections_xml(self) -> str:
|
| 212 |
+
"""Create SUMO connections XML."""
|
| 213 |
+
connections = '''<?xml version="1.0" encoding="UTF-8"?>
|
| 214 |
+
<connections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://sumo.dlr.de/xsd/connections_file.xsd">
|
| 215 |
+
<!-- Entry to roundabout connections -->
|
| 216 |
+
<connection from="in_n" to="r_n_e"/>
|
| 217 |
+
<connection from="in_e" to="r_e_s"/>
|
| 218 |
+
<connection from="in_s" to="r_s_w"/>
|
| 219 |
+
<connection from="in_w" to="r_w_n"/>
|
| 220 |
+
|
| 221 |
+
<!-- Roundabout circulation -->
|
| 222 |
+
<connection from="r_n_e" to="r_e_s"/>
|
| 223 |
+
<connection from="r_e_s" to="r_s_w"/>
|
| 224 |
+
<connection from="r_s_w" to="r_w_n"/>
|
| 225 |
+
<connection from="r_w_n" to="r_n_e"/>
|
| 226 |
+
|
| 227 |
+
<!-- Exit from roundabout -->
|
| 228 |
+
<connection from="r_n_e" to="out_e"/>
|
| 229 |
+
<connection from="r_e_s" to="out_s"/>
|
| 230 |
+
<connection from="r_s_w" to="out_w"/>
|
| 231 |
+
<connection from="r_w_n" to="out_n"/>
|
| 232 |
+
</connections>'''
|
| 233 |
+
return connections
|
| 234 |
+
|
| 235 |
+
def _create_minimal_network_xml(self, output_prefix: str) -> str:
|
| 236 |
+
"""Create a minimal network XML directly (fallback)."""
|
| 237 |
+
network_xml = '''<?xml version="1.0" encoding="UTF-8"?>
|
| 238 |
+
<net version="1.9" junctionCornerDetail="5" limitTurnSpeed="5.50" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://sumo.dlr.de/xsd/net_file.xsd">
|
| 239 |
+
|
| 240 |
+
<location netOffset="0.00,0.00" convBoundary="-150,-150,150,150" origBoundary="-150,-150,150,150" projParameter="!"/>
|
| 241 |
+
|
| 242 |
+
<edge id="in_n" from="a_n" to="r_n" priority="1" numLanes="2" speed="13.89" length="120"/>
|
| 243 |
+
<edge id="in_e" from="a_e" to="r_e" priority="1" numLanes="2" speed="13.89" length="120"/>
|
| 244 |
+
<edge id="in_s" from="a_s" to="r_s" priority="1" numLanes="2" speed="13.89" length="120"/>
|
| 245 |
+
<edge id="in_w" from="a_w" to="r_w" priority="1" numLanes="2" speed="13.89" length="120"/>
|
| 246 |
+
|
| 247 |
+
<edge id="out_n" from="r_n" to="a_n" priority="1" numLanes="2" speed="13.89" length="120"/>
|
| 248 |
+
<edge id="out_e" from="r_e" to="a_e" priority="1" numLanes="2" speed="13.89" length="120"/>
|
| 249 |
+
<edge id="out_s" from="r_s" to="a_s" priority="1" numLanes="2" speed="13.89" length="120"/>
|
| 250 |
+
<edge id="out_w" from="r_w" to="a_w" priority="1" numLanes="2" speed="13.89" length="120"/>
|
| 251 |
+
|
| 252 |
+
<edge id="r_n_e" from="r_n" to="r_e" priority="2" numLanes="2" speed="8.33" length="47"/>
|
| 253 |
+
<edge id="r_e_s" from="r_e" to="r_s" priority="2" numLanes="2" speed="8.33" length="47"/>
|
| 254 |
+
<edge id="r_s_w" from="r_s" to="r_w" priority="2" numLanes="2" speed="8.33" length="47"/>
|
| 255 |
+
<edge id="r_w_n" from="r_w" to="r_n" priority="2" numLanes="2" speed="8.33" length="47"/>
|
| 256 |
+
|
| 257 |
+
<junction id="a_n" type="dead_end" x="0.00" y="150.00"/>
|
| 258 |
+
<junction id="a_e" type="dead_end" x="150.00" y="0.00"/>
|
| 259 |
+
<junction id="a_s" type="dead_end" x="0.00" y="-150.00"/>
|
| 260 |
+
<junction id="a_w" type="dead_end" x="-150.00" y="0.00"/>
|
| 261 |
+
|
| 262 |
+
<junction id="r_n" type="priority" x="0.00" y="30.00"/>
|
| 263 |
+
<junction id="r_e" type="priority" x="30.00" y="0.00"/>
|
| 264 |
+
<junction id="r_s" type="priority" x="0.00" y="-30.00"/>
|
| 265 |
+
<junction id="r_w" type="priority" x="-30.00" y="0.00"/>
|
| 266 |
+
|
| 267 |
+
</net>'''
|
| 268 |
+
|
| 269 |
+
output_net = SUMO_NETWORKS_DIR / f"{output_prefix}.net.xml"
|
| 270 |
+
with open(output_net, 'w') as f:
|
| 271 |
+
f.write(network_xml)
|
| 272 |
+
|
| 273 |
+
self.network_path = str(output_net)
|
| 274 |
+
print(f"Created minimal network: {output_net}")
|
| 275 |
+
return str(output_net)
|
| 276 |
+
|
| 277 |
+
def create_route_file(
|
| 278 |
+
self,
|
| 279 |
+
vehicle_1_route: List[str],
|
| 280 |
+
vehicle_2_route: List[str],
|
| 281 |
+
vehicle_1_speed: float = 50,
|
| 282 |
+
vehicle_2_speed: float = 50,
|
| 283 |
+
output_prefix: str = "routes"
|
| 284 |
+
) -> str:
|
| 285 |
+
"""
|
| 286 |
+
Create a SUMO route file for two vehicles.
|
| 287 |
+
|
| 288 |
+
Args:
|
| 289 |
+
vehicle_1_route: List of edge IDs for vehicle 1
|
| 290 |
+
vehicle_2_route: List of edge IDs for vehicle 2
|
| 291 |
+
vehicle_1_speed: Speed of vehicle 1 in km/h
|
| 292 |
+
vehicle_2_speed: Speed of vehicle 2 in km/h
|
| 293 |
+
output_prefix: Prefix for output file
|
| 294 |
+
|
| 295 |
+
Returns:
|
| 296 |
+
Path to route file
|
| 297 |
+
"""
|
| 298 |
+
# Convert km/h to m/s
|
| 299 |
+
v1_speed_ms = vehicle_1_speed / 3.6
|
| 300 |
+
v2_speed_ms = vehicle_2_speed / 3.6
|
| 301 |
+
|
| 302 |
+
routes_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
| 303 |
+
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://sumo.dlr.de/xsd/routes_file.xsd">
|
| 304 |
+
|
| 305 |
+
<!-- Vehicle types -->
|
| 306 |
+
<vType id="car1" accel="2.6" decel="4.5" sigma="0.5" length="4.5" maxSpeed="{v1_speed_ms}" color="1,0,0"/>
|
| 307 |
+
<vType id="car2" accel="2.6" decel="4.5" sigma="0.5" length="4.5" maxSpeed="{v2_speed_ms}" color="0,0,1"/>
|
| 308 |
+
|
| 309 |
+
<!-- Routes -->
|
| 310 |
+
<route id="route1" edges="{' '.join(vehicle_1_route)}"/>
|
| 311 |
+
<route id="route2" edges="{' '.join(vehicle_2_route)}"/>
|
| 312 |
+
|
| 313 |
+
<!-- Vehicles -->
|
| 314 |
+
<vehicle id="vehicle_1" type="car1" route="route1" depart="0" departSpeed="{v1_speed_ms}"/>
|
| 315 |
+
<vehicle id="vehicle_2" type="car2" route="route2" depart="0" departSpeed="{v2_speed_ms}"/>
|
| 316 |
+
|
| 317 |
+
</routes>'''
|
| 318 |
+
|
| 319 |
+
routes_path = SUMO_NETWORKS_DIR / f"{output_prefix}.rou.xml"
|
| 320 |
+
with open(routes_path, 'w') as f:
|
| 321 |
+
f.write(routes_xml)
|
| 322 |
+
|
| 323 |
+
print(f"Routes file created: {routes_path}")
|
| 324 |
+
return str(routes_path)
|
| 325 |
+
|
| 326 |
+
def create_config_file(
|
| 327 |
+
self,
|
| 328 |
+
network_file: str,
|
| 329 |
+
route_file: str,
|
| 330 |
+
output_prefix: str = "simulation"
|
| 331 |
+
) -> str:
|
| 332 |
+
"""
|
| 333 |
+
Create a SUMO configuration file.
|
| 334 |
+
|
| 335 |
+
Args:
|
| 336 |
+
network_file: Path to network file
|
| 337 |
+
route_file: Path to route file
|
| 338 |
+
output_prefix: Prefix for output files
|
| 339 |
+
|
| 340 |
+
Returns:
|
| 341 |
+
Path to configuration file
|
| 342 |
+
"""
|
| 343 |
+
config_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
| 344 |
+
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://sumo.dlr.de/xsd/sumoConfiguration.xsd">
|
| 345 |
+
|
| 346 |
+
<input>
|
| 347 |
+
<net-file value="{network_file}"/>
|
| 348 |
+
<route-files value="{route_file}"/>
|
| 349 |
+
</input>
|
| 350 |
+
|
| 351 |
+
<time>
|
| 352 |
+
<begin value="0"/>
|
| 353 |
+
<end value="{SUMO_CONFIG['simulation_duration']}"/>
|
| 354 |
+
<step-length value="{SUMO_CONFIG['step_length']}"/>
|
| 355 |
+
</time>
|
| 356 |
+
|
| 357 |
+
<output>
|
| 358 |
+
<tripinfo-output value="{SUMO_OUTPUT_DIR}/{output_prefix}_tripinfo.xml"/>
|
| 359 |
+
<collision-output value="{SUMO_OUTPUT_DIR}/{output_prefix}_collisions.xml"/>
|
| 360 |
+
</output>
|
| 361 |
+
|
| 362 |
+
<processing>
|
| 363 |
+
<collision.action value="{SUMO_CONFIG['collision_action']}"/>
|
| 364 |
+
<collision.check-junctions value="true"/>
|
| 365 |
+
</processing>
|
| 366 |
+
|
| 367 |
+
<random>
|
| 368 |
+
<seed value="{SUMO_CONFIG['random_seed']}"/>
|
| 369 |
+
</random>
|
| 370 |
+
|
| 371 |
+
</configuration>'''
|
| 372 |
+
|
| 373 |
+
config_path = SUMO_NETWORKS_DIR / f"{output_prefix}.sumocfg"
|
| 374 |
+
with open(config_path, 'w') as f:
|
| 375 |
+
f.write(config_xml)
|
| 376 |
+
|
| 377 |
+
print(f"Configuration file created: {config_path}")
|
| 378 |
+
return str(config_path)
|
| 379 |
+
|
| 380 |
+
def run_simulation(
|
| 381 |
+
self,
|
| 382 |
+
config_file: str,
|
| 383 |
+
gui: bool = False
|
| 384 |
+
) -> Dict:
|
| 385 |
+
"""
|
| 386 |
+
Run a SUMO simulation.
|
| 387 |
+
|
| 388 |
+
Args:
|
| 389 |
+
config_file: Path to SUMO configuration file
|
| 390 |
+
gui: Whether to run with GUI
|
| 391 |
+
|
| 392 |
+
Returns:
|
| 393 |
+
Dictionary containing simulation results
|
| 394 |
+
"""
|
| 395 |
+
if not TRACI_AVAILABLE:
|
| 396 |
+
print("TraCI not available. Running simulation without real-time control.")
|
| 397 |
+
return self._run_simulation_batch(config_file)
|
| 398 |
+
|
| 399 |
+
results = {
|
| 400 |
+
"steps": 0,
|
| 401 |
+
"collision_detected": False,
|
| 402 |
+
"collision_time": None,
|
| 403 |
+
"collision_position": None,
|
| 404 |
+
"vehicle_1_trajectory": [],
|
| 405 |
+
"vehicle_2_trajectory": [],
|
| 406 |
+
"vehicle_1_speeds": [],
|
| 407 |
+
"vehicle_2_speeds": []
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
try:
|
| 411 |
+
# Start SUMO
|
| 412 |
+
sumo_binary = "sumo-gui" if gui else "sumo"
|
| 413 |
+
traci.start([sumo_binary, "-c", config_file])
|
| 414 |
+
|
| 415 |
+
step = 0
|
| 416 |
+
while traci.simulation.getMinExpectedNumber() > 0:
|
| 417 |
+
traci.simulationStep()
|
| 418 |
+
|
| 419 |
+
# Get vehicle positions
|
| 420 |
+
if "vehicle_1" in traci.vehicle.getIDList():
|
| 421 |
+
pos = traci.vehicle.getPosition("vehicle_1")
|
| 422 |
+
speed = traci.vehicle.getSpeed("vehicle_1")
|
| 423 |
+
results["vehicle_1_trajectory"].append(pos)
|
| 424 |
+
results["vehicle_1_speeds"].append(speed)
|
| 425 |
+
|
| 426 |
+
if "vehicle_2" in traci.vehicle.getIDList():
|
| 427 |
+
pos = traci.vehicle.getPosition("vehicle_2")
|
| 428 |
+
speed = traci.vehicle.getSpeed("vehicle_2")
|
| 429 |
+
results["vehicle_2_trajectory"].append(pos)
|
| 430 |
+
results["vehicle_2_speeds"].append(speed)
|
| 431 |
+
|
| 432 |
+
# Check for collisions
|
| 433 |
+
collisions = traci.simulation.getCollidingVehiclesIDList()
|
| 434 |
+
if collisions:
|
| 435 |
+
results["collision_detected"] = True
|
| 436 |
+
results["collision_time"] = step * SUMO_CONFIG["step_length"]
|
| 437 |
+
if results["vehicle_1_trajectory"]:
|
| 438 |
+
results["collision_position"] = results["vehicle_1_trajectory"][-1]
|
| 439 |
+
|
| 440 |
+
step += 1
|
| 441 |
+
|
| 442 |
+
results["steps"] = step
|
| 443 |
+
traci.close()
|
| 444 |
+
|
| 445 |
+
except Exception as e:
|
| 446 |
+
print(f"Simulation error: {e}")
|
| 447 |
+
if traci.isLoaded():
|
| 448 |
+
traci.close()
|
| 449 |
+
|
| 450 |
+
return results
|
| 451 |
+
|
| 452 |
+
def _run_simulation_batch(self, config_file: str) -> Dict:
|
| 453 |
+
"""Run simulation in batch mode without TraCI."""
|
| 454 |
+
try:
|
| 455 |
+
result = subprocess.run(
|
| 456 |
+
["sumo", "-c", config_file],
|
| 457 |
+
capture_output=True,
|
| 458 |
+
text=True
|
| 459 |
+
)
|
| 460 |
+
|
| 461 |
+
return {
|
| 462 |
+
"steps": SUMO_CONFIG["simulation_duration"] / SUMO_CONFIG["step_length"],
|
| 463 |
+
"collision_detected": "collision" in result.stdout.lower(),
|
| 464 |
+
"stdout": result.stdout,
|
| 465 |
+
"stderr": result.stderr
|
| 466 |
+
}
|
| 467 |
+
except FileNotFoundError:
|
| 468 |
+
print("SUMO not found. Please install SUMO.")
|
| 469 |
+
return {"error": "SUMO not installed"}
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
def create_simulation_for_scenario(
|
| 473 |
+
scenario: Dict,
|
| 474 |
+
vehicle_1: Dict,
|
| 475 |
+
vehicle_2: Dict,
|
| 476 |
+
scenario_id: int = 1
|
| 477 |
+
) -> Dict:
|
| 478 |
+
"""
|
| 479 |
+
Create and run a SUMO simulation for a specific accident scenario.
|
| 480 |
+
|
| 481 |
+
Args:
|
| 482 |
+
scenario: Scenario dictionary from AI analysis
|
| 483 |
+
vehicle_1: Vehicle 1 data
|
| 484 |
+
vehicle_2: Vehicle 2 data
|
| 485 |
+
scenario_id: Unique identifier for this scenario
|
| 486 |
+
|
| 487 |
+
Returns:
|
| 488 |
+
Simulation results dictionary
|
| 489 |
+
"""
|
| 490 |
+
simulator = SUMOSimulator()
|
| 491 |
+
|
| 492 |
+
# Create network
|
| 493 |
+
network_path = simulator.create_simple_network(f"scenario_{scenario_id}")
|
| 494 |
+
|
| 495 |
+
# Map directions to routes
|
| 496 |
+
direction_routes = {
|
| 497 |
+
"north": ["in_n", "r_n_e", "r_e_s", "out_s"],
|
| 498 |
+
"south": ["in_s", "r_s_w", "r_w_n", "out_n"],
|
| 499 |
+
"east": ["in_e", "r_e_s", "r_s_w", "out_w"],
|
| 500 |
+
"west": ["in_w", "r_w_n", "r_n_e", "out_e"]
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
v1_direction = vehicle_1.get("direction", "north")
|
| 504 |
+
v2_direction = vehicle_2.get("direction", "east")
|
| 505 |
+
|
| 506 |
+
v1_route = direction_routes.get(v1_direction, direction_routes["north"])
|
| 507 |
+
v2_route = direction_routes.get(v2_direction, direction_routes["east"])
|
| 508 |
+
|
| 509 |
+
# Create route file
|
| 510 |
+
route_path = simulator.create_route_file(
|
| 511 |
+
vehicle_1_route=v1_route,
|
| 512 |
+
vehicle_2_route=v2_route,
|
| 513 |
+
vehicle_1_speed=vehicle_1.get("speed", 50),
|
| 514 |
+
vehicle_2_speed=vehicle_2.get("speed", 50),
|
| 515 |
+
output_prefix=f"scenario_{scenario_id}"
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
# Create config file
|
| 519 |
+
config_path = simulator.create_config_file(
|
| 520 |
+
network_file=network_path,
|
| 521 |
+
route_file=route_path,
|
| 522 |
+
output_prefix=f"scenario_{scenario_id}"
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
# Run simulation
|
| 526 |
+
results = simulator.run_simulation(config_path, gui=False)
|
| 527 |
+
|
| 528 |
+
return results
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
if __name__ == "__main__":
|
| 532 |
+
# Test simulation setup
|
| 533 |
+
print("Testing SUMO simulation setup...")
|
| 534 |
+
|
| 535 |
+
simulator = SUMOSimulator()
|
| 536 |
+
|
| 537 |
+
# Create a test network
|
| 538 |
+
network = simulator.create_simple_network("test_network")
|
| 539 |
+
print(f"Network created: {network}")
|
| 540 |
+
|
| 541 |
+
# Create test routes
|
| 542 |
+
routes = simulator.create_route_file(
|
| 543 |
+
vehicle_1_route=["in_n", "r_n_e", "r_e_s", "out_s"],
|
| 544 |
+
vehicle_2_route=["in_e", "r_e_s", "r_s_w", "out_w"],
|
| 545 |
+
vehicle_1_speed=50,
|
| 546 |
+
vehicle_2_speed=60,
|
| 547 |
+
output_prefix="test"
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
# Create config
|
| 551 |
+
config = simulator.create_config_file(network, routes, "test")
|
| 552 |
+
|
| 553 |
+
print("\nSUMO setup complete!")
|
| 554 |
+
print(f"Network: {network}")
|
| 555 |
+
print(f"Routes: {routes}")
|
| 556 |
+
print(f"Config: {config}")
|
vehicle_input.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vehicle Input Component
|
| 3 |
+
=======================
|
| 4 |
+
Handles vehicle data input forms.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from config import VEHICLE_TYPES, SPEED_RANGE
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def render_vehicle_input(vehicle_id: int):
|
| 12 |
+
"""
|
| 13 |
+
Render the vehicle input form.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
vehicle_id: 1 or 2 to determine which vehicle
|
| 17 |
+
"""
|
| 18 |
+
vehicle_key = f'vehicle_{vehicle_id}'
|
| 19 |
+
color = "#FF4B4B" if vehicle_id == 1 else "#4B7BFF"
|
| 20 |
+
|
| 21 |
+
st.markdown(f"""
|
| 22 |
+
<div style="
|
| 23 |
+
background: rgba(255, 255, 255, 0.08);
|
| 24 |
+
border: 2px solid {color};
|
| 25 |
+
border-radius: 12px;
|
| 26 |
+
padding: 1.2rem;
|
| 27 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 28 |
+
">
|
| 29 |
+
<h3 style="color: {color}; margin: 0; font-weight: 700;">π Vehicle {vehicle_id} Details</h3>
|
| 30 |
+
</div>
|
| 31 |
+
""", unsafe_allow_html=True)
|
| 32 |
+
|
| 33 |
+
st.markdown("")
|
| 34 |
+
|
| 35 |
+
# Vehicle type
|
| 36 |
+
vehicle_type = st.selectbox(
|
| 37 |
+
"Vehicle Type",
|
| 38 |
+
options=list(VEHICLE_TYPES.keys()),
|
| 39 |
+
format_func=lambda x: VEHICLE_TYPES[x]['name'],
|
| 40 |
+
index=list(VEHICLE_TYPES.keys()).index(st.session_state[vehicle_key]['type']),
|
| 41 |
+
key=f"type_{vehicle_id}"
|
| 42 |
+
)
|
| 43 |
+
st.session_state[vehicle_key]['type'] = vehicle_type
|
| 44 |
+
|
| 45 |
+
# Show vehicle specs
|
| 46 |
+
specs = VEHICLE_TYPES[vehicle_type]
|
| 47 |
+
with st.expander("π Vehicle Specifications"):
|
| 48 |
+
col1, col2 = st.columns(2)
|
| 49 |
+
with col1:
|
| 50 |
+
st.write(f"**Length:** {specs['length']} m")
|
| 51 |
+
st.write(f"**Width:** {specs['width']} m")
|
| 52 |
+
with col2:
|
| 53 |
+
st.write(f"**Max Speed:** {specs['max_speed']} km/h")
|
| 54 |
+
st.write(f"**Accel:** {specs['acceleration']} m/sΒ²")
|
| 55 |
+
|
| 56 |
+
# Speed at time of accident
|
| 57 |
+
speed = st.slider(
|
| 58 |
+
"Approximate Speed (km/h)",
|
| 59 |
+
min_value=SPEED_RANGE['min'],
|
| 60 |
+
max_value=min(SPEED_RANGE['max'], specs['max_speed']),
|
| 61 |
+
value=st.session_state[vehicle_key]['speed'],
|
| 62 |
+
step=5,
|
| 63 |
+
key=f"speed_{vehicle_id}"
|
| 64 |
+
)
|
| 65 |
+
st.session_state[vehicle_key]['speed'] = speed
|
| 66 |
+
|
| 67 |
+
# Speed category indicator
|
| 68 |
+
if speed < 30:
|
| 69 |
+
st.success("π’ Low speed")
|
| 70 |
+
elif speed < 60:
|
| 71 |
+
st.info("π Normal speed")
|
| 72 |
+
elif speed < 100:
|
| 73 |
+
st.warning("ποΈ High speed")
|
| 74 |
+
else:
|
| 75 |
+
st.error("β οΈ Very high speed")
|
| 76 |
+
|
| 77 |
+
# Direction of travel
|
| 78 |
+
direction = st.selectbox(
|
| 79 |
+
"Direction of Travel",
|
| 80 |
+
options=['north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest'],
|
| 81 |
+
index=['north', 'south', 'east', 'west', 'northeast', 'northwest', 'southeast', 'southwest'].index(
|
| 82 |
+
st.session_state[vehicle_key]['direction']
|
| 83 |
+
),
|
| 84 |
+
key=f"direction_{vehicle_id}"
|
| 85 |
+
)
|
| 86 |
+
st.session_state[vehicle_key]['direction'] = direction
|
| 87 |
+
|
| 88 |
+
# Direction indicator
|
| 89 |
+
direction_arrows = {
|
| 90 |
+
'north': 'β¬οΈ', 'south': 'β¬οΈ', 'east': 'β‘οΈ', 'west': 'β¬
οΈ',
|
| 91 |
+
'northeast': 'βοΈ', 'northwest': 'βοΈ', 'southeast': 'βοΈ', 'southwest': 'βοΈ'
|
| 92 |
+
}
|
| 93 |
+
st.write(f"Direction: {direction_arrows.get(direction, 'β‘οΈ')} {direction.title()}")
|
| 94 |
+
|
| 95 |
+
# Initial action
|
| 96 |
+
action = st.selectbox(
|
| 97 |
+
"Action Before Accident",
|
| 98 |
+
options=[
|
| 99 |
+
'going_straight',
|
| 100 |
+
'turning_left',
|
| 101 |
+
'turning_right',
|
| 102 |
+
'changing_lane_left',
|
| 103 |
+
'changing_lane_right',
|
| 104 |
+
'entering_roundabout',
|
| 105 |
+
'exiting_roundabout',
|
| 106 |
+
'slowing_down',
|
| 107 |
+
'accelerating',
|
| 108 |
+
'stopped'
|
| 109 |
+
],
|
| 110 |
+
format_func=lambda x: x.replace('_', ' ').title(),
|
| 111 |
+
key=f"action_{vehicle_id}"
|
| 112 |
+
)
|
| 113 |
+
st.session_state[vehicle_key]['action'] = action
|
| 114 |
+
|
| 115 |
+
# Driver's description
|
| 116 |
+
description = st.text_area(
|
| 117 |
+
"Driver's Description of Events",
|
| 118 |
+
value=st.session_state[vehicle_key].get('description', ''),
|
| 119 |
+
placeholder=f"What did Vehicle {vehicle_id}'s driver say happened?",
|
| 120 |
+
height=100,
|
| 121 |
+
key=f"description_{vehicle_id}"
|
| 122 |
+
)
|
| 123 |
+
st.session_state[vehicle_key]['description'] = description
|
| 124 |
+
|
| 125 |
+
# Additional factors
|
| 126 |
+
st.markdown("**Additional Factors**")
|
| 127 |
+
|
| 128 |
+
col1, col2 = st.columns(2)
|
| 129 |
+
|
| 130 |
+
with col1:
|
| 131 |
+
braking = st.checkbox(
|
| 132 |
+
"Was braking",
|
| 133 |
+
key=f"braking_{vehicle_id}"
|
| 134 |
+
)
|
| 135 |
+
st.session_state[vehicle_key]['braking'] = braking
|
| 136 |
+
|
| 137 |
+
signaling = st.checkbox(
|
| 138 |
+
"Was signaling",
|
| 139 |
+
key=f"signaling_{vehicle_id}"
|
| 140 |
+
)
|
| 141 |
+
st.session_state[vehicle_key]['signaling'] = signaling
|
| 142 |
+
|
| 143 |
+
with col2:
|
| 144 |
+
lights_on = st.checkbox(
|
| 145 |
+
"Lights on",
|
| 146 |
+
value=True,
|
| 147 |
+
key=f"lights_{vehicle_id}"
|
| 148 |
+
)
|
| 149 |
+
st.session_state[vehicle_key]['lights_on'] = lights_on
|
| 150 |
+
|
| 151 |
+
horn_used = st.checkbox(
|
| 152 |
+
"Horn used",
|
| 153 |
+
key=f"horn_{vehicle_id}"
|
| 154 |
+
)
|
| 155 |
+
st.session_state[vehicle_key]['horn_used'] = horn_used
|
| 156 |
+
|
| 157 |
+
# Path status
|
| 158 |
+
st.markdown("---")
|
| 159 |
+
st.markdown("**Path Status**")
|
| 160 |
+
|
| 161 |
+
path = st.session_state[vehicle_key].get('path', [])
|
| 162 |
+
if path and len(path) >= 2:
|
| 163 |
+
st.success(f"β
Path defined with {len(path)} points")
|
| 164 |
+
else:
|
| 165 |
+
st.warning("β οΈ Please draw the vehicle's path on the map")
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def render_vehicle_summary(vehicle_id: int):
|
| 169 |
+
"""
|
| 170 |
+
Render a summary of vehicle data.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
vehicle_id: 1 or 2
|
| 174 |
+
"""
|
| 175 |
+
vehicle_key = f'vehicle_{vehicle_id}'
|
| 176 |
+
vehicle = st.session_state[vehicle_key]
|
| 177 |
+
color = "#FF4B4B" if vehicle_id == 1 else "#4B7BFF"
|
| 178 |
+
|
| 179 |
+
st.markdown(f"""
|
| 180 |
+
<div style="
|
| 181 |
+
background: rgba(255, 255, 255, 0.08);
|
| 182 |
+
backdrop-filter: blur(10px);
|
| 183 |
+
border: 2px solid {color};
|
| 184 |
+
border-radius: 12px;
|
| 185 |
+
padding: 1.2rem;
|
| 186 |
+
margin: 0.5rem 0;
|
| 187 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 188 |
+
">
|
| 189 |
+
<h4 style="color: {color}; margin: 0 0 0.8rem 0; font-weight: 700;">π Vehicle {vehicle_id}</h4>
|
| 190 |
+
<p style="margin: 0.25rem 0; color: white;"><b>Type:</b> {VEHICLE_TYPES[vehicle['type']]['name']}</p>
|
| 191 |
+
<p style="margin: 0.25rem 0; color: white;"><b>Speed:</b> {vehicle['speed']} km/h</p>
|
| 192 |
+
<p style="margin: 0.25rem 0; color: white;"><b>Direction:</b> {vehicle['direction'].title()}</p>
|
| 193 |
+
<p style="margin: 0.25rem 0; color: white;"><b>Action:</b> {vehicle.get('action', 'N/A').replace('_', ' ').title()}</p>
|
| 194 |
+
<p style="margin: 0.25rem 0; color: white;"><b>Path Points:</b> {len(vehicle.get('path', []))}</p>
|
| 195 |
+
</div>
|
| 196 |
+
""", unsafe_allow_html=True)
|
video_generator.py
ADDED
|
@@ -0,0 +1,919 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Professional Video Generator for Accident Scenarios
|
| 3 |
+
====================================================
|
| 4 |
+
Generates high-quality animated visualizations of accident scenarios
|
| 5 |
+
with realistic car graphics, detailed roads, and smooth animations.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import matplotlib.pyplot as plt
|
| 10 |
+
import matplotlib.animation as animation
|
| 11 |
+
import matplotlib.patches as patches
|
| 12 |
+
from matplotlib.patches import Rectangle, Circle, FancyBboxPatch, Polygon, Wedge, Arc
|
| 13 |
+
from matplotlib.collections import PatchCollection
|
| 14 |
+
from matplotlib.transforms import Affine2D
|
| 15 |
+
import matplotlib.patheffects as path_effects
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
import json
|
| 18 |
+
|
| 19 |
+
# Project paths
|
| 20 |
+
OUTPUT_DIR = Path(__file__).parent.parent / "output" / "visualizations"
|
| 21 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Car:
|
| 25 |
+
"""Professional car visualization with realistic shape."""
|
| 26 |
+
|
| 27 |
+
def __init__(self, ax, x, y, angle=0, color='#e74c3c', label='', scale=1.0):
|
| 28 |
+
self.ax = ax
|
| 29 |
+
self.x = x
|
| 30 |
+
self.y = y
|
| 31 |
+
self.angle = angle # degrees, 0 = facing right
|
| 32 |
+
self.color = color
|
| 33 |
+
self.label = label
|
| 34 |
+
self.scale = scale
|
| 35 |
+
self.patches = []
|
| 36 |
+
self.draw()
|
| 37 |
+
|
| 38 |
+
def draw(self):
|
| 39 |
+
"""Draw the car with all its components."""
|
| 40 |
+
# Clear previous patches
|
| 41 |
+
for p in self.patches:
|
| 42 |
+
p.remove()
|
| 43 |
+
self.patches = []
|
| 44 |
+
|
| 45 |
+
# Car dimensions (scaled)
|
| 46 |
+
length = 4.5 * self.scale
|
| 47 |
+
width = 2.0 * self.scale
|
| 48 |
+
|
| 49 |
+
# Create car body shape (rounded rectangle with hood and trunk)
|
| 50 |
+
# Main body
|
| 51 |
+
body = FancyBboxPatch(
|
| 52 |
+
(self.x - length/2, self.y - width/2),
|
| 53 |
+
length, width,
|
| 54 |
+
boxstyle="round,pad=0,rounding_size=0.3",
|
| 55 |
+
facecolor=self.color,
|
| 56 |
+
edgecolor='#2c3e50',
|
| 57 |
+
linewidth=2,
|
| 58 |
+
zorder=10
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Windshield area (darker)
|
| 62 |
+
windshield_length = length * 0.35
|
| 63 |
+
windshield = FancyBboxPatch(
|
| 64 |
+
(self.x - length/2 + length*0.55, self.y - width/2 + width*0.1),
|
| 65 |
+
windshield_length, width * 0.8,
|
| 66 |
+
boxstyle="round,pad=0,rounding_size=0.2",
|
| 67 |
+
facecolor='#34495e',
|
| 68 |
+
edgecolor='#2c3e50',
|
| 69 |
+
linewidth=1,
|
| 70 |
+
zorder=11
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
# Front windshield highlight
|
| 74 |
+
highlight = FancyBboxPatch(
|
| 75 |
+
(self.x - length/2 + length*0.58, self.y - width/2 + width*0.15),
|
| 76 |
+
windshield_length * 0.3, width * 0.7,
|
| 77 |
+
boxstyle="round,pad=0,rounding_size=0.1",
|
| 78 |
+
facecolor='#5d6d7e',
|
| 79 |
+
edgecolor='none',
|
| 80 |
+
alpha=0.5,
|
| 81 |
+
zorder=12
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# Headlights
|
| 85 |
+
headlight_y_offset = width * 0.3
|
| 86 |
+
headlight1 = Circle(
|
| 87 |
+
(self.x + length/2 - 0.3*self.scale, self.y + headlight_y_offset),
|
| 88 |
+
0.25 * self.scale,
|
| 89 |
+
facecolor='#f1c40f',
|
| 90 |
+
edgecolor='#f39c12',
|
| 91 |
+
linewidth=1,
|
| 92 |
+
zorder=12
|
| 93 |
+
)
|
| 94 |
+
headlight2 = Circle(
|
| 95 |
+
(self.x + length/2 - 0.3*self.scale, self.y - headlight_y_offset),
|
| 96 |
+
0.25 * self.scale,
|
| 97 |
+
facecolor='#f1c40f',
|
| 98 |
+
edgecolor='#f39c12',
|
| 99 |
+
linewidth=1,
|
| 100 |
+
zorder=12
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
# Taillights
|
| 104 |
+
taillight1 = Rectangle(
|
| 105 |
+
(self.x - length/2, self.y + width/2 - 0.5*self.scale),
|
| 106 |
+
0.3 * self.scale, 0.35 * self.scale,
|
| 107 |
+
facecolor='#c0392b',
|
| 108 |
+
edgecolor='#922b21',
|
| 109 |
+
linewidth=1,
|
| 110 |
+
zorder=12
|
| 111 |
+
)
|
| 112 |
+
taillight2 = Rectangle(
|
| 113 |
+
(self.x - length/2, self.y - width/2 + 0.15*self.scale),
|
| 114 |
+
0.3 * self.scale, 0.35 * self.scale,
|
| 115 |
+
facecolor='#c0392b',
|
| 116 |
+
edgecolor='#922b21',
|
| 117 |
+
linewidth=1,
|
| 118 |
+
zorder=12
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# Wheels
|
| 122 |
+
wheel_size = 0.5 * self.scale
|
| 123 |
+
wheel_positions = [
|
| 124 |
+
(self.x - length/2 + length*0.2, self.y + width/2 - 0.1*self.scale),
|
| 125 |
+
(self.x - length/2 + length*0.2, self.y - width/2 + 0.1*self.scale),
|
| 126 |
+
(self.x + length/2 - length*0.2, self.y + width/2 - 0.1*self.scale),
|
| 127 |
+
(self.x + length/2 - length*0.2, self.y - width/2 + 0.1*self.scale),
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
wheels = []
|
| 131 |
+
for wx, wy in wheel_positions:
|
| 132 |
+
# Tire
|
| 133 |
+
tire = Circle(
|
| 134 |
+
(wx, wy), wheel_size,
|
| 135 |
+
facecolor='#2c3e50',
|
| 136 |
+
edgecolor='#1a252f',
|
| 137 |
+
linewidth=1.5,
|
| 138 |
+
zorder=9
|
| 139 |
+
)
|
| 140 |
+
# Hubcap
|
| 141 |
+
hubcap = Circle(
|
| 142 |
+
(wx, wy), wheel_size * 0.6,
|
| 143 |
+
facecolor='#7f8c8d',
|
| 144 |
+
edgecolor='#95a5a6',
|
| 145 |
+
linewidth=1,
|
| 146 |
+
zorder=9
|
| 147 |
+
)
|
| 148 |
+
wheels.extend([tire, hubcap])
|
| 149 |
+
|
| 150 |
+
# Add shadow under car
|
| 151 |
+
shadow = FancyBboxPatch(
|
| 152 |
+
(self.x - length/2 + 0.2, self.y - width/2 - 0.3),
|
| 153 |
+
length, width,
|
| 154 |
+
boxstyle="round,pad=0,rounding_size=0.3",
|
| 155 |
+
facecolor='black',
|
| 156 |
+
edgecolor='none',
|
| 157 |
+
alpha=0.2,
|
| 158 |
+
zorder=5
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
# Store all patches
|
| 162 |
+
all_patches = [shadow, body, windshield, highlight,
|
| 163 |
+
headlight1, headlight2, taillight1, taillight2] + wheels
|
| 164 |
+
|
| 165 |
+
# Apply rotation transform
|
| 166 |
+
transform = Affine2D().rotate_deg_around(self.x, self.y, self.angle) + self.ax.transData
|
| 167 |
+
|
| 168 |
+
for patch in all_patches:
|
| 169 |
+
patch.set_transform(transform)
|
| 170 |
+
self.ax.add_patch(patch)
|
| 171 |
+
self.patches.append(patch)
|
| 172 |
+
|
| 173 |
+
def set_position(self, x, y, angle=None):
|
| 174 |
+
"""Update car position and optionally angle."""
|
| 175 |
+
self.x = x
|
| 176 |
+
self.y = y
|
| 177 |
+
if angle is not None:
|
| 178 |
+
self.angle = angle
|
| 179 |
+
self.draw()
|
| 180 |
+
|
| 181 |
+
def remove(self):
|
| 182 |
+
"""Remove all patches."""
|
| 183 |
+
for p in self.patches:
|
| 184 |
+
try:
|
| 185 |
+
p.remove()
|
| 186 |
+
except:
|
| 187 |
+
pass
|
| 188 |
+
self.patches = []
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class ProfessionalAccidentVideoGenerator:
|
| 192 |
+
"""Generate professional animated videos of accident scenarios."""
|
| 193 |
+
|
| 194 |
+
def __init__(self, width=800, height=600, dpi=100):
|
| 195 |
+
self.width = width
|
| 196 |
+
self.height = height
|
| 197 |
+
self.dpi = dpi
|
| 198 |
+
self.fig_width = width / dpi
|
| 199 |
+
self.fig_height = height / dpi
|
| 200 |
+
|
| 201 |
+
# Color scheme
|
| 202 |
+
self.colors = {
|
| 203 |
+
'background': '#1a1a2e',
|
| 204 |
+
'road': '#4a4a5a',
|
| 205 |
+
'road_edge': '#6a6a7a',
|
| 206 |
+
'lane_marking': '#ffffff',
|
| 207 |
+
'center_line': '#f1c40f',
|
| 208 |
+
'grass': '#2d5a27',
|
| 209 |
+
'sidewalk': '#95a5a6',
|
| 210 |
+
'car1': '#e74c3c', # Red
|
| 211 |
+
'car2': '#3498db', # Blue
|
| 212 |
+
'impact': '#f39c12',
|
| 213 |
+
'text': '#ecf0f1'
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
def _draw_road_texture(self, ax, road_rect, orientation='horizontal'):
|
| 217 |
+
"""Add realistic road texture and markings."""
|
| 218 |
+
x, y, w, h = road_rect
|
| 219 |
+
|
| 220 |
+
# Road base with gradient effect
|
| 221 |
+
road = FancyBboxPatch(
|
| 222 |
+
(x, y), w, h,
|
| 223 |
+
boxstyle="round,pad=0,rounding_size=0.5",
|
| 224 |
+
facecolor=self.colors['road'],
|
| 225 |
+
edgecolor=self.colors['road_edge'],
|
| 226 |
+
linewidth=3,
|
| 227 |
+
zorder=1
|
| 228 |
+
)
|
| 229 |
+
ax.add_patch(road)
|
| 230 |
+
|
| 231 |
+
# Add road edge lines
|
| 232 |
+
if orientation == 'horizontal':
|
| 233 |
+
# Top edge
|
| 234 |
+
ax.plot([x, x + w], [y + h, y + h], color='#ffffff', linewidth=2, zorder=2)
|
| 235 |
+
# Bottom edge
|
| 236 |
+
ax.plot([x, x + w], [y, y], color='#ffffff', linewidth=2, zorder=2)
|
| 237 |
+
else:
|
| 238 |
+
# Left edge
|
| 239 |
+
ax.plot([x, x], [y, y + h], color='#ffffff', linewidth=2, zorder=2)
|
| 240 |
+
# Right edge
|
| 241 |
+
ax.plot([x + w, x + w], [y, y + h], color='#ffffff', linewidth=2, zorder=2)
|
| 242 |
+
|
| 243 |
+
def _draw_lane_markings(self, ax, start, end, center_y, lane_width, is_two_way=False):
|
| 244 |
+
"""Draw dashed lane markings."""
|
| 245 |
+
dash_length = 3
|
| 246 |
+
gap_length = 2
|
| 247 |
+
|
| 248 |
+
if is_two_way:
|
| 249 |
+
# Center double yellow line
|
| 250 |
+
ax.plot([start, end], [center_y + 0.3, center_y + 0.3],
|
| 251 |
+
color=self.colors['center_line'], linewidth=2.5, zorder=3)
|
| 252 |
+
ax.plot([start, end], [center_y - 0.3, center_y - 0.3],
|
| 253 |
+
color=self.colors['center_line'], linewidth=2.5, zorder=3)
|
| 254 |
+
|
| 255 |
+
# Dashed white lines for lanes
|
| 256 |
+
x = start
|
| 257 |
+
while x < end:
|
| 258 |
+
ax.plot([x, min(x + dash_length, end)], [center_y + lane_width, center_y + lane_width],
|
| 259 |
+
color=self.colors['lane_marking'], linewidth=2, zorder=3, alpha=0.8)
|
| 260 |
+
ax.plot([x, min(x + dash_length, end)], [center_y - lane_width, center_y - lane_width],
|
| 261 |
+
color=self.colors['lane_marking'], linewidth=2, zorder=3, alpha=0.8)
|
| 262 |
+
x += dash_length + gap_length
|
| 263 |
+
|
| 264 |
+
def _add_impact_effect(self, ax, x, y, intensity=1.0):
|
| 265 |
+
"""Add explosion/impact effect."""
|
| 266 |
+
# Multiple circles for burst effect
|
| 267 |
+
for i in range(5):
|
| 268 |
+
radius = (2 + i * 0.8) * intensity
|
| 269 |
+
alpha = 0.8 - i * 0.15
|
| 270 |
+
color = '#f39c12' if i < 2 else '#e74c3c'
|
| 271 |
+
impact = Circle(
|
| 272 |
+
(x, y), radius,
|
| 273 |
+
facecolor=color,
|
| 274 |
+
edgecolor='#f1c40f',
|
| 275 |
+
alpha=alpha,
|
| 276 |
+
linewidth=2 if i == 0 else 0,
|
| 277 |
+
zorder=20 - i
|
| 278 |
+
)
|
| 279 |
+
ax.add_patch(impact)
|
| 280 |
+
|
| 281 |
+
# Spark lines
|
| 282 |
+
for angle in range(0, 360, 45):
|
| 283 |
+
rad = np.radians(angle)
|
| 284 |
+
length = 4 * intensity
|
| 285 |
+
ax.plot(
|
| 286 |
+
[x, x + length * np.cos(rad)],
|
| 287 |
+
[y, y + length * np.sin(rad)],
|
| 288 |
+
color='#f1c40f', linewidth=2, alpha=0.8, zorder=21
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
def generate_rear_end_collision(self, output_path=None):
|
| 292 |
+
"""Generate professional rear-end collision animation."""
|
| 293 |
+
|
| 294 |
+
if output_path is None:
|
| 295 |
+
output_path = OUTPUT_DIR / "rear_end_collision.gif"
|
| 296 |
+
|
| 297 |
+
fig, ax = plt.subplots(figsize=(self.fig_width, self.fig_height), dpi=self.dpi)
|
| 298 |
+
ax.set_xlim(0, 100)
|
| 299 |
+
ax.set_ylim(0, 75)
|
| 300 |
+
ax.set_aspect('equal')
|
| 301 |
+
ax.set_facecolor(self.colors['background'])
|
| 302 |
+
fig.patch.set_facecolor(self.colors['background'])
|
| 303 |
+
ax.axis('off')
|
| 304 |
+
|
| 305 |
+
# Draw environment
|
| 306 |
+
# Grass/ground on sides
|
| 307 |
+
grass_top = Rectangle((0, 55), 100, 20, facecolor=self.colors['grass'], zorder=0)
|
| 308 |
+
grass_bottom = Rectangle((0, 0), 100, 20, facecolor=self.colors['grass'], zorder=0)
|
| 309 |
+
ax.add_patch(grass_top)
|
| 310 |
+
ax.add_patch(grass_bottom)
|
| 311 |
+
|
| 312 |
+
# Main road
|
| 313 |
+
self._draw_road_texture(ax, (0, 20, 100, 35), 'horizontal')
|
| 314 |
+
|
| 315 |
+
# Lane markings (single lane road with dashes)
|
| 316 |
+
for x in np.arange(0, 100, 8):
|
| 317 |
+
ax.plot([x, x + 4], [37.5, 37.5], color='white', linewidth=2, zorder=3, alpha=0.8)
|
| 318 |
+
|
| 319 |
+
# Add some road details - small dots/texture
|
| 320 |
+
for _ in range(50):
|
| 321 |
+
rx, ry = np.random.uniform(5, 95), np.random.uniform(22, 53)
|
| 322 |
+
dot = Circle((rx, ry), 0.15, facecolor='#3a3a4a', alpha=0.3, zorder=2)
|
| 323 |
+
ax.add_patch(dot)
|
| 324 |
+
|
| 325 |
+
# Title
|
| 326 |
+
title = ax.text(50, 70, 'Rear-End Collision Scenario',
|
| 327 |
+
color=self.colors['text'], fontsize=18, ha='center',
|
| 328 |
+
weight='bold', zorder=30)
|
| 329 |
+
title.set_path_effects([path_effects.withStroke(linewidth=3, foreground='black')])
|
| 330 |
+
|
| 331 |
+
# Subtitle
|
| 332 |
+
subtitle = ax.text(50, 5, 'Vehicle 1 (Red) approaching Vehicle 2 (Blue) at high speed',
|
| 333 |
+
color=self.colors['text'], fontsize=11, ha='center', zorder=30)
|
| 334 |
+
subtitle.set_path_effects([path_effects.withStroke(linewidth=2, foreground='black')])
|
| 335 |
+
|
| 336 |
+
# Speed indicators
|
| 337 |
+
speed1_text = ax.text(5, 65, 'β Red Vehicle: 120 km/h', color='#e74c3c', fontsize=10, weight='bold', zorder=30)
|
| 338 |
+
speed2_text = ax.text(5, 60, 'β Blue Vehicle: 50 km/h', color='#3498db', fontsize=10, weight='bold', zorder=30)
|
| 339 |
+
|
| 340 |
+
# Animation parameters
|
| 341 |
+
frames = 80
|
| 342 |
+
impact_frame = 50
|
| 343 |
+
|
| 344 |
+
# Store car objects
|
| 345 |
+
car1_data = {'x': 10, 'y': 32, 'angle': 0}
|
| 346 |
+
car2_data = {'x': 55, 'y': 32, 'angle': 0}
|
| 347 |
+
|
| 348 |
+
# Initial car drawing
|
| 349 |
+
car1_patches = []
|
| 350 |
+
car2_patches = []
|
| 351 |
+
impact_patches = []
|
| 352 |
+
|
| 353 |
+
def draw_car(x, y, angle, color, ax):
|
| 354 |
+
"""Draw a simple but nice car."""
|
| 355 |
+
patches = []
|
| 356 |
+
scale = 1.2
|
| 357 |
+
length = 5 * scale
|
| 358 |
+
width = 2.2 * scale
|
| 359 |
+
|
| 360 |
+
# Shadow
|
| 361 |
+
shadow = FancyBboxPatch(
|
| 362 |
+
(x - length/2 + 0.3, y - width/2 - 0.4),
|
| 363 |
+
length, width,
|
| 364 |
+
boxstyle="round,pad=0,rounding_size=0.4",
|
| 365 |
+
facecolor='black', alpha=0.3, zorder=4
|
| 366 |
+
)
|
| 367 |
+
patches.append(shadow)
|
| 368 |
+
|
| 369 |
+
# Main body
|
| 370 |
+
body = FancyBboxPatch(
|
| 371 |
+
(x - length/2, y - width/2),
|
| 372 |
+
length, width,
|
| 373 |
+
boxstyle="round,pad=0,rounding_size=0.4",
|
| 374 |
+
facecolor=color, edgecolor='#1a1a2e', linewidth=2, zorder=10
|
| 375 |
+
)
|
| 376 |
+
patches.append(body)
|
| 377 |
+
|
| 378 |
+
# Cabin/windshield
|
| 379 |
+
cabin = FancyBboxPatch(
|
| 380 |
+
(x - length*0.1, y - width/2 + 0.3),
|
| 381 |
+
length * 0.35, width - 0.6,
|
| 382 |
+
boxstyle="round,pad=0,rounding_size=0.2",
|
| 383 |
+
facecolor='#2c3e50', edgecolor='#1a252f', linewidth=1, zorder=11
|
| 384 |
+
)
|
| 385 |
+
patches.append(cabin)
|
| 386 |
+
|
| 387 |
+
# Windshield reflection
|
| 388 |
+
reflection = FancyBboxPatch(
|
| 389 |
+
(x - length*0.05, y - width/2 + 0.5),
|
| 390 |
+
length * 0.15, width - 1.0,
|
| 391 |
+
boxstyle="round,pad=0,rounding_size=0.1",
|
| 392 |
+
facecolor='#5d6d7e', alpha=0.4, zorder=12
|
| 393 |
+
)
|
| 394 |
+
patches.append(reflection)
|
| 395 |
+
|
| 396 |
+
# Headlights
|
| 397 |
+
hl1 = Circle((x + length/2 - 0.4, y + width/3), 0.35,
|
| 398 |
+
facecolor='#f5f5f5', edgecolor='#bdc3c7', zorder=12)
|
| 399 |
+
hl2 = Circle((x + length/2 - 0.4, y - width/3), 0.35,
|
| 400 |
+
facecolor='#f5f5f5', edgecolor='#bdc3c7', zorder=12)
|
| 401 |
+
patches.extend([hl1, hl2])
|
| 402 |
+
|
| 403 |
+
# Taillights
|
| 404 |
+
tl1 = Rectangle((x - length/2, y + width/3 - 0.2), 0.4, 0.4,
|
| 405 |
+
facecolor='#c0392b', edgecolor='#922b21', zorder=12)
|
| 406 |
+
tl2 = Rectangle((x - length/2, y - width/3 - 0.2), 0.4, 0.4,
|
| 407 |
+
facecolor='#c0392b', edgecolor='#922b21', zorder=12)
|
| 408 |
+
patches.extend([tl1, tl2])
|
| 409 |
+
|
| 410 |
+
# Wheels
|
| 411 |
+
wheel_positions = [
|
| 412 |
+
(x - length/2 + length*0.2, y + width/2 + 0.1),
|
| 413 |
+
(x - length/2 + length*0.2, y - width/2 - 0.1),
|
| 414 |
+
(x + length/2 - length*0.2, y + width/2 + 0.1),
|
| 415 |
+
(x + length/2 - length*0.2, y - width/2 - 0.1),
|
| 416 |
+
]
|
| 417 |
+
for wx, wy in wheel_positions:
|
| 418 |
+
tire = Circle((wx, wy), 0.6, facecolor='#1a1a2e', zorder=8)
|
| 419 |
+
rim = Circle((wx, wy), 0.35, facecolor='#7f8c8d', zorder=9)
|
| 420 |
+
patches.extend([tire, rim])
|
| 421 |
+
|
| 422 |
+
# Apply rotation
|
| 423 |
+
transform = Affine2D().rotate_deg_around(x, y, angle) + ax.transData
|
| 424 |
+
for p in patches:
|
| 425 |
+
p.set_transform(transform)
|
| 426 |
+
ax.add_patch(p)
|
| 427 |
+
|
| 428 |
+
return patches
|
| 429 |
+
|
| 430 |
+
def animate(frame):
|
| 431 |
+
nonlocal car1_patches, car2_patches, impact_patches
|
| 432 |
+
|
| 433 |
+
# Remove old patches
|
| 434 |
+
for p in car1_patches + car2_patches + impact_patches:
|
| 435 |
+
try:
|
| 436 |
+
p.remove()
|
| 437 |
+
except:
|
| 438 |
+
pass
|
| 439 |
+
car1_patches = []
|
| 440 |
+
car2_patches = []
|
| 441 |
+
impact_patches = []
|
| 442 |
+
|
| 443 |
+
# Calculate positions
|
| 444 |
+
if frame < impact_frame:
|
| 445 |
+
# Car 1 moving fast
|
| 446 |
+
x1 = 10 + frame * 1.1
|
| 447 |
+
# Car 2 moving slow
|
| 448 |
+
x2 = 55 + frame * 0.25
|
| 449 |
+
angle1 = 0
|
| 450 |
+
angle2 = 0
|
| 451 |
+
else:
|
| 452 |
+
# Post-impact
|
| 453 |
+
post = frame - impact_frame
|
| 454 |
+
# Car 1 stops abruptly
|
| 455 |
+
x1 = 10 + impact_frame * 1.1 + post * 0.1
|
| 456 |
+
# Car 2 gets pushed
|
| 457 |
+
x2 = 55 + impact_frame * 0.25 + post * 0.8
|
| 458 |
+
# Add some rotation from impact
|
| 459 |
+
angle1 = -post * 0.5
|
| 460 |
+
angle2 = post * 0.8
|
| 461 |
+
|
| 462 |
+
# Draw cars
|
| 463 |
+
car1_patches = draw_car(x1, 32, angle1, self.colors['car1'], ax)
|
| 464 |
+
car2_patches = draw_car(x2, 32, angle2, self.colors['car2'], ax)
|
| 465 |
+
|
| 466 |
+
# Impact effect at collision
|
| 467 |
+
if impact_frame <= frame < impact_frame + 10:
|
| 468 |
+
intensity = 1.0 - (frame - impact_frame) * 0.1
|
| 469 |
+
impact_x = 10 + impact_frame * 1.1 + 3
|
| 470 |
+
|
| 471 |
+
# Explosion circles
|
| 472 |
+
for i in range(4):
|
| 473 |
+
radius = (1.5 + i * 0.6) * intensity
|
| 474 |
+
alpha = (0.7 - i * 0.15) * intensity
|
| 475 |
+
colors = ['#f39c12', '#e74c3c', '#f1c40f', '#e67e22']
|
| 476 |
+
impact = Circle(
|
| 477 |
+
(impact_x, 32), radius,
|
| 478 |
+
facecolor=colors[i], alpha=alpha, zorder=25 - i
|
| 479 |
+
)
|
| 480 |
+
ax.add_patch(impact)
|
| 481 |
+
impact_patches.append(impact)
|
| 482 |
+
|
| 483 |
+
# Spark lines
|
| 484 |
+
for angle in range(0, 360, 30):
|
| 485 |
+
rad = np.radians(angle + frame * 10)
|
| 486 |
+
length = 3 * intensity
|
| 487 |
+
line, = ax.plot(
|
| 488 |
+
[impact_x, impact_x + length * np.cos(rad)],
|
| 489 |
+
[32, 32 + length * np.sin(rad)],
|
| 490 |
+
color='#f1c40f', linewidth=2, alpha=intensity, zorder=26
|
| 491 |
+
)
|
| 492 |
+
# We need to track these too but plot returns Line2D not Patch
|
| 493 |
+
|
| 494 |
+
return car1_patches + car2_patches
|
| 495 |
+
|
| 496 |
+
# Create animation
|
| 497 |
+
anim = animation.FuncAnimation(fig, animate, frames=frames, interval=60, blit=False)
|
| 498 |
+
anim.save(str(output_path), writer='pillow', fps=20)
|
| 499 |
+
plt.close()
|
| 500 |
+
|
| 501 |
+
return str(output_path)
|
| 502 |
+
|
| 503 |
+
def generate_head_on_collision(self, output_path=None):
|
| 504 |
+
"""Generate professional head-on collision animation."""
|
| 505 |
+
|
| 506 |
+
if output_path is None:
|
| 507 |
+
output_path = OUTPUT_DIR / "head_on_collision.gif"
|
| 508 |
+
|
| 509 |
+
fig, ax = plt.subplots(figsize=(self.fig_width, self.fig_height), dpi=self.dpi)
|
| 510 |
+
ax.set_xlim(0, 100)
|
| 511 |
+
ax.set_ylim(0, 75)
|
| 512 |
+
ax.set_aspect('equal')
|
| 513 |
+
ax.set_facecolor(self.colors['background'])
|
| 514 |
+
fig.patch.set_facecolor(self.colors['background'])
|
| 515 |
+
ax.axis('off')
|
| 516 |
+
|
| 517 |
+
# Environment
|
| 518 |
+
grass_top = Rectangle((0, 55), 100, 20, facecolor=self.colors['grass'], zorder=0)
|
| 519 |
+
grass_bottom = Rectangle((0, 0), 100, 20, facecolor=self.colors['grass'], zorder=0)
|
| 520 |
+
ax.add_patch(grass_top)
|
| 521 |
+
ax.add_patch(grass_bottom)
|
| 522 |
+
|
| 523 |
+
# Two-lane road
|
| 524 |
+
self._draw_road_texture(ax, (0, 20, 100, 35), 'horizontal')
|
| 525 |
+
|
| 526 |
+
# Center line (double yellow)
|
| 527 |
+
ax.plot([0, 100], [37.8, 37.8], color=self.colors['center_line'], linewidth=3, zorder=3)
|
| 528 |
+
ax.plot([0, 100], [37.2, 37.2], color=self.colors['center_line'], linewidth=3, zorder=3)
|
| 529 |
+
|
| 530 |
+
# Lane markings
|
| 531 |
+
for x in np.arange(0, 100, 8):
|
| 532 |
+
ax.plot([x, x + 4], [28, 28], color='white', linewidth=2, zorder=3, alpha=0.7)
|
| 533 |
+
ax.plot([x, x + 4], [47, 47], color='white', linewidth=2, zorder=3, alpha=0.7)
|
| 534 |
+
|
| 535 |
+
# Title
|
| 536 |
+
title = ax.text(50, 70, 'Head-On Collision Scenario',
|
| 537 |
+
color=self.colors['text'], fontsize=18, ha='center',
|
| 538 |
+
weight='bold', zorder=30)
|
| 539 |
+
title.set_path_effects([path_effects.withStroke(linewidth=3, foreground='black')])
|
| 540 |
+
|
| 541 |
+
subtitle = ax.text(50, 5, 'Vehicle 1 (Red) crosses center line, Vehicle 2 (Blue) oncoming',
|
| 542 |
+
color=self.colors['text'], fontsize=11, ha='center', zorder=30)
|
| 543 |
+
subtitle.set_path_effects([path_effects.withStroke(linewidth=2, foreground='black')])
|
| 544 |
+
|
| 545 |
+
# Speed indicators
|
| 546 |
+
ax.text(5, 65, 'β Red Vehicle: 90 km/h (drifting)', color='#e74c3c', fontsize=10, weight='bold', zorder=30)
|
| 547 |
+
ax.text(5, 60, 'β Blue Vehicle: 80 km/h', color='#3498db', fontsize=10, weight='bold', zorder=30)
|
| 548 |
+
|
| 549 |
+
frames = 80
|
| 550 |
+
impact_frame = 45
|
| 551 |
+
|
| 552 |
+
car1_patches = []
|
| 553 |
+
car2_patches = []
|
| 554 |
+
impact_patches = []
|
| 555 |
+
|
| 556 |
+
def draw_car(x, y, angle, color, ax):
|
| 557 |
+
"""Draw a car."""
|
| 558 |
+
patches = []
|
| 559 |
+
scale = 1.2
|
| 560 |
+
length = 5 * scale
|
| 561 |
+
width = 2.2 * scale
|
| 562 |
+
|
| 563 |
+
shadow = FancyBboxPatch(
|
| 564 |
+
(x - length/2 + 0.3, y - width/2 - 0.4),
|
| 565 |
+
length, width,
|
| 566 |
+
boxstyle="round,pad=0,rounding_size=0.4",
|
| 567 |
+
facecolor='black', alpha=0.3, zorder=4
|
| 568 |
+
)
|
| 569 |
+
patches.append(shadow)
|
| 570 |
+
|
| 571 |
+
body = FancyBboxPatch(
|
| 572 |
+
(x - length/2, y - width/2),
|
| 573 |
+
length, width,
|
| 574 |
+
boxstyle="round,pad=0,rounding_size=0.4",
|
| 575 |
+
facecolor=color, edgecolor='#1a1a2e', linewidth=2, zorder=10
|
| 576 |
+
)
|
| 577 |
+
patches.append(body)
|
| 578 |
+
|
| 579 |
+
cabin = FancyBboxPatch(
|
| 580 |
+
(x - length*0.1, y - width/2 + 0.3),
|
| 581 |
+
length * 0.35, width - 0.6,
|
| 582 |
+
boxstyle="round,pad=0,rounding_size=0.2",
|
| 583 |
+
facecolor='#2c3e50', edgecolor='#1a252f', linewidth=1, zorder=11
|
| 584 |
+
)
|
| 585 |
+
patches.append(cabin)
|
| 586 |
+
|
| 587 |
+
# Headlights
|
| 588 |
+
hl1 = Circle((x + length/2 - 0.4, y + width/3), 0.35,
|
| 589 |
+
facecolor='#f5f5f5', edgecolor='#bdc3c7', zorder=12)
|
| 590 |
+
hl2 = Circle((x + length/2 - 0.4, y - width/3), 0.35,
|
| 591 |
+
facecolor='#f5f5f5', edgecolor='#bdc3c7', zorder=12)
|
| 592 |
+
patches.extend([hl1, hl2])
|
| 593 |
+
|
| 594 |
+
# Taillights
|
| 595 |
+
tl1 = Rectangle((x - length/2, y + width/3 - 0.2), 0.4, 0.4,
|
| 596 |
+
facecolor='#c0392b', zorder=12)
|
| 597 |
+
tl2 = Rectangle((x - length/2, y - width/3 - 0.2), 0.4, 0.4,
|
| 598 |
+
facecolor='#c0392b', zorder=12)
|
| 599 |
+
patches.extend([tl1, tl2])
|
| 600 |
+
|
| 601 |
+
# Wheels
|
| 602 |
+
wheel_positions = [
|
| 603 |
+
(x - length/2 + length*0.2, y + width/2 + 0.1),
|
| 604 |
+
(x - length/2 + length*0.2, y - width/2 - 0.1),
|
| 605 |
+
(x + length/2 - length*0.2, y + width/2 + 0.1),
|
| 606 |
+
(x + length/2 - length*0.2, y - width/2 - 0.1),
|
| 607 |
+
]
|
| 608 |
+
for wx, wy in wheel_positions:
|
| 609 |
+
tire = Circle((wx, wy), 0.6, facecolor='#1a1a2e', zorder=8)
|
| 610 |
+
rim = Circle((wx, wy), 0.35, facecolor='#7f8c8d', zorder=9)
|
| 611 |
+
patches.extend([tire, rim])
|
| 612 |
+
|
| 613 |
+
transform = Affine2D().rotate_deg_around(x, y, angle) + ax.transData
|
| 614 |
+
for p in patches:
|
| 615 |
+
p.set_transform(transform)
|
| 616 |
+
ax.add_patch(p)
|
| 617 |
+
|
| 618 |
+
return patches
|
| 619 |
+
|
| 620 |
+
def animate(frame):
|
| 621 |
+
nonlocal car1_patches, car2_patches, impact_patches
|
| 622 |
+
|
| 623 |
+
for p in car1_patches + car2_patches + impact_patches:
|
| 624 |
+
try:
|
| 625 |
+
p.remove()
|
| 626 |
+
except:
|
| 627 |
+
pass
|
| 628 |
+
car1_patches = []
|
| 629 |
+
car2_patches = []
|
| 630 |
+
impact_patches = []
|
| 631 |
+
|
| 632 |
+
if frame < impact_frame:
|
| 633 |
+
# Car 1 drifting into opposite lane
|
| 634 |
+
x1 = 10 + frame * 0.9
|
| 635 |
+
y1 = 28 + frame * 0.2 # Drifting up
|
| 636 |
+
angle1 = frame * 0.3
|
| 637 |
+
|
| 638 |
+
# Car 2 coming from right
|
| 639 |
+
x2 = 90 - frame * 0.85
|
| 640 |
+
y2 = 47
|
| 641 |
+
angle2 = 180
|
| 642 |
+
else:
|
| 643 |
+
post = frame - impact_frame
|
| 644 |
+
# Both cars stop and spin
|
| 645 |
+
x1 = 10 + impact_frame * 0.9 - post * 0.3
|
| 646 |
+
y1 = 28 + impact_frame * 0.2 - post * 0.2
|
| 647 |
+
angle1 = impact_frame * 0.3 + post * 4
|
| 648 |
+
|
| 649 |
+
x2 = 90 - impact_frame * 0.85 + post * 0.3
|
| 650 |
+
y2 = 47 + post * 0.2
|
| 651 |
+
angle2 = 180 - post * 3
|
| 652 |
+
|
| 653 |
+
car1_patches = draw_car(x1, y1, angle1, self.colors['car1'], ax)
|
| 654 |
+
car2_patches = draw_car(x2, y2, angle2, self.colors['car2'], ax)
|
| 655 |
+
|
| 656 |
+
# Impact effect
|
| 657 |
+
if impact_frame <= frame < impact_frame + 12:
|
| 658 |
+
intensity = 1.0 - (frame - impact_frame) * 0.08
|
| 659 |
+
impact_x = 50
|
| 660 |
+
impact_y = 40
|
| 661 |
+
|
| 662 |
+
for i in range(5):
|
| 663 |
+
radius = (2 + i * 0.8) * intensity
|
| 664 |
+
alpha = (0.8 - i * 0.15) * intensity
|
| 665 |
+
colors = ['#ffffff', '#f39c12', '#e74c3c', '#f1c40f', '#e67e22']
|
| 666 |
+
impact = Circle(
|
| 667 |
+
(impact_x, impact_y), radius,
|
| 668 |
+
facecolor=colors[i], alpha=alpha, zorder=25 - i
|
| 669 |
+
)
|
| 670 |
+
ax.add_patch(impact)
|
| 671 |
+
impact_patches.append(impact)
|
| 672 |
+
|
| 673 |
+
return car1_patches + car2_patches
|
| 674 |
+
|
| 675 |
+
anim = animation.FuncAnimation(fig, animate, frames=frames, interval=60, blit=False)
|
| 676 |
+
anim.save(str(output_path), writer='pillow', fps=20)
|
| 677 |
+
plt.close()
|
| 678 |
+
|
| 679 |
+
return str(output_path)
|
| 680 |
+
|
| 681 |
+
def generate_side_impact_collision(self, output_path=None):
|
| 682 |
+
"""Generate professional side impact collision animation."""
|
| 683 |
+
|
| 684 |
+
if output_path is None:
|
| 685 |
+
output_path = OUTPUT_DIR / "side_impact_collision.gif"
|
| 686 |
+
|
| 687 |
+
fig, ax = plt.subplots(figsize=(self.fig_width, self.fig_height), dpi=self.dpi)
|
| 688 |
+
ax.set_xlim(0, 100)
|
| 689 |
+
ax.set_ylim(0, 100)
|
| 690 |
+
ax.set_aspect('equal')
|
| 691 |
+
ax.set_facecolor(self.colors['background'])
|
| 692 |
+
fig.patch.set_facecolor(self.colors['background'])
|
| 693 |
+
ax.axis('off')
|
| 694 |
+
|
| 695 |
+
# Intersection - draw grass first in corners
|
| 696 |
+
# Top-left grass
|
| 697 |
+
ax.add_patch(Rectangle((0, 60), 35, 40, facecolor=self.colors['grass'], zorder=0))
|
| 698 |
+
# Top-right grass
|
| 699 |
+
ax.add_patch(Rectangle((65, 60), 35, 40, facecolor=self.colors['grass'], zorder=0))
|
| 700 |
+
# Bottom-left grass
|
| 701 |
+
ax.add_patch(Rectangle((0, 0), 35, 40, facecolor=self.colors['grass'], zorder=0))
|
| 702 |
+
# Bottom-right grass
|
| 703 |
+
ax.add_patch(Rectangle((65, 0), 35, 40, facecolor=self.colors['grass'], zorder=0))
|
| 704 |
+
|
| 705 |
+
# Horizontal road
|
| 706 |
+
self._draw_road_texture(ax, (0, 40, 100, 20), 'horizontal')
|
| 707 |
+
|
| 708 |
+
# Vertical road
|
| 709 |
+
self._draw_road_texture(ax, (35, 0, 30, 100), 'vertical')
|
| 710 |
+
|
| 711 |
+
# Intersection center
|
| 712 |
+
ax.add_patch(Rectangle((35, 40), 30, 20, facecolor=self.colors['road'], zorder=2))
|
| 713 |
+
|
| 714 |
+
# Crosswalk markings
|
| 715 |
+
for y in [40, 58]:
|
| 716 |
+
for x in np.arange(37, 63, 3):
|
| 717 |
+
ax.add_patch(Rectangle((x, y), 2, 2, facecolor='white', alpha=0.8, zorder=3))
|
| 718 |
+
for x in [35, 63]:
|
| 719 |
+
for y in np.arange(42, 58, 3):
|
| 720 |
+
ax.add_patch(Rectangle((x, y), 2, 2, facecolor='white', alpha=0.8, zorder=3))
|
| 721 |
+
|
| 722 |
+
# Lane markings on horizontal road
|
| 723 |
+
for x in np.arange(0, 35, 6):
|
| 724 |
+
ax.plot([x, x + 3], [50, 50], color='white', linewidth=2, zorder=3, alpha=0.7)
|
| 725 |
+
for x in np.arange(65, 100, 6):
|
| 726 |
+
ax.plot([x, x + 3], [50, 50], color='white', linewidth=2, zorder=3, alpha=0.7)
|
| 727 |
+
|
| 728 |
+
# Lane markings on vertical road
|
| 729 |
+
for y in np.arange(0, 40, 6):
|
| 730 |
+
ax.plot([50, 50], [y, y + 3], color='white', linewidth=2, zorder=3, alpha=0.7)
|
| 731 |
+
for y in np.arange(60, 100, 6):
|
| 732 |
+
ax.plot([50, 50], [y, y + 3], color='white', linewidth=2, zorder=3, alpha=0.7)
|
| 733 |
+
|
| 734 |
+
# Traffic light poles (simple)
|
| 735 |
+
for pos in [(33, 38), (67, 62)]:
|
| 736 |
+
ax.add_patch(Circle(pos, 1.5, facecolor='#2c3e50', zorder=5))
|
| 737 |
+
ax.add_patch(Circle(pos, 1, facecolor='#e74c3c', zorder=6)) # Red light
|
| 738 |
+
for pos in [(33, 62), (67, 38)]:
|
| 739 |
+
ax.add_patch(Circle(pos, 1.5, facecolor='#2c3e50', zorder=5))
|
| 740 |
+
ax.add_patch(Circle(pos, 1, facecolor='#27ae60', zorder=6)) # Green light
|
| 741 |
+
|
| 742 |
+
# Title
|
| 743 |
+
title = ax.text(50, 95, 'Side Impact Collision Scenario',
|
| 744 |
+
color=self.colors['text'], fontsize=18, ha='center',
|
| 745 |
+
weight='bold', zorder=30)
|
| 746 |
+
title.set_path_effects([path_effects.withStroke(linewidth=3, foreground='black')])
|
| 747 |
+
|
| 748 |
+
subtitle = ax.text(50, 5, 'Vehicle 1 (Red) runs red light, Vehicle 2 (Blue) T-boned',
|
| 749 |
+
color=self.colors['text'], fontsize=11, ha='center', zorder=30)
|
| 750 |
+
subtitle.set_path_effects([path_effects.withStroke(linewidth=2, foreground='black')])
|
| 751 |
+
|
| 752 |
+
ax.text(3, 92, 'β Red Vehicle: 70 km/h', color='#e74c3c', fontsize=10, weight='bold', zorder=30)
|
| 753 |
+
ax.text(3, 87, 'β Blue Vehicle: 50 km/h', color='#3498db', fontsize=10, weight='bold', zorder=30)
|
| 754 |
+
|
| 755 |
+
frames = 80
|
| 756 |
+
impact_frame = 40
|
| 757 |
+
|
| 758 |
+
car1_patches = []
|
| 759 |
+
car2_patches = []
|
| 760 |
+
impact_patches = []
|
| 761 |
+
|
| 762 |
+
def draw_car(x, y, angle, color, ax):
|
| 763 |
+
"""Draw a car."""
|
| 764 |
+
patches = []
|
| 765 |
+
scale = 1.0
|
| 766 |
+
length = 5 * scale
|
| 767 |
+
width = 2.2 * scale
|
| 768 |
+
|
| 769 |
+
shadow = FancyBboxPatch(
|
| 770 |
+
(x - length/2 + 0.3, y - width/2 - 0.4),
|
| 771 |
+
length, width,
|
| 772 |
+
boxstyle="round,pad=0,rounding_size=0.4",
|
| 773 |
+
facecolor='black', alpha=0.3, zorder=4
|
| 774 |
+
)
|
| 775 |
+
patches.append(shadow)
|
| 776 |
+
|
| 777 |
+
body = FancyBboxPatch(
|
| 778 |
+
(x - length/2, y - width/2),
|
| 779 |
+
length, width,
|
| 780 |
+
boxstyle="round,pad=0,rounding_size=0.4",
|
| 781 |
+
facecolor=color, edgecolor='#1a1a2e', linewidth=2, zorder=10
|
| 782 |
+
)
|
| 783 |
+
patches.append(body)
|
| 784 |
+
|
| 785 |
+
cabin = FancyBboxPatch(
|
| 786 |
+
(x - length*0.1, y - width/2 + 0.25),
|
| 787 |
+
length * 0.35, width - 0.5,
|
| 788 |
+
boxstyle="round,pad=0,rounding_size=0.2",
|
| 789 |
+
facecolor='#2c3e50', edgecolor='#1a252f', linewidth=1, zorder=11
|
| 790 |
+
)
|
| 791 |
+
patches.append(cabin)
|
| 792 |
+
|
| 793 |
+
# Headlights
|
| 794 |
+
hl1 = Circle((x + length/2 - 0.35, y + width/3), 0.3,
|
| 795 |
+
facecolor='#f5f5f5', edgecolor='#bdc3c7', zorder=12)
|
| 796 |
+
hl2 = Circle((x + length/2 - 0.35, y - width/3), 0.3,
|
| 797 |
+
facecolor='#f5f5f5', edgecolor='#bdc3c7', zorder=12)
|
| 798 |
+
patches.extend([hl1, hl2])
|
| 799 |
+
|
| 800 |
+
# Taillights
|
| 801 |
+
tl1 = Rectangle((x - length/2, y + width/3 - 0.15), 0.35, 0.3,
|
| 802 |
+
facecolor='#c0392b', zorder=12)
|
| 803 |
+
tl2 = Rectangle((x - length/2, y - width/3 - 0.15), 0.35, 0.3,
|
| 804 |
+
facecolor='#c0392b', zorder=12)
|
| 805 |
+
patches.extend([tl1, tl2])
|
| 806 |
+
|
| 807 |
+
# Wheels
|
| 808 |
+
wheel_positions = [
|
| 809 |
+
(x - length/2 + length*0.2, y + width/2 + 0.1),
|
| 810 |
+
(x - length/2 + length*0.2, y - width/2 - 0.1),
|
| 811 |
+
(x + length/2 - length*0.2, y + width/2 + 0.1),
|
| 812 |
+
(x + length/2 - length*0.2, y - width/2 - 0.1),
|
| 813 |
+
]
|
| 814 |
+
for wx, wy in wheel_positions:
|
| 815 |
+
tire = Circle((wx, wy), 0.5, facecolor='#1a1a2e', zorder=8)
|
| 816 |
+
rim = Circle((wx, wy), 0.3, facecolor='#7f8c8d', zorder=9)
|
| 817 |
+
patches.extend([tire, rim])
|
| 818 |
+
|
| 819 |
+
transform = Affine2D().rotate_deg_around(x, y, angle) + ax.transData
|
| 820 |
+
for p in patches:
|
| 821 |
+
p.set_transform(transform)
|
| 822 |
+
ax.add_patch(p)
|
| 823 |
+
|
| 824 |
+
return patches
|
| 825 |
+
|
| 826 |
+
def animate(frame):
|
| 827 |
+
nonlocal car1_patches, car2_patches, impact_patches
|
| 828 |
+
|
| 829 |
+
for p in car1_patches + car2_patches + impact_patches:
|
| 830 |
+
try:
|
| 831 |
+
p.remove()
|
| 832 |
+
except:
|
| 833 |
+
pass
|
| 834 |
+
car1_patches = []
|
| 835 |
+
car2_patches = []
|
| 836 |
+
impact_patches = []
|
| 837 |
+
|
| 838 |
+
if frame < impact_frame:
|
| 839 |
+
# Car 1 moving right (horizontal)
|
| 840 |
+
x1 = 10 + frame * 1.1
|
| 841 |
+
y1 = 45
|
| 842 |
+
angle1 = 0
|
| 843 |
+
|
| 844 |
+
# Car 2 moving down (vertical)
|
| 845 |
+
x2 = 55
|
| 846 |
+
y2 = 85 - frame * 1.0
|
| 847 |
+
angle2 = -90
|
| 848 |
+
else:
|
| 849 |
+
post = frame - impact_frame
|
| 850 |
+
# Both cars pushed to the right
|
| 851 |
+
x1 = 10 + impact_frame * 1.1 + post * 0.3
|
| 852 |
+
y1 = 45 + post * 0.4
|
| 853 |
+
angle1 = post * 3
|
| 854 |
+
|
| 855 |
+
x2 = 55 + post * 0.5
|
| 856 |
+
y2 = 85 - impact_frame * 1.0 - post * 0.2
|
| 857 |
+
angle2 = -90 + post * 5
|
| 858 |
+
|
| 859 |
+
car1_patches = draw_car(x1, y1, angle1, self.colors['car1'], ax)
|
| 860 |
+
car2_patches = draw_car(x2, y2, angle2, self.colors['car2'], ax)
|
| 861 |
+
|
| 862 |
+
# Impact effect
|
| 863 |
+
if impact_frame <= frame < impact_frame + 12:
|
| 864 |
+
intensity = 1.0 - (frame - impact_frame) * 0.08
|
| 865 |
+
impact_x = 52
|
| 866 |
+
impact_y = 47
|
| 867 |
+
|
| 868 |
+
for i in range(5):
|
| 869 |
+
radius = (1.5 + i * 0.6) * intensity
|
| 870 |
+
alpha = (0.8 - i * 0.15) * intensity
|
| 871 |
+
colors = ['#ffffff', '#f39c12', '#e74c3c', '#f1c40f', '#e67e22']
|
| 872 |
+
impact = Circle(
|
| 873 |
+
(impact_x, impact_y), radius,
|
| 874 |
+
facecolor=colors[i], alpha=alpha, zorder=25 - i
|
| 875 |
+
)
|
| 876 |
+
ax.add_patch(impact)
|
| 877 |
+
impact_patches.append(impact)
|
| 878 |
+
|
| 879 |
+
return car1_patches + car2_patches
|
| 880 |
+
|
| 881 |
+
anim = animation.FuncAnimation(fig, animate, frames=frames, interval=60, blit=False)
|
| 882 |
+
anim.save(str(output_path), writer='pillow', fps=20)
|
| 883 |
+
plt.close()
|
| 884 |
+
|
| 885 |
+
return str(output_path)
|
| 886 |
+
|
| 887 |
+
def generate_all_scenarios(self):
|
| 888 |
+
"""Generate all professional scenario animations."""
|
| 889 |
+
|
| 890 |
+
print("π¬ Generating professional accident scenario animations...")
|
| 891 |
+
|
| 892 |
+
videos = {}
|
| 893 |
+
|
| 894 |
+
print(" β€ Generating rear-end collision...")
|
| 895 |
+
videos['rear_end_collision'] = self.generate_rear_end_collision()
|
| 896 |
+
print(f" β Saved: {videos['rear_end_collision']}")
|
| 897 |
+
|
| 898 |
+
print(" β€ Generating head-on collision...")
|
| 899 |
+
videos['head_on_collision'] = self.generate_head_on_collision()
|
| 900 |
+
print(f" β Saved: {videos['head_on_collision']}")
|
| 901 |
+
|
| 902 |
+
print(" β€ Generating side impact collision...")
|
| 903 |
+
videos['side_impact'] = self.generate_side_impact_collision()
|
| 904 |
+
print(f" β Saved: {videos['side_impact']}")
|
| 905 |
+
|
| 906 |
+
# Save manifest
|
| 907 |
+
manifest_path = OUTPUT_DIR / "video_manifest.json"
|
| 908 |
+
with open(manifest_path, 'w') as f:
|
| 909 |
+
json.dump(videos, f, indent=2)
|
| 910 |
+
|
| 911 |
+
print(f"\nβ
All professional animations generated!")
|
| 912 |
+
print(f"π Location: {OUTPUT_DIR}")
|
| 913 |
+
|
| 914 |
+
return videos
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
if __name__ == "__main__":
|
| 918 |
+
generator = ProfessionalAccidentVideoGenerator()
|
| 919 |
+
generator.generate_all_scenarios()
|
video_generator_old.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Video Generator for Accident Scenarios
|
| 3 |
+
=======================================
|
| 4 |
+
Generates animated visualizations of accident scenarios.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
import matplotlib.animation as animation
|
| 10 |
+
from matplotlib.patches import Rectangle, Circle, FancyArrow
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
import json
|
| 13 |
+
|
| 14 |
+
# Project paths
|
| 15 |
+
OUTPUT_DIR = Path(__file__).parent.parent / "output" / "visualizations"
|
| 16 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class AccidentVideoGenerator:
|
| 20 |
+
"""Generate animated videos of accident scenarios."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, width=800, height=600, dpi=100):
|
| 23 |
+
"""
|
| 24 |
+
Initialize video generator.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
width: Video width in pixels
|
| 28 |
+
height: Video height in pixels
|
| 29 |
+
dpi: Dots per inch for rendering
|
| 30 |
+
"""
|
| 31 |
+
self.width = width
|
| 32 |
+
self.height = height
|
| 33 |
+
self.dpi = dpi
|
| 34 |
+
self.fig_width = width / dpi
|
| 35 |
+
self.fig_height = height / dpi
|
| 36 |
+
|
| 37 |
+
def generate_rear_end_collision(self, output_path: str = None):
|
| 38 |
+
"""Generate rear-end collision animation."""
|
| 39 |
+
|
| 40 |
+
if output_path is None:
|
| 41 |
+
output_path = OUTPUT_DIR / "rear_end_collision.gif"
|
| 42 |
+
|
| 43 |
+
# Create figure
|
| 44 |
+
fig, ax = plt.subplots(figsize=(self.fig_width, self.fig_height), dpi=self.dpi)
|
| 45 |
+
|
| 46 |
+
# Set up the plot
|
| 47 |
+
ax.set_xlim(0, 100)
|
| 48 |
+
ax.set_ylim(0, 100)
|
| 49 |
+
ax.set_aspect('equal')
|
| 50 |
+
ax.set_facecolor('#2d3436')
|
| 51 |
+
fig.patch.set_facecolor('#1e272e')
|
| 52 |
+
|
| 53 |
+
# Draw road
|
| 54 |
+
road = Rectangle((0, 40), 100, 20, facecolor='#636e72', edgecolor='white', linewidth=2)
|
| 55 |
+
ax.add_patch(road)
|
| 56 |
+
|
| 57 |
+
# Draw lane markings
|
| 58 |
+
for i in range(0, 100, 10):
|
| 59 |
+
ax.plot([i, i+5], [50, 50], 'w--', linewidth=2)
|
| 60 |
+
|
| 61 |
+
# Initialize vehicles
|
| 62 |
+
vehicle1 = Rectangle((10, 45), 8, 10, facecolor='#e74c3c', edgecolor='white', linewidth=2)
|
| 63 |
+
vehicle2 = Rectangle((30, 45), 8, 10, facecolor='#3498db', edgecolor='white', linewidth=2)
|
| 64 |
+
|
| 65 |
+
ax.add_patch(vehicle1)
|
| 66 |
+
ax.add_patch(vehicle2)
|
| 67 |
+
|
| 68 |
+
# Add labels
|
| 69 |
+
ax.text(50, 95, 'Rear-End Collision Scenario',
|
| 70 |
+
color='white', fontsize=16, ha='center', weight='bold')
|
| 71 |
+
ax.text(50, 5, 'Vehicle 1 (Red) approaching Vehicle 2 (Blue) at high speed',
|
| 72 |
+
color='white', fontsize=10, ha='center')
|
| 73 |
+
|
| 74 |
+
ax.axis('off')
|
| 75 |
+
|
| 76 |
+
# Animation frames
|
| 77 |
+
frames = 60
|
| 78 |
+
impact_frame = 40
|
| 79 |
+
|
| 80 |
+
def animate(frame):
|
| 81 |
+
# Vehicle 1 moves fast
|
| 82 |
+
if frame < impact_frame:
|
| 83 |
+
x1 = 10 + (frame * 1.2)
|
| 84 |
+
else:
|
| 85 |
+
# Stop at impact point
|
| 86 |
+
x1 = 10 + (impact_frame * 1.2)
|
| 87 |
+
|
| 88 |
+
# Vehicle 2 moves slowly
|
| 89 |
+
if frame < impact_frame:
|
| 90 |
+
x2 = 30 + (frame * 0.3)
|
| 91 |
+
else:
|
| 92 |
+
# Push forward after impact
|
| 93 |
+
x2 = 30 + (impact_frame * 0.3) + ((frame - impact_frame) * 0.5)
|
| 94 |
+
|
| 95 |
+
vehicle1.set_x(x1)
|
| 96 |
+
vehicle2.set_x(x2)
|
| 97 |
+
|
| 98 |
+
# Add impact effect
|
| 99 |
+
if frame == impact_frame:
|
| 100 |
+
impact = Circle((x1 + 8, 50), 5, color='yellow', alpha=0.6)
|
| 101 |
+
ax.add_patch(impact)
|
| 102 |
+
|
| 103 |
+
return vehicle1, vehicle2
|
| 104 |
+
|
| 105 |
+
# Create animation
|
| 106 |
+
anim = animation.FuncAnimation(fig, animate, frames=frames, interval=50, blit=True)
|
| 107 |
+
|
| 108 |
+
# Save
|
| 109 |
+
anim.save(str(output_path), writer='pillow', fps=20)
|
| 110 |
+
plt.close()
|
| 111 |
+
|
| 112 |
+
return str(output_path)
|
| 113 |
+
|
| 114 |
+
def generate_side_impact_collision(self, output_path: str = None):
|
| 115 |
+
"""Generate side impact collision animation."""
|
| 116 |
+
|
| 117 |
+
if output_path is None:
|
| 118 |
+
output_path = OUTPUT_DIR / "side_impact_collision.gif"
|
| 119 |
+
|
| 120 |
+
# Create figure
|
| 121 |
+
fig, ax = plt.subplots(figsize=(self.fig_width, self.fig_height), dpi=self.dpi)
|
| 122 |
+
|
| 123 |
+
# Set up the plot
|
| 124 |
+
ax.set_xlim(0, 100)
|
| 125 |
+
ax.set_ylim(0, 100)
|
| 126 |
+
ax.set_aspect('equal')
|
| 127 |
+
ax.set_facecolor('#2d3436')
|
| 128 |
+
fig.patch.set_facecolor('#1e272e')
|
| 129 |
+
|
| 130 |
+
# Draw intersection
|
| 131 |
+
# Horizontal road
|
| 132 |
+
h_road = Rectangle((0, 40), 100, 20, facecolor='#636e72', edgecolor='white', linewidth=2)
|
| 133 |
+
ax.add_patch(h_road)
|
| 134 |
+
|
| 135 |
+
# Vertical road
|
| 136 |
+
v_road = Rectangle((40, 0), 20, 100, facecolor='#636e72', edgecolor='white', linewidth=2)
|
| 137 |
+
ax.add_patch(v_road)
|
| 138 |
+
|
| 139 |
+
# Initialize vehicles
|
| 140 |
+
vehicle1 = Rectangle((10, 45), 8, 10, facecolor='#e74c3c', edgecolor='white', linewidth=2)
|
| 141 |
+
vehicle2 = Rectangle((45, 70), 10, 8, facecolor='#3498db', edgecolor='white', linewidth=2)
|
| 142 |
+
|
| 143 |
+
ax.add_patch(vehicle1)
|
| 144 |
+
ax.add_patch(vehicle2)
|
| 145 |
+
|
| 146 |
+
# Add labels
|
| 147 |
+
ax.text(50, 95, 'Side Impact Collision Scenario',
|
| 148 |
+
color='white', fontsize=16, ha='center', weight='bold')
|
| 149 |
+
ax.text(50, 5, 'Vehicle 1 (Red) entering intersection, Vehicle 2 (Blue) from side',
|
| 150 |
+
color='white', fontsize=10, ha='center')
|
| 151 |
+
|
| 152 |
+
ax.axis('off')
|
| 153 |
+
|
| 154 |
+
# Animation frames
|
| 155 |
+
frames = 60
|
| 156 |
+
impact_frame = 35
|
| 157 |
+
|
| 158 |
+
def animate(frame):
|
| 159 |
+
# Vehicle 1 moves horizontally
|
| 160 |
+
if frame < impact_frame:
|
| 161 |
+
x1 = 10 + (frame * 1.0)
|
| 162 |
+
else:
|
| 163 |
+
# Stop at impact
|
| 164 |
+
x1 = 10 + (impact_frame * 1.0)
|
| 165 |
+
# Slight shift from impact
|
| 166 |
+
x1 += (frame - impact_frame) * 0.2
|
| 167 |
+
|
| 168 |
+
# Vehicle 2 moves vertically
|
| 169 |
+
if frame < impact_frame:
|
| 170 |
+
y2 = 70 - (frame * 0.8)
|
| 171 |
+
else:
|
| 172 |
+
# Stop at impact
|
| 173 |
+
y2 = 70 - (impact_frame * 0.8)
|
| 174 |
+
# Slight shift from impact
|
| 175 |
+
y2 += (frame - impact_frame) * 0.2
|
| 176 |
+
|
| 177 |
+
vehicle1.set_x(x1)
|
| 178 |
+
vehicle2.set_y(y2)
|
| 179 |
+
|
| 180 |
+
# Add impact effect
|
| 181 |
+
if frame == impact_frame:
|
| 182 |
+
impact = Circle((x1 + 4, 50), 6, color='yellow', alpha=0.6)
|
| 183 |
+
ax.add_patch(impact)
|
| 184 |
+
|
| 185 |
+
return vehicle1, vehicle2
|
| 186 |
+
|
| 187 |
+
# Create animation
|
| 188 |
+
anim = animation.FuncAnimation(fig, animate, frames=frames, interval=50, blit=True)
|
| 189 |
+
|
| 190 |
+
# Save
|
| 191 |
+
anim.save(str(output_path), writer='pillow', fps=20)
|
| 192 |
+
plt.close()
|
| 193 |
+
|
| 194 |
+
return str(output_path)
|
| 195 |
+
|
| 196 |
+
def generate_head_on_collision(self, output_path: str = None):
|
| 197 |
+
"""Generate head-on collision animation."""
|
| 198 |
+
|
| 199 |
+
if output_path is None:
|
| 200 |
+
output_path = OUTPUT_DIR / "head_on_collision.gif"
|
| 201 |
+
|
| 202 |
+
# Create figure
|
| 203 |
+
fig, ax = plt.subplots(figsize=(self.fig_width, self.fig_height), dpi=self.dpi)
|
| 204 |
+
|
| 205 |
+
# Set up the plot
|
| 206 |
+
ax.set_xlim(0, 100)
|
| 207 |
+
ax.set_ylim(0, 100)
|
| 208 |
+
ax.set_aspect('equal')
|
| 209 |
+
ax.set_facecolor('#2d3436')
|
| 210 |
+
fig.patch.set_facecolor('#1e272e')
|
| 211 |
+
|
| 212 |
+
# Draw road (two-way)
|
| 213 |
+
road = Rectangle((0, 40), 100, 20, facecolor='#636e72', edgecolor='white', linewidth=2)
|
| 214 |
+
ax.add_patch(road)
|
| 215 |
+
|
| 216 |
+
# Draw center line (yellow)
|
| 217 |
+
ax.plot([0, 100], [50, 50], 'y-', linewidth=3)
|
| 218 |
+
|
| 219 |
+
# Draw lane markings
|
| 220 |
+
for i in range(0, 100, 10):
|
| 221 |
+
ax.plot([i, i+5], [45, 45], 'w--', linewidth=1)
|
| 222 |
+
ax.plot([i, i+5], [55, 55], 'w--', linewidth=1)
|
| 223 |
+
|
| 224 |
+
# Initialize vehicles
|
| 225 |
+
vehicle1 = Rectangle((15, 45), 8, 10, facecolor='#e74c3c', edgecolor='white', linewidth=2)
|
| 226 |
+
vehicle2 = Rectangle((75, 51), 8, 10, facecolor='#3498db', edgecolor='white', linewidth=2)
|
| 227 |
+
|
| 228 |
+
ax.add_patch(vehicle1)
|
| 229 |
+
ax.add_patch(vehicle2)
|
| 230 |
+
|
| 231 |
+
# Add labels
|
| 232 |
+
ax.text(50, 95, 'Head-On Collision Scenario',
|
| 233 |
+
color='white', fontsize=16, ha='center', weight='bold')
|
| 234 |
+
ax.text(50, 5, 'Vehicle 1 (Red) crosses center line, Vehicle 2 (Blue) oncoming',
|
| 235 |
+
color='white', fontsize=10, ha='center')
|
| 236 |
+
|
| 237 |
+
ax.axis('off')
|
| 238 |
+
|
| 239 |
+
# Animation frames
|
| 240 |
+
frames = 60
|
| 241 |
+
impact_frame = 35
|
| 242 |
+
|
| 243 |
+
def animate(frame):
|
| 244 |
+
# Vehicle 1 moves right and drifts up
|
| 245 |
+
if frame < impact_frame:
|
| 246 |
+
x1 = 15 + (frame * 0.9)
|
| 247 |
+
y1 = 45 + (frame * 0.15) # Drifting into opposite lane
|
| 248 |
+
else:
|
| 249 |
+
# Stop at impact
|
| 250 |
+
x1 = 15 + (impact_frame * 0.9)
|
| 251 |
+
y1 = 45 + (impact_frame * 0.15)
|
| 252 |
+
# Recoil backward
|
| 253 |
+
x1 -= (frame - impact_frame) * 0.3
|
| 254 |
+
|
| 255 |
+
# Vehicle 2 moves left
|
| 256 |
+
if frame < impact_frame:
|
| 257 |
+
x2 = 75 - (frame * 0.9)
|
| 258 |
+
else:
|
| 259 |
+
# Stop at impact
|
| 260 |
+
x2 = 75 - (impact_frame * 0.9)
|
| 261 |
+
# Recoil backward
|
| 262 |
+
x2 += (frame - impact_frame) * 0.3
|
| 263 |
+
|
| 264 |
+
vehicle1.set_x(x1)
|
| 265 |
+
vehicle1.set_y(y1)
|
| 266 |
+
vehicle2.set_x(x2)
|
| 267 |
+
|
| 268 |
+
# Add impact effect
|
| 269 |
+
if frame == impact_frame:
|
| 270 |
+
impact = Circle((50, 52), 8, color='yellow', alpha=0.6)
|
| 271 |
+
ax.add_patch(impact)
|
| 272 |
+
|
| 273 |
+
return vehicle1, vehicle2
|
| 274 |
+
|
| 275 |
+
# Create animation
|
| 276 |
+
anim = animation.FuncAnimation(fig, animate, frames=frames, interval=50, blit=True)
|
| 277 |
+
|
| 278 |
+
# Save
|
| 279 |
+
anim.save(str(output_path), writer='pillow', fps=20)
|
| 280 |
+
plt.close()
|
| 281 |
+
|
| 282 |
+
return str(output_path)
|
| 283 |
+
|
| 284 |
+
def generate_all_scenarios(self):
|
| 285 |
+
"""Generate all scenario animations."""
|
| 286 |
+
|
| 287 |
+
print("π¬ Generating accident scenario animations...")
|
| 288 |
+
|
| 289 |
+
videos = {}
|
| 290 |
+
|
| 291 |
+
# Generate rear-end collision
|
| 292 |
+
print(" β€ Generating rear-end collision...")
|
| 293 |
+
videos['rear_end_collision'] = self.generate_rear_end_collision()
|
| 294 |
+
print(f" β Saved: {videos['rear_end_collision']}")
|
| 295 |
+
|
| 296 |
+
# Generate side impact
|
| 297 |
+
print(" β€ Generating side impact...")
|
| 298 |
+
videos['side_impact'] = self.generate_side_impact_collision()
|
| 299 |
+
print(f" β Saved: {videos['side_impact']}")
|
| 300 |
+
|
| 301 |
+
# Generate head-on collision
|
| 302 |
+
print(" β€ Generating head-on collision...")
|
| 303 |
+
videos['head_on_collision'] = self.generate_head_on_collision()
|
| 304 |
+
print(f" β Saved: {videos['head_on_collision']}")
|
| 305 |
+
|
| 306 |
+
# Save manifest
|
| 307 |
+
manifest_path = OUTPUT_DIR / "video_manifest.json"
|
| 308 |
+
with open(manifest_path, 'w') as f:
|
| 309 |
+
json.dump(videos, f, indent=2)
|
| 310 |
+
|
| 311 |
+
print(f"\nβ
All animations generated successfully!")
|
| 312 |
+
print(f"π Location: {OUTPUT_DIR}")
|
| 313 |
+
|
| 314 |
+
return videos
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
if __name__ == "__main__":
|
| 318 |
+
# Generate all scenario videos
|
| 319 |
+
generator = AccidentVideoGenerator()
|
| 320 |
+
generator.generate_all_scenarios()
|
video_manifest.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"rear_end_collision": "/home/claude/traffic_project/traffic_accident_analyzer/output/visualizations/rear_end_collision.gif",
|
| 3 |
+
"head_on_collision": "/home/claude/traffic_project/traffic_accident_analyzer/output/visualizations/head_on_collision.gif",
|
| 4 |
+
"side_impact": "/home/claude/traffic_project/traffic_accident_analyzer/output/visualizations/side_impact_collision.gif"
|
| 5 |
+
}
|