Обработка изображений: цифровые фильтры

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

Первое, что мы делаем, это создаем новый dub-проект вот такой командой:

dub init digital_filters –f sdl

После этого, в папку digital_filters перетаскиваем все содержимое папки QtE5/windows32 (предполагается, что релиз-версия QtE5 у вас уже скачана и распакована в папку QtE5). Далее копируем в папку digital_filters/source (там где лежит весь исходный код будущей программы) файл qte5.d из папки QtE5. Все описанные действия являются лишь предварительной подготовкой, которая позволит нам с помощью всего лишь одной команды из командной строки Windows собрать и/или запустить приложение.

Следующим действием будет добавление новой зависимости в файл dub.sdl, который лежит в папке проекта, а зависимость, которую мы туда добавляем следующее:

dependency «dlib» version=»~master»

Теперь создадим простую форму на QtE5, например, вот такую:

Будем предполагать, что там же где будет находиться наше приложение, находиться папка filters. В этой папке мы будем хранить текстовые описания сверточных фильтров (об этом чуть позже), а также на основании файлов находящихся в filters, будет строиться список, знакомых программе фильтров. Также, список фильтров будет не чем иным, как QComboBox, что позволит построить список фильтров вот так:

dirEntries(`filters`, SpanMode.shallow)
			.filter!(a => a.isFile && a.extension == ".filter")
			.enumerate(1)
			.each!(a => combo0.addItem(a[1], a[0]));

Эта команда перечисляет все содержимое папки filters, затем профильтровывает из них только те, которые являются файлами с расширением .filter (а это и есть наши файлы с описанием фильтров), а затем нумерует их, помещая их в внутрь QComboBox.

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

Назовем, этот виджет QPictureBox и разместим в файле picturebox.d:

module digital_filters.picturebox;

private
{
	import std.file;

	import qte5;
}

extern (C)
{
	void onPaintImage(QPictureBox* pictureBox, void* eventPointer, void* painterPointer) 
	{ 
		(*pictureBox).runPaintImage(eventPointer, painterPointer);
	}
}

class QPictureBox : QWidget
{
	private
	{
		QWidget parent;
		QImage image;
		string filename;
	}

	this(QWidget parent)
	{
		super(this);
		this.parent = parent;
		this.image = new QImage(256, 256, QImage.Format.Format_ARGB32_Premultiplied);
		setPaintEvent(&onPaintImage, aThis);
	}

	void setFileName(string filename)
	{
		this.filename = filename;
		
		if (filename.exists)
		{
			image.load(filename);
		}
		else
		{
			image.fill(QtE.GlobalColor.white);
		}
	}

	void runPaintImage(void* eventPointer, void* painterPointer)
	{
		QPainter painter;

		with (painter = new QPainter('+', painterPointer))
		{
			drawImage(contentsRect(new QRect), image);
			end;
		}
	}
}

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

extern(C)
{
	void onLoadFile(MainForm* mainFormPointer) 
	{
		(*mainFormPointer).runLoadFile;
	}

	void onApplyFilters(MainForm* mainFormPointer) 
	{
		(*mainFormPointer).runApplyFilters;
	}
}

которые обеспечивают загрузку картинки и применение к ней выбранного пользователем фильтра.

Основное окно описывается с помощью простого и ясного кода размещения элементов, а также дополнительных процедур. Итак, размещение элементов в форме, которое с обработчиками мы помещаем в gui.d:

private
{
	import std.algorithm;
	import std.file;
	import std.path;
	import std.range;
	import std.stdio;
	import std.string;

	import dlib.image;
	import qte5;

	import digital_filters.picturebox;
	import digital_filters.transformations;
}

// псевдонимы под Qt'шные типы
alias WindowType = QtE.WindowType;


class MainForm : QWidget
{
	private
	{
		QVBoxLayout mainBox, vertical0, vertical1;
		QHBoxLayout horizontal0, horizontal1;
		QGroupBox box0, box1, box2;
		QPictureBox area0, area1;
		QLineEdit text0;
		QComboBox combo0;
		QPushButton button0, button1;
		QAction action0, action1;
	}

	this(QWidget parent, WindowType windowType) 
	{
		super(parent, windowType); 
		resize(700, 700); 
		setWindowTitle("DigitalFilters");
		setStyleSheet("background : white");

		mainBox = new QVBoxLayout(this);

		vertical0 = new QVBoxLayout(this);
		area0 = new QPictureBox(this);
		area0.setFileName("");
		area0.saveThis(&area0);
		vertical0.addWidget(area0);

		box0 = new QGroupBox(this);
		box0.setText("Before:");
		box0.setLayout(vertical0);

		vertical1 = new QVBoxLayout(this);
		area1 = new QPictureBox(this);
		area1.setFileName("");
		area1.saveThis(&area1);
		vertical1.addWidget(area1);

		box1 = new QGroupBox(this);
		box1.setText("After:");
		box1.setLayout(vertical1);

		horizontal0 = new QHBoxLayout(this);
		horizontal0
					.addWidget(box0)
					.addWidget(box1);

		horizontal1 = new QHBoxLayout(this);
		text0 = new QLineEdit(this);
		text0.setReadOnly(true);
		combo0 = new QComboBox(this);

		dirEntries(`filters`, SpanMode.shallow)
				.filter!(a => a.isFile && a.extension == ".filter")
				.enumerate(1)
				.each!(a => combo0.addItem(a[1], a[0]));

		button0 = new QPushButton("Select...", this);
		button1 = new QPushButton("Apply", this);
		
		action0 = new QAction(null, &onLoadFile, aThis);
		action1 = new QAction(null, &onApplyFilters, aThis);
		connects(button0, "clicked()", action0, "Slot()");
		connects(button1, "clicked()", action1, "Slot()");

		horizontal1
				.addWidget(text0)
				.addWidget(combo0)
				.addWidget(button0)
				.addWidget(button1);

		box2 = new QGroupBox(this);
		box2.setText("Image for filtering:");
		box2.setLayout(horizontal1);
		box2.setMaximumHeight(55);

		mainBox
			.addLayout(horizontal0)
			.addWidget(box2);

		setLayout(mainBox);
	}
}

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

По нажатию на кнопку «Select…» должен появиться диалог выбора файла изображения (для простоты ограничимся PNG-файлами), полный путь файла должен появиться в read-only текстовом поле, после чего происходит загрузка изображения в первый виджет QPictureBox:

void runLoadFile()
{
	QFileDialog fileDialog = new QFileDialog('+', null);
	string filename = fileDialog.getOpenFileNameSt("Open Image File", "", "*.png");

	area0.setFileName(filename);
	text0.setText(filename);
	text0.setReadOnly(true);
	area0.update;
}

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

void runApplyFilters()
{
	auto filterFileName = combo0.text!string;
	auto pictureFileName = text0.text!string;
	auto newPictureFile = pictureFileName.replace(
		".png", 
		"_" ~ filterFileName.replace(`filters\`, "") ~ ".png"
	);

	auto picture = load(pictureFileName);

	picture
		.convolution(readFilter(filterFileName))
		.savePNG(newPictureFile);

	area1.setFileName(newPictureFile);
	area1.update;
}

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

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

class DigitalFilter
{
	private
	{
		size_t width;
		size_t height;
		float divider;
		float offset;
		float[][] kernel;
	}

	final pure int getWidth() const @property
	{
		return width;
	}
	
	
	final pure int getHeight() const @property
	{
		return height;
	}
	
	
	final pure float opIndex(int i, int j) const @property
	{
		return offset + divider * kernel[i][j];
	}

	this(
		size_t width, 
		size_t height, 
		float[][] kernel, 
		float divider = 1.0f, 
		float offset = 0.0f
		)
	{
		this.width = width; 
		this.height = height; 
		this.kernel = kernel;
		this.divider = divider;
		this.offset = offset;
	}
}

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

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

filter = имя_фильтра
width = длина_окна_фильтра
height = ширина_окна_фильтра
divider = делитель
offset = смещение
kernel
коэффициенты ядра фильтра

Разбор и генерацию фильтра по описанию можно сделать так:

private
{
	import dlib.image;
}

DigitalFilter readFilter(string filename)
{
	import std.algorithm;
	import std.conv;
	import std.file;
	import std.range;
	import std.stdio;
	import std.string;

	
	float[][] kernel;

	auto content = (cast(string) std.file.read(filename)).splitLines;
	auto width = to!int(content[1].split("=")[1].strip);
	auto height = to!int(content[2].split("=")[1].strip);
	auto divider = to!float(content[3].split("=")[1].strip);
	auto offset = to!float(content[4].split("=")[1].strip);

	content[6..$]
			.map!(a => parse!float(a))
			.chunks(width)
			.each!(a => kernel ~= a.array);

	return new DigitalFilter(
		width, 
		height, 
		kernel, 
		divider, 
		offset
	);
}

Работает это просто: файл с описанием считывается в строку, которая разбивается по разделителю «новая строка», после чего происходит извлечение отдельных заполненных полей  (выбираем нужный элемент из массива строк, делим его по ключевому слову из описания, берем то, что справа от знака равенства, обрезаем пробелы с обоих сторон и приводим к нужному типу). Самое сложное – это разбор ядра, который осуществляется путем выделения остатка от массива строк файла (в конце файла – описание коэффициентов ядра), прогон каждого элемента через преобразование к типу float, разрезание на равные куски с количеством width элементов (chunks из std.range), которые превращаются в одномерные массивы и помещаются в двумерный массив ядра фильтра. После этого происходит создание объекта типа DigitalFilter фильтра.

Процедуру свертки нужно несколько переработать, исправив в ее сигнатуре тип объекта с Filter на DigitalFilter, при этом все остальное остается точно также, как и в статье про сверточный фильтр:

auto convolution(SuperImage source, DigitalFilter filter)
{
	int imageWidth = source.width;
	int imageHeight = source.height;
	int filterWidth = filter.getWidth;
	int filterHeight = filter.getHeight;
	
	
	auto convolutionResult = image(imageWidth, imageHeight);
	
	for (int i = 0; i < imageWidth; i++)
	{
		for (int j = 0; j < imageHeight; j++)
		{
			float redComponent = 0, greenComponent = 0, blueComponent = 0;
			
			for (int w = 0; w < filterWidth; w++)
			{
				for (int h = 0; h < filterHeight; h++)
				{
					auto fX = i + (w - (filterWidth / 2));
					auto fY = j + (h - (filterHeight / 2));
					
					if (((fX < 0) || (fX >= imageWidth) || (fY < 0) || (fY >= imageHeight)))
					{
						// do nothing
					}
					else
					{
						Color4f currentPixel = source[fX, fY];
						float filterValue = filter[w, h];
						
						redComponent += currentPixel.r * filterValue;
						greenComponent += currentPixel.g * filterValue;
						blueComponent += currentPixel.b * filterValue;
						
					}
				}
			}
			
			convolutionResult[i, j] = Color4f(redComponent, greenComponent, blueComponent);
		}
	}
	
	return convolutionResult;
}

Вносим последние описанные процедуры в файл transformations.d, после чего создаем файл app.d и вносим в него уже стандартную для QtE5 процедуру main и остальные необходимые описания:

module main;

import core.runtime;
import qte5;
import digital_filters.gui;

alias normalWindow = WindowType.Window;

int QtDebugInfo(bool flag)
{
	return LoadQt(dll.QtE5Widgets, flag);
}

int main(string[] args) 
{
	QtDebugInfo(true);
	
	QApplication app = new QApplication(&Runtime.cArgs.argc, Runtime.cArgs.argv, 1);
	MainForm mainForm = new MainForm(null, normalWindow);
	
	mainForm.saveThis(&mainForm);	
	mainForm.show;
	
	return app.exec;
}

От испытаний программы нас отделяет несколько файлов, а именно, файлов описания фильтров, и сейчас, я покажу вам несколько примеров.

Оператор Лапласа можно описать таким файлом:

filter = Laplace 3×3
width = 3
height = 3
divider = 1.0
offset = 0
kernel
0
1
0
1
-4
1
0
1
0

Этот фильтр представляет собой вторую производную (дискретную производную, да простят меня математики) от яркости изображения, и его применение на стандартной картинке «пингвины» из комплекта изображений Windows выглядит так:

Laplace 3x3

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

filter = Sobel X
width = 3
height = 3
divider = 1.0
offset = 0
kernel
-1
0
1
-2
0
2
-1
0
1

filter = Sobel Y
width = 3
height = 3
divider = 1.0
offset = 0
kernel
-1
-2
-1
0
0
0
1
2
1

А результат применения на «пингвинах» будет следующий:

Sobel X
Sobel X

Sobel Y
Sobel Y

А вот одно из полевых испытаний программки с ядром Щарра по иксу:

Чтобы проверить программу, переходим в командной строке в папку программы и запускаем ее с помощью dub:

dub run

На этом все. Спасибо за внимание!

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