Floyd-Steinberg Dithering: Organized Chaos in Black and White
A spore print either blocks UV light or it doesn’t—there’s no half-exposed cyanotype chemistry. You get Prussian blue or you get white paper. The question is whether microscopic spores, scattered across gill ridges, can create the illusion of gradients through their density alone.
This is the same problem newspapers faced in the 1980s when printing photographs. Each dot of ink is binary: present or absent. Floyd-Steinberg dithering solved it by pushing quantization error forward into neighbouring pixels. When you round a grey value to black or white, you don’t discard the error—you distribute it to pixels you haven’t processed yet.
def dither(pixels, width)
pixels.each_with_index do |grey, i|
new = grey > 127 ? 255 : 0
err = grey - new
pixels[i] = new
pixels[i+1] += err * 7/16.0 if (i+1) % width != 0
pixels[i+width-1] += err * 3/16.0 if i+width < pixels.size && i % width != 0
pixels[i+width] += err * 5/16.0 if i+width < pixels.size
pixels[i+width+1] += err * 1/16.0 if i+width < pixels.size && (i+1) % width != 0
end
end
The C version shows why it’s fast enough for real-time printing:
void dither(unsigned char *img, int w, int h) {
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int i = y * w + x;
int old = img[i];
int new = old > 127 ? 255 : 0;
img[i] = new;
int err = old - new;
if (x+1 < w) img[i+1] += err * 7 >> 4;
if (y+1 < h) {
if (x > 0) img[i+w-1] += err * 3 >> 4;
img[i+w] += err * 5 >> 4;
if (x+1 < w) img[i+w+1] += err >> 4;
}
}
}
}
The bit-shifting (>> 4 instead of /16) keeps it integer-only. The forward-only propagation means you can process scanlines as they arrive from the scanner or camera.
Spore prints won’t distribute error intentionally, but density variation does the same work. Whether the result is a detailed gill pattern or a flat blue disc depends on whether five-micron particles scatter densely enough to approximate a halftone screen. The first print is still exposing.