Преобразования цвета в DFL

В одном из проектов для D Form Library я столкнулся с небольшой проблемкой: необходимо было сделать рисунок в окне, но с использованием окраски «в старом стиле» (т.е. использовать не палитру RGB, а иную, более упрощенную, с меньшим количеством параметров). Это была даже не задача, поскольку думать пришлось совсем немного, однако, недостаточное количество информации чуть не погубило задумку проекта – требовалось нарисовать двумерное изображение с использованием палитры в 256 цветов (она же, если не ошибаюсь, BGR), но DFL позволяет рисовать в окне с использованием палитры RGB и описания механизма преобразования RGB в BGR почти отсутствуют…

Итак, первое решение задачи, приходящее в голову, это имея представления о том, как выглядят цвета из 256-цветной палитры, сделать так, чтобы некоторая функция, получая на вход число в диапазоне от 0 до 255, выдавала бы три числа, соответствующие компонентам цвета RGB – иными словами, легче всего построить практически буквальную функцию сопоставления цветов между двумя палитрами.

Однако, такое сопоставление – это весьма не вариант, по той причине, что придется писать очень много кода с фиксированными константами, которые перед этим еще надо как-то определить экспериментально (получается так, что нужные соответствия между цветами пришлось бы находить вручную, сопоставляя цвет RGB с цветом BGR) и потому, нужно искать другой способ преобразования цвета …

Как ни странно, но с решением подобной проблемы может помочь электроника.

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

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

Таким образом, интересующий нас фрагмент кода выглядит примерно так:

// 256 color to RGB
auto Color256(int color)
{
	ubyte red, green, blue;
	if (color <= 255)
	{
		red = cast(ubyte) (255 - color);
		green = cast(ubyte) color;
		blue = 0;
	}
	else if (color <= 551)
	{
		red = 0;
		green = 255 - cast(ubyte) (color - 256);
		blue = cast(ubyte) (color - 256);
	}
	else 
	{
		red = cast(ubyte) (color - 512);
		green = 0;
		blue = 255 - cast(ubyte) (color - 512);
	}
	return [red, green, blue];
}

Честно говоря, я не знаю, за счет чего работает этот код, но он работает (идея была нагло выдернута из кода для получения разных цветов на RGB-светодиоде, подключенного к Arduino. К сожалению, идея была подчерпнута довольно давно и у меня не осталось ссылки на статью с объяснением работы этого кода, но я думаю, подобную информацию можно достаточно легко найти, используя поисковый запрос: “RGB-светодиод Arduino”)!

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

import dfl.all;

// 256 color to RGB
auto Color256(int color)
{
	ubyte red, green, blue;
	if (color <= 255)
	{
		red = cast(ubyte) (255 - color);
		green = cast(ubyte) color;
		blue = 0;
	}
	else if (color <= 551)
	{
		red = 0;
		green = 255 - cast(ubyte) (color - 256);
		blue = cast(ubyte) (color - 256);
	}
	else 
	{
		red = cast(ubyte) (color - 512);
		green = 0;
		blue = 255 - cast(ubyte) (color - 512);
	}
	return [red, green, blue];
}


class RGBTest: dfl.form.Form
{
	this()
	{
		initializeRGBTest();
		
		//@  Other RGBTest initialization code here.
		
	}
	
	
	private void initializeRGBTest()
	{
		// Do not manually modify this function.
		//~Entice Designer 0.8.5.02 code begins here.
		//~DFL Form
		text = "RGBTest";
		clientSize = dfl.all.Size(904, 772);
		//~Entice Designer 0.8.5.02 code ends here.
	}
	
	protected override void onPaint(PaintEventArgs ea)
	{
		super.onPaint(ea);
		for (int i = 0; i < 255; i++)
		{
			auto colors = Color256(i);
			Pen p = new Pen(Color(colors[0], colors[1], colors[2]), 5);
			auto X = cast(int) (4.7 * i);
			auto Y = cast(int) (i + 1);
			ea.graphics.drawLine(p, X, 500, X, 500 - Y);
		}
	}
}


int main()
{
	int result = 0;
	
	try
	{
		Application.enableVisualStyles();
		Application.run(new RGBTest);
	}
	catch(DflThrowable o)
	{
		msgBox(o.toString(), "Fatal Error", MsgBoxButtons.OK, MsgBoxIcon.ERROR);
		
		result = 1;
	}
	
	return result;
}

В итоге, получаем вот такую картинку, по которой сразу становятся понятными все проблемы такого кода:

rgb_test1

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

Размышляя как тут быть, я нашел в интернете (по-моему, даже в Википедии) упоминание того факта, что палитра 256-цветов в некоторых компьютерах задавалась в байтовом представлении, т.е. цвет кодировался одним байтом данных (ну, логичный шаг: в одном байте как раз помещается число из диапазона 0 … 255), а общий формат цвета в таком представлении выглядит так: RRRGGGBB (соответственно, RRR – биты отвечающие за красный цвет; GGG – биты, отвечающие за зеленый цвет; BB – биты, отвечающие за синий цвет) – и именно этот факт, мы и будем использовать.

Для того, чтобы узнать каким числом представлен каждый цвет, необходимо использовать битовые маски для выделения соответствующих битов: 0b11100000 для извлечения компоненты красного цвета, 0b00011100 для извлечения компоненты зеленого цвета и 0b00000011 для извлечения компоненты синего цвета. Но после выделения компонент с помощью масок, мы столкнемся со следующей проблемой: красный цвет будет представлен числом в диапазоне от 224 до 255, зеленый будет представлен числом в диапазоне от 0 до 56, а синий – числом в диапазоне от 0 до 3, и эти диапазоны нужно представить в таком же виде, в каком используются компоненты RGB, т.е. фактически необходимо каждый из трех диапазонов привести к диапазону [0, 255] или же отобразить каждое из чисел из этих диапазонов в число из диапазона [0, 255]. После отображения числа, представляющего компоненту цвета BGR, в диапазон RGB задача уже довольно тривиальна: подставляем полученные компоненты в объект, представляющий собой цвет RGB и используем его по мере необходимости.

Для преобразования числа x из диапазона [inMin, inMax] в некоторое число, находящееся в диапазоне [outMin, outMax] можно применить вот такую функцию:

import std.range: isIntegral; 

T transform(T)(T x,, T inMax, T outMin, T outMax) if (isIntegral!T)
{
    return (x - inMin) * (outMax - outMin) / (inMax - inMin) + inMin;
}

которая представляет собой простую линейную трансформацию, которая также была позаимствована из электроники (в языке программирования Wiring, на котором программируют Arduino, есть такая функция map – именно ее реализация из описания языка платформы и была столь нагло использована).

Из определения функции видно, что она универсальная – срабатывает для любого типа данных, но чтобы предотвратить срабатывание на нечисловые типы данных, использован шаблон isIntegral из std.range – использование этого шаблона в условии описания функции (так называемое ограничение сигнатуры) гарантирует правильное и прозрачное ее применение.

Используя transform и высказанные выше предположения получаем новую функцию для генерации палитры из 256 цветов:

ubyte[] Palette256(ubyte color)
{
    ubyte b = cast(ubyte) transform(color & 0b00000011, 0, 3, 0, 255);
    ubyte g = cast(ubyte) transform(color & 0b00011100, 0, 56, 0, 255);
    ubyte r = cast(ubyte) transform(color & 0b11100000, 0, 224, 0, 255);
    return [r, g, b];
}

которая с учетом использования того же самого «треугольника» (заменяем Color256 на Palette256), что и в предыдущем испытании (только, тип переменной счетчика нужно сменить на ubyte, разумеется), выглядит так:

rgb_test2

Красиво не правда ли?! Задача выполнена и плюс к тому, мы видим несколько уровней яркости на палитре, и при этом присутствуют все спектральные цвета (правда, в разных соотношениях).

А теперь можно (ради интереса) провести ряд простых экспериментов с преобразованиями цвета.

Возьмем, к примеру, в строке кода:

Pen p = new Pen(Color(colors[0], colors[1], colors[2]), 5);

заменим Color(colors[0], colors[1], colors[2]) на Color(255 — colors[0], 255 — colors[1], 255 — colors[2]) (иными словами, вычтем каждый компонент цвета из 255) и в результате получим весьма интересную картинку:

rgb_test3получилось более яркое и более сочное изображение.

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

Если вы достаточно много проработали с графикой в DFL, то вы бы заметили, что параметры, которые передаются в структуру Color имеют тип ubyte, что дает нам ряд идей для следующего эксперимента с цветом: раз это байтовый тип (точнее, производный от него), то к нему могут применяться различные битовые операции. Битовые операции с масками не дают особо интересных результатов, но вот операции сдвига дают занятные картинки, например, возьмем первое изображение с правильной 256-цветной палитрой и сдвинем каждую компоненту на один разряд вправо (т.е. просто заменим Color(colors[0], colors[1], colors[2]) на Color(colors[0] >> 1, colors[1] >> 1, colors[2] >> 1)) и в итоге получим вот такую прелесть:

rgb_test4

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

Заменив Color(colors[0] 1, colors[1] , colors[2] ) на Color(cast(ubyte) (colors[0] << 2), cast(ubyte) (colors[1] << 2), cast(ubyte) (colors[2] << 2)) (преобразование типа необходимо для получения корректного параметра, иначе результат сдвига будет типа int, что приведет к ошибке в конструкторе структуры Color) получим следующую картинку:

rgb_test5

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

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

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

ubyte[] PaletteGray1(ubyte color)
{
    ubyte[] rgb = Palette256(color);
    double gray = 0.212 * rgb[0] + 0.7512 * rgb[1] + 0.0722 * rgb[2];
    ubyte f = ((cast(int) gray) &gt; 255) ? 255 : cast(ubyte) gray;
    return [f, f, f];
}

Для такой конверсии палитры была использована одна из известных формул преобразования яркости (ничто не запрещает вам использовать это и для нормальной палитры) и не представляет собой нечто особенное, хотя и дает специфический результат (заменяем Color256(i) на PaletteGrey1(i)):

rgb_test6

Однако, помимо этой палитры я создал еще две палитры оттенков серого (ага, вот так должны выглядеть 50 оттенков серого).

Код второй палитры:

ubyte[] PaletteGray2(ubyte color)
{
    ubyte[] rgb = Palette256(color);
    double gray = 0.3 * rgb[0] + 0.59 * rgb[1] + 0.11 * rgb[2];
    ubyte f = ((cast(int) gray) &gt; 255) ? 255 : cast(ubyte) gray;
    return [f, f, f];
}

Результат применения:

rgb_test7

Код третьей (самой простой) палитры оттенков серого:

ubyte[] PaletteGray3(ubyte color)
{
    ubyte[] rgb = Palette256(color);
    ubyte f = (rgb[0] + rgb[1] + rgb[2]) / 3;
    return [f, f, f];
}

И результат применения:

rgb_test8

Разумеется, это не все преобразования (и тем более не все интересные), однако, я считаю что полезно знать эти преобразования и уметь их применять (спектр применения огромен: создание изображений, градиентные окраски, создание градаций, объемное заполнение, трансформация изображение и многое другое), в том числе, и не по стандарту применять (ведь, есть способы отображения большего количества цветом на мониторах с меньшим, забыл как называется этот способ – но это нестандартно и оригинально, хотя это простое и банальное цветовое преобразование). Кроме того, я думаю, что это не последние наши опыты с цветовыми преобразованиями (надеюсь, у Bagomot’а более стохастические методы проверки представленного кода, ну и более занятные результаты…) и возможно, на эту тему будет еще несколько статей.

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

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