Skip to content

finnetrolle/overlayer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

Install the CLI with uv (recommended)

# Install the command-line app globally
uv tool install overlayer

# If your shell does not see the command yet
uv tool update-shell

# Verify the install
overlayer --version
overlayer --help

Run without installing

# Run the latest published version once
uvx overlayer --version

# Example: inspect a FIT file without a permanent install
uvx overlayer info ride.fit

Use as a library dependency

uv add overlayer

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)
uv sync --all-extras

# Run the local CLI from the virtual environment
uv run overlayer --help

# Or with pip
pip install -e ".[dev]"

Quick Start

60-Second First Run

You need one FIT file, for example ride.fit, exported from a bike computer or sports watch.

# Install the CLI
uv tool install overlayer

# Create a starter config you can edit
overlayer config -o config.json

# Render one preview frame to tune the layout
overlayer preview ride.fit -c config.json -o preview.png

# Generate the full PNG frame sequence
overlayer generate ride.fit -c config.json -o frames

After that:

  1. Open preview.png
  2. Tweak positions and variants in config.json
  3. Re-run overlayer preview ... until the layout looks right
  4. Run overlayer generate ... for the full sequence

Typical End-to-End Workflow

If you are using Overlayer for the first time, this is the simplest path from FIT file to finished overlay assets:

# 1. Inspect what is inside the FIT file
overlayer info ride.fit

# 2. Generate a starter config
overlayer config -o config.json

# 3. Render one preview frame and adjust the layout
overlayer preview ride.fit -c config.json -o preview.png

# 4. Render all transparent PNG frames
overlayer generate ride.fit -c config.json -o frames --fps 30

# 5. Optional: see which built-in modules are available
overlayer modules

The generated frames are saved as transparent PNG files like frames/frame_000000.png.

If you want to place them over a video in ffmpeg, a typical command looks like this:

ffmpeg \
  -i input.mp4 \
  -framerate 30 -i frames/frame_%06d.png \
  -filter_complex "[0:v][1:v]overlay=0:0" \
  -c:a copy \
  output-with-overlay.mp4

Adjust the -framerate value so it matches the --fps value you used for overlayer generate.

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

Common Installation Flows

# Install once and use it as a normal command
uv tool install overlayer
overlayer --help

# Run it one time without installing
uvx overlayer modules

# Upgrade to the latest published version
uv tool upgrade overlayer

What Each Command Does

  • overlayer info ride.fit prints the FIT file duration, timestamps, track points, max speed, and available data fields.
  • overlayer config -o config.json writes a starter JSON config you can edit.
  • overlayer preview ride.fit -c config.json -o preview.png renders one frame for fast layout tuning.
  • overlayer generate ride.fit -c config.json -o frames renders the full transparent PNG sequence.
  • overlayer modules lists the built-in overlay modules you can enable with -m.

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

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Languages