Raj Bhalerao commited on
Commit
2a7f65a
·
1 Parent(s): c1bf807

Reports, improvements, fixes..

Browse files
README.md CHANGED
@@ -18,15 +18,19 @@ Full-stack traffic analytics dashboard. YOLO + ByteTrack backend processes uploa
18
 
19
  ## Stack
20
 
21
- - **Backend**: FastAPI, Ultralytics YOLO (ONNX), ByteTrack, OpenCV
22
  - **Frontend**: Vanilla HTML/JS, TailwindCSS CDN, Chart.js
23
  - **Infra**: Docker, CPU-only inference
24
 
25
  ## Run Locally with Docker
 
 
 
 
26
 
27
  ```bash
28
  docker build -t urbanflow .
29
- docker run -p 7860:7860 -e HF_TOKEN=hf_your_token_here urbanflow
30
 
31
  ```
32
 
 
18
 
19
  ## Stack
20
 
21
+ - **Backend**: FastAPI, Ultralytics YOLO (OpenVINO), ByteTrack, OpenCV
22
  - **Frontend**: Vanilla HTML/JS, TailwindCSS CDN, Chart.js
23
  - **Infra**: Docker, CPU-only inference
24
 
25
  ## Run Locally with Docker
26
+ save tokens in .env
27
+ ```bash
28
+ HF_TOKEN=hf_xxxxx...
29
+ ```
30
 
31
  ```bash
32
  docker build -t urbanflow .
33
+ docker run -p 7860:7860 --env-file .env urbanflow
34
 
35
  ```
36
 
backend/engine.py CHANGED
@@ -36,6 +36,7 @@ def run(model, video_path, line, config, on_frame):
36
  class_out = defaultdict(int)
37
  congestion = []
38
  flow_times = []
 
39
 
40
  start = time.time()
41
 
@@ -63,6 +64,8 @@ def run(model, video_path, line, config, on_frame):
63
  xyxy = r.boxes.xyxy.cpu().numpy()
64
 
65
  active = len(ids)
 
 
66
 
67
  for obj_id, c, box in zip(ids, cls, xyxy):
68
  cx = int((box[0] + box[2]) / 2)
@@ -114,6 +117,7 @@ def run(model, video_path, line, config, on_frame):
114
  "class_out": dict(class_out),
115
  "congestion": congestion,
116
  "flow_times": flow_times,
 
117
  "processing_time": processing_time,
118
  "actual_fps": actual_fps,
119
  "speed_vs_realtime": speed_vs_rt,
 
36
  class_out = defaultdict(int)
37
  congestion = []
38
  flow_times = []
39
+ conf_scores = []
40
 
41
  start = time.time()
42
 
 
64
  xyxy = r.boxes.xyxy.cpu().numpy()
65
 
66
  active = len(ids)
67
+ confs = r.boxes.conf.cpu().numpy().tolist()
68
+ conf_scores.extend(confs)
69
 
70
  for obj_id, c, box in zip(ids, cls, xyxy):
71
  cx = int((box[0] + box[2]) / 2)
 
117
  "class_out": dict(class_out),
118
  "congestion": congestion,
119
  "flow_times": flow_times,
120
+ "conf_scores": conf_scores,
121
  "processing_time": processing_time,
122
  "actual_fps": actual_fps,
123
  "speed_vs_realtime": speed_vs_rt,
backend/server.py CHANGED
@@ -14,6 +14,7 @@ from model import load_model
14
  from config import get_optimal_config
15
  from engine import run
16
  from constants import MODEL_CLASSES, BUSINESS_MAP
 
17
 
18
  app = FastAPI()
19
 
@@ -21,8 +22,11 @@ BASE = Path(__file__).parent.parent
21
  FRONTEND = BASE / "frontend"
22
  UPLOAD_DIR = Path(tempfile.gettempdir()) / "funky_uploads"
23
  UPLOAD_DIR.mkdir(exist_ok=True)
 
 
24
 
25
  videos = {}
 
26
  model = None
27
 
28
 
@@ -69,6 +73,24 @@ def constants():
69
  return {"classes": MODEL_CLASSES, "business_map": BUSINESS_MAP}
70
 
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  @app.websocket("/ws/run")
73
  async def ws_run(ws: WebSocket):
74
  await ws.accept()
@@ -102,8 +124,10 @@ async def ws_run(ws: WebSocket):
102
  await asyncio.sleep(0.05)
103
 
104
  result = task.result()
 
105
  await ws.send_text(json.dumps({
106
  "done": True,
 
107
  "processing_time": result["processing_time"],
108
  "actual_fps": result["actual_fps"],
109
  "speed_vs_realtime": result["speed_vs_realtime"],
 
14
  from config import get_optimal_config
15
  from engine import run
16
  from constants import MODEL_CLASSES, BUSINESS_MAP
17
+ from visualize import generate_all
18
 
19
  app = FastAPI()
20
 
 
22
  FRONTEND = BASE / "frontend"
23
  UPLOAD_DIR = Path(tempfile.gettempdir()) / "funky_uploads"
24
  UPLOAD_DIR.mkdir(exist_ok=True)
25
+ REPORT_DIR = Path(tempfile.gettempdir()) / "funky_reports"
26
+ REPORT_DIR.mkdir(exist_ok=True)
27
 
28
  videos = {}
29
+ run_results = {}
30
  model = None
31
 
32
 
 
73
  return {"classes": MODEL_CLASSES, "business_map": BUSINESS_MAP}
74
 
75
 
76
+ @app.post("/reports/{video_id}")
77
+ def generate_reports(video_id: str):
78
+ data = run_results.get(video_id)
79
+ if not data:
80
+ return {"error": "no results", "files": []}
81
+ out_dir = str(REPORT_DIR / video_id)
82
+ files = generate_all(data, MODEL_CLASSES, out_dir)
83
+ return {"files": files}
84
+
85
+
86
+ @app.get("/reports/{video_id}/{name}")
87
+ def get_report(video_id: str, name: str):
88
+ path = REPORT_DIR / video_id / name
89
+ if not path.exists():
90
+ return Response(status_code=404)
91
+ return FileResponse(str(path), media_type="image/png")
92
+
93
+
94
  @app.websocket("/ws/run")
95
  async def ws_run(ws: WebSocket):
96
  await ws.accept()
 
124
  await asyncio.sleep(0.05)
125
 
126
  result = task.result()
127
+ run_results[video_id] = result
128
  await ws.send_text(json.dumps({
129
  "done": True,
130
+ "video_id": video_id,
131
  "processing_time": result["processing_time"],
132
  "actual_fps": result["actual_fps"],
133
  "speed_vs_realtime": result["speed_vs_realtime"],
backend/visualize.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import matplotlib
4
+ matplotlib.use("Agg")
5
+ import matplotlib.pyplot as plt
6
+ from matplotlib.ticker import MaxNLocator
7
+
8
+ # Formal MIS palette
9
+ C_PRIMARY = "#1e293b"
10
+ C_ACCENT = "#334155"
11
+ C_IN = "#059669"
12
+ C_OUT = "#dc2626"
13
+ C_FLOW = "#2563eb"
14
+ C_CONG = "#d97706"
15
+ C_CONF = "#7c3aed"
16
+ C_BAR = "#0f766e"
17
+ C_GRID = "#e2e8f0"
18
+ C_BG = "#ffffff"
19
+
20
+ def _style(ax, title, xlabel="", ylabel=""):
21
+ ax.set_title(title, fontsize=13, fontweight="700", color=C_PRIMARY, pad=14)
22
+ if xlabel:
23
+ ax.set_xlabel(xlabel, fontsize=9, fontweight="600", color=C_ACCENT)
24
+ if ylabel:
25
+ ax.set_ylabel(ylabel, fontsize=9, fontweight="600", color=C_ACCENT)
26
+ ax.tick_params(labelsize=8, colors=C_ACCENT)
27
+ ax.spines["top"].set_visible(False)
28
+ ax.spines["right"].set_visible(False)
29
+ ax.spines["left"].set_color(C_GRID)
30
+ ax.spines["bottom"].set_color(C_GRID)
31
+ ax.yaxis.grid(True, color=C_GRID, linewidth=0.6, alpha=0.8)
32
+ ax.set_axisbelow(True)
33
+
34
+
35
+ def _save(fig, path):
36
+ fig.savefig(path, dpi=200, bbox_inches="tight", facecolor=C_BG, edgecolor="none")
37
+ plt.close(fig)
38
+
39
+
40
+ def direction_pie(total_in, total_out, out_dir):
41
+ if total_in + total_out == 0:
42
+ return None
43
+ fig, ax = plt.subplots(figsize=(5, 5), facecolor=C_BG)
44
+ wedges, texts, autotexts = ax.pie(
45
+ [total_in, total_out],
46
+ labels=[f"Incoming ({total_in})", f"Outgoing ({total_out})"],
47
+ autopct="%1.1f%%",
48
+ startangle=90,
49
+ colors=[C_IN, C_OUT],
50
+ wedgeprops={"edgecolor": C_BG, "linewidth": 2.5},
51
+ textprops={"fontsize": 10, "fontweight": "600", "color": C_PRIMARY},
52
+ )
53
+ for t in autotexts:
54
+ t.set_fontsize(11)
55
+ t.set_fontweight("700")
56
+ t.set_color(C_BG)
57
+ ax.set_title("Directional Split", fontsize=13, fontweight="700", color=C_PRIMARY, pad=16)
58
+ total = total_in + total_out
59
+ ax.text(0, -1.35, f"Total: {total} vehicles", ha="center", fontsize=9, color=C_ACCENT, fontweight="500")
60
+ path = os.path.join(out_dir, "direction_pie.png")
61
+ _save(fig, path)
62
+ return "direction_pie.png"
63
+
64
+
65
+ def flow_histogram(flow_times, out_dir):
66
+ if not flow_times:
67
+ return None
68
+ fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG)
69
+ bins = min(30, max(5, len(set(flow_times))))
70
+ counts, edges, patches = ax.hist(flow_times, bins=bins, color=C_FLOW, alpha=0.85, edgecolor=C_BG, linewidth=0.8)
71
+ ax.yaxis.set_major_locator(MaxNLocator(integer=True))
72
+ _style(ax, "Traffic Flow Over Time", "Time (seconds)", "Vehicles Crossed")
73
+ peak_idx = int(np.argmax(counts))
74
+ peak_time = (edges[peak_idx] + edges[peak_idx + 1]) / 2
75
+ ax.text(0.98, 0.95, f"Peak: {int(counts[peak_idx])} vehicles at {peak_time:.1f}s",
76
+ transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
77
+ bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
78
+ path = os.path.join(out_dir, "flow_over_time.png")
79
+ _save(fig, path)
80
+ return "flow_over_time.png"
81
+
82
+
83
+ def congestion_chart(congestion, out_dir):
84
+ if not congestion:
85
+ return None
86
+ fig, ax = plt.subplots(figsize=(10, 4), facecolor=C_BG)
87
+ x = range(len(congestion))
88
+ ax.fill_between(x, congestion, alpha=0.08, color=C_CONG)
89
+ ax.plot(x, congestion, alpha=0.25, color=C_CONG, linewidth=0.5)
90
+ win = min(30, max(3, len(congestion) // 10))
91
+ smooth = np.convolve(congestion, np.ones(win) / win, mode="same")
92
+ ax.plot(x, smooth, linewidth=2, color=C_CONG)
93
+ ax.yaxis.set_major_locator(MaxNLocator(integer=True))
94
+ _style(ax, "Congestion Index", "Frame", "Active Vehicles")
95
+ avg = np.mean(congestion)
96
+ peak = max(congestion)
97
+ ax.axhline(avg, color=C_ACCENT, linewidth=0.8, linestyle="--", alpha=0.5)
98
+ ax.text(0.98, 0.95, f"Peak: {peak} | Avg: {avg:.1f}",
99
+ transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
100
+ bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
101
+ path = os.path.join(out_dir, "congestion_index.png")
102
+ _save(fig, path)
103
+ return "congestion_index.png"
104
+
105
+
106
+ def class_dominance(class_in, class_out, model_classes, out_dir):
107
+ totals = {}
108
+ for k in set(list(class_in.keys()) + list(class_out.keys())):
109
+ totals[k] = class_in.get(k, 0) + class_out.get(k, 0)
110
+ if not totals or sum(totals.values()) == 0:
111
+ return None
112
+ sorted_items = sorted(totals.items(), key=lambda x: x[1], reverse=True)
113
+ classes = [model_classes.get(int(i), f"cls_{i}") for i, _ in sorted_items]
114
+ values = [v for _, v in sorted_items]
115
+
116
+ fig, ax = plt.subplots(figsize=(10, 4.5), facecolor=C_BG)
117
+ n = len(classes)
118
+ bar_width = min(0.45, max(0.15, 0.6 / max(n, 1)))
119
+ bars = ax.bar(range(n), values, width=bar_width, color=C_BAR, edgecolor=C_BG, linewidth=0.5, zorder=3)
120
+ ax.set_xticks(range(n))
121
+ ax.set_xticklabels(classes, rotation=35, ha="right", fontsize=9, fontweight="500")
122
+ ax.yaxis.set_major_locator(MaxNLocator(integer=True))
123
+ for bar, v in zip(bars, values):
124
+ ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.15,
125
+ str(v), ha="center", va="bottom", fontsize=9, fontweight="700", color=C_PRIMARY)
126
+ _style(ax, "Class Dominance", "", "Vehicle Count")
127
+ total = sum(values)
128
+ ax.text(0.98, 0.95, f"Total: {total} vehicles | {n} classes detected",
129
+ transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
130
+ bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
131
+ path = os.path.join(out_dir, "class_dominance.png")
132
+ _save(fig, path)
133
+ return "class_dominance.png"
134
+
135
+
136
+ def confidence_dist(conf_scores, out_dir):
137
+ if not conf_scores:
138
+ return None
139
+ fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG)
140
+ ax.hist(conf_scores, bins=30, color=C_CONF, alpha=0.85, edgecolor=C_BG, linewidth=0.8)
141
+ ax.yaxis.set_major_locator(MaxNLocator(integer=True))
142
+ _style(ax, "Detection Confidence Distribution", "Confidence Score", "Detections")
143
+ mean_c = np.mean(conf_scores)
144
+ median_c = np.median(conf_scores)
145
+ ax.axvline(mean_c, color=C_PRIMARY, linewidth=1, linestyle="--", alpha=0.6)
146
+ ax.text(0.98, 0.95, f"Mean: {mean_c:.3f} | Median: {median_c:.3f} | N={len(conf_scores)}",
147
+ transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
148
+ bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
149
+ path = os.path.join(out_dir, "confidence_dist.png")
150
+ _save(fig, path)
151
+ return "confidence_dist.png"
152
+
153
+
154
+ def generate_all(data, model_classes, out_dir):
155
+ os.makedirs(out_dir, exist_ok=True)
156
+
157
+ plt.rcParams.update({
158
+ "font.family": "sans-serif",
159
+ "font.sans-serif": ["DejaVu Sans", "Arial", "Helvetica"],
160
+ "axes.unicode_minus": False,
161
+ })
162
+
163
+ total_in = sum(data["class_in"].values())
164
+ total_out = sum(data["class_out"].values())
165
+
166
+ files = []
167
+ for fn in [
168
+ lambda: direction_pie(total_in, total_out, out_dir),
169
+ lambda: flow_histogram(data.get("flow_times", []), out_dir),
170
+ lambda: congestion_chart(data.get("congestion", []), out_dir),
171
+ lambda: class_dominance(data["class_in"], data["class_out"], model_classes, out_dir),
172
+ lambda: confidence_dist(data.get("conf_scores", []), out_dir),
173
+ ]:
174
+ name = fn()
175
+ if name:
176
+ files.append(name)
177
+ return files
frontend/initial.html CHANGED
@@ -106,55 +106,55 @@
106
  <!-- STEP: Module Select -->
107
  <div id="step-modules" class="w-full flex flex-col fade-in justify-center">
108
  <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">Select AI Module</h2>
109
- <p class="text-[13px] font-medium text-slate-400 mb-6 text-center">Choose an intelligence pipeline for
110
  your media
111
  stream.</p>
112
 
113
  <div class="grid grid-cols-2 gap-4">
114
  <div onclick="showStep('upload')"
115
- class="group relative bg-white border-2 border-slate-900 rounded-[1.5rem] p-6 cursor-pointer hover:shadow-2xl hover:-translate-y-1 transition-all duration-300">
116
  <div
117
  class="absolute top-4 right-4 bg-slate-900 text-white text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
118
  Active</div>
119
- <i class="fa-solid fa-car-side text-3xl text-slate-900 mb-4 block"></i>
120
  <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight">Traffic <br>Analytics</h3>
121
- <p class="text-[10px] text-slate-500 font-medium">Detect, track, and analyze vehicles in
122
  real-world environments using state-of-the-art vision models.</p>
123
  </div>
124
  <div
125
- class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed">
126
  <div
127
  class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
128
  Coming Soon</div>
129
- <i class="fa-solid fa-layer-group text-3xl text-slate-300 mb-4 block"></i>
130
  <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Semantic
131
  <br>Segmentation
132
  </h3>
133
- <p class="text-[10px] text-slate-400 font-medium">Pixel-perfect instance segmentation for
134
  complex spatial scene understanding.</p>
135
  </div>
136
  <div
137
- class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed">
138
  <div
139
  class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
140
  Coming Soon</div>
141
- <i class="fa-solid fa-tags text-3xl text-slate-300 mb-4 block"></i>
142
  <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Image
143
  <br>Classification
144
  </h3>
145
- <p class="text-[10px] text-slate-400 font-medium">High-speed categorical labeling for vast and
146
  diverse image datasets.</p>
147
  </div>
148
  <div
149
- class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed">
150
  <div
151
  class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
152
  Coming Soon</div>
153
- <i class="fa-solid fa-expand text-3xl text-slate-300 mb-4 block"></i>
154
  <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Custom
155
  <br>Detection
156
  </h3>
157
- <p class="text-[10px] text-slate-400 font-medium">Deploy proprietary neural networks for highly
158
  specialized edge inference.</p>
159
  </div>
160
  </div>
@@ -168,7 +168,7 @@
168
  </button>
169
  <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">Initialize Media Source
170
  </h2>
171
- <p class="text-[13px] font-medium text-slate-400 mb-8 text-center">Provide the target video footage to
172
  configure the
173
  Traffic Analytics pipeline.</p>
174
 
 
106
  <!-- STEP: Module Select -->
107
  <div id="step-modules" class="w-full flex flex-col fade-in justify-center">
108
  <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">Select AI Module</h2>
109
+ <p class="text-[13px] font-montserrat font-medium text-slate-400 mb-6 text-center">Choose an intelligence pipeline for
110
  your media
111
  stream.</p>
112
 
113
  <div class="grid grid-cols-2 gap-4">
114
  <div onclick="showStep('upload')"
115
+ class="group relative bg-white border-2 border-slate-900 rounded-[1.5rem] p-6 cursor-pointer hover:shadow-2xl hover:-translate-y-1 transition-all duration-300 text-center">
116
  <div
117
  class="absolute top-4 right-4 bg-slate-900 text-white text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
118
  Active</div>
119
+ <i class="fa-solid fa-car-side text-3xl text-slate-900 mb-4 block mx-auto"></i>
120
  <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight">Traffic <br>Analytics</h3>
121
+ <p class="text-[10px] text-slate-500 font-montserrat font-medium">Detect, track, and analyze vehicles in
122
  real-world environments using state-of-the-art vision models.</p>
123
  </div>
124
  <div
125
+ class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed text-center">
126
  <div
127
  class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
128
  Coming Soon</div>
129
+ <i class="fa-solid fa-layer-group text-3xl text-slate-300 mb-4 block mx-auto"></i>
130
  <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Semantic
131
  <br>Segmentation
132
  </h3>
133
+ <p class="text-[10px] text-slate-400 font-montserrat font-medium">Pixel-perfect instance segmentation for
134
  complex spatial scene understanding.</p>
135
  </div>
136
  <div
137
+ class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed text-center">
138
  <div
139
  class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
140
  Coming Soon</div>
141
+ <i class="fa-solid fa-tags text-3xl text-slate-300 mb-4 block mx-auto"></i>
142
  <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Image
143
  <br>Classification
144
  </h3>
145
+ <p class="text-[10px] text-slate-400 font-montserrat font-medium">High-speed categorical labeling for vast and
146
  diverse image datasets.</p>
147
  </div>
148
  <div
149
+ class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed text-center">
150
  <div
151
  class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
152
  Coming Soon</div>
153
+ <i class="fa-solid fa-expand text-3xl text-slate-300 mb-4 block mx-auto"></i>
154
  <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Custom
155
  <br>Detection
156
  </h3>
157
+ <p class="text-[10px] text-slate-400 font-montserrat font-medium">Deploy proprietary neural networks for highly
158
  specialized edge inference.</p>
159
  </div>
160
  </div>
 
168
  </button>
169
  <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">Initialize Media Source
170
  </h2>
171
+ <p class="text-[13px] font-montserrat font-medium text-slate-400 mb-8 text-center">Provide the target video footage to
172
  configure the
173
  Traffic Analytics pipeline.</p>
174
 
frontend/vehicles.html CHANGED
@@ -104,11 +104,9 @@
104
  class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
105
  <i class="fa-solid fa-microchip w-6"></i> <span class="font-medium">Run Details</span>
106
  </a>
107
- <a
108
- class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
109
- <div class="flex items-center"><i class="fa-solid fa-file-lines w-6"></i> <span
110
- class="font-medium">Reports</span></div>
111
- <i class="fa-solid fa-lock text-[10px]"></i>
112
  </a>
113
  <a
114
  class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
@@ -297,6 +295,15 @@
297
  </div>
298
  </div>
299
 
 
 
 
 
 
 
 
 
 
300
  </main>
301
 
302
  <script>
@@ -321,17 +328,18 @@
321
  function switchTab(tab) {
322
  document.getElementById('tab-overview').classList.toggle('hidden', tab !== 'overview');
323
  document.getElementById('tab-run-details').classList.toggle('hidden', tab !== 'run-details');
 
324
 
325
  const navO = document.getElementById('nav-overview');
326
  const navR = document.getElementById('nav-run-details');
 
327
 
328
- if (tab === 'overview') {
329
- navO.className = 'flex items-center px-4 py-2.5 bg-slate-900 text-white rounded-lg shadow-md transition cursor-pointer';
330
- navR.className = 'flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer';
331
- } else {
332
- navR.className = 'flex items-center px-4 py-2.5 bg-slate-900 text-white rounded-lg shadow-md transition cursor-pointer';
333
- navO.className = 'flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer';
334
- }
335
  }
336
 
337
  // =========== Run Details helpers ===========
@@ -389,7 +397,7 @@
389
  document.getElementById('panel-model').innerHTML =
390
  `<div class="flex justify-between items-center border-b border-slate-50 pb-2">
391
  <span class="text-xs font-medium text-slate-500 mono-font">model</span>
392
- <a href="https://huggingface.co/Perception365/VehicleNet-Y26s" target="_blank" class="text-sm font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded border border-blue-100 mono-font hover:bg-blue-100 transition">Perception365/VehicleNet-Y26s <i class="fa-solid fa-arrow-up-right-from-square text-[9px] ml-1"></i></a>
393
  </div>` +
394
  detailRow('task', 'detect') +
395
  detailRow('format', 'OpenVINO') +
@@ -561,6 +569,8 @@
561
  detailRow('processing_time', d.processing_time + ' sec') +
562
  infoRow('actual_fps', d.actual_fps, 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
563
  infoRow('speed_vs_realtime', d.speed_vs_realtime + 'x', 'Processing speed relative to video playback rate.');
 
 
564
  return;
565
  }
566
 
@@ -586,6 +596,48 @@
586
  };
587
  }
588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  init();
590
  </script>
591
  </body>
 
104
  class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
105
  <i class="fa-solid fa-microchip w-6"></i> <span class="font-medium">Run Details</span>
106
  </a>
107
+ <a onclick="switchTab('reports')" id="nav-reports"
108
+ class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
109
+ <i class="fa-solid fa-file-lines w-6"></i> <span class="font-medium">Reports</span>
 
 
110
  </a>
111
  <a
112
  class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
 
295
  </div>
296
  </div>
297
 
298
+ <!-- TAB: Reports -->
299
+ <div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
300
+ <div id="reports-pending" class="flex flex-col items-center justify-center h-full">
301
+ <i class="fa-solid fa-hourglass-half text-4xl text-slate-300 mb-4"></i>
302
+ <p class="text-sm font-medium text-slate-400">Reports will be available after processing completes.</p>
303
+ </div>
304
+ <div id="reports-grid" class="hidden grid grid-cols-2 xl:grid-cols-3 gap-4"></div>
305
+ </div>
306
+
307
  </main>
308
 
309
  <script>
 
328
  function switchTab(tab) {
329
  document.getElementById('tab-overview').classList.toggle('hidden', tab !== 'overview');
330
  document.getElementById('tab-run-details').classList.toggle('hidden', tab !== 'run-details');
331
+ document.getElementById('tab-reports').classList.toggle('hidden', tab !== 'reports');
332
 
333
  const navO = document.getElementById('nav-overview');
334
  const navR = document.getElementById('nav-run-details');
335
+ const navRp = document.getElementById('nav-reports');
336
 
337
+ const active = 'flex items-center px-4 py-2.5 bg-slate-900 text-white rounded-lg shadow-md transition cursor-pointer';
338
+ const idle = 'flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer';
339
+
340
+ navO.className = tab === 'overview' ? active : idle;
341
+ navR.className = tab === 'run-details' ? active : idle;
342
+ navRp.className = tab === 'reports' ? active : idle;
 
343
  }
344
 
345
  // =========== Run Details helpers ===========
 
397
  document.getElementById('panel-model').innerHTML =
398
  `<div class="flex justify-between items-center border-b border-slate-50 pb-2">
399
  <span class="text-xs font-medium text-slate-500 mono-font">model</span>
400
+ <a href="https://huggingface.co/Perception365/VehicleNet-Y26s" target="_blank" class="text-sm font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded border border-blue-100 mono-font hover:bg-blue-100 transition">Perception365/VehicleNet-Y26s</a>
401
  </div>` +
402
  detailRow('task', 'detect') +
403
  detailRow('format', 'OpenVINO') +
 
569
  detailRow('processing_time', d.processing_time + ' sec') +
570
  infoRow('actual_fps', d.actual_fps, 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
571
  infoRow('speed_vs_realtime', d.speed_vs_realtime + 'x', 'Processing speed relative to video playback rate.');
572
+
573
+ if (d.video_id) loadReports(d.video_id);
574
  return;
575
  }
576
 
 
596
  };
597
  }
598
 
599
+ const REPORT_LABELS = {
600
+ 'direction_pie.png': { title: 'Directional Split', desc: 'Incoming vs outgoing vehicle ratio' },
601
+ 'flow_over_time.png': { title: 'Traffic Flow', desc: 'Vehicles crossing the line over time' },
602
+ 'congestion_index.png': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
603
+ 'class_dominance.png': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
604
+ 'confidence_dist.png': { title: 'Confidence Distribution', desc: 'Detection confidence histogram' },
605
+ };
606
+
607
+ async function loadReports(videoId) {
608
+ const res = await fetch(`/reports/${videoId}`, { method: 'POST' });
609
+ const data = await res.json();
610
+ if (!data.files || !data.files.length) return;
611
+
612
+ document.getElementById('reports-pending').classList.add('hidden');
613
+ const grid = document.getElementById('reports-grid');
614
+ grid.classList.remove('hidden');
615
+ grid.innerHTML = '';
616
+
617
+ data.files.forEach(name => {
618
+ const info = REPORT_LABELS[name] || { title: name, desc: '' };
619
+ const url = `/reports/${videoId}/${name}`;
620
+ const card = document.createElement('div');
621
+ card.className = 'bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden';
622
+ card.innerHTML = `
623
+ <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
624
+ <div>
625
+ <h3 class="font-bold text-slate-800 text-sm">${info.title}</h3>
626
+ <p class="text-[10px] text-slate-400 mt-0.5">${info.desc}</p>
627
+ </div>
628
+ <a href="${url}" download="${name}"
629
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-900 text-white text-[10px] font-bold rounded-lg hover:bg-slate-700 transition uppercase tracking-wider">
630
+ <i class="fa-solid fa-download text-[9px]"></i> Download
631
+ </a>
632
+ </div>
633
+ <div class="p-4 flex items-center justify-center bg-slate-50/30">
634
+ <img src="${url}" alt="${info.title}" class="max-w-full max-h-[320px] object-contain rounded">
635
+ </div>
636
+ `;
637
+ grid.appendChild(card);
638
+ });
639
+ }
640
+
641
  init();
642
  </script>
643
  </body>
requirements.txt CHANGED
@@ -2,6 +2,7 @@ fastapi==0.104.1
2
  uvicorn[standard]==0.24.0
3
  python-multipart==0.0.6
4
  opencv-python-headless==4.8.1.78
 
5
  ultralytics>=8.3.0
6
  numpy==1.24.3
7
  python-dotenv==1.0.0
 
2
  uvicorn[standard]==0.24.0
3
  python-multipart==0.0.6
4
  opencv-python-headless==4.8.1.78
5
+ matplotlib
6
  ultralytics>=8.3.0
7
  numpy==1.24.3
8
  python-dotenv==1.0.0