Spaces:
Sleeping
Sleeping
cyberai-1 commited on
Commit Β·
ef3e83f
1
Parent(s): acd405a
update log
Browse files- backend/main.py +10 -6
- backend/tracker.py +105 -52
- frontend/index.html +5 -0
backend/main.py
CHANGED
|
@@ -67,6 +67,7 @@ async def get_classes():
|
|
| 67 |
async def upload_video(
|
| 68 |
file: UploadFile = File(...),
|
| 69 |
scene_name: str = Form("scene_01"),
|
|
|
|
| 70 |
classes: str = Form(""),
|
| 71 |
conf: float = Form(0.35),
|
| 72 |
model: str = Form("yolov8n.pt"),
|
|
@@ -74,6 +75,7 @@ async def upload_video(
|
|
| 74 |
sid = str(uuid.uuid4())[:12]
|
| 75 |
|
| 76 |
suffix = Path(file.filename).suffix or ".mp4"
|
|
|
|
| 77 |
video_path = UPLOAD_DIR / f"{sid}{suffix}"
|
| 78 |
video_path.write_bytes(await file.read())
|
| 79 |
|
|
@@ -81,14 +83,14 @@ async def upload_video(
|
|
| 81 |
|
| 82 |
sessions[sid] = {
|
| 83 |
"status": "processing",
|
| 84 |
-
|
| 85 |
-
# We use a list + lock so the SSE generator can drain from the front.
|
| 86 |
-
"frames": [], # list of {frame: b64, stats: {...}, pts: float}
|
| 87 |
"frames_lock": threading.Lock(),
|
| 88 |
"stats": {},
|
| 89 |
"summary": None,
|
| 90 |
"error": None,
|
| 91 |
"scene_name": scene_name,
|
|
|
|
|
|
|
| 92 |
"progress": 0,
|
| 93 |
"total_frames": 0,
|
| 94 |
"fps": 30.0,
|
|
@@ -96,14 +98,14 @@ async def upload_video(
|
|
| 96 |
|
| 97 |
threading.Thread(
|
| 98 |
target=_process_video_thread,
|
| 99 |
-
args=(sid, str(video_path), scene_name, selected, conf, model),
|
| 100 |
daemon=True,
|
| 101 |
).start()
|
| 102 |
|
| 103 |
-
return {"session_id": sid, "scene_name": scene_name, "status": "processing"}
|
| 104 |
|
| 105 |
|
| 106 |
-
def _process_video_thread(sid, video_path, scene_name, selected_classes, conf, model_name):
|
| 107 |
"""
|
| 108 |
Processes video as fast as possible (no artificial sleep).
|
| 109 |
Each frame carries a `pts` (presentation timestamp in seconds) so the
|
|
@@ -125,6 +127,8 @@ def _process_video_thread(sid, video_path, scene_name, selected_classes, conf, m
|
|
| 125 |
selected_classes=selected_classes,
|
| 126 |
conf_threshold=conf,
|
| 127 |
scene_name=scene_name,
|
|
|
|
|
|
|
| 128 |
output_dir=str(LOG_DIR),
|
| 129 |
)
|
| 130 |
tracker.setup_video(cap)
|
|
|
|
| 67 |
async def upload_video(
|
| 68 |
file: UploadFile = File(...),
|
| 69 |
scene_name: str = Form("scene_01"),
|
| 70 |
+
group_id: str = Form("group_01"),
|
| 71 |
classes: str = Form(""),
|
| 72 |
conf: float = Form(0.35),
|
| 73 |
model: str = Form("yolov8n.pt"),
|
|
|
|
| 75 |
sid = str(uuid.uuid4())[:12]
|
| 76 |
|
| 77 |
suffix = Path(file.filename).suffix or ".mp4"
|
| 78 |
+
video_name = file.filename or f"{sid}{suffix}"
|
| 79 |
video_path = UPLOAD_DIR / f"{sid}{suffix}"
|
| 80 |
video_path.write_bytes(await file.read())
|
| 81 |
|
|
|
|
| 83 |
|
| 84 |
sessions[sid] = {
|
| 85 |
"status": "processing",
|
| 86 |
+
"frames": [],
|
|
|
|
|
|
|
| 87 |
"frames_lock": threading.Lock(),
|
| 88 |
"stats": {},
|
| 89 |
"summary": None,
|
| 90 |
"error": None,
|
| 91 |
"scene_name": scene_name,
|
| 92 |
+
"group_id": group_id,
|
| 93 |
+
"video_name": video_name,
|
| 94 |
"progress": 0,
|
| 95 |
"total_frames": 0,
|
| 96 |
"fps": 30.0,
|
|
|
|
| 98 |
|
| 99 |
threading.Thread(
|
| 100 |
target=_process_video_thread,
|
| 101 |
+
args=(sid, str(video_path), scene_name, group_id, video_name, selected, conf, model),
|
| 102 |
daemon=True,
|
| 103 |
).start()
|
| 104 |
|
| 105 |
+
return {"session_id": sid, "scene_name": scene_name, "group_id": group_id, "status": "processing"}
|
| 106 |
|
| 107 |
|
| 108 |
+
def _process_video_thread(sid, video_path, scene_name, group_id, video_name, selected_classes, conf, model_name):
|
| 109 |
"""
|
| 110 |
Processes video as fast as possible (no artificial sleep).
|
| 111 |
Each frame carries a `pts` (presentation timestamp in seconds) so the
|
|
|
|
| 127 |
selected_classes=selected_classes,
|
| 128 |
conf_threshold=conf,
|
| 129 |
scene_name=scene_name,
|
| 130 |
+
group_id=group_id,
|
| 131 |
+
video_name=video_name,
|
| 132 |
output_dir=str(LOG_DIR),
|
| 133 |
)
|
| 134 |
tracker.setup_video(cap)
|
backend/tracker.py
CHANGED
|
@@ -1,17 +1,16 @@
|
|
| 1 |
"""
|
| 2 |
tracker.py β Core detection + tracking engine
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
Video output: every processed frame (with bounding boxes) is written to
|
| 9 |
-
an .mp4 file in the logs/ directory via cv2.VideoWriter.
|
| 10 |
"""
|
| 11 |
|
| 12 |
import cv2
|
| 13 |
import csv
|
| 14 |
import json
|
|
|
|
| 15 |
import uuid
|
| 16 |
import numpy as np
|
| 17 |
from pathlib import Path
|
|
@@ -19,7 +18,7 @@ from datetime import datetime
|
|
| 19 |
from collections import defaultdict
|
| 20 |
from ultralytics import YOLO
|
| 21 |
|
| 22 |
-
# βββ
|
| 23 |
TRAFFIC_CLASSES = {
|
| 24 |
0: "person",
|
| 25 |
1: "bicycle",
|
|
@@ -40,6 +39,15 @@ CLASS_COLORS = {
|
|
| 40 |
|
| 41 |
DEFAULT_CLASSES = list(TRAFFIC_CLASSES.values())
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
class TrafficTracker:
|
| 45 |
def __init__(
|
|
@@ -48,18 +56,20 @@ class TrafficTracker:
|
|
| 48 |
selected_classes: list = None,
|
| 49 |
conf_threshold: float = 0.35,
|
| 50 |
scene_name: str = "scene_01",
|
|
|
|
|
|
|
| 51 |
output_dir: str = "logs",
|
| 52 |
):
|
| 53 |
-
# ββ
|
| 54 |
-
#
|
| 55 |
-
# as a form field and passes it here).
|
| 56 |
-
# To use a fine-tuned model, pass the path to your best.pt here, e.g.:
|
| 57 |
# model_path = "runs/detect/traffic_finetune/weights/best.pt"
|
| 58 |
self.model = YOLO(model_path)
|
| 59 |
|
| 60 |
self.selected_classes = selected_classes or DEFAULT_CLASSES
|
| 61 |
self.conf = conf_threshold
|
| 62 |
self.scene_name = scene_name
|
|
|
|
|
|
|
| 63 |
self.output_dir = Path(output_dir)
|
| 64 |
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 65 |
|
|
@@ -71,19 +81,19 @@ class TrafficTracker:
|
|
| 71 |
self.frame_height = 0
|
| 72 |
|
| 73 |
# Tracking
|
| 74 |
-
self.track_history: dict = defaultdict(list)
|
| 75 |
self.counted_ids: set = set()
|
| 76 |
self.count_per_class: dict = defaultdict(int)
|
| 77 |
|
| 78 |
# Logs
|
| 79 |
-
self.detection_log: list = []
|
| 80 |
-
self.frame_stats: list = []
|
| 81 |
|
| 82 |
-
# Video writer
|
| 83 |
-
self.video_writer
|
| 84 |
-
self.output_video_path
|
| 85 |
|
| 86 |
-
#
|
| 87 |
self._class_ids = [
|
| 88 |
cid for cid, name in TRAFFIC_CLASSES.items()
|
| 89 |
if name in self.selected_classes
|
|
@@ -108,7 +118,7 @@ class TrafficTracker:
|
|
| 108 |
# ββ Process one frame ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
def process_frame(self, frame: np.ndarray) -> tuple:
|
| 110 |
self.frame_index += 1
|
| 111 |
-
|
| 112 |
|
| 113 |
results = self.model.track(
|
| 114 |
frame,
|
|
@@ -141,30 +151,60 @@ class TrafficTracker:
|
|
| 141 |
any_object_visible = True
|
| 142 |
per_class_in_frame[cls_name] += 1
|
| 143 |
|
| 144 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
is_new = False
|
| 146 |
if track_id != -1 and track_id not in self.counted_ids:
|
| 147 |
self.counted_ids.add(track_id)
|
| 148 |
self.count_per_class[cls_name] += 1
|
| 149 |
is_new = True
|
| 150 |
|
|
|
|
| 151 |
if track_id != -1:
|
| 152 |
self.track_history[track_id].append((cx, cy))
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
color = CLASS_COLORS.get(cls_name, (255, 255, 255))
|
| 169 |
thickness = 3 if is_new else 2
|
| 170 |
cv2.rectangle(annotated, (x1, y1), (x2, y2), color, thickness)
|
|
@@ -186,13 +226,12 @@ class TrafficTracker:
|
|
| 186 |
|
| 187 |
self._draw_counters(annotated, any_object_visible)
|
| 188 |
|
| 189 |
-
# Write annotated frame to output video
|
| 190 |
if self.video_writer is not None:
|
| 191 |
self.video_writer.write(annotated)
|
| 192 |
|
| 193 |
frame_stat = {
|
| 194 |
"frame": self.frame_index,
|
| 195 |
-
"timestamp":
|
| 196 |
"scene": self.scene_name,
|
| 197 |
"detections": len(frame_detections),
|
| 198 |
"per_class": dict(per_class_in_frame),
|
|
@@ -203,7 +242,7 @@ class TrafficTracker:
|
|
| 203 |
|
| 204 |
return annotated, frame_stat
|
| 205 |
|
| 206 |
-
# ββ
|
| 207 |
def _draw_counters(self, frame: np.ndarray, any_visible: bool):
|
| 208 |
overlay = frame.copy()
|
| 209 |
cv2.rectangle(overlay, (0, 0),
|
|
@@ -225,9 +264,8 @@ class TrafficTracker:
|
|
| 225 |
cv2.putText(frame, "NO OBJECTS DETECTED", (10, y + 10),
|
| 226 |
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (80, 80, 80), 1)
|
| 227 |
|
| 228 |
-
# ββ
|
| 229 |
def save_logs(self) -> dict:
|
| 230 |
-
# Release video writer so the .mp4 is properly closed/flushed
|
| 231 |
if self.video_writer is not None:
|
| 232 |
self.video_writer.release()
|
| 233 |
self.video_writer = None
|
|
@@ -235,28 +273,41 @@ class TrafficTracker:
|
|
| 235 |
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 236 |
prefix = f"{self.scene_name}_{ts}"
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
summary = self.get_summary()
|
| 244 |
sum_path = self.output_dir / f"{prefix}_summary.json"
|
| 245 |
-
with open(sum_path, "w") as f:
|
| 246 |
json.dump(summary, f, indent=2)
|
| 247 |
|
| 248 |
-
|
|
|
|
| 249 |
if self.frame_stats:
|
| 250 |
-
with open(
|
| 251 |
writer = csv.DictWriter(f, fieldnames=self.frame_stats[0].keys())
|
| 252 |
writer.writeheader()
|
| 253 |
writer.writerows(self.frame_stats)
|
| 254 |
|
| 255 |
return {
|
| 256 |
-
"
|
| 257 |
-
"
|
| 258 |
-
"
|
| 259 |
-
"
|
|
|
|
| 260 |
}
|
| 261 |
|
| 262 |
# ββ Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -272,6 +323,8 @@ class TrafficTracker:
|
|
| 272 |
|
| 273 |
return {
|
| 274 |
"scene": self.scene_name,
|
|
|
|
|
|
|
| 275 |
"session_id": self.session_id,
|
| 276 |
"processed_at": datetime.now().isoformat(),
|
| 277 |
"total_frames": self.frame_index,
|
|
|
|
| 1 |
"""
|
| 2 |
tracker.py β Core detection + tracking engine
|
| 3 |
+
Counting : FIRST-SEEN (chaque track_id compté une seule fois dès sa 1re apparition)
|
| 4 |
+
CSV log : suit exactement le schΓ©ma SCHEMA_EXAMPLE.csv
|
| 5 |
+
frame, timestamp_sec, scene_name, group_id, video_name, track_id,
|
| 6 |
+
class_name, confidence, bbox_x1, bbox_y1, bbox_x2, bbox_y2,
|
| 7 |
+
cx, cy, frame_width, frame_height, crossed_line, direction, speed_px_s
|
|
|
|
|
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
import cv2
|
| 11 |
import csv
|
| 12 |
import json
|
| 13 |
+
import math
|
| 14 |
import uuid
|
| 15 |
import numpy as np
|
| 16 |
from pathlib import Path
|
|
|
|
| 18 |
from collections import defaultdict
|
| 19 |
from ultralytics import YOLO
|
| 20 |
|
| 21 |
+
# βββ Classes COCO traffic βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 22 |
TRAFFIC_CLASSES = {
|
| 23 |
0: "person",
|
| 24 |
1: "bicycle",
|
|
|
|
| 39 |
|
| 40 |
DEFAULT_CLASSES = list(TRAFFIC_CLASSES.values())
|
| 41 |
|
| 42 |
+
# Colonnes CSV β ordre exact du schΓ©ma
|
| 43 |
+
CSV_COLUMNS = [
|
| 44 |
+
"frame", "timestamp_sec", "scene_name", "group_id", "video_name",
|
| 45 |
+
"track_id", "class_name", "confidence",
|
| 46 |
+
"bbox_x1", "bbox_y1", "bbox_x2", "bbox_y2",
|
| 47 |
+
"cx", "cy", "frame_width", "frame_height",
|
| 48 |
+
"crossed_line", "direction", "speed_px_s",
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
|
| 52 |
class TrafficTracker:
|
| 53 |
def __init__(
|
|
|
|
| 56 |
selected_classes: list = None,
|
| 57 |
conf_threshold: float = 0.35,
|
| 58 |
scene_name: str = "scene_01",
|
| 59 |
+
group_id: str = "group_01",
|
| 60 |
+
video_name: str = "video.mp4",
|
| 61 |
output_dir: str = "logs",
|
| 62 |
):
|
| 63 |
+
# ββ ModΓ¨le ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
+
# Pour utiliser un modèle fine-tuné :
|
|
|
|
|
|
|
| 65 |
# model_path = "runs/detect/traffic_finetune/weights/best.pt"
|
| 66 |
self.model = YOLO(model_path)
|
| 67 |
|
| 68 |
self.selected_classes = selected_classes or DEFAULT_CLASSES
|
| 69 |
self.conf = conf_threshold
|
| 70 |
self.scene_name = scene_name
|
| 71 |
+
self.group_id = group_id # <β nouveau champ schΓ©ma
|
| 72 |
+
self.video_name = video_name # <β nouveau champ schΓ©ma
|
| 73 |
self.output_dir = Path(output_dir)
|
| 74 |
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 75 |
|
|
|
|
| 81 |
self.frame_height = 0
|
| 82 |
|
| 83 |
# Tracking
|
| 84 |
+
self.track_history: dict = defaultdict(list) # id -> [(cx,cy), ...]
|
| 85 |
self.counted_ids: set = set()
|
| 86 |
self.count_per_class: dict = defaultdict(int)
|
| 87 |
|
| 88 |
# Logs
|
| 89 |
+
self.detection_log: list = [] # chaque ligne = une dΓ©tection
|
| 90 |
+
self.frame_stats: list = [] # rΓ©sumΓ© par frame
|
| 91 |
|
| 92 |
+
# Video writer
|
| 93 |
+
self.video_writer = None
|
| 94 |
+
self.output_video_path = None
|
| 95 |
|
| 96 |
+
# Filtre classes COCO
|
| 97 |
self._class_ids = [
|
| 98 |
cid for cid, name in TRAFFIC_CLASSES.items()
|
| 99 |
if name in self.selected_classes
|
|
|
|
| 118 |
# ββ Process one frame ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 119 |
def process_frame(self, frame: np.ndarray) -> tuple:
|
| 120 |
self.frame_index += 1
|
| 121 |
+
timestamp_sec = round(self.frame_index / self.fps, 3)
|
| 122 |
|
| 123 |
results = self.model.track(
|
| 124 |
frame,
|
|
|
|
| 151 |
any_object_visible = True
|
| 152 |
per_class_in_frame[cls_name] += 1
|
| 153 |
|
| 154 |
+
# ββ Vitesse (px/s) ββββββββββββββββββββββββββββββββββββββββββββ
|
| 155 |
+
# Distance euclidienne entre position courante et prΓ©cΓ©dente,
|
| 156 |
+
# multipliΓ©e par fps pour obtenir des px/s.
|
| 157 |
+
speed_px_s = 0.0
|
| 158 |
+
if track_id != -1 and self.track_history[track_id]:
|
| 159 |
+
prev_cx, prev_cy = self.track_history[track_id][-1]
|
| 160 |
+
dist = math.hypot(cx - prev_cx, cy - prev_cy)
|
| 161 |
+
speed_px_s = round(dist * self.fps, 1)
|
| 162 |
+
|
| 163 |
+
# ββ Comptage first-seen βββββββββββββββββββββββββββββββββββββββ
|
| 164 |
is_new = False
|
| 165 |
if track_id != -1 and track_id not in self.counted_ids:
|
| 166 |
self.counted_ids.add(track_id)
|
| 167 |
self.count_per_class[cls_name] += 1
|
| 168 |
is_new = True
|
| 169 |
|
| 170 |
+
# Mise Γ jour historique
|
| 171 |
if track_id != -1:
|
| 172 |
self.track_history[track_id].append((cx, cy))
|
| 173 |
|
| 174 |
+
# ββ Colonnes schΓ©ma : crossed_line & direction βββββββββββββββββ
|
| 175 |
+
# On ne dessine plus de ligne physique sur la vidΓ©o, mais on
|
| 176 |
+
# conserve les colonnes dans le CSV (vide / false par dΓ©faut).
|
| 177 |
+
# Si tu veux rΓ©activer le croisement de ligne, tu peux dΓ©finir
|
| 178 |
+
# une ligne ici et complΓ©ter la logique.
|
| 179 |
+
crossed_line = False
|
| 180 |
+
direction = ""
|
| 181 |
+
|
| 182 |
+
# ββ Ligne CSV (schΓ©ma exact) βββββββββββββββββββββββββββββββββββ
|
| 183 |
+
row = {
|
| 184 |
+
"frame": self.frame_index,
|
| 185 |
+
"timestamp_sec": timestamp_sec,
|
| 186 |
+
"scene_name": self.scene_name,
|
| 187 |
+
"group_id": self.group_id,
|
| 188 |
+
"video_name": self.video_name,
|
| 189 |
+
"track_id": track_id,
|
| 190 |
+
"class_name": cls_name,
|
| 191 |
+
"confidence": round(conf_val, 3),
|
| 192 |
+
"bbox_x1": x1,
|
| 193 |
+
"bbox_y1": y1,
|
| 194 |
+
"bbox_x2": x2,
|
| 195 |
+
"bbox_y2": y2,
|
| 196 |
+
"cx": cx,
|
| 197 |
+
"cy": cy,
|
| 198 |
+
"frame_width": self.frame_width,
|
| 199 |
+
"frame_height": self.frame_height,
|
| 200 |
+
"crossed_line": str(crossed_line).lower(), # "true"/"false"
|
| 201 |
+
"direction": direction,
|
| 202 |
+
"speed_px_s": speed_px_s,
|
| 203 |
+
}
|
| 204 |
+
frame_detections.append(row)
|
| 205 |
+
self.detection_log.append(row)
|
| 206 |
+
|
| 207 |
+
# ββ Bounding box βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 208 |
color = CLASS_COLORS.get(cls_name, (255, 255, 255))
|
| 209 |
thickness = 3 if is_new else 2
|
| 210 |
cv2.rectangle(annotated, (x1, y1), (x2, y2), color, thickness)
|
|
|
|
| 226 |
|
| 227 |
self._draw_counters(annotated, any_object_visible)
|
| 228 |
|
|
|
|
| 229 |
if self.video_writer is not None:
|
| 230 |
self.video_writer.write(annotated)
|
| 231 |
|
| 232 |
frame_stat = {
|
| 233 |
"frame": self.frame_index,
|
| 234 |
+
"timestamp": timestamp_sec,
|
| 235 |
"scene": self.scene_name,
|
| 236 |
"detections": len(frame_detections),
|
| 237 |
"per_class": dict(per_class_in_frame),
|
|
|
|
| 242 |
|
| 243 |
return annotated, frame_stat
|
| 244 |
|
| 245 |
+
# ββ Overlay compteurs ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 246 |
def _draw_counters(self, frame: np.ndarray, any_visible: bool):
|
| 247 |
overlay = frame.copy()
|
| 248 |
cv2.rectangle(overlay, (0, 0),
|
|
|
|
| 264 |
cv2.putText(frame, "NO OBJECTS DETECTED", (10, y + 10),
|
| 265 |
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (80, 80, 80), 1)
|
| 266 |
|
| 267 |
+
# ββ Sauvegarde des logs ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 268 |
def save_logs(self) -> dict:
|
|
|
|
| 269 |
if self.video_writer is not None:
|
| 270 |
self.video_writer.release()
|
| 271 |
self.video_writer = None
|
|
|
|
| 273 |
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 274 |
prefix = f"{self.scene_name}_{ts}"
|
| 275 |
|
| 276 |
+
# ββ CSV principal β schΓ©ma exact ββββββββββββββββββββββββββββββββββββββ
|
| 277 |
+
csv_path = self.output_dir / f"{prefix}_detections.csv"
|
| 278 |
+
with open(csv_path, "w", newline="", encoding="utf-8") as f:
|
| 279 |
+
writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS)
|
| 280 |
+
writer.writeheader()
|
| 281 |
+
for row in self.detection_log:
|
| 282 |
+
# S'assurer que toutes les colonnes sont prΓ©sentes
|
| 283 |
+
writer.writerow({col: row.get(col, "") for col in CSV_COLUMNS})
|
| 284 |
+
|
| 285 |
+
# ββ JSONL brut (optionnel, pour debug) ββββββββββββββββββββββββββββββββ
|
| 286 |
+
jsonl_path = self.output_dir / f"{prefix}_detections.jsonl"
|
| 287 |
+
with open(jsonl_path, "w", encoding="utf-8") as f:
|
| 288 |
+
for row in self.detection_log:
|
| 289 |
+
f.write(json.dumps(row) + "\n")
|
| 290 |
+
|
| 291 |
+
# ββ Summary JSON ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 292 |
summary = self.get_summary()
|
| 293 |
sum_path = self.output_dir / f"{prefix}_summary.json"
|
| 294 |
+
with open(sum_path, "w", encoding="utf-8") as f:
|
| 295 |
json.dump(summary, f, indent=2)
|
| 296 |
|
| 297 |
+
# ββ Frame stats CSV (rΓ©sumΓ© par frame) ββββββββββββββββββββββββββββββββ
|
| 298 |
+
fstats_path = self.output_dir / f"{prefix}_frame_stats.csv"
|
| 299 |
if self.frame_stats:
|
| 300 |
+
with open(fstats_path, "w", newline="", encoding="utf-8") as f:
|
| 301 |
writer = csv.DictWriter(f, fieldnames=self.frame_stats[0].keys())
|
| 302 |
writer.writeheader()
|
| 303 |
writer.writerows(self.frame_stats)
|
| 304 |
|
| 305 |
return {
|
| 306 |
+
"detections_csv": str(csv_path),
|
| 307 |
+
"detections_jsonl": str(jsonl_path),
|
| 308 |
+
"summary": str(sum_path),
|
| 309 |
+
"frame_stats": str(fstats_path),
|
| 310 |
+
"annotated_video": self.output_video_path or "",
|
| 311 |
}
|
| 312 |
|
| 313 |
# ββ Summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 323 |
|
| 324 |
return {
|
| 325 |
"scene": self.scene_name,
|
| 326 |
+
"group_id": self.group_id,
|
| 327 |
+
"video_name": self.video_name,
|
| 328 |
"session_id": self.session_id,
|
| 329 |
"processed_at": datetime.now().isoformat(),
|
| 330 |
"total_frames": self.frame_index,
|
frontend/index.html
CHANGED
|
@@ -321,6 +321,10 @@
|
|
| 321 |
<label>SCENE NAME</label>
|
| 322 |
<input type="text" id="sceneName" value="scene_01"/>
|
| 323 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
<div class="field">
|
| 325 |
<label>MODEL</label>
|
| 326 |
<select id="modelSel">
|
|
@@ -538,6 +542,7 @@ async function startProcessing() {
|
|
| 538 |
const fd = new FormData();
|
| 539 |
fd.append('file', selectedFile);
|
| 540 |
fd.append('scene_name', document.getElementById('sceneName').value || 'scene_01');
|
|
|
|
| 541 |
fd.append('classes', selected.join(','));
|
| 542 |
fd.append('conf', document.getElementById('confVal').value);
|
| 543 |
fd.append('model', document.getElementById('modelSel').value);
|
|
|
|
| 321 |
<label>SCENE NAME</label>
|
| 322 |
<input type="text" id="sceneName" value="scene_01"/>
|
| 323 |
</div>
|
| 324 |
+
<div class="field">
|
| 325 |
+
<label>GROUP ID</label>
|
| 326 |
+
<input type="text" id="groupId" value="group_01"/>
|
| 327 |
+
</div>
|
| 328 |
<div class="field">
|
| 329 |
<label>MODEL</label>
|
| 330 |
<select id="modelSel">
|
|
|
|
| 542 |
const fd = new FormData();
|
| 543 |
fd.append('file', selectedFile);
|
| 544 |
fd.append('scene_name', document.getElementById('sceneName').value || 'scene_01');
|
| 545 |
+
fd.append('group_id', document.getElementById('groupId').value || 'group_01');
|
| 546 |
fd.append('classes', selected.join(','));
|
| 547 |
fd.append('conf', document.getElementById('confVal').value);
|
| 548 |
fd.append('model', document.getElementById('modelSel').value);
|