Day 96: Floyd-Steinberg

Tomáš Bouda
100 days of algorithms
3 min readJun 28, 2017

--

Floyd-Steinberg dithering is a truly magical technique. It is supposed to fool your eye and brain to make you think that you see more than there really is to be seen.

In general, dither is method to reduce color space of an image by adding an artificial noise. The key idea is that the amount of light in an area should remain about the same.

Floyd-Steinberg uses non-uniform distribution of quantization error to surrounding pixels. It means that the center pixel is rounded to 0 or 1. The residual error is then added to surrounding pixels.

All the three pictures you can find in this article were grayscaled and dithered. They all consist of only two-color noise. The rest is handled by your brain.

And if you want to see real masterpieces, try to google C64 artwork. The images usually have 4, 8 or 16 colors, but we percept much wider color scale just because of the dithering applied.

https://github.com/coells/100days

https://notebooks.azure.com/coells/libraries/100days

algorithm

def image_dither(path, black='#000000', white='#ffffff'):
image_rgb = read_image(path)
image_gray = grayscale(image_rgb)
image_bw = floyd_steinberg(image_gray)
show(layout([[
plot(image_gray, palette=gray(256)),
plot(image_bw, palette=[black, white])
]]))
def floyd_steinberg(image):
image = image.copy()
distribution = np.array([7, 3, 5, 1], dtype=float) / 16
u = np.array([0, 1, 1, 1])
v = np.array([1, -1, 0, 1])

for y in range(image.shape[0] - 1):
for x in range(image.shape[1] - 1):
value = np.round(image[y, x])
error = image[y, x] - value
image[y, x] = value
image[y + u, x + v] += error * distribution

image[:, -1] = 1
image[-1, :] = 1
return imagedef grayscale(image):
height, width, _ = image.shape

image = np.array(image, dtype=np.float32) / 255
image = image[:, :, 0] * .21 + \
image[:, :, 1] * .72 + \
image[:, :, 2] * .07

return image.reshape(height, width)
def read_image(path, size=400):
if path.startswith('https://'):
image = Image.open(get(path, stream=True).raw)
else:
image = Image.open(path)

width, height = image.size
width, height = size, int(size * height / width)
image = image.resize((width, height), Image.ANTIALIAS)

data = image.getdata()
assert data.bands in [3, 4], 'RGB or RGBA image is required'

raw = np.array(data, dtype=np.uint8)
return raw.reshape(height, width, data.bands)
def plot(image, palette):
y, x = image.shape
plot = figure(x_range=(0, x), y_range=(0, y),
plot_width=x, plot_height=y)
plot.axis.visible = False
plot.toolbar_location = None
plot.min_border = 0
plot.image([np.flipud(image)], x=0, y=0, dw=x, dh=y,
palette=palette)

return plot

run

image_dither('./resource/day 96 - valinka.jpg')

--

--