CoolFace
Apppublic

premdeep09/ANPR-System

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
dashboard.py409 linesDownload Raw Back to root
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