Экспериментируем с битовыми плоскостями. Часть I

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

Итак, давайте вспомним основы: (если говорить упрощенно) изображение — это некоторый файл, который содержит в себе информацию о произвольном количестве пикселей, отображаемых на какое-то устройство (экран компьютера, телевизор, принтер и т.д.). Как правило, данная информация представляет собой набор цифровых характеристик, которые описывают яркость каждого пикселя. Этот факт также позволяет предположить (а для кого и вспомнить), что у значений яркости существуют ограничения, связанные как с устройством нашего зрения, так и с устройством нашей техники. Обычно, значения яркости представлены как некие яркостные уровни, т.е. числовые значения, которые находятся в диапазоне от 0 до 2 в n-ой степени. Машинное представление данных чисел — это некий элементарный тип числовых данных, для простоты будем считать, что этот тип —  беззнаковое целое в диапазоне от 0 до 255.

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

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

enum CHANNEL : byte
{
	RED,
	GREEN,
	BLUE
}

auto getChannel(SuperImage superImage, CHANNEL channel)
{
	SuperImage newImage = image(superImage.width, superImage.height);

	foreach (x; 0..superImage.width)
	{
		foreach (y; 0..superImage.height)
		{
			auto color = superImage[x,y];

			final switch (channel) with (CHANNEL)
			{
				case RED:
					newImage[x, y] = Color4f(color.r, 0.0f, 0.0f);
					break;
				case GREEN:
					newImage[x, y] = Color4f(0.0f, color.g, 0.0f);
					break;
				case BLUE:
					newImage[x, y] = Color4f(0.0f, 0.0f, color.b);
					break;
			}
		}
	}

	return newImage;
}

Здесь мы используем уже знакомую нам библиотеку dlib, которая предоставляет все удобные типы и операции для работы с изображениями. Кроме того, используется идиоматическая конструкция final switch … with …, которая позволяет более наглядно представить необходимое сопоставление с использованием перечислений. Думаю, данный код не требует пояснений относительно его работы.

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

void main()
{
	auto img = load(`/home/invertyne/D/planes/Lenna.png`);
	img.getChannel(CHANNEL.RED).savePNG(`Lenna_red.png`);
}

Результат испытаний:

Объединим описанные концепции дискретных уровней яркости и понятия каналов: если мы возьмем некоторое изображение, а затем выделим из него некоторый из каналов, сохранив его как отдельное изображение, то мы сможем провести над полученным изображением одну интересную операцию. Операция будет простой: для каждого пикселя изображения возьмем соответствующий ему уровень яркости (напоминаю, это беззнаковое число) и представим его как набор битов, после чего выделим бит под неким номером N (N не меняется в пределах одного изображения). Если N-ый бит равен 1, то будем считать, что в новом изображении на месте взятого пикселя из исходного изображения, будет стоять белая точка; если иначе, то — черная.

В ходе применения нашей операции мы получим черно-белое изображение, которое называется битовой плоскостью (bit plane) или битовой картой (bit map), а сама операция называется извлечением битовой плоскости.

Выделение плоскости можно реализовать с помощью выделения канала и применения побитовых операций, например, вот так:

auto getBitPlane(SuperImage superImage, CHANNEL channel, int numberOfBit)
{
	ubyte getNthBit(float value, int numberOfBit)
	{
		ubyte mask = 0b00000001;
		ubyte nvalue = cast(ubyte) (value * 255);
		return (nvalue & (mask << numberOfBit)) >> numberOfBit;
	}

	SuperImage newImage = superImage.getChannel(channel);

	foreach (x; 0..newImage.width)
	{
		foreach (y; 0..newImage.height)
		{
			ubyte bp;

			final switch (channel) with (CHANNEL)
			{
				case RED:
					bp = getNthBit(newImage[x, y].r, numberOfBit);
					break;
				case GREEN:
					bp = getNthBit(newImage[x, y].g, numberOfBit);
					break;
				case BLUE:
					bp = getNthBit(newImage[x, y].b, numberOfBit);
					break;
			}

			if (bp)
			{
				newImage[x, y] = Color4f(1.0f, 1.0f, 1.0f);
			}
			else
			{
				newImage[x, y] = Color4f(0.0f, 0.0f, 0.0f);
			}
		}
	}

	return newImage;
}

Работает это так: определяем вспомогательную функцию для выделения конкретного бита из представления цвета Color4f из dlib, которая использует лишь один компонент данного типа (напоминаем, что Color4f — это вектор из 4-х float) умноженный на 255 и приведенный к типу ubyte. Далее, после определения необходимой функции, выделим интересующий нас цветовой канал и сохраним его в отдельную переменную, которая после окончания работы функции getBitPlane будет содержать в себе битовую плоскость. Используем теперь полученный канал как изображение для анализа и пройдем по всем его пикселям, осуществляя прямо на месте выделение необходимого бита и замену его на черный или белый пиксель.

Проведем испытание кода:

void main()
{
	auto img = load(`/home/invertyne/D/planes/Lenna.png`);
	img.getBitPlane(CHANNEL.RED, 2).savePNG(`Lenna_red2.png`);
}

Вот так это выглядит:

Полный код проекта:

import std.stdio;
import dlib.image;

enum CHANNEL : byte
{
	RED,
	GREEN,
	BLUE
}

auto getChannel(SuperImage superImage, CHANNEL channel)
{
	SuperImage newImage = image(superImage.width, superImage.height);

	foreach (x; 0..superImage.width)
	{
		foreach (y; 0..superImage.height)
		{
			auto color = superImage[x,y];

			final switch (channel) with (CHANNEL)
			{
				case RED:
					newImage[x, y] = Color4f(color.r, 0.0f, 0.0f);
					break;
				case GREEN:
					newImage[x, y] = Color4f(0.0f, color.g, 0.0f);
					break;
				case BLUE:
					newImage[x, y] = Color4f(0.0f, 0.0f, color.b);
					break;
			}
		}
	}

	return newImage;
}

auto getBitPlane(SuperImage superImage, CHANNEL channel, int numberOfBit)
{
	ubyte getNthBit(float value, int numberOfBit)
	{
		ubyte mask = 0b00000001;
		ubyte nvalue = cast(ubyte) (value * 255);
		return (nvalue & (mask << numberOfBit)) >> numberOfBit;
	}

	SuperImage newImage = superImage.getChannel(channel);

	foreach (x; 0..newImage.width)
	{
		foreach (y; 0..newImage.height)
		{
			ubyte bp;

			final switch (channel) with (CHANNEL)
			{
				case RED:
					bp = getNthBit(newImage[x, y].r, numberOfBit);
					break;
				case GREEN:
					bp = getNthBit(newImage[x, y].g, numberOfBit);
					break;
				case BLUE:
					bp = getNthBit(newImage[x, y].b, numberOfBit);
					break;
			}

			if (bp)
			{
				newImage[x, y] = Color4f(1.0f, 1.0f, 1.0f);
			}
			else
			{
				newImage[x, y] = Color4f(0.0f, 0.0f, 0.0f);
			}
		}
	}

	return newImage;
}

void main()
{
	auto img = load(`/home/invertyne/D/planes/Lenna.png`);

	img.getChannel(CHANNEL.RED).savePNG(`Lenna_red.png`);
	img.getBitPlane(CHANNEL.RED, 2).savePNG(`Lenna_red2.png`);
}

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

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

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