Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 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
|
| 34 |
GPU_NAME = torch.cuda.get_device_name(0) if DEVICE == "cuda" else "CPU only"
|
| 35 |
|
| 36 |
-
TRAFFIC_CLASSES
|
| 37 |
-
|
|
|
|
| 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)",
|
| 120 |
-
|
|
|
|
| 121 |
)
|
| 122 |
|
| 123 |
if model_choice == "YOLOv11 + ByteTrack (Base)":
|
| 124 |
selected_yolo_path = YOLO_BASE_PATH
|
| 125 |
-
st.sidebar.info("
|
| 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("
|
| 130 |
else:
|
| 131 |
-
st.sidebar.error("`models/best.pt` not found.
|
| 132 |
else:
|
| 133 |
selected_yolo_path = None
|
| 134 |
|
| 135 |
selected_classes = st.sidebar.multiselect(
|
| 136 |
-
"Classes to detect",
|
| 137 |
-
|
| 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
|
| 143 |
-
min_track_hits
|
| 144 |
-
shape_refine
|
| 145 |
-
person_car_ratio
|
| 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
|
| 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.
|
| 180 |
-
"π₯ Input source",
|
| 181 |
-
["Upload a video", "Use Webcam (live)"],
|
| 182 |
-
horizontal=True
|
| 183 |
-
)
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
if not os.path.exists(selected_yolo_path):
|
| 189 |
-
st.error(f"Model
|
| 190 |
st.stop()
|
| 191 |
|
| 192 |
-
model
|
| 193 |
-
cap
|
| 194 |
-
fps
|
| 195 |
-
total
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 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 |
-
|
| 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,
|
| 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 |
-
|
| 239 |
-
|
| 240 |
-
if conf < confidence_threshold:
|
| 241 |
-
|
| 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 |
-
|
| 249 |
-
|
|
|
|
| 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
|
| 257 |
thickness = 3 if cls_name == "stop sign" else 2
|
| 258 |
-
label
|
| 259 |
cv.rectangle(annotated, (x1, y1), (x2, y2), color, thickness)
|
| 260 |
-
cv.putText(annotated, label, (x1, max(y1
|
| 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"
|
|
|
|
| 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"
|
| 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
|
| 323 |
-
weights
|
| 324 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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)
|
|
|
|
| 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 |
-
|
| 374 |
-
|
|
|
|
| 375 |
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
| 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}** :
|
| 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"
|
| 420 |
|
| 421 |
-
# ββ Final Stats
|
| 422 |
if logs:
|
| 423 |
-
cols = (['timestamp_s','frame','track_id','class',
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
| 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.")
|