Download backend/vision_pipeline.py from SolutionChallenge/solution_challenge_backend: direct link, hf CLI and curl.
- Browser
- Download file 4.27 kB
-
https://huggingface.co/spaces/SolutionChallenge/solution_challenge_backend/resolve/main/backend/vision_pipeline.py
- Command line
-
hf download hf://spaces/SolutionChallenge/solution_challenge_backend/backend/vision_pipeline.py
-
curl -L -o vision_pipeline.py https://huggingface.co/spaces/SolutionChallenge/solution_challenge_backend/resolve/main/backend/vision_pipeline.py
4.27 kB
| """Bounded vision inference queue — drop stale frames, skip, single worker.""" | |
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| import time | |
| from dataclasses import dataclass, field | |
| from typing import Awaitable, Callable | |
| import cv2 | |
| import numpy as np | |
| logger = logging.getLogger(__name__) | |
| FRAME_SKIP_MOD = max(1, int(__import__("os").getenv("CEPHEUS_FRAME_SKIP", "4") or "4")) | |
| INFER_WIDTH = max(160, int(__import__("os").getenv("CEPHEUS_INFER_WIDTH", "320") or "320")) | |
| LIVE_INFER_WIDTH = max(INFER_WIDTH, int(__import__("os").getenv("CEPHEUS_LIVE_INFER_WIDTH", "480") or "480")) | |
| PIPELINE_INFER_TIMEOUT = float(__import__("os").getenv("CEPHEUS_PIPELINE_TIMEOUT", "20") or "20") | |
| class VisionPipelineStats: | |
| submitted: int = 0 | |
| processed: int = 0 | |
| dropped: int = 0 | |
| skipped: int = 0 | |
| last_inference_ms: float = 0.0 | |
| fps: float = 0.0 | |
| queue_depth: int = 0 | |
| _counter: int = 0 | |
| _last_process_ts: float = field(default_factory=time.time) | |
| def as_dict(self) -> dict: | |
| return { | |
| "submitted": self.submitted, | |
| "processed": self.processed, | |
| "dropped": self.dropped, | |
| "skipped": self.skipped, | |
| "last_inference_ms": round(self.last_inference_ms, 2), | |
| "fps": round(self.fps, 2), | |
| "queue_depth": self.queue_depth, | |
| "frame_skip_mod": FRAME_SKIP_MOD, | |
| "infer_width": INFER_WIDTH, | |
| "live_infer_width": LIVE_INFER_WIDTH, | |
| } | |
| def scale_match_bboxes(matches: list, from_frame, to_frame) -> list: | |
| """Map detection boxes from infer resolution back to full frame.""" | |
| if not matches or from_frame is None or to_frame is None: | |
| return matches | |
| fh, fw = from_frame.shape[:2] | |
| th, tw = to_frame.shape[:2] | |
| if fh == th and fw == tw: | |
| return matches | |
| sx, sy = tw / max(fw, 1), th / max(fh, 1) | |
| for m in matches: | |
| b = m.get("bbox") | |
| if b and len(b) >= 4: | |
| m["bbox"] = [b[0] * sx, b[1] * sy, b[2] * sx, b[3] * sy] | |
| return matches | |
| def resize_for_infer(frame: np.ndarray, width: int = INFER_WIDTH) -> np.ndarray: | |
| if frame is None or frame.size == 0: | |
| return frame | |
| h, w = frame.shape[:2] | |
| if w <= width: | |
| return frame | |
| nh = max(1, int(h * (width / w))) | |
| return cv2.resize(frame, (width, nh), interpolation=cv2.INTER_AREA) | |
| AsyncInferRunner = Callable[..., Awaitable] | |
| class VisionFramePipeline: | |
| """Frame skip + bounded async infer via shared face executor (no extra thread).""" | |
| def __init__(self, infer_fn, async_runner: AsyncInferRunner | None = None): | |
| self._infer_fn = infer_fn | |
| self._async_runner = async_runner | |
| self.stats = VisionPipelineStats() | |
| async def infer(self, cam_id: str, frame: np.ndarray) -> tuple[list, bool]: | |
| """Returns (matches, skipped). skipped=True means frame was not inferred.""" | |
| self.stats._counter += 1 | |
| if self.stats._counter % FRAME_SKIP_MOD != 0: | |
| self.stats.skipped += 1 | |
| return [], True | |
| if self._async_runner is None: | |
| logger.error("VisionFramePipeline missing async_runner") | |
| return [], True | |
| self.stats.submitted += 1 | |
| self.stats.queue_depth = 1 | |
| small = resize_for_infer(frame) | |
| t0 = time.perf_counter() | |
| try: | |
| matches = await self._async_runner( | |
| self._infer_fn, | |
| cam_id, | |
| small, | |
| timeout=PIPELINE_INFER_TIMEOUT, | |
| ) | |
| if not isinstance(matches, list): | |
| matches = [] | |
| except asyncio.TimeoutError: | |
| logger.error("Vision pipeline infer timed out after %.0fs", PIPELINE_INFER_TIMEOUT) | |
| matches = [] | |
| except Exception as exc: | |
| logger.warning("vision pipeline infer error: %s", exc) | |
| matches = [] | |
| elapsed_ms = (time.perf_counter() - t0) * 1000.0 | |
| self.stats.last_inference_ms = elapsed_ms | |
| self.stats.processed += 1 | |
| now = time.time() | |
| dt = now - self.stats._last_process_ts | |
| if dt > 0: | |
| self.stats.fps = 1.0 / dt | |
| self.stats._last_process_ts = now | |
| self.stats.queue_depth = 0 | |
| return matches, False | |