Spaces:
Running
Running
threaded vdeo processing, report building..
Browse files- backend/engine.py +42 -4
- backend/server.py +20 -12
backend/engine.py
CHANGED
|
@@ -20,11 +20,50 @@ def _point_to_segment_dist(px, py, ax, ay, bx, by):
|
|
| 20 |
return np.linalg.norm(P - (A + t * AB))
|
| 21 |
|
| 22 |
|
|
|
|
|
|
|
|
|
|
| 23 |
# Lightweight drawing colors (BGR for OpenCV)
|
| 24 |
_CLR_BOX = (230, 180, 50) # teal-ish
|
| 25 |
_CLR_LINE = (80, 220, 100) # green
|
| 26 |
_CLR_TEXT_BG = (30, 30, 30) # dark bg for text
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def _draw_annotations(frame, boxes, ids, clses, line_pts, options):
|
| 30 |
"""Draw bounding boxes, track IDs, and counting line on frame in-place."""
|
|
@@ -91,8 +130,7 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
|
|
| 91 |
os.makedirs(annotated_dir, exist_ok=True)
|
| 92 |
annotated_path = os.path.join(annotated_dir, f"annotated_{os.path.basename(video_path)}.mp4")
|
| 93 |
writer_fps = max(1.0, fps / stride)
|
| 94 |
-
|
| 95 |
-
writer = cv2.VideoWriter(annotated_path, fourcc, writer_fps, (out_w, out_h))
|
| 96 |
|
| 97 |
prev_side = {}
|
| 98 |
counted_ids = set()
|
|
@@ -112,7 +150,7 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
|
|
| 112 |
results = model.track(
|
| 113 |
source=video_path,
|
| 114 |
tracker="bytetrack.yaml",
|
| 115 |
-
imgsz=
|
| 116 |
conf=config.get("conf", 0.12),
|
| 117 |
iou=config.get("iou", 0.6),
|
| 118 |
vid_stride=stride,
|
|
@@ -210,7 +248,7 @@ def run(model, video_path, line, config, on_frame, save_annotated=False, annotat
|
|
| 210 |
on_frame(update)
|
| 211 |
|
| 212 |
if writer is not None:
|
| 213 |
-
writer.
|
| 214 |
|
| 215 |
processing_time = round(time.time() - start, 2)
|
| 216 |
actual_fps = round(total / processing_time, 2) if processing_time > 0 else 0
|
|
|
|
| 20 |
return np.linalg.norm(P - (A + t * AB))
|
| 21 |
|
| 22 |
|
| 23 |
+
import threading
|
| 24 |
+
import queue
|
| 25 |
+
|
| 26 |
# Lightweight drawing colors (BGR for OpenCV)
|
| 27 |
_CLR_BOX = (230, 180, 50) # teal-ish
|
| 28 |
_CLR_LINE = (80, 220, 100) # green
|
| 29 |
_CLR_TEXT_BG = (30, 30, 30) # dark bg for text
|
| 30 |
|
| 31 |
+
class ThreadedVideoWriter:
|
| 32 |
+
def __init__(self, path, fps, size):
|
| 33 |
+
self.path = path
|
| 34 |
+
self.fps = fps
|
| 35 |
+
self.size = size
|
| 36 |
+
self.queue = queue.Queue(maxsize=128)
|
| 37 |
+
self.stopped = False
|
| 38 |
+
self.writer = cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*"mp4v"), fps, size)
|
| 39 |
+
self.thread = threading.Thread(target=self._run, daemon=True)
|
| 40 |
+
self.thread.start()
|
| 41 |
+
|
| 42 |
+
def _run(self):
|
| 43 |
+
while not self.stopped or not self.queue.empty():
|
| 44 |
+
try:
|
| 45 |
+
frame = self.queue.get(timeout=1.0)
|
| 46 |
+
if frame is not None:
|
| 47 |
+
self.writer.write(frame)
|
| 48 |
+
self.queue.task_done()
|
| 49 |
+
except queue.Empty:
|
| 50 |
+
continue
|
| 51 |
+
self.writer.release()
|
| 52 |
+
print(f"[BACKEND] Threaded writer finished: {self.path}")
|
| 53 |
+
|
| 54 |
+
def write(self, frame):
|
| 55 |
+
if not self.stopped:
|
| 56 |
+
try:
|
| 57 |
+
# If queue is full, we might want to wait or drop, but here we'll wait
|
| 58 |
+
# to ensure the export video is complete and accurate.
|
| 59 |
+
self.queue.put(frame.copy())
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"[BACKEND] Writer queue error: {e}")
|
| 62 |
+
|
| 63 |
+
def stop(self):
|
| 64 |
+
self.stopped = True
|
| 65 |
+
self.thread.join()
|
| 66 |
+
|
| 67 |
|
| 68 |
def _draw_annotations(frame, boxes, ids, clses, line_pts, options):
|
| 69 |
"""Draw bounding boxes, track IDs, and counting line on frame in-place."""
|
|
|
|
| 130 |
os.makedirs(annotated_dir, exist_ok=True)
|
| 131 |
annotated_path = os.path.join(annotated_dir, f"annotated_{os.path.basename(video_path)}.mp4")
|
| 132 |
writer_fps = max(1.0, fps / stride)
|
| 133 |
+
writer = ThreadedVideoWriter(annotated_path, writer_fps, (out_w, out_h))
|
|
|
|
| 134 |
|
| 135 |
prev_side = {}
|
| 136 |
counted_ids = set()
|
|
|
|
| 150 |
results = model.track(
|
| 151 |
source=video_path,
|
| 152 |
tracker="bytetrack.yaml",
|
| 153 |
+
imgsz=640, # Optimized from 736 for real-time performance on CPU
|
| 154 |
conf=config.get("conf", 0.12),
|
| 155 |
iou=config.get("iou", 0.6),
|
| 156 |
vid_stride=stride,
|
|
|
|
| 248 |
on_frame(update)
|
| 249 |
|
| 250 |
if writer is not None:
|
| 251 |
+
writer.stop()
|
| 252 |
|
| 253 |
processing_time = round(time.time() - start, 2)
|
| 254 |
actual_fps = round(total / processing_time, 2) if processing_time > 0 else 0
|
backend/server.py
CHANGED
|
@@ -5,6 +5,7 @@ import asyncio
|
|
| 5 |
import tempfile
|
| 6 |
import shutil
|
| 7 |
from pathlib import Path
|
|
|
|
| 8 |
|
| 9 |
import cv2
|
| 10 |
from fastapi import FastAPI, WebSocket, UploadFile, File
|
|
@@ -134,6 +135,7 @@ def get_report(video_id: str, name: str):
|
|
| 134 |
media = "application/pdf"
|
| 135 |
elif name.endswith(".mp4"):
|
| 136 |
media = "video/mp4"
|
|
|
|
| 137 |
return FileResponse(str(path), media_type=media)
|
| 138 |
|
| 139 |
|
|
@@ -146,29 +148,35 @@ def download_all_reports(video_id: str):
|
|
| 146 |
return Response(content=f"Report directory not found for {video_id}", status_code=404)
|
| 147 |
|
| 148 |
try:
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
| 153 |
|
| 154 |
-
print(f"[BACKEND]
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
-
|
| 158 |
-
if not final_zip.exists():
|
| 159 |
raise Exception("Zip file was not created")
|
| 160 |
|
| 161 |
original_name = video_info.get(video_id, "UrbanFlow_Analysis")
|
| 162 |
safe_name = "".join(x for x in original_name if x.isalnum() or x in "._-").rsplit(".", 1)[0]
|
| 163 |
|
| 164 |
-
print(f"[BACKEND] Serving ZIP: {
|
| 165 |
return FileResponse(
|
| 166 |
-
str(
|
| 167 |
media_type="application/zip",
|
| 168 |
-
filename=f"{safe_name}
|
| 169 |
)
|
| 170 |
except Exception as e:
|
| 171 |
-
|
|
|
|
| 172 |
return Response(content=str(e), status_code=500)
|
| 173 |
|
| 174 |
|
|
|
|
| 5 |
import tempfile
|
| 6 |
import shutil
|
| 7 |
from pathlib import Path
|
| 8 |
+
import zipfile
|
| 9 |
|
| 10 |
import cv2
|
| 11 |
from fastapi import FastAPI, WebSocket, UploadFile, File
|
|
|
|
| 135 |
media = "application/pdf"
|
| 136 |
elif name.endswith(".mp4"):
|
| 137 |
media = "video/mp4"
|
| 138 |
+
|
| 139 |
return FileResponse(str(path), media_type=media)
|
| 140 |
|
| 141 |
|
|
|
|
| 148 |
return Response(content=f"Report directory not found for {video_id}", status_code=404)
|
| 149 |
|
| 150 |
try:
|
| 151 |
+
zip_filename = f"bundle_{video_id}.zip"
|
| 152 |
+
zip_path = REPORT_DIR / zip_filename
|
| 153 |
+
|
| 154 |
+
if zip_path.exists():
|
| 155 |
+
zip_path.unlink()
|
| 156 |
|
| 157 |
+
print(f"[BACKEND] Creating ZIP: {zip_path}")
|
| 158 |
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
| 159 |
+
for root, _, files in os.walk(base_path):
|
| 160 |
+
for file in files:
|
| 161 |
+
file_path = os.path.join(root, file)
|
| 162 |
+
arcname = os.path.relpath(file_path, base_path)
|
| 163 |
+
zipf.write(file_path, arcname)
|
| 164 |
|
| 165 |
+
if not zip_path.exists():
|
|
|
|
| 166 |
raise Exception("Zip file was not created")
|
| 167 |
|
| 168 |
original_name = video_info.get(video_id, "UrbanFlow_Analysis")
|
| 169 |
safe_name = "".join(x for x in original_name if x.isalnum() or x in "._-").rsplit(".", 1)[0]
|
| 170 |
|
| 171 |
+
print(f"[BACKEND] Serving ZIP: {zip_path}")
|
| 172 |
return FileResponse(
|
| 173 |
+
str(zip_path),
|
| 174 |
media_type="application/zip",
|
| 175 |
+
filename=f"{safe_name}_UrbanFlow.zip"
|
| 176 |
)
|
| 177 |
except Exception as e:
|
| 178 |
+
import traceback
|
| 179 |
+
print(f"[BACKEND] ZIP Error: {str(e)}\n{traceback.format_exc()}")
|
| 180 |
return Response(content=str(e), status_code=500)
|
| 181 |
|
| 182 |
|