Skip to main content

Visual overlay generator for sports data from FIT files

Project description

Overlayer - Visual Overlay Generator for Sports Data

Python 3.10+ License: MIT GitHub release

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.variant changes the shared color palette and drawing tokens.
  • <module>.variant changes 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_minimal
  • gauge.variant: cockpit_bar, analog_arc, ride_minimal
  • map.variant: tactical_panel, clean_trace, ride_minimal
  • stats.variant: telemetry_cards, compact_strip, ride_minimal
  • heart_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:

  1. Edit module positions and sizes in config.json.
  2. Run overlayer preview ....
  3. Open preview.png.
  4. Repeat until the layout looks right.

Useful layout fields:

  • map.x, map.y, map.width, map.height
  • gauge.panel_x, gauge.panel_y, gauge.panel_width, gauge.panel_height
  • speed_display.x, speed_display.y, speed_display.width, speed_display.height
  • time.x, time.y, time.width, time.height
  • distance.x, distance.y, distance.width, distance.height
  • stats.x, stats.y, stats.card_width, stats.card_height, stats.columns, stats.gap
  • heart_rate_chart.x, heart_rate_chart.y, heart_rate_chart.width, heart_rate_chart.height
  • power_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

  • fitparse - Python library for parsing FIT files
  • OpenCV - Computer vision library
  • Pydantic - Data validation using Python type hints

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

overlayer-2.0.0.tar.gz (218.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

overlayer-2.0.0-py3-none-any.whl (57.4 kB view details)

Uploaded Python 3

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

Hashes for overlayer-2.0.0.tar.gz
Algorithm Hash digest
SHA256 7e03e14e91ab972b3d263176985656d90c4c4a0dda715f2601bae805f5129a4d
MD5 94c7a6a3717c0cfb86521b5c4e6716d5
BLAKE2b-256 31a3344d80ee47159e38e087e3320a3f93353fbe34a516cedbb809236b0ee902

See more details on using hashes here.

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

Hashes for overlayer-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ce5e1c9480e5c78ca734844a400ee8fa58087f36a885bfa3a8b8c68c8669fb60
MD5 7c2d2c0be8deb8fbcd43ec3525338fe5
BLAKE2b-256 25ec849f6a45b5fb7c44576fd8dbcd315d4afb7dc6b4e6fe6ee9f1c104746cce

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page