Spaces:
Running
Running
Raj Bhalerao commited on
Commit ·
3e04ea5
1
Parent(s): 2a7f65a
Release: Optimized Processing Engine
Browse files- Dockerfile +1 -0
- backend/engine.py +57 -2
- backend/server.py +22 -3
- backend/visualize.py +32 -23
- frontend/initial.html +25 -126
- frontend/settings.html +426 -0
- frontend/vehicles.html +673 -144
Dockerfile
CHANGED
|
@@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y \
|
|
| 6 |
libgl1 \
|
| 7 |
libglib2.0-0 \
|
| 8 |
curl \
|
|
|
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
| 11 |
COPY requirements.txt .
|
|
|
|
| 6 |
libgl1 \
|
| 7 |
libglib2.0-0 \
|
| 8 |
curl \
|
| 9 |
+
ffmpeg \
|
| 10 |
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
|
| 12 |
COPY requirements.txt .
|
backend/engine.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
|
|
| 1 |
import time
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
import cv2
|
| 4 |
from collections import defaultdict
|
|
@@ -17,19 +19,53 @@ def _point_to_segment_dist(px, py, ax, ay, bx, by):
|
|
| 17 |
return np.linalg.norm(P - (A + t * AB))
|
| 18 |
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
"""
|
| 22 |
Runs YOLO tracking on video. Calls on_frame(update_dict) after each processed frame.
|
| 23 |
line: [[x1,y1], [x2,y2]]
|
|
|
|
| 24 |
"""
|
| 25 |
cap = cv2.VideoCapture(video_path)
|
| 26 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 27 |
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
|
|
|
|
|
| 28 |
cap.release()
|
| 29 |
|
| 30 |
stride = config["detect_stride"]
|
| 31 |
total_iters = total // stride
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
prev_side = {}
|
| 34 |
counted_ids = set()
|
| 35 |
class_in = defaultdict(int)
|
|
@@ -57,6 +93,8 @@ def run(model, video_path, line, config, on_frame):
|
|
| 57 |
|
| 58 |
for frame_idx, r in enumerate(results):
|
| 59 |
active = 0
|
|
|
|
|
|
|
| 60 |
|
| 61 |
if r.boxes.id is not None:
|
| 62 |
ids = r.boxes.id.cpu().numpy()
|
|
@@ -67,6 +105,9 @@ def run(model, video_path, line, config, on_frame):
|
|
| 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)
|
| 72 |
cy = int((box[1] + box[3]) / 2)
|
|
@@ -89,6 +130,12 @@ def run(model, video_path, line, config, on_frame):
|
|
| 89 |
|
| 90 |
prev_side[obj_id] = current
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
congestion.append(active)
|
| 93 |
|
| 94 |
elapsed = time.time() - start
|
|
@@ -108,11 +155,14 @@ def run(model, video_path, line, config, on_frame):
|
|
| 108 |
|
| 109 |
on_frame(update)
|
| 110 |
|
|
|
|
|
|
|
|
|
|
| 111 |
processing_time = round(time.time() - start, 2)
|
| 112 |
actual_fps = round(config["frames"] / processing_time, 2) if processing_time > 0 else 0
|
| 113 |
speed_vs_rt = round(actual_fps / fps, 2) if fps > 0 else 0
|
| 114 |
|
| 115 |
-
|
| 116 |
"class_in": dict(class_in),
|
| 117 |
"class_out": dict(class_out),
|
| 118 |
"congestion": congestion,
|
|
@@ -122,3 +172,8 @@ def run(model, video_path, line, config, on_frame):
|
|
| 122 |
"actual_fps": actual_fps,
|
| 123 |
"speed_vs_realtime": speed_vs_rt,
|
| 124 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
import time
|
| 3 |
+
import tempfile
|
| 4 |
import numpy as np
|
| 5 |
import cv2
|
| 6 |
from collections import defaultdict
|
|
|
|
| 19 |
return np.linalg.norm(P - (A + t * AB))
|
| 20 |
|
| 21 |
|
| 22 |
+
# Lightweight drawing colors (BGR for OpenCV)
|
| 23 |
+
_CLR_BOX = (230, 180, 50) # teal-ish
|
| 24 |
+
_CLR_LINE = (80, 220, 100) # green
|
| 25 |
+
_CLR_TEXT_BG = (30, 30, 30) # dark bg for text
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _draw_annotations(frame, boxes, ids, line_pts):
|
| 29 |
+
"""Draw bounding boxes, track IDs, and counting line on frame in-place."""
|
| 30 |
+
# Counting line
|
| 31 |
+
cv2.line(frame, tuple(line_pts[0]), tuple(line_pts[1]), _CLR_LINE, 3, cv2.LINE_AA)
|
| 32 |
+
|
| 33 |
+
if boxes is not None and ids is not None:
|
| 34 |
+
for box, obj_id in zip(boxes, ids):
|
| 35 |
+
x1, y1, x2, y2 = map(int, box)
|
| 36 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), _CLR_BOX, 2)
|
| 37 |
+
label = f"ID:{int(obj_id)}"
|
| 38 |
+
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)
|
| 39 |
+
cv2.rectangle(frame, (x1, y1 - th - 6), (x1 + tw + 6, y1), _CLR_TEXT_BG, -1)
|
| 40 |
+
cv2.putText(frame, label, (x1 + 3, y1 - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def run(model, video_path, line, config, on_frame, save_annotated=False):
|
| 44 |
"""
|
| 45 |
Runs YOLO tracking on video. Calls on_frame(update_dict) after each processed frame.
|
| 46 |
line: [[x1,y1], [x2,y2]]
|
| 47 |
+
save_annotated: if True, writes annotated MP4 with boxes + IDs + counting line
|
| 48 |
"""
|
| 49 |
cap = cv2.VideoCapture(video_path)
|
| 50 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 51 |
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 52 |
+
out_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 53 |
+
out_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 54 |
cap.release()
|
| 55 |
|
| 56 |
stride = config["detect_stride"]
|
| 57 |
total_iters = total // stride
|
| 58 |
|
| 59 |
+
# Annotated video writer (temp directory — auto-cleaned on container shutdown)
|
| 60 |
+
annotated_path = None
|
| 61 |
+
writer = None
|
| 62 |
+
if save_annotated:
|
| 63 |
+
annotated_dir = os.path.join(tempfile.gettempdir(), "funky_reports")
|
| 64 |
+
os.makedirs(annotated_dir, exist_ok=True)
|
| 65 |
+
annotated_path = os.path.join(annotated_dir, "annotated.mp4")
|
| 66 |
+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
| 67 |
+
writer = cv2.VideoWriter(annotated_path, fourcc, fps / stride, (out_w, out_h))
|
| 68 |
+
|
| 69 |
prev_side = {}
|
| 70 |
counted_ids = set()
|
| 71 |
class_in = defaultdict(int)
|
|
|
|
| 93 |
|
| 94 |
for frame_idx, r in enumerate(results):
|
| 95 |
active = 0
|
| 96 |
+
cur_boxes = None
|
| 97 |
+
cur_ids = None
|
| 98 |
|
| 99 |
if r.boxes.id is not None:
|
| 100 |
ids = r.boxes.id.cpu().numpy()
|
|
|
|
| 105 |
confs = r.boxes.conf.cpu().numpy().tolist()
|
| 106 |
conf_scores.extend(confs)
|
| 107 |
|
| 108 |
+
cur_boxes = xyxy
|
| 109 |
+
cur_ids = ids
|
| 110 |
+
|
| 111 |
for obj_id, c, box in zip(ids, cls, xyxy):
|
| 112 |
cx = int((box[0] + box[2]) / 2)
|
| 113 |
cy = int((box[1] + box[3]) / 2)
|
|
|
|
| 130 |
|
| 131 |
prev_side[obj_id] = current
|
| 132 |
|
| 133 |
+
# Write annotated frame (only for frames we already process)
|
| 134 |
+
if writer is not None:
|
| 135 |
+
frame = r.orig_img.copy()
|
| 136 |
+
_draw_annotations(frame, cur_boxes, cur_ids, [a, b])
|
| 137 |
+
writer.write(frame)
|
| 138 |
+
|
| 139 |
congestion.append(active)
|
| 140 |
|
| 141 |
elapsed = time.time() - start
|
|
|
|
| 155 |
|
| 156 |
on_frame(update)
|
| 157 |
|
| 158 |
+
if writer is not None:
|
| 159 |
+
writer.release()
|
| 160 |
+
|
| 161 |
processing_time = round(time.time() - start, 2)
|
| 162 |
actual_fps = round(config["frames"] / processing_time, 2) if processing_time > 0 else 0
|
| 163 |
speed_vs_rt = round(actual_fps / fps, 2) if fps > 0 else 0
|
| 164 |
|
| 165 |
+
result = {
|
| 166 |
"class_in": dict(class_in),
|
| 167 |
"class_out": dict(class_out),
|
| 168 |
"congestion": congestion,
|
|
|
|
| 172 |
"actual_fps": actual_fps,
|
| 173 |
"speed_vs_realtime": speed_vs_rt,
|
| 174 |
}
|
| 175 |
+
|
| 176 |
+
if annotated_path and os.path.exists(annotated_path):
|
| 177 |
+
result["annotated_video"] = annotated_path
|
| 178 |
+
|
| 179 |
+
return result
|
backend/server.py
CHANGED
|
@@ -79,7 +79,16 @@ def generate_reports(video_id: str):
|
|
| 79 |
if not data:
|
| 80 |
return {"error": "no results", "files": []}
|
| 81 |
out_dir = str(REPORT_DIR / video_id)
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
return {"files": files}
|
| 84 |
|
| 85 |
|
|
@@ -88,7 +97,12 @@ 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
|
| 94 |
@app.websocket("/ws/run")
|
|
@@ -99,6 +113,8 @@ async def ws_run(ws: WebSocket):
|
|
| 99 |
video_id = data["video_id"]
|
| 100 |
line = data["line"]
|
| 101 |
cfg = data["config"]
|
|
|
|
|
|
|
| 102 |
|
| 103 |
path = videos.get(video_id)
|
| 104 |
|
|
@@ -109,7 +125,9 @@ async def ws_run(ws: WebSocket):
|
|
| 109 |
def on_frame(update):
|
| 110 |
loop.call_soon_threadsafe(queue.put_nowait, update)
|
| 111 |
|
| 112 |
-
task = loop.run_in_executor(
|
|
|
|
|
|
|
| 113 |
|
| 114 |
try:
|
| 115 |
while True:
|
|
@@ -124,6 +142,7 @@ async def ws_run(ws: WebSocket):
|
|
| 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,
|
|
|
|
| 79 |
if not data:
|
| 80 |
return {"error": "no results", "files": []}
|
| 81 |
out_dir = str(REPORT_DIR / video_id)
|
| 82 |
+
report_format = data.get("report_format", "png")
|
| 83 |
+
files = generate_all(data, MODEL_CLASSES, out_dir, report_format)
|
| 84 |
+
# Include annotated video if it exists
|
| 85 |
+
annotated_src = data.get("annotated_video")
|
| 86 |
+
if annotated_src and os.path.exists(annotated_src):
|
| 87 |
+
import shutil
|
| 88 |
+
dest = os.path.join(out_dir, "annotated.mp4")
|
| 89 |
+
if not os.path.exists(dest):
|
| 90 |
+
shutil.copy2(annotated_src, dest)
|
| 91 |
+
files.append("annotated.mp4")
|
| 92 |
return {"files": files}
|
| 93 |
|
| 94 |
|
|
|
|
| 97 |
path = REPORT_DIR / video_id / name
|
| 98 |
if not path.exists():
|
| 99 |
return Response(status_code=404)
|
| 100 |
+
media = "image/png"
|
| 101 |
+
if name.endswith(".pdf"):
|
| 102 |
+
media = "application/pdf"
|
| 103 |
+
elif name.endswith(".mp4"):
|
| 104 |
+
media = "video/mp4"
|
| 105 |
+
return FileResponse(str(path), media_type=media)
|
| 106 |
|
| 107 |
|
| 108 |
@app.websocket("/ws/run")
|
|
|
|
| 113 |
video_id = data["video_id"]
|
| 114 |
line = data["line"]
|
| 115 |
cfg = data["config"]
|
| 116 |
+
save_annotated = data.get("annotated_video", False)
|
| 117 |
+
report_format = data.get("report_format", "png")
|
| 118 |
|
| 119 |
path = videos.get(video_id)
|
| 120 |
|
|
|
|
| 125 |
def on_frame(update):
|
| 126 |
loop.call_soon_threadsafe(queue.put_nowait, update)
|
| 127 |
|
| 128 |
+
task = loop.run_in_executor(
|
| 129 |
+
None, run, model, path, line, cfg, on_frame, save_annotated
|
| 130 |
+
)
|
| 131 |
|
| 132 |
try:
|
| 133 |
while True:
|
|
|
|
| 142 |
await asyncio.sleep(0.05)
|
| 143 |
|
| 144 |
result = task.result()
|
| 145 |
+
result["report_format"] = report_format
|
| 146 |
run_results[video_id] = result
|
| 147 |
await ws.send_text(json.dumps({
|
| 148 |
"done": True,
|
backend/visualize.py
CHANGED
|
@@ -32,12 +32,14 @@ def _style(ax, title, xlabel="", ylabel=""):
|
|
| 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)
|
|
@@ -47,7 +49,7 @@ def direction_pie(total_in, total_out, out_dir):
|
|
| 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:
|
|
@@ -58,11 +60,12 @@ def direction_pie(total_in, total_out, out_dir):
|
|
| 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 |
-
|
|
|
|
| 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)
|
|
@@ -76,11 +79,12 @@ def flow_histogram(flow_times, out_dir):
|
|
| 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 |
-
|
|
|
|
| 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)
|
|
@@ -99,11 +103,12 @@ def congestion_chart(congestion, out_dir):
|
|
| 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 |
-
|
|
|
|
| 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)
|
|
@@ -129,11 +134,12 @@ def class_dominance(class_in, class_out, model_classes, out_dir):
|
|
| 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 |
-
|
|
|
|
| 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)
|
|
@@ -147,11 +153,12 @@ def confidence_dist(conf_scores, out_dir):
|
|
| 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 |
-
|
|
|
|
| 152 |
|
| 153 |
|
| 154 |
-
def generate_all(data, model_classes, out_dir):
|
| 155 |
os.makedirs(out_dir, exist_ok=True)
|
| 156 |
|
| 157 |
plt.rcParams.update({
|
|
@@ -163,13 +170,15 @@ def generate_all(data, model_classes, out_dir):
|
|
| 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:
|
|
|
|
| 32 |
ax.set_axisbelow(True)
|
| 33 |
|
| 34 |
|
| 35 |
+
def _save(fig, path, fmt="png"):
|
| 36 |
+
if fmt == "pdf":
|
| 37 |
+
path = path.rsplit(".", 1)[0] + ".pdf"
|
| 38 |
fig.savefig(path, dpi=200, bbox_inches="tight", facecolor=C_BG, edgecolor="none")
|
| 39 |
plt.close(fig)
|
| 40 |
|
| 41 |
|
| 42 |
+
def direction_pie(total_in, total_out, out_dir, fmt="png"):
|
| 43 |
if total_in + total_out == 0:
|
| 44 |
return None
|
| 45 |
fig, ax = plt.subplots(figsize=(5, 5), facecolor=C_BG)
|
|
|
|
| 49 |
autopct="%1.1f%%",
|
| 50 |
startangle=90,
|
| 51 |
colors=[C_IN, C_OUT],
|
| 52 |
+
wedgeprops={"edgecolor": C_BG, "linewidth": 2.5 if (total_in > 0 and total_out > 0) else 0},
|
| 53 |
textprops={"fontsize": 10, "fontweight": "600", "color": C_PRIMARY},
|
| 54 |
)
|
| 55 |
for t in autotexts:
|
|
|
|
| 60 |
total = total_in + total_out
|
| 61 |
ax.text(0, -1.35, f"Total: {total} vehicles", ha="center", fontsize=9, color=C_ACCENT, fontweight="500")
|
| 62 |
path = os.path.join(out_dir, "direction_pie.png")
|
| 63 |
+
_save(fig, path, fmt)
|
| 64 |
+
ext = fmt if fmt == "pdf" else "png"
|
| 65 |
+
return f"direction_pie.{ext}"
|
| 66 |
|
| 67 |
|
| 68 |
+
def flow_histogram(flow_times, out_dir, fmt="png"):
|
| 69 |
if not flow_times:
|
| 70 |
return None
|
| 71 |
fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG)
|
|
|
|
| 79 |
transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
|
| 80 |
bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
|
| 81 |
path = os.path.join(out_dir, "flow_over_time.png")
|
| 82 |
+
_save(fig, path, fmt)
|
| 83 |
+
ext = fmt if fmt == "pdf" else "png"
|
| 84 |
+
return f"flow_over_time.{ext}"
|
| 85 |
|
| 86 |
|
| 87 |
+
def congestion_chart(congestion, out_dir, fmt="png"):
|
| 88 |
if not congestion:
|
| 89 |
return None
|
| 90 |
fig, ax = plt.subplots(figsize=(10, 4), facecolor=C_BG)
|
|
|
|
| 103 |
transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
|
| 104 |
bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
|
| 105 |
path = os.path.join(out_dir, "congestion_index.png")
|
| 106 |
+
_save(fig, path, fmt)
|
| 107 |
+
ext = fmt if fmt == "pdf" else "png"
|
| 108 |
+
return f"congestion_index.{ext}"
|
| 109 |
|
| 110 |
|
| 111 |
+
def class_dominance(class_in, class_out, model_classes, out_dir, fmt="png"):
|
| 112 |
totals = {}
|
| 113 |
for k in set(list(class_in.keys()) + list(class_out.keys())):
|
| 114 |
totals[k] = class_in.get(k, 0) + class_out.get(k, 0)
|
|
|
|
| 134 |
transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
|
| 135 |
bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
|
| 136 |
path = os.path.join(out_dir, "class_dominance.png")
|
| 137 |
+
_save(fig, path, fmt)
|
| 138 |
+
ext = fmt if fmt == "pdf" else "png"
|
| 139 |
+
return f"class_dominance.{ext}"
|
| 140 |
|
| 141 |
|
| 142 |
+
def confidence_dist(conf_scores, out_dir, fmt="png"):
|
| 143 |
if not conf_scores:
|
| 144 |
return None
|
| 145 |
fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG)
|
|
|
|
| 153 |
transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
|
| 154 |
bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
|
| 155 |
path = os.path.join(out_dir, "confidence_dist.png")
|
| 156 |
+
_save(fig, path, fmt)
|
| 157 |
+
ext = fmt if fmt == "pdf" else "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 |
|
| 164 |
plt.rcParams.update({
|
|
|
|
| 170 |
total_in = sum(data["class_in"].values())
|
| 171 |
total_out = sum(data["class_out"].values())
|
| 172 |
|
| 173 |
+
fmt = report_format
|
| 174 |
+
|
| 175 |
files = []
|
| 176 |
for fn in [
|
| 177 |
+
lambda: direction_pie(total_in, total_out, out_dir, fmt),
|
| 178 |
+
lambda: flow_histogram(data.get("flow_times", []), out_dir, fmt),
|
| 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:
|
frontend/initial.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>UrbanFlow
|
| 8 |
<link rel="icon" type="image/svg+xml" href="uf.svg">
|
| 9 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
@@ -14,7 +14,7 @@
|
|
| 14 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
| 15 |
<style>
|
| 16 |
body {
|
| 17 |
-
font-family: '
|
| 18 |
}
|
| 19 |
|
| 20 |
.font-montserrat {
|
|
@@ -62,13 +62,6 @@
|
|
| 62 |
|
| 63 |
<header class="mt-8 flex flex-col items-center flex-shrink-0 w-full z-10">
|
| 64 |
<img src="uf_logo.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
|
| 65 |
-
<div class="flex items-center space-x-3">
|
| 66 |
-
<span class="w-12 h-[1px] bg-slate-200"></span>
|
| 67 |
-
<p class="font-montserrat font-bold tracking-[0.25em] uppercase text-[10px] text-slate-400 text-center">
|
| 68 |
-
Enterprise AI Pipeline
|
| 69 |
-
</p>
|
| 70 |
-
<span class="w-12 h-[1px] bg-slate-200"></span>
|
| 71 |
-
</div>
|
| 72 |
</header>
|
| 73 |
|
| 74 |
<main
|
|
@@ -82,7 +75,7 @@
|
|
| 82 |
<p
|
| 83 |
class="font-montserrat font-bold mb-8 text-sm uppercase tracking-[0.2em] text-slate-400 flex items-center">
|
| 84 |
<span class="bg-slate-100 text-slate-600 px-3 py-1 rounded-full text-[10px] mr-3">v1.0 CORE</span>
|
| 85 |
-
Powered by
|
| 86 |
</p>
|
| 87 |
<ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-montserrat font-medium text-slate-700">
|
| 88 |
<li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> Real-time
|
|
@@ -104,59 +97,25 @@
|
|
| 104 |
class="lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto min-h-[450px] mb-12 lg:mb-0">
|
| 105 |
|
| 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">
|
| 109 |
-
<
|
| 110 |
-
|
| 111 |
-
|
| 112 |
|
| 113 |
-
<div class="
|
| 114 |
<div onclick="showStep('upload')"
|
| 115 |
-
class="group relative bg-white border-2 border-slate-900 rounded-[
|
| 116 |
<div
|
| 117 |
-
class="absolute top-4 right-
|
| 118 |
Active</div>
|
| 119 |
-
<i class="fa-solid fa-car-side text-
|
| 120 |
-
<h3 class="font-montserrat font-bold text-
|
| 121 |
-
<p class="text-[10px] text-slate-500 font-montserrat font-medium">Detect, track,
|
|
|
|
|
|
|
| 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>
|
| 161 |
</div>
|
| 162 |
|
|
@@ -164,13 +123,14 @@
|
|
| 164 |
<div id="step-upload" class="hidden w-full flex flex-col fade-in justify-center">
|
| 165 |
<button onclick="showStep('modules')"
|
| 166 |
class="text-slate-400 hover:text-black transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
|
| 167 |
-
<i class="fa-solid fa-arrow-left mr-2"></i> Back to
|
| 168 |
</button>
|
| 169 |
-
<h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">
|
| 170 |
</h2>
|
| 171 |
-
<p class="text-[13px] font-montserrat font-medium text-slate-400 mb-8 text-center">Provide the target
|
|
|
|
| 172 |
configure the
|
| 173 |
-
|
| 174 |
|
| 175 |
<div id="dropzone"
|
| 176 |
class="border border-dashed border-slate-300 rounded-[2rem] p-12 flex flex-col items-center justify-center cursor-pointer hover:border-black hover:bg-slate-50 transition-all duration-300 group">
|
|
@@ -196,21 +156,7 @@
|
|
| 196 |
</div>
|
| 197 |
</div>
|
| 198 |
|
| 199 |
-
<!-- STEP: Config -->
|
| 200 |
-
<div id="step-config" class="hidden w-full flex flex-col fade-in justify-center">
|
| 201 |
-
<h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">System Configuration</h2>
|
| 202 |
-
<p
|
| 203 |
-
class="text-[11px] font-montserrat font-bold text-slate-400 mb-8 uppercase tracking-widest text-center">
|
| 204 |
-
Optimal values auto-configured for performance
|
| 205 |
-
</p>
|
| 206 |
-
|
| 207 |
-
<div class="space-y-4 xl:space-y-5 mb-8" id="config-fields"></div>
|
| 208 |
|
| 209 |
-
<button onclick="showStep('draw')"
|
| 210 |
-
class="w-full py-3.5 bg-black text-white rounded-full font-montserrat font-semibold hover:bg-slate-800 transition-all text-base xl:text-lg flex justify-center items-center">
|
| 211 |
-
Continue <i class="fa-solid fa-arrow-right ml-3 text-sm"></i>
|
| 212 |
-
</button>
|
| 213 |
-
</div>
|
| 214 |
|
| 215 |
<!-- STEP: Draw Line -->
|
| 216 |
<div id="step-draw" class="hidden w-full flex flex-col fade-in justify-center">
|
|
@@ -239,8 +185,8 @@
|
|
| 239 |
Reset
|
| 240 |
</button>
|
| 241 |
<button id="btn-proceed" onclick="startRun()"
|
| 242 |
-
class="w-2/3 py-3.5 bg-black text-white rounded-full font-montserrat font-
|
| 243 |
-
|
| 244 |
</button>
|
| 245 |
</div>
|
| 246 |
</div>
|
|
@@ -252,20 +198,12 @@
|
|
| 252 |
let videoId = null;
|
| 253 |
let runConfig = {};
|
| 254 |
|
| 255 |
-
const configParams = [
|
| 256 |
-
{ key: "imgsz", label: "Image Size", step: 32, min: 640, max: 1280 },
|
| 257 |
-
{ key: "conf", label: "Confidence", step: 0.01, min: 0.08, max: 1.0, decimals: 2 },
|
| 258 |
-
{ key: "iou", label: "IoU Threshold", step: 0.05, min: 0.5, max: 1.0, decimals: 2 },
|
| 259 |
-
{ key: "detect_stride", label: "Frame Stride", step: 1, min: 1, max: 10 }
|
| 260 |
-
];
|
| 261 |
-
|
| 262 |
function showStep(name) {
|
| 263 |
-
['modules', 'upload', '
|
| 264 |
document.getElementById('step-' + s).classList.add('hidden');
|
| 265 |
});
|
| 266 |
document.getElementById('step-' + name).classList.remove('hidden');
|
| 267 |
|
| 268 |
-
if (name === 'config') renderConfig();
|
| 269 |
if (name === 'draw') loadFirstFrame();
|
| 270 |
}
|
| 271 |
|
|
@@ -319,53 +257,14 @@
|
|
| 319 |
runConfig.conf = 0.12;
|
| 320 |
runConfig.iou = 0.60;
|
| 321 |
txt.innerText = 'Initialization Complete';
|
| 322 |
-
setTimeout(() => showStep('
|
| 323 |
});
|
| 324 |
};
|
| 325 |
|
| 326 |
xhr.send(form);
|
| 327 |
}
|
| 328 |
|
| 329 |
-
// Config
|
| 330 |
-
function renderConfig() {
|
| 331 |
-
const container = document.getElementById('config-fields');
|
| 332 |
-
container.innerHTML = '';
|
| 333 |
-
|
| 334 |
-
configParams.forEach(p => {
|
| 335 |
-
let val = runConfig[p.key];
|
| 336 |
-
const d = p.decimals || 0;
|
| 337 |
-
|
| 338 |
-
const row = document.createElement('div');
|
| 339 |
-
row.className = 'flex items-center justify-between border-b border-slate-100 pb-3';
|
| 340 |
-
row.innerHTML = `
|
| 341 |
-
<span class="font-montserrat font-medium text-slate-700 text-sm xl:text-base">${p.label}</span>
|
| 342 |
-
<div class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
|
| 343 |
-
<button class="text-slate-400 hover:text-black transition p-1" data-dir="-1" data-key="${p.key}">
|
| 344 |
-
<i class="fa-solid fa-chevron-left text-[10px]"></i>
|
| 345 |
-
</button>
|
| 346 |
-
<span class="font-mono font-bold w-12 text-center text-slate-900 text-base" id="cfg-${p.key}">${d ? val.toFixed(d) : val}</span>
|
| 347 |
-
<button class="text-slate-400 hover:text-black transition p-1" data-dir="1" data-key="${p.key}">
|
| 348 |
-
<i class="fa-solid fa-chevron-right text-[10px]"></i>
|
| 349 |
-
</button>
|
| 350 |
-
</div>
|
| 351 |
-
`;
|
| 352 |
-
container.appendChild(row);
|
| 353 |
-
});
|
| 354 |
|
| 355 |
-
container.querySelectorAll('button').forEach(btn => {
|
| 356 |
-
btn.addEventListener('click', () => {
|
| 357 |
-
const key = btn.dataset.key;
|
| 358 |
-
const dir = parseInt(btn.dataset.dir);
|
| 359 |
-
const param = configParams.find(p => p.key === key);
|
| 360 |
-
let val = runConfig[key] + dir * param.step;
|
| 361 |
-
val = Math.max(param.min, Math.min(param.max, val));
|
| 362 |
-
val = param.decimals ? parseFloat(val.toFixed(param.decimals)) : Math.round(val);
|
| 363 |
-
runConfig[key] = val;
|
| 364 |
-
const d = param.decimals || 0;
|
| 365 |
-
document.getElementById('cfg-' + key).innerText = d ? val.toFixed(d) : val;
|
| 366 |
-
});
|
| 367 |
-
});
|
| 368 |
-
}
|
| 369 |
|
| 370 |
// Draw line
|
| 371 |
const canvas = document.getElementById('drawing-canvas');
|
|
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>UrbanFlow</title>
|
| 8 |
<link rel="icon" type="image/svg+xml" href="uf.svg">
|
| 9 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
| 14 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
| 15 |
<style>
|
| 16 |
body {
|
| 17 |
+
font-family: 'Montserrat', sans-serif;
|
| 18 |
}
|
| 19 |
|
| 20 |
.font-montserrat {
|
|
|
|
| 62 |
|
| 63 |
<header class="mt-8 flex flex-col items-center flex-shrink-0 w-full z-10">
|
| 64 |
<img src="uf_logo.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
</header>
|
| 66 |
|
| 67 |
<main
|
|
|
|
| 75 |
<p
|
| 76 |
class="font-montserrat font-bold mb-8 text-sm uppercase tracking-[0.2em] text-slate-400 flex items-center">
|
| 77 |
<span class="bg-slate-100 text-slate-600 px-3 py-1 rounded-full text-[10px] mr-3">v1.0 CORE</span>
|
| 78 |
+
Powered by High-Density Vision Metrics
|
| 79 |
</p>
|
| 80 |
<ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-montserrat font-medium text-slate-700">
|
| 81 |
<li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> Real-time
|
|
|
|
| 97 |
class="lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto min-h-[450px] mb-12 lg:mb-0">
|
| 98 |
|
| 99 |
<!-- STEP: Module Select -->
|
| 100 |
+
<div id="step-modules" class="w-full flex flex-col fade-in justify-center items-center">
|
| 101 |
+
<h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">UrbanFlow
|
| 102 |
+
</h2>
|
| 103 |
+
<p class="text-[13px] font-montserrat font-medium text-slate-400 mb-10 text-center">Active Analytical
|
| 104 |
+
Environment: Traffic Dynamics</p>
|
| 105 |
|
| 106 |
+
<div class="flex justify-center w-full">
|
| 107 |
<div onclick="showStep('upload')"
|
| 108 |
+
class="group relative bg-white border-2 border-slate-900 rounded-[2rem] p-8 cursor-pointer hover:shadow-2xl hover:-translate-y-1 transition-all duration-300 text-center max-w-sm w-full">
|
| 109 |
<div
|
| 110 |
+
class="absolute top-4 right-6 bg-slate-900 text-white text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
|
| 111 |
Active</div>
|
| 112 |
+
<i class="fa-solid fa-car-side text-4xl text-slate-900 mb-4 block mx-auto"></i>
|
| 113 |
+
<h3 class="font-montserrat font-bold text-lg mb-2 leading-tight">Traffic <br>Dynamics</h3>
|
| 114 |
+
<p class="text-[10px] text-slate-500 font-montserrat font-medium leading-relaxed">Detect, track,
|
| 115 |
+
and analyze
|
| 116 |
+
vehicles in
|
| 117 |
real-world environments using state-of-the-art vision models.</p>
|
| 118 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
|
|
|
|
| 123 |
<div id="step-upload" class="hidden w-full flex flex-col fade-in justify-center">
|
| 124 |
<button onclick="showStep('modules')"
|
| 125 |
class="text-slate-400 hover:text-black transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
|
| 126 |
+
<i class="fa-solid fa-arrow-left mr-2"></i> Back to Suite
|
| 127 |
</button>
|
| 128 |
+
<h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">Define Media Source
|
| 129 |
</h2>
|
| 130 |
+
<p class="text-[13px] font-montserrat font-medium text-slate-400 mb-8 text-center">Provide the target
|
| 131 |
+
video footage to
|
| 132 |
configure the
|
| 133 |
+
analytical pipeline.</p>
|
| 134 |
|
| 135 |
<div id="dropzone"
|
| 136 |
class="border border-dashed border-slate-300 rounded-[2rem] p-12 flex flex-col items-center justify-center cursor-pointer hover:border-black hover:bg-slate-50 transition-all duration-300 group">
|
|
|
|
| 156 |
</div>
|
| 157 |
</div>
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
<!-- STEP: Draw Line -->
|
| 162 |
<div id="step-draw" class="hidden w-full flex flex-col fade-in justify-center">
|
|
|
|
| 185 |
Reset
|
| 186 |
</button>
|
| 187 |
<button id="btn-proceed" onclick="startRun()"
|
| 188 |
+
class="w-2/3 py-3.5 bg-black text-white rounded-full font-montserrat font-medium hover:bg-slate-800 transition-all text-center text-sm xl:text-base">
|
| 189 |
+
Continue →
|
| 190 |
</button>
|
| 191 |
</div>
|
| 192 |
</div>
|
|
|
|
| 198 |
let videoId = null;
|
| 199 |
let runConfig = {};
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
function showStep(name) {
|
| 202 |
+
['modules', 'upload', 'draw'].forEach(s => {
|
| 203 |
document.getElementById('step-' + s).classList.add('hidden');
|
| 204 |
});
|
| 205 |
document.getElementById('step-' + name).classList.remove('hidden');
|
| 206 |
|
|
|
|
| 207 |
if (name === 'draw') loadFirstFrame();
|
| 208 |
}
|
| 209 |
|
|
|
|
| 257 |
runConfig.conf = 0.12;
|
| 258 |
runConfig.iou = 0.60;
|
| 259 |
txt.innerText = 'Initialization Complete';
|
| 260 |
+
setTimeout(() => showStep('draw'), 400);
|
| 261 |
});
|
| 262 |
};
|
| 263 |
|
| 264 |
xhr.send(form);
|
| 265 |
}
|
| 266 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
// Draw line
|
| 270 |
const canvas = document.getElementById('drawing-canvas');
|
frontend/settings.html
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>UrbanFlow</title>
|
| 8 |
+
<link rel="icon" type="image/svg+xml" href="uf.svg">
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 11 |
+
<link
|
| 12 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@400;500;600;700;800;900&display=swap"
|
| 13 |
+
rel="stylesheet">
|
| 14 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
| 15 |
+
<style>
|
| 16 |
+
body {
|
| 17 |
+
font-family: 'Inter', sans-serif;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.font-montserrat {
|
| 21 |
+
font-family: 'Montserrat', sans-serif;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.mono-font {
|
| 25 |
+
font-family: 'JetBrains Mono', monospace;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.fade-in {
|
| 29 |
+
animation: fadeIn 0.4s ease-in-out forwards;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
@keyframes fadeIn {
|
| 33 |
+
from {
|
| 34 |
+
opacity: 0;
|
| 35 |
+
transform: translateY(10px);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
to {
|
| 39 |
+
opacity: 1;
|
| 40 |
+
transform: translateY(0);
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.bg-glow {
|
| 45 |
+
position: absolute;
|
| 46 |
+
width: 600px;
|
| 47 |
+
height: 600px;
|
| 48 |
+
background: radial-gradient(circle, rgba(241, 245, 249, 1) 0%, rgba(255, 255, 255, 0) 70%);
|
| 49 |
+
top: 50%;
|
| 50 |
+
left: 0;
|
| 51 |
+
transform: translateY(-50%);
|
| 52 |
+
z-index: -1;
|
| 53 |
+
pointer-events: none;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.toggle-track {
|
| 57 |
+
width: 36px;
|
| 58 |
+
height: 20px;
|
| 59 |
+
border-radius: 10px;
|
| 60 |
+
background: #cbd5e1;
|
| 61 |
+
position: relative;
|
| 62 |
+
cursor: pointer;
|
| 63 |
+
transition: background 0.2s;
|
| 64 |
+
flex-shrink: 0;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.toggle-track.active {
|
| 68 |
+
background: #0f172a;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.toggle-thumb {
|
| 72 |
+
width: 16px;
|
| 73 |
+
height: 16px;
|
| 74 |
+
border-radius: 50%;
|
| 75 |
+
background: #fff;
|
| 76 |
+
position: absolute;
|
| 77 |
+
top: 2px;
|
| 78 |
+
left: 2px;
|
| 79 |
+
transition: transform 0.2s;
|
| 80 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.toggle-track.active .toggle-thumb {
|
| 84 |
+
transform: translateX(16px);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.toggle-track.locked {
|
| 88 |
+
opacity: 0.4;
|
| 89 |
+
cursor: not-allowed;
|
| 90 |
+
pointer-events: none;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.custom-select {
|
| 94 |
+
appearance: none;
|
| 95 |
+
background: #f8fafc;
|
| 96 |
+
border: 1px solid #e2e8f0;
|
| 97 |
+
border-radius: 8px;
|
| 98 |
+
padding: 6px 32px 6px 12px;
|
| 99 |
+
font-size: 12px;
|
| 100 |
+
font-weight: 600;
|
| 101 |
+
color: #1e293b;
|
| 102 |
+
cursor: pointer;
|
| 103 |
+
outline: none;
|
| 104 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2364748b'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z'/%3E%3C/svg%3E");
|
| 105 |
+
background-repeat: no-repeat;
|
| 106 |
+
background-position: right 10px center;
|
| 107 |
+
background-size: 14px;
|
| 108 |
+
transition: border-color 0.15s;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.custom-select:hover {
|
| 112 |
+
border-color: #94a3b8;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.custom-select:focus {
|
| 116 |
+
border-color: #0f172a;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.custom-select:disabled {
|
| 120 |
+
opacity: 0.4;
|
| 121 |
+
cursor: not-allowed;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.stepper-btn {
|
| 125 |
+
cursor: pointer;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.stepper-btn:disabled {
|
| 129 |
+
opacity: 0.3;
|
| 130 |
+
cursor: not-allowed;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.setting-card {
|
| 134 |
+
background: #fff;
|
| 135 |
+
border: 1px solid #e2e8f0;
|
| 136 |
+
border-radius: 16px;
|
| 137 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
| 138 |
+
overflow: hidden;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.setting-card-header {
|
| 142 |
+
padding: 16px 24px;
|
| 143 |
+
border-bottom: 1px solid #f1f5f9;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.setting-row {
|
| 147 |
+
display: flex;
|
| 148 |
+
align-items: center;
|
| 149 |
+
justify-content: space-between;
|
| 150 |
+
padding: 14px 24px;
|
| 151 |
+
border-bottom: 1px solid #f8fafc;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.setting-row:last-child {
|
| 155 |
+
border-bottom: none;
|
| 156 |
+
}
|
| 157 |
+
</style>
|
| 158 |
+
</head>
|
| 159 |
+
|
| 160 |
+
<body
|
| 161 |
+
class="bg-white text-slate-900 min-h-screen w-full overflow-x-hidden flex flex-col items-center selection:bg-black selection:text-white relative z-0">
|
| 162 |
+
|
| 163 |
+
<div class="bg-glow"></div>
|
| 164 |
+
|
| 165 |
+
<header class="mt-6 flex flex-col items-center flex-shrink-0 w-full z-10">
|
| 166 |
+
<img src="uf_logo.png" alt="UrbanFlow Logo" class="h-28 w-auto object-contain mb-2">
|
| 167 |
+
<div class="flex items-center space-x-3">
|
| 168 |
+
<span class="w-12 h-[1px] bg-slate-200"></span>
|
| 169 |
+
<p class="font-montserrat font-bold tracking-[0.25em] uppercase text-[10px] text-slate-400 text-center">
|
| 170 |
+
Configure & Launch
|
| 171 |
+
</p>
|
| 172 |
+
<span class="w-12 h-[1px] bg-slate-200"></span>
|
| 173 |
+
</div>
|
| 174 |
+
</header>
|
| 175 |
+
|
| 176 |
+
<main class="flex-1 w-full max-w-4xl mx-auto px-8 py-6 z-10 fade-in">
|
| 177 |
+
|
| 178 |
+
<div class="grid grid-cols-2 gap-5 mb-6">
|
| 179 |
+
|
| 180 |
+
<!-- Processing Parameters (locked once started) -->
|
| 181 |
+
<div class="setting-card" id="card-processing">
|
| 182 |
+
<div class="setting-card-header">
|
| 183 |
+
<h3 class="font-montserrat font-bold text-slate-900 text-base">Processing Parameters</h3>
|
| 184 |
+
<p class="text-[10px] font-montserrat font-medium text-slate-400 mt-1 uppercase tracking-widest">
|
| 185 |
+
Auto-configured · adjustable before start</p>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
<!-- Image Size -->
|
| 189 |
+
<div class="setting-row">
|
| 190 |
+
<div>
|
| 191 |
+
<div class="text-xs font-semibold text-slate-700">Image Size</div>
|
| 192 |
+
<div class="text-[10px] text-slate-400 mt-0.5">Model input resolution</div>
|
| 193 |
+
</div>
|
| 194 |
+
<div
|
| 195 |
+
class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
|
| 196 |
+
<button class="stepper-btn text-slate-400 hover:text-black transition p-1"
|
| 197 |
+
onclick="adjust('imgsz',-1)">
|
| 198 |
+
<i class="fa-solid fa-chevron-left text-[10px]"></i>
|
| 199 |
+
</button>
|
| 200 |
+
<span class="mono-font font-bold w-12 text-center text-slate-900 text-sm"
|
| 201 |
+
id="val-imgsz">640</span>
|
| 202 |
+
<button class="stepper-btn text-slate-400 hover:text-black transition p-1"
|
| 203 |
+
onclick="adjust('imgsz',1)">
|
| 204 |
+
<i class="fa-solid fa-chevron-right text-[10px]"></i>
|
| 205 |
+
</button>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<!-- Confidence -->
|
| 210 |
+
<div class="setting-row">
|
| 211 |
+
<div>
|
| 212 |
+
<div class="text-xs font-semibold text-slate-700">Confidence</div>
|
| 213 |
+
<div class="text-[10px] text-slate-400 mt-0.5">Minimum detection threshold</div>
|
| 214 |
+
</div>
|
| 215 |
+
<div
|
| 216 |
+
class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
|
| 217 |
+
<button class="stepper-btn text-slate-400 hover:text-black transition p-1"
|
| 218 |
+
onclick="adjust('conf',-1)">
|
| 219 |
+
<i class="fa-solid fa-chevron-left text-[10px]"></i>
|
| 220 |
+
</button>
|
| 221 |
+
<span class="mono-font font-bold w-12 text-center text-slate-900 text-sm"
|
| 222 |
+
id="val-conf">0.12</span>
|
| 223 |
+
<button class="stepper-btn text-slate-400 hover:text-black transition p-1"
|
| 224 |
+
onclick="adjust('conf',1)">
|
| 225 |
+
<i class="fa-solid fa-chevron-right text-[10px]"></i>
|
| 226 |
+
</button>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<!-- IoU -->
|
| 231 |
+
<div class="setting-row">
|
| 232 |
+
<div>
|
| 233 |
+
<div class="text-xs font-semibold text-slate-700">IoU Threshold</div>
|
| 234 |
+
<div class="text-[10px] text-slate-400 mt-0.5">Non-max suppression overlap</div>
|
| 235 |
+
</div>
|
| 236 |
+
<div
|
| 237 |
+
class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
|
| 238 |
+
<button class="stepper-btn text-slate-400 hover:text-black transition p-1"
|
| 239 |
+
onclick="adjust('iou',-1)">
|
| 240 |
+
<i class="fa-solid fa-chevron-left text-[10px]"></i>
|
| 241 |
+
</button>
|
| 242 |
+
<span class="mono-font font-bold w-12 text-center text-slate-900 text-sm"
|
| 243 |
+
id="val-iou">0.60</span>
|
| 244 |
+
<button class="stepper-btn text-slate-400 hover:text-black transition p-1"
|
| 245 |
+
onclick="adjust('iou',1)">
|
| 246 |
+
<i class="fa-solid fa-chevron-right text-[10px]"></i>
|
| 247 |
+
</button>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<!-- Frame Stride -->
|
| 252 |
+
<div class="setting-row">
|
| 253 |
+
<div>
|
| 254 |
+
<div class="text-xs font-semibold text-slate-700">Frame Stride</div>
|
| 255 |
+
<div class="text-[10px] text-slate-400 mt-0.5">Frames skipped between detections</div>
|
| 256 |
+
</div>
|
| 257 |
+
<div
|
| 258 |
+
class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
|
| 259 |
+
<button class="stepper-btn text-slate-400 hover:text-black transition p-1"
|
| 260 |
+
onclick="adjust('detect_stride',-1)">
|
| 261 |
+
<i class="fa-solid fa-chevron-left text-[10px]"></i>
|
| 262 |
+
</button>
|
| 263 |
+
<span class="mono-font font-bold w-12 text-center text-slate-900 text-sm"
|
| 264 |
+
id="val-detect_stride">3</span>
|
| 265 |
+
<button class="stepper-btn text-slate-400 hover:text-black transition p-1"
|
| 266 |
+
onclick="adjust('detect_stride',1)">
|
| 267 |
+
<i class="fa-solid fa-chevron-right text-[10px]"></i>
|
| 268 |
+
</button>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<!-- Report Format -->
|
| 273 |
+
<div class="setting-row">
|
| 274 |
+
<div>
|
| 275 |
+
<div class="text-xs font-semibold text-slate-700">Report Format</div>
|
| 276 |
+
<div class="text-[10px] text-slate-400 mt-0.5">Export file type for report charts</div>
|
| 277 |
+
</div>
|
| 278 |
+
<select class="custom-select" id="sel-report-format">
|
| 279 |
+
<option value="png" selected>PNG Image</option>
|
| 280 |
+
<option value="pdf">PDF Document</option>
|
| 281 |
+
</select>
|
| 282 |
+
</div>
|
| 283 |
+
|
| 284 |
+
<!-- Annotated Video -->
|
| 285 |
+
<div class="setting-row">
|
| 286 |
+
<div>
|
| 287 |
+
<div class="text-xs font-semibold text-slate-700">Export Annotated Video</div>
|
| 288 |
+
<div class="text-[10px] text-slate-400 mt-0.5">Bounding boxes + track IDs + counting line</div>
|
| 289 |
+
</div>
|
| 290 |
+
<div class="toggle-track" id="tog-annotated" onclick="toggleSwitch(this)">
|
| 291 |
+
<div class="toggle-thumb"></div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
<!-- Display Preferences (editable anytime) -->
|
| 297 |
+
<div class="setting-card" id="card-display">
|
| 298 |
+
<div class="setting-card-header">
|
| 299 |
+
<h3 class="font-montserrat font-bold text-slate-900 text-base">Display Preferences</h3>
|
| 300 |
+
<p class="text-[10px] font-montserrat font-medium text-slate-400 mt-1 uppercase tracking-widest">
|
| 301 |
+
Editable anytime · applies to live charts</p>
|
| 302 |
+
</div>
|
| 303 |
+
|
| 304 |
+
<!-- Chart Color Palette -->
|
| 305 |
+
<div class="setting-row">
|
| 306 |
+
<div>
|
| 307 |
+
<div class="text-xs font-semibold text-slate-700">Chart Color Palette</div>
|
| 308 |
+
<div class="text-[10px] text-slate-400 mt-0.5">Color scheme for all dashboard charts</div>
|
| 309 |
+
</div>
|
| 310 |
+
<select class="custom-select" id="sel-palette">
|
| 311 |
+
<option value="default" selected>Default</option>
|
| 312 |
+
<option value="vibrant">Vibrant</option>
|
| 313 |
+
<option value="corporate">Corporate</option>
|
| 314 |
+
</select>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<!-- Palette Preview -->
|
| 318 |
+
<div class="px-6 py-4">
|
| 319 |
+
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-3">Palette Preview
|
| 320 |
+
</div>
|
| 321 |
+
<div class="flex gap-2" id="palette-preview"></div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
</div>
|
| 326 |
+
|
| 327 |
+
<!-- Start Button -->
|
| 328 |
+
<button id="btn-start" onclick="startProcessing()"
|
| 329 |
+
class="w-full py-4 bg-black text-white rounded-full font-montserrat font-semibold hover:bg-slate-800 transition-all text-lg flex justify-center items-center shadow-lg">
|
| 330 |
+
Start Processing <i class="fa-solid fa-arrow-right ml-3 text-sm"></i>
|
| 331 |
+
</button>
|
| 332 |
+
|
| 333 |
+
</main>
|
| 334 |
+
|
| 335 |
+
<script>
|
| 336 |
+
// =========== Config params ===========
|
| 337 |
+
const paramDefs = {
|
| 338 |
+
imgsz: { step: 32, min: 640, max: 1280, decimals: 0 },
|
| 339 |
+
conf: { step: 0.01, min: 0.08, max: 1.0, decimals: 2 },
|
| 340 |
+
iou: { step: 0.05, min: 0.5, max: 1.0, decimals: 2 },
|
| 341 |
+
detect_stride: { step: 1, min: 1, max: 10, decimals: 0 }
|
| 342 |
+
};
|
| 343 |
+
|
| 344 |
+
// =========== Palette definitions ===========
|
| 345 |
+
const PALETTES = {
|
| 346 |
+
default: { congestion: '#f97316', dominance: '#14b8a6', flow: '#3b82f6', label: 'Default' },
|
| 347 |
+
vibrant: { congestion: '#e11d48', dominance: '#8b5cf6', flow: '#06b6d4', label: 'Vibrant' },
|
| 348 |
+
corporate: { congestion: '#1e40af', dominance: '#0d9488', flow: '#475569', label: 'Corporate' }
|
| 349 |
+
};
|
| 350 |
+
|
| 351 |
+
// =========== Load run data from initial.html ===========
|
| 352 |
+
const raw = sessionStorage.getItem('funky_run');
|
| 353 |
+
if (!raw) { window.location.href = '/'; }
|
| 354 |
+
const runData = JSON.parse(raw);
|
| 355 |
+
const config = runData.config;
|
| 356 |
+
|
| 357 |
+
// Populate stepper values from auto-calculated config
|
| 358 |
+
function renderValues() {
|
| 359 |
+
for (const key of Object.keys(paramDefs)) {
|
| 360 |
+
const d = paramDefs[key].decimals;
|
| 361 |
+
const val = config[key];
|
| 362 |
+
document.getElementById('val-' + key).innerText = d ? val.toFixed(d) : val;
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
renderValues();
|
| 366 |
+
|
| 367 |
+
// =========== Stepper ===========
|
| 368 |
+
function adjust(key, dir) {
|
| 369 |
+
const p = paramDefs[key];
|
| 370 |
+
let val = config[key] + dir * p.step;
|
| 371 |
+
val = Math.max(p.min, Math.min(p.max, val));
|
| 372 |
+
val = p.decimals ? parseFloat(val.toFixed(p.decimals)) : Math.round(val);
|
| 373 |
+
config[key] = val;
|
| 374 |
+
document.getElementById('val-' + key).innerText = p.decimals ? val.toFixed(p.decimals) : val;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// =========== Toggle ===========
|
| 378 |
+
function toggleSwitch(el) {
|
| 379 |
+
if (el.classList.contains('locked')) return;
|
| 380 |
+
el.classList.toggle('active');
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
// =========== Palette preview ===========
|
| 384 |
+
function updatePalettePreview() {
|
| 385 |
+
const key = document.getElementById('sel-palette').value;
|
| 386 |
+
const pal = PALETTES[key];
|
| 387 |
+
const container = document.getElementById('palette-preview');
|
| 388 |
+
const colors = [
|
| 389 |
+
{ color: pal.congestion, label: 'Congestion' },
|
| 390 |
+
{ color: pal.dominance, label: 'Dominance' },
|
| 391 |
+
{ color: pal.flow, label: 'Flow' }
|
| 392 |
+
];
|
| 393 |
+
container.innerHTML = colors.map(c =>
|
| 394 |
+
`<div class="flex-1 rounded-lg overflow-hidden border border-slate-100">
|
| 395 |
+
<div class="h-8" style="background:${c.color}"></div>
|
| 396 |
+
<div class="text-[9px] font-bold text-slate-500 text-center py-1.5 bg-slate-50">${c.label}</div>
|
| 397 |
+
</div>`
|
| 398 |
+
).join('');
|
| 399 |
+
}
|
| 400 |
+
document.getElementById('sel-palette').addEventListener('change', updatePalettePreview);
|
| 401 |
+
updatePalettePreview();
|
| 402 |
+
|
| 403 |
+
// =========== Start Processing ===========
|
| 404 |
+
function startProcessing() {
|
| 405 |
+
// Gather settings
|
| 406 |
+
const settings = {
|
| 407 |
+
reportFormat: document.getElementById('sel-report-format').value,
|
| 408 |
+
annotatedVideo: document.getElementById('tog-annotated').classList.contains('active'),
|
| 409 |
+
palette: document.getElementById('sel-palette').value
|
| 410 |
+
};
|
| 411 |
+
|
| 412 |
+
// Update config from steppers (already in config object)
|
| 413 |
+
// Save everything to sessionStorage
|
| 414 |
+
sessionStorage.setItem('funky_run', JSON.stringify({
|
| 415 |
+
video_id: runData.video_id,
|
| 416 |
+
line: runData.line,
|
| 417 |
+
config: config,
|
| 418 |
+
settings: settings
|
| 419 |
+
}));
|
| 420 |
+
|
| 421 |
+
window.location.href = 'vehicles.html';
|
| 422 |
+
}
|
| 423 |
+
</script>
|
| 424 |
+
</body>
|
| 425 |
+
|
| 426 |
+
</html>
|
frontend/vehicles.html
CHANGED
|
@@ -4,16 +4,18 @@
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
-
<title>UrbanFlow
|
| 8 |
<link rel="icon" type="image/svg+xml" href="uf.svg">
|
| 9 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 11 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 12 |
-
<link
|
|
|
|
|
|
|
| 13 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 14 |
<style>
|
| 15 |
body {
|
| 16 |
-
font-family: '
|
| 17 |
}
|
| 18 |
|
| 19 |
.mono-font {
|
|
@@ -85,6 +87,126 @@
|
|
| 85 |
.info-wrap:hover .info-tip {
|
| 86 |
display: block;
|
| 87 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</style>
|
| 89 |
</head>
|
| 90 |
|
|
@@ -96,35 +218,25 @@
|
|
| 96 |
<img src="uf_logo.png" alt="UrbanFlow Logo" class="h-24 w-auto object-contain">
|
| 97 |
</div>
|
| 98 |
<nav class="flex-1 px-4 py-4 space-y-1.5 overflow-y-auto text-sm">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
<a onclick="switchTab('overview')" id="nav-overview"
|
| 100 |
-
class="flex items-center px-4 py-2.5 bg-slate-
|
| 101 |
<i class="fa-solid fa-desktop w-6"></i> <span class="font-medium">Overview</span>
|
| 102 |
</a>
|
| 103 |
<a onclick="switchTab('run-details')" id="nav-run-details"
|
| 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
|
| 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
|
| 113 |
-
<
|
| 114 |
-
class="font-medium">Analytics</span></div>
|
| 115 |
-
<i class="fa-solid fa-lock text-[10px]"></i>
|
| 116 |
-
</a>
|
| 117 |
-
<a
|
| 118 |
-
class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
|
| 119 |
-
<div class="flex items-center"><i class="fa-solid fa-video w-6"></i> <span class="font-medium">RTSP
|
| 120 |
-
Feed</span></div>
|
| 121 |
-
<i class="fa-solid fa-lock text-[10px]"></i>
|
| 122 |
-
</a>
|
| 123 |
-
<a
|
| 124 |
-
class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
|
| 125 |
-
<div class="flex items-center"><i class="fa-solid fa-gear w-6"></i> <span
|
| 126 |
-
class="font-medium">Settings</span></div>
|
| 127 |
-
<i class="fa-solid fa-lock text-[10px]"></i>
|
| 128 |
</a>
|
| 129 |
</nav>
|
| 130 |
<div class="mt-auto border-t border-slate-100 p-4 flex items-center justify-center bg-white flex-shrink-0">
|
|
@@ -134,167 +246,232 @@
|
|
| 134 |
|
| 135 |
<main class="flex-1 flex flex-col h-full min-w-0 p-4 gap-4">
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
<!-- Progress Bar (shared) -->
|
| 138 |
<div
|
| 139 |
class="bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
|
| 140 |
-
<div class="flex items-center space-x-4
|
| 141 |
-
<span class="text-[11px] font-black text-slate-900 uppercase tracking-wider"
|
| 142 |
-
id="proc-label">
|
| 143 |
-
<div class="flex-1 h-
|
| 144 |
<div id="proc-bar" class="h-full bg-slate-900 rounded-full transition-all duration-500 ease-out"
|
| 145 |
style="width: 0%"></div>
|
| 146 |
</div>
|
| 147 |
</div>
|
| 148 |
-
<div class="flex items-center space-x-6 text-xs font-bold text-slate-900">
|
| 149 |
<span id="proc-frames">0 / 0 Frames</span>
|
| 150 |
<span id="proc-pct">0%</span>
|
| 151 |
</div>
|
| 152 |
</div>
|
| 153 |
|
| 154 |
<!-- TAB: Overview -->
|
| 155 |
-
<div id="tab-overview" class="grid grid-cols-
|
| 156 |
|
| 157 |
<!-- Congestion Index -->
|
| 158 |
-
<div
|
|
|
|
| 159 |
<div class="flex justify-between items-center mb-2 relative">
|
| 160 |
-
<h3 class="font-bold text-slate-900 text-sm flex items-center">Congestion Index
|
| 161 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 162 |
<span class="info-tip">Count of active tracked vehicles visible in each processed
|
| 163 |
frame.</span></span>
|
| 164 |
</h3>
|
| 165 |
</div>
|
| 166 |
-
<div class="flex-1 relative w-full min-h-
|
| 167 |
<canvas id="congestionChart"></canvas>
|
| 168 |
</div>
|
| 169 |
</div>
|
| 170 |
|
| 171 |
-
<!--
|
| 172 |
-
<div
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
<
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
<span class="text-3xl font-black text-slate-900" id="cnt-total">0</span>
|
| 179 |
-
|
| 180 |
-
</div>
|
| 181 |
-
<div
|
| 182 |
-
class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm flex-1 flex flex-col items-center justify-center">
|
| 183 |
-
<div class="text-slate-500 text-[10px] font-bold uppercase tracking-wide text-center">Incoming
|
| 184 |
-
(Crossed)</div>
|
| 185 |
-
<div class="flex items-center justify-center mt-1">
|
| 186 |
-
<span class="text-3xl font-black text-slate-900" id="cnt-in">0</span>
|
| 187 |
-
</div>
|
| 188 |
-
</div>
|
| 189 |
-
<div
|
| 190 |
-
class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm flex-1 flex flex-col items-center justify-center">
|
| 191 |
-
<div class="text-slate-500 text-[10px] font-bold uppercase tracking-wide text-center">Outgoing
|
| 192 |
-
(Crossed)</div>
|
| 193 |
-
<div class="flex items-center justify-center mt-1">
|
| 194 |
-
<span class="text-3xl font-black text-slate-900" id="cnt-out">0</span>
|
| 195 |
</div>
|
| 196 |
</div>
|
| 197 |
</div>
|
| 198 |
|
| 199 |
<!-- Vehicle Classification Breakdown -->
|
| 200 |
<div
|
| 201 |
-
class="col-span-
|
| 202 |
<div class="flex justify-between items-center mb-1 relative">
|
| 203 |
-
<h3 class="font-bold text-slate-900 text-sm flex items-center">Classification
|
| 204 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 205 |
<span class="info-tip">Per-class vehicle distribution with directional split across the
|
| 206 |
-
|
| 207 |
</h3>
|
| 208 |
</div>
|
| 209 |
-
<div class="text-[10px] text-slate-400 mb-3">
|
| 210 |
-
<div class="flex-1 overflow-y-auto pr-
|
| 211 |
</div>
|
| 212 |
|
| 213 |
<!-- Class Dominance -->
|
| 214 |
-
<div
|
|
|
|
| 215 |
<div class="flex justify-between items-center mb-2 relative">
|
| 216 |
-
<h3 class="font-bold text-slate-900 text-sm flex items-center">
|
| 217 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 218 |
-
<span class="info-tip">Aggregated vehicle count grouped by
|
| 219 |
</h3>
|
| 220 |
-
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">
|
| 221 |
-
|
| 222 |
</div>
|
| 223 |
-
<div class="flex-1 w-full relative min-h-
|
| 224 |
<canvas id="dominanceChart"></canvas>
|
| 225 |
</div>
|
| 226 |
</div>
|
| 227 |
|
| 228 |
<!-- Traffic Flow Over Time -->
|
| 229 |
-
<div
|
|
|
|
| 230 |
<div class="flex justify-between items-center mb-2 relative">
|
| 231 |
-
<h3 class="font-bold text-slate-900 text-sm flex items-center">Traffic Flow
|
| 232 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 233 |
-
<span class="info-tip">Number of vehicles
|
| 234 |
-
video duration.</span></span>
|
| 235 |
</h3>
|
| 236 |
-
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">
|
| 237 |
</div>
|
| 238 |
-
<div class="flex-1 w-full relative min-h-
|
| 239 |
<canvas id="flowChart"></canvas>
|
| 240 |
</div>
|
| 241 |
</div>
|
| 242 |
|
| 243 |
</div>
|
| 244 |
|
| 245 |
-
<!-- TAB: Run
|
| 246 |
<div id="tab-run-details" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 247 |
-
<div class="
|
| 248 |
-
<!-- Video Input -->
|
| 249 |
-
<div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 250 |
-
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
|
| 251 |
-
<h3 class="font-bold text-slate-800 text-sm"><i
|
| 252 |
-
class="fa-solid fa-film mr-2 text-slate-400"></i> Video Input</h3>
|
| 253 |
-
</div>
|
| 254 |
-
<div class="p-5 space-y-3" id="panel-video"></div>
|
| 255 |
-
</div>
|
| 256 |
|
| 257 |
-
<!--
|
| 258 |
-
<div
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
</div>
|
| 263 |
-
<div class="p-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
</div>
|
| 272 |
-
<div class="p-5 space-y-3" id="panel-model"></div>
|
| 273 |
</div>
|
| 274 |
|
| 275 |
-
<!--
|
| 276 |
-
<div class="
|
| 277 |
-
<div class="
|
| 278 |
-
<
|
| 279 |
-
|
|
|
|
|
|
|
| 280 |
</div>
|
| 281 |
-
<div class="
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
class="col-span-2 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 287 |
-
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
|
| 288 |
-
<h3 class="font-bold text-slate-800 text-sm"><i
|
| 289 |
-
class="fa-solid fa-stopwatch mr-2 text-slate-400"></i> Processing Results</h3>
|
| 290 |
</div>
|
| 291 |
-
<div class="
|
| 292 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
| 293 |
</div>
|
| 294 |
</div>
|
|
|
|
| 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">
|
|
@@ -304,6 +481,122 @@
|
|
| 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>
|
|
@@ -326,20 +619,20 @@
|
|
| 326 |
|
| 327 |
// =========== Tab switching ===========
|
| 328 |
function switchTab(tab) {
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
}
|
| 344 |
|
| 345 |
// =========== Run Details helpers ===========
|
|
@@ -397,7 +690,7 @@
|
|
| 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-
|
| 401 |
</div>` +
|
| 402 |
detailRow('task', 'detect') +
|
| 403 |
detailRow('format', 'OpenVINO') +
|
|
@@ -405,15 +698,28 @@
|
|
| 405 |
|
| 406 |
document.getElementById('panel-infer').innerHTML =
|
| 407 |
infoRow('imgsz', c.imgsz, 'Input image resolution for model inference.') +
|
| 408 |
-
infoRow('vid_stride', c.detect_stride, 'Frames skipped between consecutive detections.') +
|
| 409 |
infoRow('conf', c.conf || 0.12, 'Minimum confidence threshold for valid detections.') +
|
| 410 |
infoRow('iou', c.iou || 0.60, 'Intersection-over-Union threshold for non-max suppression.') +
|
| 411 |
infoRow('stream', 'TRUE', 'Frame-by-frame processing for constant memory usage.') +
|
| 412 |
infoRow('verbose', 'FALSE', 'Console logging suppressed during inference.');
|
| 413 |
}
|
| 414 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
// =========== Charts ===========
|
| 416 |
-
Chart.defaults.font.family = "'
|
| 417 |
Chart.defaults.color = '#64748b';
|
| 418 |
|
| 419 |
let MODEL_CLASSES = {};
|
|
@@ -421,7 +727,17 @@
|
|
| 421 |
|
| 422 |
const congChart = new Chart(document.getElementById('congestionChart').getContext('2d'), {
|
| 423 |
type: 'line',
|
| 424 |
-
data: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
options: {
|
| 426 |
responsive: true, maintainAspectRatio: false,
|
| 427 |
plugins: { legend: { display: false } },
|
|
@@ -433,9 +749,30 @@
|
|
| 433 |
}
|
| 434 |
});
|
| 435 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
const domChart = new Chart(document.getElementById('dominanceChart').getContext('2d'), {
|
| 437 |
type: 'bar',
|
| 438 |
-
data: { labels: [], datasets: [{ data: [], backgroundColor:
|
| 439 |
options: {
|
| 440 |
responsive: true, maintainAspectRatio: false,
|
| 441 |
plugins: { legend: { display: false } },
|
|
@@ -449,7 +786,7 @@
|
|
| 449 |
|
| 450 |
const flowChart = new Chart(document.getElementById('flowChart').getContext('2d'), {
|
| 451 |
type: 'bar',
|
| 452 |
-
data: { labels: [], datasets: [{ data: [], backgroundColor:
|
| 453 |
options: {
|
| 454 |
responsive: true, maintainAspectRatio: false,
|
| 455 |
plugins: { legend: { display: false } },
|
|
@@ -464,11 +801,113 @@
|
|
| 464 |
// =========== Update functions ===========
|
| 465 |
function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
|
| 466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
function updateBreakdown(classIn, classOut) {
|
| 468 |
const container = document.getElementById('class-breakdown');
|
| 469 |
const totalAll = sumValues(classIn) + sumValues(classOut);
|
| 470 |
container.innerHTML = '';
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
Object.keys(MODEL_CLASSES).map(Number).sort((a, b) => a - b).forEach(id => {
|
| 473 |
const inC = classIn[String(id)] || 0;
|
| 474 |
const outC = classOut[String(id)] || 0;
|
|
@@ -528,30 +967,85 @@
|
|
| 528 |
}
|
| 529 |
|
| 530 |
// =========== Main ===========
|
|
|
|
|
|
|
| 531 |
async function init() {
|
| 532 |
const raw = sessionStorage.getItem('funky_run');
|
| 533 |
if (!raw) { window.location.href = '/'; return; }
|
| 534 |
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
| 536 |
|
| 537 |
const cRes = await fetch('/constants');
|
| 538 |
const cData = await cRes.json();
|
| 539 |
MODEL_CLASSES = cData.classes;
|
| 540 |
BUSINESS_MAP = cData.business_map;
|
| 541 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
populateRunDetails(params.config);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
|
| 544 |
-
|
| 545 |
-
const
|
| 546 |
|
| 547 |
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
| 548 |
const ws = new WebSocket(`${proto}://${location.host}/ws/run`);
|
| 549 |
|
| 550 |
ws.onopen = () => {
|
| 551 |
ws.send(JSON.stringify({
|
| 552 |
-
video_id:
|
| 553 |
-
line:
|
| 554 |
-
config:
|
|
|
|
|
|
|
| 555 |
}));
|
| 556 |
};
|
| 557 |
|
|
@@ -565,10 +1059,17 @@
|
|
| 565 |
document.getElementById('proc-bar').style.width = '100%';
|
| 566 |
document.getElementById('proc-pct').innerText = '100%';
|
| 567 |
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
|
| 573 |
if (d.video_id) loadReports(d.video_id);
|
| 574 |
return;
|
|
@@ -582,8 +1083,10 @@
|
|
| 582 |
const totalIn = sumValues(d.class_in);
|
| 583 |
const totalOut = sumValues(d.class_out);
|
| 584 |
document.getElementById('cnt-total').innerText = totalIn + totalOut;
|
| 585 |
-
|
| 586 |
-
|
|
|
|
|
|
|
| 587 |
|
| 588 |
const now = performance.now();
|
| 589 |
if (now - lastUIUpdate < 300) return;
|
|
@@ -598,10 +1101,16 @@
|
|
| 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) {
|
|
@@ -617,8 +1126,28 @@
|
|
| 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>
|
|
@@ -631,7 +1160,7 @@
|
|
| 631 |
</a>
|
| 632 |
</div>
|
| 633 |
<div class="p-4 flex items-center justify-center bg-slate-50/30">
|
| 634 |
-
|
| 635 |
</div>
|
| 636 |
`;
|
| 637 |
grid.appendChild(card);
|
|
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>UrbanFlow</title>
|
| 8 |
<link rel="icon" type="image/svg+xml" href="uf.svg">
|
| 9 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 11 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 12 |
+
<link
|
| 13 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@400;500;600;700;800;900&display=swap"
|
| 14 |
+
rel="stylesheet">
|
| 15 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 16 |
<style>
|
| 17 |
body {
|
| 18 |
+
font-family: 'Montserrat', sans-serif;
|
| 19 |
}
|
| 20 |
|
| 21 |
.mono-font {
|
|
|
|
| 87 |
.info-wrap:hover .info-tip {
|
| 88 |
display: block;
|
| 89 |
}
|
| 90 |
+
|
| 91 |
+
/* Settings controls */
|
| 92 |
+
.toggle-track {
|
| 93 |
+
width: 36px;
|
| 94 |
+
height: 20px;
|
| 95 |
+
border-radius: 10px;
|
| 96 |
+
background: #cbd5e1;
|
| 97 |
+
position: relative;
|
| 98 |
+
cursor: pointer;
|
| 99 |
+
transition: background 0.2s;
|
| 100 |
+
flex-shrink: 0;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.toggle-track.active {
|
| 104 |
+
background: #0f172a;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.toggle-thumb {
|
| 108 |
+
width: 16px;
|
| 109 |
+
height: 16px;
|
| 110 |
+
border-radius: 50%;
|
| 111 |
+
background: #fff;
|
| 112 |
+
position: absolute;
|
| 113 |
+
top: 2px;
|
| 114 |
+
left: 2px;
|
| 115 |
+
transition: transform 0.2s;
|
| 116 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.toggle-track.active .toggle-thumb {
|
| 120 |
+
transform: translateX(16px);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.toggle-track.locked {
|
| 124 |
+
opacity: 0.4;
|
| 125 |
+
cursor: not-allowed;
|
| 126 |
+
pointer-events: none;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.custom-select {
|
| 130 |
+
appearance: none;
|
| 131 |
+
background: #f8fafc;
|
| 132 |
+
border: 1px solid #e2e8f0;
|
| 133 |
+
border-radius: 8px;
|
| 134 |
+
padding: 6px 32px 6px 12px;
|
| 135 |
+
font-size: 12px;
|
| 136 |
+
font-weight: 600;
|
| 137 |
+
color: #1e293b;
|
| 138 |
+
cursor: pointer;
|
| 139 |
+
outline: none;
|
| 140 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2364748b'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z'/%3E%3C/svg%3E");
|
| 141 |
+
background-repeat: no-repeat;
|
| 142 |
+
background-position: right 10px center;
|
| 143 |
+
background-size: 14px;
|
| 144 |
+
transition: border-color 0.15s;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.custom-select:hover {
|
| 148 |
+
border-color: #94a3b8;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.custom-select:disabled {
|
| 152 |
+
opacity: 0.4;
|
| 153 |
+
cursor: not-allowed;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.s-stepper {
|
| 157 |
+
display: inline-flex;
|
| 158 |
+
align-items: center;
|
| 159 |
+
border: 1px solid #e2e8f0;
|
| 160 |
+
border-radius: 10px;
|
| 161 |
+
background: #f8fafc;
|
| 162 |
+
overflow: hidden;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.s-stepper button {
|
| 166 |
+
background: none;
|
| 167 |
+
border: none;
|
| 168 |
+
padding: 4px 10px;
|
| 169 |
+
cursor: pointer;
|
| 170 |
+
color: #64748b;
|
| 171 |
+
font-size: 14px;
|
| 172 |
+
line-height: 1;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.s-stepper button:hover {
|
| 176 |
+
background: #e2e8f0;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.s-stepper button:disabled {
|
| 180 |
+
opacity: 0.25;
|
| 181 |
+
cursor: not-allowed;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.s-stepper .s-val {
|
| 185 |
+
min-width: 48px;
|
| 186 |
+
text-align: center;
|
| 187 |
+
font-family: 'JetBrains Mono', monospace;
|
| 188 |
+
font-size: 13px;
|
| 189 |
+
font-weight: 700;
|
| 190 |
+
color: #0f172a;
|
| 191 |
+
padding: 4px 0;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.s-row {
|
| 195 |
+
display: flex;
|
| 196 |
+
align-items: center;
|
| 197 |
+
justify-content: space-between;
|
| 198 |
+
padding: 10px 0;
|
| 199 |
+
border-bottom: 1px solid #f1f5f9;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.s-row:last-child {
|
| 203 |
+
border-bottom: none;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.s-row.disabled {
|
| 207 |
+
opacity: 0.45;
|
| 208 |
+
pointer-events: none;
|
| 209 |
+
}
|
| 210 |
</style>
|
| 211 |
</head>
|
| 212 |
|
|
|
|
| 218 |
<img src="uf_logo.png" alt="UrbanFlow Logo" class="h-24 w-auto object-contain">
|
| 219 |
</div>
|
| 220 |
<nav class="flex-1 px-4 py-4 space-y-1.5 overflow-y-auto text-sm">
|
| 221 |
+
<a onclick="switchTab('about')" id="nav-about"
|
| 222 |
+
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">
|
| 223 |
+
<i class="fa-solid fa-circle-info w-6"></i> <span class="font-medium">About</span>
|
| 224 |
+
</a>
|
| 225 |
<a onclick="switchTab('overview')" id="nav-overview"
|
| 226 |
+
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">
|
| 227 |
<i class="fa-solid fa-desktop w-6"></i> <span class="font-medium">Overview</span>
|
| 228 |
</a>
|
| 229 |
<a onclick="switchTab('run-details')" id="nav-run-details"
|
| 230 |
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">
|
| 231 |
+
<i class="fa-solid fa-microchip w-6"></i> <span class="font-medium">Run</span>
|
| 232 |
</a>
|
| 233 |
<a onclick="switchTab('reports')" id="nav-reports"
|
| 234 |
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">
|
| 235 |
<i class="fa-solid fa-file-lines w-6"></i> <span class="font-medium">Reports</span>
|
| 236 |
</a>
|
| 237 |
+
<a onclick="switchTab('settings')" id="nav-settings"
|
| 238 |
+
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">
|
| 239 |
+
<i class="fa-solid fa-sliders w-6"></i> <span class="font-medium">Settings</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
</a>
|
| 241 |
</nav>
|
| 242 |
<div class="mt-auto border-t border-slate-100 p-4 flex items-center justify-center bg-white flex-shrink-0">
|
|
|
|
| 246 |
|
| 247 |
<main class="flex-1 flex flex-col h-full min-w-0 p-4 gap-4">
|
| 248 |
|
| 249 |
+
<!-- TAB: About -->
|
| 250 |
+
<div id="tab-about" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 251 |
+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm p-8 max-w-[1400px] w-full">
|
| 252 |
+
<h2 class="text-2xl font-bold text-slate-800 mb-6">UrbanFlow</h2>
|
| 253 |
+
|
| 254 |
+
<div class="mb-10 py-6 border-y border-slate-50">
|
| 255 |
+
<p
|
| 256 |
+
class="max-w-3xl mx-auto text-center text-slate-400 text-sm font-semibold leading-relaxed tracking-wide italic px-8">
|
| 257 |
+
"Urban Flow is more than a metric of transit; it is the pulse of a city’s collective intent.
|
| 258 |
+
This workspace is designed to observe that rhythm, transforming raw motion into the clarity
|
| 259 |
+
required for decisive, visionary infrastructure."
|
| 260 |
+
</p>
|
| 261 |
+
</div>
|
| 262 |
+
|
| 263 |
+
<div class="space-y-6 text-slate-600 leading-relaxed">
|
| 264 |
+
<p>
|
| 265 |
+
UrbanFlow is an advanced vision intelligence suite designed for real-time traffic dynamics
|
| 266 |
+
analysis.
|
| 267 |
+
Leveraging state-of-the-art deep learning models, it provides consultative-grade insights into
|
| 268 |
+
vehicle movement, classification, and throughput.
|
| 269 |
+
</p>
|
| 270 |
+
<p>
|
| 271 |
+
This platform is engineered for urban planners and infrastructure executives who require
|
| 272 |
+
high-density data to drive visionary urban transformation. By transforming raw video footage
|
| 273 |
+
into actionable intelligence, UrbanFlow enables data-driven decision making for safer,
|
| 274 |
+
more efficient urban mobility.
|
| 275 |
+
</p>
|
| 276 |
+
<div class="grid grid-cols-2 gap-8 pt-6 border-t border-slate-50">
|
| 277 |
+
<div class="space-y-4">
|
| 278 |
+
<h4 class="font-bold text-slate-800 text-sm uppercase tracking-wider">Core Capabilities</h4>
|
| 279 |
+
<ul class="text-xs space-y-2">
|
| 280 |
+
<li class="flex items-center gap-2"><i
|
| 281 |
+
class="fa-solid fa-circle text-[6px] text-slate-400"></i> Real-time Spatial
|
| 282 |
+
Detection</li>
|
| 283 |
+
<li class="flex items-center gap-2"><i
|
| 284 |
+
class="fa-solid fa-circle text-[6px] text-slate-400"></i> Multi-class Object
|
| 285 |
+
Tracking</li>
|
| 286 |
+
<li class="flex items-center gap-2"><i
|
| 287 |
+
class="fa-solid fa-circle text-[6px] text-slate-400"></i> Bidirectional Flow
|
| 288 |
+
Analysis</li>
|
| 289 |
+
<li class="flex items-center gap-2"><i
|
| 290 |
+
class="fa-solid fa-circle text-[6px] text-slate-400"></i> High-Density Metrics
|
| 291 |
+
Capture</li>
|
| 292 |
+
</ul>
|
| 293 |
+
</div>
|
| 294 |
+
<div class="space-y-4">
|
| 295 |
+
<h4 class="font-bold text-slate-800 text-sm uppercase tracking-wider">Intelligence Framework
|
| 296 |
+
</h4>
|
| 297 |
+
<ul class="text-xs space-y-2">
|
| 298 |
+
<li class="flex items-center gap-2"><i
|
| 299 |
+
class="fa-solid fa-circle text-[6px] text-slate-400"></i> OpenVINO Performance
|
| 300 |
+
Optimization</li>
|
| 301 |
+
<li class="flex items-center gap-2"><i
|
| 302 |
+
class="fa-solid fa-circle text-[6px] text-slate-400"></i> ByteTrack Temporal
|
| 303 |
+
Assignment</li>
|
| 304 |
+
<li class="flex items-center gap-2"><i
|
| 305 |
+
class="fa-solid fa-circle text-[6px] text-slate-400"></i> Sub-millimeter
|
| 306 |
+
Coordinate Logic</li>
|
| 307 |
+
<li class="flex items-center gap-2"><i
|
| 308 |
+
class="fa-solid fa-circle text-[6px] text-slate-400"></i> Enterprise MIS
|
| 309 |
+
Compliance</li>
|
| 310 |
+
</ul>
|
| 311 |
+
</div>
|
| 312 |
+
</div>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
|
| 318 |
<!-- Progress Bar (shared) -->
|
| 319 |
<div
|
| 320 |
class="bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
|
| 321 |
+
<div class="flex items-center space-x-4 flex-1 mr-6">
|
| 322 |
+
<span class="text-[11px] font-black text-slate-900 uppercase tracking-wider whitespace-nowrap"
|
| 323 |
+
id="proc-label">Waiting</span>
|
| 324 |
+
<div class="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden relative">
|
| 325 |
<div id="proc-bar" class="h-full bg-slate-900 rounded-full transition-all duration-500 ease-out"
|
| 326 |
style="width: 0%"></div>
|
| 327 |
</div>
|
| 328 |
</div>
|
| 329 |
+
<div class="flex items-center space-x-6 text-xs font-bold text-slate-900 whitespace-nowrap">
|
| 330 |
<span id="proc-frames">0 / 0 Frames</span>
|
| 331 |
<span id="proc-pct">0%</span>
|
| 332 |
</div>
|
| 333 |
</div>
|
| 334 |
|
| 335 |
<!-- TAB: Overview -->
|
| 336 |
+
<div id="tab-overview" class="grid grid-cols-12 gap-4 flex-1 min-h-0 overflow-hidden">
|
| 337 |
|
| 338 |
<!-- Congestion Index -->
|
| 339 |
+
<div
|
| 340 |
+
class="col-span-12 xl:col-span-6 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col min-h-0">
|
| 341 |
<div class="flex justify-between items-center mb-2 relative">
|
| 342 |
+
<h3 class="font-bold text-slate-900 text-sm flex items-center">Real-time Congestion Index
|
| 343 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 344 |
<span class="info-tip">Count of active tracked vehicles visible in each processed
|
| 345 |
frame.</span></span>
|
| 346 |
</h3>
|
| 347 |
</div>
|
| 348 |
+
<div class="flex-1 relative w-full min-h-[220px]">
|
| 349 |
<canvas id="congestionChart"></canvas>
|
| 350 |
</div>
|
| 351 |
</div>
|
| 352 |
|
| 353 |
+
<!-- Doughnut — In / Out -->
|
| 354 |
+
<div
|
| 355 |
+
class="col-span-6 xl:col-span-3 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col items-center justify-center min-h-0">
|
| 356 |
+
<h3 class="font-bold text-slate-900 text-sm mb-2 self-start flex items-center">Directional Distribution
|
| 357 |
+
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 358 |
+
<span class="info-tip">Incoming vs outgoing vehicles analyzed within the spatial
|
| 359 |
+
boundary.</span></span>
|
| 360 |
+
</h3>
|
| 361 |
+
<div class="relative w-full flex-1 min-h-[180px] flex items-center justify-center">
|
| 362 |
+
<canvas id="doughnutChart"></canvas>
|
| 363 |
+
<div class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
|
| 364 |
<span class="text-3xl font-black text-slate-900" id="cnt-total">0</span>
|
| 365 |
+
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Total Units</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
</div>
|
| 367 |
</div>
|
| 368 |
</div>
|
| 369 |
|
| 370 |
<!-- Vehicle Classification Breakdown -->
|
| 371 |
<div
|
| 372 |
+
class="col-span-6 xl:col-span-3 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
|
| 373 |
<div class="flex justify-between items-center mb-1 relative">
|
| 374 |
+
<h3 class="font-bold text-slate-900 text-sm flex items-center">Vehicle Classification
|
| 375 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 376 |
<span class="info-tip">Per-class vehicle distribution with directional split across the
|
| 377 |
+
spatial boundary.</span></span>
|
| 378 |
</h3>
|
| 379 |
</div>
|
| 380 |
+
<div class="text-[10px] text-slate-400 mb-3 uppercase tracking-widest">Inventory Breakdown</div>
|
| 381 |
+
<div class="flex-1 overflow-y-auto pr-2 space-y-2" id="class-breakdown"></div>
|
| 382 |
</div>
|
| 383 |
|
| 384 |
<!-- Class Dominance -->
|
| 385 |
+
<div
|
| 386 |
+
class="col-span-12 xl:col-span-6 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col min-h-0">
|
| 387 |
<div class="flex justify-between items-center mb-2 relative">
|
| 388 |
+
<h3 class="font-bold text-slate-900 text-sm flex items-center">Category Dominance
|
| 389 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 390 |
+
<span class="info-tip">Aggregated vehicle count grouped by dominant category.</span></span>
|
| 391 |
</h3>
|
| 392 |
+
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Dominant
|
| 393 |
+
Intelligence</span>
|
| 394 |
</div>
|
| 395 |
+
<div class="flex-1 w-full relative min-h-[220px]">
|
| 396 |
<canvas id="dominanceChart"></canvas>
|
| 397 |
</div>
|
| 398 |
</div>
|
| 399 |
|
| 400 |
<!-- Traffic Flow Over Time -->
|
| 401 |
+
<div
|
| 402 |
+
class="col-span-12 xl:col-span-6 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col min-h-0">
|
| 403 |
<div class="flex justify-between items-center mb-2 relative">
|
| 404 |
+
<h3 class="font-bold text-slate-900 text-sm flex items-center">Temporal Traffic Flow
|
| 405 |
<span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 406 |
+
<span class="info-tip">Number of vehicles analyzed within the spatial boundary, binned per
|
| 407 |
+
second of video duration.</span></span>
|
| 408 |
</h3>
|
| 409 |
+
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Units / Second</span>
|
| 410 |
</div>
|
| 411 |
+
<div class="flex-1 w-full relative min-h-[220px]">
|
| 412 |
<canvas id="flowChart"></canvas>
|
| 413 |
</div>
|
| 414 |
</div>
|
| 415 |
|
| 416 |
</div>
|
| 417 |
|
| 418 |
+
<!-- TAB: Run -->
|
| 419 |
<div id="tab-run-details" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 420 |
+
<div class="space-y-6 w-full max-w-[1400px]">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
+
<!-- HERO: Process Analytics -->
|
| 423 |
+
<div id="run-results-card"
|
| 424 |
+
class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
|
| 425 |
+
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
|
| 426 |
+
<div>
|
| 427 |
+
<h3 class="font-bold text-slate-800 text-sm">Process Analytics</h3>
|
| 428 |
+
<p
|
| 429 |
+
class="text-[10px] text-slate-400 mt-0.5 uppercase tracking-widest font-medium text-center">
|
| 430 |
+
Execution Telemetry</p>
|
| 431 |
+
</div>
|
| 432 |
+
<div id="results-status-badge"
|
| 433 |
+
class="px-2.5 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold rounded-full uppercase tracking-tighter">
|
| 434 |
+
Awaiting Execution
|
| 435 |
+
</div>
|
| 436 |
</div>
|
| 437 |
+
<div class="p-8">
|
| 438 |
+
<div id="run-results-content" class="grid grid-cols-3 gap-12">
|
| 439 |
+
<div
|
| 440 |
+
class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-slate-100 rounded-2xl col-span-3 text-slate-300">
|
| 441 |
+
<i class="fa-solid fa-chart-line text-4xl mb-4"></i>
|
| 442 |
+
<span class="text-xs font-semibold">Initiate a run to view performance insights</span>
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
</div>
|
|
|
|
| 446 |
</div>
|
| 447 |
|
| 448 |
+
<!-- Technical Context Row -->
|
| 449 |
+
<div class="grid grid-cols-3 gap-6">
|
| 450 |
+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 451 |
+
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
|
| 452 |
+
<h3 class="font-bold text-slate-800 text-sm">Stream Source Profile</h3>
|
| 453 |
+
</div>
|
| 454 |
+
<div class="p-6 space-y-4" id="panel-video"></div>
|
| 455 |
</div>
|
| 456 |
+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 457 |
+
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
|
| 458 |
+
<h3 class="font-bold text-slate-800 text-sm">System Resource Utilization</h3>
|
| 459 |
+
</div>
|
| 460 |
+
<div class="p-6 space-y-4" id="panel-perf"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
</div>
|
| 462 |
+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 463 |
+
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
|
| 464 |
+
<h3 class="font-bold text-slate-800 text-sm">Model Architecture & Logic</h3>
|
| 465 |
+
</div>
|
| 466 |
+
<div class="p-6 space-y-4" id="panel-model"></div>
|
| 467 |
</div>
|
| 468 |
</div>
|
| 469 |
+
|
| 470 |
</div>
|
| 471 |
</div>
|
| 472 |
|
| 473 |
+
|
| 474 |
+
|
| 475 |
<!-- TAB: Reports -->
|
| 476 |
<div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 477 |
<div id="reports-pending" class="flex flex-col items-center justify-center h-full">
|
|
|
|
| 481 |
<div id="reports-grid" class="hidden grid grid-cols-2 xl:grid-cols-3 gap-4"></div>
|
| 482 |
</div>
|
| 483 |
|
| 484 |
+
<!-- TAB: Settings -->
|
| 485 |
+
<div id="tab-settings" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 486 |
+
<div class="grid grid-cols-3 gap-6 w-full">
|
| 487 |
+
|
| 488 |
+
<!-- Processing Parameters -->
|
| 489 |
+
<div class="col-span-2 bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
| 490 |
+
<div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
|
| 491 |
+
<h3 class="font-bold text-slate-800 text-sm flex items-center">Inference Configuration Profile
|
| 492 |
+
<span class="info-wrap">
|
| 493 |
+
<span class="info-btn"><i class="fa-solid fa-info"></i></span>
|
| 494 |
+
<span class="info-tip">Figures and numbers below are auto-calculated for best
|
| 495 |
+
performance.</span>
|
| 496 |
+
</span>
|
| 497 |
+
</h3>
|
| 498 |
+
<p class="text-[10px] text-slate-400 mt-0.1 uppercase tracking-widest font-medium">
|
| 499 |
+
Auto-configured pipeline parameters</p>
|
| 500 |
+
</div>
|
| 501 |
+
<div class="px-6 py-2" id="settings-params">
|
| 502 |
+
<div class="s-row" data-param="imgsz">
|
| 503 |
+
<div>
|
| 504 |
+
<div class="text-xs font-semibold text-slate-700">Image Size</div>
|
| 505 |
+
<div class="text-[10px] text-slate-400">Model input resolution</div>
|
| 506 |
+
</div>
|
| 507 |
+
<div class="s-stepper"><button onclick="stepParam('imgsz',-32)">‹</button><span
|
| 508 |
+
class="s-val" id="sv-imgsz">640</span><button
|
| 509 |
+
onclick="stepParam('imgsz',32)">›</button></div>
|
| 510 |
+
</div>
|
| 511 |
+
<div class="s-row" data-param="conf">
|
| 512 |
+
<div>
|
| 513 |
+
<div class="text-xs font-semibold text-slate-700">Confidence</div>
|
| 514 |
+
<div class="text-[10px] text-slate-400">Minimum detection threshold</div>
|
| 515 |
+
</div>
|
| 516 |
+
<div class="s-stepper"><button onclick="stepParam('conf',-0.01)">‹</button><span
|
| 517 |
+
class="s-val" id="sv-conf">0.12</span><button
|
| 518 |
+
onclick="stepParam('conf',0.01)">›</button></div>
|
| 519 |
+
</div>
|
| 520 |
+
<div class="s-row" data-param="iou">
|
| 521 |
+
<div>
|
| 522 |
+
<div class="text-xs font-semibold text-slate-700">IoU Threshold</div>
|
| 523 |
+
<div class="text-[10px] text-slate-400">Non-max suppression overlap</div>
|
| 524 |
+
</div>
|
| 525 |
+
<div class="s-stepper"><button onclick="stepParam('iou',-0.05)">‹</button><span
|
| 526 |
+
class="s-val" id="sv-iou">0.60</span><button
|
| 527 |
+
onclick="stepParam('iou',0.05)">›</button></div>
|
| 528 |
+
</div>
|
| 529 |
+
<div class="s-row" data-param="stride">
|
| 530 |
+
<div>
|
| 531 |
+
<div class="text-xs font-semibold text-slate-700">Frame Stride</div>
|
| 532 |
+
<div class="text-[10px] text-slate-400">Frames skipped between detections</div>
|
| 533 |
+
</div>
|
| 534 |
+
<div class="s-stepper"><button onclick="stepParam('stride',-1)">‹</button><span
|
| 535 |
+
class="s-val" id="sv-stride">2</span><button
|
| 536 |
+
onclick="stepParam('stride',1)">›</button></div>
|
| 537 |
+
</div>
|
| 538 |
+
<div class="s-row" data-param="report">
|
| 539 |
+
<div>
|
| 540 |
+
<div class="text-xs font-semibold text-slate-700">Report Format</div>
|
| 541 |
+
<div class="text-[10px] text-slate-400">Export file type for charts</div>
|
| 542 |
+
</div>
|
| 543 |
+
<select class="custom-select" id="sv-report">
|
| 544 |
+
<option value="png" selected>PNG Image</option>
|
| 545 |
+
<option value="pdf">PDF Document</option>
|
| 546 |
+
</select>
|
| 547 |
+
</div>
|
| 548 |
+
<div class="s-row" data-param="annotated">
|
| 549 |
+
<div>
|
| 550 |
+
<div class="text-xs font-semibold text-slate-700">Export Annotated Video</div>
|
| 551 |
+
<div class="text-[10px] text-slate-400">Bounding boxes + track IDs + spatial boundary
|
| 552 |
+
overlays</div>
|
| 553 |
+
</div>
|
| 554 |
+
<div class="toggle-track" id="sv-annotated" onclick="this.classList.toggle('active')">
|
| 555 |
+
<div class="toggle-thumb"></div>
|
| 556 |
+
</div>
|
| 557 |
+
</div>
|
| 558 |
+
</div>
|
| 559 |
+
</div>
|
| 560 |
+
|
| 561 |
+
<!-- Display Preferences -->
|
| 562 |
+
<div
|
| 563 |
+
class="col-span-1 bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
|
| 564 |
+
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
|
| 565 |
+
<h3 class="font-bold text-slate-800 text-sm">Display Preferences</h3>
|
| 566 |
+
</div>
|
| 567 |
+
<div class="px-5 py-4 flex-1 flex flex-col">
|
| 568 |
+
<div class="s-row">
|
| 569 |
+
<div>
|
| 570 |
+
<div class="text-xs font-semibold text-slate-700">Chart Color Palette</div>
|
| 571 |
+
<div class="text-[10px] text-slate-400">Color scheme for all charts</div>
|
| 572 |
+
</div>
|
| 573 |
+
<select class="custom-select" id="live-palette-select" onchange="applyPalette(this.value)">
|
| 574 |
+
<option value="default">Default</option>
|
| 575 |
+
<option value="vibrant">Vibrant</option>
|
| 576 |
+
<option value="corporate">Corporate</option>
|
| 577 |
+
</select>
|
| 578 |
+
</div>
|
| 579 |
+
<div class="mt-3">
|
| 580 |
+
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2">Palette
|
| 581 |
+
Preview</div>
|
| 582 |
+
<div class="flex gap-1.5" id="live-palette-preview"></div>
|
| 583 |
+
</div>
|
| 584 |
+
</div>
|
| 585 |
+
</div>
|
| 586 |
+
|
| 587 |
+
<!-- Start Button -->
|
| 588 |
+
<div class="col-span-3 pb-4" id="settings-start-wrap">
|
| 589 |
+
<button id="btn-start-processing" onclick="startProcessingFromSettings()"
|
| 590 |
+
class="w-full py-4 bg-slate-900 text-white font-medium text-sm rounded-full hover:bg-slate-800 active:scale-[0.99] transition flex items-center justify-center gap-2 shadow-lg">
|
| 591 |
+
<span>Process →</span>
|
| 592 |
+
</button>
|
| 593 |
+
</div>
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
</div>
|
| 598 |
+
</div>
|
| 599 |
+
|
| 600 |
</main>
|
| 601 |
|
| 602 |
<script>
|
|
|
|
| 619 |
|
| 620 |
// =========== Tab switching ===========
|
| 621 |
function switchTab(tab) {
|
| 622 |
+
['about', 'overview', 'run-details', 'reports', 'settings'].forEach(t => {
|
| 623 |
+
const el = document.getElementById('tab-' + t);
|
| 624 |
+
const nav = document.getElementById('nav-' + t);
|
| 625 |
+
if (el) el.classList.toggle('hidden', tab !== t);
|
| 626 |
+
if (nav) {
|
| 627 |
+
if (tab === t) {
|
| 628 |
+
nav.classList.add('bg-slate-900', 'text-white', 'shadow-md');
|
| 629 |
+
nav.classList.remove('text-slate-600', 'hover:bg-slate-50', 'hover:text-slate-900');
|
| 630 |
+
} else {
|
| 631 |
+
nav.classList.remove('bg-slate-900', 'text-white', 'shadow-md');
|
| 632 |
+
nav.classList.add('text-slate-600', 'hover:bg-slate-50', 'hover:text-slate-900');
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
});
|
| 636 |
}
|
| 637 |
|
| 638 |
// =========== Run Details helpers ===========
|
|
|
|
| 690 |
document.getElementById('panel-model').innerHTML =
|
| 691 |
`<div class="flex justify-between items-center border-b border-slate-50 pb-2">
|
| 692 |
<span class="text-xs font-medium text-slate-500 mono-font">model</span>
|
| 693 |
+
<a href="https://huggingface.co/Perception365/VehicleNet-Y26s" target="_blank" class="text-sm font-bold text-slate-800 mono-font hover:text-slate-600 transition underline underline-offset-4 decoration-slate-200">Perception365/VehicleNet-Y26s</a>
|
| 694 |
</div>` +
|
| 695 |
detailRow('task', 'detect') +
|
| 696 |
detailRow('format', 'OpenVINO') +
|
|
|
|
| 698 |
|
| 699 |
document.getElementById('panel-infer').innerHTML =
|
| 700 |
infoRow('imgsz', c.imgsz, 'Input image resolution for model inference.') +
|
| 701 |
+
infoRow('vid_stride', c.detect_stride, 'Frames skipped between consecutive detections relative to the spatial boundary.') +
|
| 702 |
infoRow('conf', c.conf || 0.12, 'Minimum confidence threshold for valid detections.') +
|
| 703 |
infoRow('iou', c.iou || 0.60, 'Intersection-over-Union threshold for non-max suppression.') +
|
| 704 |
infoRow('stream', 'TRUE', 'Frame-by-frame processing for constant memory usage.') +
|
| 705 |
infoRow('verbose', 'FALSE', 'Console logging suppressed during inference.');
|
| 706 |
}
|
| 707 |
|
| 708 |
+
// =========== Palettes ===========
|
| 709 |
+
const PALETTES = {
|
| 710 |
+
default: { congestion: '#f97316', congestionBg: 'rgba(249,115,22,0.08)', dominance: '#14b8a6', flow: '#3b82f6', doughIn: '#3b82f6', doughOut: '#f97316' },
|
| 711 |
+
vibrant: { congestion: '#e11d48', congestionBg: 'rgba(225,29,72,0.08)', dominance: '#8b5cf6', flow: '#06b6d4', doughIn: '#06b6d4', doughOut: '#e11d48' },
|
| 712 |
+
corporate: { congestion: '#1e40af', congestionBg: 'rgba(30,64,175,0.08)', dominance: '#0d9488', flow: '#475569', doughIn: '#475569', doughOut: '#1e40af' }
|
| 713 |
+
};
|
| 714 |
+
|
| 715 |
+
// Read settings from sessionStorage (set by settings.html)
|
| 716 |
+
const rawRun = sessionStorage.getItem('funky_run');
|
| 717 |
+
const runSettings = rawRun ? (JSON.parse(rawRun).settings || {}) : {};
|
| 718 |
+
let currentPalette = runSettings.palette || 'default';
|
| 719 |
+
let activePalette = PALETTES[currentPalette];
|
| 720 |
+
|
| 721 |
// =========== Charts ===========
|
| 722 |
+
Chart.defaults.font.family = "'Montserrat', sans-serif";
|
| 723 |
Chart.defaults.color = '#64748b';
|
| 724 |
|
| 725 |
let MODEL_CLASSES = {};
|
|
|
|
| 727 |
|
| 728 |
const congChart = new Chart(document.getElementById('congestionChart').getContext('2d'), {
|
| 729 |
type: 'line',
|
| 730 |
+
data: {
|
| 731 |
+
labels: [], datasets: [{
|
| 732 |
+
data: [],
|
| 733 |
+
borderColor: activePalette.congestion,
|
| 734 |
+
backgroundColor: activePalette.congestionBg,
|
| 735 |
+
fill: true,
|
| 736 |
+
tension: 0.2,
|
| 737 |
+
borderWidth: 1.5,
|
| 738 |
+
pointRadius: 0
|
| 739 |
+
}]
|
| 740 |
+
},
|
| 741 |
options: {
|
| 742 |
responsive: true, maintainAspectRatio: false,
|
| 743 |
plugins: { legend: { display: false } },
|
|
|
|
| 749 |
}
|
| 750 |
});
|
| 751 |
|
| 752 |
+
const doughChart = new Chart(document.getElementById('doughnutChart').getContext('2d'), {
|
| 753 |
+
type: 'doughnut',
|
| 754 |
+
data: {
|
| 755 |
+
labels: ['Incoming', 'Outgoing'], datasets: [{
|
| 756 |
+
data: [0, 0],
|
| 757 |
+
backgroundColor: [activePalette.doughIn, activePalette.doughOut],
|
| 758 |
+
borderColor: '#ffffff',
|
| 759 |
+
borderWidth: 3,
|
| 760 |
+
hoverOffset: 6
|
| 761 |
+
}]
|
| 762 |
+
},
|
| 763 |
+
options: {
|
| 764 |
+
responsive: true, maintainAspectRatio: false,
|
| 765 |
+
cutout: '68%',
|
| 766 |
+
plugins: {
|
| 767 |
+
legend: { display: true, position: 'bottom', labels: { padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 10, weight: '600' } } }
|
| 768 |
+
},
|
| 769 |
+
animation: { duration: 0 }
|
| 770 |
+
}
|
| 771 |
+
});
|
| 772 |
+
|
| 773 |
const domChart = new Chart(document.getElementById('dominanceChart').getContext('2d'), {
|
| 774 |
type: 'bar',
|
| 775 |
+
data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.dominance, borderRadius: 2 }] },
|
| 776 |
options: {
|
| 777 |
responsive: true, maintainAspectRatio: false,
|
| 778 |
plugins: { legend: { display: false } },
|
|
|
|
| 786 |
|
| 787 |
const flowChart = new Chart(document.getElementById('flowChart').getContext('2d'), {
|
| 788 |
type: 'bar',
|
| 789 |
+
data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.flow, borderColor: '#ffffff', borderWidth: 1.5, barPercentage: 1.0, categoryPercentage: 1.0 }] },
|
| 790 |
options: {
|
| 791 |
responsive: true, maintainAspectRatio: false,
|
| 792 |
plugins: { legend: { display: false } },
|
|
|
|
| 801 |
// =========== Update functions ===========
|
| 802 |
function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
|
| 803 |
|
| 804 |
+
// =========== Live Palette Switching ===========
|
| 805 |
+
function applyPalette(key) {
|
| 806 |
+
activePalette = PALETTES[key] || PALETTES.default;
|
| 807 |
+
currentPalette = key;
|
| 808 |
+
|
| 809 |
+
// Congestion
|
| 810 |
+
congChart.data.datasets[0].borderColor = activePalette.congestion;
|
| 811 |
+
congChart.data.datasets[0].backgroundColor = activePalette.congestionBg;
|
| 812 |
+
congChart.update();
|
| 813 |
+
|
| 814 |
+
// Doughnut
|
| 815 |
+
doughChart.data.datasets[0].backgroundColor = [activePalette.doughIn, activePalette.doughOut];
|
| 816 |
+
doughChart.update();
|
| 817 |
+
|
| 818 |
+
// Dominance
|
| 819 |
+
domChart.data.datasets[0].backgroundColor = activePalette.dominance;
|
| 820 |
+
domChart.update();
|
| 821 |
+
|
| 822 |
+
// Flow
|
| 823 |
+
flowChart.data.datasets[0].backgroundColor = activePalette.flow;
|
| 824 |
+
flowChart.update();
|
| 825 |
+
|
| 826 |
+
// Update palette preview
|
| 827 |
+
renderPalettePreview(key);
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
function renderPalettePreview(key) {
|
| 831 |
+
const pal = PALETTES[key];
|
| 832 |
+
const colors = [
|
| 833 |
+
{ color: pal.congestion, label: 'Congestion' },
|
| 834 |
+
{ color: pal.dominance, label: 'Dominance' },
|
| 835 |
+
{ color: pal.flow, label: 'Flow' },
|
| 836 |
+
{ color: pal.doughIn, label: 'Incoming' },
|
| 837 |
+
{ color: pal.doughOut, label: 'Outgoing' }
|
| 838 |
+
];
|
| 839 |
+
const el = document.getElementById('live-palette-preview');
|
| 840 |
+
if (el) {
|
| 841 |
+
el.innerHTML = colors.map(c =>
|
| 842 |
+
`<div class="flex-1 rounded-lg overflow-hidden border border-slate-100">
|
| 843 |
+
<div class="h-6" style="background:${c.color}"></div>
|
| 844 |
+
<div class="text-[8px] font-bold text-slate-400 text-center py-1 bg-slate-50">${c.label}</div>
|
| 845 |
+
</div>`
|
| 846 |
+
).join('');
|
| 847 |
+
}
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
function populateSettingsTab(config, settings) {
|
| 851 |
+
// Populate stepper values from config
|
| 852 |
+
document.getElementById('sv-imgsz').textContent = config.imgsz || 640;
|
| 853 |
+
document.getElementById('sv-conf').textContent = (config.conf || 0.12).toFixed(2);
|
| 854 |
+
document.getElementById('sv-iou').textContent = (config.iou || 0.60).toFixed(2);
|
| 855 |
+
document.getElementById('sv-stride').textContent = config.detect_stride || 2;
|
| 856 |
+
|
| 857 |
+
// Populate export settings
|
| 858 |
+
const selReport = document.getElementById('sv-report');
|
| 859 |
+
if (selReport) selReport.value = settings.reportFormat || 'png';
|
| 860 |
+
const togAnnot = document.getElementById('sv-annotated');
|
| 861 |
+
if (togAnnot && settings.annotatedVideo) togAnnot.classList.add('active');
|
| 862 |
+
|
| 863 |
+
// Set live palette dropdown
|
| 864 |
+
const sel = document.getElementById('live-palette-select');
|
| 865 |
+
if (sel) sel.value = currentPalette;
|
| 866 |
+
renderPalettePreview(currentPalette);
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
// =========== Settings Stepper Logic ===========
|
| 870 |
+
const PARAM_LIMITS = {
|
| 871 |
+
imgsz: { min: 640, max: 1280 },
|
| 872 |
+
conf: { min: 0.10, max: 0.95 },
|
| 873 |
+
iou: { min: 0.50, max: 0.95 },
|
| 874 |
+
stride: { min: 1, max: 10 }
|
| 875 |
+
};
|
| 876 |
+
|
| 877 |
+
function stepParam(param, delta) {
|
| 878 |
+
const el = document.getElementById('sv-' + param);
|
| 879 |
+
if (!el) return;
|
| 880 |
+
const limits = PARAM_LIMITS[param];
|
| 881 |
+
let val = parseFloat(el.textContent);
|
| 882 |
+
val = Math.round((val + delta) * 100) / 100;
|
| 883 |
+
val = Math.max(limits.min, Math.min(limits.max, val));
|
| 884 |
+
el.textContent = (param === 'conf' || param === 'iou') ? val.toFixed(2) : val;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
function lockSettings() {
|
| 888 |
+
// Disable critical param rows
|
| 889 |
+
document.querySelectorAll('#settings-params .s-row').forEach(row => {
|
| 890 |
+
const p = row.dataset.param;
|
| 891 |
+
if (p && p !== 'palette') row.classList.add('disabled');
|
| 892 |
+
});
|
| 893 |
+
// Hide start button
|
| 894 |
+
const wrap = document.getElementById('settings-start-wrap');
|
| 895 |
+
if (wrap) wrap.style.display = 'none';
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
function updateBreakdown(classIn, classOut) {
|
| 899 |
const container = document.getElementById('class-breakdown');
|
| 900 |
const totalAll = sumValues(classIn) + sumValues(classOut);
|
| 901 |
container.innerHTML = '';
|
| 902 |
|
| 903 |
+
// Update Doughnut border logic (remove gap if unidirectional)
|
| 904 |
+
const sumIn = sumValues(classIn);
|
| 905 |
+
const sumOut = sumValues(classOut);
|
| 906 |
+
doughChart.data.datasets[0].borderWidth = (sumIn === 0 || sumOut === 0) ? 0 : 3;
|
| 907 |
+
doughChart.data.datasets[0].data = [sumIn, sumOut];
|
| 908 |
+
doughChart.update();
|
| 909 |
+
document.getElementById('cnt-total').innerText = totalAll;
|
| 910 |
+
|
| 911 |
Object.keys(MODEL_CLASSES).map(Number).sort((a, b) => a - b).forEach(id => {
|
| 912 |
const inC = classIn[String(id)] || 0;
|
| 913 |
const outC = classOut[String(id)] || 0;
|
|
|
|
| 967 |
}
|
| 968 |
|
| 969 |
// =========== Main ===========
|
| 970 |
+
let _params = null;
|
| 971 |
+
|
| 972 |
async function init() {
|
| 973 |
const raw = sessionStorage.getItem('funky_run');
|
| 974 |
if (!raw) { window.location.href = '/'; return; }
|
| 975 |
|
| 976 |
+
_params = JSON.parse(raw);
|
| 977 |
+
|
| 978 |
+
// SECURITY: Clear session storage so refresh always redirects home
|
| 979 |
+
sessionStorage.removeItem('funky_run');
|
| 980 |
|
| 981 |
const cRes = await fetch('/constants');
|
| 982 |
const cData = await cRes.json();
|
| 983 |
MODEL_CLASSES = cData.classes;
|
| 984 |
BUSINESS_MAP = cData.business_map;
|
| 985 |
|
| 986 |
+
populateAndInit(_params);
|
| 987 |
+
|
| 988 |
+
// SECURITY: Clear session storage after populate so refresh triggers redirect
|
| 989 |
+
sessionStorage.removeItem('funky_run');
|
| 990 |
+
|
| 991 |
+
// Show Settings tab first, but also initialized with About implicitly in sidebar hierarchy
|
| 992 |
+
switchTab('settings');
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
function populateAndInit(params) {
|
| 996 |
populateRunDetails(params.config);
|
| 997 |
+
populateSettingsTab(params.config, params.settings || {});
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
function startProcessingFromSettings() {
|
| 1001 |
+
if (!_params) return;
|
| 1002 |
+
|
| 1003 |
+
// Read current stepper/control values
|
| 1004 |
+
const imgsz = parseInt(document.getElementById('sv-imgsz').textContent);
|
| 1005 |
+
const conf = parseFloat(document.getElementById('sv-conf').textContent);
|
| 1006 |
+
const iou = parseFloat(document.getElementById('sv-iou').textContent);
|
| 1007 |
+
const stride = parseInt(document.getElementById('sv-stride').textContent);
|
| 1008 |
+
const reportFmt = document.getElementById('sv-report').value;
|
| 1009 |
+
const annotated = document.getElementById('sv-annotated').classList.contains('active');
|
| 1010 |
+
|
| 1011 |
+
// Apply to config
|
| 1012 |
+
_params.config.imgsz = imgsz;
|
| 1013 |
+
_params.config.conf = conf;
|
| 1014 |
+
_params.config.iou = iou;
|
| 1015 |
+
_params.config.detect_stride = stride;
|
| 1016 |
+
|
| 1017 |
+
// Lock settings
|
| 1018 |
+
lockSettings();
|
| 1019 |
+
|
| 1020 |
+
// Switch to overview
|
| 1021 |
+
switchTab('overview');
|
| 1022 |
+
document.getElementById('proc-label').innerText = 'Processing';
|
| 1023 |
+
|
| 1024 |
+
// Reset Run Tab Results to Awaiting
|
| 1025 |
+
const badge = document.getElementById('results-status-badge');
|
| 1026 |
+
if (badge) {
|
| 1027 |
+
badge.innerText = 'Processing';
|
| 1028 |
+
badge.className = 'px-2.5 py-1 bg-blue-100 text-blue-700 text-[10px] font-bold rounded-full uppercase tracking-tighter animate-pulse';
|
| 1029 |
+
}
|
| 1030 |
+
document.getElementById('run-results-content').innerHTML = `
|
| 1031 |
+
<div class="flex flex-col items-center justify-center p-8 bg-slate-50/50 border border-slate-100 rounded-2xl col-span-3 text-slate-400">
|
| 1032 |
+
<i class="fa-solid fa-spinner fa-spin text-2xl mb-3"></i>
|
| 1033 |
+
<span class="text-xs font-semibold">Executing inference pipeline... results pending</span>
|
| 1034 |
+
</div>`;
|
| 1035 |
|
| 1036 |
+
// Start WebSocket
|
| 1037 |
+
const videoDuration = _params.config.duration || 10;
|
| 1038 |
|
| 1039 |
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
| 1040 |
const ws = new WebSocket(`${proto}://${location.host}/ws/run`);
|
| 1041 |
|
| 1042 |
ws.onopen = () => {
|
| 1043 |
ws.send(JSON.stringify({
|
| 1044 |
+
video_id: _params.video_id,
|
| 1045 |
+
line: _params.line,
|
| 1046 |
+
config: _params.config,
|
| 1047 |
+
annotated_video: annotated,
|
| 1048 |
+
report_format: reportFmt
|
| 1049 |
}));
|
| 1050 |
};
|
| 1051 |
|
|
|
|
| 1059 |
document.getElementById('proc-bar').style.width = '100%';
|
| 1060 |
document.getElementById('proc-pct').innerText = '100%';
|
| 1061 |
|
| 1062 |
+
// Update Run Tab Badge
|
| 1063 |
+
const badge = document.getElementById('results-status-badge');
|
| 1064 |
+
if (badge) {
|
| 1065 |
+
badge.innerText = 'Completed';
|
| 1066 |
+
badge.className = 'px-2.5 py-1 bg-green-100 text-green-700 text-[10px] font-bold rounded-full uppercase tracking-tighter';
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
document.getElementById('run-results-content').innerHTML =
|
| 1070 |
+
detailRow('Inference Time', d.processing_time + ' sec') +
|
| 1071 |
+
infoRow('Throughput (FPS)', d.actual_fps, 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
|
| 1072 |
+
infoRow('Real-time Ratio', d.speed_vs_realtime + 'x', 'Processing speed relative to video playback rate.');
|
| 1073 |
|
| 1074 |
if (d.video_id) loadReports(d.video_id);
|
| 1075 |
return;
|
|
|
|
| 1083 |
const totalIn = sumValues(d.class_in);
|
| 1084 |
const totalOut = sumValues(d.class_out);
|
| 1085 |
document.getElementById('cnt-total').innerText = totalIn + totalOut;
|
| 1086 |
+
|
| 1087 |
+
// Update doughnut
|
| 1088 |
+
doughChart.data.datasets[0].data = [totalIn, totalOut];
|
| 1089 |
+
doughChart.update();
|
| 1090 |
|
| 1091 |
const now = performance.now();
|
| 1092 |
if (now - lastUIUpdate < 300) return;
|
|
|
|
| 1101 |
|
| 1102 |
const REPORT_LABELS = {
|
| 1103 |
'direction_pie.png': { title: 'Directional Split', desc: 'Incoming vs outgoing vehicle ratio' },
|
| 1104 |
+
'direction_pie.pdf': { title: 'Directional Split', desc: 'Incoming vs outgoing vehicle ratio' },
|
| 1105 |
'flow_over_time.png': { title: 'Traffic Flow', desc: 'Vehicles crossing the line over time' },
|
| 1106 |
+
'flow_over_time.pdf': { title: 'Traffic Flow', desc: 'Vehicles crossing the line over time' },
|
| 1107 |
'congestion_index.png': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
|
| 1108 |
+
'congestion_index.pdf': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
|
| 1109 |
'class_dominance.png': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
|
| 1110 |
+
'class_dominance.pdf': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
|
| 1111 |
'confidence_dist.png': { title: 'Confidence Distribution', desc: 'Detection confidence histogram' },
|
| 1112 |
+
'confidence_dist.pdf': { title: 'Confidence Distribution', desc: 'Detection confidence histogram' },
|
| 1113 |
+
'annotated.mp4': { title: 'Annotated Video', desc: 'Video with bounding boxes, track IDs, and spatial boundary overlays' },
|
| 1114 |
};
|
| 1115 |
|
| 1116 |
async function loadReports(videoId) {
|
|
|
|
| 1126 |
data.files.forEach(name => {
|
| 1127 |
const info = REPORT_LABELS[name] || { title: name, desc: '' };
|
| 1128 |
const url = `/reports/${videoId}/${name}`;
|
| 1129 |
+
const isVideo = name.endsWith('.mp4');
|
| 1130 |
+
const isPDF = name.endsWith('.pdf');
|
| 1131 |
const card = document.createElement('div');
|
| 1132 |
card.className = 'bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden';
|
| 1133 |
+
|
| 1134 |
+
let previewHTML = '';
|
| 1135 |
+
if (isVideo) {
|
| 1136 |
+
previewHTML = `
|
| 1137 |
+
<div class="flex flex-col items-center justify-center py-12 text-slate-300">
|
| 1138 |
+
<i class="fa-solid fa-film text-6xl mb-4"></i>
|
| 1139 |
+
<span class="text-xs font-bold uppercase tracking-widest text-slate-400">Video Ready for Local Analysis</span>
|
| 1140 |
+
</div>`;
|
| 1141 |
+
} else if (isPDF) {
|
| 1142 |
+
previewHTML = `
|
| 1143 |
+
<div class="flex flex-col items-center justify-center py-12 text-slate-300">
|
| 1144 |
+
<i class="fa-solid fa-file-pdf text-6xl mb-4"></i>
|
| 1145 |
+
<span class="text-xs font-bold uppercase tracking-widest text-slate-400">PDF Document</span>
|
| 1146 |
+
</div>`;
|
| 1147 |
+
} else {
|
| 1148 |
+
previewHTML = `<img src="${url}" alt="${info.title}" class="max-w-full max-h-[320px] object-contain rounded">`;
|
| 1149 |
+
}
|
| 1150 |
+
|
| 1151 |
card.innerHTML = `
|
| 1152 |
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
|
| 1153 |
<div>
|
|
|
|
| 1160 |
</a>
|
| 1161 |
</div>
|
| 1162 |
<div class="p-4 flex items-center justify-center bg-slate-50/30">
|
| 1163 |
+
${previewHTML}
|
| 1164 |
</div>
|
| 1165 |
`;
|
| 1166 |
grid.appendChild(card);
|