svg2gpx 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. svg2gpx-0.4.0/LICENSE +21 -0
  2. svg2gpx-0.4.0/PKG-INFO +326 -0
  3. svg2gpx-0.4.0/README.md +273 -0
  4. svg2gpx-0.4.0/pyproject.toml +47 -0
  5. svg2gpx-0.4.0/setup.cfg +4 -0
  6. svg2gpx-0.4.0/src/svg2gpx/__init__.py +22 -0
  7. svg2gpx-0.4.0/src/svg2gpx/__main__.py +4 -0
  8. svg2gpx-0.4.0/src/svg2gpx/benchmark.py +301 -0
  9. svg2gpx-0.4.0/src/svg2gpx/best_route.py +156 -0
  10. svg2gpx-0.4.0/src/svg2gpx/chicago_map.py +326 -0
  11. svg2gpx-0.4.0/src/svg2gpx/cli.py +69 -0
  12. svg2gpx-0.4.0/src/svg2gpx/gen.py +2406 -0
  13. svg2gpx-0.4.0/src/svg2gpx/gpx.py +30 -0
  14. svg2gpx-0.4.0/src/svg2gpx/preview_features.py +74 -0
  15. svg2gpx-0.4.0/src/svg2gpx/shapes/Cat.svg +89 -0
  16. svg2gpx-0.4.0/src/svg2gpx/shapes/Crow.svg +5 -0
  17. svg2gpx-0.4.0/src/svg2gpx/shapes/Horse.svg +22 -0
  18. svg2gpx-0.4.0/src/svg2gpx/shapes/Knight.svg +5 -0
  19. svg2gpx-0.4.0/src/svg2gpx/shapes/Pawn.svg +2 -0
  20. svg2gpx-0.4.0/src/svg2gpx/shapes/Shark.svg +25 -0
  21. svg2gpx-0.4.0/src/svg2gpx/shapes/__init__.py +25 -0
  22. svg2gpx-0.4.0/src/svg2gpx/shapes/circle.svg +3 -0
  23. svg2gpx-0.4.0/src/svg2gpx/shapes/donut.svg +5 -0
  24. svg2gpx-0.4.0/src/svg2gpx/shapes/duck.svg +5 -0
  25. svg2gpx-0.4.0/src/svg2gpx/shapes/face.svg +6 -0
  26. svg2gpx-0.4.0/src/svg2gpx/shapes/ghost.svg +5 -0
  27. svg2gpx-0.4.0/src/svg2gpx/shapes/heart.svg +5 -0
  28. svg2gpx-0.4.0/src/svg2gpx/shapes/lshape.svg +3 -0
  29. svg2gpx-0.4.0/src/svg2gpx/shapes/mushroom.svg +5 -0
  30. svg2gpx-0.4.0/src/svg2gpx/shapes/pig.svg +6 -0
  31. svg2gpx-0.4.0/src/svg2gpx/shapes/square.svg +3 -0
  32. svg2gpx-0.4.0/src/svg2gpx/shapes/star.svg +3 -0
  33. svg2gpx-0.4.0/src/svg2gpx/shapes/whale.svg +5 -0
  34. svg2gpx-0.4.0/src/svg2gpx.egg-info/PKG-INFO +326 -0
  35. svg2gpx-0.4.0/src/svg2gpx.egg-info/SOURCES.txt +39 -0
  36. svg2gpx-0.4.0/src/svg2gpx.egg-info/dependency_links.txt +1 -0
  37. svg2gpx-0.4.0/src/svg2gpx.egg-info/entry_points.txt +2 -0
  38. svg2gpx-0.4.0/src/svg2gpx.egg-info/requires.txt +13 -0
  39. svg2gpx-0.4.0/src/svg2gpx.egg-info/top_level.txt +1 -0
  40. svg2gpx-0.4.0/tests/test_inner_features.py +65 -0
  41. svg2gpx-0.4.0/tests/test_routing.py +192 -0
svg2gpx-0.4.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chieler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
svg2gpx-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,326 @@
1
+ Metadata-Version: 2.4
2
+ Name: svg2gpx
3
+ Version: 0.4.0
4
+ Summary: Turn any SVG shape into a runnable GPS-art route on real city streets.
5
+ Author-email: Chieler <chielerli@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Chieler
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/Chieler/svg2gpx
29
+ Project-URL: Issues, https://github.com/Chieler/svg2gpx/issues
30
+ Keywords: gps art,strava art,gpx,svg,running,route,openstreetmap,gps drawing,route art
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Intended Audience :: End Users/Desktop
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Topic :: Scientific/Engineering :: GIS
37
+ Classifier: Topic :: Multimedia :: Graphics
38
+ Requires-Python: >=3.9
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: numpy
42
+ Requires-Dist: scipy
43
+ Requires-Dist: shapely
44
+ Requires-Dist: opencv-python-headless
45
+ Requires-Dist: skia-python
46
+ Provides-Extra: osm
47
+ Requires-Dist: osmnx; extra == "osm"
48
+ Requires-Dist: matplotlib; extra == "osm"
49
+ Provides-Extra: dev
50
+ Requires-Dist: build; extra == "dev"
51
+ Requires-Dist: twine; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ <h1 align="center">svg2gpx</h1>
55
+
56
+ <p align="center">
57
+ <b>Turn any SVG shape into a runnable <a href="#">GPS&nbsp;art</a> route on real city streets.</b><br>
58
+ The automatic, open-source way to make <b>Strava art</b> β€” no hand-drawing required.
59
+ </p>
60
+
61
+ <p align="center">
62
+ <img src="https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white" alt="Python 3.9+">
63
+ <img src="https://img.shields.io/badge/License-MIT-2ea44f" alt="License: MIT">
64
+ <img src="https://img.shields.io/badge/PRs-welcome-ff69b4" alt="PRs welcome">
65
+ <img src="https://img.shields.io/badge/built%20with-NumPy%20Β·%20SciPy%20Β·%20Shapely%20Β·%20OSMnx-f7931e" alt="Built with NumPy, SciPy, Shapely, OSMnx">
66
+ </p>
67
+
68
+ <p align="center">
69
+ <img src="chicago_maps/all_shapes_chicago.png" alt="Shapes routed on the real Chicago street network" width="820">
70
+ <br>
71
+ <sub><i>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).</i></sub>
72
+ </p>
73
+
74
+ ---
75
+
76
+ **svg2gpx** takes an SVG silhouette and a location, lays the shape over a city's
77
+ walkable street network, and generates a single **closed running route** whose
78
+ path resembles the shape β€” a boar, a star, a heart β€” drawn in streets you can
79
+ actually run or ride. Unlike the hand-draw GPS art planners, it **fits and routes
80
+ the shape automatically**, and it ships a **fidelity engine** that measures how
81
+ faithfully the route reproduces your shape β€” so quality is a number you can track
82
+ and tune, not just something you eyeball.
83
+
84
+ Routes export as **GPX** (ready for **Strava**, **Garmin**, or **Komoot**),
85
+ GeoJSON (WGS84), and map images.
86
+
87
+ ## ✨ Why svg2gpx
88
+
89
+ - 🎨 **Automatic, not hand-drawn.** Feed it an `<svg>` β€” it searches scale,
90
+ rotation, offset and stretch to seat the figure on the streets and routes it for
91
+ you. No dragging a pen across a map.
92
+ - 🧭 **Real street networks.** Snaps to the actual walkable graph from
93
+ OpenStreetMap (via OSMnx), so every route is a connected walk on real roads.
94
+ - πŸ“ **A fidelity engine, not a guess.** Seven complementary metrics β€” FrΓ©chet,
95
+ Hausdorff, IoU, DTW, turning distance, a perceptual render-compare, and a
96
+ **feature ledger** β€” score how recognizably the route reads as the shape.
97
+ - πŸ‘€ **Inner features.** Eyes, a smile, a donut's hole, a wing line β€” interior
98
+ detail is extracted and drawn too, not just the silhouette.
99
+ - 🧠 **Per-shape engine.** A compactness test routes blobby shapes and
100
+ elongated/protruding ones through the strategy that measured best for each.
101
+ - πŸ” **Reproducible.** A fast synthetic-grid mode runs offline and deterministically
102
+ for CI and benchmarking β€” no network required.
103
+
104
+ ## πŸš€ Quick start
105
+
106
+ ```bash
107
+ git clone https://github.com/Chieler/svg2gpx.git
108
+ cd svg2gpx
109
+
110
+ pip install -e . # core: synthetic-grid runs, offline
111
+ pip install -e ".[osm]" # + real OpenStreetMap data & plotting
112
+ ```
113
+
114
+ `skia-python` needs system GL libraries on Linux:
115
+
116
+ ```bash
117
+ sudo apt-get install -y libegl1 libgl1
118
+ ```
119
+
120
+ Generate your first route and export it as GPX:
121
+
122
+ ```bash
123
+ svg2gpx --svg star --lat 41.9285 --lng -87.7075 --save star.png --gpx star.gpx
124
+ ```
125
+
126
+ `--svg` takes any [bundled shape](#-shapes) stem (`star`, `Horse`, `donut`, …) or
127
+ a path to your own SVG. `star.gpx` is ready to import into Strava, Garmin, or
128
+ Komoot.
129
+
130
+ ## 🐍 Use it from Python
131
+
132
+ One call: give it a location and a shape, get a route back.
133
+
134
+ ```python
135
+ from svg2gpx import get_route
136
+
137
+ route = get_route(41.9285, -87.7075, "star") # lat, lng, shape (stem or .svg path)
138
+
139
+ route.to_gpx("star.gpx") # Strava / Garmin / Komoot-ready
140
+ route.plot() # quick matplotlib look (or save="star.png")
141
+ print(route.distance_km, route.iou) # 10.8, 0.33
142
+ coords = route.latlon # (N, 2) array of (lat, lon)
143
+ ```
144
+
145
+ Common options: `radius_m` (bigger = higher fidelity, longer route),
146
+ `granularity` (0 smooth … 1 detailed), `seed` (reproducible), `graphml` (route on
147
+ a saved OSMnx network, offline), `engine`, or any [`CONFIG`](src/svg2gpx/gen.py)
148
+ key as a keyword. Requires the `[osm]` extra.
149
+
150
+ ## πŸ–ΌοΈ Gallery
151
+
152
+ Pick the placement that reads best, tune detail, or let the shape choose its own
153
+ engine β€” the search returns several routings so you can eyeball the winner.
154
+
155
+ | Five detail/engine options per shape | Fidelity across scales (how short a route can still read) |
156
+ | :---: | :---: |
157
+ | ![engine options](docs/engines-panel-chicago.png) | ![scale ladder](docs/compact-ladder-chicago.png) |
158
+
159
+ ## 🧠 How it works
160
+
161
+ The pipeline (`gen.py`) runs end to end:
162
+
163
+ | Stage | Function | What it does |
164
+ | --- | --- | --- |
165
+ | 1. Build grid | `build_grid` | Pull and normalize the walkable street network (and parks) into `[0, 1]` space. |
166
+ | 2. Extract shape | `extract_shape` | Render the SVG and trace its outer outline **and inner features** as polylines. |
167
+ | 3. Search placement | `search_placement` | Find the scale / rotation / offset / stretch that seats the shape on the streets with the best *routed* fidelity. |
168
+ | 4. Snap waypoints | `snap_waypoints` | Densify the placed outline and snap points to street nodes β€” dense anchors so each hop barely deviates. |
169
+ | 5. Route | `route_contour` | Walk consecutive anchors with a contour-biased Dijkstra so the path hugs the shape. |
170
+ | 6. Cleanup + plot | `cleanup`, `plot` | Close the loop, dissolve backtracks / combs / nooks, report fidelity, draw. |
171
+
172
+ Fidelity comes from **dense waypoints**: spacing anchors well below one block means
173
+ each Dijkstra hop is short and has little room to stray. The dominant quality lever
174
+ is **resolution** (blocks per shape) β€” a bigger canvas or a denser street fabric
175
+ reads better, at the cost of a longer route.
176
+
177
+ ## πŸ“ Fidelity metrics
178
+
179
+ Each metric catches a failure the others miss (all in `gen.py`):
180
+
181
+ | Metric | Answers |
182
+ | --- | --- |
183
+ | **FrΓ©chet** | Order-aware worst-case leash β€” punishes out-of-sequence detours. |
184
+ | **Hausdorff** | The single largest excursion from the outline. |
185
+ | **IoU** | Area overlap of the two thickened outlines. |
186
+ | **Perceptual cost** | Blur-tolerant render-and-compare (`1 βˆ’ soft-IoU`) β€” the gestalt the eye sees. |
187
+ | **DTW** | Cyclic dynamic time warping β€” rewards hugging the outline *everywhere*, not just at the worst point. |
188
+ | **Turning distance** | Scale/rotation-invariant measure of **form** (corners, protrusions) that ignores staircase jitter. |
189
+ | **Feature ledger** | Recall / precision of the shape's **defining corners** β€” catches a feature vanishing when IoU can't. |
190
+ | **On-land % Β· distance** | Runnability sanity checks. |
191
+
192
+ Read together they tell you *how* a result is good or bad β€” path order (FrΓ©chet/DTW),
193
+ one bad excursion (Hausdorff), overall area (IoU), and whether the identity-carrying
194
+ corners landed (turning distance, feature ledger).
195
+
196
+ ## πŸ› οΈ Usage
197
+
198
+ <details open>
199
+ <summary><b>Generate a route</b></summary>
200
+
201
+ ```bash
202
+ svg2gpx # CONFIG defaults
203
+ svg2gpx --svg Crow --granularity 0.8
204
+ svg2gpx --svg star --lat 41.9285 --lng -87.7075 --save route.png --gpx route.gpx --no-show
205
+ ```
206
+
207
+ Common knobs are CLI flags (`--svg`, `--lat/--lng/--radius`, `--granularity`,
208
+ `--graphml`, `--seed`, `--save`, `--gpx`, `--no-show`, `--no-inner-features`);
209
+ everything else is tuned from `svg2gpx.CONFIG`. `--graphml` loads a saved OSMnx
210
+ network for offline / reproducible runs. `python -m svg2gpx` works identically
211
+ to the `svg2gpx` command.
212
+ </details>
213
+
214
+ <details>
215
+ <summary><b>Trace every shape on the real Chicago map</b></summary>
216
+
217
+ ```bash
218
+ python -m svg2gpx.chicago_map # all shapes, Logan Square window
219
+ python -m svg2gpx.chicago_map --shape star # one shape
220
+ python -m svg2gpx.chicago_map --live # fetch fresh OSM data instead
221
+ ```
222
+
223
+ Renders each route on the real OSMnx map plus a gallery image, and writes per-shape
224
+ GeoJSON (WGS84) and a metrics CSV to `chicago_maps/`.
225
+ </details>
226
+
227
+ <details>
228
+ <summary><b>Benchmark fidelity across shapes</b></summary>
229
+
230
+ ```bash
231
+ python -m svg2gpx.benchmark # synthetic grid, all shapes (offline, CI-friendly)
232
+ python -m svg2gpx.benchmark --grid-size 60 # finer lattice
233
+ python -m svg2gpx.benchmark --real # real OSM (cached on disk)
234
+ python -m svg2gpx.benchmark --json # also write benchmark_results.json
235
+ ```
236
+ </details>
237
+
238
+ <details>
239
+ <summary><b>Pick the best route per shape</b></summary>
240
+
241
+ ```bash
242
+ python -m svg2gpx.best_route # all shapes, synthetic grid
243
+ python -m svg2gpx.best_route --shape star # just one shape
244
+ python -m svg2gpx.best_route --grid real # real OSM
245
+ ```
246
+
247
+ Routes the top candidate placements, selects the lowest-cost one, and upserts its
248
+ metrics into [`result.csv`](result.csv) β€” one "best route" row per shape.
249
+ </details>
250
+
251
+ ## 🧩 Shapes
252
+
253
+ Eighteen SVGs ship with the package (see [`src/svg2gpx/shapes/`](src/svg2gpx/shapes))
254
+ β€” animals (`Horse`, `Shark`, `Crow`, `Cat`, `pig`, `duck`, `whale`, `ghost`), figures
255
+ (`Knight`, `Pawn`, `face`), and geometric primitives (`square`, `circle`, `star`,
256
+ `heart`, `donut`, `mushroom`, `lshape`). Pass any of these as a bare `--svg` stem, or
257
+ point `--svg` at your own SVG file β€” no code changes needed either way.
258
+
259
+ ### Inner features
260
+
261
+ `extract_shape()` finds a shape's **inner features** from the raster's ink/paper
262
+ contour tree and routes them alongside the outline:
263
+
264
+ - **holes** β€” a donut's hole, an eye (closed loops);
265
+ - **disconnected elements** β€” a face's eyes and smile (closed loops);
266
+ - **interior strokes** β€” a wing line, a horse's mane (open paths, run as out-and-back spurs).
267
+
268
+ Placement folds each candidate's **feature fidelity into its cost**, so a route that
269
+ seats the body nicely but strands the eye ranks below one that draws both. Small
270
+ features get extra rescues (feature-scaled smoothing and per-feature re-seating on
271
+ the local street fabric). Toggle with `inner_features=False` or `--no-inner-features`.
272
+ Visual check: `python -m svg2gpx.preview_features`.
273
+
274
+ ## πŸ“Š Continuous fidelity tracking
275
+
276
+ The **Best Route** GitHub Action
277
+ ([`.github/workflows/best-route.yml`](.github/workflows/best-route.yml)) runs
278
+ `svg2gpx.best_route` on demand (`workflow_dispatch`) and commits the updated
279
+ `result.csv` back to the repo, so fidelity is tracked over time.
280
+
281
+ ## πŸ—ΊοΈ Roadmap
282
+
283
+ - [x] **GPX export** β€” `--gpx route.gpx` writes a Strava / Garmin / Komoot-ready track.
284
+ - [ ] **PyPI package** β€” `pip install svg2gpx`.
285
+ - [ ] **Walk-network resolution** β€” alleys and footpaths for ~2Γ— finer routes.
286
+ - [ ] **Semantic recognizability judge** β€” a sketch classifier as a dev-time oracle.
287
+
288
+ ## πŸ“¦ Repository layout
289
+
290
+ ```
291
+ pyproject.toml # package metadata, the svg2gpx console entry point
292
+ src/svg2gpx/
293
+ gen.py # the full SVG -> street-route pipeline + fidelity metrics
294
+ cli.py # the svg2gpx command (CONFIG overrides + --gpx)
295
+ gpx.py # GPX 1.1 export
296
+ chicago_map.py # route every shape on the real Chicago OSM network
297
+ benchmark.py # fidelity + runtime benchmark over all shapes
298
+ best_route.py # best-of-N selection -> result.csv
299
+ preview_features.py # visualize extracted inner features
300
+ shapes/ # bundled sample SVGs (package data)
301
+ tests/ # routing + inner-feature checks
302
+ docs/ # design notes and comparison figures
303
+ .github/workflows/ # Best Route GitHub Action
304
+ ```
305
+
306
+ ## 🀝 Contributing
307
+
308
+ Issues and PRs are welcome. Before opening a PR:
309
+
310
+ ```bash
311
+ pip install -e ".[osm,dev]"
312
+
313
+ python tests/test_routing.py # routing / connectivity
314
+ python tests/test_inner_features.py # inner-feature extraction
315
+ python -m svg2gpx.benchmark # fidelity smoke on the synthetic grid
316
+ ```
317
+
318
+ ## πŸ“„ License
319
+
320
+ [MIT](LICENSE) Β© Chieler.
321
+
322
+ ---
323
+
324
+ <sub><i>Keywords: GPS art Β· Strava art generator Β· GPS drawing Β· SVG to GPX Β· SVG to
325
+ route Β· running route art Β· GPX route maker Β· route art Β· OpenStreetMap Β· running Β·
326
+ cycling Β· fitness map art.</i></sub>
@@ -0,0 +1,273 @@
1
+ <h1 align="center">svg2gpx</h1>
2
+
3
+ <p align="center">
4
+ <b>Turn any SVG shape into a runnable <a href="#">GPS&nbsp;art</a> route on real city streets.</b><br>
5
+ The automatic, open-source way to make <b>Strava art</b> β€” no hand-drawing required.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <img src="https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white" alt="Python 3.9+">
10
+ <img src="https://img.shields.io/badge/License-MIT-2ea44f" alt="License: MIT">
11
+ <img src="https://img.shields.io/badge/PRs-welcome-ff69b4" alt="PRs welcome">
12
+ <img src="https://img.shields.io/badge/built%20with-NumPy%20Β·%20SciPy%20Β·%20Shapely%20Β·%20OSMnx-f7931e" alt="Built with NumPy, SciPy, Shapely, OSMnx">
13
+ </p>
14
+
15
+ <p align="center">
16
+ <img src="chicago_maps/all_shapes_chicago.png" alt="Shapes routed on the real Chicago street network" width="820">
17
+ <br>
18
+ <sub><i>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).</i></sub>
19
+ </p>
20
+
21
+ ---
22
+
23
+ **svg2gpx** takes an SVG silhouette and a location, lays the shape over a city's
24
+ walkable street network, and generates a single **closed running route** whose
25
+ path resembles the shape β€” a boar, a star, a heart β€” drawn in streets you can
26
+ actually run or ride. Unlike the hand-draw GPS art planners, it **fits and routes
27
+ the shape automatically**, and it ships a **fidelity engine** that measures how
28
+ faithfully the route reproduces your shape β€” so quality is a number you can track
29
+ and tune, not just something you eyeball.
30
+
31
+ Routes export as **GPX** (ready for **Strava**, **Garmin**, or **Komoot**),
32
+ GeoJSON (WGS84), and map images.
33
+
34
+ ## ✨ Why svg2gpx
35
+
36
+ - 🎨 **Automatic, not hand-drawn.** Feed it an `<svg>` β€” it searches scale,
37
+ rotation, offset and stretch to seat the figure on the streets and routes it for
38
+ you. No dragging a pen across a map.
39
+ - 🧭 **Real street networks.** Snaps to the actual walkable graph from
40
+ OpenStreetMap (via OSMnx), so every route is a connected walk on real roads.
41
+ - πŸ“ **A fidelity engine, not a guess.** Seven complementary metrics β€” FrΓ©chet,
42
+ Hausdorff, IoU, DTW, turning distance, a perceptual render-compare, and a
43
+ **feature ledger** β€” score how recognizably the route reads as the shape.
44
+ - πŸ‘€ **Inner features.** Eyes, a smile, a donut's hole, a wing line β€” interior
45
+ detail is extracted and drawn too, not just the silhouette.
46
+ - 🧠 **Per-shape engine.** A compactness test routes blobby shapes and
47
+ elongated/protruding ones through the strategy that measured best for each.
48
+ - πŸ” **Reproducible.** A fast synthetic-grid mode runs offline and deterministically
49
+ for CI and benchmarking β€” no network required.
50
+
51
+ ## πŸš€ Quick start
52
+
53
+ ```bash
54
+ git clone https://github.com/Chieler/svg2gpx.git
55
+ cd svg2gpx
56
+
57
+ pip install -e . # core: synthetic-grid runs, offline
58
+ pip install -e ".[osm]" # + real OpenStreetMap data & plotting
59
+ ```
60
+
61
+ `skia-python` needs system GL libraries on Linux:
62
+
63
+ ```bash
64
+ sudo apt-get install -y libegl1 libgl1
65
+ ```
66
+
67
+ Generate your first route and export it as GPX:
68
+
69
+ ```bash
70
+ svg2gpx --svg star --lat 41.9285 --lng -87.7075 --save star.png --gpx star.gpx
71
+ ```
72
+
73
+ `--svg` takes any [bundled shape](#-shapes) stem (`star`, `Horse`, `donut`, …) or
74
+ a path to your own SVG. `star.gpx` is ready to import into Strava, Garmin, or
75
+ Komoot.
76
+
77
+ ## 🐍 Use it from Python
78
+
79
+ One call: give it a location and a shape, get a route back.
80
+
81
+ ```python
82
+ from svg2gpx import get_route
83
+
84
+ route = get_route(41.9285, -87.7075, "star") # lat, lng, shape (stem or .svg path)
85
+
86
+ route.to_gpx("star.gpx") # Strava / Garmin / Komoot-ready
87
+ route.plot() # quick matplotlib look (or save="star.png")
88
+ print(route.distance_km, route.iou) # 10.8, 0.33
89
+ coords = route.latlon # (N, 2) array of (lat, lon)
90
+ ```
91
+
92
+ Common options: `radius_m` (bigger = higher fidelity, longer route),
93
+ `granularity` (0 smooth … 1 detailed), `seed` (reproducible), `graphml` (route on
94
+ a saved OSMnx network, offline), `engine`, or any [`CONFIG`](src/svg2gpx/gen.py)
95
+ key as a keyword. Requires the `[osm]` extra.
96
+
97
+ ## πŸ–ΌοΈ Gallery
98
+
99
+ Pick the placement that reads best, tune detail, or let the shape choose its own
100
+ engine β€” the search returns several routings so you can eyeball the winner.
101
+
102
+ | Five detail/engine options per shape | Fidelity across scales (how short a route can still read) |
103
+ | :---: | :---: |
104
+ | ![engine options](docs/engines-panel-chicago.png) | ![scale ladder](docs/compact-ladder-chicago.png) |
105
+
106
+ ## 🧠 How it works
107
+
108
+ The pipeline (`gen.py`) runs end to end:
109
+
110
+ | Stage | Function | What it does |
111
+ | --- | --- | --- |
112
+ | 1. Build grid | `build_grid` | Pull and normalize the walkable street network (and parks) into `[0, 1]` space. |
113
+ | 2. Extract shape | `extract_shape` | Render the SVG and trace its outer outline **and inner features** as polylines. |
114
+ | 3. Search placement | `search_placement` | Find the scale / rotation / offset / stretch that seats the shape on the streets with the best *routed* fidelity. |
115
+ | 4. Snap waypoints | `snap_waypoints` | Densify the placed outline and snap points to street nodes β€” dense anchors so each hop barely deviates. |
116
+ | 5. Route | `route_contour` | Walk consecutive anchors with a contour-biased Dijkstra so the path hugs the shape. |
117
+ | 6. Cleanup + plot | `cleanup`, `plot` | Close the loop, dissolve backtracks / combs / nooks, report fidelity, draw. |
118
+
119
+ Fidelity comes from **dense waypoints**: spacing anchors well below one block means
120
+ each Dijkstra hop is short and has little room to stray. The dominant quality lever
121
+ is **resolution** (blocks per shape) β€” a bigger canvas or a denser street fabric
122
+ reads better, at the cost of a longer route.
123
+
124
+ ## πŸ“ Fidelity metrics
125
+
126
+ Each metric catches a failure the others miss (all in `gen.py`):
127
+
128
+ | Metric | Answers |
129
+ | --- | --- |
130
+ | **FrΓ©chet** | Order-aware worst-case leash β€” punishes out-of-sequence detours. |
131
+ | **Hausdorff** | The single largest excursion from the outline. |
132
+ | **IoU** | Area overlap of the two thickened outlines. |
133
+ | **Perceptual cost** | Blur-tolerant render-and-compare (`1 βˆ’ soft-IoU`) β€” the gestalt the eye sees. |
134
+ | **DTW** | Cyclic dynamic time warping β€” rewards hugging the outline *everywhere*, not just at the worst point. |
135
+ | **Turning distance** | Scale/rotation-invariant measure of **form** (corners, protrusions) that ignores staircase jitter. |
136
+ | **Feature ledger** | Recall / precision of the shape's **defining corners** β€” catches a feature vanishing when IoU can't. |
137
+ | **On-land % Β· distance** | Runnability sanity checks. |
138
+
139
+ Read together they tell you *how* a result is good or bad β€” path order (FrΓ©chet/DTW),
140
+ one bad excursion (Hausdorff), overall area (IoU), and whether the identity-carrying
141
+ corners landed (turning distance, feature ledger).
142
+
143
+ ## πŸ› οΈ Usage
144
+
145
+ <details open>
146
+ <summary><b>Generate a route</b></summary>
147
+
148
+ ```bash
149
+ svg2gpx # CONFIG defaults
150
+ svg2gpx --svg Crow --granularity 0.8
151
+ svg2gpx --svg star --lat 41.9285 --lng -87.7075 --save route.png --gpx route.gpx --no-show
152
+ ```
153
+
154
+ Common knobs are CLI flags (`--svg`, `--lat/--lng/--radius`, `--granularity`,
155
+ `--graphml`, `--seed`, `--save`, `--gpx`, `--no-show`, `--no-inner-features`);
156
+ everything else is tuned from `svg2gpx.CONFIG`. `--graphml` loads a saved OSMnx
157
+ network for offline / reproducible runs. `python -m svg2gpx` works identically
158
+ to the `svg2gpx` command.
159
+ </details>
160
+
161
+ <details>
162
+ <summary><b>Trace every shape on the real Chicago map</b></summary>
163
+
164
+ ```bash
165
+ python -m svg2gpx.chicago_map # all shapes, Logan Square window
166
+ python -m svg2gpx.chicago_map --shape star # one shape
167
+ python -m svg2gpx.chicago_map --live # fetch fresh OSM data instead
168
+ ```
169
+
170
+ Renders each route on the real OSMnx map plus a gallery image, and writes per-shape
171
+ GeoJSON (WGS84) and a metrics CSV to `chicago_maps/`.
172
+ </details>
173
+
174
+ <details>
175
+ <summary><b>Benchmark fidelity across shapes</b></summary>
176
+
177
+ ```bash
178
+ python -m svg2gpx.benchmark # synthetic grid, all shapes (offline, CI-friendly)
179
+ python -m svg2gpx.benchmark --grid-size 60 # finer lattice
180
+ python -m svg2gpx.benchmark --real # real OSM (cached on disk)
181
+ python -m svg2gpx.benchmark --json # also write benchmark_results.json
182
+ ```
183
+ </details>
184
+
185
+ <details>
186
+ <summary><b>Pick the best route per shape</b></summary>
187
+
188
+ ```bash
189
+ python -m svg2gpx.best_route # all shapes, synthetic grid
190
+ python -m svg2gpx.best_route --shape star # just one shape
191
+ python -m svg2gpx.best_route --grid real # real OSM
192
+ ```
193
+
194
+ Routes the top candidate placements, selects the lowest-cost one, and upserts its
195
+ metrics into [`result.csv`](result.csv) β€” one "best route" row per shape.
196
+ </details>
197
+
198
+ ## 🧩 Shapes
199
+
200
+ Eighteen SVGs ship with the package (see [`src/svg2gpx/shapes/`](src/svg2gpx/shapes))
201
+ β€” animals (`Horse`, `Shark`, `Crow`, `Cat`, `pig`, `duck`, `whale`, `ghost`), figures
202
+ (`Knight`, `Pawn`, `face`), and geometric primitives (`square`, `circle`, `star`,
203
+ `heart`, `donut`, `mushroom`, `lshape`). Pass any of these as a bare `--svg` stem, or
204
+ point `--svg` at your own SVG file β€” no code changes needed either way.
205
+
206
+ ### Inner features
207
+
208
+ `extract_shape()` finds a shape's **inner features** from the raster's ink/paper
209
+ contour tree and routes them alongside the outline:
210
+
211
+ - **holes** β€” a donut's hole, an eye (closed loops);
212
+ - **disconnected elements** β€” a face's eyes and smile (closed loops);
213
+ - **interior strokes** β€” a wing line, a horse's mane (open paths, run as out-and-back spurs).
214
+
215
+ Placement folds each candidate's **feature fidelity into its cost**, so a route that
216
+ seats the body nicely but strands the eye ranks below one that draws both. Small
217
+ features get extra rescues (feature-scaled smoothing and per-feature re-seating on
218
+ the local street fabric). Toggle with `inner_features=False` or `--no-inner-features`.
219
+ Visual check: `python -m svg2gpx.preview_features`.
220
+
221
+ ## πŸ“Š Continuous fidelity tracking
222
+
223
+ The **Best Route** GitHub Action
224
+ ([`.github/workflows/best-route.yml`](.github/workflows/best-route.yml)) runs
225
+ `svg2gpx.best_route` on demand (`workflow_dispatch`) and commits the updated
226
+ `result.csv` back to the repo, so fidelity is tracked over time.
227
+
228
+ ## πŸ—ΊοΈ Roadmap
229
+
230
+ - [x] **GPX export** β€” `--gpx route.gpx` writes a Strava / Garmin / Komoot-ready track.
231
+ - [ ] **PyPI package** β€” `pip install svg2gpx`.
232
+ - [ ] **Walk-network resolution** β€” alleys and footpaths for ~2Γ— finer routes.
233
+ - [ ] **Semantic recognizability judge** β€” a sketch classifier as a dev-time oracle.
234
+
235
+ ## πŸ“¦ Repository layout
236
+
237
+ ```
238
+ pyproject.toml # package metadata, the svg2gpx console entry point
239
+ src/svg2gpx/
240
+ gen.py # the full SVG -> street-route pipeline + fidelity metrics
241
+ cli.py # the svg2gpx command (CONFIG overrides + --gpx)
242
+ gpx.py # GPX 1.1 export
243
+ chicago_map.py # route every shape on the real Chicago OSM network
244
+ benchmark.py # fidelity + runtime benchmark over all shapes
245
+ best_route.py # best-of-N selection -> result.csv
246
+ preview_features.py # visualize extracted inner features
247
+ shapes/ # bundled sample SVGs (package data)
248
+ tests/ # routing + inner-feature checks
249
+ docs/ # design notes and comparison figures
250
+ .github/workflows/ # Best Route GitHub Action
251
+ ```
252
+
253
+ ## 🀝 Contributing
254
+
255
+ Issues and PRs are welcome. Before opening a PR:
256
+
257
+ ```bash
258
+ pip install -e ".[osm,dev]"
259
+
260
+ python tests/test_routing.py # routing / connectivity
261
+ python tests/test_inner_features.py # inner-feature extraction
262
+ python -m svg2gpx.benchmark # fidelity smoke on the synthetic grid
263
+ ```
264
+
265
+ ## πŸ“„ License
266
+
267
+ [MIT](LICENSE) Β© Chieler.
268
+
269
+ ---
270
+
271
+ <sub><i>Keywords: GPS art Β· Strava art generator Β· GPS drawing Β· SVG to GPX Β· SVG to
272
+ route Β· running route art Β· GPX route maker Β· route art Β· OpenStreetMap Β· running Β·
273
+ cycling Β· fitness map art.</i></sub>
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "svg2gpx"
7
+ version = "0.4.0"
8
+ description = "Turn any SVG shape into a runnable GPS-art route on real city streets."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Chieler", email = "chielerli@gmail.com" }]
13
+ keywords = ["gps art", "strava art", "gpx", "svg", "running", "route",
14
+ "openstreetmap", "gps drawing", "route art"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: Scientific/Engineering :: GIS",
22
+ "Topic :: Multimedia :: Graphics",
23
+ ]
24
+ dependencies = [
25
+ "numpy",
26
+ "scipy",
27
+ "shapely",
28
+ "opencv-python-headless",
29
+ "skia-python",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ osm = ["osmnx", "matplotlib"]
34
+ dev = ["build", "twine"]
35
+
36
+ [project.scripts]
37
+ svg2gpx = "svg2gpx.cli:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/Chieler/svg2gpx"
41
+ Issues = "https://github.com/Chieler/svg2gpx/issues"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.setuptools.package-data]
47
+ svg2gpx = ["shapes/*.svg"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+