Subh775 commited on
Commit
b96bb3a
·
1 Parent(s): fbe8d6c

new feat: dynamic rolling counters, theme switch, heatmap and csv for raw data

Browse files
backend/engine.py CHANGED
@@ -101,6 +101,8 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
101
  congestion = []
102
  flow_times = []
103
  conf_scores = []
 
 
104
 
105
  start = time.time()
106
 
@@ -154,6 +156,8 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
154
  cx = int((box[0] + box[2]) / 2)
155
  cy = int((box[1] + box[3]) / 2)
156
 
 
 
157
  current = _side((cx, cy), a, b)
158
 
159
  # Skip if centroid is exactly on the line (cross-product == 0)
@@ -170,8 +174,10 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
170
 
171
  if current > 0:
172
  class_in[int(c)] += 1
 
173
  else:
174
  class_out[int(c)] += 1
 
175
 
176
  counted_ids.add(obj_id)
177
 
@@ -216,6 +222,8 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
216
  "congestion": congestion,
217
  "flow_times": flow_times,
218
  "conf_scores": conf_scores,
 
 
219
  "processing_time": processing_time,
220
  "actual_fps": actual_fps,
221
  "speed_vs_realtime": speed_vs_rt,
 
101
  congestion = []
102
  flow_times = []
103
  conf_scores = []
104
+ heatmap_points = []
105
+ raw_events = [["frame_index", "timestamp_sec", "vehicle_id", "class_name", "direction"]]
106
 
107
  start = time.time()
108
 
 
156
  cx = int((box[0] + box[2]) / 2)
157
  cy = int((box[1] + box[3]) / 2)
158
 
159
+ heatmap_points.append([cx, cy])
160
+
161
  current = _side((cx, cy), a, b)
162
 
163
  # Skip if centroid is exactly on the line (cross-product == 0)
 
174
 
175
  if current > 0:
176
  class_in[int(c)] += 1
177
+ raw_events.append([frame_idx + 1, round(t, 2), int(obj_id), MODEL_CLASSES.get(int(c), f"cls_{int(c)}"), "IN"])
178
  else:
179
  class_out[int(c)] += 1
180
+ raw_events.append([frame_idx + 1, round(t, 2), int(obj_id), MODEL_CLASSES.get(int(c), f"cls_{int(c)}"), "OUT"])
181
 
182
  counted_ids.add(obj_id)
183
 
 
222
  "congestion": congestion,
223
  "flow_times": flow_times,
224
  "conf_scores": conf_scores,
225
+ "heatmap_points": heatmap_points,
226
+ "raw_events": raw_events,
227
  "processing_time": processing_time,
228
  "actual_fps": actual_fps,
229
  "speed_vs_realtime": speed_vs_rt,
backend/server.py CHANGED
@@ -177,6 +177,7 @@ async def ws_run(ws: WebSocket):
177
 
178
  result = task.result() # re-raises any exception from the engine
179
  result["report_format"] = report_format
 
180
  run_results[video_id] = result
181
  await ws.send_text(json.dumps({
182
  "done": True,
 
177
 
178
  result = task.result() # re-raises any exception from the engine
179
  result["report_format"] = report_format
180
+ result["video_path"] = path
181
  run_results[video_id] = result
182
  await ws.send_text(json.dumps({
183
  "done": True,
backend/visualize.py CHANGED
@@ -1,5 +1,7 @@
1
  import os
2
  import numpy as np
 
 
3
  import matplotlib
4
  matplotlib.use("Agg")
5
  import matplotlib.pyplot as plt
@@ -158,6 +160,51 @@ def confidence_dist(conf_scores, out_dir, fmt="png"):
158
  return f"confidence_dist.{ext}"
159
 
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  def generate_all(data, model_classes, out_dir, report_format="png"):
162
  os.makedirs(out_dir, exist_ok=True)
163
 
@@ -172,6 +219,10 @@ def generate_all(data, model_classes, out_dir, report_format="png"):
172
 
173
  fmt = report_format
174
 
 
 
 
 
175
  files = []
176
  for fn in [
177
  lambda: direction_pie(total_in, total_out, out_dir, fmt),
@@ -179,6 +230,8 @@ def generate_all(data, model_classes, out_dir, report_format="png"):
179
  lambda: congestion_chart(data.get("congestion", []), out_dir, fmt),
180
  lambda: class_dominance(data["class_in"], data["class_out"], model_classes, out_dir, fmt),
181
  lambda: confidence_dist(data.get("conf_scores", []), out_dir, fmt),
 
 
182
  ]:
183
  name = fn()
184
  if name:
 
1
  import os
2
  import numpy as np
3
+ import cv2
4
+ import csv
5
  import matplotlib
6
  matplotlib.use("Agg")
7
  import matplotlib.pyplot as plt
 
160
  return f"confidence_dist.{ext}"
161
 
162
 
163
+ def export_csv(raw_events, out_dir):
164
+ if not raw_events or len(raw_events) <= 1:
165
+ return None
166
+ path = os.path.join(out_dir, "raw_data.csv")
167
+ with open(path, mode="w", newline="") as f:
168
+ writer = csv.writer(f)
169
+ writer.writerows(raw_events)
170
+ return "raw_data.csv"
171
+
172
+ def spatial_heatmap(heatmap_points, video_path, out_dir):
173
+ if not heatmap_points or not video_path or not os.path.exists(video_path):
174
+ return None
175
+
176
+ cap = cv2.VideoCapture(video_path)
177
+ ret, frame = cap.read()
178
+ cap.release()
179
+ if not ret:
180
+ return None
181
+
182
+ h, w = frame.shape[:2]
183
+ density = np.zeros((h, w), dtype=np.float32)
184
+
185
+ for pt in heatmap_points:
186
+ cx, cy = int(pt[0]), int(pt[1])
187
+ if 0 <= cx < w and 0 <= cy < h:
188
+ cv2.circle(density, (cx, cy), 15, 1.0, -1)
189
+
190
+ density = cv2.GaussianBlur(density, (75, 75), 0)
191
+
192
+ max_val = np.max(density)
193
+ if max_val > 0:
194
+ density = (density / max_val) * 255.0
195
+ density = density.astype(np.uint8)
196
+
197
+ heatmap = cv2.applyColorMap(density, cv2.COLORMAP_JET)
198
+ mask = density > 10
199
+
200
+ overlay = frame.copy()
201
+ overlay[mask] = cv2.addWeighted(frame[mask], 0.3, heatmap[mask], 0.7, 0).squeeze()
202
+
203
+ path = os.path.join(out_dir, "heatmap.png")
204
+ cv2.imwrite(path, overlay)
205
+ return "heatmap.png"
206
+
207
+
208
  def generate_all(data, model_classes, out_dir, report_format="png"):
209
  os.makedirs(out_dir, exist_ok=True)
210
 
 
219
 
220
  fmt = report_format
221
 
222
+ video_path = data.get("video_path")
223
+ heatmap_points = data.get("heatmap_points", [])
224
+ raw_events = data.get("raw_events", [])
225
+
226
  files = []
227
  for fn in [
228
  lambda: direction_pie(total_in, total_out, out_dir, fmt),
 
230
  lambda: congestion_chart(data.get("congestion", []), out_dir, fmt),
231
  lambda: class_dominance(data["class_in"], data["class_out"], model_classes, out_dir, fmt),
232
  lambda: confidence_dist(data.get("conf_scores", []), out_dir, fmt),
233
+ lambda: spatial_heatmap(heatmap_points, video_path, out_dir),
234
+ lambda: export_csv(raw_events, out_dir),
235
  ]:
236
  name = fn()
237
  if name:
frontend/vehicles.html CHANGED
@@ -26,6 +26,37 @@
26
  --cocoa: #8b5e3c;
27
  --cocoa-l: #c89a6c;
28
  --cocoa-xl: #d4b08a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
 
31
  body {
@@ -137,14 +168,14 @@
137
  .bg-slate-50,
138
  .bg-slate-900,
139
  .bg-neutral-900 {
140
- background-color: #0c0c0c !important;
141
  }
142
 
143
  .text-slate-900,
144
  .text-slate-800,
145
  .text-slate-700,
146
  .text-neutral-900 {
147
- color: #ffffff !important;
148
  }
149
 
150
  .text-slate-600,
@@ -152,7 +183,7 @@
152
  .text-slate-400,
153
  .text-neutral-500,
154
  .text-neutral-400 {
155
- color: #888888 !important;
156
  }
157
 
158
  .shadow-sm {
@@ -342,17 +373,17 @@
342
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
343
  <i class="fa-solid fa-circle-info w-6"></i> <span class="font-medium">About</span>
344
  </a>
345
- <a onclick="switchTab('overview')" id="nav-overview"
346
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
347
- <i class="fa-solid fa-desktop w-6"></i> <span class="font-medium">Overview</span>
348
  </a>
349
  <a onclick="switchTab('run-details')" id="nav-run-details"
350
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
351
  <i class="fa-solid fa-microchip w-6"></i> <span class="font-medium">Run</span>
352
  </a>
353
- <a onclick="switchTab('reports')" id="nav-reports"
354
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
355
- <i class="fa-solid fa-file-lines w-6"></i> <span class="font-medium">Reports</span>
356
  </a>
357
  <a onclick="switchTab('settings')" id="nav-settings"
358
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
@@ -635,8 +666,8 @@
635
 
636
 
637
 
638
- <!-- TAB: Reports -->
639
- <div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
640
  <div id="reports-pending" class="flex flex-col items-center justify-center h-full">
641
  <i class="fa-solid fa-hourglass-half text-4xl text-slate-300 mb-4"></i>
642
  <p class="text-sm font-medium text-slate-400">Reports will be available after processing completes.</p>
@@ -647,6 +678,23 @@
647
  <!-- TAB: Settings -->
648
  <div id="tab-settings" class="hidden flex-1 min-h-0 overflow-y-auto">
649
  <div class="grid grid-cols-3 gap-6 w-full">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
 
651
  <!-- Processing Parameters -->
652
  <div class="col-span-2 bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
@@ -741,39 +789,7 @@
741
  </div>
742
  </div>
743
 
744
- <!-- Display Preferences -->
745
- <div
746
- class="col-span-1 bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
747
- <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
748
- <h3 class="font-bold text-slate-800 text-sm">Display Preferences</h3>
749
- </div>
750
- <div class="px-5 py-4 flex-1 flex flex-col">
751
- <div class="s-row">
752
- <div>
753
- <div class="text-xs font-semibold text-slate-700">Chart Color Palette</div>
754
- <div class="text-[10px] text-slate-400">Color scheme for all charts</div>
755
- </div>
756
- <select class="custom-select" id="live-palette-select" onchange="applyPalette(this.value)">
757
- <option value="default">Default</option>
758
- <option value="vibrant">Vibrant</option>
759
- <option value="corporate">Corporate</option>
760
- </select>
761
- </div>
762
- <div class="s-row hidden">
763
- <div>
764
- <div class="text-xs font-semibold text-slate-700">Interface Mode</div>
765
- <div class="text-[10px] text-slate-400">Locked to Professional Dark</div>
766
- </div>
767
- <div class="text-xs font-bold text-white px-3 py-1 bg-slate-800 rounded-full">Dark Mode Only
768
- </div>
769
- </div>
770
- <div class="mt-3">
771
- <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2">Palette
772
- Preview</div>
773
- <div class="flex gap-1.5" id="live-palette-preview"></div>
774
- </div>
775
- </div>
776
- </div>
777
 
778
  <!-- Start Button -->
779
  <div class="col-span-3 pb-4" id="settings-start-wrap">
@@ -800,6 +816,34 @@
800
 
801
  <script>
802
  // =========== Theme Management ===========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
  // =========== Tooltip ===========
804
  // Position and toggle tooltip visibility
805
  document.addEventListener('mouseover', e => {
@@ -1404,11 +1448,17 @@
1404
  const pct = ((d.frame_index / d.total_iters) * 100).toFixed(1);
1405
  document.getElementById('proc-bar').style.width = pct + '%';
1406
  document.getElementById('proc-frames').innerText = `${d.frame_index} / ${d.total_iters} Frames`;
1407
- document.getElementById('proc-pct').innerText = pct + '%';
 
 
 
1408
 
1409
  const totalIn = sumValues(d.class_in);
1410
  const totalOut = sumValues(d.class_out);
1411
- document.getElementById('cnt-total').innerText = totalIn + totalOut;
 
 
 
1412
 
1413
  // Update doughnut
1414
  doughChart.data.datasets[0].data = [totalIn, totalOut];
@@ -1434,9 +1484,11 @@
1434
  'congestion_index.pdf': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
1435
  'class_dominance.png': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
1436
  'class_dominance.pdf': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
1437
- 'confidence_dist.png': { title: 'Confidence Distribution', desc: 'Detection confidence histogram' },
1438
- 'confidence_dist.pdf': { title: 'Confidence Distribution', desc: 'Detection confidence histogram' },
1439
- 'annotated.mp4': { title: 'Annotated Video', desc: 'Video with custom diagnostic overlays (Boxes, Names, IDs, etc.)' },
 
 
1440
  };
1441
 
1442
  async function loadReports(videoId) {
 
26
  --cocoa: #8b5e3c;
27
  --cocoa-l: #c89a6c;
28
  --cocoa-xl: #d4b08a;
29
+ --chart-grid: #2a2a2a;
30
+ }
31
+
32
+ :root[data-theme="neon"] {
33
+ --bg-primary: #090014;
34
+ --bg-secondary: #090014;
35
+ --text-primary: #ffffff;
36
+ --text-secondary: #b399ff;
37
+ --border-color: #3b0086;
38
+ --sidebar-bg: #090014;
39
+ --card-bg: #12002b;
40
+ --input-bg: #1a0040;
41
+ --cocoa: #ff00ff;
42
+ --cocoa-l: #00ffff;
43
+ --cocoa-xl: #00ffff;
44
+ --chart-grid: #2a005e;
45
+ }
46
+
47
+ :root[data-theme="glass"] {
48
+ --bg-primary: #f8fafc;
49
+ --bg-secondary: #f8fafc;
50
+ --text-primary: #0f172a;
51
+ --text-secondary: #475569;
52
+ --border-color: #cbd5e1;
53
+ --sidebar-bg: #f8fafc;
54
+ --card-bg: #ffffff;
55
+ --input-bg: #f1f5f9;
56
+ --cocoa: #2563eb;
57
+ --cocoa-l: #3b82f6;
58
+ --cocoa-xl: #1d4ed8;
59
+ --chart-grid: #e2e8f0;
60
  }
61
 
62
  body {
 
168
  .bg-slate-50,
169
  .bg-slate-900,
170
  .bg-neutral-900 {
171
+ background-color: var(--input-bg) !important;
172
  }
173
 
174
  .text-slate-900,
175
  .text-slate-800,
176
  .text-slate-700,
177
  .text-neutral-900 {
178
+ color: var(--text-primary) !important;
179
  }
180
 
181
  .text-slate-600,
 
183
  .text-slate-400,
184
  .text-neutral-500,
185
  .text-neutral-400 {
186
+ color: var(--text-secondary) !important;
187
  }
188
 
189
  .shadow-sm {
 
373
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
374
  <i class="fa-solid fa-circle-info w-6"></i> <span class="font-medium">About</span>
375
  </a>
376
+ <a onclick="switchTab('stats')" id="nav-stats"
377
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
378
+ <i class="fa-solid fa-desktop w-6"></i> <span class="font-medium">Stats</span>
379
  </a>
380
  <a onclick="switchTab('run-details')" id="nav-run-details"
381
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
382
  <i class="fa-solid fa-microchip w-6"></i> <span class="font-medium">Run</span>
383
  </a>
384
+ <a onclick="switchTab('intelligence')" id="nav-intelligence"
385
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
386
+ <i class="fa-solid fa-file-lines w-6"></i> <span class="font-medium">Intelligence</span>
387
  </a>
388
  <a onclick="switchTab('settings')" id="nav-settings"
389
  class="flex items-center px-4 py-2.5 rounded-lg transition cursor-pointer nav-item-inactive">
 
666
 
667
 
668
 
669
+ <!-- TAB: Intelligence -->
670
+ <div id="tab-intelligence" class="hidden flex-1 min-h-0 overflow-y-auto">
671
  <div id="reports-pending" class="flex flex-col items-center justify-center h-full">
672
  <i class="fa-solid fa-hourglass-half text-4xl text-slate-300 mb-4"></i>
673
  <p class="text-sm font-medium text-slate-400">Reports will be available after processing completes.</p>
 
678
  <!-- TAB: Settings -->
679
  <div id="tab-settings" class="hidden flex-1 min-h-0 overflow-y-auto">
680
  <div class="grid grid-cols-3 gap-6 w-full">
681
+ <!-- Output Format & Themes -->
682
+ <div class="col-span-1 flex flex-col gap-6">
683
+ <!-- Themes Card -->
684
+ <div class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
685
+ <div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
686
+ <h3 class="font-bold text-slate-800 text-sm">Dashboard Theme</h3>
687
+ <p class="text-[10px] text-slate-400 mt-0.1 uppercase tracking-widest font-medium">Select aesthetic</p>
688
+ </div>
689
+ <div class="p-6">
690
+ <select id="theme-selector" class="custom-select w-full" onchange="setTheme(this.value)">
691
+ <option value="cocoa" selected>Cocoa & Slate (Classic)</option>
692
+ <option value="neon">Neon Cyberpunk</option>
693
+ <option value="glass">Arctic Glass (Light)</option>
694
+ </select>
695
+ </div>
696
+ </div>
697
+ </div>
698
 
699
  <!-- Processing Parameters -->
700
  <div class="col-span-2 bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
 
789
  </div>
790
  </div>
791
 
792
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
 
794
  <!-- Start Button -->
795
  <div class="col-span-3 pb-4" id="settings-start-wrap">
 
816
 
817
  <script>
818
  // =========== Theme Management ===========
819
+ function setTheme(theme) {
820
+ document.documentElement.setAttribute('data-theme', theme);
821
+ if (window.pieChart) window.pieChart.update();
822
+ if (window.domChart) window.domChart.update();
823
+ if (window.flowChart) window.flowChart.update();
824
+ if (window.congChart) window.congChart.update();
825
+ if (window.confChart) window.confChart.update();
826
+ }
827
+
828
+ // =========== Rolling Counter ===========
829
+ function animateValue(obj, start, end, duration) {
830
+ let startTimestamp = null;
831
+ const step = (timestamp) => {
832
+ if (!startTimestamp) startTimestamp = timestamp;
833
+ const progress = Math.min((timestamp - startTimestamp) / duration, 1);
834
+ // easeOutQuad
835
+ const ease = progress * (2 - progress);
836
+ const current = Math.floor(ease * (end - start) + start);
837
+ obj.innerText = typeof end === 'string' && end.includes('%') ? current + '%' : current;
838
+ if (progress < 1) {
839
+ window.requestAnimationFrame(step);
840
+ } else {
841
+ obj.innerText = end;
842
+ }
843
+ };
844
+ window.requestAnimationFrame(step);
845
+ }
846
+
847
  // =========== Tooltip ===========
848
  // Position and toggle tooltip visibility
849
  document.addEventListener('mouseover', e => {
 
1448
  const pct = ((d.frame_index / d.total_iters) * 100).toFixed(1);
1449
  document.getElementById('proc-bar').style.width = pct + '%';
1450
  document.getElementById('proc-frames').innerText = `${d.frame_index} / ${d.total_iters} Frames`;
1451
+
1452
+ const procPctEl = document.getElementById('proc-pct');
1453
+ const currPct = parseFloat(procPctEl.innerText) || 0;
1454
+ animateValue(procPctEl, currPct, pct + '%', 300);
1455
 
1456
  const totalIn = sumValues(d.class_in);
1457
  const totalOut = sumValues(d.class_out);
1458
+
1459
+ const cntTotalEl = document.getElementById('cnt-total');
1460
+ const currTotal = parseInt(cntTotalEl.innerText) || 0;
1461
+ animateValue(cntTotalEl, currTotal, totalIn + totalOut, 300);
1462
 
1463
  // Update doughnut
1464
  doughChart.data.datasets[0].data = [totalIn, totalOut];
 
1484
  'congestion_index.pdf': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
1485
  'class_dominance.png': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
1486
  'class_dominance.pdf': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
1487
+ 'confidence_dist.png': { title: 'Confidence Curve', desc: 'Distribution of detection confidences' },
1488
+ 'confidence_dist.pdf': { title: 'Confidence Curve', desc: 'Distribution of detection confidences' },
1489
+ 'annotated.mp4': { title: 'Annotated Video Export', desc: 'Rendered video with tracking overlays' },
1490
+ 'heatmap.png': { title: 'Spatial Density Heatmap', desc: 'Aggregated path utilization mapping' },
1491
+ 'raw_data.csv': { title: 'Raw Analytics Export', desc: 'Comma-separated values of all crossings' }
1492
  };
1493
 
1494
  async function loadReports(videoId) {