Spaces:
Running
Running
png xet enabled
Browse files- .gitattributes +1 -0
- .gitignore +1 -0
- Dockerfile +20 -0
- README.md +36 -1
- backend/config.py +74 -0
- backend/constants.py +14 -0
- backend/engine.py +120 -0
- backend/model.py +33 -0
- backend/server.py +120 -0
- frontend/initial.html +447 -0
- frontend/run_details.html +8 -0
- frontend/uf.svg +0 -0
- frontend/uf_logo.png +3 -0
- frontend/vehicles.html +510 -0
- requirements.txt +15 -0
.gitattributes
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.png filter=xet diff=xet merge=xet -text
|
| 4 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 6 |
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
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 .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
COPY backend/ ./backend/
|
| 15 |
+
COPY frontend/ ./frontend/
|
| 16 |
+
COPY .env .env
|
| 17 |
+
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
CMD ["python", "backend/server.py"]
|
README.md
CHANGED
|
@@ -10,4 +10,39 @@ short_description: Monitoring Made Easy
|
|
| 10 |
thumbnail: >-
|
| 11 |
https://cdn-uploads.huggingface.co/production/uploads/66c6048d0bf40704e4159a23/2EUWmy9YzOM4eHft6E04y.png
|
| 12 |
---
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
thumbnail: >-
|
| 11 |
https://cdn-uploads.huggingface.co/production/uploads/66c6048d0bf40704e4159a23/2EUWmy9YzOM4eHft6E04y.png
|
| 12 |
---
|
| 13 |
+
|
| 14 |
+
# UrbanFlow 🚛
|
| 15 |
+
Monitoring made Easy with Computer Vision
|
| 16 |
+
|
| 17 |
+
Full-stack traffic analytics dashboard. YOLO + ByteTrack backend processes uploaded videos, counts vehicles crossing a user-drawn line, and streams results to the browser in real time via WebSocket.
|
| 18 |
+
|
| 19 |
+
## Stack
|
| 20 |
+
|
| 21 |
+
- **Backend**: FastAPI, Ultralytics YOLO (ONNX), ByteTrack, OpenCV
|
| 22 |
+
- **Frontend**: Vanilla HTML/JS, TailwindCSS CDN, Chart.js
|
| 23 |
+
- **Infra**: Docker, CPU-only inference
|
| 24 |
+
|
| 25 |
+
## Run with Docker
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
docker build -t funky .
|
| 29 |
+
docker run -p 7860:7860 funky
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Open `http://localhost:7860`
|
| 33 |
+
|
| 34 |
+
## Run Locally
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
pip install -r requirements.txt
|
| 38 |
+
python backend/server.py
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## Flow
|
| 42 |
+
|
| 43 |
+
1. Select Traffic Analytics module
|
| 44 |
+
2. Upload video file
|
| 45 |
+
3. System auto-calculates optimal inference settings (adjustable)
|
| 46 |
+
4. Draw counting line on first frame
|
| 47 |
+
5. Engine processes frames, dashboard updates in real time
|
| 48 |
+
6. View run details and live analytics
|
backend/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import multiprocessing as mp
|
| 3 |
+
|
| 4 |
+
BASE_IMG_SIZE = 640
|
| 5 |
+
REF_PIXELS = 640 * 640
|
| 6 |
+
REF_FPS_CPU = 13.0
|
| 7 |
+
TRACK_STABILITY_STRIDE = 3
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _cpu_score():
|
| 11 |
+
return mp.cpu_count()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _video_meta(path):
|
| 15 |
+
cap = cv2.VideoCapture(path)
|
| 16 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 17 |
+
frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 18 |
+
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 19 |
+
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 20 |
+
cap.release()
|
| 21 |
+
|
| 22 |
+
duration = frames / fps if fps else 0
|
| 23 |
+
pixels = w * h
|
| 24 |
+
return fps, frames, duration, w, h, pixels
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _estimate_fps(imgsz, cpu_score):
|
| 28 |
+
scale = (imgsz * imgsz) / REF_PIXELS
|
| 29 |
+
return (REF_FPS_CPU * cpu_score / 12) / scale
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _select_imgsz(pixels):
|
| 33 |
+
if pixels >= 3840 * 2160:
|
| 34 |
+
return 640
|
| 35 |
+
if pixels >= 2560 * 1440:
|
| 36 |
+
return 704
|
| 37 |
+
if pixels >= 1920 * 1080:
|
| 38 |
+
return 736
|
| 39 |
+
if pixels >= 1280 * 720:
|
| 40 |
+
return 800
|
| 41 |
+
return 960
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _select_stride(video_fps, model_fps):
|
| 45 |
+
if model_fps >= video_fps:
|
| 46 |
+
return 1
|
| 47 |
+
ratio = video_fps / model_fps
|
| 48 |
+
stride = int(round(ratio))
|
| 49 |
+
return max(1, min(stride, TRACK_STABILITY_STRIDE))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_optimal_config(video_path):
|
| 53 |
+
fps, frames, duration, w, h, pixels = _video_meta(video_path)
|
| 54 |
+
cpu_score = _cpu_score()
|
| 55 |
+
|
| 56 |
+
imgsz = _select_imgsz(pixels)
|
| 57 |
+
model_fps = _estimate_fps(imgsz, cpu_score)
|
| 58 |
+
detect_stride = _select_stride(fps, model_fps)
|
| 59 |
+
effective_fps = model_fps / detect_stride
|
| 60 |
+
realtime_possible = effective_fps >= fps
|
| 61 |
+
|
| 62 |
+
return {
|
| 63 |
+
"video_fps": fps,
|
| 64 |
+
"frames": frames,
|
| 65 |
+
"duration": round(duration, 2),
|
| 66 |
+
"resolution": [w, h],
|
| 67 |
+
"pixels": pixels,
|
| 68 |
+
"cpu_score": cpu_score,
|
| 69 |
+
"imgsz": imgsz,
|
| 70 |
+
"detect_stride": detect_stride,
|
| 71 |
+
"model_fps_est": round(model_fps, 2),
|
| 72 |
+
"effective_fps_est": round(effective_fps, 2),
|
| 73 |
+
"realtime_possible": realtime_possible
|
| 74 |
+
}
|
backend/constants.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MODEL_CLASSES = {
|
| 2 |
+
0: "Hatchback", 1: "Sedan", 2: "SUV", 3: "MUV", 4: "Bus", 5: "Truck",
|
| 3 |
+
6: "Three-wheeler", 7: "Two-wheeler", 8: "LCV", 9: "Mini-bus",
|
| 4 |
+
10: "Tempo-traveller", 11: "Bicycle", 12: "Van", 13: "Others"
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
BUSINESS_MAP = {
|
| 8 |
+
"Cars": [0, 1, 2, 3, 12],
|
| 9 |
+
"Buses": [4, 9, 10],
|
| 10 |
+
"Two-wheelers": [7, 11],
|
| 11 |
+
"Three-wheelers": [6],
|
| 12 |
+
"Trucks": [5, 8],
|
| 13 |
+
"Others": [13]
|
| 14 |
+
}
|
backend/engine.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import numpy as np
|
| 3 |
+
import cv2
|
| 4 |
+
from collections import defaultdict
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _side(p, a, b):
|
| 8 |
+
return np.sign((b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]))
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _point_to_segment_dist(px, py, ax, ay, bx, by):
|
| 12 |
+
A = np.array([ax, ay], dtype=float)
|
| 13 |
+
B = np.array([bx, by], dtype=float)
|
| 14 |
+
P = np.array([px, py], dtype=float)
|
| 15 |
+
AB = B - A
|
| 16 |
+
t = np.clip(np.dot(P - A, AB) / np.dot(AB, AB), 0, 1)
|
| 17 |
+
return np.linalg.norm(P - (A + t * AB))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def run(model, video_path, line, config, on_frame):
|
| 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)
|
| 36 |
+
class_out = defaultdict(int)
|
| 37 |
+
congestion = []
|
| 38 |
+
flow_times = []
|
| 39 |
+
|
| 40 |
+
start = time.time()
|
| 41 |
+
|
| 42 |
+
results = model.track(
|
| 43 |
+
source=video_path,
|
| 44 |
+
tracker="bytetrack.yaml",
|
| 45 |
+
imgsz=config["imgsz"],
|
| 46 |
+
conf=config.get("conf", 0.12),
|
| 47 |
+
iou=config.get("iou", 0.6),
|
| 48 |
+
vid_stride=stride,
|
| 49 |
+
stream=True,
|
| 50 |
+
verbose=False,
|
| 51 |
+
persist=True
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
a = line[0]
|
| 55 |
+
b = line[1]
|
| 56 |
+
|
| 57 |
+
for frame_idx, r in enumerate(results):
|
| 58 |
+
active = 0
|
| 59 |
+
|
| 60 |
+
if r.boxes.id is not None:
|
| 61 |
+
ids = r.boxes.id.cpu().numpy()
|
| 62 |
+
cls = r.boxes.cls.cpu().numpy()
|
| 63 |
+
xyxy = r.boxes.xyxy.cpu().numpy()
|
| 64 |
+
|
| 65 |
+
active = len(ids)
|
| 66 |
+
|
| 67 |
+
for obj_id, c, box in zip(ids, cls, xyxy):
|
| 68 |
+
cx = int((box[0] + box[2]) / 2)
|
| 69 |
+
cy = int((box[1] + box[3]) / 2)
|
| 70 |
+
|
| 71 |
+
current = _side((cx, cy), a, b)
|
| 72 |
+
|
| 73 |
+
if obj_id in prev_side and obj_id not in counted_ids:
|
| 74 |
+
if prev_side[obj_id] != current:
|
| 75 |
+
dist = _point_to_segment_dist(cx, cy, a[0], a[1], b[0], b[1])
|
| 76 |
+
if dist < 12:
|
| 77 |
+
t = frame_idx * stride / fps
|
| 78 |
+
flow_times.append(round(t, 2))
|
| 79 |
+
|
| 80 |
+
if current > 0:
|
| 81 |
+
class_in[int(c)] += 1
|
| 82 |
+
else:
|
| 83 |
+
class_out[int(c)] += 1
|
| 84 |
+
|
| 85 |
+
counted_ids.add(obj_id)
|
| 86 |
+
|
| 87 |
+
prev_side[obj_id] = current
|
| 88 |
+
|
| 89 |
+
congestion.append(active)
|
| 90 |
+
|
| 91 |
+
elapsed = time.time() - start
|
| 92 |
+
|
| 93 |
+
update = {
|
| 94 |
+
"frame_index": frame_idx + 1,
|
| 95 |
+
"total_iters": total_iters,
|
| 96 |
+
"total_frames": total,
|
| 97 |
+
"active": active,
|
| 98 |
+
"congestion": congestion.copy(),
|
| 99 |
+
"class_in": {str(k): v for k, v in class_in.items()},
|
| 100 |
+
"class_out": {str(k): v for k, v in class_out.items()},
|
| 101 |
+
"flow_times": flow_times.copy(),
|
| 102 |
+
"elapsed": round(elapsed, 2),
|
| 103 |
+
"fps": round((frame_idx + 1) / elapsed, 2) if elapsed > 0 else 0,
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
on_frame(update)
|
| 107 |
+
|
| 108 |
+
processing_time = round(time.time() - start, 2)
|
| 109 |
+
actual_fps = round(config["frames"] / processing_time, 2) if processing_time > 0 else 0
|
| 110 |
+
speed_vs_rt = round(actual_fps / fps, 2) if fps > 0 else 0
|
| 111 |
+
|
| 112 |
+
return {
|
| 113 |
+
"class_in": dict(class_in),
|
| 114 |
+
"class_out": dict(class_out),
|
| 115 |
+
"congestion": congestion,
|
| 116 |
+
"flow_times": flow_times,
|
| 117 |
+
"processing_time": processing_time,
|
| 118 |
+
"actual_fps": actual_fps,
|
| 119 |
+
"speed_vs_realtime": speed_vs_rt,
|
| 120 |
+
}
|
backend/model.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from ultralytics import YOLO
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
MODEL_DIR = Path(__file__).parent / "weights"
|
| 9 |
+
PT_PATH = MODEL_DIR / "best.pt"
|
| 10 |
+
ONNX_PATH = MODEL_DIR / "best.onnx"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def ensure_onnx():
|
| 14 |
+
MODEL_DIR.mkdir(exist_ok=True)
|
| 15 |
+
|
| 16 |
+
if not PT_PATH.exists():
|
| 17 |
+
token = os.getenv("HF_TOKEN")
|
| 18 |
+
os.system(
|
| 19 |
+
f'curl -L -H "Authorization: Bearer {token}" '
|
| 20 |
+
f'-o {PT_PATH} '
|
| 21 |
+
f'https://huggingface.co/Perception365/VehicleNet-Y26s/resolve/main/weights/best.pt'
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
if not ONNX_PATH.exists():
|
| 25 |
+
YOLO(str(PT_PATH)).export(format="onnx", dynamic=True)
|
| 26 |
+
exported = PT_PATH.with_suffix(".onnx")
|
| 27 |
+
if exported != ONNX_PATH:
|
| 28 |
+
exported.rename(ONNX_PATH)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def load_model():
|
| 32 |
+
ensure_onnx()
|
| 33 |
+
return YOLO(str(ONNX_PATH), task="detect")
|
backend/server.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import uuid
|
| 4 |
+
import asyncio
|
| 5 |
+
import tempfile
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import cv2
|
| 9 |
+
from fastapi import FastAPI, WebSocket, UploadFile, File
|
| 10 |
+
from fastapi.responses import FileResponse, Response
|
| 11 |
+
from fastapi.staticfiles import StaticFiles
|
| 12 |
+
|
| 13 |
+
from model import load_model
|
| 14 |
+
from config import get_optimal_config
|
| 15 |
+
from engine import run
|
| 16 |
+
from constants import MODEL_CLASSES, BUSINESS_MAP
|
| 17 |
+
|
| 18 |
+
app = FastAPI()
|
| 19 |
+
|
| 20 |
+
BASE = Path(__file__).parent.parent
|
| 21 |
+
FRONTEND = BASE / "frontend"
|
| 22 |
+
UPLOAD_DIR = Path(tempfile.gettempdir()) / "funky_uploads"
|
| 23 |
+
UPLOAD_DIR.mkdir(exist_ok=True)
|
| 24 |
+
|
| 25 |
+
videos = {}
|
| 26 |
+
model = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@app.on_event("startup")
|
| 30 |
+
def startup():
|
| 31 |
+
global model
|
| 32 |
+
model = load_model()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@app.get("/")
|
| 36 |
+
def index():
|
| 37 |
+
return FileResponse(FRONTEND / "initial.html")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@app.post("/upload")
|
| 41 |
+
async def upload(file: UploadFile = File(...)):
|
| 42 |
+
video_id = str(uuid.uuid4())[:8]
|
| 43 |
+
path = UPLOAD_DIR / f"{video_id}.mp4"
|
| 44 |
+
with open(path, "wb") as f:
|
| 45 |
+
f.write(await file.read())
|
| 46 |
+
videos[video_id] = str(path)
|
| 47 |
+
return {"video_id": video_id}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@app.get("/config/{video_id}")
|
| 51 |
+
def config_endpoint(video_id: str):
|
| 52 |
+
path = videos.get(video_id)
|
| 53 |
+
cfg = get_optimal_config(path)
|
| 54 |
+
return cfg
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@app.get("/first-frame/{video_id}")
|
| 58 |
+
def first_frame(video_id: str):
|
| 59 |
+
path = videos.get(video_id)
|
| 60 |
+
cap = cv2.VideoCapture(path)
|
| 61 |
+
ret, frame = cap.read()
|
| 62 |
+
cap.release()
|
| 63 |
+
_, buf = cv2.imencode(".jpg", frame)
|
| 64 |
+
return Response(content=buf.tobytes(), media_type="image/jpeg")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@app.get("/constants")
|
| 68 |
+
def constants():
|
| 69 |
+
return {"classes": MODEL_CLASSES, "business_map": BUSINESS_MAP}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@app.websocket("/ws/run")
|
| 73 |
+
async def ws_run(ws: WebSocket):
|
| 74 |
+
await ws.accept()
|
| 75 |
+
data = json.loads(await ws.receive_text())
|
| 76 |
+
|
| 77 |
+
video_id = data["video_id"]
|
| 78 |
+
line = data["line"]
|
| 79 |
+
cfg = data["config"]
|
| 80 |
+
|
| 81 |
+
path = videos.get(video_id)
|
| 82 |
+
|
| 83 |
+
loop = asyncio.get_event_loop()
|
| 84 |
+
|
| 85 |
+
queue = asyncio.Queue()
|
| 86 |
+
|
| 87 |
+
def on_frame(update):
|
| 88 |
+
loop.call_soon_threadsafe(queue.put_nowait, update)
|
| 89 |
+
|
| 90 |
+
task = loop.run_in_executor(None, run, model, path, line, cfg, on_frame)
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
while True:
|
| 94 |
+
done = task.done()
|
| 95 |
+
while not queue.empty():
|
| 96 |
+
update = queue.get_nowait()
|
| 97 |
+
await ws.send_text(json.dumps(update))
|
| 98 |
+
|
| 99 |
+
if done:
|
| 100 |
+
break
|
| 101 |
+
|
| 102 |
+
await asyncio.sleep(0.05)
|
| 103 |
+
|
| 104 |
+
result = task.result()
|
| 105 |
+
await ws.send_text(json.dumps({
|
| 106 |
+
"done": True,
|
| 107 |
+
"processing_time": result["processing_time"],
|
| 108 |
+
"actual_fps": result["actual_fps"],
|
| 109 |
+
"speed_vs_realtime": result["speed_vs_realtime"],
|
| 110 |
+
}))
|
| 111 |
+
await ws.close()
|
| 112 |
+
except Exception:
|
| 113 |
+
pass
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
app.mount("/", StaticFiles(directory=str(FRONTEND)), name="frontend")
|
| 117 |
+
|
| 118 |
+
if __name__ == "__main__":
|
| 119 |
+
import uvicorn
|
| 120 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
frontend/initial.html
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 - Enterprise Setup</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&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 |
+
</style>
|
| 56 |
+
</head>
|
| 57 |
+
|
| 58 |
+
<body
|
| 59 |
+
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">
|
| 60 |
+
|
| 61 |
+
<div class="bg-glow"></div>
|
| 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
|
| 75 |
+
class="flex-1 w-full max-w-[90rem] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20 px-10 py-6 items-center z-10">
|
| 76 |
+
|
| 77 |
+
<div class="lg:col-span-7 flex flex-col justify-center xl:pl-10 pb-10 lg:pb-0">
|
| 78 |
+
<h1
|
| 79 |
+
class="text-5xl xl:text-[4.5rem] font-montserrat font-extrabold mb-4 leading-[1.1] text-slate-900 tracking-tight">
|
| 80 |
+
Automated <br>Vision Intelligence
|
| 81 |
+
</h1>
|
| 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 Deep Learning Inference
|
| 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
|
| 89 |
+
spatial detection and tracking</li>
|
| 90 |
+
<li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> Multi-class
|
| 91 |
+
object categorization</li>
|
| 92 |
+
<li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> Bidirectional
|
| 93 |
+
movement analysis</li>
|
| 94 |
+
<li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> High-performance
|
| 95 |
+
multi-object tracking</li>
|
| 96 |
+
<li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> Intelligent
|
| 97 |
+
processing optimization</li>
|
| 98 |
+
<li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> Comprehensive
|
| 99 |
+
analytics reporting</li>
|
| 100 |
+
</ul>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<div
|
| 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">Select AI Module</h2>
|
| 109 |
+
<p class="text-[13px] font-medium text-slate-400 mb-6">Choose an intelligence pipeline for your media
|
| 110 |
+
stream.</p>
|
| 111 |
+
|
| 112 |
+
<div class="grid grid-cols-2 gap-4">
|
| 113 |
+
<div onclick="showStep('upload')"
|
| 114 |
+
class="group relative bg-white border-2 border-slate-900 rounded-[1.5rem] p-6 cursor-pointer hover:shadow-2xl hover:-translate-y-1 transition-all duration-300">
|
| 115 |
+
<div
|
| 116 |
+
class="absolute top-4 right-4 bg-slate-900 text-white text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
|
| 117 |
+
Active</div>
|
| 118 |
+
<i class="fa-solid fa-car-side text-3xl text-slate-900 mb-4 block"></i>
|
| 119 |
+
<h3 class="font-montserrat font-bold text-sm mb-2 leading-tight">Traffic <br>Analytics</h3>
|
| 120 |
+
<p class="text-[10px] text-slate-500 font-medium">Detect, track, and analyze vehicles in
|
| 121 |
+
real-world environments using state-of-the-art vision models.</p>
|
| 122 |
+
</div>
|
| 123 |
+
<div
|
| 124 |
+
class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed">
|
| 125 |
+
<div
|
| 126 |
+
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">
|
| 127 |
+
Coming Soon</div>
|
| 128 |
+
<i class="fa-solid fa-layer-group text-3xl text-slate-300 mb-4 block"></i>
|
| 129 |
+
<h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Semantic
|
| 130 |
+
<br>Segmentation</h3>
|
| 131 |
+
<p class="text-[10px] text-slate-400 font-medium">Pixel-perfect instance segmentation for
|
| 132 |
+
complex spatial scene understanding.</p>
|
| 133 |
+
</div>
|
| 134 |
+
<div
|
| 135 |
+
class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed">
|
| 136 |
+
<div
|
| 137 |
+
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">
|
| 138 |
+
Coming Soon</div>
|
| 139 |
+
<i class="fa-solid fa-tags text-3xl text-slate-300 mb-4 block"></i>
|
| 140 |
+
<h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Image
|
| 141 |
+
<br>Classification</h3>
|
| 142 |
+
<p class="text-[10px] text-slate-400 font-medium">High-speed categorical labeling for vast and
|
| 143 |
+
diverse image datasets.</p>
|
| 144 |
+
</div>
|
| 145 |
+
<div
|
| 146 |
+
class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed">
|
| 147 |
+
<div
|
| 148 |
+
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">
|
| 149 |
+
Coming Soon</div>
|
| 150 |
+
<i class="fa-solid fa-expand text-3xl text-slate-300 mb-4 block"></i>
|
| 151 |
+
<h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Custom
|
| 152 |
+
<br>Detection</h3>
|
| 153 |
+
<p class="text-[10px] text-slate-400 font-medium">Deploy proprietary neural networks for highly
|
| 154 |
+
specialized edge inference.</p>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<!-- STEP: Upload -->
|
| 160 |
+
<div id="step-upload" class="hidden w-full flex flex-col fade-in justify-center">
|
| 161 |
+
<button onclick="showStep('modules')"
|
| 162 |
+
class="text-slate-400 hover:text-black transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
|
| 163 |
+
<i class="fa-solid fa-arrow-left mr-2"></i> Back to Modules
|
| 164 |
+
</button>
|
| 165 |
+
<h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900">Initialize Media Source</h2>
|
| 166 |
+
<p class="text-[13px] font-medium text-slate-400 mb-8">Provide the target video footage to configure the
|
| 167 |
+
Traffic Analytics pipeline.</p>
|
| 168 |
+
|
| 169 |
+
<div id="dropzone"
|
| 170 |
+
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">
|
| 171 |
+
<i
|
| 172 |
+
class="fa-solid fa-arrow-up-from-bracket text-4xl text-slate-300 group-hover:text-black transition mb-5"></i>
|
| 173 |
+
<span class="font-montserrat font-semibold text-slate-800 text-lg mb-2 text-center">Select or drop
|
| 174 |
+
media file to proceed</span>
|
| 175 |
+
<span class="text-[10px] font-bold uppercase tracking-widest text-slate-400 text-center">Accepted
|
| 176 |
+
formats: MP4, AVI, MOV</span>
|
| 177 |
+
<input id="file-input" type="file" accept="video/*" class="hidden">
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
<div id="upload-progress-container" class="hidden mt-10 w-full">
|
| 181 |
+
<div
|
| 182 |
+
class="flex justify-between text-[11px] font-montserrat font-bold uppercase tracking-widest mb-3 text-slate-900">
|
| 183 |
+
<span id="upload-text">Uploading...</span>
|
| 184 |
+
<span id="upload-percentage">0%</span>
|
| 185 |
+
</div>
|
| 186 |
+
<div class="w-full h-1 bg-slate-100 rounded-full overflow-hidden">
|
| 187 |
+
<div id="upload-bar"
|
| 188 |
+
class="h-full bg-black w-0 transition-all duration-75 ease-linear rounded-full"></div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<!-- STEP: Config -->
|
| 194 |
+
<div id="step-config" class="hidden w-full flex flex-col fade-in justify-center">
|
| 195 |
+
<h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900">System Configuration</h2>
|
| 196 |
+
<p class="text-[11px] font-montserrat font-bold text-slate-400 mb-8 uppercase tracking-widest">
|
| 197 |
+
Optimal values auto-configured for performance
|
| 198 |
+
</p>
|
| 199 |
+
|
| 200 |
+
<div class="space-y-4 xl:space-y-5 mb-8" id="config-fields"></div>
|
| 201 |
+
|
| 202 |
+
<button onclick="showStep('draw')"
|
| 203 |
+
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">
|
| 204 |
+
Continue <i class="fa-solid fa-arrow-right ml-3 text-sm"></i>
|
| 205 |
+
</button>
|
| 206 |
+
</div>
|
| 207 |
+
|
| 208 |
+
<!-- STEP: Draw Line -->
|
| 209 |
+
<div id="step-draw" class="hidden w-full flex flex-col fade-in justify-center">
|
| 210 |
+
<h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900">Define Boundary</h2>
|
| 211 |
+
<p class="text-[11px] font-montserrat font-bold uppercase tracking-widest text-slate-400 mb-6">Click two
|
| 212 |
+
points to establish counting threshold</p>
|
| 213 |
+
|
| 214 |
+
<div
|
| 215 |
+
class="relative w-full aspect-video bg-slate-900 rounded-3xl overflow-hidden cursor-crosshair mb-6 shadow-inner">
|
| 216 |
+
<img id="frame-img" class="absolute inset-0 w-full h-full object-contain" style="display:none;">
|
| 217 |
+
<div id="frame-placeholder"
|
| 218 |
+
class="absolute inset-0 flex flex-col items-center justify-center text-slate-500 pointer-events-none select-none">
|
| 219 |
+
<i class="fa-solid fa-video text-4xl mb-3 opacity-30"></i>
|
| 220 |
+
<span
|
| 221 |
+
class="font-montserrat font-semibold text-[10px] uppercase tracking-widest opacity-50">Media
|
| 222 |
+
Frame Preview</span>
|
| 223 |
+
</div>
|
| 224 |
+
<canvas id="drawing-canvas" class="absolute inset-0 w-full h-full"></canvas>
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
<div class="flex space-x-3">
|
| 228 |
+
<button onclick="resetCanvas()"
|
| 229 |
+
class="w-1/3 py-3.5 bg-white border border-slate-200 text-slate-800 rounded-full font-montserrat font-semibold hover:border-black hover:text-black transition-all text-sm xl:text-base">
|
| 230 |
+
Reset
|
| 231 |
+
</button>
|
| 232 |
+
<button id="btn-proceed" onclick="startRun()"
|
| 233 |
+
class="w-2/3 py-3.5 bg-black text-white rounded-full font-montserrat font-semibold hover:bg-slate-800 transition-all text-center text-sm xl:text-base">
|
| 234 |
+
Proceed
|
| 235 |
+
</button>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
</div>
|
| 240 |
+
</main>
|
| 241 |
+
|
| 242 |
+
<script>
|
| 243 |
+
let videoId = null;
|
| 244 |
+
let runConfig = {};
|
| 245 |
+
|
| 246 |
+
const configParams = [
|
| 247 |
+
{ key: "imgsz", label: "Image Size", step: 32, min: 320, max: 1280 },
|
| 248 |
+
{ key: "conf", label: "Confidence", step: 0.01, min: 0.01, max: 1.0, decimals: 2 },
|
| 249 |
+
{ key: "iou", label: "IoU Threshold", step: 0.05, min: 0.1, max: 1.0, decimals: 2 },
|
| 250 |
+
{ key: "detect_stride", label: "Frame Stride", step: 1, min: 1, max: 10 }
|
| 251 |
+
];
|
| 252 |
+
|
| 253 |
+
function showStep(name) {
|
| 254 |
+
['modules', 'upload', 'config', 'draw'].forEach(s => {
|
| 255 |
+
document.getElementById('step-' + s).classList.add('hidden');
|
| 256 |
+
});
|
| 257 |
+
document.getElementById('step-' + name).classList.remove('hidden');
|
| 258 |
+
|
| 259 |
+
if (name === 'config') renderConfig();
|
| 260 |
+
if (name === 'draw') loadFirstFrame();
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// Upload
|
| 264 |
+
const dropzone = document.getElementById('dropzone');
|
| 265 |
+
const fileInput = document.getElementById('file-input');
|
| 266 |
+
|
| 267 |
+
dropzone.addEventListener('click', () => fileInput.click());
|
| 268 |
+
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('border-black', 'bg-slate-50'); });
|
| 269 |
+
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('border-black', 'bg-slate-50'));
|
| 270 |
+
dropzone.addEventListener('drop', e => {
|
| 271 |
+
e.preventDefault();
|
| 272 |
+
dropzone.classList.remove('border-black', 'bg-slate-50');
|
| 273 |
+
if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0]);
|
| 274 |
+
});
|
| 275 |
+
fileInput.addEventListener('change', () => { if (fileInput.files.length) uploadFile(fileInput.files[0]); });
|
| 276 |
+
|
| 277 |
+
function uploadFile(file) {
|
| 278 |
+
dropzone.classList.add('hidden');
|
| 279 |
+
const prog = document.getElementById('upload-progress-container');
|
| 280 |
+
const bar = document.getElementById('upload-bar');
|
| 281 |
+
const pct = document.getElementById('upload-percentage');
|
| 282 |
+
const txt = document.getElementById('upload-text');
|
| 283 |
+
prog.classList.remove('hidden');
|
| 284 |
+
|
| 285 |
+
const form = new FormData();
|
| 286 |
+
form.append('file', file);
|
| 287 |
+
|
| 288 |
+
const xhr = new XMLHttpRequest();
|
| 289 |
+
xhr.open('POST', '/upload');
|
| 290 |
+
|
| 291 |
+
xhr.upload.onprogress = e => {
|
| 292 |
+
if (e.lengthComputable) {
|
| 293 |
+
const p = Math.round(e.loaded / e.total * 100);
|
| 294 |
+
bar.style.width = p + '%';
|
| 295 |
+
pct.innerText = p + '%';
|
| 296 |
+
}
|
| 297 |
+
};
|
| 298 |
+
|
| 299 |
+
xhr.onload = () => {
|
| 300 |
+
const res = JSON.parse(xhr.responseText);
|
| 301 |
+
videoId = res.video_id;
|
| 302 |
+
txt.innerText = 'Extracting Metadata...';
|
| 303 |
+
bar.style.width = '100%';
|
| 304 |
+
pct.innerText = '100%';
|
| 305 |
+
|
| 306 |
+
fetch('/config/' + videoId)
|
| 307 |
+
.then(r => r.json())
|
| 308 |
+
.then(cfg => {
|
| 309 |
+
runConfig = cfg;
|
| 310 |
+
runConfig.conf = 0.12;
|
| 311 |
+
runConfig.iou = 0.60;
|
| 312 |
+
txt.innerText = 'Initialization Complete';
|
| 313 |
+
setTimeout(() => showStep('config'), 400);
|
| 314 |
+
});
|
| 315 |
+
};
|
| 316 |
+
|
| 317 |
+
xhr.send(form);
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
// Config
|
| 321 |
+
function renderConfig() {
|
| 322 |
+
const container = document.getElementById('config-fields');
|
| 323 |
+
container.innerHTML = '';
|
| 324 |
+
|
| 325 |
+
configParams.forEach(p => {
|
| 326 |
+
let val = runConfig[p.key];
|
| 327 |
+
const d = p.decimals || 0;
|
| 328 |
+
|
| 329 |
+
const row = document.createElement('div');
|
| 330 |
+
row.className = 'flex items-center justify-between border-b border-slate-100 pb-3';
|
| 331 |
+
row.innerHTML = `
|
| 332 |
+
<span class="font-montserrat font-medium text-slate-700 text-sm xl:text-base">${p.label}</span>
|
| 333 |
+
<div class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
|
| 334 |
+
<button class="text-slate-400 hover:text-black transition p-1" data-dir="-1" data-key="${p.key}">
|
| 335 |
+
<i class="fa-solid fa-chevron-left text-[10px]"></i>
|
| 336 |
+
</button>
|
| 337 |
+
<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>
|
| 338 |
+
<button class="text-slate-400 hover:text-black transition p-1" data-dir="1" data-key="${p.key}">
|
| 339 |
+
<i class="fa-solid fa-chevron-right text-[10px]"></i>
|
| 340 |
+
</button>
|
| 341 |
+
</div>
|
| 342 |
+
`;
|
| 343 |
+
container.appendChild(row);
|
| 344 |
+
});
|
| 345 |
+
|
| 346 |
+
container.querySelectorAll('button').forEach(btn => {
|
| 347 |
+
btn.addEventListener('click', () => {
|
| 348 |
+
const key = btn.dataset.key;
|
| 349 |
+
const dir = parseInt(btn.dataset.dir);
|
| 350 |
+
const param = configParams.find(p => p.key === key);
|
| 351 |
+
let val = runConfig[key] + dir * param.step;
|
| 352 |
+
val = Math.max(param.min, Math.min(param.max, val));
|
| 353 |
+
val = param.decimals ? parseFloat(val.toFixed(param.decimals)) : Math.round(val);
|
| 354 |
+
runConfig[key] = val;
|
| 355 |
+
const d = param.decimals || 0;
|
| 356 |
+
document.getElementById('cfg-' + key).innerText = d ? val.toFixed(d) : val;
|
| 357 |
+
});
|
| 358 |
+
});
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
// Draw line
|
| 362 |
+
const canvas = document.getElementById('drawing-canvas');
|
| 363 |
+
const ctx = canvas.getContext('2d');
|
| 364 |
+
let points = [];
|
| 365 |
+
let imgNatW = 0, imgNatH = 0;
|
| 366 |
+
|
| 367 |
+
function loadFirstFrame() {
|
| 368 |
+
const img = document.getElementById('frame-img');
|
| 369 |
+
img.src = '/first-frame/' + videoId;
|
| 370 |
+
img.onload = () => {
|
| 371 |
+
imgNatW = img.naturalWidth;
|
| 372 |
+
imgNatH = img.naturalHeight;
|
| 373 |
+
img.style.display = 'block';
|
| 374 |
+
document.getElementById('frame-placeholder').style.display = 'none';
|
| 375 |
+
initCanvas();
|
| 376 |
+
};
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
function initCanvas() {
|
| 380 |
+
canvas.width = canvas.offsetWidth;
|
| 381 |
+
canvas.height = canvas.offsetHeight;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
window.addEventListener('resize', () => {
|
| 385 |
+
if (!document.getElementById('step-draw').classList.contains('hidden')) {
|
| 386 |
+
initCanvas();
|
| 387 |
+
points.forEach(p => drawDot(p.cx, p.cy));
|
| 388 |
+
if (points.length === 2) drawLine();
|
| 389 |
+
}
|
| 390 |
+
});
|
| 391 |
+
|
| 392 |
+
canvas.addEventListener('mousedown', e => {
|
| 393 |
+
if (points.length >= 2) return;
|
| 394 |
+
|
| 395 |
+
const rect = canvas.getBoundingClientRect();
|
| 396 |
+
const cx = e.clientX - rect.left;
|
| 397 |
+
const cy = e.clientY - rect.top;
|
| 398 |
+
|
| 399 |
+
const rx = cx / canvas.width * imgNatW;
|
| 400 |
+
const ry = cy / canvas.height * imgNatH;
|
| 401 |
+
|
| 402 |
+
points.push({ cx, cy, rx: Math.round(rx), ry: Math.round(ry) });
|
| 403 |
+
drawDot(cx, cy);
|
| 404 |
+
if (points.length === 2) drawLine();
|
| 405 |
+
});
|
| 406 |
+
|
| 407 |
+
function drawDot(x, y) {
|
| 408 |
+
ctx.beginPath();
|
| 409 |
+
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
| 410 |
+
ctx.fillStyle = '#10b981';
|
| 411 |
+
ctx.fill();
|
| 412 |
+
ctx.strokeStyle = '#ffffff';
|
| 413 |
+
ctx.lineWidth = 2;
|
| 414 |
+
ctx.stroke();
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
function drawLine() {
|
| 418 |
+
ctx.beginPath();
|
| 419 |
+
ctx.moveTo(points[0].cx, points[0].cy);
|
| 420 |
+
ctx.lineTo(points[1].cx, points[1].cy);
|
| 421 |
+
ctx.strokeStyle = '#10b981';
|
| 422 |
+
ctx.lineWidth = 3;
|
| 423 |
+
ctx.stroke();
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
function resetCanvas() {
|
| 427 |
+
points = [];
|
| 428 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
function startRun() {
|
| 432 |
+
if (points.length < 2) return;
|
| 433 |
+
|
| 434 |
+
const line = [[points[0].rx, points[0].ry], [points[1].rx, points[1].ry]];
|
| 435 |
+
|
| 436 |
+
sessionStorage.setItem('funky_run', JSON.stringify({
|
| 437 |
+
video_id: videoId,
|
| 438 |
+
line: line,
|
| 439 |
+
config: runConfig
|
| 440 |
+
}));
|
| 441 |
+
|
| 442 |
+
window.location.href = 'vehicles.html';
|
| 443 |
+
}
|
| 444 |
+
</script>
|
| 445 |
+
</body>
|
| 446 |
+
|
| 447 |
+
</html>
|
frontend/run_details.html
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta http-equiv="refresh" content="0;url=vehicles.html">
|
| 6 |
+
</head>
|
| 7 |
+
|
| 8 |
+
</html>
|
frontend/uf.svg
ADDED
|
|
frontend/uf_logo.png
ADDED
|
Git LFS Details
|
frontend/vehicles.html
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 - Dashboard</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 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 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: 'Inter', sans-serif;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.mono-font {
|
| 20 |
+
font-family: 'JetBrains Mono', monospace;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
::-webkit-scrollbar {
|
| 24 |
+
width: 5px;
|
| 25 |
+
height: 5px;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
::-webkit-scrollbar-track {
|
| 29 |
+
background: #f1f5f9;
|
| 30 |
+
border-radius: 4px;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
::-webkit-scrollbar-thumb {
|
| 34 |
+
background: #cbd5e1;
|
| 35 |
+
border-radius: 4px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
::-webkit-scrollbar-thumb:hover {
|
| 39 |
+
background: #94a3b8;
|
| 40 |
+
}
|
| 41 |
+
</style>
|
| 42 |
+
</head>
|
| 43 |
+
|
| 44 |
+
<body class="bg-[#F3F4F8] text-slate-900 h-screen w-screen overflow-hidden flex">
|
| 45 |
+
|
| 46 |
+
<!-- Sidebar -->
|
| 47 |
+
<aside class="w-60 bg-white shadow-xl flex flex-col z-20 flex-shrink-0 border-r border-slate-200 relative">
|
| 48 |
+
<div class="h-28 bg-white flex items-center justify-center px-4 my-2 border-b border-slate-100 flex-shrink-0">
|
| 49 |
+
<img src="uf_logo.png" alt="UrbanFlow Logo" class="h-24 w-auto object-contain">
|
| 50 |
+
</div>
|
| 51 |
+
<nav class="flex-1 px-4 py-4 space-y-1.5 overflow-y-auto text-sm">
|
| 52 |
+
<a onclick="switchTab('overview')" id="nav-overview"
|
| 53 |
+
class="flex items-center px-4 py-2.5 bg-slate-900 text-white rounded-lg shadow-md transition cursor-pointer">
|
| 54 |
+
<i class="fa-solid fa-desktop w-6"></i> <span class="font-medium">Overview</span>
|
| 55 |
+
</a>
|
| 56 |
+
<a onclick="switchTab('run-details')" id="nav-run-details"
|
| 57 |
+
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">
|
| 58 |
+
<i class="fa-solid fa-microchip w-6"></i> <span class="font-medium">Run Details</span>
|
| 59 |
+
</a>
|
| 60 |
+
<a
|
| 61 |
+
class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
|
| 62 |
+
<div class="flex items-center"><i class="fa-solid fa-file-lines w-6"></i> <span
|
| 63 |
+
class="font-medium">Reports</span></div>
|
| 64 |
+
<i class="fa-solid fa-lock text-[10px]"></i>
|
| 65 |
+
</a>
|
| 66 |
+
<a
|
| 67 |
+
class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
|
| 68 |
+
<div class="flex items-center"><i class="fa-solid fa-chart-line w-6"></i> <span
|
| 69 |
+
class="font-medium">Analytics</span></div>
|
| 70 |
+
<i class="fa-solid fa-lock text-[10px]"></i>
|
| 71 |
+
</a>
|
| 72 |
+
<a
|
| 73 |
+
class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
|
| 74 |
+
<div class="flex items-center"><i class="fa-solid fa-video w-6"></i> <span class="font-medium">RTSP
|
| 75 |
+
Feed</span></div>
|
| 76 |
+
<i class="fa-solid fa-lock text-[10px]"></i>
|
| 77 |
+
</a>
|
| 78 |
+
<a
|
| 79 |
+
class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
|
| 80 |
+
<div class="flex items-center"><i class="fa-solid fa-gear w-6"></i> <span
|
| 81 |
+
class="font-medium">Settings</span></div>
|
| 82 |
+
<i class="fa-solid fa-lock text-[10px]"></i>
|
| 83 |
+
</a>
|
| 84 |
+
</nav>
|
| 85 |
+
<div class="mt-auto border-t border-slate-100 p-4 flex items-center justify-center bg-white flex-shrink-0">
|
| 86 |
+
<img src="uf_logo.png" alt="UrbanFlow Bottom Logo" class="h-20 w-auto object-contain opacity-80">
|
| 87 |
+
</div>
|
| 88 |
+
</aside>
|
| 89 |
+
|
| 90 |
+
<main class="flex-1 flex flex-col h-full min-w-0 p-4 gap-4">
|
| 91 |
+
|
| 92 |
+
<!-- Progress Bar (shared) -->
|
| 93 |
+
<div
|
| 94 |
+
class="bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
|
| 95 |
+
<div class="flex items-center space-x-4 w-3/4">
|
| 96 |
+
<span class="text-[11px] font-black text-slate-900 uppercase tracking-wider"
|
| 97 |
+
id="proc-label">Processing</span>
|
| 98 |
+
<div class="flex-1 h-1.5 bg-slate-200 rounded-full overflow-hidden relative">
|
| 99 |
+
<div id="proc-bar" class="h-full bg-slate-900 rounded-full transition-all duration-500 ease-out"
|
| 100 |
+
style="width: 0%"></div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="flex items-center space-x-6 text-xs font-bold text-slate-900">
|
| 104 |
+
<span id="proc-frames">0 / 0 Frames</span>
|
| 105 |
+
<span id="proc-pct">0%</span>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<!-- TAB: Overview -->
|
| 110 |
+
<div id="tab-overview" class="grid grid-cols-4 gap-4 flex-1 min-h-0 grid-rows-2">
|
| 111 |
+
|
| 112 |
+
<!-- Congestion Index -->
|
| 113 |
+
<div class="col-span-2 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col">
|
| 114 |
+
<div class="flex justify-between items-center mb-2">
|
| 115 |
+
<h3 class="font-bold text-slate-900 text-sm">Congestion Index <span
|
| 116 |
+
class="text-[10px] font-normal text-slate-500 ml-1">(Active Vehicles per Frame)</span></h3>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="flex-1 relative w-full min-h-0">
|
| 119 |
+
<canvas id="congestionChart"></canvas>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<!-- Counters -->
|
| 124 |
+
<div class="col-span-1 flex flex-col gap-4">
|
| 125 |
+
<div
|
| 126 |
+
class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm flex-1 flex flex-col items-center justify-center">
|
| 127 |
+
<div class="text-slate-500 text-[10px] font-bold uppercase tracking-wide text-center">Total Vehicles
|
| 128 |
+
</div>
|
| 129 |
+
<div class="flex items-center justify-center mt-1">
|
| 130 |
+
<span class="text-3xl font-black text-slate-900" id="cnt-total">0</span>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
<div
|
| 134 |
+
class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm flex-1 flex flex-col items-center justify-center">
|
| 135 |
+
<div class="text-slate-500 text-[10px] font-bold uppercase tracking-wide text-center">Incoming
|
| 136 |
+
(Crossed)</div>
|
| 137 |
+
<div class="flex items-center justify-center mt-1">
|
| 138 |
+
<span class="text-3xl font-black text-slate-900" id="cnt-in">0</span>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
<div
|
| 142 |
+
class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm flex-1 flex flex-col items-center justify-center">
|
| 143 |
+
<div class="text-slate-500 text-[10px] font-bold uppercase tracking-wide text-center">Outgoing
|
| 144 |
+
(Crossed)</div>
|
| 145 |
+
<div class="flex items-center justify-center mt-1">
|
| 146 |
+
<span class="text-3xl font-black text-slate-900" id="cnt-out">0</span>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<!-- Vehicle Classification Breakdown -->
|
| 152 |
+
<div
|
| 153 |
+
class="col-span-1 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 154 |
+
<div class="flex justify-between items-center mb-1">
|
| 155 |
+
<h3 class="font-bold text-slate-900 text-sm">Vehicle Classification Breakdown</h3>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="text-[10px] text-slate-400 mb-3">Detailed distribution by vehicle subclass</div>
|
| 158 |
+
<div class="flex-1 overflow-y-auto pr-3 space-y-2" id="class-breakdown"></div>
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
<!-- Class Dominance -->
|
| 162 |
+
<div class="col-span-2 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col">
|
| 163 |
+
<div class="flex justify-between items-center mb-2">
|
| 164 |
+
<h3 class="font-bold text-slate-900 text-sm">Class Dominance</h3>
|
| 165 |
+
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Grouped
|
| 166 |
+
Categories</span>
|
| 167 |
+
</div>
|
| 168 |
+
<div class="flex-1 w-full relative min-h-0">
|
| 169 |
+
<canvas id="dominanceChart"></canvas>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
|
| 173 |
+
<!-- Traffic Flow Over Time -->
|
| 174 |
+
<div class="col-span-2 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col">
|
| 175 |
+
<div class="flex justify-between items-center mb-2">
|
| 176 |
+
<h3 class="font-bold text-slate-900 text-sm">Traffic Flow Over Time</h3>
|
| 177 |
+
<span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Vehicles / Sec</span>
|
| 178 |
+
</div>
|
| 179 |
+
<div class="flex-1 w-full relative min-h-0">
|
| 180 |
+
<canvas id="flowChart"></canvas>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<!-- TAB: Run Details -->
|
| 187 |
+
<div id="tab-run-details" class="hidden flex-1 min-h-0 overflow-y-auto">
|
| 188 |
+
<div class="grid grid-cols-2 gap-4 h-full">
|
| 189 |
+
<!-- Video Input -->
|
| 190 |
+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 191 |
+
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
|
| 192 |
+
<h3 class="font-bold text-slate-800 text-sm"><i
|
| 193 |
+
class="fa-solid fa-film mr-2 text-slate-400"></i> Video Input</h3>
|
| 194 |
+
</div>
|
| 195 |
+
<div class="p-5 space-y-3" id="panel-video"></div>
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
<!-- Performance -->
|
| 199 |
+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 200 |
+
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
|
| 201 |
+
<h3 class="font-bold text-slate-800 text-sm"><i
|
| 202 |
+
class="fa-solid fa-gauge-high mr-2 text-slate-400"></i> Performance</h3>
|
| 203 |
+
</div>
|
| 204 |
+
<div class="p-5 space-y-3" id="panel-perf"></div>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<!-- Model Config -->
|
| 208 |
+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 209 |
+
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
|
| 210 |
+
<h3 class="font-bold text-slate-800 text-sm"><i
|
| 211 |
+
class="fa-solid fa-cube mr-2 text-slate-400"></i> Model Config</h3>
|
| 212 |
+
</div>
|
| 213 |
+
<div class="p-5 space-y-3" id="panel-model"></div>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<!-- Inference Settings -->
|
| 217 |
+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 218 |
+
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
|
| 219 |
+
<h3 class="font-bold text-slate-800 text-sm"><i
|
| 220 |
+
class="fa-solid fa-sliders mr-2 text-slate-400"></i> Inference Settings</h3>
|
| 221 |
+
</div>
|
| 222 |
+
<div class="p-5 grid grid-cols-2 gap-x-8 gap-y-3" id="panel-infer"></div>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
<!-- Processing Results -->
|
| 226 |
+
<div
|
| 227 |
+
class="col-span-2 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
|
| 228 |
+
<div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
|
| 229 |
+
<h3 class="font-bold text-slate-800 text-sm"><i
|
| 230 |
+
class="fa-solid fa-stopwatch mr-2 text-slate-400"></i> Processing Results</h3>
|
| 231 |
+
</div>
|
| 232 |
+
<div class="p-5 space-y-3" id="panel-proc-results">
|
| 233 |
+
<div class="text-xs text-slate-400 italic">Waiting for processing to complete...</div>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
</main>
|
| 240 |
+
|
| 241 |
+
<script>
|
| 242 |
+
// =========== Tab switching ===========
|
| 243 |
+
function switchTab(tab) {
|
| 244 |
+
document.getElementById('tab-overview').classList.toggle('hidden', tab !== 'overview');
|
| 245 |
+
document.getElementById('tab-run-details').classList.toggle('hidden', tab !== 'run-details');
|
| 246 |
+
|
| 247 |
+
const navO = document.getElementById('nav-overview');
|
| 248 |
+
const navR = document.getElementById('nav-run-details');
|
| 249 |
+
|
| 250 |
+
if (tab === 'overview') {
|
| 251 |
+
navO.className = 'flex items-center px-4 py-2.5 bg-slate-900 text-white rounded-lg shadow-md transition cursor-pointer';
|
| 252 |
+
navR.className = 'flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer';
|
| 253 |
+
} else {
|
| 254 |
+
navR.className = 'flex items-center px-4 py-2.5 bg-slate-900 text-white rounded-lg shadow-md transition cursor-pointer';
|
| 255 |
+
navO.className = 'flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer';
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// =========== Run Details panel ===========
|
| 260 |
+
function detailRow(label, value, extra) {
|
| 261 |
+
extra = extra || '';
|
| 262 |
+
return `<div class="flex justify-between items-center border-b border-slate-50 pb-2">
|
| 263 |
+
<span class="text-xs font-medium text-slate-500 mono-font">${label}</span>
|
| 264 |
+
<span class="text-sm font-bold text-slate-800">${value}${extra}</span>
|
| 265 |
+
</div>`;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function boolBadge(val) {
|
| 269 |
+
if (val) return `<span class="inline-flex items-center bg-green-50 text-green-700 text-[10px] font-bold px-2 py-0.5 rounded border border-green-200"><i class="fa-solid fa-check mr-1"></i>TRUE</span>`;
|
| 270 |
+
return `<span class="text-[10px] font-bold text-slate-300">FALSE</span>`;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
function populateRunDetails(c) {
|
| 274 |
+
const res = c.resolution || [0, 0];
|
| 275 |
+
|
| 276 |
+
document.getElementById('panel-video').innerHTML =
|
| 277 |
+
detailRow('video_fps', c.video_fps) +
|
| 278 |
+
detailRow('frames', c.frames) +
|
| 279 |
+
detailRow('duration', c.duration + ' sec') +
|
| 280 |
+
detailRow('resolution', res[0] + ' <span class="text-slate-400 text-xs">x</span> ' + res[1]) +
|
| 281 |
+
detailRow('pixels', (c.pixels || 0).toLocaleString());
|
| 282 |
+
|
| 283 |
+
const cpuPct = Math.min(100, Math.round((c.cpu_score / 10) * 100));
|
| 284 |
+
document.getElementById('panel-perf').innerHTML =
|
| 285 |
+
`<div class="flex justify-between items-center border-b border-slate-50 pb-2">
|
| 286 |
+
<span class="text-xs font-medium text-slate-500 mono-font">cpu_score</span>
|
| 287 |
+
<div class="flex items-center">
|
| 288 |
+
<span class="text-sm font-bold text-slate-800 mr-2">${c.cpu_score}</span>
|
| 289 |
+
<div class="w-16 h-1.5 bg-slate-100 rounded-full overflow-hidden">
|
| 290 |
+
<div class="h-full bg-emerald-500" style="width:${cpuPct}%"></div>
|
| 291 |
+
</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>` +
|
| 294 |
+
detailRow('model_fps_est', c.model_fps_est, ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
|
| 295 |
+
detailRow('effective_fps', c.effective_fps_est, ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
|
| 296 |
+
`<div class="flex justify-between items-center pt-1">
|
| 297 |
+
<span class="text-xs font-medium text-slate-500 mono-font">realtime_possible</span>
|
| 298 |
+
${boolBadge(c.realtime_possible)}
|
| 299 |
+
</div>`;
|
| 300 |
+
|
| 301 |
+
document.getElementById('panel-model').innerHTML =
|
| 302 |
+
`<div class="flex justify-between items-center border-b border-slate-50 pb-2">
|
| 303 |
+
<span class="text-xs font-medium text-slate-500 mono-font">model</span>
|
| 304 |
+
<span class="text-sm font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded border border-blue-100 mono-font">Perception365/VehicleNet-Y26s</span>
|
| 305 |
+
</div>` +
|
| 306 |
+
detailRow('task', 'detect') +
|
| 307 |
+
detailRow('tracker', 'ByteTrack');
|
| 308 |
+
|
| 309 |
+
document.getElementById('panel-infer').innerHTML =
|
| 310 |
+
detailRow('imgsz', c.imgsz) +
|
| 311 |
+
detailRow('detect_stride', c.detect_stride) +
|
| 312 |
+
detailRow('conf', c.conf || 0.12) +
|
| 313 |
+
detailRow('iou', c.iou || 0.60) +
|
| 314 |
+
`<div class="flex justify-between items-center border-b border-slate-50 pb-2">
|
| 315 |
+
<span class="text-xs font-medium text-slate-500 mono-font">stream</span>
|
| 316 |
+
<span class="text-[10px] font-bold text-slate-400">TRUE</span>
|
| 317 |
+
</div>` +
|
| 318 |
+
`<div class="flex justify-between items-center border-b border-slate-50 pb-2">
|
| 319 |
+
<span class="text-xs font-medium text-slate-500 mono-font">verbose</span>
|
| 320 |
+
${boolBadge(false)}
|
| 321 |
+
</div>`;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// =========== Charts ===========
|
| 325 |
+
Chart.defaults.font.family = "'Inter', sans-serif";
|
| 326 |
+
Chart.defaults.color = '#64748b';
|
| 327 |
+
|
| 328 |
+
let MODEL_CLASSES = {};
|
| 329 |
+
let BUSINESS_MAP = {};
|
| 330 |
+
|
| 331 |
+
const congChart = new Chart(document.getElementById('congestionChart').getContext('2d'), {
|
| 332 |
+
type: 'line',
|
| 333 |
+
data: { labels: [], datasets: [{ data: [], borderColor: '#f97316', backgroundColor: 'rgba(249, 115, 22, 0.08)', fill: true, tension: 0.2, borderWidth: 1.5, pointRadius: 0 }] },
|
| 334 |
+
options: {
|
| 335 |
+
responsive: true, maintainAspectRatio: false,
|
| 336 |
+
plugins: { legend: { display: false } },
|
| 337 |
+
scales: {
|
| 338 |
+
x: { grid: { display: false }, ticks: { maxTicksLimit: 10, font: { size: 10 } }, title: { display: true, text: 'Frame Index', font: { size: 11, weight: '600' }, color: '#475569' } },
|
| 339 |
+
y: { grid: { color: '#e2e8f0' }, beginAtZero: true, ticks: { font: { size: 10 } }, title: { display: true, text: 'Active Vehicles', font: { size: 11, weight: '600' }, color: '#475569' } }
|
| 340 |
+
},
|
| 341 |
+
animation: { duration: 0 }
|
| 342 |
+
}
|
| 343 |
+
});
|
| 344 |
+
|
| 345 |
+
const domChart = new Chart(document.getElementById('dominanceChart').getContext('2d'), {
|
| 346 |
+
type: 'bar',
|
| 347 |
+
data: { labels: [], datasets: [{ data: [], backgroundColor: '#14b8a6', borderRadius: 2 }] },
|
| 348 |
+
options: {
|
| 349 |
+
responsive: true, maintainAspectRatio: false,
|
| 350 |
+
plugins: { legend: { display: false } },
|
| 351 |
+
scales: {
|
| 352 |
+
x: { grid: { display: false }, ticks: { font: { size: 10, weight: '500' } } },
|
| 353 |
+
y: { grid: { color: '#e2e8f0' }, beginAtZero: true, ticks: { font: { size: 10 } }, title: { display: true, text: 'Total Vehicle Count', font: { size: 11, weight: '600' }, color: '#475569' } }
|
| 354 |
+
},
|
| 355 |
+
animation: { duration: 0 }
|
| 356 |
+
}
|
| 357 |
+
});
|
| 358 |
+
|
| 359 |
+
const flowChart = new Chart(document.getElementById('flowChart').getContext('2d'), {
|
| 360 |
+
type: 'bar',
|
| 361 |
+
data: { labels: [], datasets: [{ data: [], backgroundColor: '#3b82f6', borderColor: '#ffffff', borderWidth: 1.5, barPercentage: 1.0, categoryPercentage: 1.0 }] },
|
| 362 |
+
options: {
|
| 363 |
+
responsive: true, maintainAspectRatio: false,
|
| 364 |
+
plugins: { legend: { display: false } },
|
| 365 |
+
scales: {
|
| 366 |
+
x: { grid: { display: false }, ticks: { maxTicksLimit: 10, font: { size: 10 } }, title: { display: true, text: 'Time (seconds)', font: { size: 11, weight: '600' }, color: '#475569' } },
|
| 367 |
+
y: { grid: { color: '#e2e8f0' }, beginAtZero: true, ticks: { font: { size: 10 } }, title: { display: true, text: 'Vehicles Crossed', font: { size: 11, weight: '600' }, color: '#475569' } }
|
| 368 |
+
},
|
| 369 |
+
animation: { duration: 0 }
|
| 370 |
+
}
|
| 371 |
+
});
|
| 372 |
+
|
| 373 |
+
// =========== Update functions ===========
|
| 374 |
+
function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
|
| 375 |
+
|
| 376 |
+
function updateBreakdown(classIn, classOut) {
|
| 377 |
+
const container = document.getElementById('class-breakdown');
|
| 378 |
+
const totalAll = sumValues(classIn) + sumValues(classOut);
|
| 379 |
+
container.innerHTML = '';
|
| 380 |
+
|
| 381 |
+
Object.keys(MODEL_CLASSES).map(Number).sort((a, b) => a - b).forEach(id => {
|
| 382 |
+
const inC = classIn[String(id)] || 0;
|
| 383 |
+
const outC = classOut[String(id)] || 0;
|
| 384 |
+
const total = inC + outC;
|
| 385 |
+
const pct = totalAll > 0 ? ((total / totalAll) * 100).toFixed(1) : '0.0';
|
| 386 |
+
|
| 387 |
+
const row = document.createElement('div');
|
| 388 |
+
row.className = 'flex items-center justify-between text-xs py-2 border-b border-slate-50';
|
| 389 |
+
row.innerHTML = `
|
| 390 |
+
<div class="w-[30%] font-bold text-slate-800 truncate" title="${MODEL_CLASSES[id]}">${MODEL_CLASSES[id]}</div>
|
| 391 |
+
<div class="w-[20%] text-slate-500 text-[11px]">${total} total</div>
|
| 392 |
+
<div class="w-[15%] text-slate-500 text-[11px]"><i class="fa-solid fa-arrow-down text-[9px] mr-1"></i>${inC}</div>
|
| 393 |
+
<div class="w-[15%] text-slate-500 text-[11px]"><i class="fa-solid fa-arrow-up text-[9px] mr-1"></i>${outC}</div>
|
| 394 |
+
<div class="w-[20%] text-right font-bold text-slate-900">${pct}%</div>
|
| 395 |
+
`;
|
| 396 |
+
container.appendChild(row);
|
| 397 |
+
});
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
function updateDominance(classIn, classOut) {
|
| 401 |
+
const labels = [], values = [];
|
| 402 |
+
for (const [group, ids] of Object.entries(BUSINESS_MAP)) {
|
| 403 |
+
let total = 0;
|
| 404 |
+
ids.forEach(id => { total += (classIn[String(id)] || 0) + (classOut[String(id)] || 0); });
|
| 405 |
+
labels.push(group);
|
| 406 |
+
values.push(total);
|
| 407 |
+
}
|
| 408 |
+
domChart.data.labels = labels;
|
| 409 |
+
domChart.data.datasets[0].data = values;
|
| 410 |
+
domChart.update();
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
function buildFlowHistogram(flowTimes, videoDuration) {
|
| 414 |
+
const binCount = Math.max(1, Math.ceil(videoDuration));
|
| 415 |
+
const bins = new Array(binCount).fill(0);
|
| 416 |
+
const labels = [];
|
| 417 |
+
for (let i = 0; i < binCount; i++) labels.push(i);
|
| 418 |
+
flowTimes.forEach(t => { bins[Math.min(Math.floor(t), binCount - 1)]++; });
|
| 419 |
+
flowChart.data.labels = labels;
|
| 420 |
+
flowChart.data.datasets[0].data = bins;
|
| 421 |
+
flowChart.update();
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
function updateCongestion(congestion, stride) {
|
| 425 |
+
const len = congestion.length;
|
| 426 |
+
if (len <= 200) {
|
| 427 |
+
congChart.data.labels = congestion.map((_, i) => i * stride);
|
| 428 |
+
congChart.data.datasets[0].data = congestion;
|
| 429 |
+
} else {
|
| 430 |
+
const step = 10;
|
| 431 |
+
const sampled = [], labels = [];
|
| 432 |
+
for (let i = 0; i < len; i += step) { labels.push(i * stride); sampled.push(congestion[i]); }
|
| 433 |
+
congChart.data.labels = labels;
|
| 434 |
+
congChart.data.datasets[0].data = sampled;
|
| 435 |
+
}
|
| 436 |
+
congChart.update();
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// =========== Main ===========
|
| 440 |
+
async function init() {
|
| 441 |
+
const raw = sessionStorage.getItem('funky_run');
|
| 442 |
+
if (!raw) { window.location.href = '/'; return; }
|
| 443 |
+
|
| 444 |
+
const params = JSON.parse(raw);
|
| 445 |
+
|
| 446 |
+
const cRes = await fetch('/constants');
|
| 447 |
+
const cData = await cRes.json();
|
| 448 |
+
MODEL_CLASSES = cData.classes;
|
| 449 |
+
BUSINESS_MAP = cData.business_map;
|
| 450 |
+
|
| 451 |
+
populateRunDetails(params.config);
|
| 452 |
+
|
| 453 |
+
const videoDuration = params.config.duration || 10;
|
| 454 |
+
const stride = params.config.detect_stride || 1;
|
| 455 |
+
|
| 456 |
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
| 457 |
+
const ws = new WebSocket(`${proto}://${location.host}/ws/run`);
|
| 458 |
+
|
| 459 |
+
ws.onopen = () => {
|
| 460 |
+
ws.send(JSON.stringify({
|
| 461 |
+
video_id: params.video_id,
|
| 462 |
+
line: params.line,
|
| 463 |
+
config: params.config
|
| 464 |
+
}));
|
| 465 |
+
};
|
| 466 |
+
|
| 467 |
+
let lastUIUpdate = 0;
|
| 468 |
+
|
| 469 |
+
ws.onmessage = e => {
|
| 470 |
+
const d = JSON.parse(e.data);
|
| 471 |
+
|
| 472 |
+
if (d.done) {
|
| 473 |
+
document.getElementById('proc-label').innerText = 'Complete';
|
| 474 |
+
document.getElementById('proc-bar').style.width = '100%';
|
| 475 |
+
document.getElementById('proc-pct').innerText = '100%';
|
| 476 |
+
|
| 477 |
+
document.getElementById('panel-proc-results').innerHTML =
|
| 478 |
+
detailRow('processing_time', d.processing_time + ' sec') +
|
| 479 |
+
detailRow('actual_fps', d.actual_fps, ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
|
| 480 |
+
detailRow('speed_vs_realtime', d.speed_vs_realtime + 'x');
|
| 481 |
+
return;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
const pct = ((d.frame_index / d.total_iters) * 100).toFixed(1);
|
| 485 |
+
document.getElementById('proc-bar').style.width = pct + '%';
|
| 486 |
+
document.getElementById('proc-frames').innerText = `${d.frame_index} / ${d.total_iters} Frames`;
|
| 487 |
+
document.getElementById('proc-pct').innerText = pct + '%';
|
| 488 |
+
|
| 489 |
+
const totalIn = sumValues(d.class_in);
|
| 490 |
+
const totalOut = sumValues(d.class_out);
|
| 491 |
+
document.getElementById('cnt-total').innerText = totalIn + totalOut;
|
| 492 |
+
document.getElementById('cnt-in').innerText = totalIn;
|
| 493 |
+
document.getElementById('cnt-out').innerText = totalOut;
|
| 494 |
+
|
| 495 |
+
const now = performance.now();
|
| 496 |
+
if (now - lastUIUpdate < 300) return;
|
| 497 |
+
lastUIUpdate = now;
|
| 498 |
+
|
| 499 |
+
updateCongestion(d.congestion, stride);
|
| 500 |
+
updateBreakdown(d.class_in, d.class_out);
|
| 501 |
+
updateDominance(d.class_in, d.class_out);
|
| 502 |
+
buildFlowHistogram(d.flow_times, videoDuration);
|
| 503 |
+
};
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
init();
|
| 507 |
+
</script>
|
| 508 |
+
</body>
|
| 509 |
+
|
| 510 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn[standard]==0.24.0
|
| 3 |
+
python-multipart==0.0.6
|
| 4 |
+
opencv-python-headless==4.8.1.78
|
| 5 |
+
ultralytics>=8.3.0
|
| 6 |
+
numpy==1.24.3
|
| 7 |
+
python-dotenv==1.0.0
|
| 8 |
+
websockets==12.0
|
| 9 |
+
onnxruntime
|
| 10 |
+
onnx>=1.12.0
|
| 11 |
+
onnxslim>=0.1.71
|
| 12 |
+
lap>=0.5.12
|
| 13 |
+
torch==2.1.0+cpu
|
| 14 |
+
torchvision==0.16.0+cpu
|
| 15 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|