Day 96: Floyd-Steinberg
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')