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.
- restring_art-2.0.0/LICENSE +21 -0
- restring_art-2.0.0/PKG-INFO +243 -0
- restring_art-2.0.0/README.md +232 -0
- restring_art-2.0.0/pyproject.toml +49 -0
- restring_art-2.0.0/rust/Cargo.lock +365 -0
- restring_art-2.0.0/rust/Cargo.toml +24 -0
- restring_art-2.0.0/rust/core/Cargo.lock +199 -0
- restring_art-2.0.0/rust/core/Cargo.toml +12 -0
- restring_art-2.0.0/rust/core/src/lib.rs +2028 -0
- restring_art-2.0.0/rust/src/lib.rs +441 -0
- restring_art-2.0.0/src/restring/__init__.py +17 -0
- restring_art-2.0.0/src/restring/__main__.py +4 -0
- restring_art-2.0.0/src/restring/_core.pyi +22 -0
- restring_art-2.0.0/src/restring/adaptive.py +102 -0
- restring_art-2.0.0/src/restring/animation.py +53 -0
- restring_art-2.0.0/src/restring/api.py +239 -0
- restring_art-2.0.0/src/restring/canvas.py +78 -0
- restring_art-2.0.0/src/restring/cli.py +245 -0
- restring_art-2.0.0/src/restring/generator.py +202 -0
- restring_art-2.0.0/src/restring/image_transformations.py +35 -0
- restring_art-2.0.0/src/restring/layouts.py +231 -0
- restring_art-2.0.0/src/restring/metrics.py +24 -0
- restring_art-2.0.0/src/restring/nails.py +13 -0
- restring_art-2.0.0/src/restring/palettes.py +99 -0
- restring_art-2.0.0/src/restring/poster.py +139 -0
- restring_art-2.0.0/src/restring/project.py +118 -0
- restring_art-2.0.0/src/restring/py.typed +0 -0
|
@@ -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
|
+
[](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
|
+

|
|
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
|
+
[](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
|
+

|
|
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"
|