Skip to content

User guide

Setup, the web demo, the sample images, every command and argument, output files, recipes and troubleshooting.

1. Setup

You need: - Windows, macOS or Linux, about 2 GB of free disk space and an internet connection for the first setup. - uv, the Python package manager. It installs the right Python (3.12) by itself.

Install uv (once):

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

Get the code and install everything:

git clone https://github.com/Prem-Dharshan/string-art.git
cd string-art
uv sync --extra demo          # creates .venv with Python 3.12 + all dependencies (incl. the web demo)
uv run stringart fetch-models # downloads the face models into models/ (0.2 MB + 56 MB)

  • uv sync without --extra demo installs everything except the web demo (Gradio).
  • fetch-models downloads YuNet (face detection) and LBF (68 facial landmarks). Without them the program still works, but it can't crop to faces or weight facial features. --no-lbf skips the 56 MB landmark model.

Check that it works:

uv run pytest -q              # 87 tests, about 1.5 minutes

The first run of anything that solves takes 10–30 s longer than usual, because the solver compiles itself once (numba) and caches the result. Later runs start straight away.


2. Quick start

uv run stringart run sample:woman_smiling     # solve a sample photo
uv run stringart viz outputs/woman_smiling_greedy   # watch it form, thread by thread

Open outputs/woman_smiling_greedy/render.png to see the simulated piece. Every command below works the same way on your own photo: replace sample:woman_smiling with a path such as C:\photos\me.jpg or ~/photos/me.jpg.


3. The web demo

uv run --extra demo stringart demo
Then open http://127.0.0.1:7860. It runs only on your computer; nothing is uploaded anywhere. Stop it with Ctrl+C.

  1. Photo: upload a file (any photo, or one from data/samples/) or paste from the clipboard.
  2. Frame and thread:
control default what it does
Pins 300 nails around the frame. More = finer detail, with little gain past ~256
Frame diameter (mm) 700 the real frame size
Thread width (mm) 0.25 with the frame size, sets how dark one thread looks: thinner thread or a bigger frame = more lines and more detail
Thread colours 1 1 = black thread only; 2–5 = colour, with the thread colours chosen automatically
  1. Advanced (optional):
control default what it does
Refinement sweeps 2 extra passes that move, add or remove pins to improve the path (black only). 0 is fastest
Crop face face centres the frame on the largest face; center uses the middle of the photo
Importance weights auto spends more thread on eyes, mouth and edges. auto = only when a face is found
  1. Click Make string art. It takes about 10–30 s.

You get back: - the preprocessed target and the simulated string art - the build-up animation (GIF), with an mp4 to download - a summary line: number of lines, thread length, faces found, time and a quality score - the build kit zip: winding list, 1:1 pin template (A4 PDF + SVG), pin coordinates, shopping list - sequence.json

Other options: stringart demo --port 8000 uses another port. --host 0.0.0.0 makes it reachable from other devices on your network; the Docker setup uses this.

The online version (Streamlit)

The same pipeline also runs as a Streamlit app, app/streamlit_app.py, hosted at https://cv-string-art.streamlit.app/. It has the same controls in the sidebar, plus a picker for the sample photos. To run it locally:

uv run --extra web streamlit run app/streamlit_app.py    # opens http://localhost:8501

The hosted copy runs on a free shared CPU, so a photo takes about 30–90 s there. Photos uploaded to it are processed on Streamlit's servers.

Deploying it (one-time, on Streamlit Community Cloud): sign in with GitHub, click Create app → Deploy a public app from GitHub, and enter repository Prem-Dharshan/string-art, branch master, main file app/streamlit_app.py. Under Advanced settings, pick Python 3.12. Streamlit installs app/requirements.txt (the locked dependencies, with headless OpenCV) and redeploys on every push to master. The repo is private, so the GitHub account that deploys it needs access to it.

app/requirements.txt is generated from uv.lock. After changing dependencies, run uv run python tools/streamlit_requirements.py again. (tools/deploy_space.py packages the Gradio demo for Hugging Face Spaces instead, but free Gradio Spaces need a PRO account.)


4. Sample images

Eight test photos are in data/samples/. Credits and licences are in Credits. Use them as sample:<name> or by path:

name good for
woman_smiling black thread, clear frontal face
elderly_man_bw black thread, lots of fine detail (beard, wrinkles)
girl_bw black-and-white child portrait
athlete_dark_bg face on a dark background
boy_yellow_shirt colour: skin tones plus a saturated yellow shirt
child_monk_orange colour: orange robe, small face
border_collie animal (no human face), colour
lighthouse object/scene, red and white stripes, colour

scikit-image's built-in test images also work: sample:astronaut, sample:camera, sample:coffee and sample:chelsea (a cat).

# black thread
uv run stringart run sample:woman_smiling
uv run stringart run sample:elderly_man_bw --pins 300

# colour (4 thread spools)
uv run stringart run sample:boy_yellow_shirt --colors 4
uv run stringart run sample:lighthouse --colors 4

# by path, same thing
uv run stringart run data/samples/border_collie.jpg --colors 3

# then look at any result
uv run stringart viz outputs/boy_yellow_shirt_greedy_color

To try all samples at once:

# PowerShell
Get-ChildItem data/samples/*.jpg | ForEach-Object { uv run stringart run $_.FullName --quiet }
# bash
for f in data/samples/*.jpg; do uv run stringart run "$f" --quiet; done


5. Command reference and arguments

Every command prints its own help with defaults: uv run stringart <command> --help.

command what it does
run solve: photo → pin sequence, render, build sheet
viz replay a result thread by thread; export video/GIF/snapshot grid
kit build kit: 1:1 pin template, pin coordinates, shopping list
wind winding assistant for a real piece, one line at a time
calibrate measure your real thread's darkness from one photo
demo the web demo (section 3)
fetch-models download the face models

stringart run

uv run stringart run IMAGE [options]
IMAGE is a photo path or sample:<name>.

The canvas and thread model

The photo is turned into a 600 × 600 px canvas (--size). Each thread darkens every pixel it crosses by opacity: the fraction of a pixel's width the thread covers. Overlapping threads darken further but never past black. With a real frame and thread:

opacity = thread width (mm) × canvas size (px) / frame size (mm)
        = 0.25 × 600 / 700 ≈ 0.21     (0.25 mm thread on a 700 mm frame)

Give --thread-mm and --frame-mm and the opacity is computed for you. Lower opacity (thinner thread or bigger frame) means more lines and finer detail.

Frame and thread

argument default meaning when to change it
--frame {circle,rect} circle pins on a circle or a square square frames
--pins N 256 number of pins your frame's pin count. 200–320 is typical; gains are small past ~256
--size PX 600 canvas resolution larger = finer and slower. Keep 600 to match the evaluation
--opacity A 0.2 darkness of one thread on one pixel (see above) set it directly, or use the next two
--thread-mm W – real thread width, mm with --frame-mm, sets --opacity
--frame-mm D – real frame diameter (or side), mm also makes the build sheet show metres of thread
--min-gap G 10 never connect pins closer than G positions apart (short chords hug the rim and waste thread) about 4% of --pins

Solver

argument default meaning when to change it
--solver {greedy,baseline} greedy greedy = this project's solver (picks the best line each step, stops by itself). baseline = the prior LessWrong/Vrellis method, for comparison comparisons only
--lines N – greedy: a cap (default 8,000; it usually stops earlier by itself). Baseline: the exact count (default 3,000). Colour: cap 12,000 to force fewer lines
--refine K 2 refinement sweeps after greedy. Each sweep tries to delete, move or insert a pin anywhere on the path and keeps a change only if the picture gets closer to the target (black only) 0 for a fast preview (~1 s); 3 for a little more quality
--objective {pixel,blur} pixel what "closer" means: per pixel (fast), or after a blur that simulates viewing distance (about 3× slower, slightly better from afar) experiments
--blur-sigma S 1.5 blur radius for --objective blur, px –
--max-repeats R 2 how often the same pin pair may be used rarely

Colour

argument default meaning when to change it
--colors N 1 number of thread colours. 1 = black only; ≥ 2 = colour mode (always includes black) 3–4 for colour photos
--palette LIST – choose the threads yourself, e.g. black,red,tan,blue when you already own the spools. Names: black, white, grey, red, maroon, orange, yellow, tan, brown, green, dark_green, cyan, blue, navy, purple, pink
--palette-method {gamut,kmeans,auto} gamut how threads are chosen. gamut: the threads that can best reproduce the photo's colours (most accurate colour). kmeans: the photo's main colour clusters snapped to threads (slightly more light/dark detail, duller colour). auto: preview both (not reliable) kmeans if structure matters more than colour
--min-run L 100 lines in a row before switching to another colour spool lower = more spool switches (no quality gain measured)

Preprocessing and importance

argument default meaning when to change it
--crop {face,center} face centre the frame on the largest face (falls back to the centre) center for scenes, or to keep the original framing
--face-zoom Z 1.8 crop size = Z × face height. Smaller = face fills more of the frame 1.5 tighter, 2.5 head and shoulders
--importance {auto,on,off} auto spend more thread on eyes, brows, nose, mouth, edges and salient areas. auto = only when a face is found on to use edges/saliency on non-face images
--importance-floor F 0.1 weight of unimportant areas (0–1). Lower = background matters less 0.05 for even more face detail
--background {none,fade} none fade lightens the background (GrabCut), so fewer threads go there. Faces only for a cleaner, more graphic look
--clahe C 2.0 local contrast boost (CLAHE clip limit); 0 = off 0 for already contrasty photos
--legacy-prep off use the old preprocessing (centre crop, CLAHE, Gaussian blur) instead comparisons only
--blur S 1.0 Gaussian blur for --legacy-prep –

Badly exposed photos (too dark, flat or washed out) are detected and corrected automatically (level stretch + gamma). Well-exposed photos are left alone.

Baseline-only

argument default meaning
--line-strength S 0.1 darkness removed from the target per line
--candidates N all score only N random candidate lines per step (as in the original method)
--darkness-penalty D 0.0 penalty for over-darkening already dark pixels
--seed N 0 random seed for --candidates

Output and misc

argument default meaning
--out DIR outputs/<image>_<solver> (colour: …_color) where to write the results
--viz off open the visualizer when done
--quiet off less console output

Examples:

# your photo, for a 700 mm frame with 300 pins and 0.25 mm thread
uv run stringart run photo.jpg --pins 300 --frame-mm 700 --thread-mm 0.25

# fast preview, then the real thing
uv run stringart run photo.jpg --refine 0 --out outputs/preview
uv run stringart run photo.jpg --refine 3 --out outputs/final

# colour with the threads you own
uv run stringart run photo.jpg --palette black,tan,brown,red --frame-mm 700 --thread-mm 0.25

# square frame, 200 pins, no face crop
uv run stringart run scene.jpg --frame rect --pins 200 --crop center

stringart viz

Replays a result thread by thread through the same renderer as the final image.

uv run stringart viz RUN_DIR_or_sequence.json [options]

argument default meaning
--save FILE – export .mp4 or .gif (repeatable: --save a.mp4 --save a.gif)
--grid 100,500,1500,3000 – save grid.png showing the piece at those line counts
--show off also open the live player when exporting
--step N auto lines drawn per frame
--fps N 30 export frame rate (GIFs are capped at 10)
--duration S 15 export length in seconds (sets --step automatically)
--scale X 1.0 export resolution scale (0.5 = half size, smaller files)
--target FILE target.png image shown beside the render
--opacity A from the run override the thread darkness

With no --save or --grid, the live player opens. It shows three panels (target | threads so far with the newest line highlighted | error map), plus line number, pins and running SSIM. Keys:

key action
space pause / resume
→ one line (while paused)
+ / - faster / slower
e jump to the end
uv run stringart viz outputs/woman_smiling_greedy
uv run stringart viz outputs/woman_smiling_greedy --save build.mp4 --save build.gif --scale 0.5
uv run stringart viz outputs/woman_smiling_greedy --grid 250,1000,2000,3000

stringart kit

Everything needed at the workbench for a real piece. Details are in Build guide.

uv run stringart kit RUN_DIR --frame-mm 700 [--spool-m 500]

argument default meaning
--frame-mm D required real frame diameter (or side) in mm
--spool-m M 500 metres of thread per spool, for the spool count

It writes frame_template_A4_tiles.pdf, frame_template.svg, pins.csv and shopping_list.txt into the run folder (see Output files).

stringart wind

The winding assistant. It steps through the lines one at a time and saves progress so you can stop and resume.

uv run stringart wind RUN_DIR [--reset]

key action
space / → / n line done, show the next
← / b back one line
digits, then enter jump to that line number
e jump to the end
q quit (progress is already saved)

On screen: the picture so far with the next line highlighted (circle = from pin, square = to pin), line 1,234 / 3,187 · black · pin 87 → pin 203, a spool-switch notice in colour runs, and the next six steps. --reset starts again from line 1.

stringart calibrate

Measures how dark your real thread looks, so the simulation matches reality. It needs a built frame; see Build guide.

uv run stringart calibrate sheet [--pins 300] [--frame-mm 700] [--lines 250] [--out outputs/calibration]
uv run stringart calibrate fit PHOTO [--sheet outputs/calibration] [--circle cx,cy,r]
- sheet writes a short test pattern and its build sheet. - fit takes a front photo of the wound pattern (pin 0 at the top), finds the frame automatically (--circle overrides it) and prints the --thread-mm to use from then on.

stringart demo

uv run --extra demo stringart demo [--port 7860] [--host 127.0.0.1]

stringart fetch-models

uv run stringart fetch-models [--no-lbf]
This downloads into models/. Set the environment variable STRINGART_MODELS to keep the models elsewhere.


6. Output files

A run folder (e.g. outputs/woman_smiling_greedy/) contains:

file what it is
render.png the simulated piece (render.svg too, for black thread)
target.png the photo after cropping and preprocessing: what the solver aimed for
importance.png where the solver spent extra effort (when importance was used)
sequence.json the result: pin positions and the pin order (colour: every step with its thread)
instructions.txt the build sheet: numbered winding list in blocks of 100 lines, with running thread length (metres if --frame-mm was given)
metrics.json settings, timing, faces found, quality scores (SSIM/PSNR; colour: CIEDE2000 plus lines per colour and spool switches) and, for black runs with --frame-mm, thread length

After stringart viz --save/--grid:

file what it is
build.mp4, build.gif the build-up animation
grid.png snapshots at chosen line counts

After stringart kit and stringart wind:

file what it is
frame_template_A4_tiles.pdf 1:1 pin template over A4 pages. Print at 100% / actual size and check the 50 mm bar
frame_template.svg the same template as one sheet (print shop / plotter)
pins.csv each pin's x/y in mm and its angle clockwise from the top
shopping_list.txt board size, pins, thread per colour and spools
progress.json how far you've wound (from stringart wind)

7. Recipes

goal command
quick look at a photo uv run stringart run photo.jpg --refine 0 --viz
best quality, black uv run stringart run photo.jpg --refine 3
face fills more of the frame uv run stringart run photo.jpg --face-zoom 1.5
cleaner background uv run stringart run photo.jpg --background fade
colour uv run stringart run photo.jpg --colors 4
colour with spools you own uv run stringart run photo.jpg --palette black,red,yellow,blue
finer detail (thinner thread or bigger frame) uv run stringart run photo.jpg --frame-mm 900 --thread-mm 0.25
compare with the old method uv run stringart run photo.jpg --solver baseline --legacy-prep --out outputs/old
video for a presentation uv run stringart viz outputs/<run> --save talk.mp4 --duration 20
everything for a real build uv run stringart kit outputs/<run> --frame-mm 700, then uv run stringart wind outputs/<run>

8. Docker

A Linux image with everything, including the demo. Datasets, face models and results live in named Docker volumes. The report's numbers were produced this way.

docker compose build
docker compose run --rm stringart stringart fetch-models
docker compose run --rm stringart stringart run sample:woman_smiling --out outputs/docker_test
docker compose run --rm stringart pytest -q
docker compose up demo          # web demo at http://localhost:7860
  • Memory per container is capped (STRINGART_MEM_LIMIT, default 6g), and so are solver threads (STRINGART_THREADS, default 8).
  • Copy results out with docker compose cp stringart:/app/outputs ./outputs_docker.

9. Experiments and the report

The 30-image evaluation set (Wikimedia Commons, openly licensed) and a separately chosen held-out set are downloaded by script, with attribution in data/ATTRIBUTION.md and data/ATTRIBUTION_heldout.md.

uv run python experiments/fetch_dataset.py         # evaluation set -> data/raw/
uv run python experiments/evaluate_dataset.py      # main comparison, robustness, curves, sweeps
uv run python experiments/evaluate_refine.py       # refinement
uv run python experiments/evaluate_color.py        # colour vs the prior method
uv run python experiments/evaluate_exposure.py     # exposure handling, incl. held-out set
uv run python experiments/figures_m6.py --docs     # report figures

Or run all of them in Docker in one go:

docker compose run -d --name sa-exp stringart bash experiments/run_all.sh
docker logs -f sa-exp


10. Development

uv run pytest -q                              # 87 tests
uv run python tools/ruff.py check             # lint
uv run python tools/ruff.py format --check    # formatting

tools/ruff.py runs ruff. If Windows blocks the unsigned ruff.exe, it runs the same version through WSL instead.


11. Troubleshooting

problem fix
note: face model not found run uv run stringart fetch-models
first run is slow the solver compiles once (10–30 s) and caches it; later runs are fast
no face found in a portrait very small, turned or covered faces can be missed. The photo's centre is then used and --face-zoom has no effect, so crop the photo closer to the face first
result too dark / too light overall adjust the thread: lower --opacity (or a thinner --thread-mm) → more, lighter lines
demo: port already in use uv run --extra demo stringart demo --port 7861
demo animation not playing in the browser the page shows a GIF; the mp4 is offered as a download (browsers often can't play OpenCV's mp4 codec)
ImportError mentioning gradio install the demo extra: uv sync --extra demo
OpenCV import errors after installing other packages don't also install opencv-python; it conflicts with opencv-contrib-python
PC runs low on memory during long experiments run them in Docker (memory is capped), see section 8
printed template is the wrong size print at 100% / actual size, never "fit to page"; check the 50 mm bar with a ruler

12. Project layout

src/stringart/
  preprocess.py   crop, exposure (stretch + gamma), smoothing, CLAHE, background fade
  face.py         YuNet face detection + LBF landmarks
  importance.py   face / edge / saliency importance maps
  raster.py       anti-aliased line rasterizer
  render.py       thread renderer (black)        color.py   colour model, palettes, solver
  solver/         greedy.py (main), refine.py (path refinement), baseline.py (prior method)
  viz.py          thread-by-thread player and exports
  fabrication.py  build sheets                   kit.py     template, pins, shopping list
  wind.py         winding assistant              calibrate.py  thread calibration
  metrics.py      PSNR / SSIM                    demo.py    web demo        cli.py  commands
data/samples/     8 sample photos (+ ATTRIBUTION.md)
experiments/      evaluation scripts and figures
docs/             this site: guide, report, milestone log, build guide, credits
tests/            87 tests