Configurable multi-camera person counting service with pluggable detector and smoothing modules.
- YAML configuration with
defaultsand per-camera overrides. - Per-camera
source_urlis required (global/default source URL is intentionally disallowed). - Per-camera ROI presets plus optional ad-hoc ROI queries.
- REST endpoints for count and ROI discovery.
- SSE endpoints for live count updates.
- Modular architecture for detector backends and smoothing algorithms.
- Frame cadence controls: scale, frame stride, max processed FPS, infer interval.
- Automatic source reconnects after repeated stream read failures.
Permission from UBCO must be obtained if hosting this project with UBC streams beyond personal use, according to the Terms of Use and Copyright information linked on the Current Students page.
For any other stream, obtain permission from the stream owner as needed.
This project is under the MIT license.
- (Optional) Install export tooling and export a YOLO model to OpenVINO XML/BIN:
uv sync --extra export
uv run python tools/export_yolo.py --model yolov8s.pt --output-dir models --dynamic --half- Edit
config.yamlfor your stream URLs, model path, and ROIs. - Run the API:
uv run visualcounter-apiBy default, config is loaded from config.yaml. Override with:
VISUALCOUNTER_CONFIG=/path/to/config.yaml uv run visualcounter-apiTo enable API key auth, set VISUALCOUNTER_API_KEYS to a comma-separated list and send one of the keys in the X-API-Key header:
VISUALCOUNTER_API_KEYS=dev-key-1,dev-key-2 uv run visualcounter-apiExample request:
curl -H 'X-API-Key: dev-key-1' http://localhost:8000/timcam_courtyard/countAPI key enforcement is controlled by the top-level api config block:
api:
api_key_mode: custom_rois
allow_custom_rois: trueModes:
disabled: no API key required anywhereall: API key required for all routescustom_rois: preset/default ROI access is public, but requests using?roi=...require an API key
If allow_custom_rois is false, requests using ?roi=... are rejected entirely.
Build the image:
docker build -t visualcounter-api .Run it directly:
docker run --rm -p 8000:8000 \
-e VISUALCOUNTER_API_KEYS=dev-key-1 \
-v "$(pwd)/config.yaml:/app/config.yaml:ro" \
-v "$(pwd)/models:/app/models:ro" \
visualcounter-apiOr use Compose:
docker compose up --buildNotes:
config.yamlandmodels/are mounted read-only incompose.yamlso you can update settings or swap models without rebuilding the image.processing.show_preview: trueis usually not practical inside a container unless you explicitly wire through an X11/Wayland GUI. For normal container deployments, set it tofalse.- The image installs
ffmpegso OpenCV can open HLS sources such as.m3u8streams.
GET /{camera_name}/count?roi_name=queueGET /{camera_name}/count?roi=0.3047,0.4514;0.3516,0.3333;0.5625,0.3333;0.5625,0.4514GET /{camera_name}/count/stream?roi_name=queueGET /{camera_name}/count/live?roi_name=queueGET /{camera_name}GET /{camera_name}/rois
If no ROI query is provided, the API uses default_roi when configured.
See config.yaml for a full example.
Top-level keys:
api: API exposure policy (api_key_mode,allow_custom_rois)defaults: shared base settings applied to all cameras (exceptsource_url).cameras: per-camera settings and overrides.
Camera settings include:
source_urldetector(type,model_path,device, thresholds, etc.)processing(scale,every_n_frames,max_processed_fps,infer_every_seconds, crop settings,show_preview)smoothing(optional; enabled only when present)rois(named polygons in normalized coordinates0..1; each camera can define multiple ROI presets)default_roi(optional)details(optional arbitrary metadata returned byGET /{camera_name})
When processing.show_preview is true, a local OpenCV preview window is shown for that camera with ROI and detection overlays. Press q in the preview window to stop that worker.
Cropping behavior:
processing.source_crop(optional) crops the source frame first, before ROI polygon conversion and inference.- ROI points are still authored in full-source normalized coordinates.
- For source-cropped cameras, ROIs are transformed into the cropped coordinate space.
- If an ROI is partially outside the source crop, it is clipped to the crop bounds.
- If clipping leaves fewer than 3 points, that ROI is treated as empty (count is
0).
Freshness behavior:
processing.latest_frame_queue_sizeenables queued capture mode when set above0.- A value of
1keeps only the freshest frame (lowest staleness, highest frame drop). - Larger values keep a short queue of recent frames (less drop, potentially more staleness).
Stream resiliency:
processing.read_failures_before_reconnectcontrols how many failedcap.read()calls are tolerated before the source is reopened.processing.reconnect_delay_secondscontrols how long the worker waits before reconnecting to the source.processing.ffmpeg_open_timeout_msandprocessing.ffmpeg_read_timeout_msare passed to OpenCV's FFmpeg backend when supported, which can reduce how long dead HLS/TCP reads block before reconnect logic runs.- For bursty sources, set
processing.ffmpeg_read_timeout_mscomfortably above the longest expected gap between frame batches, or the worker will reconnect during normal idle periods.
Set allowed origins in visualcounter/api.py, the default is all, as this project was started to create a public API
- Add detector backends by implementing
visualcounter/detectors/base.pyand registering invisualcounter/service.py. - Add smoothing algorithms by implementing
visualcounter/smoothing/base.pyand registering invisualcounter/service.py. - Reuse the same processing engine with file inputs by setting
source_urlto a local video path.