Вставка сообщений в изображения

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

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

И тут встал интересный вопрос: как доказать всем (в том числе, и судебным работникам) свое авторство или некоторое участие в работе относительно некоторого произведения?

Давайте предположим, что самое лучшее доказательство авторства для изображения, это само данное изображение. Но, стиль, использованный в создании изображения, не является объективным критерием, а множество действительно первоклассных художников умеют неплохо подстраиваться под практически любой стиль, с легкостью его копируя. Также, множество цифровых художников очень любят оставлять в своих работах разного рода скрытые сообщения, увидеть которые могут лишь «избранные» или особо сообразительные почитатели цифрового таланта…

Так вот почему бы не сделать скрытые сообщения более привычными и более доступными для обычных людей?

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

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

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

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

// спрятать байт в цвете
Color4f[] setWith(Color4f color1, Color4f color2, ubyte unit)
{
	int R1 = (cast(int) (color1.r * 255));
	int G1 = (cast(int) (color1.g * 255));

	int R2 = (cast(int) (color2.r * 255));
	int G2 = (cast(int) (color2.g * 255));

	// first half of byte
	auto r1 = (R1 & 0b11111100) | (unit & 0b00000011);
	auto g1 = (G1 & 0b11111100) | ((unit & 0b00001100) >> 2);

    // second half of byte
    auto r2 = (R2 & 0b11111100) | ((unit & 0b00110000) >> 4);
    auto g2 = (G2 & 0b11111100) | ((unit & 0b11000000) >> 6);
	

	return [
				Color4f(r1 / 255.0, g1 / 255.0, color1.b),
				Color4f(r2 / 255.0, g2 / 255.0, color2.b),
		   ];
}

Извлечение скрытой информации из пикселей также несложно реализовать, просто нужно исходить из тех же положений, что и при сокрытии, но нужно извлечь двойки битов, совместив их в один байт:

// извлечь байт из цвета
ubyte getWith(Color4f[] colors)
{
	ubyte result = 0;
	
	int x0 = (cast(int) (colors[0].r * 255));
	int x1 = (cast(int) (colors[0].g * 255));
	int x2 = (cast(int) (colors[1].r * 255));
	int x3 = (cast(int) (colors[1].g * 255));

	auto r = x0 & 0b00000011;
	auto g = x1 & 0b00000011;
	auto b = x2 & 0b00000011;
	auto a = x3 & 0b00000011;

	result |= r;
	result |= (g << 2);
	result |= (b << 4);
	result |= (a << 6);

	return result;
}

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

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

// добавить сообщение в изображение
auto addMessage(SuperImage superImage, ubyte[] message)
{
	auto width = superImage.width;
	auto height = superImage.height;
	SuperImage newImage = superImage.dup;

	if ((message.length * 2) > (width * height))
	{
		throw new Exception("Unable to add message: message length less than number of pixels");
	}

	for (int i = 0; i < message.length; i++)
	{
		auto N = i * 2;

		auto x0 = N / width;
		auto y0 = N % height;

		auto x1 = (N + 1) / width;
		auto y1 = (N + 1) % height;

		auto colors = setWith(superImage[x0, y0], superImage[x1,y1], message[i]);

		newImage[x0, y0] = colors[0];
		newImage[x1, y1] = colors[1];  
	}

	return newImage;
}

Извлечение сообщения будем производить так:

// извлечь сообщение
auto extractMessage(SuperImage superImage)
{
	auto width = superImage.width;
	auto height = superImage.height;

	ubyte[] message;

	for (int i = 0; i < ((width * height) / 2); i++)
	{
		auto N = i * 2;

		auto x0 = N / width;
		auto y0 = N % height;

		auto x1 = (N + 1) / width;
		auto y1 = (N + 1) % height;

		message ~= getWith([superImage[x0, y0], superImage[x1,y1]]);
	}

	return message;
}

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

Однако, данные о длине сообщения, могут быть сохранены у авторы изображения, как часть некоего «ключа» сообщения, которая может быть дополнительным свидетельством авторства, хоть и такое свидетельство очень слабое.

Давайте возьмем какую-нибудь картинку и испытаем наши функции…

Исходная картинка «смеющийся гепард» (на самом деле, зевающий):

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

void main()
{
	import std.stdio;
	import std.string;

	auto img = load("gepard2.png");
	auto bytes = cast(ubyte[]) "Команда LightHouse Software".representation;
	
	img
		.addMessage(bytes)
		.savePNG("gepard_with_message.png");


	auto img2 = load("gepard_with_message.png");
	auto message = extractMessage(img2);
	writeln(cast(string) message[0..34]);
}

Результат:

Сообщение, извлеченное из гепарда:

Performing "debug" build using dmd for x86_64.
dlib 0.12.2: target for configuration "library" is up to date.
digital_sign ~master: building configuration "application"...
Linking...
To force a rebuild of up-to-date targets, run again with --force.
Running ./digital_sign 
Команда LightHouse Software

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

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

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

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