Реализуем дизеринг в dlib

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

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

Для начала объясним в двух словах, что такое дизеринг.

Дизеринг— это такая схема смешивания цветов из ограниченной палитры, которая позволяет смешивая цвета из палитры определенным образом: к примеру отсутствующие цвета, могут получаться путем расстановки существующих цветов в шахматном (или ином) порядке, а также эффект может достигаться подмешиванием шума со специально подобранными характеристиками. Для достижения эффекта глубины цвета (скажем сразу, глубина, а также отсутствующие цвета, которые при этом не исчезают с обработанного изображения являются чистой воды иллюзией) есть несколько очень известных алгоритмов. Мы будем делать реализацию одного из таких алгоритмов, который называется алгоритмом Флойда-Стейнберга.

Данный алгоритм основан на распространении ошибки, что означает, что ошибки от квантования переносятся на соседние пиксели, с которыми далее продолжается работа. По сути для того, чтобы алгоритм работал надо фактически вычислить насколько пиксель, который заменит текущий отличается от существующего в изображении (это и есть та самая ошибка) и применить эту разницу с определенными коэффициентами к соседним пикселям. Фактически, нам необходимо выполнить перемножение полученной ошибки на следующую матрицу значений:

Коэффициенты для учета распространения ошибки в алгоритме Флойда-Стейнберга

Здесь звездочкой обозначен текущий пиксель, а коэффициенты показаны для его ближайших соседей (в контексте данного алгоритма), и на схеме показан только один шаг алгоритма, а его надо повторить для всех пикселей в изображении. Повтор этого шага приводит к постоянному накоплению и продвижению разницы между исходным и пикселем результатом трансформации, что в итоге приводит к изменению всех пикселей в изображении.

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

Как вы понимаете, решение о том, как это сделать переходит на самого программиста. И вот тут мы кое что придумали…

Однажды мы уже выкладывали набор палитр в удобной для загрузки форме, и мы как раз предлагаем использовать либо загрузку палитры из какого-либо файла этой коллекции, либо ее ручное создание (путь для истинных ценителей, так сказать). Но просто загрузки недостаточно, поскольку сама палитра — это просто набор цветов, а в данном случае необходимо для каждого пикселя из изображения найти наиболее «подходящий» цвет из палитры.

Для достижения подобных целей, а также для реализации всего алгоритма будем использовать библиотеку dlib, с помощью которой задача загрузки и поиска ближайшего цвета из палитры решаются так:

import std.algorithm;
import std.conv;
import std.math;
import std.range;
import std.stdio;
import std.string;
import std.file;

import dlib.image;

Color4f[] getPalette(string filename)
{
    Color4f[] palette;

    auto extractField(string triplet)
    {
        auto content = triplet.split(";");

	auto r = parse!float(content[0]);
        auto g = parse!float(content[1]);
        auto b = parse!float(content[2]);

        return Color4f(r / 255.0f, g / 255.0f, b / 255.0f);
    }

    palette = (cast(string)(read(filename)))
                                        .splitLines
                                        .map!(a => extractField(a))
                                        .array;

    return palette;
}


auto calculateColorDistance(Color4f a, Color4f b)
{
	auto dx = a.r - b.r;
	auto dy = a.g - b.g;
	auto dz = a.b - b.b;

	return sqrt((dx ^^ 2) + (dy ^^ 2) + (dz ^^ 2));
}


auto nearestColorFromPalette(Color4f a, ref Color4f[] palette)
{
	import std.math : abs;

	float distance = float.max;
	Color4f r = palette[0];
	
	foreach (e; palette.sort!`a.luminance < b.luminance`)
	{
		auto d = calculateColorDistance(e, a);

		if (d <= distance)
		{
			r = e;
			distance = d;
		}
	}

	return r;
}

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

Сам алгоритм Флойда-Стейнберга можно выразить следующим псевдокодом:

for each y from top to bottom do
    for each x from left to right do
        oldpixel := pixels[x][y]
        newpixel := nearestColorFromPalette(oldpixel)
        pixels[x][y] := newpixel
        quant_error := oldpixel - newpixel
        pixels[x + 1][y    ] := pixels[x + 1][y    ] + quant_error × 7 / 16
        pixels[x - 1][y + 1] := pixels[x - 1][y + 1] + quant_error × 3 / 16
        pixels[x    ][y + 1] := pixels[x    ][y + 1] + quant_error × 5 / 16
        pixels[x + 1][y + 1] := pixels[x + 1][y + 1] + quant_error × 1 / 16

который можно буквально перевести на D следующим образом:

auto dithering(SuperImage simg, ref Color4f[] palette)
{
	foreach (x; 0..simg.width)
	{
		foreach (y; 0..simg.height)
		{
		   auto oldpixel = simg[x, y];
		   auto newpixel = nearestColorFromPalette(oldpixel, palette);
           
		   simg[x, y] = newpixel;
		   auto quant_error = oldpixel - newpixel;
		   simg[x + 1, y    ] = simg[x + 1, y    ] + quant_error * 7.0 / 16.0;
		   simg[x - 1, y + 1] = simg[x - 1, y + 1] + quant_error * 3.0 / 16.0;
		   simg[x    , y + 1] = simg[x    , y + 1] + quant_error * 5.0 / 16.0;
		   simg[x + 1, y + 1] = simg[x + 1, y + 1] + quant_error * 1.0 / 16.0;
		}
	}

	return simg;
}

И для испытания которого мы создадим палитру в стиле ZX-Spectrum и попробуем в качестве исходной картинки, взять изображение, созданное одним из авторов этого блога. Само изображение выглядит следующим образом:

Девиз D

Код для испытания:

void main()
{
	//auto palette = getPalette(`bone.csv`);

	/*
	auto palette = [
		Color4f(0.0f, 0.0f, 0.0f),
		Color4f(1.0f, 1.0f, 1.0f)
	];
	*/
	
	auto ZX_SPECTRUM_PALETTE = 
	[
		Color4f(0.0f, 0.0f, 0.0f),
		Color4f(0.0f, 0.0f, 238.0f),
		Color4f(238.0 / 255.0f, 0.0f, 0.0f),
		Color4f(238.0 / 255.0f, 0.0f, 238.0 / 255.0f),
		Color4f(0.0f, 238.0 / 255.0f, 0.0f),
		Color4f(0.0f, 238.0 / 255.0f, 238.0 / 255.0f),
		Color4f(238.0 / 255.0f, 238.0 / 255.0f, 0.0f),
		Color4f(238.0 / 255.0f, 238.0 / 255.0f, 238.0 / 255.0f),
		Color4f(0.0f, 0.0f, 0.0f),
		Color4f(0.0f, 0.0f , 255.0 / 255.0f),
		Color4f(255.0 / 255.0f, 0.0, 0.0),
		Color4f(255.0 / 255.0f, 0.0f, 255.0 / 255.0f),
		Color4f(0.0f, 255.0 / 255.0f, 0.0f),
		Color4f(0.0f, 255.0 / 255.0f, 255.0 / 255.0f),
		Color4f(255.0 / 255.0f, 255.0 / 255.0f, 0.0f),
		Color4f(255.0 / 255.0f, 255.0 / 255.0f, 255.0 / 255.0f)
	];

	alias palette = ZX_SPECTRUM_PALETTE;
	
	auto img = loadImage(`dlang.jpg`);
	img.dithering(palette);
	img.saveImage(`dlang_dither.png`);
}

Результат тестирования с палитрой от ZX Spectrum 48/128 (16 цветов):

Изображение после дизеринга

Код очень простой и может быть использован не только с той палитрой, которую мы задали в примере для тестирования, но и с любой другой, которая представлена в форме массива (не обязательно упорядоченного) цветов типа Color4f. Также не обязательно в алгоритме использовать именно наш подход с поиском ближайшего цвета на основе кратчайшего расстояния между цветом из палитры и текущим, можно использовать и иные варианты — здесь выбор остается за вами.

Напоследок, мы оставляем ряд ссылок, которые использовались при подготовке данной статьи:

Полный код dub-скрипта из статьи:

#!/usr/bin/env dub
/+ dub.sdl:
	dependency "dlib" version="~>1.0.0"
+/
import std.algorithm;
import std.conv;
import std.math;
import std.range;
import std.stdio;
import std.string;
import std.file;

import dlib.image;

Color4f[] getPalette(string filename)
{
    Color4f[] palette;

    auto extractField(string triplet)
    {
        auto content = triplet.split(";");

	auto r = parse!float(content[0]);
        auto g = parse!float(content[1]);
        auto b = parse!float(content[2]);

        return Color4f(r / 255.0f, g / 255.0f, b / 255.0f);
    }

    palette = (cast(string)(read(filename)))
                                        .splitLines
                                        .map!(a => extractField(a))
                                        .array;

    return palette;
}


auto calculateColorDistance(Color4f a, Color4f b)
{
	auto dx = a.r - b.r;
	auto dy = a.g - b.g;
	auto dz = a.b - b.b;

	return sqrt((dx ^^ 2) + (dy ^^ 2) + (dz ^^ 2));
}


auto nearestColorFromPalette(Color4f a, ref Color4f[] palette)
{
	import std.math : abs;

	float distance = float.max;
	Color4f r = palette[0];
	
	foreach (e; palette.sort!`a.luminance < b.luminance`)
	{
		auto d = calculateColorDistance(e, a);

		if (d <= distance)
		{
			r = e;
			distance = d;
		}
	}

	return r;
}


auto dithering(SuperImage simg, ref Color4f[] palette)
{
	foreach (x; 0..simg.width)
	{
		foreach (y; 0..simg.height)
		{
		   auto oldpixel = simg[x, y];
		   auto newpixel = nearestColorFromPalette(oldpixel, palette);
           
		   simg[x, y] = newpixel;
		   auto quant_error = oldpixel - newpixel;
		   simg[x + 1, y    ] = simg[x + 1, y    ] + quant_error * 7.0 / 16.0;
		   simg[x - 1, y + 1] = simg[x - 1, y + 1] + quant_error * 3.0 / 16.0;
		   simg[x    , y + 1] = simg[x    , y + 1] + quant_error * 5.0 / 16.0;
		   simg[x + 1, y + 1] = simg[x + 1, y + 1] + quant_error * 1.0 / 16.0;
		}
	}

	return simg;
}

void main()
{
	//auto palette = getPalette(`bone.csv`);

	/*
	auto palette = [
		Color4f(0.0f, 0.0f, 0.0f),
		Color4f(1.0f, 1.0f, 1.0f)
	];
	*/
	
	auto ZX_SPECTRUM_PALETTE = 
	[
		Color4f(0.0f, 0.0f, 0.0f),
		Color4f(0.0f, 0.0f, 238.0f),
		Color4f(238.0 / 255.0f, 0.0f, 0.0f),
		Color4f(238.0 / 255.0f, 0.0f, 238.0 / 255.0f),
		Color4f(0.0f, 238.0 / 255.0f, 0.0f),
		Color4f(0.0f, 238.0 / 255.0f, 238.0 / 255.0f),
		Color4f(238.0 / 255.0f, 238.0 / 255.0f, 0.0f),
		Color4f(238.0 / 255.0f, 238.0 / 255.0f, 238.0 / 255.0f),
		Color4f(0.0f, 0.0f, 0.0f),
		Color4f(0.0f, 0.0f , 255.0 / 255.0f),
		Color4f(255.0 / 255.0f, 0.0, 0.0),
		Color4f(255.0 / 255.0f, 0.0f, 255.0 / 255.0f),
		Color4f(0.0f, 255.0 / 255.0f, 0.0f),
		Color4f(0.0f, 255.0 / 255.0f, 255.0 / 255.0f),
		Color4f(255.0 / 255.0f, 255.0 / 255.0f, 0.0f),
		Color4f(255.0 / 255.0f, 255.0 / 255.0f, 255.0 / 255.0f)
	];

	alias palette = ZX_SPECTRUM_PALETTE;
	
	auto img = loadImage(`dlang.jpg`);
	img.dithering(palette);
	img.saveImage(`dlang_dither.png`);
}

P.S: На самом деле какое-то подобие дизеринга мы уже делали вот в этой статье, если интересно, можете с ней ознакомится.

Добавить комментарий