Binta26 commited on
Commit
7b4fe2c
Β·
verified Β·
1 Parent(s): 8bf988b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -169
app.py CHANGED
@@ -15,97 +15,30 @@ import plotly.express as px
15
  from ultralytics import YOLO
16
  from datetime import datetime
17
  from utils.yolo_tracker import normalize_class_name
18
- from streamlit_webrtc import webrtc_streamer, VideoProcessorBase, RTCConfiguration
19
- import av
20
  if 'global_unique_ids' not in st.session_state:
21
  st.session_state.global_unique_ids = {}
22
 
23
  def refine_class_by_shape(cls_name: str, x1: int, y1: int, x2: int, y2: int) -> str:
24
- """Reduce common false positives where vertical human boxes become 'car'."""
25
  w = max(1, x2 - x1)
26
  h = max(1, y2 - y1)
27
  if cls_name == "car" and (h / w) > 1.2:
28
  return "person"
29
  return cls_name
30
 
31
-
32
  # ── GPU Detection ──────────────────────────────────────────────────────────────
33
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
34
  GPU_NAME = torch.cuda.get_device_name(0) if DEVICE == "cuda" else "CPU only"
35
 
36
- TRAFFIC_CLASSES = ['car', 'truck', 'bus', 'motorbike', 'bicycle', 'person', 'traffic sign', 'traffic light']
37
- YOLO_BASE_PATH = "models/yolo11n.pt"
 
38
  YOLO_FINETUNED_PATH = "models/best.pt"
39
  os.makedirs("logs", exist_ok=True)
40
 
41
- class TrafficVideoProcessor(VideoProcessorBase):
42
- def __init__(self):
43
- self.model = YOLO(
44
- "models/best.pt" if os.path.exists("models/best.pt")
45
- else "models/yolo11n.pt"
46
- )
47
- self.unique_ids = {}
48
- self.track_hits = {}
49
- self.frame_idx = 0
50
-
51
- def recv(self, frame):
52
- img = frame.to_ndarray(format="bgr24")
53
-
54
- results = self.model.track(
55
- img,
56
- persist = True,
57
- tracker = "bytetrack.yaml",
58
- conf = 0.4,
59
- device = DEVICE,
60
- verbose = False
61
- )
62
-
63
- annotated = img.copy()
64
- no_object = True
65
-
66
- if results[0].boxes is not None and results[0].boxes.id is not None:
67
- for box in results[0].boxes:
68
- track_id = int(box.id[0])
69
- cls_name = self.model.names[int(box.cls[0])]
70
- conf_val = round(float(box.conf[0]), 3)
71
- x1, y1, x2, y2 = map(int, box.xyxy[0])
72
-
73
- no_object = False
74
- self.track_hits[track_id] = self.track_hits.get(track_id, 0) + 1
75
-
76
- if self.track_hits[track_id] >= 3:
77
- self.unique_ids.setdefault(track_id, cls_name)
78
-
79
- color = (0, 0, 255) if cls_name == "stop sign" else (0, 255, 0)
80
- label = f"{cls_name} ID:{track_id} {conf_val:.2f}"
81
- cv.rectangle(annotated, (x1, y1), (x2, y2), color, 2)
82
- cv.putText(annotated, label, (x1, max(y1 - 8, 0)),
83
- cv.FONT_HERSHEY_SIMPLEX, 0.55, color, 2)
84
-
85
- if no_object:
86
- cv.putText(annotated, "No selected object detected",
87
- (20, 50), cv.FONT_HERSHEY_SIMPLEX,
88
- 1.0, (0, 0, 255), 2)
89
-
90
- # Afficher les compteurs sur la frame
91
- y_off = 30
92
- stats = {}
93
- for cls in self.unique_ids.values():
94
- stats[cls] = stats.get(cls, 0) + 1
95
- for cls_name, count in sorted(stats.items()):
96
- label = f"{cls_name}: {count} unique"
97
- cv.putText(annotated, label, (10, y_off),
98
- cv.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
99
- y_off += 30
100
-
101
- self.frame_idx += 1
102
- return av.VideoFrame.from_ndarray(annotated, format="bgr24")
103
-
104
-
105
  st.set_page_config(page_title="Traffic Monitor", layout="wide")
106
  st.title("🚦 Traffic Object Detection & Tracking")
107
 
108
- # GPU status badge
109
  if DEVICE == "cuda":
110
  st.success(f"⚑ GPU Active : {GPU_NAME}")
111
  else:
@@ -116,49 +49,41 @@ st.sidebar.header("βš™οΈ Configuration")
116
 
117
  model_choice = st.sidebar.radio(
118
  "Model",
119
- ["YOLOv11 + ByteTrack (Base)", "YOLOv11 + ByteTrack (Fine-tuned)", "SSD MobileNetV3"],
120
- help="Choose base YOLO, fine-tuned YOLO, or SSD."
 
121
  )
122
 
123
  if model_choice == "YOLOv11 + ByteTrack (Base)":
124
  selected_yolo_path = YOLO_BASE_PATH
125
- st.sidebar.info("YOLO weights: base `models/yolo11n.pt`")
126
  elif model_choice == "YOLOv11 + ByteTrack (Fine-tuned)":
127
  selected_yolo_path = YOLO_FINETUNED_PATH
128
  if os.path.exists(selected_yolo_path):
129
- st.sidebar.success("YOLO weights: fine-tuned `models/best.pt`")
130
  else:
131
- st.sidebar.error("`models/best.pt` not found. Train/copy your fine-tuned model first.")
132
  else:
133
  selected_yolo_path = None
134
 
135
  selected_classes = st.sidebar.multiselect(
136
- "Classes to detect",
137
- TRAFFIC_CLASSES,
138
- default=['car', 'truck', 'bus', 'traffic sign','person', 'traffic light']
139
  )
140
-
141
  confidence_threshold = st.sidebar.slider("Confidence threshold", 0.1, 1.0, 0.4)
142
- min_box_area = st.sidebar.number_input("Min box area (pxΒ²)", min_value=0, value=1600, step=100)
143
- min_track_hits = st.sidebar.number_input("Min track hits (YOLO only)", min_value=1, value=5, step=1)
144
- shape_refine = st.sidebar.checkbox("Reclassify tall 'car' boxes as person", value=True)
145
- person_car_ratio = st.sidebar.slider(
146
- "Person/Car shape ratio threshold (H/W)",
147
- min_value=0.8,
148
- max_value=2.0,
149
- value=1.2,
150
- step=0.05
151
- )
152
 
153
  uploaded_file = st.file_uploader(
154
  "πŸ“€ Upload a video (mp4, avi, mov)",
155
  type=["mp4", "avi", "mov"]
156
  )
157
 
158
-
159
  # ── Main ───────────────────────────────────────────────────────────────────────
160
  if not selected_classes:
161
- st.warning("Select at least one class in the menu.")
162
 
163
  elif uploaded_file:
164
  tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
@@ -176,30 +101,24 @@ elif uploaded_file:
176
  progress_bar = st.progress(0)
177
 
178
  if st.button("▢️ Start Detection", type="primary"):
179
- st.session_state.ginput_source = st.radio(
180
- "πŸ“₯ Input source",
181
- ["Upload a video", "Use Webcam (live)"],
182
- horizontal=True
183
- )
184
 
185
-
186
- # ── YOLOv11 ─────────────────────────────────────────────────────────
187
- if model_choice in ("YOLOv11 + ByteTrack (Base)", "YOLOv11 + ByteTrack (Fine-tuned)"):
188
  if not os.path.exists(selected_yolo_path):
189
- st.error(f"Model file not found: `{selected_yolo_path}`")
190
  st.stop()
191
 
192
- model = YOLO(selected_yolo_path)
193
- cap = cv.VideoCapture(tfile.name)
194
- fps = cap.get(cv.CAP_PROP_FPS) or 25
195
- total = int(cap.get(cv.CAP_PROP_FRAME_COUNT))
196
-
197
- #unique_ids = {}
198
- track_hits = {}
199
- logs = []
200
- frame_idx = 0
201
- log_path = f"logs/log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
202
- name_to_id = {v: k for k, v in model.names.items()}
203
  selected_ids = [name_to_id[c] for c in selected_classes if c in name_to_id]
204
 
205
  while cap.isOpened():
@@ -208,19 +127,15 @@ elif uploaded_file:
208
  break
209
 
210
  timestamp = round(frame_idx / fps, 3)
211
-
212
- results = model.track(
213
- frame,
214
- persist=True,
215
- tracker="bytetrack.yaml",
216
  conf=confidence_threshold,
217
  classes=selected_ids if selected_ids else None,
218
- device=DEVICE, # ← GPU
219
- verbose=False
220
  )
221
 
222
  no_object = True
223
- accepted = []
224
 
225
  if results[0].boxes is not None and results[0].boxes.id is not None:
226
  for box in results[0].boxes:
@@ -228,42 +143,40 @@ elif uploaded_file:
228
  cls_name = normalize_class_name(model.names[int(box.cls[0])])
229
  conf = round(float(box.conf[0]), 3)
230
  x1, y1, x2, y2 = map(int, box.xyxy[0])
 
231
  if shape_refine:
232
  w = max(1, x2 - x1)
233
  h = max(1, y2 - y1)
234
  if cls_name == "car" and (h / w) > person_car_ratio:
235
  cls_name = "person"
236
- area = max(0, x2 - x1) * max(0, y2 - y1)
237
 
238
- if cls_name not in selected_classes:
239
- continue
240
- if conf < confidence_threshold:
241
- continue
242
- if area < min_box_area:
243
- continue
244
 
245
  no_object = False
246
  track_hits[track_id] = track_hits.get(track_id, 0) + 1
247
  if track_hits[track_id] >= min_track_hits:
248
- #unique_ids.setdefault(track_id, cls_name)
249
- st.session_state.global_unique_ids.setdefault(track_id, cls_name)
 
250
  accepted.append((track_id, cls_name, conf, x1, y1, x2, y2))
251
  logs.append([timestamp, frame_idx, track_id,
252
  cls_name, x1, y1, x2, y2, conf])
253
 
254
  annotated = frame.copy()
255
  for track_id, cls_name, conf, x1, y1, x2, y2 in accepted:
256
- color = (0, 0, 255) if cls_name == "stop sign" else (0, 255, 0)
257
  thickness = 3 if cls_name == "stop sign" else 2
258
- label = f"{cls_name} ID:{track_id} {conf:.2f}"
259
  cv.rectangle(annotated, (x1, y1), (x2, y2), color, thickness)
260
- cv.putText(annotated, label, (x1, max(y1 - 8, 0)),
261
  cv.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
262
 
263
  if no_object:
264
  cv.putText(annotated, "No selected object detected",
265
- (20,50), cv.FONT_HERSHEY_SIMPLEX,
266
- 1.0, (0,0,255), 2)
267
  status_display.warning("⚠️ No selected object in this frame")
268
  else:
269
  status_display.empty()
@@ -277,7 +190,8 @@ elif uploaded_file:
277
  for cls in st.session_state.global_unique_ids.values():
278
  stats[cls] = stats.get(cls, 0) + 1
279
  counter_display.markdown(
280
- "\n".join([f"**{c}** : {n} unique" for c, n in sorted(stats.items())])
 
281
  or "_Waiting for objects..._"
282
  )
283
 
@@ -290,13 +204,13 @@ elif uploaded_file:
290
 
291
  with open(log_path, 'w', newline='') as f:
292
  writer = csv.writer(f)
293
- writer.writerow(['timestamp_s','frame','track_id',
294
- 'class','x1','y1','x2','y2','confidence'])
295
  writer.writerows(logs)
296
 
297
- st.success(f"βœ… Done β€” {frame_idx} frames analyzed")
298
 
299
- # ── SSD ─────────────────────────────────────────────────────────────
300
  else:
301
  import torchvision
302
  from torchvision.transforms import functional as TF
@@ -319,9 +233,11 @@ elif uploaded_file:
319
  "toothbrush"
320
  ]
321
 
322
- device = torch.device(DEVICE)
323
- weights = torchvision.models.detection.SSDLite320_MobileNet_V3_Large_Weights.COCO_V1
324
- ssd_model = torchvision.models.detection.ssdlite320_mobilenet_v3_large(weights=weights)
 
 
325
  ssd_model.to(device)
326
  ssd_model.eval()
327
 
@@ -332,7 +248,9 @@ elif uploaded_file:
332
  frame_idx = 0
333
  log_path = f"logs/ssd_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
334
  np.random.seed(42)
335
- COLORS = np.random.randint(0, 255, size=(len(COCO_CLASSES), 3), dtype="uint8")
 
 
336
 
337
  while cap.isOpened():
338
  ret, frame = cap.read()
@@ -355,7 +273,8 @@ elif uploaded_file:
355
  for box, label, score in zip(boxes_arr, labels_arr, scores_arr):
356
  if score < confidence_threshold:
357
  continue
358
- cls_name = COCO_CLASSES[label] if label < len(COCO_CLASSES) else "unknown"
 
359
  cls_name = normalize_class_name(cls_name)
360
 
361
  x1, y1, x2, y2 = map(int, box)
@@ -364,26 +283,28 @@ elif uploaded_file:
364
  h = max(1, y2 - y1)
365
  if cls_name == "car" and (h / w) > person_car_ratio:
366
  cls_name = "person"
367
- if cls_name not in selected_classes:
368
- continue
369
- area = max(0, x2 - x1) * max(0, y2 - y1)
370
- if area < min_box_area:
371
- continue
372
 
373
- no_object = False
374
- color = (0,0,255) if cls_name == "stop sign" else [int(c) for c in COLORS[label]]
 
375
 
376
- cv.rectangle(frame, (x1,y1),(x2,y2), color, 3 if cls_name=="stop sign" else 2)
377
- label_text = "STOP SIGN" if cls_name=="stop sign" else f"{cls_name}: {score:.2f}"
 
 
 
 
 
 
378
  cv.putText(frame, label_text, (x1, max(y1-8,0)),
379
  cv.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
380
-
381
  logs.append([timestamp, frame_idx, cls_name,
382
  x1, y1, x2, y2, round(float(score),3)])
383
 
384
  if no_object:
385
  cv.putText(frame, "No selected object detected",
386
- (20,50), cv.FONT_HERSHEY_SIMPLEX, 1.0, (0,0,255), 2)
 
387
  status_display.warning("⚠️ No selected object in this frame")
388
  else:
389
  status_display.empty()
@@ -393,13 +314,8 @@ elif uploaded_file:
393
  channels="RGB", use_column_width=True
394
  )
395
 
396
- stats = {}
397
- for row in logs:
398
- cls = row[2]
399
- if cls not in stats :
400
- stats[cls] = 1
401
  counter_display.markdown(
402
- "\n".join([f"**{c}** : {n} unique" for c, n in sorted(stats.items())])
403
  or "_Waiting..._"
404
  )
405
 
@@ -416,18 +332,20 @@ elif uploaded_file:
416
  'x1','y1','x2','y2','confidence'])
417
  writer.writerows(logs)
418
 
419
- st.success(f"βœ… Done β€” {frame_idx} frames analyzed")
420
 
421
- # ── Final Stats (common) ─────────────────────────────────────────────
422
  if logs:
423
- cols = (['timestamp_s','frame','track_id','class','x1','y1','x2','y2','confidence']
424
- if model_choice in ("YOLOv11 + ByteTrack (Base)", "YOLOv11 + ByteTrack (Fine-tuned)")
425
- else ['timestamp_s','frame','class','x1','y1','x2','y2','confidence'])
 
 
 
426
  df = pd.DataFrame(logs, columns=cols)
427
 
428
  st.subheader("πŸ“Š Final Statistics")
429
  ca, cb = st.columns(2)
430
-
431
  with ca:
432
  counts = df['class'].value_counts().reset_index()
433
  counts.columns = ['Class','Count']
@@ -456,4 +374,4 @@ elif uploaded_file:
456
  os.unlink(tfile.name)
457
 
458
  else:
459
- st.info("πŸ‘† Upload a video to start.")
 
15
  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
  def refine_class_by_shape(cls_name: str, x1: int, y1: int, x2: int, y2: int) -> str:
 
23
  w = max(1, x2 - x1)
24
  h = max(1, y2 - y1)
25
  if cls_name == "car" and (h / w) > 1.2:
26
  return "person"
27
  return cls_name
28
 
 
29
  # ── GPU Detection ──────────────────────────────────────────────────────────────
30
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
31
  GPU_NAME = torch.cuda.get_device_name(0) if DEVICE == "cuda" else "CPU only"
32
 
33
+ TRAFFIC_CLASSES = ['car', 'truck', 'bus', 'motorbike', 'bicycle',
34
+ 'person', 'traffic sign', 'traffic light']
35
+ YOLO_BASE_PATH = "models/yolo11n.pt"
36
  YOLO_FINETUNED_PATH = "models/best.pt"
37
  os.makedirs("logs", exist_ok=True)
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  st.set_page_config(page_title="Traffic Monitor", layout="wide")
40
  st.title("🚦 Traffic Object Detection & Tracking")
41
 
 
42
  if DEVICE == "cuda":
43
  st.success(f"⚑ GPU Active : {GPU_NAME}")
44
  else:
 
49
 
50
  model_choice = st.sidebar.radio(
51
  "Model",
52
+ ["YOLOv11 + ByteTrack (Base)",
53
+ "YOLOv11 + ByteTrack (Fine-tuned)",
54
+ "SSD MobileNetV3"],
55
  )
56
 
57
  if model_choice == "YOLOv11 + ByteTrack (Base)":
58
  selected_yolo_path = YOLO_BASE_PATH
59
+ st.sidebar.info("Weights: `models/yolo11n.pt`")
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 = st.sidebar.multiselect(
70
+ "Classes to detect", TRAFFIC_CLASSES,
71
+ default=['car', 'truck', 'bus', 'person', 'traffic light']
 
72
  )
 
73
  confidence_threshold = st.sidebar.slider("Confidence threshold", 0.1, 1.0, 0.4)
74
+ min_box_area = st.sidebar.number_input("Min box area (pxΒ²)", min_value=0, value=1600, step=100)
75
+ min_track_hits = st.sidebar.number_input("Min track hits (YOLO only)", min_value=1, value=5, step=1)
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
  "πŸ“€ Upload a video (mp4, avi, mov)",
81
  type=["mp4", "avi", "mov"]
82
  )
83
 
 
84
  # ── Main ───────────────────────────────────────────────────────────────────────
85
  if not selected_classes:
86
+ st.warning("Select at least one class in the sidebar.")
87
 
88
  elif uploaded_file:
89
  tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
 
101
  progress_bar = st.progress(0)
102
 
103
  if st.button("▢️ Start Detection", type="primary"):
104
+ st.session_state.global_unique_ids = {}
 
 
 
 
105
 
106
+ # ── YOLOv11 ───────────────────────────────────────────────────────────
107
+ if model_choice in ("YOLOv11 + ByteTrack (Base)",
108
+ "YOLOv11 + ByteTrack (Fine-tuned)"):
109
  if not os.path.exists(selected_yolo_path):
110
+ st.error(f"Model not found: `{selected_yolo_path}`")
111
  st.stop()
112
 
113
+ model = YOLO(selected_yolo_path)
114
+ cap = cv.VideoCapture(tfile.name)
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()}
 
 
122
  selected_ids = [name_to_id[c] for c in selected_classes if c in name_to_id]
123
 
124
  while cap.isOpened():
 
127
  break
128
 
129
  timestamp = round(frame_idx / fps, 3)
130
+ results = model.track(
131
+ frame, persist=True, tracker="bytetrack.yaml",
 
 
 
132
  conf=confidence_threshold,
133
  classes=selected_ids if selected_ids else None,
134
+ device=DEVICE, verbose=False
 
135
  )
136
 
137
  no_object = True
138
+ accepted = []
139
 
140
  if results[0].boxes is not None and results[0].boxes.id is not None:
141
  for box in results[0].boxes:
 
143
  cls_name = normalize_class_name(model.names[int(box.cls[0])])
144
  conf = round(float(box.conf[0]), 3)
145
  x1, y1, x2, y2 = map(int, box.xyxy[0])
146
+
147
  if shape_refine:
148
  w = max(1, x2 - x1)
149
  h = max(1, y2 - y1)
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
155
+ if conf < confidence_threshold: continue
156
+ if area < min_box_area: continue
 
 
157
 
158
  no_object = False
159
  track_hits[track_id] = track_hits.get(track_id, 0) + 1
160
  if track_hits[track_id] >= min_track_hits:
161
+ st.session_state.global_unique_ids.setdefault(
162
+ track_id, cls_name)
163
+
164
  accepted.append((track_id, cls_name, conf, x1, y1, x2, y2))
165
  logs.append([timestamp, frame_idx, track_id,
166
  cls_name, x1, y1, x2, y2, conf])
167
 
168
  annotated = frame.copy()
169
  for track_id, cls_name, conf, x1, y1, x2, y2 in accepted:
170
+ color = (0, 0, 255) if cls_name == "stop sign" else (0, 255, 0)
171
  thickness = 3 if cls_name == "stop sign" else 2
172
+ label = f"{cls_name} ID:{track_id} {conf:.2f}"
173
  cv.rectangle(annotated, (x1, y1), (x2, y2), color, thickness)
174
+ cv.putText(annotated, label, (x1, max(y1-8, 0)),
175
  cv.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
176
 
177
  if no_object:
178
  cv.putText(annotated, "No selected object detected",
179
+ (20, 50), cv.FONT_HERSHEY_SIMPLEX, 1.0, (0,0,255), 2)
 
180
  status_display.warning("⚠️ No selected object in this frame")
181
  else:
182
  status_display.empty()
 
190
  for cls in st.session_state.global_unique_ids.values():
191
  stats[cls] = stats.get(cls, 0) + 1
192
  counter_display.markdown(
193
+ "\n".join([f"**{c}** : {n} unique"
194
+ for c, n in sorted(stats.items())])
195
  or "_Waiting for objects..._"
196
  )
197
 
 
204
 
205
  with open(log_path, 'w', newline='') as f:
206
  writer = csv.writer(f)
207
+ writer.writerow(['timestamp_s', 'frame', 'track_id',
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:
215
  import torchvision
216
  from torchvision.transforms import functional as TF
 
233
  "toothbrush"
234
  ]
235
 
236
+ device = torch.device(DEVICE)
237
+ weights = torchvision.models.detection\
238
+ .SSDLite320_MobileNet_V3_Large_Weights.COCO_V1
239
+ ssd_model = torchvision.models.detection\
240
+ .ssdlite320_mobilenet_v3_large(weights=weights)
241
  ssd_model.to(device)
242
  ssd_model.eval()
243
 
 
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 = np.random.randint(0, 255, size=(len(COCO_CLASSES), 3),
252
+ dtype="uint8")
253
+ seen_classes = set()
254
 
255
  while cap.isOpened():
256
  ret, frame = cap.read()
 
273
  for box, label, score in zip(boxes_arr, labels_arr, scores_arr):
274
  if score < confidence_threshold:
275
  continue
276
+ cls_name = COCO_CLASSES[label] if label < len(COCO_CLASSES) \
277
+ else "unknown"
278
  cls_name = normalize_class_name(cls_name)
279
 
280
  x1, y1, x2, y2 = map(int, box)
 
283
  h = max(1, y2 - y1)
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
289
+ if area < min_box_area: continue
290
 
291
+ no_object = False
292
+ seen_classes.add(cls_name)
293
+ color = (0,0,255) if cls_name == "stop sign" \
294
+ else [int(c) for c in COLORS[label]]
295
+ cv.rectangle(frame, (x1,y1),(x2,y2), color,
296
+ 3 if cls_name=="stop sign" else 2)
297
+ label_text = "STOP SIGN" if cls_name=="stop sign" \
298
+ else f"{cls_name}: {score:.2f}"
299
  cv.putText(frame, label_text, (x1, max(y1-8,0)),
300
  cv.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
 
301
  logs.append([timestamp, frame_idx, cls_name,
302
  x1, y1, x2, y2, round(float(score),3)])
303
 
304
  if no_object:
305
  cv.putText(frame, "No selected object detected",
306
+ (20,50), cv.FONT_HERSHEY_SIMPLEX,
307
+ 1.0, (0,0,255), 2)
308
  status_display.warning("⚠️ No selected object in this frame")
309
  else:
310
  status_display.empty()
 
314
  channels="RGB", use_column_width=True
315
  )
316
 
 
 
 
 
 
317
  counter_display.markdown(
318
+ "\n".join([f"**{c}** : 1 unique" for c in sorted(seen_classes)])
319
  or "_Waiting..._"
320
  )
321
 
 
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:
339
+ cols = (['timestamp_s','frame','track_id','class',
340
+ 'x1','y1','x2','y2','confidence']
341
+ if model_choice in ("YOLOv11 + ByteTrack (Base)",
342
+ "YOLOv11 + ByteTrack (Fine-tuned)")
343
+ else ['timestamp_s','frame','class',
344
+ 'x1','y1','x2','y2','confidence'])
345
  df = pd.DataFrame(logs, columns=cols)
346
 
347
  st.subheader("πŸ“Š Final Statistics")
348
  ca, cb = st.columns(2)
 
349
  with ca:
350
  counts = df['class'].value_counts().reset_index()
351
  counts.columns = ['Class','Count']
 
374
  os.unlink(tfile.name)
375
 
376
  else:
377
+ st.info("πŸ‘† Upload a video to start.")