restring-art 2.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020-2026 Kaspar Valk, Kert Tali, Almas Baktubayev
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.
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: restring-art
3
+ Version: 2.0.0
4
+ License-File: LICENSE
5
+ Summary: Recreate images by connecting nails with strings using a greedy algorithm (B/W and RGB)
6
+ Author: Kaspar Valk, Kert Tali, Almas Baktubayev
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
10
+
11
+ [![CI](https://github.com/InzGIBA/ReString/actions/workflows/ci.yml/badge.svg)](https://github.com/InzGIBA/ReString/actions/workflows/ci.yml)
12
+
13
+ <picture>
14
+ <source media="(prefers-color-scheme: dark)" srcset="assets/hero-dark.svg">
15
+ <source media="(prefers-color-scheme: light)" srcset="assets/hero-light.svg">
16
+ <img alt="ReString — turn any photo into string art you can wind by hand" src="assets/hero-light.svg" width="100%">
17
+ </picture>
18
+
19
+ Feed in a portrait. The greedy search picks one thread at a time, and each pull moves the canvas measurably closer to the photo. The output is the picture one uncut thread draws around the nails, plus the nail-by-nail winding order. Black and white uses one thread. Color runs pick a palette: the pure RGB triple, CMY, CMYK or a JSON list of the floss colors you own.
20
+
21
+ [Quick start](#quick-start) · [Usage](#usage) · [Tuning](#tuning) · [Development](#development) · [Contributing](#contributing) · [License](#license)
22
+
23
+ Try it in the browser: the [playground](https://inzgiba.github.io/ReString/) runs the same core as a wasm module. Drop a photo, no install. The page deploys from `web/` (`.github/workflows/pages.yml`).
24
+
25
+ ## Overview
26
+
27
+ - **For:** makers winding physical boards, machine and plotter owners, and anyone who wants a digital string-art render.
28
+ - **Input:** a PNG or JPEG photo; face-centered portraits work best.
29
+ - **Output:** a PNG render of the thread pattern; the Python API also returns the continuous nail-by-nail pull order.
30
+ - **Scope:** greedy search on a 300 px working grid, five nail-layout families, black-and-white and color palettes, `.restring` project files, printable winding posters, GIF animations and a physical thread model. Building from source requires a Rust toolchain, there is no pure-Python fallback.
31
+
32
+ ![A photo becomes a thread drawing](assets/demo.jpg)
33
+
34
+ One run of the CLI: the same portrait at 1000 threads around 300 nails and 3000 threads around 598 nails (`--rect -s 0.05`).
35
+
36
+ The project started as a 2020 university course assignment by Kaspar Valk, Kert Tali and Almas Baktubayev. The [project poster](https://raw.githubusercontent.com/InzGIBA/ReString/main/docs/Algoritmika%20poster%20puhtand.pdf) describes the original idea.
37
+
38
+ ## Features
39
+
40
+ <picture>
41
+ <source media="(prefers-color-scheme: dark)" srcset="assets/feature-01-dark.svg">
42
+ <img alt="Fast by design — a Rust greedy search, 7000 pulls in 0.8 s" src="assets/feature-01-light.svg" width="32%">
43
+ </picture>
44
+ <picture>
45
+ <source media="(prefers-color-scheme: dark)" srcset="assets/feature-02-dark.svg">
46
+ <img alt="Zero dependencies — a thin Python API over a Rust core" src="assets/feature-02-light.svg" width="32%">
47
+ </picture>
48
+ <picture>
49
+ <source media="(prefers-color-scheme: dark)" srcset="assets/feature-03-dark.svg">
50
+ <img alt="Verified output — golden tests pin pixels and pull order" src="assets/feature-03-light.svg" width="32%">
51
+ </picture>
52
+
53
+ - **One uncut thread**: the pull order is continuous by construction, so you wind a single thread by hand or machine.
54
+ - **Real thread colors**: black-on-white, white-on-black, or a palette. The built-ins are RGB, CMY and CMYK, or load a JSON list of the floss colors you own. CMYK measures 42 to 45 percent better than RGB at an equal budget.
55
+ - **Custom boards**: polygon, star, multi-ring and SVG-outline layouts via `--layout star:5:0.55`, plus non-square aspect crops ([layouts guide](docs/layouts.md)).
56
+ - **From photo to board**: save a `.restring` project, re-render in milliseconds, print a numbered-nail poster with the thread length in meters, watch the winding as a GIF.
57
+ - **Measured, not guessed**: every flag has a degradation curve behind it in the [tuning guide](docs/tuning.md).
58
+
59
+ ## Quick start
60
+
61
+ ### Install
62
+
63
+ ```bash
64
+ pip install restring-art
65
+ ```
66
+
67
+ Wheels cover linux (x86_64, aarch64), macos (x86_64, arm64) and windows (x86_64) on CPython 3.12 and newer. No Rust toolchain needed. Building from source needs uv and Rust, see [Development](#development).
68
+
69
+ ### Run your first example
70
+
71
+ ```bash
72
+ restring -i portrait.png -o out.png --rect -n 2 -l 3000 # best quality, ~1.4 s
73
+ ```
74
+
75
+ Expected result:
76
+
77
+ ```text
78
+ [...] [restring.cli] [INFO]: Nails amount: 598
79
+ ```
80
+
81
+ `out.png` now holds the rendered thread pattern.
82
+
83
+ ## Usage
84
+
85
+ ### Basic example
86
+
87
+ Three presets cover most jobs (timed on a 24-core Linux box):
88
+
89
+ ```bash
90
+ # fast preview, ~0.3 s
91
+ restring -i portrait.png -o out.png --rect -l 300
92
+
93
+ # balanced, ~0.6 s
94
+ restring -i portrait.png -o out.png --rect -n 4 -l 2000
95
+
96
+ # maximum measured quality, ~1.4 s
97
+ restring -i portrait.png -o out.png --rect -n 2 -l 3000
98
+ ```
99
+
100
+ ### CLI reference
101
+
102
+ | Flag | Meaning | Default |
103
+ |---|---|---|
104
+ | `-i <path>` | input image (PNG or JPEG) | required |
105
+ | `-o <path>` | output PNG | `output.png` |
106
+ | `-d <int>` | output side length in pixels | 300 |
107
+ | `-n <int>` | nail step for the circle and rect layouts. Smaller values place more nails. Minimum 1 | 4 |
108
+ | `--layout <spec>` | nail layout: `circle`, `rect`, `polygon:sides[:count]`, `star:points:inner[:count]`, `rings:r:count,r:count`, `svg:file.svg[:count]`, optional `@aspect` suffix for non-square boards. Custom layouts take counts instead of `-n` | circle |
109
+ | `-l <int>` | iteration budget. Without the flag the search stops after 3 pulls with no improvement | run to exhaustion |
110
+ | `-s <float>` | thread strength at export, 0..1. Leave unset and the run derives the value from the actual pull count: k/L with k=104, clamped to 0.03..0.2, within 2 percent of the hand-tuned optimum at the measured budgets | auto |
111
+ | `-r <int>` | scan a fixed random subset of candidates each iteration. Trades quality for speed. Good values sit near 100 to 150 | all nails |
112
+ | `-r1 <float>` | vertical nail ellipse multiplier | 1 |
113
+ | `-r2 <float>` | horizontal nail ellipse multiplier | 1 |
114
+ | `--rect` | nails on the rectangle border instead of the circle. Better for square portraits, about 2x lower error | circle |
115
+ | `--rgb` | colored output with three interleaved pure RGB threads | black and white |
116
+ | `--palette <name|file>` | thread palette: `rgb`, `cmy`, `cmyk`, or a JSON file of `{"name", "rgb": [r, g, b]}` colors (cap 8 threads). CMYK measures 42 to 45 percent better color MSE than RGB at an equal budget | off |
117
+ | `--wb` | white thread on a black canvas | black on white |
118
+ | `--weight <float>` | importance weighting. A Gaussian weight map steers threads toward the image center, where the viewer looks. Suggested range 1.5 to 5.0. Portrait subjects only, about 25 percent more scan time | off |
119
+ | `--min-chord <px>` | candidate filter: chords shorter than the distance in working-grid pixels are banned. Portrait measurement: the greedy never picks short chords, the filter only degrades MSE past 250 px. Intended for sparse custom layouts | off |
120
+ | `--project-out <path>` | save the nail layout and pull order as a `.restring` project alongside the PNG | off |
121
+ | `--project <path>` | re-render from a `.restring` project instead of an image: skips the search, applies the current `-s` and `-d` | off |
122
+ | `--anim <path>` | with `--project`: animated GIF of the winding sequence. `--anim-frames` sets the frame count, 180 is about 6 s at 30 fps. A 2000-pull project at the default 600 px export lands near 45 MB; `-d 450` brings it to about 28 MB, `-d 380` under 25 MB | off |
123
+ | `--realistic` | physical thread model: threads of `--thread-width` mm (default 0.2) on a `--canvas-size` mm board (default 400). At exports well above 300 px the 0.3 mm width beats the hairline by 35 percent MSE. The search never changes | hairline |
124
+ | `--rollback` | truncate a stagnating run to its best-error prefix. Measured no-op: the search never applies non-improving pulls, so nothing rolls back ([measurement](docs/research/rollback-experiment.md)) | off |
125
+ | `--metrics <path.json>` | write a quality report: MSE, correlation, plain and blurred SSIM against the working target, plus pulls, nails and elapsed time. Generate mode only | off |
126
+ | `--thread-width <mm>` | thread diameter for `--realistic`. Birsak used 0.15, typical polyester sits near 0.2 | 0.2 |
127
+ | `--canvas-size <mm>` | board size: the poster printing size (default 260 for `--poster`) or the physical size behind `--realistic` (default 400) | per feature |
128
+
129
+ Nails sit evenly on the perimeter. The circle starts at 9 o'clock and runs clockwise. The rectangle starts at the top left corner. The pull order is continuous by construction, so you wind one uncut thread.
130
+
131
+ ### Save and re-render
132
+
133
+ The project file keeps the nail layout and the pull order in plain JSON, so a result re-renders in milliseconds without a search and any tool can read the winding sequence:
134
+
135
+ ```bash
136
+ restring -i portrait.png -o out.png --rect -l 2000 --project-out result.restring
137
+ restring --project result.restring -o out2.png # byte-identical re-render
138
+ ```
139
+
140
+ The current `-s` and `-d` apply at re-render time. Schema and compatibility promise: [docs/project-format.md](docs/project-format.md).
141
+
142
+ ### Wind it for real
143
+
144
+ Turn a result into a printable poster: numbered nails, the footer with the nail count, pull count and total thread length in meters.
145
+
146
+ ```bash
147
+ restring -i portrait.png -o out.png --rect -l 2000 --project-out result.restring --poster poster.svg
148
+ restring --project result.restring --poster poster.svg --canvas-size 300
149
+ ```
150
+
151
+ Print at 100 percent scale, hammer the nails on the board outline, wind. Printing checklist and thread buying guidance: [docs/winding.md](docs/winding.md).
152
+
153
+ ### As a library
154
+
155
+ ```python
156
+ from restring import GenerateOptions, generate
157
+
158
+ result = generate("portrait.png", GenerateOptions(layout="rect", pulls=2000))
159
+ result.image.save_png("out.png")
160
+ print(result.stats.pulls_per_thread)
161
+ ```
162
+
163
+ The full options table, the Result fields and the deprecated 1.x names: [docs/api.md](docs/api.md). Images travel as `_core.Image` objects; the pixel data stays in Rust memory, so calls pass references instead of copying arrays.
164
+
165
+ ## Tuning
166
+
167
+ `-s` is fitted for you: the run derives the export strength from the actual pull count (104/L clamped to 0.03..0.2) and lands within 2 percent of the hand-tuned optimum at the measured budgets. The working simulation darkens each pull with strength 0.05; the export accumulates with strength `-s`. When `-s` is too high for the thread count, the render reaches its target halfway, then overshoots and darkens. Manual `-s` values stay for special cases, see the tuning guide.
168
+
169
+ | Flag | Recommended | Rule |
170
+ |---|---|---|
171
+ | `--rect` | always on for square portraits | 1.8 to 2.7x lower error than the circle |
172
+ | `-s` | leave unset, auto fits the pull count | measured optimum scales as 1 over thread count, k=104 clamped 0.03 to 0.2 |
173
+ | `-l` | up to saturation, about 2500 to 2900 threads | beyond saturation the search finds no improvements |
174
+ | `-n` | 4 for the knee, 2 for maximum quality | 844 circle nails buy 6 percent error reduction at 4x the cost |
175
+ | `-r` | 100 to 150 while exploring, drop for the final run | k=100 saves 40 percent of the time for 15 percent error |
176
+ | `-d` | 300 | exporting above the working grid hurts |
177
+ | `--weight` | 3.0 for portraits, off for other subjects | center MSE falls 24 to 28 percent, global MSE rises 3 percent |
178
+ | `--min-chord` | off | the greedy never picks short chords, thresholds bite past 230 px at up to 24 percent MSE cost |
179
+
180
+ Every flag, the numbers behind the rules and the degradation curves: [docs/tuning.md](docs/tuning.md).
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ git clone git@github.com:InzGIBA/ReString.git
186
+ cd ReString
187
+ uv sync
188
+ ```
189
+
190
+ Requirements: [uv](https://docs.astral.sh/uv/getting-started/installation/) and a Rust toolchain with a C linker for cargo. uv fetches Python 3.14 and compiles the core on first sync. No system C compiler (minimal WSL or container)? Use zig as the cargo linker — the recipe lives in [docs/architecture.md](docs/architecture.md#build).
191
+
192
+ Source checkouts prefix every command with `uv run`, for example `uv run restring -i portrait.png -o out.png --rect -l 300`.
193
+
194
+ ### Checks
195
+
196
+ ```bash
197
+ uv run pytest # 211 tests, ~11 s
198
+ uv run ruff check .
199
+ uv run python scripts/benchmark.py --label before # or after, then --plot
200
+ uv run python scripts/make_gallery.py # regenerate docs/img/gallery.png
201
+ ```
202
+
203
+ CI runs the same checks on every push to main and every pull request, on linux, macos and windows (`.github/workflows/ci.yml`). A `v*` tag builds wheels for three platforms, publishes to PyPI and opens the GitHub Release (`.github/workflows/release.yml`).
204
+
205
+ ## Documentation
206
+
207
+ | Document | Contents |
208
+ |---|---|
209
+ | [docs/architecture.md](docs/architecture.md) | how the pipeline works, the Rust core, correctness model, performance, build, wasm target |
210
+ | [docs/api.md](docs/api.md) | the 2.0 facade: `GenerateOptions`, `Result`, deprecated 1.x names |
211
+ | [docs/tuning.md](docs/tuning.md) | every flag, degradation curves, recommended values, presets |
212
+ | [docs/layouts.md](docs/layouts.md) | layout specs, the clockwise ordering rule, non-square boards |
213
+ | [docs/project-format.md](docs/project-format.md) | the `.restring` schema and the compatibility promise |
214
+ | [docs/winding.md](docs/winding.md) | printing checklist, reading the poster, buying thread |
215
+ | [docs/history.md](docs/history.md) | the migration story: stages, numbers, rejected hypotheses |
216
+ | [docs/research/](docs/research/) | twelve experiment docs with the numbers behind every verdict, plus the related-work survey |
217
+
218
+ ## Roadmap
219
+
220
+ Open experiment ideas arrive through the research-idea issue template: each states a question, a hypothesis and a pre-registered success bar. Closed questions live next to their measurements in [docs/research/](docs/research/), so nobody re-litigates them. Answered so far:
221
+
222
+ - [x] Re-run the RGB sweeps: working strength 0.05 adopted, 13 to 17 percent better MSE ([measurement](docs/research/rgb-channels-experiment.md)).
223
+ - [x] Test the working grid above 300 px: keep 300, under 3 percent MSE at up to 2.4x time ([measurement](docs/research/grid-scale-experiment.md)).
224
+
225
+ ## Contributing
226
+
227
+ Issues and pull requests are welcome. Templates structure both: bug reports carry the command line and image, feature requests check the measured dead ends, research ideas state a pre-registered success bar, and the PR template asks for the measurement or test evidence.
228
+
229
+ Wound a physical board? Post the photos under the "show and tell" label. Physical outputs are the best advertisement a string-art project gets. The example gallery ([examples/gallery/](examples/gallery/)) shows the configuration families and the commands behind every tile.
230
+
231
+ 1. Describe the problem or the proposed improvement.
232
+ 2. Make a focused change.
233
+ 3. Add or update relevant tests and documentation.
234
+ 4. Run the checks above and open a pull request.
235
+
236
+ ## License
237
+
238
+ MIT. See [LICENSE](LICENSE).
239
+
240
+ ## Photo credits
241
+
242
+ Portrait fixtures in `tests/data/` are square face-centered crops of studio photos provided for this project. The demo image above uses the `portrait_female.png` fixture. Earlier fixtures came from [Unsplash](https://unsplash.com/s/photos/people) under the [Unsplash License](https://unsplash.com/license).
243
+
@@ -0,0 +1,232 @@
1
+ [![CI](https://github.com/InzGIBA/ReString/actions/workflows/ci.yml/badge.svg)](https://github.com/InzGIBA/ReString/actions/workflows/ci.yml)
2
+
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="assets/hero-dark.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="assets/hero-light.svg">
6
+ <img alt="ReString — turn any photo into string art you can wind by hand" src="assets/hero-light.svg" width="100%">
7
+ </picture>
8
+
9
+ Feed in a portrait. The greedy search picks one thread at a time, and each pull moves the canvas measurably closer to the photo. The output is the picture one uncut thread draws around the nails, plus the nail-by-nail winding order. Black and white uses one thread. Color runs pick a palette: the pure RGB triple, CMY, CMYK or a JSON list of the floss colors you own.
10
+
11
+ [Quick start](#quick-start) · [Usage](#usage) · [Tuning](#tuning) · [Development](#development) · [Contributing](#contributing) · [License](#license)
12
+
13
+ Try it in the browser: the [playground](https://inzgiba.github.io/ReString/) runs the same core as a wasm module. Drop a photo, no install. The page deploys from `web/` (`.github/workflows/pages.yml`).
14
+
15
+ ## Overview
16
+
17
+ - **For:** makers winding physical boards, machine and plotter owners, and anyone who wants a digital string-art render.
18
+ - **Input:** a PNG or JPEG photo; face-centered portraits work best.
19
+ - **Output:** a PNG render of the thread pattern; the Python API also returns the continuous nail-by-nail pull order.
20
+ - **Scope:** greedy search on a 300 px working grid, five nail-layout families, black-and-white and color palettes, `.restring` project files, printable winding posters, GIF animations and a physical thread model. Building from source requires a Rust toolchain, there is no pure-Python fallback.
21
+
22
+ ![A photo becomes a thread drawing](assets/demo.jpg)
23
+
24
+ One run of the CLI: the same portrait at 1000 threads around 300 nails and 3000 threads around 598 nails (`--rect -s 0.05`).
25
+
26
+ The project started as a 2020 university course assignment by Kaspar Valk, Kert Tali and Almas Baktubayev. The [project poster](https://raw.githubusercontent.com/InzGIBA/ReString/main/docs/Algoritmika%20poster%20puhtand.pdf) describes the original idea.
27
+
28
+ ## Features
29
+
30
+ <picture>
31
+ <source media="(prefers-color-scheme: dark)" srcset="assets/feature-01-dark.svg">
32
+ <img alt="Fast by design — a Rust greedy search, 7000 pulls in 0.8 s" src="assets/feature-01-light.svg" width="32%">
33
+ </picture>
34
+ <picture>
35
+ <source media="(prefers-color-scheme: dark)" srcset="assets/feature-02-dark.svg">
36
+ <img alt="Zero dependencies — a thin Python API over a Rust core" src="assets/feature-02-light.svg" width="32%">
37
+ </picture>
38
+ <picture>
39
+ <source media="(prefers-color-scheme: dark)" srcset="assets/feature-03-dark.svg">
40
+ <img alt="Verified output — golden tests pin pixels and pull order" src="assets/feature-03-light.svg" width="32%">
41
+ </picture>
42
+
43
+ - **One uncut thread**: the pull order is continuous by construction, so you wind a single thread by hand or machine.
44
+ - **Real thread colors**: black-on-white, white-on-black, or a palette. The built-ins are RGB, CMY and CMYK, or load a JSON list of the floss colors you own. CMYK measures 42 to 45 percent better than RGB at an equal budget.
45
+ - **Custom boards**: polygon, star, multi-ring and SVG-outline layouts via `--layout star:5:0.55`, plus non-square aspect crops ([layouts guide](docs/layouts.md)).
46
+ - **From photo to board**: save a `.restring` project, re-render in milliseconds, print a numbered-nail poster with the thread length in meters, watch the winding as a GIF.
47
+ - **Measured, not guessed**: every flag has a degradation curve behind it in the [tuning guide](docs/tuning.md).
48
+
49
+ ## Quick start
50
+
51
+ ### Install
52
+
53
+ ```bash
54
+ pip install restring-art
55
+ ```
56
+
57
+ Wheels cover linux (x86_64, aarch64), macos (x86_64, arm64) and windows (x86_64) on CPython 3.12 and newer. No Rust toolchain needed. Building from source needs uv and Rust, see [Development](#development).
58
+
59
+ ### Run your first example
60
+
61
+ ```bash
62
+ restring -i portrait.png -o out.png --rect -n 2 -l 3000 # best quality, ~1.4 s
63
+ ```
64
+
65
+ Expected result:
66
+
67
+ ```text
68
+ [...] [restring.cli] [INFO]: Nails amount: 598
69
+ ```
70
+
71
+ `out.png` now holds the rendered thread pattern.
72
+
73
+ ## Usage
74
+
75
+ ### Basic example
76
+
77
+ Three presets cover most jobs (timed on a 24-core Linux box):
78
+
79
+ ```bash
80
+ # fast preview, ~0.3 s
81
+ restring -i portrait.png -o out.png --rect -l 300
82
+
83
+ # balanced, ~0.6 s
84
+ restring -i portrait.png -o out.png --rect -n 4 -l 2000
85
+
86
+ # maximum measured quality, ~1.4 s
87
+ restring -i portrait.png -o out.png --rect -n 2 -l 3000
88
+ ```
89
+
90
+ ### CLI reference
91
+
92
+ | Flag | Meaning | Default |
93
+ |---|---|---|
94
+ | `-i <path>` | input image (PNG or JPEG) | required |
95
+ | `-o <path>` | output PNG | `output.png` |
96
+ | `-d <int>` | output side length in pixels | 300 |
97
+ | `-n <int>` | nail step for the circle and rect layouts. Smaller values place more nails. Minimum 1 | 4 |
98
+ | `--layout <spec>` | nail layout: `circle`, `rect`, `polygon:sides[:count]`, `star:points:inner[:count]`, `rings:r:count,r:count`, `svg:file.svg[:count]`, optional `@aspect` suffix for non-square boards. Custom layouts take counts instead of `-n` | circle |
99
+ | `-l <int>` | iteration budget. Without the flag the search stops after 3 pulls with no improvement | run to exhaustion |
100
+ | `-s <float>` | thread strength at export, 0..1. Leave unset and the run derives the value from the actual pull count: k/L with k=104, clamped to 0.03..0.2, within 2 percent of the hand-tuned optimum at the measured budgets | auto |
101
+ | `-r <int>` | scan a fixed random subset of candidates each iteration. Trades quality for speed. Good values sit near 100 to 150 | all nails |
102
+ | `-r1 <float>` | vertical nail ellipse multiplier | 1 |
103
+ | `-r2 <float>` | horizontal nail ellipse multiplier | 1 |
104
+ | `--rect` | nails on the rectangle border instead of the circle. Better for square portraits, about 2x lower error | circle |
105
+ | `--rgb` | colored output with three interleaved pure RGB threads | black and white |
106
+ | `--palette <name|file>` | thread palette: `rgb`, `cmy`, `cmyk`, or a JSON file of `{"name", "rgb": [r, g, b]}` colors (cap 8 threads). CMYK measures 42 to 45 percent better color MSE than RGB at an equal budget | off |
107
+ | `--wb` | white thread on a black canvas | black on white |
108
+ | `--weight <float>` | importance weighting. A Gaussian weight map steers threads toward the image center, where the viewer looks. Suggested range 1.5 to 5.0. Portrait subjects only, about 25 percent more scan time | off |
109
+ | `--min-chord <px>` | candidate filter: chords shorter than the distance in working-grid pixels are banned. Portrait measurement: the greedy never picks short chords, the filter only degrades MSE past 250 px. Intended for sparse custom layouts | off |
110
+ | `--project-out <path>` | save the nail layout and pull order as a `.restring` project alongside the PNG | off |
111
+ | `--project <path>` | re-render from a `.restring` project instead of an image: skips the search, applies the current `-s` and `-d` | off |
112
+ | `--anim <path>` | with `--project`: animated GIF of the winding sequence. `--anim-frames` sets the frame count, 180 is about 6 s at 30 fps. A 2000-pull project at the default 600 px export lands near 45 MB; `-d 450` brings it to about 28 MB, `-d 380` under 25 MB | off |
113
+ | `--realistic` | physical thread model: threads of `--thread-width` mm (default 0.2) on a `--canvas-size` mm board (default 400). At exports well above 300 px the 0.3 mm width beats the hairline by 35 percent MSE. The search never changes | hairline |
114
+ | `--rollback` | truncate a stagnating run to its best-error prefix. Measured no-op: the search never applies non-improving pulls, so nothing rolls back ([measurement](docs/research/rollback-experiment.md)) | off |
115
+ | `--metrics <path.json>` | write a quality report: MSE, correlation, plain and blurred SSIM against the working target, plus pulls, nails and elapsed time. Generate mode only | off |
116
+ | `--thread-width <mm>` | thread diameter for `--realistic`. Birsak used 0.15, typical polyester sits near 0.2 | 0.2 |
117
+ | `--canvas-size <mm>` | board size: the poster printing size (default 260 for `--poster`) or the physical size behind `--realistic` (default 400) | per feature |
118
+
119
+ Nails sit evenly on the perimeter. The circle starts at 9 o'clock and runs clockwise. The rectangle starts at the top left corner. The pull order is continuous by construction, so you wind one uncut thread.
120
+
121
+ ### Save and re-render
122
+
123
+ The project file keeps the nail layout and the pull order in plain JSON, so a result re-renders in milliseconds without a search and any tool can read the winding sequence:
124
+
125
+ ```bash
126
+ restring -i portrait.png -o out.png --rect -l 2000 --project-out result.restring
127
+ restring --project result.restring -o out2.png # byte-identical re-render
128
+ ```
129
+
130
+ The current `-s` and `-d` apply at re-render time. Schema and compatibility promise: [docs/project-format.md](docs/project-format.md).
131
+
132
+ ### Wind it for real
133
+
134
+ Turn a result into a printable poster: numbered nails, the footer with the nail count, pull count and total thread length in meters.
135
+
136
+ ```bash
137
+ restring -i portrait.png -o out.png --rect -l 2000 --project-out result.restring --poster poster.svg
138
+ restring --project result.restring --poster poster.svg --canvas-size 300
139
+ ```
140
+
141
+ Print at 100 percent scale, hammer the nails on the board outline, wind. Printing checklist and thread buying guidance: [docs/winding.md](docs/winding.md).
142
+
143
+ ### As a library
144
+
145
+ ```python
146
+ from restring import GenerateOptions, generate
147
+
148
+ result = generate("portrait.png", GenerateOptions(layout="rect", pulls=2000))
149
+ result.image.save_png("out.png")
150
+ print(result.stats.pulls_per_thread)
151
+ ```
152
+
153
+ The full options table, the Result fields and the deprecated 1.x names: [docs/api.md](docs/api.md). Images travel as `_core.Image` objects; the pixel data stays in Rust memory, so calls pass references instead of copying arrays.
154
+
155
+ ## Tuning
156
+
157
+ `-s` is fitted for you: the run derives the export strength from the actual pull count (104/L clamped to 0.03..0.2) and lands within 2 percent of the hand-tuned optimum at the measured budgets. The working simulation darkens each pull with strength 0.05; the export accumulates with strength `-s`. When `-s` is too high for the thread count, the render reaches its target halfway, then overshoots and darkens. Manual `-s` values stay for special cases, see the tuning guide.
158
+
159
+ | Flag | Recommended | Rule |
160
+ |---|---|---|
161
+ | `--rect` | always on for square portraits | 1.8 to 2.7x lower error than the circle |
162
+ | `-s` | leave unset, auto fits the pull count | measured optimum scales as 1 over thread count, k=104 clamped 0.03 to 0.2 |
163
+ | `-l` | up to saturation, about 2500 to 2900 threads | beyond saturation the search finds no improvements |
164
+ | `-n` | 4 for the knee, 2 for maximum quality | 844 circle nails buy 6 percent error reduction at 4x the cost |
165
+ | `-r` | 100 to 150 while exploring, drop for the final run | k=100 saves 40 percent of the time for 15 percent error |
166
+ | `-d` | 300 | exporting above the working grid hurts |
167
+ | `--weight` | 3.0 for portraits, off for other subjects | center MSE falls 24 to 28 percent, global MSE rises 3 percent |
168
+ | `--min-chord` | off | the greedy never picks short chords, thresholds bite past 230 px at up to 24 percent MSE cost |
169
+
170
+ Every flag, the numbers behind the rules and the degradation curves: [docs/tuning.md](docs/tuning.md).
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ git clone git@github.com:InzGIBA/ReString.git
176
+ cd ReString
177
+ uv sync
178
+ ```
179
+
180
+ Requirements: [uv](https://docs.astral.sh/uv/getting-started/installation/) and a Rust toolchain with a C linker for cargo. uv fetches Python 3.14 and compiles the core on first sync. No system C compiler (minimal WSL or container)? Use zig as the cargo linker — the recipe lives in [docs/architecture.md](docs/architecture.md#build).
181
+
182
+ Source checkouts prefix every command with `uv run`, for example `uv run restring -i portrait.png -o out.png --rect -l 300`.
183
+
184
+ ### Checks
185
+
186
+ ```bash
187
+ uv run pytest # 211 tests, ~11 s
188
+ uv run ruff check .
189
+ uv run python scripts/benchmark.py --label before # or after, then --plot
190
+ uv run python scripts/make_gallery.py # regenerate docs/img/gallery.png
191
+ ```
192
+
193
+ CI runs the same checks on every push to main and every pull request, on linux, macos and windows (`.github/workflows/ci.yml`). A `v*` tag builds wheels for three platforms, publishes to PyPI and opens the GitHub Release (`.github/workflows/release.yml`).
194
+
195
+ ## Documentation
196
+
197
+ | Document | Contents |
198
+ |---|---|
199
+ | [docs/architecture.md](docs/architecture.md) | how the pipeline works, the Rust core, correctness model, performance, build, wasm target |
200
+ | [docs/api.md](docs/api.md) | the 2.0 facade: `GenerateOptions`, `Result`, deprecated 1.x names |
201
+ | [docs/tuning.md](docs/tuning.md) | every flag, degradation curves, recommended values, presets |
202
+ | [docs/layouts.md](docs/layouts.md) | layout specs, the clockwise ordering rule, non-square boards |
203
+ | [docs/project-format.md](docs/project-format.md) | the `.restring` schema and the compatibility promise |
204
+ | [docs/winding.md](docs/winding.md) | printing checklist, reading the poster, buying thread |
205
+ | [docs/history.md](docs/history.md) | the migration story: stages, numbers, rejected hypotheses |
206
+ | [docs/research/](docs/research/) | twelve experiment docs with the numbers behind every verdict, plus the related-work survey |
207
+
208
+ ## Roadmap
209
+
210
+ Open experiment ideas arrive through the research-idea issue template: each states a question, a hypothesis and a pre-registered success bar. Closed questions live next to their measurements in [docs/research/](docs/research/), so nobody re-litigates them. Answered so far:
211
+
212
+ - [x] Re-run the RGB sweeps: working strength 0.05 adopted, 13 to 17 percent better MSE ([measurement](docs/research/rgb-channels-experiment.md)).
213
+ - [x] Test the working grid above 300 px: keep 300, under 3 percent MSE at up to 2.4x time ([measurement](docs/research/grid-scale-experiment.md)).
214
+
215
+ ## Contributing
216
+
217
+ Issues and pull requests are welcome. Templates structure both: bug reports carry the command line and image, feature requests check the measured dead ends, research ideas state a pre-registered success bar, and the PR template asks for the measurement or test evidence.
218
+
219
+ Wound a physical board? Post the photos under the "show and tell" label. Physical outputs are the best advertisement a string-art project gets. The example gallery ([examples/gallery/](examples/gallery/)) shows the configuration families and the commands behind every tile.
220
+
221
+ 1. Describe the problem or the proposed improvement.
222
+ 2. Make a focused change.
223
+ 3. Add or update relevant tests and documentation.
224
+ 4. Run the checks above and open a pull request.
225
+
226
+ ## License
227
+
228
+ MIT. See [LICENSE](LICENSE).
229
+
230
+ ## Photo credits
231
+
232
+ Portrait fixtures in `tests/data/` are square face-centered crops of studio photos provided for this project. The demo image above uses the `portrait_female.png` fixture. Earlier fixtures came from [Unsplash](https://unsplash.com/s/photos/people) under the [Unsplash License](https://unsplash.com/license).
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "restring-art"
3
+ version = "2.0.0"
4
+ description = "Recreate images by connecting nails with strings using a greedy algorithm (B/W and RGB)"
5
+ authors = [
6
+ { name = "Kaspar Valk" },
7
+ { name = "Kert Tali" },
8
+ { name = "Almas Baktubayev" },
9
+ ]
10
+ readme = "README.md"
11
+ license = "MIT"
12
+ # abi3-py312 in rust/Cargo.toml: one wheel serves CPython 3.12 and newer;
13
+ # development runs on 3.14 (.python-version) and CI tests that version
14
+ requires-python = ">=3.12"
15
+ dependencies = []
16
+
17
+ [project.scripts]
18
+ restring = "restring.cli:main"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=8.0",
23
+ "ruff>=0.14",
24
+ "matplotlib>=3.8,<4",
25
+ "numpy>=2.3",
26
+ ]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+ filterwarnings = [
31
+ "ignore:restring.generate_bw is deprecated:DeprecationWarning",
32
+ "ignore:restring.generate_rgb is deprecated:DeprecationWarning",
33
+ ]
34
+
35
+ [tool.ruff]
36
+ line-length = 120
37
+ src = ["src", "tests"]
38
+ extend-exclude = ["notebooks"]
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E4", "E7", "E9", "F", "I"]
42
+
43
+ [build-system]
44
+ requires = ["maturin>=1.5"]
45
+ build-backend = "maturin"
46
+
47
+ [tool.maturin]
48
+ python-source = "src"
49
+ module-name = "restring._core"