premdeep09/ANPR-System
0
1import streamlit as st
2import pandas as pd
3import requests
4import time
5import os
6from streamlit_autorefresh import st_autorefresh
7
8import socket
9
10def get_local_ip():
11 try:
12 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
13 s.connect(('10.255.255.255', 1))
14 IP = s.getsockname()[0]
15 except Exception:
16 IP = '127.0.0.1'
17 finally:
18 s.close()
19 return IP
20
21# Configuration
22# On Hugging Face/Docker, FastAPI and Streamlit run on the same machine.
23# We use localhost for internal communication.
24API_BASE_URL = os.getenv("API_URL", "http://127.0.0.1:8000/api")
25REFRESH_INTERVAL_MS = 5000 # 5 seconds
26
27# Set up page configuration
28st.set_page_config(
29 page_title="ANPR Dashboard",
30 page_icon="π",
31 layout="wide",
32 initial_sidebar_state="expanded"
33)
34
35# Custom CSS will be injected dynamically based on the selected theme later.
36
37# Auto-refresh component
38count = st_autorefresh(interval=REFRESH_INTERVAL_MS, limit=None, key="data_refresh")
39
40# Fetch data functions
41def fetch_stats():
42 try:
43 response = requests.get(f"{API_BASE_URL}/stats")
44 response.raise_for_status()
45 return response.json()
46 except requests.exceptions.RequestException as e:
47 st.error(f"Error fetching stats: {e}")
48 return {"total_vehicles_today": 0, "currently_inside": 0}
49
50def fetch_vehicles():
51 try:
52 response = requests.get(f"{API_BASE_URL}/vehicles")
53 response.raise_for_status()
54 return response.json()
55 except requests.exceptions.RequestException as e:
56 st.error(f"Error fetching vehicles: {e}")
57 return []
58
59def submit_manual_entry(plate, v_type):
60 try:
61 payload = {"plate_number": plate, "vehicle_type": v_type}
62 response = requests.post(f"{API_BASE_URL}/manual_entry", json=payload)
63 response.raise_for_status()
64 return True, response.json().get("message", "Success")
65 except requests.exceptions.RequestException as e:
66 return False, str(e)
67
68def submit_video_source(source_type, rtsp_url=None, file=None):
69 try:
70 data = {"source_type": source_type}
71 if rtsp_url:
72 data["rtsp_url"] = rtsp_url
73
74 files = None
75 if file:
76 files = {"file": (file.name, file.getvalue(), file.type)}
77
78 response = requests.post(f"{API_BASE_URL}/video_source", data=data, files=files)
79 response.raise_for_status()
80 return True, response.json().get("message", "Success")
81 except requests.exceptions.RequestException as e:
82 return False, str(e)
83
84
85
86# -- Sidebar: Settings & Controls --
87with st.sidebar:
88 st.header("βοΈ System Control")
89
90 st.subheader("π¨ Theme Settings")
91 theme = st.selectbox("Dashboard Theme", ["Dark Mode", "Light Mode"])
92
93 # Define and inject dynamic CSS based on theme
94 if theme == "Dark Mode":
95 primary_color = "#6366F1" # Modern Indigo
96 accent_color = "#10B981" # Emerald
97 bg_color = "#0F172A" # Slate 900
98 card_bg = "#1E293B" # Slate 800
99 text_primary = "#F8FAFC" # Slate 50
100 text_secondary = "#94A3B8" # Slate 400
101 border_color = "#334155" # Slate 700
102 else:
103 primary_color = "#4F46E5" # Indigo
104 accent_color = "#059669" # Emerald
105 bg_color = "#F8FAFC" # Slate 50
106 card_bg = "#FFFFFF" # White
107 text_primary = "#0F172A" # Slate 900
108 text_secondary = "#64748B" # Slate 500
109 border_color = "#E2E8F0" # Slate 200
110
111 st.markdown(f"""
112 <style>
113 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
114
115 :root {{
116 --primary-color: {primary_color};
117 --accent-color: {accent_color};
118 --bg-color: {bg_color};
119 --card-bg: {card_bg};
120 --text-primary: {text_primary};
121 --text-secondary: {text_secondary};
122 --border-color: {border_color};
123 }}
124
125 html, body, [class*="css"] {{
126 font-family: 'Inter', sans-serif;
127 }}
128
129 /* App Background */
130 .stApp {{
131 background-color: var(--bg-color);
132 }}
133
134 /* Sidebar Background */
135 [data-testid="stSidebar"] > div:first-child {{
136 background-color: var(--card-bg) !important;
137 border-right: 1px solid var(--border-color);
138 }}
139
140 /* Top Header */
141 [data-testid="stHeader"] {{
142 background-color: transparent;
143 }}
144
145 /* Reduce gap in sidebar */
146 [data-testid="stSidebar"] [data-testid="stVerticalBlock"] {{
147 gap: 0.5rem !important;
148 }}
149 [data-testid="stSidebar"] hr {{
150 margin-top: 0.5rem;
151 margin-bottom: 0.5rem;
152 }}
153
154 /* Maximize width of the main container to give table more space */
155 .block-container {{
156 padding-left: 1.5rem !important;
157 padding-right: 1.5rem !important;
158 max-width: 100% !important;
159 }}
160
161 /* Text Colors */
162 h1, h2, h3, h4, h5, h6, .stMarkdown p {{
163 color: var(--text-primary) !important;
164 }}
165
166 label, .stText, .stCaption {{
167 color: var(--text-secondary) !important;
168 }}
169
170 /* Metric Cards */
171 [data-testid="stMetric"] {{
172 background-color: var(--card-bg);
173 border-radius: 16px;
174 padding: 24px;
175 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
176 border: 1px solid var(--border-color);
177 transition: transform 0.2s ease, box-shadow 0.2s ease;
178 }}
179 [data-testid="stMetric"]:hover {{
180 transform: translateY(-5px);
181 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
182 }}
183 [data-testid="stMetricLabel"] > div {{
184 font-size: 1.1rem;
185 font-weight: 600;
186 color: var(--text-secondary) !important;
187 }}
188 [data-testid="stMetricValue"] > div {{
189 font-size: 2.8rem;
190 font-weight: 800;
191 color: var(--primary-color) !important;
192 }}
193
194 /* Form & Cards */
195 [data-testid="stForm"] {{
196 border-radius: 16px;
197 border: 1px solid var(--border-color);
198 background-color: var(--card-bg);
199 padding: 20px;
200 }}
201
202 /* Video Feed Container */
203 .video-container {{
204 background-color: var(--card-bg);
205 padding: 16px;
206 border-radius: 16px;
207 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
208 border: 1px solid var(--border-color);
209 margin-top: 10px;
210 }}
211 .video-container img {{
212 border-radius: 12px;
213 width: 100%;
214 border: 2px solid var(--border-color);
215 }}
216 </style>
217 """, unsafe_allow_html=True)
218
219 st.markdown("---")
220 st.subheader("Manual OCR Entry")
221 st.caption("Use this fallback if OCR fails to detect the plate.")
222
223 with st.form("manual_entry_form", clear_on_submit=True):
224 plate_input = st.text_input("Plate Number", placeholder="e.g. MH12AB1234")
225 type_input = st.selectbox("Vehicle Type", ["Car", "Truck", "Two-Wheeler", "Van", "Other"])
226
227 submitted = st.form_submit_button("Add Entry")
228 if submitted:
229 if plate_input:
230 success, msg = submit_manual_entry(plate_input, type_input)
231 if success:
232 st.success("Entry added!", icon="β
")
233 # Force a rerun to show the new data immediately, though autorefresh will catch it in 5s
234 # time.sleep(0.5)
235 # st.rerun()
236 else:
237 st.error(f"Failed: {msg}")
238 else:
239 st.warning("Please enter a plate number.")
240
241 st.markdown("---")
242 st.subheader("Video Source Control")
243 st.caption("Change the live feed source.")
244
245 # Initialize state
246 if "current_source_type" not in st.session_state:
247 st.session_state.current_source_type = "Webcam"
248 if "current_rtsp" not in st.session_state:
249 st.session_state.current_rtsp = ""
250 if "processed_file_id" not in st.session_state:
251 st.session_state.processed_file_id = None
252
253 source_options = ["Webcam", "CCTV Stream", "Upload Video", "Upload Picture"]
254
255 # On selectbox change, user must click a prominent button to take action
256 selected_source = st.selectbox("Select Source", source_options,
257 index=source_options.index(st.session_state.current_source_type) if st.session_state.current_source_type in source_options else 0)
258
259 if selected_source == "Webcam":
260 st.info("Start the system camera feed and stream it live on the dashboard.")
261 if st.button("π· Start Webcam", type="primary", use_container_width=True):
262 with st.spinner("Connecting to System Camera..."):
263 success, msg = submit_video_source("Webcam")
264 if success:
265 st.session_state.current_source_type = "Webcam"
266 st.success("Connected to Webcam! Streaming on the side screen.", icon="β
")
267 else:
268 st.error(f"Failed to connect: {msg}")
269
270 elif selected_source == "CCTV Stream":
271 rtsp_input = st.text_input("RTSP/HTTP URL", placeholder="rtsp://admin:pass@192.168.1.100:554/stream", value=st.session_state.current_rtsp)
272 if st.button("Connect CCTV", type="primary", use_container_width=True):
273 if not rtsp_input:
274 st.warning("Please enter a valid RTSP/HTTP URL.")
275 else:
276 with st.spinner("Connecting Stream..."):
277 success, msg = submit_video_source("CCTV Stream", rtsp_input)
278 if success:
279 st.session_state.current_source_type = "CCTV Stream"
280 st.session_state.current_rtsp = rtsp_input
281 st.success("Connected to Stream!", icon="β
")
282 else:
283 st.error(f"Failed to connect: {msg}")
284
285 elif selected_source in ["Upload Video", "Upload Picture"]:
286 file_types = ["mp4", "avi", "mov", "mkv"] if selected_source == "Upload Video" else ["jpg", "jpeg", "png", "bmp"]
287 uploaded_file = st.file_uploader(f"Upload {selected_source.split()[1]} File", type=file_types)
288
289 if uploaded_file is not None:
290 if st.button("Upload / Enter", type="primary", use_container_width=True):
291 with st.spinner("Processing uploaded file..."):
292 success, msg = submit_video_source(selected_source, None, uploaded_file)
293 if success:
294 st.session_state.current_source_type = selected_source
295 st.success(f"{selected_source.split()[1]} uploaded successfully! Processing...", icon="β
")
296 # Add a small delay so pipelines can parse it before we refresh dashboard table
297 if selected_source == "Upload Picture":
298 time.sleep(3.5)
299 st.rerun()
300 else:
301 st.error(f"Failed to process file: {msg}")
302
303# -- Main Dashboard Area --
304st.title("πVehicle Monitoring System")
305st.markdown("Real-time vehicle monitoring and automatic number plate recognition dashboard.")
306
307# Create Two Columns: Left for Data, Right for Video
308main_col, video_col = st.columns([3, 2])
309
310with main_col:
311 # Fetch data
312 stats_data = fetch_stats()
313 vehicles_data = fetch_vehicles()
314
315 # Display Metrics
316 col1, col2, col3 = st.columns(3)
317
318 with col1:
319 st.metric(label="Total Vehicles Today", value=stats_data.get("total_vehicles_today", 0), delta="Active")
320
321 with col2:
322 st.metric(label="Currently Inside", value=stats_data.get("currently_inside", 0), delta="Live", delta_color="normal")
323
324 with col3:
325 blacklisted_count = len([v for v in vehicles_data if v.get("blacklisted")])
326 st.metric(label="Blacklisted Intercepts", value=blacklisted_count, delta="Alerts", delta_color="inverse")
327
328 # Data Table Section
329 st.markdown("<div style='margin-top: -10px;'></div>", unsafe_allow_html=True)
330 st.subheader("π Recent Vehicle Logs")
331
332 # Filtering and Search
333 filter_col1, filter_col2, filter_col3 = st.columns([2, 1, 1])
334 with filter_col1:
335 search_query = st.text_input("π Search Plate Number", "")
336 with filter_col2:
337 status_filter = st.selectbox("Filter Status", ["All", "INSIDE", "EXITED"])
338 with filter_col3:
339 type_filter = st.selectbox("Filter Type", ["All", "Car", "Truck", "Two-Wheeler", "Van"])
340
341 # Process Data for Table
342 if vehicles_data:
343 df = pd.DataFrame(vehicles_data)
344
345 # Apply Filters
346 if search_query:
347 df = df[df['plate_number'].str.contains(search_query, case=False, na=False)]
348 if status_filter != "All":
349 df = df[df['status'] == status_filter]
350 if type_filter != "All":
351 df = df[df['vehicle_type'] == type_filter]
352
353 # Reorder columns for display
354 display_cols = ['plate_number', 'vehicle_type', 'entry_time', 'exit_time', 'status', 'blacklisted']
355 df_display = df[display_cols].copy().reset_index(drop=True)
356
357 # Optional styling: highlight blacklisted vehicles and add zebra striping
358 def style_rows(row):
359 if row['blacklisted']:
360 return ['background-color: rgba(239, 68, 68, 0.15); color: #EF4444; font-weight: bold'] * len(row)
361 elif row.name % 2 == 0:
362 return ['background-color: rgba(128, 128, 128, 0.05)'] * len(row)
363 return [''] * len(row)
364
365 styled_df = df_display.style.apply(style_rows, axis=1)
366
367 st.dataframe(
368 styled_df,
369 width="stretch",
370 hide_index=True,
371 column_config={
372 "plate_number": st.column_config.TextColumn("Plate Number", width="medium"),
373 "vehicle_type": st.column_config.TextColumn("Type", width="small"),
374 "entry_time": st.column_config.DatetimeColumn("Entry Time", format="YYYY-MM-DD HH:mm:ss", width="medium"),
375 "exit_time": st.column_config.DatetimeColumn("Exit Time", format="YYYY-MM-DD HH:mm:ss", width="medium"),
376 "status": st.column_config.TextColumn("Status", width="small"),
377 "blacklisted": st.column_config.CheckboxColumn("Blacklisted?", width="small")
378 },
379 height=400
380 )
381
382 csv = df.to_csv(index=False).encode('utf-8')
383 st.download_button(
384 label="Download Data as CSV",
385 data=csv,
386 file_name='anpr_logs.csv',
387 mime='text/csv',
388 )
389 else:
390 st.info("No vehicle data available yet.")
391
392with video_col:
393 st.subheader("π₯ Live Camera Feed")
394 st.markdown("Automated plate detection feed pulling straight from the YOLOv8 pipeline.")
395
396 # Use requests to fetch the image bytes internally on the server
397 # This ensures the browser never has to talk to port 8000
398 try:
399 response = requests.get(f"{API_BASE_URL}/latest_frame", timeout=2)
400 if response.status_code == 200:
401 st.image(response.content, width='stretch', caption="Live Detection Stream")
402 else:
403 st.warning("Waiting for pipeline to start...")
404 except Exception as e:
405 st.error("Could not connect to backend pipeline.")
406
407# Footer auto-refresh indicator
408st.caption(f"Dashboard auto-refreshes every {REFRESH_INTERVAL_MS // 1000} seconds. Last fetched: {time.strftime('%H:%M:%S')}")
409 