Дизеринг Флойда-Стейнберга - поистине волшебная техника. Предполагается, что это обманывает ваш глаз и мозг, заставляя вас думать, что вы видите больше, чем есть на самом деле.

В общем, дизеринг - это метод уменьшения цветового пространства изображения путем добавления искусственного шума. Ключевой идеей является то, что количество света в области должно оставаться примерно одинаковым.

Floyd-Steinberg использует неравномерное распределение ошибки квантования по окружающим пикселям. Это означает, что центральный пиксель округляется до 0 или 1. Затем остаточная ошибка добавляется к окружающим пикселям.

Все три изображения, которые вы можете найти в этой статье, были полутоновыми и размытыми. Все они состоят только из двухцветного шума. Остальным занимается ваш мозг.

А если хотите увидеть настоящие шедевры, попробуйте погуглить C64 artwork. Изображения обычно имеют 4, 8 или 16 цветов, но мы воспринимаем гораздо более широкую цветовую гамму только из-за примененного дизеринга.

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

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

алгоритм

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 image
def 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

запустить

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