Generate a topographic / survey-style trail-running poster from a photo + a transparent Strava route PNG. The runner is segmented and placed in front of the route; concentric topographic "echo" rings radiate from the GPS trace; stats sit in a modular grid below (Distance · Time · Pace + GAP · Elevation Gain).
Two front-ends share one look:
web/— Browser studio. Fully client-side. Real-time preview with adjustable knobs (route scale/position, line thickness, echo intensity, darkness), runner cut out in the browser via RMBG-2.0 (BiRefNet). No server compute; the photo never leaves the device.app/— Python CLI + tiny server. One-shot rendering withrembg/ OpenCV / Pillow. Stdlib-only HTTP server, no framework.
Static files — serve the web/ directory with any static web server (it uses ES modules
and a Web Worker, so it must be served over HTTP, not opened as file://).
# 1. Fetch runtime assets (transformers.js, ORT WASM, fonts, the gated model)
bash web/assets/fetch-assets.sh # see web/assets/README.md (needs an HF token)
# 2. Serve it
cd web && python3 -m http.server 8099
# open http://127.0.0.1:8099/Upload a runner photo + the Strava route PNG, fill in the fields, drag the knobs, and download the poster (rendered at full resolution).
Browser support: any modern browser renders the poster. The runner cutout uses a large model — a WebGPU browser (Chrome / Edge) is strongly recommended; without it the model falls back to WASM, which is much slower and memory-hungry. If the model can't load, the studio degrades gracefully to drawing the route over the runner (no cutout).
pip install pillow numpy opencv-python rembg
python3 app/trail_poster.py \
--photo run.jpg --route route.png --output poster.jpg \
--title "SOARING EAGLE" \
--location "SAMMAMISH · WA · 47.6062° N · 122.0356° W" \
--distance "13.42 MI" --time "2H 20M" \
--pace "10:26 /MI" --gap "9:58 /MI" --elev "1,257 FT"Or run the bundled server (then reverse-proxy it; it has no auth of its own):
python3 app/server.py --host 127.0.0.1 --port 8765- Photo — JPG/PNG of you on the trail. Tall portrait composes best.
- Route overlay PNG — Strava's transparent share image (the route silhouette on a transparent background): Strava → activity → share → "Share image" → transparent route. A baked-in stats footer is auto-detected and cropped.
- Stats — free-text fields; format them however you like.
The studio is static and has no authentication of its own — put it behind your web
server's auth. Example nginx (basic-auth, no-cache, noindex) and a systemd unit for the
Python server live in deploy/. Generate a real htpasswd from
deploy/.htpasswd-strava.example and keep it out of
git.
cd web
node --test tests/ # unit: distance transform, route crop, design tokens
npx playwright test # smoke test (segmentation mocked, no model download)The render is a port of app/trail_poster.py onto Canvas 2D. A single Euclidean
distance transform of the route trace drives the white line, its dark outline, and the
topographic echo rings (so line-thickness and echo-intensity are cheap threshold/opacity
tweaks). Segmentation runs once per photo in a Web Worker and is cached; knob changes only
re-run the fast compositing pass, at a downscaled preview resolution for responsiveness,
then re-render at full resolution on download. Design notes:
docs/.
- This code: MIT (see LICENSE).
- RMBG-2.0 model: governed by BRIA's separate license (non-commercial terms). It
is not bundled — you fetch it yourself after accepting the terms. See
web/assets/README.md. - Fonts: generated from DejaVu (freely redistributable).