fire-watch-co / app.py
tstone87's picture
Update app.py
7b0fee6 verified
import cv2
import streamlit as st
from ultralytics import YOLO
import time
import numpy as np
from datetime import datetime
import pytz
# Page config and header
st.set_page_config(
page_title="Fire Watch: AI-Powered Fire and Smoke Detection",
page_icon="🔥",
layout="wide",
initial_sidebar_state="expanded"
)
st.title("Fire Watch: Fire Detection with an AI Vision Model")
# --- Session State Initialization ---
if "streams" not in st.session_state:
st.session_state.streams = []
if "num_streams" not in st.session_state:
st.session_state.num_streams = 1
if "confidence" not in st.session_state:
st.session_state.confidence = 0.30 # default 30%
if "target_fps" not in st.session_state:
st.session_state.target_fps = 1.0 # default 1 FPS
# --- Default URLs and Names for 10 Streams ---
default_m3u8_urls = [
"https://publicstreamer2.cotrip.org/rtplive/070E27555CAM1RP1/playlist.m3u8", # EB @ York St Denver
"https://publicstreamer1.cotrip.org/rtplive/225N00535CAM1RP1/playlist.m3u8", # NB at Iliff Denver
"https://publicstreamer2.cotrip.org/rtplive/070W28220CAM1RHS/playlist.m3u8", # WB Half Mile West of I225 Denver
"https://publicstreamer1.cotrip.org/rtplive/070W26805CAM1RHS/playlist.m3u8", # 1 mile E of Kipling Denver
"https://publicstreamer4.cotrip.org/rtplive/076W03150CAM1RP1/playlist.m3u8", # Main St Hudson
"https://publicstreamer2.cotrip.org/rtplive/070E27660CAM1NEC/playlist.m3u8", # EB Colorado Blvd i70 Denver
"https://publicstreamer2.cotrip.org/rtplive/070W27475CAM1RHS/playlist.m3u8", # E of Washington St Denver
"https://publicstreamer3.cotrip.org/rtplive/070W28155CAM1RHS/playlist.m3u8", # WB Peroia St Underpass Denver
"https://publicstreamer3.cotrip.org/rtplive/070E11660CAM1RHS/playlist.m3u8", # Grand Ave Glenwood
"https://publicstreamer4.cotrip.org/rtplive/070E27890CAM1RHS/playlist.m3u8" # EB at i270
]
default_names = [
"EB @ York St Denver",
"NB at Iliff Denver",
"WB Half Mile West of I225 Denver",
"1 mile E of Kipling Denver",
"Main St Hudson",
"EB Colorado Blvd i70 Denver",
"E of Washington St Denver",
"WB Peroia St Underpass Denver",
"Grand Ave Glenwood",
"EB at i270"
]
# --- Sidebar Settings ---
with st.sidebar:
st.header("Stream Settings")
# Custom configuration for stream 1 only.
custom_m3u8 = st.text_input("Custom M3U8 URL for Stream 1 (optional)", value="", key="custom_m3u8")
custom_name = st.text_input("Custom Webcam Name for Stream 1 (optional)", value="", key="custom_name")
# Choose number of streams (1 to 10)
num_streams = st.selectbox("Number of Streams", list(range(1, 11)), index=0)
st.session_state.num_streams = num_streams
# Global settings for confidence and processing rate.
confidence = float(st.slider("Confidence Threshold", 5, 100, 30)) / 100
st.session_state.confidence = confidence
fps_options = {
"1 FPS": 1,
"1 frame/2s": 0.5,
"1 frame/3s": 0.3333,
"1 frame/5s": 0.2,
"1 frame/15s": 0.0667,
"1 frame/30s": 0.0333
}
video_option = st.selectbox("Processing Rate", list(fps_options.keys()), index=3)
st.session_state.target_fps = fps_options[video_option]
# Update or initialize the streams using defaults (with custom override for stream 1).
if len(st.session_state.streams) != st.session_state.num_streams:
st.session_state.streams = []
for i in range(st.session_state.num_streams):
if i == 0:
url = custom_m3u8.strip() if custom_m3u8.strip() else default_m3u8_urls[0]
display_name = custom_name.strip() if custom_name.strip() else default_names[0]
else:
url = default_m3u8_urls[i] if i < len(default_m3u8_urls) else ""
display_name = default_names[i] if i < len(default_names) else f"Stream {i+1}"
st.session_state.streams.append({
"current_m3u8_url": url,
"processed_frame": np.zeros((480, 640, 3), dtype=np.uint8),
"start_time": time.time(),
"processed_count": 0,
"detected_frames": [],
"last_processed_time": 0,
"stats_text": "Processing FPS: 0.00\nFrame Delay: 0.00 sec\nTensor Results: No detections",
"highest_match": 0.0,
"display_name": display_name
})
else:
if st.session_state.num_streams > 0:
url = custom_m3u8.strip() if custom_m3u8.strip() else default_m3u8_urls[0]
display_name = custom_name.strip() if custom_name.strip() else default_names[0]
st.session_state.streams[0]["current_m3u8_url"] = url
st.session_state.streams[0]["display_name"] = display_name
confidence = st.session_state.confidence
target_fps = st.session_state.target_fps
# --- Load Model ---
model_path = 'https://huggingface.co/spaces/tstone87/ccr-colorado/resolve/main/best.pt'
@st.cache_resource
def load_model():
return YOLO(model_path)
try:
model = load_model()
except Exception as ex:
st.error(f"Model loading failed: {str(ex)}")
st.stop()
# --- Create Placeholders for Streams in a 2-Column Grid ---
num_streams = st.session_state.num_streams
feed_placeholders = []
stats_placeholders = []
cols = st.columns(2)
for i in range(num_streams):
col_index = i % 2
if i >= 2 and col_index == 0:
cols = st.columns(2)
feed_placeholders.append(cols[col_index].empty())
stats_placeholders.append(cols[col_index].empty())
if num_streams == 1:
_ = st.columns(2)
def update_stream(i):
current_time = time.time()
sleep_time = 1.0 / target_fps
stream_state = st.session_state.streams[i]
if current_time - stream_state["last_processed_time"] >= sleep_time:
url = stream_state["current_m3u8_url"]
cap = cv2.VideoCapture(url)
if not cap.isOpened():
stats_placeholders[i].text("Failed to open M3U8 stream.")
return
ret, frame = cap.read()
cap.release()
if not ret:
stats_placeholders[i].text("Stream interrupted or ended.")
return
res = model.predict(frame, conf=confidence)
processed_frame = res[0].plot()[:, :, ::-1]
# Extract detection results.
tensor_info = "No detections"
max_conf = 0.0
try:
boxes = res[0].boxes
if boxes is not None and len(boxes) > 0:
max_conf = float(boxes.conf.max())
tensor_info = f"Detections: {len(boxes)} | Max Confidence: {max_conf:.2f}"
except Exception as ex:
tensor_info = f"Error extracting detections: {ex}"
# Only update if new detection's confidence is >= current highest.
if max_conf >= stream_state["highest_match"]:
stream_state["highest_match"] = max_conf
stream_state["detected_frames"].append(processed_frame)
stream_state["processed_count"] += 1
stream_state["last_processed_time"] = current_time
mt_time = datetime.now(pytz.timezone('America/Denver')).strftime('%Y-%m-%d %H:%M:%S MT')
stream_state["processed_frame"] = processed_frame
stream_state["stats_text"] = (
f"Processing FPS: {stream_state['processed_count'] / (current_time - stream_state['start_time']):.2f}\n"
f"{tensor_info}\n"
f"Highest Match: {stream_state['highest_match']:.2f}"
)
feed_placeholders[i].image(
processed_frame,
caption=f"Stream {i+1} - {stream_state['display_name']} - {mt_time}",
use_container_width=True
)
stats_placeholders[i].text(stream_state["stats_text"])
# --- Continuous Processing Loop ---
while True:
for i in range(num_streams):
update_stream(i)
# time.sleep(1.0 / target_fps)