| title | Traffic Tracking Analytics |
|---|---|
| emoji | π |
| colorFrom | blue |
| colorTo | indigo |
| sdk | gradio |
| sdk_version | 5.25.0 |
| app_file | app.py |
| pinned | false |
| license | mit |
| short_description | YOLOv8 + ByteTrack vehicle detection, tracking & analytics |
A production-grade traffic analysis system that detects vehicles and pedestrians using YOLOv8, tracks them across frames with ByteTrack, computes real-time analytics (vehicle count, speed estimation, traffic density heatmap), and serves everything through a FastAPI backend with an interactive Streamlit dashboard.
YOLOv8n + ByteTrack live on UK motorway footage β 6 s clip, 12 fps. Track IDs persist through partial occlusions; trajectory tails show last 30 frames.
Multi-lane vehicle detection and tracking β UK motorway, dusk, 2560Γ1440
| Frame 1 | Frame 2 |
|---|---|
Green boxes = cars Β· Orange = trucks Β· Blue = motorcycles Β· Labels show track ID and confidence
Traffic density heatmap (60 s clip, decay = 0.98):
Hot spots correspond to the main travel lanes; the hard shoulder stays cold.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β INPUT β
β Video file (MP4/AVI/MOV) or RTSP stream β
βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β BGR frames (2560Γ1440)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DETECTION β src/detection/detector.py β
β β
β YOLOv8n (COCO pretrained) β
β β’ Letterbox-resize β 640Γ640 β
β β’ Single-shot inference + NMS β
β β’ Class filter: person, bicycle, car, motorcycle, bus, truck β
β β’ Output: List[Detection(bbox, confidence, class)] β
βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β N detections per frame
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TRACKING β src/tracking/byte_tracker.py β
β β
β ByteTrack (two-stage IoU matching) β
β β’ Kalman filter predicts position during occlusion β
β β’ Stage 1: high-conf dets β all tracks (thresh = 0.4 IoU) β
β β’ Stage 2: low-conf dets β unmatched tracks β
β β’ States: TENTATIVE (< 3 hits) β CONFIRMED β LOST β deleted β
β β’ Output: List[Track(id, class, bbox, trajectory, state)] β
ββββββββββββββββ¬βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββ
β confirmed tracks β
βββββββββΌβββββββββββ βββββββββββΌβββββββββββββββββ
β COUNTING β β SPEED ESTIMATION β
β counter.py β β speed.py β
β β β β
β Virtual lines β β Pixel displacement β
β Cross-product β β Γ px/m constant β
β direction check β β Γ· Ξt (fps) β
β Counted-IDs set β β 5-frame smoothing window β
βββββββββ¬βββββββββββ βββββββββββ¬βββββββββββββββββ
β β
βββββββββΌβββββββββββββββββββββββββββββββΌββββββββββββββββββ
β HEATMAP GENERATOR β heatmap.py β
β β
β 64Γ48 accumulation grid, exponential decay (Ξ± = 0.98) β
β Mapped to hot colormap and overlaid on frame β
βββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β annotated frames + metrics
ββββββββββββββββΌβββββββββββββββββββββββββββββββ
β SERVING β
β β
β FastAPI :8000 β REST API (upload β JSON) β
β Streamlit :8501 β Live dashboard β
ββββββββββββββββββββββββββββββββββββββββββββββββ
Benchmarked on a 60-second UK motorway clip, 2560Γ1440 @ 30 fps.
| Metric | Value |
|---|---|
| Processing FPS β GTX 1650 (GPU) | ~45 fps |
| Processing FPS β CPU (no GPU) | 17.4 fps |
| ms / frame β GPU | ~22 ms |
| ms / frame β CPU | 57.3 ms |
| Source video resolution | 2560 Γ 1440 |
| YOLO input size (letterboxed) | 640 Γ 640 |
| Tracks in 60 s clip | ~80 unique IDs |
| Avg vehicles visible per frame | 12β18 |
Note on GPU FPS: The Docker image ships with a CPU-only PyTorch wheel. To use the GTX 1650, run with
docker-compose --profile gpu upand ensure the image is rebuilt with a CUDA 12.x-compatible torch wheel (pip install torch --index-url https://download.pytorch.org/whl/cu124). The ~45 fps GPU figure is derived from published YOLOv8n benchmarks for the GTX 1650 tier; runscripts/benchmark.pywith CUDA enabled to measure it precisely on your setup.
docker-compose up --build
# API: http://localhost:8000/docs
# Dashboard: http://localhost:8501# Requires: NVIDIA Container Toolkit
docker-compose --profile gpu up --buildconda create -n traffic-track python=3.10 -y
conda activate traffic-track
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124
pip install -r requirements.txt
# Run full pipeline on a video
python scripts/run_pipeline.py --video assets/sample_videos/traffic.mp4 --output results/
# API server
uvicorn src.api.main:app --reload --port 8000
# Dashboard (separate terminal)
streamlit run src/dashboard/app.pytraffic-tracking-analytics/
βββ README.md
βββ requirements.txt
βββ Dockerfile
βββ docker-compose.yml
βββ .flake8
βββ .github/
β βββ workflows/
β βββ ci.yml # pytest + flake8 on every push; Docker smoke test on main
βββ configs/
β βββ detector.yaml # YOLOv8 model, confidence / IoU thresholds, target classes
β βββ tracker.yaml # ByteTrack parameters
β βββ analytics.yaml # Counting lines, speed calibration, heatmap settings
βββ src/
β βββ detection/
β β βββ detector.py # YOLOv8 wrapper β class filtering, batch inference
β β βββ model_utils.py # Model loading, benchmarking helpers
β βββ tracking/
β β βββ byte_tracker.py # Two-stage ByteTrack with Kalman IoU matching
β β βββ track.py # Track dataclass β state machine + Kalman filter
β β βββ kalman_filter.py # Constant-velocity Kalman (8-D state, 4-D measurement)
β β βββ trajectory.py # Trajectory storage and smoothing
β βββ analytics/
β β βββ counter.py # Virtual-line crossing counter (cross-product method)
β β βββ speed.py # Speed estimation from pixel displacement
β β βββ heatmap.py # Exponential-decay traffic density heatmap
β β βββ zone.py # Zone-based occupancy tracking (optional)
β βββ api/
β β βββ main.py # FastAPI app β lifespan, endpoints, pipeline orchestration
β β βββ schemas.py # Pydantic request/response models
β βββ dashboard/
β βββ app.py # Streamlit dashboard β tabs: overview, timeline, classes, heatmap
βββ scripts/
β βββ run_pipeline.py # CLI entry point for offline video processing
β βββ download_sample.py # Download sample traffic videos
β βββ benchmark.py # FPS and latency benchmarking
βββ tests/
β βββ test_detector.py # 20 tests β class filtering, thresholds, batch, mock YOLO
β βββ test_tracker.py # 34 tests β lifecycle, Kalman, two-stage matching
β βββ test_counter.py # 37 tests β crossing, direction, double-count prevention
β βββ test_speed.py # 20 tests β pixelβkm/h conversion, smoothing, cap
β βββ test_heatmap.py # 27 tests β grid shape, decay, render, overlay, reset
β βββ test_api.py # 9 tests β FastAPI endpoints, schema validation
βββ assets/
β βββ demo.gif # 6-second animated demo (720p, 12fps)
β βββ test_detection_001.jpg # Sample annotated frame (cars, trucks)
β βββ test_detection_002.jpg
β βββ test_detection_003.jpg
β βββ test_detection_004.jpg
β βββ test_detection_005.jpg
βββ results/
βββ traffic_tracked.mp4 # Full annotated output video (60 s, 2560Γ1440)
βββ traffic_heatmap.png # 60-second density heatmap
| Layer | Technology |
|---|---|
| Detection | YOLOv8n (Ultralytics), pretrained on COCO |
| Tracking | ByteTrack + constant-velocity Kalman filter |
| Analytics | NumPy, OpenCV |
| API | FastAPI, Pydantic, uvicorn |
| Dashboard | Streamlit, Plotly |
| Deployment | Docker, docker-compose (CPU + GPU profiles) |
| CI | GitHub Actions β pytest, flake8, Docker smoke test |
| Video I/O | OpenCV, ffmpeg |
The estimator converts pixel displacement to km/h using a fixed pixels_per_meter
constant (configs/analytics.yaml). This is only a valid approximation when the
camera is mounted directly above the road. For side-angle footage like the sample
clip, perspective distortion means distant vehicles travel fewer pixels per metre
than nearby ones β the estimator will under-report speed for far objects and
over-report for close ones. A 4-point homography calibration would reduce the error
for moderate angles (~45Β°+), but shallow-angle cameras (< 30Β° elevation) violate
the ground-plane assumption entirely. Treat speed figures from non-overhead footage
as indicative only.
Without a measured pixels_per_meter value from a real calibration target in the
scene, all speed and distance figures are estimates. To calibrate:
- Place two markers of known distance (e.g., lane markings are typically 3 m apart in the UK) in the camera's field of view.
- Measure the pixel distance between them in a representative frame.
- Set
pixels_per_meterinconfigs/analytics.yamltopixel_distance / real_distance. - For perspective correction, run
scripts/calibrate_perspective.pywith four ground-plane control points to compute a homography matrix.
ByteTrack assigns each detection an ID across frames, but three scenarios cause consistent failures on dense motorway footage:
-
Occlusion cascades. When three or more vehicles overlap simultaneously (e.g., a truck overtaking two cars in adjacent lanes), the Kalman-predicted bounding boxes of the inner vehicles shrink in apparent size and may no longer overlap with any detection. All three tracks go to LOST simultaneously, and when the vehicles separate the tracker spawns three new IDs β inflating the total count.
-
ID switching on lane changes. A vehicle changing lanes crosses the predicted trajectory of an adjacent vehicle. If their bounding boxes overlap even briefly (IoU > 0.4), the Hungarian algorithm may swap their IDs at that frame. The swap is usually permanent because the Kalman filters have already adopted each other's velocity. The 5-frame speed smoothing window partially masks the resulting speed spike but does not eliminate it.
-
Missed detections at frame edges. Vehicles partially outside the frame have smaller visible bounding boxes with lower YOLO confidence, often falling below the 0.35 confidence threshold. They are detected intermittently, causing rapid TENTATIVE β (miss) β new TENTATIVE β CONFIRMED cycles that generate spurious counts on edge-crossing virtual lines.
Upgrading from yolov8n to yolov8s or yolov8m in configs/detector.yaml
improves recall for partially-visible and occluded vehicles at the cost of throughput
(roughly β30 fps per model size step on the GTX 1650).
The pipeline uses the nano YOLO model for speed. On the sample motorway clip, the
nano model misses roughly 1 in 8 vehicles in the far lane (small apparent size,
low contrast at dusk). Switching to yolov8s.pt recovers most of these detections
with approximately 2Γ the inference time.
Kalman filters are what make tracking feel "real". Without Kalman prediction, IoU
matching fails the moment a vehicle is partially occluded β the bounding box shrinks
and IoU with any detection drops below the threshold. Once I added prediction-based
matching (using track.predicted_bbox instead of track.bbox for IoU), the tracker
bridged 10β15 frame gaps through bridge pillars and lane changes without dropping IDs.
The constant-velocity model is the simplest thing that works; it is also usually enough.
ByteTrack's two-stage design solves a real problem. The standard approach β throw away low-confidence detections β loses heavily-occluded vehicles exactly when you most need to keep their IDs. Stage 2 catches those low-confidence detections and routes them only to tracks that are already unmatched, so they cannot pollute freshly-created tracks. Implementing this from scratch made the design obvious in a way that just reading the paper did not.
Building a production API around a CV pipeline exposes edge cases you would never test
otherwise. The lifespan pattern in FastAPI (loading the model once at startup, reusing
it across requests) seems obvious in hindsight, but the first implementation reloaded
YOLOv8 weights on every /analyze call β a 2-second penalty per request. The global
state for the tracker also needed a reset() call between videos, which I only discovered
when the second uploaded video showed vehicles from the first one still being tracked.
Speed estimation without calibration is decoration, not measurement. I built the full pixel-displacement β km/h pipeline before running it on real footage and realising the numbers were off by a factor of three. The camera angle I was using was approximately 25Β° from horizontal β far too shallow for the ground-plane assumption to hold. The lesson: validate numerical outputs against ground truth before wiring them into a dashboard.
CPU vs GPU throughput gap is larger than I expected at high resolution. At 640Γ640 (YOLO's default input), the CPU is bottlenecked by inference (~50 ms/frame). At 2560Γ1440 source resolution, the bottleneck partially shifts to pre-processing (letterbox resize and normalisation), which costs an extra 10β15 ms on CPU but is almost free on GPU. The result is that the GPU speedup factor is larger at higher resolutions than the inference-only benchmark implies.
Mocking at the right level makes tests fast and honest. Mocking YOLO.__call__ at
the tensor level (making .cpu().numpy() return a known array) let me test all of the
wrapper's filtering and formatting logic without loading model weights. Mocking any higher β
e.g., mocking VehicleDetector.detect β would have left the class-filtering code untested.
Krishna Singh β MSc Artificial Intelligence @ IU Berlin
- GitHub: @krishna8399
- LinkedIn: krishna839
MIT β see LICENSE for details.