cyberai-1 commited on
Commit
ef3e83f
Β·
1 Parent(s): acd405a

update log

Browse files
Files changed (3) hide show
  1. backend/main.py +10 -6
  2. backend/tracker.py +105 -52
  3. 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
- # Unlimited queue β€” frames are never dropped before the client reads them.
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
- Uses YOLOv8 (ultralytics) with ByteTrack built-in tracker.
4
-
5
- Counting strategy: FIRST-SEEN β€” each unique track_id is counted exactly
6
- once, on the first frame it appears. No counting line.
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
- # ─── Class configuration ──────────────────────────────────────────────────────
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
- # ── Model ─────────────────────────────────────────────────────────────
54
- # model_path is selected in the frontend dropdown (main.py receives it
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 (set in setup_video)
83
- self.video_writer = None
84
- self.output_video_path = None
85
 
86
- # COCO class-id filter
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
- timestamp = self.frame_index / self.fps
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
- # First-seen counting
 
 
 
 
 
 
 
 
 
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
- frame_detections.append({
155
- "frame": self.frame_index,
156
- "timestamp": round(timestamp, 3),
157
- "scene": self.scene_name,
158
- "track_id": track_id,
159
- "class": cls_name,
160
- "confidence": round(conf_val, 3),
161
- "bbox": [x1, y1, x2, y2],
162
- "center": [cx, cy],
163
- "is_new": is_new,
164
- })
165
- self.detection_log.append(frame_detections[-1])
166
-
167
- # Draw bounding box
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": round(timestamp, 3),
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
- # ── Counters overlay ───────────────────────────────────────────────────────
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
- # ── Finalise and save all logs ─────────────────────────────────────────────
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
- log_path = self.output_dir / f"{prefix}_detections.jsonl"
239
- with open(log_path, "w") as f:
240
- for entry in self.detection_log:
241
- f.write(json.dumps(entry) + "\n")
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
- csv_path = self.output_dir / f"{prefix}_frame_stats.csv"
 
249
  if self.frame_stats:
250
- with open(csv_path, "w", newline="") as f:
251
  writer = csv.DictWriter(f, fieldnames=self.frame_stats[0].keys())
252
  writer.writeheader()
253
  writer.writerows(self.frame_stats)
254
 
255
  return {
256
- "detection_log": str(log_path),
257
- "summary": str(sum_path),
258
- "frame_stats": str(csv_path),
259
- "annotated_video": self.output_video_path or "",
 
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);