Spaces:
Running
Running
Raj Bhalerao commited on
Commit ·
2a7f65a
1
Parent(s): c1bf807
Reports, improvements, fixes..
Browse files- README.md +6 -2
- backend/engine.py +4 -0
- backend/server.py +24 -0
- backend/visualize.py +177 -0
- frontend/initial.html +14 -14
- frontend/vehicles.html +65 -13
- requirements.txt +1 -0
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 (
|
| 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 -
|
| 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
|
| 109 |
-
<
|
| 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 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 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
|
| 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
|