Skip to content

Under the hood

One line at a time, greedily.

There is no neural network here and no cloud. The whole thing is a loop that asks the same question a few thousand times: from where the thread is now, which single nail would make the board look most like the photograph?

Line0 / 4,022Nail001

Step one

Turn the photo into a debt.

The cropped square is reduced to a 500-pixel working image and converted to luma. The tones are stretched between the 1st and 99th percentile, and everything outside the board outline is set to white so no line ever tries to help there.

What comes out is a map of wanted darkness: for every pixel, how dark the finished board should be there. Alongside it the solver keeps a second map of the darkness it has actually delivered so far. The gap between them is the error it is shrinking.

Nothing is capped or compressed. An earlier version of this tool scaled the whole target down whenever a photo asked for more darkness than it thought thread could deliver, which is a documented way to wash a picture out — and it did. The greedy loop below allocates the thread budget perfectly well on its own.

nowbest
Every candidate chord is scored by walking its pixels and adding up how much error it would remove. The best wins; if none would remove any, the piece is finished. With 240 nails that is a couple of hundred candidates per line, each a few hundred pixels — cheap enough to run on a phone.

Step two

Pick, lay it down, repeat.

01

Score every candidate

Walk each possible chord with an anti-aliased line and add up how much it would reduce the squared error. Pixels count in proportion to how much of them the thread really covers.

02

Let the algebra handle overshoot

Crossing somewhere already dark enough scores negative, because the error there would get worse. No tuned penalty and no fudge factor: it falls out of the objective.

03

Refuse silly chords

No chord shorter than a tenth of the ring, no nail pair reused, no nail revisited within the last 24 moves. These stop the thread rattling around one arc.

04

Lay it down and move on

The chosen chord darkens the delivered map by exactly what one real pass of thread would, the thread is now at that nail, and the loop runs again.

The objective, in full

Thread over thread does not add up in a straight line. The second pass over a spot darkens it less than the first, because there is less white left to cover. So one more pass over pixel p deposits

g = (INK − delivered) × α × coverage

and adding a chord changes the squared error there by (r − g)² − r², where r is how much darkness is still wanted. Summed along the chord, the solver takes whichever one maximises

Σ ( 2·r·g − g² )

That single line replaced three hand-tuned constants. Over-darkening is penalised by exactly the right amount, because r goes negative and takes the score with it. The loopstops by itself when no chord has a positive score left, so there is no arbitrary threshold. And the target never needs compressing.

α is the fraction of a pixel one pass darkens: thread width divided by pixel width, multiplied by 1.4. Real thread is fuzzy and casts a shadow, so it covers more than its cross-section suggests — but not so much more that heavy, coarse lines are right either. The value was set by a head-to-head bench against published gallery results: heavier lines reach the same tone with fewer chords but read coarse next to the fine mezzotint texture the good pieces show; lighter lines with a bigger budget match it while keeping faces legible.

The rendering constraint

Every chord is stroked separately.

This sounds like an implementation detail. It is the difference between a portrait and a flat grey disc.

Canvas paints a path once, as a union. Batch three thousand overlapping segments into a single path and every crossing gets painted at the same alpha as a single thread — so nothing ever gets darker, which is the one thing string art depends on. Safari showed this immediately; desktop Chrome hid it on large paths, which made it a genuinely nasty bug.

So the renderer here calls beginPath() and stroke() once per chord, several thousand times. It is slower and it is correct. If you ever fork this and the preview goes flat, that is what you changed.

Colour

CMYK mode splits the image into yellow, cyan, magenta and black targets, solves each one separately with a share of the line budget proportional to how much of that colour the image contains, and previews them multiplied together. You wind them in that order — light to dark — because the last thread laid sits on top.

What it is not

  • Not optimal. Greedy algorithms take the best step now, not the best sequence overall. A globally optimal solution exists and is far more expensive to find; the difference is not visible on a board.
  • Not face-aware. "Favour the centre" is a radial weight, not face detection. It helps because faces are usually in the middle.
  • Not deterministic across settings. Nail count, board size and thread thickness all feed the same equation. Change one and expect a different sequence.
  • Not the last word. The strongest published solvers also remove chords they placed earlier and try better ones in their place. This one only ever adds. That is the most promising thing left to try.

How it was tuned

The constants were not chosen by eye. A bench in the repo solves a set of reference pictures under every candidate configuration, renders them with the physical model, blurs both render and target to roughly what an eye resolves across a room, and scores three things: absolute error against the achievable tonal range, the spread of the result, and how dark the darkest fifth of the board actually gets.

The bench also runs head-to-head against the published results of other generators, on their own source photos, scoring both closeness to the photo and closeness to their output. Three decisions came out of losing that comparison and fixing why: chords are scored by their total error reduction rather than the per-pixel average (the average favoured short rim-hugging chords — the source of an ugly dark ring), the highlight-clipping in auto-levels was softened five-fold (it was mapping bright areas to "needs no thread at all"), and the darkness presets now deliberately ask for a little more than the photo, which is the open secret behind every rich-looking gallery piece.

Ten seconds is a cheap experiment.