Side project · Crochet · Math

Turning pictures into tapestry crochet

I crochet, so I built a tool that turns a picture into a stitch chart. One Reddit post brought 500+ people to it.

A crocheted wall hanging with a dark red devil holding a trident on a tan background.
Something I made. The Manchester United devil, in two colors of yarn.

A picture, one stitch at a time

Tapestry crochet is colorwork. You carry the yarn you aren't using inside your stitches, and switch colors when the chart tells you to. Every stitch is one cell in a grid, so a finished piece is really a low-resolution image made of yarn.

A small crocheted square showing a tree with a green canopy and a brown trunk on a cream background.A square-grid chart of the same tree's canopy, with numbered rows, on a computer screen.
The piece and its chart. Each square on the screen is one stitch. The chart is from Stitch Fiddle.

Getting from a picture to a chart is the step TapestryCrochet does for you. You upload an image, pick a width and height from 20 to 100 stitches, and pick 2 to 8 colors. You get a chart you can recolor, export as text, HTML or JSON, and work through row by row with notes. All of it runs in the browser, so your picture never leaves your computer.

I posted it on Reddit when I launched it, and that post brought in 500+ users.

Step 1: make the picture small

The app draws your image onto a canvas exactly as many pixels wide as you want stitches. Each pixel becomes one stitch, so 40 by 40 is already 1,600 stitches. It doesn't correct for the fact that a stitch isn't perfectly square.

The devil at 20 stitches wide, barely recognizable.20 wideThe devil at 40 stitches wide, with the trident visible.40 wideThe devil at 80 stitches wide, with clean edges.80 wide
Resolution is a stitch budget. A photo of my finished piece, put back through the app's own code with two colors.

Step 2: find the yarn colors

A photo has thousands of colors and you own a few balls of yarn. So picture every color as a point in 3D space, with red, green and blue as the axes. Now the job is to pick the k points that best stand in for the whole cloud. That's k-means clustering:

  1. Pick k colors from the image at random as starting centers.
  2. Assign every color to its nearest center.
  3. Move each center to the average of the colors assigned to it.
  4. Repeat until nothing changes.

“Nearest” needs a definition, because your eye doesn't treat red, green and blue equally. The clustering weights green the most and also compares brightness:

d² = 2Δr² + 4Δg² + 3Δb² + ½ΔL²

L = 0.2126r + 0.7152g + 0.0722b

Each stitch then gets the number of its closest yarn, and the chart is just those numbers in rows:

Row 1: 1 1 1 2 2 1 1
Row 2: 1 1 2 2 2 2 1

The bug I found writing this

Rereading the code for this post, I noticed step 4 never happens. Here's the check for whether the centers have stopped moving:

let assignments = new Array(colors.length);
// ...
hasConverged = assignments.every((a, i) => a === newAssignments[i]);

On the first pass, assignments is an array of empty slots, and every skips empty slots. With nothing to check it returns true, so the loop quits after one round.

I measured what that costs on my tree photo, at 40 by 40 with 3 colors, over 200 random starts. One round averaged a squared color error of 828 per stitch. Letting it run until it settled averaged 427, about half. The worst start was 4,821 against 777.

The tree in three colors with speckled noise in the background.One round, as the code runs todayThe same tree in three colors with a much cleaner background.Run until it settles
Same photo, same random start. I used the start with the median error of the 200, so this isn't a cherry-picked one.

People were getting usable charts from one round of k-means, which says something about how forgiving yarn is. The fix is one line: fill that first array with -1.

The code is on GitHub.