Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
Streamlit Web Interface β Traffic Detection
|
| 3 |
Supports: YOLOv11 (tracking) | SSD MobileNetV3
|
| 4 |
GPU forced automatically.
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import streamlit as st
|
|
@@ -16,13 +17,20 @@ from ultralytics import YOLO
|
|
| 16 |
from datetime import datetime
|
| 17 |
from utils.yolo_tracker import normalize_class_name
|
| 18 |
|
|
|
|
| 19 |
if 'global_unique_ids' not in st.session_state:
|
| 20 |
st.session_state.global_unique_ids = {}
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
w = max(1, x2 - x1)
|
| 24 |
h = max(1, y2 - y1)
|
| 25 |
-
if cls_name == "car" and (h / w) >
|
| 26 |
return "person"
|
| 27 |
return cls_name
|
| 28 |
|
|
@@ -60,13 +68,13 @@ if model_choice == "YOLOv11 + ByteTrack (Base)":
|
|
| 60 |
elif model_choice == "YOLOv11 + ByteTrack (Fine-tuned)":
|
| 61 |
selected_yolo_path = YOLO_FINETUNED_PATH
|
| 62 |
if os.path.exists(selected_yolo_path):
|
| 63 |
-
st.sidebar.success("Weights: `models/best.pt` ")
|
| 64 |
else:
|
| 65 |
st.sidebar.error("`models/best.pt` not found.")
|
| 66 |
else:
|
| 67 |
selected_yolo_path = None
|
| 68 |
|
| 69 |
-
selected_classes
|
| 70 |
"Classes to detect", TRAFFIC_CLASSES,
|
| 71 |
default=['car', 'truck', 'bus', 'person', 'traffic light']
|
| 72 |
)
|
|
@@ -76,33 +84,77 @@ min_track_hits = st.sidebar.number_input("Min track hits (YOLO only)", min
|
|
| 76 |
shape_refine = st.sidebar.checkbox("Reclassify tall 'car' as person", value=True)
|
| 77 |
person_car_ratio = st.sidebar.slider("Person/Car ratio (H/W)", 0.8, 2.0, 1.2, 0.05)
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
uploaded_file = st.file_uploader(
|
| 80 |
-
"
|
| 81 |
type=["mp4", "avi", "mov"]
|
| 82 |
)
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
st.session_state.global_unique_ids = {}
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
# ββ YOLOv11 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 107 |
if model_choice in ("YOLOv11 + ByteTrack (Base)",
|
| 108 |
"YOLOv11 + ByteTrack (Fine-tuned)"):
|
|
@@ -111,11 +163,10 @@ elif uploaded_file:
|
|
| 111 |
st.stop()
|
| 112 |
|
| 113 |
model = YOLO(selected_yolo_path)
|
| 114 |
-
cap = cv.VideoCapture(
|
| 115 |
fps = cap.get(cv.CAP_PROP_FPS) or 25
|
| 116 |
total = int(cap.get(cv.CAP_PROP_FRAME_COUNT))
|
| 117 |
track_hits = {}
|
| 118 |
-
logs = []
|
| 119 |
frame_idx = 0
|
| 120 |
log_path = f"logs/log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 121 |
name_to_id = {v: k for k, v in model.names.items()}
|
|
@@ -145,10 +196,8 @@ elif uploaded_file:
|
|
| 145 |
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 146 |
|
| 147 |
if shape_refine:
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
if cls_name == "car" and (h / w) > person_car_ratio:
|
| 151 |
-
cls_name = "person"
|
| 152 |
|
| 153 |
area = max(0, x2-x1) * max(0, y2-y1)
|
| 154 |
if cls_name not in selected_classes: continue
|
|
@@ -176,7 +225,8 @@ elif uploaded_file:
|
|
| 176 |
|
| 177 |
if no_object:
|
| 178 |
cv.putText(annotated, "No selected object detected",
|
| 179 |
-
(20, 50), cv.FONT_HERSHEY_SIMPLEX,
|
|
|
|
| 180 |
status_display.warning("β οΈ No selected object in this frame")
|
| 181 |
else:
|
| 182 |
status_display.empty()
|
|
@@ -208,7 +258,7 @@ elif uploaded_file:
|
|
| 208 |
'class', 'x1', 'y1', 'x2', 'y2', 'confidence'])
|
| 209 |
writer.writerows(logs)
|
| 210 |
|
| 211 |
-
st.success(f"Done β {frame_idx} frames analyzed")
|
| 212 |
|
| 213 |
# ββ SSD βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 214 |
else:
|
|
@@ -241,15 +291,14 @@ elif uploaded_file:
|
|
| 241 |
ssd_model.to(device)
|
| 242 |
ssd_model.eval()
|
| 243 |
|
| 244 |
-
cap = cv.VideoCapture(
|
| 245 |
fps = cap.get(cv.CAP_PROP_FPS) or 25
|
| 246 |
total = int(cap.get(cv.CAP_PROP_FRAME_COUNT))
|
| 247 |
-
logs = []
|
| 248 |
frame_idx = 0
|
| 249 |
log_path = f"logs/ssd_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 250 |
np.random.seed(42)
|
| 251 |
-
COLORS
|
| 252 |
-
|
| 253 |
seen_classes = set()
|
| 254 |
|
| 255 |
while cap.isOpened():
|
|
@@ -279,10 +328,8 @@ elif uploaded_file:
|
|
| 279 |
|
| 280 |
x1, y1, x2, y2 = map(int, box)
|
| 281 |
if shape_refine:
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
if cls_name == "car" and (h / w) > person_car_ratio:
|
| 285 |
-
cls_name = "person"
|
| 286 |
|
| 287 |
area = max(0, x2-x1) * max(0, y2-y1)
|
| 288 |
if cls_name not in selected_classes: continue
|
|
@@ -332,7 +379,9 @@ elif uploaded_file:
|
|
| 332 |
'x1','y1','x2','y2','confidence'])
|
| 333 |
writer.writerows(logs)
|
| 334 |
|
| 335 |
-
st.success(f"Done β {frame_idx} frames analyzed")
|
|
|
|
|
|
|
| 336 |
|
| 337 |
# ββ Final Stats ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 338 |
if logs:
|
|
@@ -371,7 +420,15 @@ elif uploaded_file:
|
|
| 371 |
mime="text/csv"
|
| 372 |
)
|
| 373 |
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
|
| 376 |
else:
|
|
|
|
|
|
|
|
|
|
| 377 |
st.info("π Upload a video to start.")
|
|
|
|
| 2 |
Streamlit Web Interface β Traffic Detection
|
| 3 |
Supports: YOLOv11 (tracking) | SSD MobileNetV3
|
| 4 |
GPU forced automatically.
|
| 5 |
+
Upload and processing are separated to avoid 403 errors.
|
| 6 |
"""
|
| 7 |
|
| 8 |
import streamlit as st
|
|
|
|
| 17 |
from datetime import datetime
|
| 18 |
from utils.yolo_tracker import normalize_class_name
|
| 19 |
|
| 20 |
+
# ββ Session state init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
if 'global_unique_ids' not in st.session_state:
|
| 22 |
st.session_state.global_unique_ids = {}
|
| 23 |
+
if 'video_ready' not in st.session_state:
|
| 24 |
+
st.session_state.video_ready = False
|
| 25 |
+
if 'tmp_path' not in st.session_state:
|
| 26 |
+
st.session_state.tmp_path = None
|
| 27 |
+
if 'processing_done' not in st.session_state:
|
| 28 |
+
st.session_state.processing_done = False
|
| 29 |
+
|
| 30 |
+
def refine_class_by_shape(cls_name, x1, y1, x2, y2, ratio=1.2):
|
| 31 |
w = max(1, x2 - x1)
|
| 32 |
h = max(1, y2 - y1)
|
| 33 |
+
if cls_name == "car" and (h / w) > ratio:
|
| 34 |
return "person"
|
| 35 |
return cls_name
|
| 36 |
|
|
|
|
| 68 |
elif model_choice == "YOLOv11 + ByteTrack (Fine-tuned)":
|
| 69 |
selected_yolo_path = YOLO_FINETUNED_PATH
|
| 70 |
if os.path.exists(selected_yolo_path):
|
| 71 |
+
st.sidebar.success("Weights: `models/best.pt` β
")
|
| 72 |
else:
|
| 73 |
st.sidebar.error("`models/best.pt` not found.")
|
| 74 |
else:
|
| 75 |
selected_yolo_path = None
|
| 76 |
|
| 77 |
+
selected_classes = st.sidebar.multiselect(
|
| 78 |
"Classes to detect", TRAFFIC_CLASSES,
|
| 79 |
default=['car', 'truck', 'bus', 'person', 'traffic light']
|
| 80 |
)
|
|
|
|
| 84 |
shape_refine = st.sidebar.checkbox("Reclassify tall 'car' as person", value=True)
|
| 85 |
person_car_ratio = st.sidebar.slider("Person/Car ratio (H/W)", 0.8, 2.0, 1.2, 0.05)
|
| 86 |
|
| 87 |
+
if not selected_classes:
|
| 88 |
+
st.warning("Select at least one class in the sidebar.")
|
| 89 |
+
st.stop()
|
| 90 |
+
|
| 91 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 92 |
+
# ΓTAPE 1 β UPLOAD
|
| 93 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
st.subheader("π€ Step 1 β Upload your video")
|
| 95 |
+
|
| 96 |
uploaded_file = st.file_uploader(
|
| 97 |
+
"Choose a video file (mp4, avi, mov)",
|
| 98 |
type=["mp4", "avi", "mov"]
|
| 99 |
)
|
| 100 |
|
| 101 |
+
if uploaded_file is not None:
|
| 102 |
+
# Sauvegarde du fichier en session pour ne pas le reperdre
|
| 103 |
+
if not st.session_state.video_ready:
|
| 104 |
+
with st.spinner("Saving video..."):
|
| 105 |
+
tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 106 |
+
tfile.write(uploaded_file.read())
|
| 107 |
+
tfile.flush()
|
| 108 |
+
tfile.close()
|
| 109 |
+
st.session_state.tmp_path = tfile.name
|
| 110 |
+
st.session_state.video_ready = True
|
| 111 |
+
st.session_state.processing_done = False
|
| 112 |
+
st.session_state.global_unique_ids = {}
|
| 113 |
+
|
| 114 |
+
st.success(f"β
Video ready : **{uploaded_file.name}** "
|
| 115 |
+
f"({uploaded_file.size / 1e6:.1f} MB)")
|
| 116 |
+
|
| 117 |
+
# AperΓ§u info vidΓ©o
|
| 118 |
+
cap_info = cv.VideoCapture(st.session_state.tmp_path)
|
| 119 |
+
fps_info = cap_info.get(cv.CAP_PROP_FPS)
|
| 120 |
+
w_info = int(cap_info.get(cv.CAP_PROP_FRAME_WIDTH))
|
| 121 |
+
h_info = int(cap_info.get(cv.CAP_PROP_FRAME_HEIGHT))
|
| 122 |
+
total_info = int(cap_info.get(cv.CAP_PROP_FRAME_COUNT))
|
| 123 |
+
cap_info.release()
|
| 124 |
+
|
| 125 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 126 |
+
col1.metric("Resolution", f"{w_info}x{h_info}")
|
| 127 |
+
col2.metric("FPS", f"{fps_info:.1f}")
|
| 128 |
+
col3.metric("Frames", total_info)
|
| 129 |
+
col4.metric("Duration", f"{total_info/fps_info:.1f}s" if fps_info > 0 else "N/A")
|
| 130 |
+
|
| 131 |
+
st.divider()
|
| 132 |
+
|
| 133 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
+
# ΓTAPE 2 β TRAITEMENT
|
| 135 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 136 |
+
st.subheader("βΆοΈ Step 2 β Run Detection")
|
| 137 |
+
|
| 138 |
+
start = st.button("βΆοΈ Start Detection", type="primary",
|
| 139 |
+
disabled=st.session_state.processing_done)
|
| 140 |
+
|
| 141 |
+
if start:
|
| 142 |
+
st.session_state.processing_done = False
|
| 143 |
st.session_state.global_unique_ids = {}
|
| 144 |
|
| 145 |
+
col_video, col_stats = st.columns([2, 1])
|
| 146 |
+
with col_video:
|
| 147 |
+
st.subheader("πΉ Annotated video")
|
| 148 |
+
frame_display = st.empty()
|
| 149 |
+
status_display = st.empty()
|
| 150 |
+
with col_stats:
|
| 151 |
+
st.subheader("π’ Unique counters")
|
| 152 |
+
counter_display = st.empty()
|
| 153 |
+
progress_bar = st.progress(0)
|
| 154 |
+
|
| 155 |
+
tmp_path = st.session_state.tmp_path
|
| 156 |
+
logs = []
|
| 157 |
+
|
| 158 |
# ββ YOLOv11 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 159 |
if model_choice in ("YOLOv11 + ByteTrack (Base)",
|
| 160 |
"YOLOv11 + ByteTrack (Fine-tuned)"):
|
|
|
|
| 163 |
st.stop()
|
| 164 |
|
| 165 |
model = YOLO(selected_yolo_path)
|
| 166 |
+
cap = cv.VideoCapture(tmp_path)
|
| 167 |
fps = cap.get(cv.CAP_PROP_FPS) or 25
|
| 168 |
total = int(cap.get(cv.CAP_PROP_FRAME_COUNT))
|
| 169 |
track_hits = {}
|
|
|
|
| 170 |
frame_idx = 0
|
| 171 |
log_path = f"logs/log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 172 |
name_to_id = {v: k for k, v in model.names.items()}
|
|
|
|
| 196 |
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 197 |
|
| 198 |
if shape_refine:
|
| 199 |
+
cls_name = refine_class_by_shape(
|
| 200 |
+
cls_name, x1, y1, x2, y2, person_car_ratio)
|
|
|
|
|
|
|
| 201 |
|
| 202 |
area = max(0, x2-x1) * max(0, y2-y1)
|
| 203 |
if cls_name not in selected_classes: continue
|
|
|
|
| 225 |
|
| 226 |
if no_object:
|
| 227 |
cv.putText(annotated, "No selected object detected",
|
| 228 |
+
(20, 50), cv.FONT_HERSHEY_SIMPLEX,
|
| 229 |
+
1.0, (0, 0, 255), 2)
|
| 230 |
status_display.warning("β οΈ No selected object in this frame")
|
| 231 |
else:
|
| 232 |
status_display.empty()
|
|
|
|
| 258 |
'class', 'x1', 'y1', 'x2', 'y2', 'confidence'])
|
| 259 |
writer.writerows(logs)
|
| 260 |
|
| 261 |
+
st.success(f"β
Done β {frame_idx} frames analyzed")
|
| 262 |
|
| 263 |
# ββ SSD βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 264 |
else:
|
|
|
|
| 291 |
ssd_model.to(device)
|
| 292 |
ssd_model.eval()
|
| 293 |
|
| 294 |
+
cap = cv.VideoCapture(tmp_path)
|
| 295 |
fps = cap.get(cv.CAP_PROP_FPS) or 25
|
| 296 |
total = int(cap.get(cv.CAP_PROP_FRAME_COUNT))
|
|
|
|
| 297 |
frame_idx = 0
|
| 298 |
log_path = f"logs/ssd_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 299 |
np.random.seed(42)
|
| 300 |
+
COLORS = np.random.randint(0, 255, size=(len(COCO_CLASSES), 3),
|
| 301 |
+
dtype="uint8")
|
| 302 |
seen_classes = set()
|
| 303 |
|
| 304 |
while cap.isOpened():
|
|
|
|
| 328 |
|
| 329 |
x1, y1, x2, y2 = map(int, box)
|
| 330 |
if shape_refine:
|
| 331 |
+
cls_name = refine_class_by_shape(
|
| 332 |
+
cls_name, x1, y1, x2, y2, person_car_ratio)
|
|
|
|
|
|
|
| 333 |
|
| 334 |
area = max(0, x2-x1) * max(0, y2-y1)
|
| 335 |
if cls_name not in selected_classes: continue
|
|
|
|
| 379 |
'x1','y1','x2','y2','confidence'])
|
| 380 |
writer.writerows(logs)
|
| 381 |
|
| 382 |
+
st.success(f"β
Done β {frame_idx} frames analyzed")
|
| 383 |
+
|
| 384 |
+
st.session_state.processing_done = True
|
| 385 |
|
| 386 |
# ββ Final Stats ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 387 |
if logs:
|
|
|
|
| 420 |
mime="text/csv"
|
| 421 |
)
|
| 422 |
|
| 423 |
+
if st.button("π Process another video"):
|
| 424 |
+
st.session_state.video_ready = False
|
| 425 |
+
st.session_state.tmp_path = None
|
| 426 |
+
st.session_state.processing_done = False
|
| 427 |
+
st.session_state.global_unique_ids = {}
|
| 428 |
+
st.rerun()
|
| 429 |
|
| 430 |
else:
|
| 431 |
+
st.session_state.video_ready = False
|
| 432 |
+
st.session_state.tmp_path = None
|
| 433 |
+
st.session_state.processing_done = False
|
| 434 |
st.info("π Upload a video to start.")
|