Visual overlay generator for sports data from FIT files
Project description
Overlayer - Visual Overlay Generator for Sports Data
A modern Python application for generating visual overlays from FIT files created by sports devices like bike computers (Garmin, Magene, Wahoo) and GPS watches.
overlayer generate, overlayer preview, overlayer info, and overlayer modules run on the new v2 pipeline. The old FrameGenerator, legacy module runtime, and legacy FIT processor have been removed from the active codebase, so new development should target GenerateService and overlayer.v2.
Features
- ๐บ๏ธ GPS Track Map - Visualize your route with current position
- ๐ด Speedometer - Horizontal or analog gauge showing current speed
- โค๏ธ Heart Rate Chart - Bar, line, or area heart-rate graph
- ๐ Statistics - Display all available metrics (heart rate, cadence, power, etc.)
- โฑ๏ธ Time Display - Current activity time
- ๐๏ธ Module Variants - Mix themes with per-module rendering styles
- ๐ Modular Architecture - Easy to extend with custom modules and renderer variants
- โ๏ธ Flexible Configuration - JSON config or environment variables
Installation
Using pip
pip install overlayer
From source (recommended for development)
# Clone the repository
git clone https://github.com/finnetrolle/overlayer.git
cd overlayer
# Install with uv (recommended)
pip install uv
uv sync
# Or with pip
pip install -e ".[dev]"
Quick Start
Command Line Interface
# Generate overlays from a FIT file
overlayer generate ride.fit
# Specify output directory and FPS
overlayer generate ride.fit -o output_frames --fps 2
# Generate only specific modules
overlayer generate ride.fit -m map -m speedometer
# Use custom config file
overlayer generate ride.fit -c config.json
# Render one preview frame for layout tuning
overlayer preview ride.fit -c config.json -o preview.png
# Get info about a FIT file
overlayer info ride.fit
# List available modules
overlayer modules
# Generate default config
overlayer config -o config.json
As a Library
from overlayer import AppConfig, GenerateService
# Load configuration
config = AppConfig.from_json("config.json")
# Generate frames
generator = GenerateService(config)
total_frames = generator.generate(
fit_file="ride.fit",
output_dir="frames",
fps=1,
duration=0, # 0 = full duration
)
print(f"Generated {total_frames} frames")
Configuration
Configuration is managed via JSON file or environment variables.
Theme vs Module Variant
There are now two visual controls:
theme.variantchanges the shared color palette and drawing tokens.<module>.variantchanges how an individual built-in module is rendered.
Examples:
theme.variant = "street_racer"keeps the same modules, but changes the palette.gauge.variant = "analog_arc"changes the speedometer geometry.heart_rate_chart.variant = "line"changes the chart style without changing the theme.map.variant = "clean_trace"swaps the tactical HUD map for a cleaner route trace.*.variant = "ride_minimal"switches to the new low-chrome action-cam inspired style.
Available module variants:
time.variant,distance.variant,speed_display.variant:cyberpunk_panel,minimal,broadcast_bug,ride_minimalgauge.variant:cockpit_bar,analog_arc,ride_minimalmap.variant:tactical_panel,clean_trace,ride_minimalstats.variant:telemetry_cards,compact_strip,ride_minimalheart_rate_chart.variant,power_chart.variant:bars,line,area,ride_minimal
Layout Tuning With preview
Use preview when you want to place modules on screen without generating a full frame sequence.
# Render one frame from the middle of the activity
overlayer preview ride.fit -c config.json -o preview.png
# Render a specific moment of the ride
overlayer preview ride.fit -c config.json -o preview.png --at-seconds 120
# Focus only on a few modules while tuning
overlayer preview ride.fit -c config.json -o preview.png -m speedometer -m stats -m map
Fast workflow:
- Edit module positions and sizes in
config.json. - Run
overlayer preview .... - Open
preview.png. - Repeat until the layout looks right.
Useful layout fields:
map.x,map.y,map.width,map.heightgauge.panel_x,gauge.panel_y,gauge.panel_width,gauge.panel_heightspeed_display.x,speed_display.y,speed_display.width,speed_display.heighttime.x,time.y,time.width,time.heightdistance.x,distance.y,distance.width,distance.heightstats.x,stats.y,stats.card_width,stats.card_height,stats.columns,stats.gapheart_rate_chart.x,heart_rate_chart.y,heart_rate_chart.width,heart_rate_chart.heightpower_chart.x,power_chart.y,power_chart.width,power_chart.height
JSON Configuration
Create a config.json file (see config.example.json for a complete example). If you want to start directly with the new minimal style, use config.ride-minimal.json.
{
"frame": {
"width": 1920,
"height": 1080
},
"map": {
"variant": "tactical_panel",
"x": 1450,
"y": 610,
"width": 450,
"height": 450,
"margin": 20
},
"gauge": {
"variant": "cockpit_bar",
"center_x": 150,
"center_y": 930,
"radius": 120,
"start_angle": -135,
"end_angle": 135,
"panel_x": 320,
"panel_y": 850,
"panel_width": 1280,
"panel_height": 150
},
"time": {
"variant": "cyberpunk_panel",
"x": 1690,
"y": 24,
"width": 210,
"height": 68,
"font_scale": 1.0,
"color": [255, 255, 255, 255]
},
"distance": {
"variant": "cyberpunk_panel",
"x": 1690,
"y": 104,
"width": 210,
"height": 68,
"font_scale": 0.8,
"color": [255, 255, 255, 255]
},
"stats": {
"variant": "telemetry_cards",
"x": 10,
"y": 30,
"line_height": 30,
"font_scale": 0.7,
"card_width": 180,
"card_height": 96,
"columns": 3,
"gap": 18,
"max_cards": 6,
"color": [0, 255, 0, 255]
},
"speed_display": {
"variant": "broadcast_bug",
"x": 1390,
"y": 754,
"width": 210,
"height": 80
},
"heart_rate_chart": {
"variant": "line",
"x": 500,
"y": 900,
"width": 400,
"height": 150,
"history_seconds": 60,
"bar_gap": 3,
"zones": {
"zone1_max": 110,
"zone2_max": 130,
"zone3_max": 150,
"zone4_max": 165
}
},
"power_chart": {
"variant": "area",
"x": 930,
"y": 900,
"width": 400,
"height": 150,
"history_seconds": 60,
"bar_gap": 3
},
"theme": {
"variant": "neon_cockpit"
},
"modules": ["time", "distance", "map", "speedometer", "speed_display", "stats", "heart_rate_chart", "power_chart"],
"output_dir": "frames",
"duration": 0,
"fps": 1
}
Environment Variables
All configuration options can be set via environment variables with the OVERLAYER_ prefix:
export OVERLAYER_FRAME__WIDTH=1920
export OVERLAYER_FRAME__HEIGHT=1080
export OVERLAYER_FPS=2
export OVERLAYER_MODULES='["map", "speedometer"]'
export OVERLAYER_THEME__VARIANT=street_racer
export OVERLAYER_GAUGE__VARIANT=analog_arc
export OVERLAYER_HEART_RATE_CHART__VARIANT=line
Project Structure
overlayer/
โโโ src/overlayer/
โ โโโ __init__.py # Package exports
โ โโโ __main__.py # Entry point (python -m overlayer)
โ โโโ cli.py # Typer CLI commands
โ โโโ core/
โ โ โโโ config.py # Shared Pydantic configuration
โ โ โโโ constants.py # Physical constants
โ โ โโโ __init__.py # Shared core exports
โ โโโ v2/
โ โโโ fit_reader.py # FIT -> ActivityData
โ โโโ timeline.py # Fast time-indexed access
โ โโโ frame_state.py # Per-frame state for modules
โ โโโ surface.py # RGBA surface abstraction
โ โโโ compositor.py # Alpha compositing
โ โโโ writer.py # PNG output
โ โโโ view_models.py # Shared presenter/renderer models
โ โโโ generate_service.py # Main v2 pipeline
โ โโโ presenters/ # Build per-module view models
โ โโโ renderers/ # Variant-specific renderers
โ โโโ styles/ # Theme tokens and drawing primitives
โ โโโ modules/ # Semantic built-in modules
โโโ tests/
โโโ pyproject.toml # Project configuration
โโโ README.md
Creating Custom Modules
Create new modules against the v2 contract:
import cv2
from overlayer.v2 import BaseModule, FrameState, Surface
class MyCustomModule(BaseModule):
name = "custom"
def render(self, surface: Surface, frame_state: FrameState) -> None:
cv2.putText(
surface.pixels,
f"Speed: {frame_state.current_speed_kmh:.1f} km/h",
(100, 100),
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(255, 255, 255, 255),
2,
)
Register the module in a ModuleRegistry and pass that registry to GenerateService.
Built-in modules now use an internal presenter + renderer + style split. For a quick custom module, subclassing BaseModule is still perfectly fine. If you want multiple rendering styles for one custom module, follow the same internal pattern used by the built-ins and keep data preparation separate from drawing.
Development
Setup
# Install development dependencies
uv sync --all-extras
# Or with pip
pip install -e ".[dev]"
Testing
# Run tests
pytest
# Run with coverage
pytest --cov=overlayer
Type Checking
mypy src/overlayer
Linting
ruff check src/overlayer
ruff format src/overlayer
Requirements
- Python 3.10+
- OpenCV (opencv-python)
- NumPy
- fitparse
- Pydantic v2
- Typer
- Rich
- structlog
License
This project is licensed under the MIT License - see the LICENSE file for details.
Contributing
Contributions are welcome! Please read the Contributing Guide for details on our code of conduct and the process for submitting pull requests.
Changelog
See CHANGELOG.md for a list of changes.
Acknowledgments
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file overlayer-2.0.0.tar.gz.
File metadata
- Download URL: overlayer-2.0.0.tar.gz
- Upload date:
- Size: 218.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e03e14e91ab972b3d263176985656d90c4c4a0dda715f2601bae805f5129a4d
|
|
| MD5 |
94c7a6a3717c0cfb86521b5c4e6716d5
|
|
| BLAKE2b-256 |
31a3344d80ee47159e38e087e3320a3f93353fbe34a516cedbb809236b0ee902
|
File details
Details for the file overlayer-2.0.0-py3-none-any.whl.
File metadata
- Download URL: overlayer-2.0.0-py3-none-any.whl
- Upload date:
- Size: 57.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce5e1c9480e5c78ca734844a400ee8fa58087f36a885bfa3a8b8c68c8669fb60
|
|
| MD5 |
7c2d2c0be8deb8fbcd43ec3525338fe5
|
|
| BLAKE2b-256 |
25ec849f6a45b5fb7c44576fd8dbcd315d4afb7dc6b4e6fe6ee9f1c104746cce
|