Skip to content

krishna8399/traffic-tracking-analytics

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

15 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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

Multi-Object Traffic Tracking & Analytics

CI Python YOLOv8 FastAPI Docker License

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.


Demo

Traffic tracking demo

YOLOv8n + ByteTrack live on UK motorway footage β€” 6 s clip, 12 fps. Track IDs persist through partial occlusions; trajectory tails show last 30 frames.


Sample Output

Multi-lane vehicle detection and tracking β€” UK motorway, dusk, 2560Γ—1440

Frame 1 Frame 2
Detection 1 Detection 2

Green boxes = cars Β· Orange = trucks Β· Blue = motorcycles Β· Labels show track ID and confidence

Traffic density heatmap (60 s clip, decay = 0.98):

Heatmap

Hot spots correspond to the main travel lanes; the hard shoulder stays cold.


Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        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            β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Results

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 up and 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; run scripts/benchmark.py with CUDA enabled to measure it precisely on your setup.


Quick Start

Option 1: Docker (CPU)

docker-compose up --build
# API:       http://localhost:8000/docs
# Dashboard: http://localhost:8501

Option 2: Docker (GPU β€” GTX 1650 / any NVIDIA)

# Requires: NVIDIA Container Toolkit
docker-compose --profile gpu up --build

Option 3: Local

conda 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.py

Project Structure

traffic-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

Tech Stack

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

Limitations

Speed estimation requires a near-overhead camera

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.

Camera calibration is a prerequisite for reliable metrics

Without a measured pixels_per_meter value from a real calibration target in the scene, all speed and distance figures are estimates. To calibrate:

  1. 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.
  2. Measure the pixel distance between them in a representative frame.
  3. Set pixels_per_meter in configs/analytics.yaml to pixel_distance / real_distance.
  4. For perspective correction, run scripts/calibrate_perspective.py with four ground-plane control points to compute a homography matrix.

Tracking failures in dense traffic

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).

YOLOv8n accuracy trade-off

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.


What I Learned

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.


Author

Krishna Singh β€” MSc Artificial Intelligence @ IU Berlin


License

MIT β€” see LICENSE for details.

About

Multi-object traffic analysis system using YOLOv8 detection + ByteTrack tracking. Counts vehicles via virtual line-crossing, estimates speed, generates density heatmaps, and serves results through a FastAPI backend with an interactive Streamlit dashboard. Dockerized with multi-service compose.

Topics

Resources

License

Contributing

Stars

3 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors