Skip to main content

Turn any SVG shape into a runnable GPS-art route on real city streets.

Project description

svg2gpx

Turn any SVG shape into a runnable GPS art route on real city streets.
The automatic, open-source way to make Strava art — no hand-drawing required.

Python 3.9+ License: MIT PRs welcome Built with NumPy, SciPy, Shapely, OSMnx

Shapes routed on the real Chicago street network
Every bundled shape, routed on the real Chicago street network. Orange = the runnable route, blue dashed = the target outline, orange interior lines = inner features (the face's eyes and smile, the donut's hole).


svg2gpx takes an SVG silhouette and a location, lays the shape over a city's walkable street network, and generates a single closed running route whose path resembles the shape — a boar, a star, a heart — drawn in streets you can actually run or ride. Unlike the hand-draw GPS art planners, it fits and routes the shape automatically, and it ships a fidelity engine that measures how faithfully the route reproduces your shape — so quality is a number you can track and tune, not just something you eyeball.

Routes export as GPX (ready for Strava, Garmin, or Komoot), GeoJSON (WGS84), and map images.

✨ Why svg2gpx

  • 🎨 Automatic, not hand-drawn. Feed it an <svg> — it searches scale, rotation, offset and stretch to seat the figure on the streets and routes it for you. No dragging a pen across a map.
  • 🧭 Real street networks. Snaps to the actual walkable graph from OpenStreetMap (via OSMnx), so every route is a connected walk on real roads.
  • 📐 A fidelity engine, not a guess. Seven complementary metrics — Fréchet, Hausdorff, IoU, DTW, turning distance, a perceptual render-compare, and a feature ledger — score how recognizably the route reads as the shape.
  • 👀 Inner features. Eyes, a smile, a donut's hole, a wing line — interior detail is extracted and drawn too, not just the silhouette.
  • 🧠 Per-shape engine. A compactness test routes blobby shapes and elongated/protruding ones through the strategy that measured best for each.
  • 🔁 Reproducible. A fast synthetic-grid mode runs offline and deterministically for CI and benchmarking — no network required.

🚀 Quick start

git clone https://github.com/Chieler/svg2gpx.git
cd svg2gpx

pip install -e .            # core: synthetic-grid runs, offline
pip install -e ".[osm]"     # + real OpenStreetMap data & plotting

skia-python needs system GL libraries on Linux:

sudo apt-get install -y libegl1 libgl1

Generate your first route and export it as GPX:

svg2gpx --svg star --lat 41.9285 --lng -87.7075 --save star.png --gpx star.gpx

--svg takes any bundled shape stem (star, Horse, donut, …) or a path to your own SVG. star.gpx is ready to import into Strava, Garmin, or Komoot.

🐍 Use it from Python

One call: give it a location and a shape, get a route back.

from svg2gpx import get_route

route = get_route(41.9285, -87.7075, "star")   # lat, lng, shape (stem or .svg path)

route.to_gpx("star.gpx")                        # Strava / Garmin / Komoot-ready
route.plot()                                    # quick matplotlib look (or save="star.png")
print(route.distance_km, route.iou)             # 10.8, 0.33
coords = route.latlon                           # (N, 2) array of (lat, lon)

Common options: radius_m (bigger = higher fidelity, longer route), granularity (0 smooth … 1 detailed), seed (reproducible), graphml (route on a saved OSMnx network, offline), engine, or any CONFIG key as a keyword. Requires the [osm] extra.

🖼️ Gallery

Pick the placement that reads best, tune detail, or let the shape choose its own engine — the search returns several routings so you can eyeball the winner.

Five detail/engine options per shape Fidelity across scales (how short a route can still read)
engine options scale ladder

🧠 How it works

The pipeline (gen.py) runs end to end:

Stage Function What it does
1. Build grid build_grid Pull and normalize the walkable street network (and parks) into [0, 1] space.
2. Extract shape extract_shape Render the SVG and trace its outer outline and inner features as polylines.
3. Search placement search_placement Find the scale / rotation / offset / stretch that seats the shape on the streets with the best routed fidelity.
4. Snap waypoints snap_waypoints Densify the placed outline and snap points to street nodes — dense anchors so each hop barely deviates.
5. Route route_contour Walk consecutive anchors with a contour-biased Dijkstra so the path hugs the shape.
6. Cleanup + plot cleanup, plot Close the loop, dissolve backtracks / combs / nooks, report fidelity, draw.

Fidelity comes from dense waypoints: spacing anchors well below one block means each Dijkstra hop is short and has little room to stray. The dominant quality lever is resolution (blocks per shape) — a bigger canvas or a denser street fabric reads better, at the cost of a longer route.

📐 Fidelity metrics

Each metric catches a failure the others miss (all in gen.py):

Metric Answers
Fréchet Order-aware worst-case leash — punishes out-of-sequence detours.
Hausdorff The single largest excursion from the outline.
IoU Area overlap of the two thickened outlines.
Perceptual cost Blur-tolerant render-and-compare (1 − soft-IoU) — the gestalt the eye sees.
DTW Cyclic dynamic time warping — rewards hugging the outline everywhere, not just at the worst point.
Turning distance Scale/rotation-invariant measure of form (corners, protrusions) that ignores staircase jitter.
Feature ledger Recall / precision of the shape's defining corners — catches a feature vanishing when IoU can't.
On-land % · distance Runnability sanity checks.

Read together they tell you how a result is good or bad — path order (Fréchet/DTW), one bad excursion (Hausdorff), overall area (IoU), and whether the identity-carrying corners landed (turning distance, feature ledger).

🛠️ Usage

Generate a route
svg2gpx                                             # CONFIG defaults
svg2gpx --svg Crow --granularity 0.8
svg2gpx --svg star --lat 41.9285 --lng -87.7075 --save route.png --gpx route.gpx --no-show

Common knobs are CLI flags (--svg, --lat/--lng/--radius, --granularity, --graphml, --seed, --save, --gpx, --no-show, --no-inner-features); everything else is tuned from svg2gpx.CONFIG. --graphml loads a saved OSMnx network for offline / reproducible runs. python -m svg2gpx works identically to the svg2gpx command.

Trace every shape on the real Chicago map
python -m svg2gpx.chicago_map                 # all shapes, Logan Square window
python -m svg2gpx.chicago_map --shape star    # one shape
python -m svg2gpx.chicago_map --live          # fetch fresh OSM data instead

Renders each route on the real OSMnx map plus a gallery image, and writes per-shape GeoJSON (WGS84) and a metrics CSV to chicago_maps/.

Benchmark fidelity across shapes
python -m svg2gpx.benchmark                 # synthetic grid, all shapes (offline, CI-friendly)
python -m svg2gpx.benchmark --grid-size 60  # finer lattice
python -m svg2gpx.benchmark --real          # real OSM (cached on disk)
python -m svg2gpx.benchmark --json          # also write benchmark_results.json
Pick the best route per shape
python -m svg2gpx.best_route                # all shapes, synthetic grid
python -m svg2gpx.best_route --shape star   # just one shape
python -m svg2gpx.best_route --grid real    # real OSM

Routes the top candidate placements, selects the lowest-cost one, and upserts its metrics into result.csv — one "best route" row per shape.

🧩 Shapes

Eighteen SVGs ship with the package (see src/svg2gpx/shapes/) — animals (Horse, Shark, Crow, Cat, pig, duck, whale, ghost), figures (Knight, Pawn, face), and geometric primitives (square, circle, star, heart, donut, mushroom, lshape). Pass any of these as a bare --svg stem, or point --svg at your own SVG file — no code changes needed either way.

Inner features

extract_shape() finds a shape's inner features from the raster's ink/paper contour tree and routes them alongside the outline:

  • holes — a donut's hole, an eye (closed loops);
  • disconnected elements — a face's eyes and smile (closed loops);
  • interior strokes — a wing line, a horse's mane (open paths, run as out-and-back spurs).

Placement folds each candidate's feature fidelity into its cost, so a route that seats the body nicely but strands the eye ranks below one that draws both. Small features get extra rescues (feature-scaled smoothing and per-feature re-seating on the local street fabric). Toggle with inner_features=False or --no-inner-features. Visual check: python -m svg2gpx.preview_features.

📊 Continuous fidelity tracking

The Best Route GitHub Action (.github/workflows/best-route.yml) runs svg2gpx.best_route on demand (workflow_dispatch) and commits the updated result.csv back to the repo, so fidelity is tracked over time.

🗺️ Roadmap

  • GPX export--gpx route.gpx writes a Strava / Garmin / Komoot-ready track.
  • PyPI packagepip install svg2gpx.
  • Walk-network resolution — alleys and footpaths for ~2× finer routes.
  • Semantic recognizability judge — a sketch classifier as a dev-time oracle.

📦 Repository layout

pyproject.toml              # package metadata, the svg2gpx console entry point
src/svg2gpx/
  gen.py                    # the full SVG -> street-route pipeline + fidelity metrics
  cli.py                    # the svg2gpx command (CONFIG overrides + --gpx)
  gpx.py                    # GPX 1.1 export
  chicago_map.py            # route every shape on the real Chicago OSM network
  benchmark.py              # fidelity + runtime benchmark over all shapes
  best_route.py             # best-of-N selection -> result.csv
  preview_features.py       # visualize extracted inner features
  shapes/                   # bundled sample SVGs (package data)
tests/                       # routing + inner-feature checks
docs/                        # design notes and comparison figures
.github/workflows/           # Best Route GitHub Action

🤝 Contributing

Issues and PRs are welcome. Before opening a PR:

pip install -e ".[osm,dev]"

python tests/test_routing.py          # routing / connectivity
python tests/test_inner_features.py   # inner-feature extraction
python -m svg2gpx.benchmark           # fidelity smoke on the synthetic grid

📄 License

MIT © Chieler.


Keywords: GPS art · Strava art generator · GPS drawing · SVG to GPX · SVG to route · running route art · GPX route maker · route art · OpenStreetMap · running · cycling · fitness map art.

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

svg2gpx-0.4.0.tar.gz (73.5 kB view details)

Uploaded Source

Built Distribution

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

svg2gpx-0.4.0-py3-none-any.whl (73.3 kB view details)

Uploaded Python 3

File details

Details for the file svg2gpx-0.4.0.tar.gz.

File metadata

  • Download URL: svg2gpx-0.4.0.tar.gz
  • Upload date:
  • Size: 73.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for svg2gpx-0.4.0.tar.gz
Algorithm Hash digest
SHA256 c12ed3f7bfd464e43a2b551754f3b9e0d7d666e660ba0928be2d1cb7c1d73fa1
MD5 2c4598935e3597afc1fcf20db750fd50
BLAKE2b-256 0e8f30a34a386f1b90a835c1a01844757d0999a0ead6987c59770cba32ba0619

See more details on using hashes here.

File details

Details for the file svg2gpx-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: svg2gpx-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 73.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for svg2gpx-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e79f62b4029080572cdbe4e1f734cbc0214d5a125246f7b125999b6c819f7246
MD5 657e7132a8a71f99e46a3ed35668b7c9
BLAKE2b-256 14ff0ce262c14dd3d02ea00f1465f2ce8d3b5be809bb4f18fec47644b60fcaa3

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