AccidentAI / streamlit_app.py
krupal02's picture
Upload streamlit_app.py with huggingface_hub
f45c73e verified
Raw
History Blame Contribute Delete
18.6 kB
import streamlit as st
import cv2
import numpy as np
from ultralytics import YOLO
import requests
import math
import time
import os
from tempfile import NamedTemporaryFile
import folium
from streamlit_folium import st_folium
# ============================================================
# CONFIGURATION
# ============================================================
MODEL_PATH = "model/best.pt"
CLASS_NAMES = {0: "Accident", 1: "Non-accident", 2: "Fire"}
ALERT_CLASSES = {"Accident", "Fire"}
DEFAULT_CONFIDENCE = 0.3
HOSPITAL_RADIUS_M = 5000
# ============================================================
# PAGE CONFIG
# ============================================================
st.set_page_config(
page_title="AccidentAI — Real-Time Detection & Alert",
page_icon="🚨",
layout="wide",
initial_sidebar_state="expanded",
)
# ============================================================
# CUSTOM CSS
# ============================================================
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&display=swap');
html, body, .stApp {
font-family: 'Inter', sans-serif !important;
}
/* Hero */
.hero-wrap {
text-align: center;
padding: 2.5rem 1rem 1.5rem;
}
.hero-wrap h1 {
font-size: 2.8rem;
font-weight: 900;
background: linear-gradient(135deg, #ff3b30, #ff9500, #ff3b30);
background-size: 200% 200%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: grad 4s ease infinite;
margin-bottom: 0.25rem;
}
@keyframes grad {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.hero-wrap .subtitle {
font-size: 1.1rem;
color: #aaa;
font-weight: 400;
}
.hero-wrap .pills {
margin-top: 0.75rem;
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
.hero-wrap .pill {
background: rgba(255,59,48,0.12);
border: 1px solid rgba(255,59,48,0.25);
color: #ff6b5e;
padding: 0.3rem 0.85rem;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 500;
}
/* Glass card */
.glass {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 1.5rem;
backdrop-filter: blur(12px);
margin-bottom: 1rem;
}
/* Stat box */
.stat-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin: 0.5rem 0;
}
.stat-box {
flex: 1;
min-width: 120px;
text-align: center;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 1rem 0.75rem;
}
.stat-box .num {
font-size: 1.8rem;
font-weight: 700;
}
.stat-box .lbl {
font-size: 0.75rem;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Alert banner */
@keyframes pulse-border {
0%, 100% { border-color: rgba(255,59,48,0.25); }
50% { border-color: rgba(255,59,48,0.65); }
}
.alert-banner {
background: linear-gradient(135deg, rgba(255,59,48,0.10), rgba(255,149,0,0.06));
border: 2px solid rgba(255,59,48,0.35);
border-radius: 14px;
padding: 1.25rem 1.5rem;
animation: pulse-border 2s ease infinite;
margin-bottom: 1rem;
}
.alert-banner h3 {
margin: 0 0 0.25rem;
color: #ff5e57;
}
.alert-banner p {
margin: 0;
color: #ccc;
font-size: 0.92rem;
}
/* Hospital card */
.hosp-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.09);
border-radius: 12px;
padding: 1rem 1.25rem;
margin-bottom: 0.65rem;
transition: border-color 0.25s;
}
.hosp-card:hover {
border-color: rgba(52,199,89,0.45);
}
.hosp-card .name {
font-weight: 600;
font-size: 1rem;
color: #e0e0e0;
}
.hosp-card .meta {
font-size: 0.82rem;
color: #999;
margin-top: 0.25rem;
}
/* Notification log entry */
.notif-entry {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 1rem;
border-radius: 10px;
margin-bottom: 0.4rem;
font-size: 0.88rem;
}
.notif-ok {
background: rgba(52,199,89,0.10);
border: 1px solid rgba(52,199,89,0.20);
color: #34c759;
}
/* Hide default header & footer */
#MainMenu, header, footer { visibility: hidden; }
/* Folium map container */
iframe { border-radius: 14px !important; }
</style>
""", unsafe_allow_html=True)
# ============================================================
# MODEL LOADING
# ============================================================
@st.cache_resource
def load_model():
if not os.path.exists(MODEL_PATH):
st.error(f"Model file not found at `{MODEL_PATH}`.")
st.stop()
return YOLO(MODEL_PATH)
model = load_model()
# ============================================================
# UTILITY FUNCTIONS
# ============================================================
def haversine_km(lat1, lon1, lat2, lon2):
"""Return distance in km between two lat/lon points."""
R = 6371
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (
math.sin(dlat / 2) ** 2
+ math.cos(math.radians(lat1))
* math.cos(math.radians(lat2))
* math.sin(dlon / 2) ** 2
)
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
# ============================================================
# DETECTION FUNCTIONS
# ============================================================
def detect_in_image(image_array, conf):
"""Run YOLO on a single image. Returns (detections, annotated_rgb)."""
results = model.predict(image_array, conf=conf, verbose=False)
detections = []
for result in results:
for box in result.boxes:
cls_id = int(box.cls[0])
detections.append(
{
"label": CLASS_NAMES.get(cls_id, "Unknown"),
"confidence": float(box.conf[0]),
}
)
annotated_bgr = results[0].plot()
annotated_rgb = cv2.cvtColor(annotated_bgr, cv2.COLOR_BGR2RGB)
return detections, annotated_rgb
def detect_in_video(video_path, conf, frame_skip=4, progress_cb=None):
"""Run YOLO on video frames. Returns (detections, best_annotated_rgb)."""
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 1
detections = []
best_frame = None
best_conf = 0
idx = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if idx % frame_skip == 0:
results = model.predict(frame, conf=conf, verbose=False)
for result in results:
for box in result.boxes:
cls_id = int(box.cls[0])
c = float(box.conf[0])
label = CLASS_NAMES.get(cls_id, "Unknown")
if label in ALERT_CLASSES:
detections.append(
{"label": label, "confidence": c, "frame": idx}
)
if c > best_conf:
best_conf = c
best_frame = results[0].plot()
idx += 1
if progress_cb:
progress_cb(min(idx / total, 1.0))
cap.release()
if best_frame is not None:
best_frame = cv2.cvtColor(best_frame, cv2.COLOR_BGR2RGB)
return detections, best_frame
# ============================================================
# LOCATION & HOSPITAL FUNCTIONS
# ============================================================
def get_ip_location():
"""Free IP-based geolocation (no API key required)."""
try:
r = requests.get("http://ip-api.com/json/", timeout=5)
d = r.json()
if d.get("status") == "success":
return {
"lat": d["lat"],
"lon": d["lon"],
"city": d.get("city", ""),
"region": d.get("regionName", ""),
"country": d.get("country", ""),
}
except Exception:
pass
return None
def fetch_hospitals(lat, lon, radius_m=5000):
"""Query OpenStreetMap Overpass API for hospitals within radius."""
query = f"""
[out:json][timeout:10];
(
node["amenity"="hospital"](around:{radius_m},{lat},{lon});
way["amenity"="hospital"](around:{radius_m},{lat},{lon});
relation["amenity"="hospital"](around:{radius_m},{lat},{lon});
);
out center body;
"""
try:
r = requests.post(
"https://overpass-api.de/api/interpreter",
data={"data": query},
timeout=15,
)
elements = r.json().get("elements", [])
except Exception:
return []
hospitals = []
for el in elements:
tags = el.get("tags", {})
if el["type"] == "node":
h_lat, h_lon = el["lat"], el["lon"]
else:
c = el.get("center", {})
h_lat = c.get("lat", lat)
h_lon = c.get("lon", lon)
dist = haversine_km(lat, lon, h_lat, h_lon)
hospitals.append(
{
"name": tags.get("name", "Unnamed Hospital"),
"lat": h_lat,
"lon": h_lon,
"distance_km": round(dist, 2),
"phone": tags.get("phone", tags.get("contact:phone", "—")),
"beds": tags.get("beds", "—"),
"emergency": tags.get("emergency", "unknown"),
}
)
hospitals.sort(key=lambda h: h["distance_km"])
return hospitals
def build_map(lat, lon, hospitals, radius_m=5000):
"""Create a folium map with accident marker, radius circle, and hospitals."""
m = folium.Map(location=[lat, lon], zoom_start=14, tiles="CartoDB dark_matter")
# Accident location
folium.Marker(
[lat, lon],
popup="🚨 Accident",
icon=folium.Icon(color="red", icon="exclamation-triangle", prefix="fa"),
).add_to(m)
# Radius
folium.Circle(
[lat, lon],
radius=radius_m,
color="#ff3b30",
fill=True,
fill_opacity=0.06,
weight=2,
dash_array="6 4",
).add_to(m)
# Hospitals
for h in hospitals:
folium.Marker(
[h["lat"], h["lon"]],
popup=f"🏥 {h['name']}{h['distance_km']} km",
icon=folium.Icon(color="green", icon="plus-square", prefix="fa"),
).add_to(m)
return m
# ============================================================
# SIDEBAR
# ============================================================
with st.sidebar:
st.markdown("## ⚙️ Detection Settings")
confidence = st.slider(
"Confidence threshold", 0.10, 0.90, DEFAULT_CONFIDENCE, 0.05
)
st.markdown("---")
st.markdown("## 📍 Location")
st.caption(
"Location is auto-detected via IP. On cloud deployments this returns "
"the server location — use manual override for accurate testing."
)
use_manual = st.checkbox("Use manual coordinates")
if use_manual:
manual_lat = st.number_input("Latitude", value=28.6139, format="%.4f")
manual_lon = st.number_input("Longitude", value=77.2090, format="%.4f")
else:
manual_lat, manual_lon = None, None
st.markdown("---")
st.markdown("## ℹ️ About")
st.markdown(
"**AccidentAI** uses a custom-trained **YOLOv8** model to detect "
"accidents and fires in CCTV footage, then automatically locates "
"nearby hospitals and sends simulated emergency alerts."
)
st.markdown(
"Built with Ultralytics, Streamlit, OpenStreetMap Overpass API, and Folium."
)
# ============================================================
# HERO
# ============================================================
st.markdown(
"""
<div class="hero-wrap">
<h1>🚨 AccidentAI</h1>
<p class="subtitle">Real-Time Accident Detection &amp; Emergency Alert System</p>
<div class="pills">
<span class="pill">YOLOv8</span>
<span class="pill">CCTV Analysis</span>
<span class="pill">Hospital Alerts</span>
<span class="pill">Live Map</span>
</div>
</div>
""",
unsafe_allow_html=True,
)
# ============================================================
# FILE UPLOAD
# ============================================================
uploaded = st.file_uploader(
"Upload CCTV footage or image",
type=["jpg", "jpeg", "png", "mp4", "avi", "mov"],
help="Supported formats: JPG, PNG images and MP4, AVI, MOV videos",
)
if not uploaded:
st.info("👆 Upload an image or video to start detection.")
st.stop()
# ============================================================
# RUN DETECTION
# ============================================================
is_video = uploaded.name.lower().endswith((".mp4", ".avi", ".mov"))
if is_video:
with NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
tmp.write(uploaded.read())
tmp_path = tmp.name
st.markdown("### 🎬 Analyzing video frames…")
pbar = st.progress(0)
detections, annotated = detect_in_video(
tmp_path, confidence, progress_cb=pbar.progress
)
pbar.empty()
try:
os.unlink(tmp_path)
except OSError:
pass
else:
raw = np.asarray(bytearray(uploaded.read()), dtype=np.uint8)
image = cv2.imdecode(raw, cv2.IMREAD_COLOR)
detections, annotated = detect_in_image(image, confidence)
alert_dets = [d for d in detections if d["label"] in ALERT_CLASSES]
# ============================================================
# RESULTS
# ============================================================
st.markdown("---")
col_img, col_stats = st.columns([2, 1], gap="large")
with col_img:
st.markdown("### 🔍 Detection Output")
if annotated is not None:
st.image(annotated, use_container_width=True)
elif is_video:
st.info("No hazard frames captured — video appears safe.")
with col_stats:
st.markdown("### 📊 Analysis")
n_acc = sum(1 for d in alert_dets if d["label"] == "Accident")
n_fire = sum(1 for d in alert_dets if d["label"] == "Fire")
total = len(detections)
st.markdown(
f"""
<div class="stat-row">
<div class="stat-box">
<div class="num" style="color:#ff5e57">{n_acc}</div>
<div class="lbl">Accidents</div>
</div>
<div class="stat-box">
<div class="num" style="color:#ff9500">{n_fire}</div>
<div class="lbl">Fires</div>
</div>
<div class="stat-box">
<div class="num" style="color:#34c759">{total}</div>
<div class="lbl">Total Detections</div>
</div>
</div>
""",
unsafe_allow_html=True,
)
if alert_dets:
peak = max(d["confidence"] for d in alert_dets)
avg = sum(d["confidence"] for d in alert_dets) / len(alert_dets)
st.metric("Peak Confidence", f"{peak:.1%}")
st.metric("Avg Confidence", f"{avg:.1%}")
if is_video:
frames_hit = len(set(d.get("frame", 0) for d in alert_dets))
st.metric("Frames with Hazards", frames_hit)
else:
st.success("✅ No hazards detected in the uploaded media.")
# ============================================================
# EMERGENCY ALERT SYSTEM
# ============================================================
if not alert_dets:
st.stop()
st.markdown("---")
# Alert banner
top_label = "Accident" if n_acc else "Fire"
st.markdown(
f"""
<div class="alert-banner">
<h3>🚨 EMERGENCY — {top_label} Detected</h3>
<p>Initiating automated alert protocol • Searching hospitals within 5 km radius</p>
</div>
""",
unsafe_allow_html=True,
)
# -- Get location --
if use_manual and manual_lat is not None:
loc = {
"lat": manual_lat,
"lon": manual_lon,
"city": "Manual",
"region": "",
"country": "",
}
else:
with st.spinner("📍 Detecting location…"):
loc = get_ip_location()
if loc is None:
st.warning(
"Could not detect location automatically. "
"Enable **manual coordinates** in the sidebar."
)
st.stop()
loc_str = ", ".join(filter(None, [loc["city"], loc["region"], loc["country"]]))
st.markdown(
f"**📍 Incident Location:** {loc_str} &nbsp;|&nbsp; "
f"`{loc['lat']:.4f}, {loc['lon']:.4f}`"
)
# -- Fetch hospitals --
with st.spinner("🏥 Querying OpenStreetMap for nearby hospitals…"):
hospitals = fetch_hospitals(loc["lat"], loc["lon"], HOSPITAL_RADIUS_M)
if not hospitals:
st.warning(
"No hospitals found within 5 km. Try different coordinates or increase radius."
)
st.stop()
st.markdown(f"**Found {len(hospitals)} hospital(s) within 5 km**")
# -- Map & hospital list side by side --
col_map, col_list = st.columns([1, 1], gap="large")
with col_map:
st.markdown("### 🗺️ Incident Map")
m = build_map(loc["lat"], loc["lon"], hospitals)
st_folium(m, height=420, use_container_width=True)
with col_list:
st.markdown("### 🏥 Nearby Hospitals")
for i, h in enumerate(hospitals):
emoji = "🥇" if i == 0 else "🥈" if i == 1 else "🥉" if i == 2 else "🏥"
st.markdown(
f"""<div class="hosp-card">
<div class="name">{emoji} {h['name']}</div>
<div class="meta">
📏 {h['distance_km']} km &nbsp;|&nbsp;
📞 {h['phone']} &nbsp;|&nbsp;
🛏️ Beds: {h['beds']}
</div>
</div>""",
unsafe_allow_html=True,
)
if len(hospitals) > 8:
st.caption(f"Showing all {len(hospitals)} results")
# ============================================================
# SIMULATED NOTIFICATIONS
# ============================================================
st.markdown("---")
st.markdown("### 📨 Alert Dispatch Log")
st.caption("Sending automated emergency alerts to the nearest hospitals…")
notify_count = min(len(hospitals), 3)
log_container = st.container()
for i in range(notify_count):
h = hospitals[i]
time.sleep(0.7)
log_container.markdown(
f"""<div class="notif-entry notif-ok">
✅ &nbsp; Alert dispatched to <b>{h['name']}</b> — {h['distance_km']} km away
</div>""",
unsafe_allow_html=True,
)
st.success(
f"✅ Emergency alerts sent to **{notify_count}** hospital(s). "
f"Nearest: **{hospitals[0]['name']}** ({hospitals[0]['distance_km']} km)"
)