SugboFlowSee how Cebu moves

How SugboFlow works

The models, the pipeline, and the math behind the map — from a video frame to a signal timing recommendation.

1 · Detection & tracking models

ComponentWhat we useDetails
Object detectorYOLO11s (Ultralytics)9.5M parameters, COCO-pretrained, 640px input. Filtered to vehicle classes only: car, motorcycle, bus, truck, bicycle.
Multi-object trackerByteTrackAssociates detections across frames into stable track IDs, keeping low-confidence boxes in play so vehicles survive brief occlusions (a jeepney passing behind a truck keeps its ID).
HardwareAuto-selectedApple GPU (MPS) → NVIDIA CUDA → CPU fallback. On an Apple Silicon Mac the demo clip (89s @ 30fps) processes in about real time.

Known limitation: COCO has no jeepney or tricycle class, so jeepneys land in bus/truck and tricycles in motorcycle. The data model already carries the local taxonomy — the planned upgrade is a fine-tuned model on annotated PH footage, which slots in without any schema change.

2 · From boxes to counts: the virtual counting line

Each camera stores a counting line drawn on its view (Monitoring page), saved in normalized 0–1 coordinates so any resolution works. Every frame, each tracked vehicle's box center is tested against the line with a signed side-of-line function:

side(p) = (x₂−x₁)(pᵧ−y₁) − (y₂−y₁)(pₓ−x₁)

When the sign flips between consecutive frames, that track crossed the line — counted exactly once, with the flip direction giving the travel direction (northbound vs southbound). Each crossing is timestamped as video start + frame / fps and dropped into a 1-minute bin per (camera, class, direction).

Frame→YOLO11s detects vehicles→ByteTrack assigns IDs→Side-of-line sign test→Sign flip = 1 count

3 · System architecture

BrowserNext.js dashboard :3001 — MapLibre map, Recharts
↕ REST (JSON)
FastAPI :8001cameras · counts · jobs · analytics · optimize · routes
↙ enqueue job    ↘ read/write
Redisarq job queue
PostgreSQLcameras · video_jobs · vehicle_counts
↖ consumes queue    ↗ writes bins + progress
arq workerruns the vision pipeline, renders the annotated mp4 (ffmpeg → H.264)

The worker is a separate process so a 30-minute video never blocks the API. It also handles link ingestion — YouTube and public Facebook videos (yt-dlp downloads the video, or records a live stream for a set duration, before counting). The same design will host RTSP live-stream consumers later — a stream is just a video that never ends.

4 · How the database is updated

Upload video / paste YouTube or Facebook link→video_jobs row (queued)→worker sets running + progress every 2s→bulk-insert vehicle_counts→job done + totals
TableOne row is…Key columns
camerasa monitoring pointname, intersection, lat/lng, counting-line config (JSON), approach lanes
video_jobsone processed videostatus, progress, video_start, per-class totals, annotated file path
vehicle_countsa 1-minute bincamera, bin_start, vehicle_class, direction, count, PCU, source

Three properties keep the data trustworthy:

  • Idempotent re-runs — counts carry their job_id; re-running a job deletes its old rows first, so nothing double-counts.
  • Source tagging — every row is video, demo (synthetic seed data), or live, so demo data can be swept away the moment real footage arrives.
  • Minute-level atoms — hourly/daily views are aggregated in SQL (date_trunc), never stored, so there is one source of truth.

5 · The traffic math

PCU weighting

Counts are weighted into passenger-car units so a bus and a motorcycle don't count the same: motorcycle ×0.5 · car ×1.0 · jeepney ×1.5 · bus/truck ×2.0 · bicycle ×0.3.

Congestion index (map colors)

ratio = latest-hour PCU ÷ typical PCU for this weekday & hour (averaged over history, ignoring partially-recorded hours). Free flow <0.75 · moderate <1.05 · heavy <1.35 · severe ≥1.35. With no history yet, it falls back to utilization of the camera's approach capacity.

Forecast (next 24h)

Seasonal-naive baseline — the average for the same weekday & hour — scaled by how the last 3 hours ran versus their own baseline, with that trend factor decaying over the forecast horizon.

Signal timing — Webster's method

C = (1.5·L + 5) / (1 − Y)

L = lost time (4s per phase), Y = sum of critical flow ratios (measured peak-hour PCU ÷ saturation flow of 1800 PCU/hr/lane). Green time is split in proportion to each approach's flow ratio, and average delay per vehicle is estimated with Webster's delay formula for both the current and recommended plans — that difference is the "estimated improvement" on the Optimize page. Verified in unit tests against a hand-worked textbook example.

Congestion-aware routing

The drivable road network around the cameras (12.6k nodes, 27.7k edges from OpenStreetMap via osmnx) gets baseline travel times per edge; edges within 600m of a congested camera are slowed by its level (moderate ×1.25, heavy ×1.7, severe ×2.4), then a shortest-path search finds the fastest adjusted route and reports what it avoided.

Full source layout and setup commands are in the project README.