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.
- svg2gpx-0.4.0/LICENSE +21 -0
- svg2gpx-0.4.0/PKG-INFO +326 -0
- svg2gpx-0.4.0/README.md +273 -0
- svg2gpx-0.4.0/pyproject.toml +47 -0
- svg2gpx-0.4.0/setup.cfg +4 -0
- svg2gpx-0.4.0/src/svg2gpx/__init__.py +22 -0
- svg2gpx-0.4.0/src/svg2gpx/__main__.py +4 -0
- svg2gpx-0.4.0/src/svg2gpx/benchmark.py +301 -0
- svg2gpx-0.4.0/src/svg2gpx/best_route.py +156 -0
- svg2gpx-0.4.0/src/svg2gpx/chicago_map.py +326 -0
- svg2gpx-0.4.0/src/svg2gpx/cli.py +69 -0
- svg2gpx-0.4.0/src/svg2gpx/gen.py +2406 -0
- svg2gpx-0.4.0/src/svg2gpx/gpx.py +30 -0
- svg2gpx-0.4.0/src/svg2gpx/preview_features.py +74 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/Cat.svg +89 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/Crow.svg +5 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/Horse.svg +22 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/Knight.svg +5 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/Pawn.svg +2 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/Shark.svg +25 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/__init__.py +25 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/circle.svg +3 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/donut.svg +5 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/duck.svg +5 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/face.svg +6 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/ghost.svg +5 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/heart.svg +5 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/lshape.svg +3 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/mushroom.svg +5 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/pig.svg +6 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/square.svg +3 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/star.svg +3 -0
- svg2gpx-0.4.0/src/svg2gpx/shapes/whale.svg +5 -0
- svg2gpx-0.4.0/src/svg2gpx.egg-info/PKG-INFO +326 -0
- svg2gpx-0.4.0/src/svg2gpx.egg-info/SOURCES.txt +39 -0
- svg2gpx-0.4.0/src/svg2gpx.egg-info/dependency_links.txt +1 -0
- svg2gpx-0.4.0/src/svg2gpx.egg-info/entry_points.txt +2 -0
- svg2gpx-0.4.0/src/svg2gpx.egg-info/requires.txt +13 -0
- svg2gpx-0.4.0/src/svg2gpx.egg-info/top_level.txt +1 -0
- svg2gpx-0.4.0/tests/test_inner_features.py +65 -0
- 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 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
|
+
|  |  |
|
|
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>
|
svg2gpx-0.4.0/README.md
ADDED
|
@@ -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 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
|
+
|  |  |
|
|
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"]
|
svg2gpx-0.4.0/setup.cfg
ADDED