From 646c94908b2ebd35f5ac03938c1fef2251de36b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?= <d_muel20@uni-muenster.de>
Date: Mon, 13 Jan 2020 22:27:47 +0100
Subject: [PATCH] * Added plenty of properties to the Random Forest ML block

---
 Grinder/Grinder.pro                           |  17 +-
 Grinder/Version.h                             |   4 +-
 Grinder/common/properties/PropertyID.cpp      |   9 +
 Grinder/common/properties/PropertyID.h        |   9 +
 .../RandomForestClassifierConfiguration.cpp   |  63 +-
 .../rf/RandomForestClassifierConfiguration.h  |  47 ++
 Grinder/ml/rf/RandomForestFeatures.cpp        |  88 +++
 Grinder/ml/rf/RandomForestFeatures.h          | 221 ++++++
 .../rf/blocks/RandomForestClassifierBlock.cpp |  69 ++
 .../rf/blocks/RandomForestClassifierBlock.h   |  46 ++
 .../rf/properties/MaxFeaturesTypeProperty.cpp |  17 +
 .../rf/properties/MaxFeaturesTypeProperty.h   |  26 +
 .../RandomForestFeaturesProperty.cpp          |  30 +
 .../properties/RandomForestFeaturesProperty.h |  29 +
 Grinder/res/Grinder.qrc                       |  18 +
 Grinder/res/gabor/filter0.png                 | Bin 0 -> 1522 bytes
 Grinder/res/gabor/filter1.png                 | Bin 0 -> 862 bytes
 Grinder/res/gabor/filter10.png                | Bin 0 -> 1728 bytes
 Grinder/res/gabor/filter11.png                | Bin 0 -> 1179 bytes
 Grinder/res/gabor/filter12.png                | Bin 0 -> 1313 bytes
 Grinder/res/gabor/filter13.png                | Bin 0 -> 917 bytes
 Grinder/res/gabor/filter14.png                | Bin 0 -> 1572 bytes
 Grinder/res/gabor/filter15.png                | Bin 0 -> 1595 bytes
 Grinder/res/gabor/filter2.png                 | Bin 0 -> 1779 bytes
 Grinder/res/gabor/filter3.png                 | Bin 0 -> 1497 bytes
 Grinder/res/gabor/filter4.png                 | Bin 0 -> 1310 bytes
 Grinder/res/gabor/filter5.png                 | Bin 0 -> 921 bytes
 Grinder/res/gabor/filter6.png                 | Bin 0 -> 1565 bytes
 Grinder/res/gabor/filter7.png                 | Bin 0 -> 1596 bytes
 Grinder/res/gabor/filter8.png                 | Bin 0 -> 1165 bytes
 Grinder/res/gabor/filter9.png                 | Bin 0 -> 719 bytes
 .../rf/RandomForestFeaturesPropertyEditor.cpp |  22 +
 .../rf/RandomForestFeaturesPropertyEditor.h   |  26 +
 .../ui/ml/rf/RandomForestFeaturesDialog.cpp   | 216 ++++++
 Grinder/ui/ml/rf/RandomForestFeaturesDialog.h |  61 ++
 .../ui/ml/rf/RandomForestFeaturesDialog.ui    | 734 ++++++++++++++++++
 36 files changed, 1746 insertions(+), 6 deletions(-)
 create mode 100644 Grinder/ml/rf/RandomForestFeatures.cpp
 create mode 100644 Grinder/ml/rf/RandomForestFeatures.h
 create mode 100644 Grinder/ml/rf/properties/MaxFeaturesTypeProperty.cpp
 create mode 100644 Grinder/ml/rf/properties/MaxFeaturesTypeProperty.h
 create mode 100644 Grinder/ml/rf/properties/RandomForestFeaturesProperty.cpp
 create mode 100644 Grinder/ml/rf/properties/RandomForestFeaturesProperty.h
 create mode 100644 Grinder/res/gabor/filter0.png
 create mode 100644 Grinder/res/gabor/filter1.png
 create mode 100644 Grinder/res/gabor/filter10.png
 create mode 100644 Grinder/res/gabor/filter11.png
 create mode 100644 Grinder/res/gabor/filter12.png
 create mode 100644 Grinder/res/gabor/filter13.png
 create mode 100644 Grinder/res/gabor/filter14.png
 create mode 100644 Grinder/res/gabor/filter15.png
 create mode 100644 Grinder/res/gabor/filter2.png
 create mode 100644 Grinder/res/gabor/filter3.png
 create mode 100644 Grinder/res/gabor/filter4.png
 create mode 100644 Grinder/res/gabor/filter5.png
 create mode 100644 Grinder/res/gabor/filter6.png
 create mode 100644 Grinder/res/gabor/filter7.png
 create mode 100644 Grinder/res/gabor/filter8.png
 create mode 100644 Grinder/res/gabor/filter9.png
 create mode 100644 Grinder/ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.cpp
 create mode 100644 Grinder/ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.h
 create mode 100644 Grinder/ui/ml/rf/RandomForestFeaturesDialog.cpp
 create mode 100644 Grinder/ui/ml/rf/RandomForestFeaturesDialog.h
 create mode 100644 Grinder/ui/ml/rf/RandomForestFeaturesDialog.ui

diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro
index dc51a21..0c119b4 100644
--- a/Grinder/Grinder.pro
+++ b/Grinder/Grinder.pro
@@ -488,7 +488,12 @@ SOURCES += \
     ui/image/commands/CreateLayerUndoCommand.cpp \
     ui/image/commands/RemoveLayerUndoCommand.cpp \
     pipeline/blocks/MaskInputBlock.cpp \
-    engine/processors/MaskInputProcessor.cpp
+    engine/processors/MaskInputProcessor.cpp \
+    ml/rf/properties/MaxFeaturesTypeProperty.cpp \
+    ml/rf/RandomForestFeatures.cpp \
+    ml/rf/properties/RandomForestFeaturesProperty.cpp \
+    ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.cpp \
+    ui/ml/rf/RandomForestFeaturesDialog.cpp
 
 HEADERS += \
 	ui/mainwnd/GrinderWindow.h \
@@ -1070,7 +1075,12 @@ HEADERS += \
     ui/image/commands/RemoveLayerUndoCommand.h \
     util/MathUtils.impl.h \
     pipeline/blocks/MaskInputBlock.h \
-    engine/processors/MaskInputProcessor.h
+    engine/processors/MaskInputProcessor.h \
+    ml/rf/properties/MaxFeaturesTypeProperty.h \
+    ml/rf/RandomForestFeatures.h \
+    ml/rf/properties/RandomForestFeaturesProperty.h \
+    ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.h \
+    ui/ml/rf/RandomForestFeaturesDialog.h
 
 FORMS += \
 	ui/mainwnd/GrinderWindow.ui \
@@ -1090,7 +1100,8 @@ FORMS += \
 	ui/cmd/tasks/CommandInterfaceTaskWidget.ui \
 	ui/cmd/tasks/CommandInterfaceTestTaskWidget.ui \
     ui/dlg/BrowseDialog.ui \
-    ui/image/LayerSettingsDialog.ui
+    ui/image/LayerSettingsDialog.ui \
+    ui/ml/rf/RandomForestFeaturesDialog.ui
 
 RESOURCES += \
 	res/Grinder.qrc
diff --git a/Grinder/Version.h b/Grinder/Version.h
index 777de0e..d65d4a8 100644
--- a/Grinder/Version.h
+++ b/Grinder/Version.h
@@ -10,14 +10,14 @@
 
 #define GRNDR_INFO_TITLE		"Grinder"
 #define GRNDR_INFO_COPYRIGHT	"Copyright (c) WWU Muenster"
-#define GRNDR_INFO_DATE			"30.12.2019"
+#define GRNDR_INFO_DATE			"13.01.2020"
 #define GRNDR_INFO_COMPANY		"WWU Muenster"
 #define GRNDR_INFO_WEBSITE		"http://www.uni-muenster.de"
 
 #define GRNDR_VERSION_MAJOR		0
 #define GRNDR_VERSION_MINOR		16
 #define GRNDR_VERSION_REVISION	0
-#define GRNDR_VERSION_BUILD		419
+#define GRNDR_VERSION_BUILD		422
 
 namespace grndr
 {
diff --git a/Grinder/common/properties/PropertyID.cpp b/Grinder/common/properties/PropertyID.cpp
index edcc885..55dd100 100644
--- a/Grinder/common/properties/PropertyID.cpp
+++ b/Grinder/common/properties/PropertyID.cpp
@@ -76,3 +76,12 @@ const char* PropertyID::SnapshotInterval = "SnapshotInterval";
 const char* PropertyID::Renderable = "Renderable";
 const char* PropertyID::State = "State";
 const char* PropertyID::Filename = "Filename";
+const char* PropertyID::Estimators = "Estimators";
+const char* PropertyID::MaxFeaturesType = "MaxFeaturesType";
+const char* PropertyID::MaxFeatures = "MaxFeatures";
+const char* PropertyID::MaxDepth = "MaxDepth";
+const char* PropertyID::MaxLeafNodes = "MaxLeafNodes";
+const char* PropertyID::MinSamples = "MinSamples";
+const char* PropertyID::Count = "Count";
+const char* PropertyID::Verbose = "Verbose";
+const char* PropertyID::Features = "Features";
diff --git a/Grinder/common/properties/PropertyID.h b/Grinder/common/properties/PropertyID.h
index 9ca6d4c..173bd94 100644
--- a/Grinder/common/properties/PropertyID.h
+++ b/Grinder/common/properties/PropertyID.h
@@ -83,6 +83,15 @@ namespace grndr
 		static const char* Renderable;
 		static const char* State;
 		static const char* Filename;
+		static const char* Estimators;
+		static const char* MaxFeaturesType;
+		static const char* MaxFeatures;
+		static const char* MaxDepth;
+		static const char* MaxLeafNodes;
+		static const char* MinSamples;
+		static const char* Count;
+		static const char* Verbose;
+		static const char* Features;
 
 	public:
 		using QString::QString;
diff --git a/Grinder/ml/rf/RandomForestClassifierConfiguration.cpp b/Grinder/ml/rf/RandomForestClassifierConfiguration.cpp
index 2cda51d..2f96e5d 100644
--- a/Grinder/ml/rf/RandomForestClassifierConfiguration.cpp
+++ b/Grinder/ml/rf/RandomForestClassifierConfiguration.cpp
@@ -36,9 +36,70 @@ void RandomForestClassifierConfiguration::verifyConfiguration() const
 		throw MachineLearningException{_EXCPT("No or an invalid Python interpreter was specified; please configure a valid interpreter in the Grinder options")};
 
 	ExternalClassifierConfiguration::verifyConfiguration();	
+
+	if (_features.getActiveFeatures().empty())
+		throw MachineLearningException{_EXCPT("At least one feature must be selected")};
+
+	if (_estimators == 0)
+		throw MachineLearningException{_EXCPT("The random forest must consist of at least one tree")};
+
+	if (_maxFeaturesType == MaxFeaturesType::Value)
+	{
+		if (_maxFeatures <= 0.0f)
+			throw MachineLearningException{_EXCPT("The maximum number of features must be greater than 0.0")};
+	}
+
+	if (_minSamples <= 0.0f)
+		throw MachineLearningException{_EXCPT("The minimum number of samples must be greater than 0.0")};
 }
 
 QString RandomForestClassifierConfiguration::getUserData() const
 {
-	return "";
+	QStringList userData;
+
+	// Features
+
+	userData << _features.getFeaturesUserData();
+
+	// Settings
+
+	userData << QString{"Estimators:%1"}.arg(_estimators);
+
+	switch (_maxFeaturesType)
+	{
+	case MaxFeaturesType::Value:
+		userData << QString{"Max Features:%1"}.arg(_maxFeatures);
+		break;
+
+	case MaxFeaturesType::Sqrt:
+		userData << QString{"Max Features:sqrt"};
+		break;
+
+	case MaxFeaturesType::Log2:
+		userData << QString{"Max Features:log2"};
+		break;
+
+	case MaxFeaturesType::All:
+		userData << QString{"Max Features:None"};
+		break;
+	}
+
+	if (_maxDepth > 0)
+		userData << QString{"Max Depth:%1"}.arg(_maxDepth);
+
+	if (_maxLeafNodes > 0)
+		userData << QString{"Max Leaf Nodes:%1"}.arg(_maxLeafNodes);
+
+	userData << QString{"Min Samples Split:%1"}.arg(_minSamples);
+
+	// Performance
+	int jobCount = _jobCount != 0 ? static_cast<int>(_jobCount) : -1;
+	userData << QString{"Jobs:%1"}.arg(jobCount);
+
+	// Miscellaneous
+
+	if (_verbose)
+		userData << QString{"Verbose"};
+
+	return userData.join("\n");
 }
diff --git a/Grinder/ml/rf/RandomForestClassifierConfiguration.h b/Grinder/ml/rf/RandomForestClassifierConfiguration.h
index 893f1d3..9a88c5a 100644
--- a/Grinder/ml/rf/RandomForestClassifierConfiguration.h
+++ b/Grinder/ml/rf/RandomForestClassifierConfiguration.h
@@ -7,6 +7,7 @@
 #define RANDOMFORESTCLASSIFIERCONFIGURATION_H
 
 #include "ml/external/ExternalClassifierConfiguration.h"
+#include "ml/rf/RandomForestFeatures.h"
 
 namespace grndr
 {
@@ -14,6 +15,15 @@ namespace grndr
 	{
 		Q_OBJECT
 
+	public:
+		enum class MaxFeaturesType
+		{
+			Value,
+			Sqrt,
+			Log2,
+			All,
+		};
+
 	public:
 		RandomForestClassifierConfiguration();
 
@@ -26,6 +36,43 @@ namespace grndr
 
 	public:
 		virtual QString getUserData() const override;
+
+	public:
+		RandomForestFeatures getFeatures() { return _features; }
+		void setFeatures(const RandomForestFeatures& features) { setValue(_features, features); }
+
+		unsigned int getEstimators() const { return _estimators; }
+		void setEstimators(unsigned int estimators) { setValue(_estimators, estimators); }
+		MaxFeaturesType getMaxFeaturesType() const { return _maxFeaturesType; }
+		void setMaxFeaturesType(MaxFeaturesType featuresType) { setValue(_maxFeaturesType, featuresType); }
+		float getMaxFeatures() const { return _maxFeatures; }
+		void setMaxFeatures(float maxFeatures) { if (maxFeatures > 1.0f) maxFeatures = static_cast<float>(static_cast<int>(maxFeatures)); setValue(_maxFeatures, maxFeatures); }
+		unsigned int getMaxDepth() const { return _maxDepth; }
+		void setMaxDepth(unsigned int maxDepth) { setValue(_maxDepth, maxDepth); }
+		unsigned int getMaxLeafNodes() const { return _maxLeafNodes; }
+		void setMaxLeafNodes(unsigned int leafNodes) { setValue(_maxLeafNodes, leafNodes); }
+		float getMinSamples() const { return _minSamples; }
+		void setMinSamples(float minSamples) { if (minSamples > 1.0f) minSamples = static_cast<float>(static_cast<int>(minSamples)); setValue(_minSamples, minSamples); }
+
+		unsigned int getJobCount() const { return _jobCount; }
+		void setJobCount(unsigned int jobCount) { setValue(_jobCount, jobCount); }
+
+		bool isVerbose() const { return _verbose; }
+		void setVerbose(bool verbose) { setValue(_verbose, verbose); }
+
+	private:
+		RandomForestFeatures _features;
+
+		unsigned int _estimators{10};
+		MaxFeaturesType _maxFeaturesType{MaxFeaturesType::Sqrt};
+		float _maxFeatures{1.0f};
+		unsigned int _maxDepth{0};
+		unsigned int _maxLeafNodes{0};
+		float _minSamples{0.05f};
+
+		unsigned int _jobCount{0};
+
+		bool _verbose{false};
 	};
 }
 
diff --git a/Grinder/ml/rf/RandomForestFeatures.cpp b/Grinder/ml/rf/RandomForestFeatures.cpp
new file mode 100644
index 0000000..1d4be36
--- /dev/null
+++ b/Grinder/ml/rf/RandomForestFeatures.cpp
@@ -0,0 +1,88 @@
+/******************************************************************************
+ * File: RandomForestFeatures.cpp
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "RandomForestFeatures.h"
+#include "util/StringConv.h"
+
+const char* RandomForestFeatures::Serialization_Value_GaborFilters = "GaborFilters";
+const char* RandomForestFeatures::Serialization_Value_Sobel = "Sobel";
+const char* RandomForestFeatures::Serialization_Value_Gaussian = "Gaussian";
+const char* RandomForestFeatures::Serialization_Value_Color = "Color";
+const char* RandomForestFeatures::Serialization_Value_DifferenceOfGaussians = "DoG";
+const char* RandomForestFeatures::Serialization_Value_LaplacianOfGaussians = "LoG";
+const char* RandomForestFeatures::Serialization_Value_Hessian = "Hessian";
+const char* RandomForestFeatures::Serialization_Value_StructureTensor = "StructureTensor";
+
+std::vector<const RandomForestFeatures::Feature*> RandomForestFeatures::getActiveFeatures() const
+{
+	std::vector<const Feature*> features;
+
+	for (const auto& feat : allFeatures())
+	{
+		if (feat->isActive)
+			features.push_back(feat);
+	}
+
+	return features;
+}
+
+QStringList RandomForestFeatures::getFeaturesUserData() const
+{
+	QStringList features;
+
+	for (const auto& feat : allFeatures())
+	{
+		if (feat->isActive)
+		{
+			QString value = feat->getFeatureValue();
+
+			if (!value.isEmpty())
+				features << QString{"%1:%2"}.arg(feat->getUserDataName()).arg(value);
+			else
+				features << QString{"%1"}.arg(feat->getUserDataName());
+		}
+	}
+
+	return features;
+}
+
+bool RandomForestFeatures::operator ==(const RandomForestFeatures& other) const
+{
+	auto features = allFeatures();
+	auto otherFeatures = other.allFeatures();
+
+	for (unsigned int i = 0; i < features.size(); ++i)
+	{
+		if (!features[i]->compare(*otherFeatures[i]))
+			return false;
+	}
+
+	return true;
+}
+
+void RandomForestFeatures::serialize(SerializationContext& ctx) const
+{
+	for (const auto& feat : allFeatures())
+		ctx.settings()(feat->serializationValueName) = QString{"%1|%2"}.arg(feat->isActive ? "on" : "off").arg(feat->getFeatureValue(true));
+}
+
+void RandomForestFeatures::deserialize(DeserializationContext& ctx)
+{
+	for (auto& feat : allFeatures())
+	{
+		QString value = ctx.settings()(feat->serializationValueName).toString();
+		QStringList tokens = value.split("|");
+
+		if (tokens.size() >= 1)
+		{
+			QString active = tokens.front();
+			tokens.pop_front();
+
+			feat->isActive = (active.compare("on", Qt::CaseInsensitive) == 0);
+			feat->setFeatureValue(tokens);
+		}
+	}
+}
diff --git a/Grinder/ml/rf/RandomForestFeatures.h b/Grinder/ml/rf/RandomForestFeatures.h
new file mode 100644
index 0000000..ef76b0b
--- /dev/null
+++ b/Grinder/ml/rf/RandomForestFeatures.h
@@ -0,0 +1,221 @@
+/******************************************************************************
+ * File: RandomForestFeatures.h
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#ifndef RANDOMFORESTFEATURES_H
+#define RANDOMFORESTFEATURES_H
+
+#include "common/serialization/SerializationContext.h"
+#include "common/serialization/DeserializationContext.h"
+#include "util/StringConv.h"
+
+namespace grndr
+{
+	class RandomForestFeatures
+	{
+	public:
+		static const char* Serialization_Value_GaborFilters;
+		static const char* Serialization_Value_Sobel;
+		static const char* Serialization_Value_Gaussian;
+		static const char* Serialization_Value_Color;
+		static const char* Serialization_Value_DifferenceOfGaussians;
+		static const char* Serialization_Value_LaplacianOfGaussians;
+		static const char* Serialization_Value_Hessian;
+		static const char* Serialization_Value_StructureTensor;
+
+	public:
+		enum class MatrixFeatureType
+		{
+			Raw,
+			Eigen,
+			Both,
+		};
+
+	public:
+		struct Feature
+		{
+			QString name{""};
+			QString serializationValueName{""};
+
+			bool isActive{false};
+
+			Feature(QString n, QString v) : name{n}, serializationValueName{v} { }
+
+			virtual bool compare(const Feature& other) const {
+				return name == other.name && serializationValueName == other.serializationValueName && isActive == other.isActive;
+			}
+
+			virtual QString getUserDataName() const { return name; }
+			virtual QString getFeatureValue(bool serialization = false) const { Q_UNUSED(serialization); return ""; }
+			virtual void setFeatureValue(QStringList valueTokens) { Q_UNUSED(valueTokens); }
+		};
+
+		struct GaussFeature : Feature
+		{
+			using Feature::Feature;
+
+			float stdDeviation{0.5f};
+
+			virtual bool compare(const Feature& other) const override {
+				const GaussFeature& o = dynamic_cast<const GaussFeature&>(other); return stdDeviation == o.stdDeviation && Feature::compare(other);
+			}
+
+			virtual QString getFeatureValue(bool serialization = false) const override { Q_UNUSED(serialization); return QString{"%1"}.arg(stdDeviation); }
+			virtual void setFeatureValue(QStringList valueTokens) override {
+				if (valueTokens.size() >= 1)
+					stdDeviation = StringConv::convertString<float>(valueTokens[0]);
+			}
+		};
+
+		struct DualGaussFeature : Feature
+		{
+			using Feature::Feature;
+
+			float stdDeviation1{0.5f};
+			float stdDeviation2{0.33f};
+
+			virtual bool compare(const Feature& other) const override {
+				const DualGaussFeature& o = dynamic_cast<const DualGaussFeature&>(other); return stdDeviation1 == o.stdDeviation1 && stdDeviation2 == o.stdDeviation2 && Feature::compare(other);
+			}
+
+			virtual QString getFeatureValue(bool serialization = false) const override { Q_UNUSED(serialization); return QString{"%1|%2"}.arg(stdDeviation1).arg(stdDeviation2); }
+			virtual void setFeatureValue(QStringList valueTokens) override {
+				if (valueTokens.size() >= 2)
+				{
+					stdDeviation1 = StringConv::convertString<float>(valueTokens[0]);
+					stdDeviation2 = StringConv::convertString<float>(valueTokens[1]);
+				}
+			}
+		};
+
+		struct MatrixFeature : Feature
+		{
+			using Feature::Feature;
+
+			MatrixFeatureType matrixType{MatrixFeatureType::Both};
+			float stdDeviation{0.5f};
+
+			virtual bool compare(const Feature& other) const override {
+				const MatrixFeature& o = dynamic_cast<const MatrixFeature&>(other); return matrixType == o.matrixType && stdDeviation == o.stdDeviation && Feature::compare(other);
+			}
+
+			virtual QString getUserDataName() const override {
+				QString fullName = name;
+
+				switch (matrixType)
+				{
+				case MatrixFeatureType::Raw:
+					fullName += " Raw";
+					break;
+
+				case MatrixFeatureType::Eigen:
+					fullName += " Eigen";
+					break;
+
+				case MatrixFeatureType::Both:
+					break;
+				}
+
+				return fullName;
+			}
+
+			virtual QString getFeatureValue(bool serialization = false) const override {
+				if (serialization)
+					return QString{"%1|%2"}.arg(stdDeviation).arg(static_cast<int>(matrixType));
+				else
+					return QString{"%1"}.arg(stdDeviation);
+			}
+
+			virtual void setFeatureValue(QStringList valueTokens) override {
+				if (valueTokens.size() >= 2)
+				{
+					stdDeviation = StringConv::convertString<float>(valueTokens[0]);
+					matrixType = static_cast<MatrixFeatureType>(StringConv::convertString<int>(valueTokens[1]));
+				}
+			}
+		};
+
+		struct GaborFilters : Feature
+		{
+			using Feature::Feature;
+
+			unsigned short activeFilters{0};
+
+			virtual bool compare(const Feature& other) const override {
+				const GaborFilters& o = dynamic_cast<const GaborFilters&>(other); return activeFilters == o.activeFilters && Feature::compare(other);
+			}
+
+			virtual QString getFeatureValue(bool serialization = false) const override {
+				Q_UNUSED(serialization);
+
+				QStringList filters;
+
+				for (unsigned short i = 0; i < 16; ++i)
+				{
+					if (activeFilters & (1 << i))
+						filters << QString{"%1"}.arg(i);
+				}
+
+				return filters.join("|");
+			}
+
+			virtual void setFeatureValue(QStringList valueTokens) override {
+				activeFilters = 0;
+
+				for (int i = 0; i < valueTokens.size(); ++i)
+				{
+					int value = StringConv::convertString<int>(valueTokens[i]);
+					activeFilters |= 1 << value;
+				}
+			}
+		};
+
+	public:
+		GaborFilters& gaborFilters() { return _gaborFilters; }
+		const GaborFilters& gaborFilters() const { return _gaborFilters; }
+		Feature& sobel() { return _sobel; }
+		const Feature& sobel() const { return _sobel; }
+		GaussFeature& gaussian() { return _gaussian; }
+		const GaussFeature& gaussian() const { return _gaussian; }
+		GaussFeature& color() { return _color; }
+		const GaussFeature& color() const { return _color; }
+		DualGaussFeature& differenceOfGaussians() { return _dog; }
+		const DualGaussFeature& differenceOfGaussians() const { return _dog; }
+		GaussFeature& laplacianOfGaussians() { return _log; }
+		const GaussFeature& laplacianOfGaussians() const { return _log; }
+		MatrixFeature& hessian() { return _hessian; }
+		const MatrixFeature& hessian() const { return _hessian; }
+		MatrixFeature& structureTensor() { return _structureTensor; }
+		const MatrixFeature& structureTensor() const { return _structureTensor; }
+
+	public:
+		std::vector<const Feature*> getActiveFeatures() const;
+
+		QStringList getFeaturesUserData() const;
+
+	public:
+		bool operator ==(const RandomForestFeatures& other) const;
+		bool operator !=(const RandomForestFeatures& other) const { return !operator ==(other); }
+
+	public:
+		void serialize(SerializationContext& ctx) const;
+		void deserialize(DeserializationContext& ctx);
+
+	private:
+		std::vector<Feature*> allFeatures() { return {&_gaborFilters, &_sobel, &_gaussian, &_color, &_dog, &_log, &_hessian, &_structureTensor}; }
+		std::vector<const Feature*> allFeatures() const { return {&_gaborFilters, &_sobel, &_gaussian, &_color, &_dog, &_log, &_hessian, &_structureTensor}; }
+
+	private:
+		GaborFilters _gaborFilters{"Gabor", Serialization_Value_GaborFilters};
+		Feature _sobel{"Sobel", Serialization_Value_Sobel};
+		GaussFeature _gaussian{"Gaussian", Serialization_Value_Gaussian};
+		GaussFeature _color{"Color", Serialization_Value_Color};
+		DualGaussFeature _dog{"DoG", Serialization_Value_DifferenceOfGaussians};
+		GaussFeature _log{"LoG", Serialization_Value_LaplacianOfGaussians};
+		MatrixFeature _hessian{"Hessian", Serialization_Value_Hessian};
+		MatrixFeature _structureTensor{"Structure Tensor", Serialization_Value_StructureTensor};
+	};
+}
+
+#endif
diff --git a/Grinder/ml/rf/blocks/RandomForestClassifierBlock.cpp b/Grinder/ml/rf/blocks/RandomForestClassifierBlock.cpp
index 1225183..7854d67 100644
--- a/Grinder/ml/rf/blocks/RandomForestClassifierBlock.cpp
+++ b/Grinder/ml/rf/blocks/RandomForestClassifierBlock.cpp
@@ -12,3 +12,72 @@ RandomForestClassifierBlock::RandomForestClassifierBlock(Pipeline* pipeline, QSt
 {
 
 }
+
+void RandomForestClassifierBlock::createProperties()
+{
+	ExternalClassifierBlock::createProperties();
+
+	setPropertyGroup("Features");
+
+	_features = createProperty<RandomForestFeaturesProperty>(PropertyID::Features, "Image features");
+	features()->setDescription("The features to extract and use from the input image.");
+
+	setPropertyGroup("Settings");
+
+	_estimators = createProperty<UIntProperty>(PropertyID::Estimators, "Tree count", _method.config().getEstimators());
+	estimators()->createConstraint<RangeConstraint>(1, std::numeric_limits<unsigned int>::max());
+	estimators()->setDescription("The number of trees in the forest.");
+
+	_maxFeaturesType = createProperty<MaxFeaturesTypeProperty>(PropertyID::MaxFeaturesType, "Max. features type", static_cast<int>(_method.config().getMaxFeaturesType()));
+	maxFeaturesType()->setDescription("How to choose the number of features to consider when looking for the best split.");
+
+	_maxFeatures = createProperty<RealProperty>(PropertyID::MaxFeatures, "Max. features", _method.config().getMaxFeatures());
+	maxFeatures()->setDescription("The number of features to consider when looking for the best split.");
+
+	_maxDepth = createProperty<UIntProperty>(PropertyID::MaxDepth, "Max. depth", _method.config().getMaxDepth());
+	maxDepth()->setDescription("The maximum depth of a tree. A value of 0 means an unlimited depth.");
+
+	_maxLeafNodes = createProperty<UIntProperty>(PropertyID::MaxLeafNodes, "Max. leaf nodes", _method.config().getMaxLeafNodes());
+	maxLeafNodes()->setDescription("The maximum number of leafs in a tree. A value of 0 means an unlimited number of leafs.");
+
+	_minSamples = createProperty<RealProperty>(PropertyID::MinSamples, "Min. samples per leaf", _method.config().getMinSamples());
+	minSamples()->setDescription("The minimum number of samples required for a leaf node. Values smaller than 1.0 are treated as fractions of the number of samples.");
+
+	setPropertyGroup("Performance");
+
+	_jobCount = createProperty<UIntProperty>(PropertyID::Count, "Job count", _method.config().getJobCount());
+	jobCount()->setDescription("The number of jobs to run in parallel. If 0, all processors will be used.");
+
+	setPropertyGroup("Miscellaneous");
+
+	_verbose = createProperty<BoolProperty>(PropertyID::Verbose, "Verbose", _method.config().isVerbose());
+	verbose()->setDescription("Send status updates; note that this will increase the overall computation time.");
+}
+
+bool RandomForestClassifierBlock::updateProperties(PropertyBase* updatedProp)
+{
+	bool updated = ExternalClassifierBlock::updateProperties(updatedProp);
+
+	// Update properties to reflect property dependencies
+	updated |= enableDependantProperty(updatedProp, _maxFeaturesType, _maxFeatures, [this]() { return *maxFeaturesType() == RandomForestClassifierConfiguration::MaxFeaturesType::Value; });
+
+	return updated;
+}
+
+void RandomForestClassifierBlock::updateConfiguration()
+{
+	ExternalClassifierBlock::updateConfiguration();
+
+	_method.config().setFeatures(features()->object());
+
+	_method.config().setEstimators(*estimators());
+	_method.config().setMaxFeaturesType(*maxFeaturesType());
+	_method.config().setMaxFeatures(*maxFeatures());
+	_method.config().setMaxDepth(*maxDepth());
+	_method.config().setMaxLeafNodes(*maxLeafNodes());
+	_method.config().setMinSamples(*minSamples());
+
+	_method.config().setJobCount(*jobCount());
+
+	_method.config().setVerbose(*verbose());
+}
diff --git a/Grinder/ml/rf/blocks/RandomForestClassifierBlock.h b/Grinder/ml/rf/blocks/RandomForestClassifierBlock.h
index ba6594f..b1c08fd 100644
--- a/Grinder/ml/rf/blocks/RandomForestClassifierBlock.h
+++ b/Grinder/ml/rf/blocks/RandomForestClassifierBlock.h
@@ -8,6 +8,8 @@
 
 #include "ml/external/blocks/ExternalClassifierBlock.h"
 #include "ml/rf/RandomForestClassifierMethod.h"
+#include "ml/rf/properties/RandomForestFeaturesProperty.h"
+#include "ml/rf/properties/MaxFeaturesTypeProperty.h"
 
 namespace grndr
 {
@@ -20,6 +22,50 @@ namespace grndr
 
 	public:
 		RandomForestClassifierBlock(Pipeline* pipeline, QString name = "");
+
+	public:
+		auto features() { return dynamic_cast<RandomForestFeaturesProperty*>(_features.get()); }
+		auto features() const { return dynamic_cast<const RandomForestFeaturesProperty*>(_features.get()); }
+
+		auto estimators() { return dynamic_cast<UIntProperty*>(_estimators.get()); }
+		auto estimators() const { return dynamic_cast<const UIntProperty*>(_estimators.get()); }
+		auto maxFeaturesType() { return dynamic_cast<MaxFeaturesTypeProperty*>(_maxFeaturesType.get()); }
+		auto maxFeaturesType() const { return dynamic_cast<const MaxFeaturesTypeProperty*>(_maxFeaturesType.get()); }
+		auto maxFeatures() { return dynamic_cast<RealProperty*>(_maxFeatures.get()); }
+		auto maxFeatures() const { return dynamic_cast<const RealProperty*>(_maxFeatures.get()); }
+		auto maxDepth() { return dynamic_cast<UIntProperty*>(_maxDepth.get()); }
+		auto maxDepth() const { return dynamic_cast<const UIntProperty*>(_maxDepth.get()); }
+		auto maxLeafNodes() { return dynamic_cast<UIntProperty*>(_maxLeafNodes.get()); }
+		auto maxLeafNodes() const { return dynamic_cast<const UIntProperty*>(_maxLeafNodes.get()); }
+		auto minSamples() { return dynamic_cast<RealProperty*>(_minSamples.get()); }
+		auto minSamples() const { return dynamic_cast<const RealProperty*>(_minSamples.get()); }
+
+		auto jobCount() { return dynamic_cast<UIntProperty*>(_jobCount.get()); }
+		auto jobCount() const { return dynamic_cast<const UIntProperty*>(_jobCount.get()); }
+
+		auto verbose() { return dynamic_cast<BoolProperty*>(_verbose.get()); }
+		auto verbose() const { return dynamic_cast<const BoolProperty*>(_verbose.get()); }
+
+	protected:
+		virtual void createProperties() override;
+		virtual bool updateProperties(PropertyBase* updatedProp = nullptr) override;
+
+	protected:
+		virtual void updateConfiguration() override;
+
+	private:
+		std::shared_ptr<PropertyBase> _features;
+
+		std::shared_ptr<PropertyBase> _estimators;
+		std::shared_ptr<PropertyBase> _maxFeaturesType;
+		std::shared_ptr<PropertyBase> _maxFeatures;
+		std::shared_ptr<PropertyBase> _maxDepth;
+		std::shared_ptr<PropertyBase> _maxLeafNodes;
+		std::shared_ptr<PropertyBase> _minSamples;
+
+		std::shared_ptr<PropertyBase> _jobCount;
+
+		std::shared_ptr<PropertyBase> _verbose;
 	};
 }
 
diff --git a/Grinder/ml/rf/properties/MaxFeaturesTypeProperty.cpp b/Grinder/ml/rf/properties/MaxFeaturesTypeProperty.cpp
new file mode 100644
index 0000000..be56f94
--- /dev/null
+++ b/Grinder/ml/rf/properties/MaxFeaturesTypeProperty.cpp
@@ -0,0 +1,17 @@
+/******************************************************************************
+ * File: MaxFeaturesTypeProperty.h
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "MaxFeaturesTypeProperty.h"
+
+void MaxFeaturesTypeProperty::initProperty()
+{
+	property_type::initProperty();
+
+	_enumMap[RandomForestClassifierConfiguration::MaxFeaturesType::Value] = "Value";
+	_enumMap[RandomForestClassifierConfiguration::MaxFeaturesType::Sqrt] = "Square root";
+	_enumMap[RandomForestClassifierConfiguration::MaxFeaturesType::Log2] = "Log_2";
+	_enumMap[RandomForestClassifierConfiguration::MaxFeaturesType::All] = "All";
+}
diff --git a/Grinder/ml/rf/properties/MaxFeaturesTypeProperty.h b/Grinder/ml/rf/properties/MaxFeaturesTypeProperty.h
new file mode 100644
index 0000000..0d27349
--- /dev/null
+++ b/Grinder/ml/rf/properties/MaxFeaturesTypeProperty.h
@@ -0,0 +1,26 @@
+/******************************************************************************
+ * File: MaxFeaturesTypeProperty.h
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#ifndef MAXFEATURESTYPEPROPERTY_H
+#define MAXFEATURESTYPEPROPERTY_H
+
+#include "common/properties/types/EnumProperty.h"
+#include "ml/rf/RandomForestClassifierConfiguration.h"
+
+namespace grndr
+{
+	class MaxFeaturesTypeProperty : public EnumProperty<RandomForestClassifierConfiguration::MaxFeaturesType>
+	{
+		Q_OBJECT
+
+	public:
+		using EnumProperty<RandomForestClassifierConfiguration::MaxFeaturesType>::EnumProperty;
+
+	public:
+		virtual void initProperty() override;
+	};
+}
+
+#endif
diff --git a/Grinder/ml/rf/properties/RandomForestFeaturesProperty.cpp b/Grinder/ml/rf/properties/RandomForestFeaturesProperty.cpp
new file mode 100644
index 0000000..1e4c0d5
--- /dev/null
+++ b/Grinder/ml/rf/properties/RandomForestFeaturesProperty.cpp
@@ -0,0 +1,30 @@
+/******************************************************************************
+ * File: RandomForestFeaturesProperty.cpp
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "RandomForestFeaturesProperty.h"
+#include "ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.h"
+
+QWidget* RandomForestFeaturesProperty::createEditor(QWidget* parent)
+{
+	return new RandomForestFeaturesPropertyEditor{this, parent};
+}
+
+QString RandomForestFeaturesProperty::toString() const
+{
+	auto features = _object.getActiveFeatures();
+
+	if (!features.empty())
+	{
+		QStringList names;
+
+		for (auto feat : features)
+			names << feat->name;
+
+		return names.join(", ");
+	}
+	else
+		return "No features active";
+}
diff --git a/Grinder/ml/rf/properties/RandomForestFeaturesProperty.h b/Grinder/ml/rf/properties/RandomForestFeaturesProperty.h
new file mode 100644
index 0000000..087be08
--- /dev/null
+++ b/Grinder/ml/rf/properties/RandomForestFeaturesProperty.h
@@ -0,0 +1,29 @@
+/******************************************************************************
+ * File: RandomForestFeaturesProperty.h
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#ifndef RANDOMFORESTFEATURESPROPERTY_H
+#define RANDOMFORESTFEATURESPROPERTY_H
+
+#include "common/properties/ObjectProperty.h"
+#include "ml/rf/RandomForestFeatures.h"
+
+namespace grndr
+{
+	class RandomForestFeaturesProperty : public ObjectProperty<RandomForestFeatures>
+	{
+		Q_OBJECT
+
+	public:
+		using ObjectProperty::ObjectProperty;
+
+	public:
+		virtual QWidget* createEditor(QWidget* parent) override;
+
+	public:
+		virtual QString toString() const override;
+	};
+}
+
+#endif
diff --git a/Grinder/res/Grinder.qrc b/Grinder/res/Grinder.qrc
index a5bc8b4..e011e85 100644
--- a/Grinder/res/Grinder.qrc
+++ b/Grinder/res/Grinder.qrc
@@ -118,4 +118,22 @@
         <file>cursors/polyline-cursor-add.png</file>
         <file>cursors/polyline-cursor-del.png</file>
     </qresource>
+    <qresource prefix="/gabor">
+        <file>gabor/filter0.png</file>
+        <file>gabor/filter1.png</file>
+        <file>gabor/filter2.png</file>
+        <file>gabor/filter3.png</file>
+        <file>gabor/filter4.png</file>
+        <file>gabor/filter5.png</file>
+        <file>gabor/filter6.png</file>
+        <file>gabor/filter7.png</file>
+        <file>gabor/filter8.png</file>
+        <file>gabor/filter9.png</file>
+        <file>gabor/filter10.png</file>
+        <file>gabor/filter11.png</file>
+        <file>gabor/filter12.png</file>
+        <file>gabor/filter13.png</file>
+        <file>gabor/filter14.png</file>
+        <file>gabor/filter15.png</file>
+    </qresource>
 </RCC>
diff --git a/Grinder/res/gabor/filter0.png b/Grinder/res/gabor/filter0.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2c17e3d2dbc523c92c68e01b46764e51b7dcdca
GIT binary patch
literal 1522
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2Qj-)+A3C$B>G+w|5Lzi8@NOTwI>#U|?DlzV7seqgh1>kMa*V&9bY!_%+<>
z&TqrdFQ=Yg&*9!R??~!IQRRmo7bow~*_5Orqgf?1-8HPIMcLK=(WVn!6APBS@Qe|Q
zG!*I+vF$vqr0sTS;v)6J$dscV7C~QB^t#tba8lcl|JSC!yRB^>C)g*VduHRqvxlHA
zJR~E1PY3Fjp(pWi>+heZ-iHGNmb*BoVsil`2)7s<Z!_nHhWL<{aDV#U?Be%#*R|^*
z5hr-}qI{RwvyOK!_<=rlDgV;GsMT_j^)Kg(hh#3w{#}@n*!QCEU%(av_Y(Jdji?#Q
zyB6C6<C{WlHTJ7&)@s!Vch}T+58I$V-d2A?`wxHo6Z`*4z??g@^96GL{;#`#d#Bxg
lrNK}NVU&mexAyn%uRp<l`tMY89$=Bo;OXk;vd$@?2>=woeOv$l

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter1.png b/Grinder/res/gabor/filter1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c6928391b89b49e8938a8777d596090f4394377e
GIT binary patch
literal 862
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2Qj-rgNSyjv*CsZ|`i(lyDGX2^4+6CBq^5s4jNdT%H^o@fX@Nrt8<J+3x5+
zKmYA(&d2jty<qRETR;>cxA_0J2(S8i2GcCABxs7((w4M!`~P&~lz(=KI)y6^25J2&
zpaWUe^Y0<hPFuR#Ip<?k>V_#5^^+yMWwJI*>9V5-DYy8u^SpyWkIvh7ZalJ(&3omG
z!!#vd8vfe;v+B@~<yyZQ7s+c7Lu~*3=MTRYZ_KI%J;v3*<j>&g>gTe~DWM4f)0>D@

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter10.png b/Grinder/res/gabor/filter10.png
new file mode 100644
index 0000000000000000000000000000000000000000..afba2eebd7800d6b26142b6a5d2e3456dd240e32
GIT binary patch
literal 1728
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv3?%0qQ0W0ujKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WMyDrW(e>JaRn)2WMpDuVqsxnWo2b!V`FD$=iuPr<mBYy;^OA!=HcPt<>lq$
z;}Z}N5EK*?5)u*-5fKv;6BiekkdTm)l9H8`m6w-SP*6}-R#sP6*U-?=*4Eb3(=#wI
zu(!8&baZrgclY-84hRT{jg3uAOiWEp&CJZq&(H7d?3^%R!u<L37c5w?a^=e1yLX>I
zfByFE+s~gr|Mcn8|NsAiZW{%oAwbg*xas`p7tjLE0*}aI1_r*vAk26?e?<yVP_o1|
zq9iy!t)x7$D3zfgF*C13FE6!3!9>qM&&cqjLgrLpE>iJyaSX|DetY|PUQD<EL!jmC
zbC$P@&fWa~KcDTFNMm`T9&Zmr>DzlH<x|Z>+?y{h-pyX|bN`-_r$^Z{TKd*4vAglZ
z@pt&9MfV()OG;F~9z4c>YyTwCuY6Jq7i+KNKUdN1apW)4Jo$Om43n?jQSoSZx?yq4
zVY^GD>MPk9Tb`*oT8SJwcCk&lZIkLe*?9*SFJuQPOYn~6kam~8wp;a-R$%_)X3y+A
zp$$v*TGW;9@qh&vRwZ4UHJc@8WnjtUy<D&F3Z+h6QJw%7GEaOALJ#f!o%)~^Cx7qI
z?!)XqDt@rc=d%S_${3wBcaik>By<a|Y5qD9eAE2HxetPN@^X)7Ka8$1{{l6&v@7pU
zcW%V{X&S3t_V)6tt=;K)U|Q7j7rBq;{km8uEzcMK;PpfHKPG>=?zhSLL={hN(B0~H
zvA237FT{1*KOS6SYR-8h=(Wh+{@;!j{B^Olo$uS{fBgMH{hw5oLFQRsrfpZOT+QDd
zHCt2eJtcI}y<^a@SR4Fh+sEr4o_(;hliqiz|FHg#<R8-ic?fuASN+KfESBHhVP3ZP
z^yg{ozt`X6`F8HY?IZV$|K;Z^?7h+UHLr7%*Ol8p;6AoH`tje~cPIG|)#)%TS|jw;
z%nlxU{)Kz|=i5qtb-u5D&2Hbj%{lTp-!-?#hZAsAmHd8AX)yZ!ai1-7{OXt1X8Q^*
zZ_lq0UH<;54Agj$yA|sXRUr}yFup|YTFi<B2mbw?A#b5UDGo|HXs+gdc(VI&U_INe
zzx<;7V4u7U0HviD;D}>}CPItH7avV^d0>}GFbV75x%-zpW9FW9ul0K+Kv~(-)z4*}
HQ$iB}g8T3a

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter11.png b/Grinder/res/gabor/filter11.png
new file mode 100644
index 0000000000000000000000000000000000000000..703d7087a6b5f1d29785aa3975f222fb874584df
GIT binary patch
literal 1179
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2W0T+@3CuAr*0N?`~Wx!z$AHP&}|{^#blJ1=FG?mxK?(Wepx492y5KH79Ww
z%9}jonPhMAf9}L*tn>Ds{CR0=+4;YVr@Gy$ty<Tf{e`u6--3(cCcm28;xD}5mJ|GY
zz4?2(-JPC(LpD~o`ztT1V<%n;Ud+xw(;O%D_wLMizI*e2zI<@pIG?Z1;`6Tt@t*A*
zH9dwJ2DdEqCmiS~T?#Z&`Z=qR#Kv_dzm8-)-s8Zed~K@Re2LsS;vNY{c3pZQytwx>
z6K79ExKwS@mclrNM#0yoycb^&zw&}xs@8F_KFr!V?$0H2A3R+5x9r35nfv+d=KcKr
zV7~EuzKVkt-3N|KG&4cXX<Tp(ZYJsMGP%8ZSJ7;?`RH-A&hEj=wI7d|*VI4T-JE}Z
zkJXQ7XD&19OXLgwkVp<lfH+)R;0oLmWUzltEkyD!IF3FXVGen0nf>GMnfdHC?@Z^(
zx1Ue{XZxe**?#6YgL_IJ438~1014DEg%zf-049U|Rr@g{Ng#ze|1wzIF*&Y<$0Hf+
zcu9DQgBkz)Vr7F=#ks6MT<#C1TlL)k!U6PNoubMIZKY$MA0Oa87R_RJOhN>jOt@IK
z!jlXc?9VSsSA)|#%=X@W50+l6T$1(Ye9(jaGkTV9YpgFbn)ie4pZcjE?kmx4$DT;N
z7W?;@o#R$9NQ%w;(s}V%Wh0B*8tq;?Lukr>y7b~}?JTrBBPTdFeapkYl5+3vwUM6X
e_`m=8V{e<a(5mr%%r0Qr!QkoY=d#Wzp$P!75C}^E

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter12.png b/Grinder/res/gabor/filter12.png
new file mode 100644
index 0000000000000000000000000000000000000000..14f058f807ebe87a04992a5d7da5aa9062a1b1b4
GIT binary patch
literal 1313
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2W0T6FprVLn`9l-Z5Mi6E5I-G5;~6MZmj2&97TbVl*QIKIUC;71{c!skpqI
z|JUo8`|Un`cb9Q*%X^eMv1{Vv6Bm5<=mZ%S$w=<%obIaS)~D=j{&ADX(G!I!FVy11
zR!Ug*@!UOfT1iy-_{4>?D<YR9J+Uw<5!UYx>7lwI_Sfw1Ztwr|ePX@+BXR8~70xG|
zj(ULL38y~u7M+kD5Ny#IY7#4IOgmLH|NTGF-(T~;<6EoDMOS$zu_71oT8_vQg0Wu|
zQxx4xF1Amgg+%;5YgfB&(JB{v`$rZ=TP7&)dU5!IQ{Rh4{Lb8#i@NQ2bf0wS{W745
z#NqR$`@%%`b-Gr$?EC7vPn%9&h6L-+^Se%!FL(NVB7LLBq!aa$VnurtQx+yI0Ya4&
z#pBXWVp?t>2#km!Ch=z@Fg^WA8eCZyR*3xCU0eH?Nh#i>x$1CDBd`Qx@O1TaS?83{
F1OTsK9Mb>*

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter13.png b/Grinder/res/gabor/filter13.png
new file mode 100644
index 0000000000000000000000000000000000000000..3302326b020d4db80be50eb3ec8c3b4cd9e5f41b
GIT binary patch
literal 917
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2Qj-W>!xZ$B>G+w|6%7-f<9Nx#;a8$&q~`z>Diq)ATt^xAd3}PYgV-{%diD
z(C+y8+yB%EPX51oQGCD`?yfq=i~1VBSY7uocp)zGw+V;f2ZLYw@4qYC#~qP2KA|XD
z%aJ+3S?ia;C585oFN#YX`BuJg_Gp#~y4XH}&2q^iens9Q4_A37sV5V<>{?97&`|qv
zi;K|X-xKZoYiymWo4dd!i!KG3tmOqVIb;gR<P{nqrvwCooU&jM*eR}Hr;x3o{o+lN
z7m9ytTiR9mOhFE}xB~Ly!vL`FoWcI0iNu!=?QuC$HNul$D1Kb)|7iCjM+$vHd{|vN
z{K6*p!aq@!i9ht6sTqs7(h1PQ^$&aZHl6(3W#=<5{7d4eJ&X7!KNqc?VYB|l;U|@@
p@@nU`e;HK9U2G>>$+w?>m{+Y-jh0(pq6Ew}44$rjF6*2UngE=#a+d%A

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter14.png b/Grinder/res/gabor/filter14.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb7a47c0355dca7c255bf6698f685fda24373267
GIT binary patch
literal 1572
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv3?%0qQ0W0ujKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WMyDrW(e>JaRn)2WMpDuVqsxnWo2b!V`FD$=iuPr<mBYy;^OA!=HcPt<KyG!
z=NAwV5E2p+6BCn=kdTy=l#-H?mX?;4m6ey5S5{V5QBl#>*4ES0GcYhPHa0dfF|oI|
zcXM;|^z;l44h{_sO-@cuNlD4g&8@4etFN!`?ChK~XU_8F%XjbIy?_7y3l}atd-m+@
z+qeJ!{|7p06pV%deL`UElhgBnHgOhsL>4nJ@ErzW#^d=bQh<VzC9V-A!TD(=<%vb9
z3<Zgqc_n&zsU->~dIow%h94C&r!p`wb$GfshGaOuy?r|DQG$SLV(Sero;Hsuv+qpp
zUhw6&eXRdJmAxf*B)4o$k8FH1s~{}&>2s~4wXSV;a{C_DT|2C=A{x8HC$Av7uzf<i
z&6Bq$G_%vEB{wLZ>SD5+zIvhiRF%2<E0(TqTh;}Vws-K0wEpbgV{mXyQkUVCppH`}
zPv68ml*-Cj#Zh^}`Cebl{!8MT&5X}quzIi2Hj7B?Th3*=WX|=h9YMj%m3t;Ztmswb
zk7|1uu*}C|`uZE8G8>MIb$6w&Sjab5D)KTf$e49^Tmq&=waxU141RY{`<%tYj_q8g
zIw^j#R*RM@K2hZV$v;6l!YJ82aEAM`UsHQ+tOMqqpTFY)bC!ji>b<ab<L?g+u`Q0A
z(Pr~RG2i{&u5|s0&%f~|e?H;0f0MZ8a^dGs&12&3*v+j;e9x%OFC%PXeH!AhzxV2Q
zzWb`;q^S5zC|TRF&wb{ngOw94=NrAKndZf=xpiVxFuxwx&S|Y+gV)vPUwF9O>Yuvs
zfkQ4IzAbB8>|q%=@2trC_se?bu1wD5nmN<!)TKF}1U|97o6!E}|K!^)Ww~n7S0>y_
zwOgIIaY9A(-&1k(WHrjIe_sm!eR*l^t0Q%#7EkypPyBDPe)8W%FKU*t>pk0R?Yvh4
z+RshqOV5*wd>L-0l)Cp>OqKkvM}J?O_<iZ%0qHAsC!FOzl|Av}x_e?)-t^D66PZny
ztlYSGL%zgp_hnISH+_I1?S9VM_LldPosFv8;E1|&@_w|bNoc_}j>Re)53VSmmZj;#
zlDaZgqH9Xy`K#y7a6bPHiST#!KIOpZUGVFh`HCa^w#3b-N=oC&ys-Yu-Su<7FERqh
dSCftJe}<OdGis0i)64?p8&6k1mvv4FO#n3%nPLC{

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter15.png b/Grinder/res/gabor/filter15.png
new file mode 100644
index 0000000000000000000000000000000000000000..1fbe08527e55909b7bdf4c212fe21ec84972a161
GIT binary patch
literal 1595
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv3?%0qQ0W0ujKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WMyDrW(e>JaRrKtii*n0%BrZSn3$MYSy}n}`v(LBgoK2Ig@uKOhet+6#>U3R
z$HylmBqSy#CMPGSrlzK)rDbGfWM*b&XJ_Z+<mBe&=H=z(=jRs`6ciQ~78e(nmX?;4
zl~q?)*Vfk7*VnhTwRLoK^z`)f_V!MnK7H1#S@Y)2n?Ha4qD6}qFJ8QA)vD8{Pk;FE
z;s5{tK=+J-(GZ|p2<#1Q-ww2lv%n*=n1O-sFbFdq&tH)O6qGD+jVKAuPb(=;EJ|f4
zNX*PD(aTFMQ83Xn&@(dpsE|38fq`khr;B4qhV$FoC!-z(2(%?m@NDr=ND^XWlyu$l
z{{OqG&uqUg=9Y`)*6!w+vp(WO^l~BTpW(|Ynhw9*lf?P`d9shzob%}#AO1|8aQ^{U
z<%$0*_x$l{6O>82$t4}J%R?*3B3`2^@8PB=d_cJdttbEI$JF`G)jut+lW=rNc;d^F
zZrhrDI_woKQtMpL$ciiSSDtu1@pbq-6_06!dT|@wwlC^c<g*CNNJ!%kIJ~vv(AUTl
z&T^lso*1RAN|oqM@jA4#?)bVz&1%0a!uK>LsTH#w()9hRUkOq4Z{K-WpJ|zrRZ2^}
zcm7^sv9Oixo%WT2X_oG`ZZD%%J2{W3JW=HT$v<Iss$Q>jn6~DWSK+rlt#Pijs(sXC
zy<*|T2^F=n=MMIzt?$?5`h2MnV(P4}I~IkT4fAi@>gV~Y?K|;OR$*M-!G}kmhMi`g
zB_<J7;Jx8yz%z(Xe(&x7`>)$c>5b|U$D}W+9*Y%&-&yDzJr8D^>ld_AdFibn?Wswt
zr^ZCL**sBP@4l{n{)^|6UE1Sc{pc@aNVjhKU2SUO;??~r#sA~S#OWJ1zkYG6^~)Qt
zO=s*<<vvYl|HFR5RCK3^SMcUjjW?g3(fRDl`+W0}*RR880e!La_RQ(~X3t+&yZT5?
zsfkhHT!<Zhg|&ejKfL*Paq|iG>*+?%E3Q10dE?&pYw9+Smt~gM=e6&vvfCwK82OCz
zRWQWEQ|{lF4LZE|jN}W$Q<wVE^6zaB^;<b@;q}RyoX-#a_S3hEpJtRM1$53Hxt-?q
z^XKY&o5pl+D9JjiQn=5+?~a7|8kG~@#i!`jIRBs24)o`L$4}bR_U&8o;@BOQWFtd;
zS?l@02oSZMe|mlX|Ej0|q!srcl>Ic}e{JQw|7-_-x%^r%t129nqdZ;xT-G@yGywo!
Cz`+y%

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter2.png b/Grinder/res/gabor/filter2.png
new file mode 100644
index 0000000000000000000000000000000000000000..206e8c58ee23a84f2304a3bc0a4e49426abc857f
GIT binary patch
literal 1779
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv3?%0qQ0W0ujKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WMyDrW(e>JaRn)2WMpDuVqsxnWo2b!V`FD$=iuPr<mBYy;^OA!=HcPt<>lq$
z;}Z}N5EK*?5)u*-5fKv;6BiekkdTm)l9H8`m6w-SP*6}-R#sP6*U-?=*4Eb3(=#wI
zu(!8&baZrgclY-84hRT{jg3uAOiWEp&CJZq&(H7d?3^%R!u<L37c5w?a^=e1yLX>I
zfByFE+s~gr|Mcn8|NsAiZW{%oAwbg*xas`p7tjLE0*}aI1_r*vAk26?e?<yVP_o1|
zq9iy!t)x7$D3zfgF*C13FE6!3!9>qM&&cqjLgrLpE(-T_aSX|DetY|R-V-(f)<Da*
zQGKuHT()}t<^TV*1+O_28d;AWII%%)R~*Zx-}f#xa|Ft~diL%9N3T6qPqrR(j1b;+
z<dpuxsmft>2WDMRm7DkH=|RWRQ}OcmI?UPGeb-D4EXcCn_(^#A*RCzg9z_4Va3o5S
zb@?{o@=s=dGeX=yUi*-@$J`<#X>AWvsk=|uyElT9krck_oAGCVQTxXq9ld%Fwm)uu
zIQd6O0oaIFO&VcxW=`9b)@uF;voqNk=GM4Nq072*vAcW9d>;8@&ks-kc=f@8&aFmV
zS6t;TO)bCVb|P$vDeL`H?P}LfX#|K@ddRozx#;bla$nM}d->t=kB9NA&i{3@{?3b5
z<xTJT<Bxnl?EmpIe)aM#e~v!<sk}*^H~x74;rx%k@hi{&W%_vq_rsk#Bvb6=_I1X$
z**{u;&`~%$RpiPtEB=?KtTPvv-b4zStt-zt|7v*h)^Qs<|NUd@56}Pj9lr(Vi>^CY
zZ;CZrw@^<b{JL#?mRZ#FwI11A`=;`PVkuzuE1M<S@0avmOVz$KtLc5!&gz|cZ|*#8
z%`tC&$FtkF<b1_n-X-hjZV}uhn(2FR`=^EQp#AP%EzXqk+xdR{Ut6XRHV^K}@s(hP
zcKAI{X~pP?*F<WrpSG=icil(%#J3-}HouX}sXkTvS2;i4I#W3XWOe>iXOOp7bc5WJ
zzF#oqcgCgXQ))9_3ZJR|ZLxVWTBM(F-u21v76&+NcAHn4H09{usrbDtys+xShV|;M
zV57JfMO5B^Dv8TG{7`Um+_Eoesp})Fo`3o!ZctnLt;5qbD)__N8jOUv+x%zeWw-xI
z0eAm7yRHGtHwEABY*(*myLz{l|Kt5Xr*9ZvI3=iUpWypKNPB-vSl^DIy6WHj88i2+
UGg<Y1A1L>Gy85}Sb4q9e06cFIL;wH)

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter3.png b/Grinder/res/gabor/filter3.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec9e63cb3ba44ad7bceb652523bb3acaf906e2f5
GIT binary patch
literal 1497
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv3?%0qQ0W0ujKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WMyDrW(e>JaRrLAv9XDXiRtL*I6FIshK9z)#l^?RCnhGQrKM$NX6EMR=H=z(
z=jRs`6ciQ~78e(nl$4Z~mR43)R##UyG&Hofwsv-QPMtb+!GZ-VSFT*MX3dEcCmuh3
z{Nu-u|Ns93%^wA$Auwn|KzVCH5zuj*1s;*b3=DjSL74G){)!Z!pk#?_L`iUdT1k0g
zQ7S`0VrE{6US4X6f{C7io{`~4h0Li83{1A3E{-7?&Tns@_PdiH;F2gb$${x)!D-f_
zhkx%2t-PXRm#Nd!BC7xD&8@q!>8T%YC;RcOe-u6Ak(Q5Ry6V*REUr%*&v}*Uxfe<<
z+rb%pAu)aCsp*OwKv6#H(=t<wZ!h>Ev}^}AP@>&rg^YwcL}tmse^25Mi)>Hq`fPBt
z-+)!N%%k~KV_{Q3h}??}&QdZ{<6AWMwQJh7Y1XxBf<!|2dW2gReR61*y_6tc)K~MT
z^_S$b9|FsM$SzxOm@l(J>$%cnmW3;9K-Sot|Mc46=yL;B^DCJ%9{JB`;<H}zaOIqC
zxCtNLIGtnWYd`qg;3yE?2U~wQ*iu{)=DLTLTK3$*HZ7X>S~dSc95C<H&Jz}n8e04%
zHxzl#|9dk3kcf=1{~?k7L@uj06WW#+=^6P6be&=b`s8qN$horza}V=wJ-hZ8T#x_L
zn3OpRBDYUiekzoBGGoVC#z}Dh&O23^@w{*zdu<%!o|~r`k&J@4>geo~m7-_8v*O}E
zC(TY)0@;D$+{$Up_70yetJp^sIbXyG=$f#@?B+6T+kbkr!)*-Vt1&vs%vZl~+fRnc
zNTR?nN|LrLc=C8YdjM)&?$Ke-lNKon3;O8=ilsGA>`(ExtMmBOcd1k!&;sQTPgg&e
IbxsLQ0GV4-`2YX_

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter4.png b/Grinder/res/gabor/filter4.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4f16a4123bbe42af5425574d94686cba9c73c63
GIT binary patch
literal 1310
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2W0TeV#6kAr*0N?-+7vgbKJGT)%^Xxi~!a)wY;*QX(ZaY&9O2r<&g>zts5i
z;~D??fBtknK5@Z!k4}(bk&NW7&grgNZhgwm<{vkC96eE(@<J_6Y^8){AJ5$*r<FvN
zk561UyCQN)(i01#5@G%BkRBQLw!BBF6T2o-+mK)TpB5fnKmX6allk-iTsrx&Z9;1o
z(3-A^6Ix~RIU-j`tOP>N$P+_MBJgLHkY~`Jk0;~j|M}VZtySittGts~k&AdON8|~?
z*e{AHitZ&B+ku{>zC^s;p1-HlH#+sdc<&F4H?XY|Q6O6rGr+bk1lvjziP}Fg&$B}x
z&W>2({v<m7$?hcDc@r}zC)QsJ{Wl>zL;k0Zn5WRs8DT>$)!<I#`=74%d{RjD+((si
f;8*<bkB{Y*;!T>><LpiYi!TOGS3j3^P6<r_d3-9_

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter5.png b/Grinder/res/gabor/filter5.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9fb4e7aa2d069b55a04cbb3b8cea62ae2fce22b
GIT binary patch
literal 921
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2Qj-W=>BR$B>G+w|6%7Mg>Z+CQ22uiydUkJL$TkBH!pz=X1~LyARfI3jX3(
z-lSf!w|?7w`6DMkciGLcS^wfNRT%FROVXtF+r2oi94}|NWD&n2Z;^+qypz<E30-zA
zCMOg{YdJC}IBWe9xTMe?@<nlpBj3sw&K}J&K^NO6u#usm{o>DpW3MOf@BdTX@{KKC
z18iCs*fi0lAnUcfK-Py$0a?F717v+bAjtX!i@?^qlBwZRcX;OnHrplt`#+?0E(E*W
z8SK?Iuvd9ZL0*-}0(sTo3dpO8D?na7902wz1seDlf8KKH!OR&h{GI8wLW;ce;%MVR
zEI}yu??QL|3F8d?j}Psd^rui7b;Kl&mx*84XZ#eL^JisOo#RD)jbE&;`xm?r7x~+?
lC_dl|H+I2)h3~(cuUe@ZUEeEy5SViqJYD@<);T3K0RR*6dOrXF

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter6.png b/Grinder/res/gabor/filter6.png
new file mode 100644
index 0000000000000000000000000000000000000000..5073215f98572823eb472c52d3ccc4e615843bfd
GIT binary patch
literal 1565
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv3?%0qQ0W0ujKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WMyDrW(e>JaRn)2WMpDuVqsxnWo2b!V`FD$=iuPr<mBYy;^OA!=HcPt<KyG!
z=NAwV5E2p+6BCn=kdTy=l#-H?mX?;4m6ey5S5{V5QBl#>*4ES0GcYhPHa0dfF|oI|
zcXM;|^z;l44h{_sO-@cuNlD4g&8@4etFN!`?ChK~XU_8F%XjbIy?_7y3l}atd-m+@
z+qeJ!{|7p06pV%deL`UElhgBnHgOhsL>4nJ@ErzW#^d=bQh<VzC9V-A!TD(=<%vb9
z3<Zgqc_n&zsU->~dIow%h94C&r!p`wHF~-@hGaOuy?rw5QG!5QVyZw#LWj%DlrT2O
zm%sP#x@Y5Qv+JGCt*Y`l4)<<-n7iuK{MmX|thrk*wDhmNCR_F5@k49o9ryO@dkB1*
z(7s2`GWBInv8nX^1$kR7sx^(CuqyLBtX?U#O3O*^(}e#_AnALL*zWMIoT3tQlg0Q9
z_cDpcf>|q8aa5jgj_Z#p+M*%FdrsQ)PLP^EEBBg>N0%kEimyo2DQ<I?J*V(Qk-yTm
z=;Eiv<r>aUB~LfW@>wLsf8a7*lX5Pk-%x61h8)lqu6Gmu3-9b%`9R0HwdiM9!68ZU
zJwPM$i_NNfcFt5fztj02>$?f<fBsLtojUFGnR%5em)Gk&x!L!r<ktbw`2mNYDt+v;
znzObu?b)%&w1k)KHcu4SyZc4H6Au2})>)adHr}#h%IBG%f3e%e#oEpP```gvw)q@^
zC7P-F5a<2>zVCniG|!fqEjPS+7PIV(NjatdJoH)3i3OKEwcakda;Vq%RGk01yRBe@
z*VWfwIRAVnU;Vnr_h&GCQ)~KOZW`lqYUZbuWfj%NsXD&0{>vsD2?-JehVCEnleuc{
z%DYdky!-T&-se!>-+PZ(zY6F3l;gfL=GJt(VzqB8Ma|Zood9(6*AuppG8LCBWU9Vd
zeAD^l%Bwwd^UW6p+n!C_T9>LAv?}1E&6neh9iNs2pKzA@RP}^y+P<ZZv#h+otZ3VK
zX67ryXT`#2&+JP~k~^ufI^dxWzeC5gw9B0<${?!!{@q)>aFx?Dp05(h#;)^<Q}>0x
z6IMMweX`#Iow>Q{bF6_8b)vp8MoSeKwHF@p_?U>U`)+wURqgBf@U<G=>Bo0R-37VQ
d^u+GF`&)j`sI7H6dj^zYJYD@<);T3K0RY#LmKp#6

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter7.png b/Grinder/res/gabor/filter7.png
new file mode 100644
index 0000000000000000000000000000000000000000..46d300f5edd01d66228b8a8a9b3b1b42cf589bcb
GIT binary patch
literal 1596
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv3?%0qQ0W0ujKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WMyDrW(e>JaRrKtii*n0%BrZSn3$MYSy}n}`v(LBgoK2Ig@uKOhet+6#>U3R
z$HylmBqSy#CMPGSrlzK)rDbGfWM*b&XJ_Z+<mBe&=H=z(=jRs`6ciQ~78e(nmX?;4
zl~q?)*Vfk7*VnhTwRLoK^z`)f_V!MnK7H1#S@Y)2n?Ha4qD6}qFJ8QA)vD8{Pk;FE
z;s5{tK=+J-(GZ|p2<#1Q-ww2lv%n*=n1O-sFbFdq&tH)O6qGD+jVKAuPb(=;EJ|f4
zNX*PD(aTFMQ83Xn&@(dpsE|38fq`j(r;B4qhV$For^6lv2)HFqn5{9<p+!Z3sYvu<
z-T&>c<!TJGw_bk7v*laQggLRo|E{cTx%MZYOJ_oIlwsrJpIoQAB;_7HKJ(L5as5NS
z$`k)r?^(0qP!O+c*OEgYle$eebs5&2(TO=M`)R`eM(Zc{!}FFlZJK8HKu%BRG;>)%
zjMa*Olus?`JND_Q-qU%a$X{t(^!F4{#l3^yyMvDkDwkYbf8zmbR>dUC$YqBsc`8ph
z-|M+kYdc*eQ!DtLrTF?CAv_n|_<whquSirb7P387p_7~jH08wXiPGjib0?_e{aET@
z85ozR9rfZt&v!1<H>rN{{e}XDk;`%;+iadF{%0)Oe5TWGnZ_G;zF$)p+eim(i!$Hw
zh&jt*zG|F*`(@F&D>;u<<;Lye0$UxUJXu;jZ~HS|>)#V6)-BZyvXVXbTK`AI*V5&W
zo3^pKiQN&****oRRPIySle+iX`~QE9Qd%OkNZYVV=t<2qP5C+DGmDj*qjXoAObK#d
zSvn!fRq?tS#KEucPyh8(=ki1Tl0UW94SQt|{@yKhPHCz0%}vX9?A>~L$BoiCZ=Ae#
zPg2?FKNITo$tPx>d{XhI<;a_&<Y_;bwf&8I^!1B&Zl&3hJuzpz<InnwKU?{Ik+H9-
z5!6ZZbnLoj=zM<j@#N+c;-)z%e*4!SetE0E=Zm)c#3HLgJsbYLpKHE2$_MX{ll(NH
zeUIeMo|!uzPG4HyIq&KR6;J>tC};+KZkzk}#KEtrn!qTqUwUiWFWwas+HIbkJ#qJX
zvG}zUbM*E3)72!hA0JsemCIGF^HI=o`=w%f-uV{if#D*xYC?VdPj#X9r|-{Ovms{I
z^mTLQ&rbF0kzN*jy!h{_=dY(u0i`8ic>mv9WA~r;z%Q3y@y7K`pe*I->gTe~DWM4f
D(F4BH

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter8.png b/Grinder/res/gabor/filter8.png
new file mode 100644
index 0000000000000000000000000000000000000000..28638380ca58be737358b33e8b75eb59df04f626
GIT binary patch
literal 1165
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2Qj-=D(gUjv*CsZ|@kc3JDZ&y|~`b!N9aAtnc)NgISvvJc>Q&x+E-aPjUIJ
z3-bSW_-pr_@9UcQc=O_<CqLe-kg&9~)^ejz=>M7N?_Qt!E_!_8!d995A88L$CjxCa
zalxt2yk_%<O&&*26sBY-9+%!1SrNG;>4}BWmI;q}@9XT*2{J5_k&HU=u`ORLPHd%w
zWgn03lZwOU-1^-iJu>cXVnur%&TgIV3Ur{dGq>fv4`&aZRuWaFr!`aWM=i>~dwy!X
zHrNS^y6u2Y?9lsVuw{bst`~=aUVO2LALusfOW4`1|MQ}5{h!D}P!I{meo4$wbT7Hs
z4h-UpuJXXJaS^YTpoxV2bzYkI2xqAMNsoOh|B37WVey~m_Yd8)f;;8cAIsOJZk7XQ
f-}_%aeiW}i!G3yLx=Jvxv|#Xb^>bP0l+XkKU|bPo

literal 0
HcmV?d00001

diff --git a/Grinder/res/gabor/filter9.png b/Grinder/res/gabor/filter9.png
new file mode 100644
index 0000000000000000000000000000000000000000..c659e0778fddc03f94a1e2da3bd44725bc6cf138
GIT binary patch
literal 719
zcmeAS@N?(olHy`uVBq!ia0vp^H6YBv1SC~zoL>Sd#^NA%Cx&(BWI!C2bVpxD28NCO
z+<y{Tfqc#akH}&M2EM}}%y>M1MG8<*vcxr_Bsf2<q&%@Gm7yRpGp|H1FSSI$M9)Ca
z$nc{==2Qj-CVNj8$B>G+w|6#nrZ@_)Tr_;36Om})eD1z!u|KDV<-Y^Vg#7hAoN`}W
zx8A?*1+%NX#xIA9>|J&NUm6$ji_|W_D#X9IzOr7f()Q5CBLU4aK^OU*d5b(;?RiX3
zD2o0yxT4S=^5rmfB`zI)@zaH%yP&R*tC#=zyw?8HXO81SG<O+_@4v>F;|)0E;BCM7
zQ}O%*dAm;g|0;c|ZBkDrbk#{@O>oxwmAJx@Z{>^jg>05f7RggtLeA~HI)T`QIr87@
bd++~i@y4t=U*aeLOm7UHu6{1-oD!M<r~nzp

literal 0
HcmV?d00001

diff --git a/Grinder/ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.cpp b/Grinder/ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.cpp
new file mode 100644
index 0000000..188c7c5
--- /dev/null
+++ b/Grinder/ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.cpp
@@ -0,0 +1,22 @@
+/******************************************************************************
+ * File: RandomForestFeaturesPropertyEditor.cpp
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "RandomForestFeaturesPropertyEditor.h"
+#include "ui/ml/rf/RandomForestFeaturesDialog.h"
+
+RandomForestFeaturesPropertyEditor::RandomForestFeaturesPropertyEditor(RandomForestFeaturesProperty* property, QWidget* parent) : DialogPropertyEditor(property, parent)
+{
+
+}
+
+void RandomForestFeaturesPropertyEditor::invokeDialog()
+{
+	RandomForestFeaturesDialog dlg{&_property->object(), this};
+	dlg.exec();
+
+	// The features have been modified, so notify the property
+	_property->objectModified();
+}
diff --git a/Grinder/ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.h b/Grinder/ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.h
new file mode 100644
index 0000000..3a288e5
--- /dev/null
+++ b/Grinder/ui/ml/editors/rf/RandomForestFeaturesPropertyEditor.h
@@ -0,0 +1,26 @@
+/******************************************************************************
+ * File: RandomForestFeaturesPropertyEditor.h
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#ifndef RANDOMFORESTFEATURESPROPERTYEDITOR_H
+#define RANDOMFORESTFEATURESPROPERTYEDITOR_H
+
+#include "ui/properties/editors/DialogPropertyEditor.h"
+#include "ml/rf/properties/RandomForestFeaturesProperty.h"
+
+namespace grndr
+{
+	class RandomForestFeaturesPropertyEditor : public DialogPropertyEditor<RandomForestFeaturesProperty>
+	{
+		Q_OBJECT
+
+	public:
+		RandomForestFeaturesPropertyEditor(RandomForestFeaturesProperty* property, QWidget *parent = nullptr);
+
+	public:
+		virtual void invokeDialog() override;
+	};
+}
+
+#endif
diff --git a/Grinder/ui/ml/rf/RandomForestFeaturesDialog.cpp b/Grinder/ui/ml/rf/RandomForestFeaturesDialog.cpp
new file mode 100644
index 0000000..b3d9eb1
--- /dev/null
+++ b/Grinder/ui/ml/rf/RandomForestFeaturesDialog.cpp
@@ -0,0 +1,216 @@
+/******************************************************************************
+ * File: RandomForestFeaturesDialog.cpp
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "RandomForestFeaturesDialog.h"
+#include "ui_RandomForestFeaturesDialog.h"
+
+RandomForestFeaturesDialog::RandomForestFeaturesDialog(RandomForestFeatures* features, QWidget* parent) : QDialog(parent, Qt::Dialog|Qt::WindowTitleHint|Qt::WindowCloseButtonHint),
+	ui{new Ui::RandomForestFeaturesDialog}, _features{features}
+{
+	if (!features)
+		throw std::invalid_argument{_EXCPT("features may not be null")};
+
+	setupUi();
+	updateUi(false);
+}
+
+RandomForestFeaturesDialog::~RandomForestFeaturesDialog()
+{
+	delete ui;
+}
+
+void RandomForestFeaturesDialog::accept()
+{
+	if (ui->chkGabor->isChecked())
+	{
+		if (getSelectedGaborFilters() == 0)
+		{
+			QMessageBox::warning(nullptr, "Gabor filters", "Please select at least one Gabor filter to use.");
+			return;
+		}
+	}
+
+	updateUi(true);
+	QDialog::accept();
+}
+
+void RandomForestFeaturesDialog::setupUi()
+{
+	ui->setupUi(this);
+
+	fillGaborFilters();
+	fillMatrixTypes(ui->lstTypeHessian);
+	fillMatrixTypes(ui->lstTypeStructureTensor);
+}
+
+void RandomForestFeaturesDialog::updateUi(bool save)
+{
+	std::map<RandomForestFeatures::Feature*, QCheckBox*> checkBoxes;
+
+	checkBoxes[&_features->gaborFilters()] = ui->chkGabor;
+	checkBoxes[&_features->sobel()] = ui->chkSobel;
+	checkBoxes[&_features->gaussian()] = ui->chkGaussian;
+	checkBoxes[&_features->color()] = ui->chkColor;
+	checkBoxes[&_features->differenceOfGaussians()] = ui->chkDoG;
+	checkBoxes[&_features->laplacianOfGaussians()] = ui->chkLoG;
+	checkBoxes[&_features->hessian()] = ui->chkHessian;
+	checkBoxes[&_features->structureTensor()] = ui->chkStructureTensor;
+
+	if (save)
+	{
+		for (auto it : checkBoxes)
+			it.first->isActive = it.second->isChecked();
+
+		_features->gaborFilters().activeFilters = getSelectedGaborFilters();
+
+		_features->gaussian().stdDeviation = ui->txtStdDevGaussian->value();
+		_features->color().stdDeviation = ui->txtStdDevColor->value();
+		_features->differenceOfGaussians().stdDeviation1 = ui->txtStdDev1DoG->value();
+		_features->differenceOfGaussians().stdDeviation2 = ui->txtStdDev2DoG->value();
+		_features->laplacianOfGaussians().stdDeviation = ui->txtStdDevLoG->value();
+		_features->hessian().stdDeviation = ui->txtStdDevHessian->value();
+		_features->structureTensor().stdDeviation = ui->txtStdDevStructureTensor->value();
+
+		_features->hessian().matrixType = getSelectedMatrixType(ui->lstTypeHessian);
+		_features->structureTensor().matrixType = getSelectedMatrixType(ui->lstTypeStructureTensor);
+	}
+	else
+	{
+		for (auto it : checkBoxes)
+			it.second->setChecked(it.first->isActive);
+
+		on_chkGabor_stateChanged();
+		on_chkGaussian_stateChanged();
+		on_chkColor_stateChanged();
+		on_chkDoG_stateChanged();
+		on_chkLoG_stateChanged();
+		on_chkHessian_stateChanged();
+		on_chkStructureTensor_stateChanged();
+
+		setSelectedGaborFilters(_features->gaborFilters().activeFilters);
+
+		ui->txtStdDevGaussian->setValue(_features->gaussian().stdDeviation);
+		ui->txtStdDevColor->setValue(_features->color().stdDeviation);
+		ui->txtStdDev1DoG->setValue(_features->differenceOfGaussians().stdDeviation1);
+		ui->txtStdDev2DoG->setValue(_features->differenceOfGaussians().stdDeviation2);
+		ui->txtStdDevLoG->setValue(_features->laplacianOfGaussians().stdDeviation);
+		ui->txtStdDevHessian->setValue(_features->hessian().stdDeviation);
+		ui->txtStdDevStructureTensor->setValue(_features->structureTensor().stdDeviation);
+
+		setSelectedMatrixType(ui->lstTypeHessian, _features->hessian().matrixType);
+		setSelectedMatrixType(ui->lstTypeStructureTensor, _features->structureTensor().matrixType);
+	}
+}
+
+void RandomForestFeaturesDialog::fillGaborFilters()
+{
+	for (int i = 0; i < 16; ++i)
+	{
+		auto item = new QListWidgetItem(QIcon{QString{":/gabor/gabor/filter%1.png"}.arg(i)}, QString{"Filter %1"}.arg(i + 1), ui->lstGabor);
+		item->setCheckState(Qt::Unchecked);
+		item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsUserCheckable);
+	}
+}
+
+void RandomForestFeaturesDialog::fillMatrixTypes(QComboBox* comboBox)
+{
+	comboBox->addItem("Matrix", static_cast<int>(RandomForestFeatures::MatrixFeatureType::Raw));
+	comboBox->addItem("Eigenvalues", static_cast<int>(RandomForestFeatures::MatrixFeatureType::Eigen));
+	comboBox->addItem("Both", static_cast<int>(RandomForestFeatures::MatrixFeatureType::Both));
+}
+
+unsigned short RandomForestFeaturesDialog::getSelectedGaborFilters() const
+{
+	unsigned short filters = 0;
+
+	for (int i = 0; i < ui->lstGabor->count(); ++i)
+	{
+		if (ui->lstGabor->item(i)->checkState() == Qt::Checked)
+			filters |= 1 << i;
+	}
+
+	return filters;
+}
+
+void RandomForestFeaturesDialog::setSelectedGaborFilters(unsigned short filters)
+{
+	for (int i = 0; i < ui->lstGabor->count(); ++i)
+	{
+		if (filters & (1 << i))
+			ui->lstGabor->item(i)->setCheckState(Qt::Checked);
+		else
+			ui->lstGabor->item(i)->setCheckState(Qt::Unchecked);
+	}
+}
+
+RandomForestFeatures::MatrixFeatureType RandomForestFeaturesDialog::getSelectedMatrixType(QComboBox* comboBox) const
+{
+	return static_cast<RandomForestFeatures::MatrixFeatureType>(comboBox->currentData().toInt());
+}
+
+void RandomForestFeaturesDialog::setSelectedMatrixType(QComboBox* comboBox, RandomForestFeatures::MatrixFeatureType type)
+{
+	for (int i = 0; i < comboBox->count(); ++i)
+	{
+		if (comboBox->itemData(i).toInt() == static_cast<int>(type))
+		{
+			comboBox->setCurrentIndex(i);
+			break;
+		}
+	}
+}
+
+void RandomForestFeaturesDialog::on_chkGabor_stateChanged(int checkState)
+{
+	Q_UNUSED(checkState);
+
+	ui->lstGabor->setEnabled(ui->chkGabor->isChecked());
+}
+
+void RandomForestFeaturesDialog::on_chkGaussian_stateChanged(int checkState)
+{
+	Q_UNUSED(checkState);
+
+	ui->txtStdDevGaussian->setEnabled(ui->chkGaussian->isChecked());
+}
+
+void RandomForestFeaturesDialog::on_chkColor_stateChanged(int checkState)
+{
+	Q_UNUSED(checkState);
+
+	ui->txtStdDevColor->setEnabled(ui->chkColor->isChecked());
+}
+
+void RandomForestFeaturesDialog::on_chkDoG_stateChanged(int checkState)
+{
+	Q_UNUSED(checkState);
+
+	ui->txtStdDev1DoG->setEnabled(ui->chkDoG->isChecked());
+	ui->txtStdDev2DoG->setEnabled(ui->chkDoG->isChecked());
+}
+
+void RandomForestFeaturesDialog::on_chkLoG_stateChanged(int checkState)
+{
+	Q_UNUSED(checkState);
+
+	ui->txtStdDevLoG->setEnabled(ui->chkLoG->isChecked());
+}
+
+void RandomForestFeaturesDialog::on_chkHessian_stateChanged(int checkState)
+{
+	Q_UNUSED(checkState);
+
+	ui->txtStdDevHessian->setEnabled(ui->chkHessian->isChecked());
+	ui->lstTypeHessian->setEnabled(ui->chkHessian->isChecked());
+}
+
+void RandomForestFeaturesDialog::on_chkStructureTensor_stateChanged(int checkState)
+{
+	Q_UNUSED(checkState);
+
+	ui->txtStdDevStructureTensor->setEnabled(ui->chkStructureTensor->isChecked());
+	ui->lstTypeStructureTensor->setEnabled(ui->chkStructureTensor->isChecked());
+}
diff --git a/Grinder/ui/ml/rf/RandomForestFeaturesDialog.h b/Grinder/ui/ml/rf/RandomForestFeaturesDialog.h
new file mode 100644
index 0000000..e56d375
--- /dev/null
+++ b/Grinder/ui/ml/rf/RandomForestFeaturesDialog.h
@@ -0,0 +1,61 @@
+/******************************************************************************
+ * File: RandomForestFeaturesDialog.h
+ * Date: 13.1.2020
+ *****************************************************************************/
+
+#ifndef RANDOMFORESTFEATURESDIALOG_H
+#define RANDOMFORESTFEATURESDIALOG_H
+
+#include <QDialog>
+#include <QComboBox>
+
+#include "ml/rf/RandomForestFeatures.h"
+
+namespace Ui
+{
+	class RandomForestFeaturesDialog;
+}
+
+namespace grndr
+{
+	class RandomForestFeaturesDialog : public QDialog
+	{
+		Q_OBJECT
+
+	public:
+		RandomForestFeaturesDialog(RandomForestFeatures* features, QWidget *parent = nullptr);
+		~RandomForestFeaturesDialog();
+
+	public:
+		virtual void accept() override;
+
+	private:
+		void setupUi();
+		void updateUi(bool save);
+		Ui::RandomForestFeaturesDialog *ui;
+
+	private:
+		void fillGaborFilters();
+		void fillMatrixTypes(QComboBox* comboBox);
+
+		unsigned short getSelectedGaborFilters() const;
+		void setSelectedGaborFilters(unsigned short filters);
+
+		RandomForestFeatures::MatrixFeatureType getSelectedMatrixType(QComboBox* comboBox) const;
+		void setSelectedMatrixType(QComboBox* comboBox, RandomForestFeatures::MatrixFeatureType type);
+
+	private slots:
+		void on_chkGabor_stateChanged(int checkState = 0);
+		void on_chkGaussian_stateChanged(int checkState = 0);
+		void on_chkColor_stateChanged(int checkState = 0);
+		void on_chkDoG_stateChanged(int checkState = 0);
+		void on_chkLoG_stateChanged(int checkState = 0);
+		void on_chkHessian_stateChanged(int checkState = 0);
+		void on_chkStructureTensor_stateChanged(int checkState = 0);
+
+	private:
+		RandomForestFeatures* _features{nullptr};
+	};
+}
+
+#endif
diff --git a/Grinder/ui/ml/rf/RandomForestFeaturesDialog.ui b/Grinder/ui/ml/rf/RandomForestFeaturesDialog.ui
new file mode 100644
index 0000000..803bee1
--- /dev/null
+++ b/Grinder/ui/ml/rf/RandomForestFeaturesDialog.ui
@@ -0,0 +1,734 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>RandomForestFeaturesDialog</class>
+ <widget class="QDialog" name="RandomForestFeaturesDialog">
+  <property name="windowModality">
+   <enum>Qt::ApplicationModal</enum>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>411</width>
+    <height>730</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Random Forest features</string>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_4">
+   <property name="sizeConstraint">
+    <enum>QLayout::SetFixedSize</enum>
+   </property>
+   <item row="0" column="0">
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="title">
+      <string>Image features</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout_8">
+      <item row="5" column="0" colspan="3">
+       <widget class="QWidget" name="widget_4" native="true">
+        <layout class="QGridLayout" name="gridLayout_5">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item row="1" column="2">
+          <widget class="QDoubleSpinBox" name="txtStdDevLoG">
+           <property name="minimum">
+            <double>0.010000000000000</double>
+           </property>
+           <property name="maximum">
+            <double>999999.000000000000000</double>
+           </property>
+           <property name="singleStep">
+            <double>0.100000000000000</double>
+           </property>
+           <property name="value">
+            <double>0.500000000000000</double>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <spacer name="horizontalSpacer_8">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeType">
+            <enum>QSizePolicy::Fixed</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>18</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="1" column="1">
+          <widget class="QLabel" name="label_5">
+           <property name="text">
+            <string>Standard deviation:</string>
+           </property>
+           <property name="buddy">
+            <cstring>txtStdDevLoG</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="3">
+          <spacer name="horizontalSpacer_9">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="0" column="0" colspan="4">
+          <widget class="QCheckBox" name="chkLoG">
+           <property name="text">
+            <string>&amp;Laplacian of Gaussians</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QCheckBox" name="chkSobel">
+        <property name="text">
+         <string>&amp;Sobel</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="3" column="0" colspan="3">
+       <widget class="QWidget" name="widget_2" native="true">
+        <layout class="QGridLayout" name="gridLayout_3">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item row="1" column="2">
+          <widget class="QDoubleSpinBox" name="txtStdDevColor">
+           <property name="minimum">
+            <double>0.010000000000000</double>
+           </property>
+           <property name="maximum">
+            <double>999999.000000000000000</double>
+           </property>
+           <property name="singleStep">
+            <double>0.100000000000000</double>
+           </property>
+           <property name="value">
+            <double>0.500000000000000</double>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <spacer name="horizontalSpacer_4">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeType">
+            <enum>QSizePolicy::Fixed</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>18</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="1" column="1">
+          <widget class="QLabel" name="label_2">
+           <property name="text">
+            <string>Standard deviation:</string>
+           </property>
+           <property name="buddy">
+            <cstring>txtStdDevColor</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="3">
+          <spacer name="horizontalSpacer_5">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="0" column="0" colspan="4">
+          <widget class="QCheckBox" name="chkColor">
+           <property name="text">
+            <string>&amp;Color</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="4" column="0" colspan="3">
+       <widget class="QWidget" name="widget_3" native="true">
+        <layout class="QGridLayout" name="gridLayout">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item row="1" column="0">
+          <spacer name="horizontalSpacer_6">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeType">
+            <enum>QSizePolicy::Fixed</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>18</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="1" column="1">
+          <widget class="QLabel" name="label_3">
+           <property name="text">
+            <string>Standard deviation 1:</string>
+           </property>
+           <property name="buddy">
+            <cstring>txtStdDev1DoG</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="2">
+          <widget class="QDoubleSpinBox" name="txtStdDev1DoG">
+           <property name="minimum">
+            <double>0.010000000000000</double>
+           </property>
+           <property name="maximum">
+            <double>999999.000000000000000</double>
+           </property>
+           <property name="singleStep">
+            <double>0.100000000000000</double>
+           </property>
+           <property name="value">
+            <double>0.500000000000000</double>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="3">
+          <spacer name="horizontalSpacer_7">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="2" column="1">
+          <widget class="QLabel" name="label_4">
+           <property name="text">
+            <string>Standard deviation 2:</string>
+           </property>
+           <property name="buddy">
+            <cstring>txtStdDev2DoG</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="2">
+          <widget class="QDoubleSpinBox" name="txtStdDev2DoG">
+           <property name="minimum">
+            <double>0.010000000000000</double>
+           </property>
+           <property name="maximum">
+            <double>999999.000000000000000</double>
+           </property>
+           <property name="singleStep">
+            <double>0.100000000000000</double>
+           </property>
+           <property name="value">
+            <double>0.500000000000000</double>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="0" colspan="4">
+          <widget class="QCheckBox" name="chkDoG">
+           <property name="text">
+            <string>&amp;Difference of Gaussians</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="0" colspan="2">
+       <widget class="QWidget" name="widget_7" native="true">
+        <layout class="QGridLayout" name="gridLayout_9">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item row="1" column="1">
+          <widget class="QListWidget" name="lstGabor">
+           <property name="editTriggers">
+            <set>QAbstractItemView::NoEditTriggers</set>
+           </property>
+           <property name="selectionMode">
+            <enum>QAbstractItemView::NoSelection</enum>
+           </property>
+           <property name="iconSize">
+            <size>
+             <width>48</width>
+             <height>48</height>
+            </size>
+           </property>
+           <property name="viewMode">
+            <enum>QListView::IconMode</enum>
+           </property>
+           <property name="uniformItemSizes">
+            <bool>true</bool>
+           </property>
+           <property name="itemAlignment">
+            <set>Qt::AlignCenter</set>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <spacer name="horizontalSpacer_14">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeType">
+            <enum>QSizePolicy::Fixed</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>18</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="0" column="0" colspan="2">
+          <widget class="QCheckBox" name="chkGabor">
+           <property name="text">
+            <string>Gabor &amp;filters</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="6" column="0" colspan="3">
+       <widget class="QWidget" name="widget_5" native="true">
+        <layout class="QGridLayout" name="gridLayout_6">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item row="5" column="1" colspan="2">
+          <widget class="QLabel" name="label_7">
+           <property name="text">
+            <string>Standard deviation:</string>
+           </property>
+           <property name="buddy">
+            <cstring>txtStdDevHessian</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="5" column="3">
+          <widget class="QDoubleSpinBox" name="txtStdDevHessian">
+           <property name="minimum">
+            <double>0.010000000000000</double>
+           </property>
+           <property name="maximum">
+            <double>999999.000000000000000</double>
+           </property>
+           <property name="singleStep">
+            <double>0.100000000000000</double>
+           </property>
+           <property name="value">
+            <double>0.500000000000000</double>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="0">
+          <spacer name="horizontalSpacer_10">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeType">
+            <enum>QSizePolicy::Fixed</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>18</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="5" column="4">
+          <spacer name="horizontalSpacer_12">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>162</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="2" column="3" colspan="2">
+          <widget class="QComboBox" name="lstTypeHessian"/>
+         </item>
+         <item row="2" column="1" colspan="2">
+          <widget class="QLabel" name="label_6">
+           <property name="text">
+            <string>Type:</string>
+           </property>
+           <property name="buddy">
+            <cstring>lstTypeHessian</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="0" colspan="4">
+          <widget class="QCheckBox" name="chkHessian">
+           <property name="text">
+            <string>&amp;Hessian</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="7" column="0" colspan="3">
+       <widget class="QWidget" name="widget_6" native="true">
+        <layout class="QGridLayout" name="gridLayout_7">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item row="2" column="0">
+          <spacer name="horizontalSpacer_11">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeType">
+            <enum>QSizePolicy::Fixed</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>18</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="5" column="1">
+          <widget class="QLabel" name="label_8">
+           <property name="text">
+            <string>Standard deviation:</string>
+           </property>
+           <property name="buddy">
+            <cstring>txtStdDevStructureTensor</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="1">
+          <widget class="QLabel" name="label_9">
+           <property name="text">
+            <string>Type:</string>
+           </property>
+           <property name="buddy">
+            <cstring>lstTypeStructureTensor</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="0" colspan="5">
+          <widget class="QCheckBox" name="chkStructureTensor">
+           <property name="text">
+            <string>&amp;Structure Tensor</string>
+           </property>
+          </widget>
+         </item>
+         <item row="2" column="2" colspan="3">
+          <widget class="QComboBox" name="lstTypeStructureTensor"/>
+         </item>
+         <item row="5" column="2">
+          <widget class="QDoubleSpinBox" name="txtStdDevStructureTensor">
+           <property name="minimum">
+            <double>0.010000000000000</double>
+           </property>
+           <property name="maximum">
+            <double>999999.000000000000000</double>
+           </property>
+           <property name="singleStep">
+            <double>0.100000000000000</double>
+           </property>
+           <property name="value">
+            <double>0.500000000000000</double>
+           </property>
+          </widget>
+         </item>
+         <item row="5" column="3" colspan="2">
+          <spacer name="horizontalSpacer_13">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>34</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="2" column="0" colspan="3">
+       <widget class="QWidget" name="widget" native="true">
+        <layout class="QGridLayout" name="gridLayout_2">
+         <property name="leftMargin">
+          <number>0</number>
+         </property>
+         <property name="topMargin">
+          <number>0</number>
+         </property>
+         <property name="rightMargin">
+          <number>0</number>
+         </property>
+         <property name="bottomMargin">
+          <number>0</number>
+         </property>
+         <item row="1" column="2">
+          <widget class="QDoubleSpinBox" name="txtStdDevGaussian">
+           <property name="minimum">
+            <double>0.010000000000000</double>
+           </property>
+           <property name="maximum">
+            <double>999999.000000000000000</double>
+           </property>
+           <property name="singleStep">
+            <double>0.100000000000000</double>
+           </property>
+           <property name="value">
+            <double>0.500000000000000</double>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="0">
+          <spacer name="horizontalSpacer_2">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeType">
+            <enum>QSizePolicy::Fixed</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>18</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="1" column="1">
+          <widget class="QLabel" name="label">
+           <property name="text">
+            <string>Standard deviation:</string>
+           </property>
+           <property name="buddy">
+            <cstring>txtStdDevGaussian</cstring>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="3">
+          <spacer name="horizontalSpacer_3">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+         <item row="0" column="0" colspan="4">
+          <widget class="QCheckBox" name="chkGaussian">
+           <property name="text">
+            <string>&amp;Gaussian</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="3" column="0">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeType">
+      <enum>QSizePolicy::Fixed</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>200</width>
+       <height>17</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>chkSobel</tabstop>
+  <tabstop>chkGaussian</tabstop>
+  <tabstop>txtStdDevGaussian</tabstop>
+  <tabstop>chkColor</tabstop>
+  <tabstop>txtStdDevColor</tabstop>
+  <tabstop>chkDoG</tabstop>
+  <tabstop>txtStdDev1DoG</tabstop>
+  <tabstop>txtStdDev2DoG</tabstop>
+  <tabstop>chkLoG</tabstop>
+  <tabstop>txtStdDevLoG</tabstop>
+  <tabstop>chkHessian</tabstop>
+  <tabstop>lstTypeHessian</tabstop>
+  <tabstop>txtStdDevHessian</tabstop>
+  <tabstop>chkStructureTensor</tabstop>
+  <tabstop>lstTypeStructureTensor</tabstop>
+  <tabstop>txtStdDevStructureTensor</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>RandomForestFeaturesDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>194</x>
+     <y>480</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>13</x>
+     <y>445</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>RandomForestFeaturesDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>276</x>
+     <y>486</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>246</x>
+     <y>450</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
-- 
GitLab