diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro
index 338272ee6e8c756636bb8642aadbf6028608fb52..8b2bdfd1e0c97413a83ad57479efaaeb4b82f006 100644
--- a/Grinder/Grinder.pro
+++ b/Grinder/Grinder.pro
@@ -455,7 +455,8 @@ SOURCES += \
     ui/ml/editors/MachineLearningStatePropertyEditor.cpp \
     ml/blocks/MachineLearningBlock.cpp \
     ml/blocks/TrainingBlock.cpp \
-    ml/processors/TrainingProcessor.cpp
+    ml/processors/TrainingProcessor.cpp \
+    project/exporters/HDF5File.cpp
 
 HEADERS += \
 	ui/mainwnd/GrinderWindow.h \
@@ -984,7 +985,8 @@ HEADERS += \
     ml/processors/MachineLearningMethodProcessor.impl.h \
     ml/processors/MachineLearningProcessor.h \
     ml/processors/MachineLearningProcessor.impl.h \
-    ml/barista/BaristaClassifierTaskSpawner.impl.h
+    ml/barista/BaristaClassifierTaskSpawner.impl.h \
+    project/exporters/HDF5File.h
 
 FORMS += \
 	ui/mainwnd/GrinderWindow.ui \
diff --git a/Grinder/Version.h b/Grinder/Version.h
index f4f176dd4e9425e5d7053624ce1a6cc91eb968db..09743bf4a67b6c9682f3510bdcabca6fc1c43070 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			"14.8.2019"
+#define GRNDR_INFO_DATE			"26.8.2019"
 #define GRNDR_INFO_COMPANY		"WWU Muenster"
 #define GRNDR_INFO_WEBSITE		"http://www.uni-muenster.de"
 
 #define GRNDR_VERSION_MAJOR		0
 #define GRNDR_VERSION_MINOR		15
 #define GRNDR_VERSION_REVISION	0
-#define GRNDR_VERSION_BUILD		375
+#define GRNDR_VERSION_BUILD		376
 
 namespace grndr
 {
diff --git a/Grinder/image/DraftItem.cpp b/Grinder/image/DraftItem.cpp
index 154e4e9b06bf3d86c1db53ca57ca5ea9e7aeccc8..8c6cb769454df799edc8f8e6c5c1edb3e6bb0743 100644
--- a/Grinder/image/DraftItem.cpp
+++ b/Grinder/image/DraftItem.cpp
@@ -7,6 +7,7 @@
 #include "DraftItem.h"
 #include "Layer.h"
 #include "ImageBuild.h"
+#include "ImageTags.h"
 
 const char* DraftItem::Serialization_Value_Type = "Type";
 
@@ -31,6 +32,16 @@ int DraftItem::getZOrder() const
 	return _layer->getZOrder();
 }
 
+void DraftItem::setDefaultPropertyValues()
+{
+	// If the primary color of the item matches a tag, assign that tag to the item
+	if (auto imageTags = _layer->imageBuild()->inputImageTags())
+	{
+		if (auto imageTag = imageTags->tags().selectByColor(*primaryColor()))
+			imageTagsAllotment()->object().allotTag(imageTag.get());
+	}
+}
+
 void DraftItem::serialize(SerializationContext& ctx) const
 {
 	PropertyObject::serialize(ctx);
diff --git a/Grinder/image/DraftItem.h b/Grinder/image/DraftItem.h
index 6c7654f5bd9961229fa079cae5b3c2184d262e40..7a094e29756ffce2d58d54937ca47575eec651ce 100644
--- a/Grinder/image/DraftItem.h
+++ b/Grinder/image/DraftItem.h
@@ -44,7 +44,7 @@ namespace grndr
 		auto imageTagsAllotment() const { return dynamic_cast<const ImageTagsAllotmentProperty*>(_imageTagsAllotment.get()); }
 
 	public:
-		virtual void setDefaultPropertyValues() { }
+		virtual void setDefaultPropertyValues();
 		virtual void setDragPropertyValues(QPoint initialPos, QPoint currentPos) { Q_UNUSED(initialPos); Q_UNUSED(currentPos); }
 
 		virtual void normalizePropertyValues() { }				
diff --git a/Grinder/image/ImageTagVector.cpp b/Grinder/image/ImageTagVector.cpp
index 5943c287a6c40ef4276d719c895ba9ae0a6d7b0b..7a7c8129a209e20dd2b700dfb926b17b374af93c 100644
--- a/Grinder/image/ImageTagVector.cpp
+++ b/Grinder/image/ImageTagVector.cpp
@@ -5,6 +5,7 @@
 
 #include "Grinder.h"
 #include "ImageTagVector.h"
+#include "cv/CVUtils.h"
 
 const char* ImageTagVector::Serialization_Group = "ImageTags";
 const char* ImageTagVector::Serialization_Element = "ImageTag";
@@ -16,5 +17,5 @@ ImageTagVector::pointer_type ImageTagVector::selectByName(QString name) const
 
 ImageTagVector::pointer_type ImageTagVector::selectByColor(QColor color) const
 {
-	return selectFirst([color](auto imageTag) { return imageTag->getColor() == color; });
+	return selectFirst([color](auto imageTag) { return CVUtils::compareColorsNoAlpha(imageTag->getColor(), color); });
 }
diff --git a/Grinder/image/Layer.cpp b/Grinder/image/Layer.cpp
index bef5269fefb3dd27d4a1fe477ca3898066332df6..385ed2756a5572bd8f2a2e807f3901c80a3a76ee 100644
--- a/Grinder/image/Layer.cpp
+++ b/Grinder/image/Layer.cpp
@@ -59,14 +59,14 @@ std::shared_ptr<DraftItem> Layer::createDraftItem(DraftItemType type)
 	std::shared_ptr<DraftItem> item = DraftItemCatalog::createDraftItem(this, type);
 
 	try {	// Propagate initialization errors to the caller
-		item->initDraftItem();
+		item->initDraftItem();		
 	}
 	catch (...) {
 		throw;
 	}
 
 	_draftItems.push_back(item);
-	emit draftItemCreated(item);
+	emit draftItemCreated(item);																
 
 	return item;
 }
diff --git a/Grinder/ml/barista/BaristaNetwork.cpp b/Grinder/ml/barista/BaristaNetwork.cpp
index 5100fceb8a690b53c76ff6491857bd19fc2cc7f7..033ca735e513ddb72e52b6a5af566a8a35db1a43 100644
--- a/Grinder/ml/barista/BaristaNetwork.cpp
+++ b/Grinder/ml/barista/BaristaNetwork.cpp
@@ -141,13 +141,13 @@ void BaristaNetwork::exportTrainingData(const BaristaNetworkContext& ctx) const
 
 	try {
 		// Export the images
-		HDF5Exporter::ExportFlags exportFlags{HDF5Exporter::ExportFlag::ExportTags};
+		HDF5File::ExportFlags exportFlags{HDF5File::ExportFlag::ExportTags};
 
 		if (_networkInfo.mergeTags())
-			exportFlags |= HDF5Exporter::ExportFlag::MergeTags;
+			exportFlags |= HDF5File::ExportFlag::MergeTags;
 
 		if (_networkInfo.requiresGrayscale())
-			exportFlags |= HDF5Exporter::ExportFlag::ExportAsGrayscale;
+			exportFlags |= HDF5File::ExportFlag::ExportAsGrayscale;
 
 		HDF5Exporter exporter{ctx.getLabel(), ctx.getCanvasBlock(), ctx.getImageReferences(), exportFlags};
 		exporter.exportProject(&grinder()->project(), hdf5File);
diff --git a/Grinder/ml/processors/TrainingProcessor.cpp b/Grinder/ml/processors/TrainingProcessor.cpp
index dd685ed171e3ddacfc01fbde2a3a27465d2a4cf5..cff78977efa5cf42c8787c6a0dc6103db0748570 100644
--- a/Grinder/ml/processors/TrainingProcessor.cpp
+++ b/Grinder/ml/processors/TrainingProcessor.cpp
@@ -17,8 +17,8 @@ void TrainingProcessor::execute(EngineExecutionContext& ctx, const MachineLearni
 	// Training is only executed in batch mode
 	if (ctx.hasExecutionFlag(Engine::ExecutionFlag::Batch))
 	{
-		// Spawn the training task when the last image is active
-		if (ctx.isLastImage())
+		// Spawn the training task when the first image is active
+		if (ctx.isFirstImage())
 		{
 			// TODO: Gather images, labels etc. before spawning
 			spawnTask(SpawnType::Training, method, state);
diff --git a/Grinder/project/exporters/HDF5Exporter.cpp b/Grinder/project/exporters/HDF5Exporter.cpp
index dcd4891e309535f3733217af8c8b15b7ebb28d2f..c697e189c354fc27627cd9952324b27531abe008 100644
--- a/Grinder/project/exporters/HDF5Exporter.cpp
+++ b/Grinder/project/exporters/HDF5Exporter.cpp
@@ -11,15 +11,10 @@
 #include "image/ImageTags.h"
 #include "ui/dlg/HDF5ExportDialog.h"
 
-#include <opencv2/imgproc.hpp>
-
-#define HDF5_DATASET_DATA	"data"
-#define HDF5_DATASET_TAGS	"label"
-
-HDF5Exporter::HDF5Exporter(Label* label, const Block* canvasBlock, const ImageReferenceSelection& imageReferences, ExportFlags exportFlags) : ProjectExporter("HDF5 Exporter", "*.h5;*.hdf5"),
+HDF5Exporter::HDF5Exporter(Label* label, const Block* canvasBlock, const ImageReferenceSelection& imageReferences, HDF5File::ExportFlags exportFlags) : ProjectExporter("HDF5 Exporter", "*.h5;*.hdf5"),
 	_label{label}, _canvasBlock{canvasBlock}, _imageReferences{imageReferences}, _exportFlags{exportFlags}
 {
-	H5::Exception::dontPrint();
+
 }
 
 bool HDF5Exporter::invokeUi(const Project* project, QWidget* parent)
@@ -35,13 +30,13 @@ bool HDF5Exporter::invokeUi(const Project* project, QWidget* parent)
 		_canvasBlock = dlg.getCanvasBlock();
 		_imageReferences = dlg.getImageReferences();
 
-		_exportFlags = ExportFlag::None;
+		_exportFlags = HDF5File::ExportFlag::None;
 
 		if (dlg.exportAsGrayscale())
-			_exportFlags |= ExportFlag::ExportAsGrayscale;
+			_exportFlags |= HDF5File::ExportFlag::ExportAsGrayscale;
 
 		if (dlg.exportImageTags())
-			_exportFlags |= ExportFlag::ExportTags;
+			_exportFlags |= HDF5File::ExportFlag::ExportTags;
 
 		return true;
 	}
@@ -58,21 +53,13 @@ void HDF5Exporter::exportProject(const Project* project, QString fileName)
 	// Make sure that all image references can be exported
 	verifyImageReferences(project);
 
-	try {
-		H5::H5File h5File{fileName.toLatin1(), H5F_ACC_TRUNC};
-
-		// Export images and tags
-		opExport.setStatusMessage("Images");
-		exportImageReferences(project, h5File);
+	// Prepare the H5 file
+	HDF5File h5File{fileName};
+	h5File.initExport(getImageSize(), _imageReferences.size(), getImageTagsCount(), _exportFlags);
 
-		if (_exportFlags.testFlag(ExportFlag::ExportTags))
-		{
-			opExport.setStatusMessage("Tags");
-			exportImageTags(project, h5File);
-		}
-	} catch (H5::Exception& e) {
-		throw ExportException{project, _EXCPT(QString{"HDF5 error: %1"}.arg(e.getDetailMsg().data()))};
-	}
+	// Export images; tags will also be exported if necessary
+	opExport.setStatusMessage("Images");
+	exportImageReferences(project, h5File);
 }
 
 void HDF5Exporter::verifyImageReferences(const Project* project) const
@@ -85,36 +72,42 @@ void HDF5Exporter::verifyImageReferences(const Project* project) const
 
 	for (const auto& imgRef : _imageReferences)
 	{
-		if (imgRef->getImageInfo().imageSize.width() != static_cast<int>(imgSize.first) || imgRef->getImageInfo().imageSize.height() != static_cast<int>(imgSize.second))
-			throw ExportException{project, _EXCPT(QString{"All exported images must be of the same size (%1x%2)"}.arg(imgSize.first).arg(imgSize.second))};
+		if (imgRef->getImageInfo().imageSize.width() != imgSize.width() || imgRef->getImageInfo().imageSize.height() != imgSize.height())
+			throw ExportException{project, _EXCPT(QString{"All exported images must be of the same size (%1x%2)"}.arg(imgSize.width()).arg(imgSize.height()))};
 	}
 }
 
-std::pair<hsize_t, hsize_t> HDF5Exporter::getImageSize() const
+QSize HDF5Exporter::getImageSize() const
 {
 	if (!_imageReferences.empty())
+		return _imageReferences[0]->getImageInfo().imageSize;
+	else
+		return QSize{0, 0};
+}
+
+unsigned int HDF5Exporter::getImageTagsCount() const
+{
+	// Get the total number of tags
+	auto tagsCount = ImageTags::Maximum_Tags_Count;
+
+	if (_canvasBlock)
 	{
-		QSize imgSize = _imageReferences[0]->getImageInfo().imageSize;
-		return std::make_pair(static_cast<hsize_t>(imgSize.width()), static_cast<hsize_t>(imgSize.height()));
+		if (auto imageTagsProperty = _canvasBlock->portProperty<ImageTagsProperty>(PortType::ImageTagsIn, PropertyID::ImageTags))
+			tagsCount = imageTagsProperty->object().tags().size();
 	}
-	else
-		return std::make_pair(static_cast<hsize_t>(0), static_cast<hsize_t>(0));
+
+	return tagsCount;
 }
 
-void HDF5Exporter::exportImageReferences(const Project* project, H5::H5File& h5File) const
+void HDF5Exporter::exportImageReferences(const Project* project, const HDF5File& h5File) const
 {
 	LongOperation opExportImages{"Exporting images", static_cast<unsigned int>(_imageReferences.size())};
 
-	// Export all images
-	auto dataSpaceImg = createDataSpace(_exportFlags.testFlag(ExportFlag::ExportAsGrayscale) ? 1 : 3);
-	auto dataSetImg = createDataSet(h5File, dataSpaceImg, HDF5_DATASET_DATA, H5::PredType::NATIVE_FLOAT);
-	unsigned int index = 0;
-
 	for (const auto& imgRef : _imageReferences)
-		exportImageReference(project, imgRef, index++, dataSpaceImg, dataSetImg);
+		exportImageReference(project, imgRef, h5File);
 }
 
-void HDF5Exporter::exportImageReference(const Project* project, const ImageReference* imgRef, unsigned int index, const H5::DataSpace& dataSpace, const H5::DataSet& dataSet) const
+void HDF5Exporter::exportImageReference(const Project* project, const ImageReference* imgRef, const HDF5File& h5File) const
 {
 	LongOperationStep opExportImage{imgRef->getImageFilePath()};
 	cv::Mat imgData;
@@ -135,88 +128,24 @@ void HDF5Exporter::exportImageReference(const Project* project, const ImageRefer
 	if (imgData.channels() != 3)
 		throw ExportException{project, _EXCPT(QString{"The data for '%1' is invalid"}.arg(imgRef->getImageFilePath()))};
 
-	// Convert the image data to float and map intensities to [0,1]
-	imgData.convertTo(imgData, CV_32F);
-	imgData /= 255.0f;
-
-	// Convert colors if necessary
-	if (imgData.channels() == 1 && !_exportFlags.testFlag(ExportFlag::ExportAsGrayscale))
-		cv::cvtColor(imgData, imgData, cv::COLOR_GRAY2BGR);
-	else if (imgData.channels() == 3 && _exportFlags.testFlag(ExportFlag::ExportAsGrayscale))
-		cv::cvtColor(imgData, imgData, cv::COLOR_BGR2GRAY);
-
-	// Write each channel individually; this works for single-channel images as well
-	cv::Mat imageChannels[3];	// BGR order
-	cv::split(imgData, imageChannels);
-
-	auto imgSize = getImageSize();
-	hsize_t imgDims[] = {imgSize.second, imgSize.first};
-	hsize_t count[] = {1, 1, imgSize.second, imgSize.first};
-
-	for (unsigned int chan = 0; chan < static_cast<unsigned int>(imgData.channels()); ++chan)
-	{
-		// Select the proper hyperslab to write the data to
-		hsize_t start[] = {index, chan, 0, 0};
-		dataSpace.selectHyperslab(H5S_SELECT_SET, count, start);
-
-		// Write the image data to the dataset
-		dataSet.write(imageChannels[chan].data, H5::PredType::NATIVE_FLOAT, H5::DataSpace{2, imgDims}, dataSpace);
-	}
-}
-
-void HDF5Exporter::exportImageTags(const Project* project, H5::H5File& h5File) const
-{	
-	if (_label && _canvasBlock)
-	{
-		// Get the total number of tags
-		auto tagsCount = ImageTags::Maximum_Tags_Count;
-
-		if (auto imageTagsProperty = _canvasBlock->portProperty<ImageTagsProperty>(PortType::ImageTagsIn, PropertyID::ImageTags))
-			tagsCount = imageTagsProperty->object().tags().size();
-
-		LongOperation opExportImages{"Exporting tags", static_cast<unsigned int>(_imageReferences.size())};
-
-		// Export the tags of all images
-		auto dataSpaceTags = createDataSpace(_exportFlags.testFlag(ExportFlag::MergeTags) ? 1 : tagsCount);
-		auto dataSetTags = createDataSet(h5File, dataSpaceTags, HDF5_DATASET_TAGS, H5::PredType::NATIVE_INT32);
-		int index = 0;
-
-		for (const auto& imgRef : _imageReferences)
-			exportImageTags(project, imgRef, tagsCount, index++, dataSpaceTags, dataSetTags);
-	}
-}
-
-void HDF5Exporter::exportImageTags(const Project* project, const ImageReference* imgRef, unsigned int tagCount, unsigned int index, const H5::DataSpace& dataSpace, const H5::DataSet& dataSet) const
-{
-	Q_UNUSED(project);
+	// Generate the tag matrices
+	std::vector<cv::Mat> tagMatrices;
 
-	LongOperationStep opExportTags{imgRef->getImageFilePath()};
-	auto imageTagsBitmap = grinder()->engineController().generateImageTagsBitmap(_label, _canvasBlock, imgRef, false);
-
-	if (imageTagsBitmap.isValid())
+	if (_exportFlags.testFlag(HDF5File::ExportFlag::ExportTags))
 	{
-		std::vector<cv::Mat> flagMatrices;
-
-		if (_exportFlags.testFlag(ExportFlag::MergeTags))
-			flagMatrices = exportImageTags_Merged(imageTagsBitmap, tagCount);
-		else
-			flagMatrices = exportImageTags_Individually(imageTagsBitmap, tagCount);
+		auto imageTagsBitmap = grinder()->engineController().generateImageTagsBitmap(_label, _canvasBlock, imgRef, false);
 
-		// Export all flag matrices
-		auto imgSize = getImageSize();
-		hsize_t imgDims[] = {imgSize.second, imgSize.first};
-		hsize_t count[] = {1, 1, imgSize.second, imgSize.first};
-
-		for (unsigned int i = 0; i < flagMatrices.size(); ++i)
+		if (imageTagsBitmap.isValid())
 		{
-			// Select the proper hyperslab to write the data to
-			hsize_t start[] = {index, i, 0, 0};
-			dataSpace.selectHyperslab(H5S_SELECT_SET, count, start);
-
-			// Write the tags data to the dataset
-			dataSet.write(flagMatrices[i].data, H5::PredType::NATIVE_INT32, H5::DataSpace{2, imgDims}, dataSpace);
+			if (_exportFlags.testFlag(HDF5File::ExportFlag::MergeTags))
+				tagMatrices = exportImageTags_Merged(imageTagsBitmap, getImageTagsCount());
+			else
+				tagMatrices = exportImageTags_Individually(imageTagsBitmap, getImageTagsCount());
 		}
 	}
+
+	// Export the image and tags
+	h5File.exportImageEx(imgData, tagMatrices);
 }
 
 std::vector<cv::Mat> HDF5Exporter::exportImageTags_Individually(const ImageTagsBitmap& imageTagsBitmap, unsigned int tagCount) const
@@ -229,7 +158,7 @@ std::vector<cv::Mat> HDF5Exporter::exportImageTags_Individually(const ImageTagsB
 	std::vector<cv::Mat> flagMatrices;
 
 	for (unsigned int i = 0; i < tagCount; ++i)
-		flagMatrices.push_back(cv::Mat::zeros(imgSize.second, imgSize.first, CV_32SC1));
+		flagMatrices.push_back(cv::Mat::zeros(imgSize.height(), imgSize.width(), CV_32SC1));
 
 	for (int r = 0; r < bitmap.size().height(); ++r)
 	{
@@ -253,7 +182,7 @@ std::vector<cv::Mat> HDF5Exporter::exportImageTags_Merged(const ImageTagsBitmap&
 	auto imgSize = getImageSize();
 
 	// Create a flag matrix for all tags
-	cv::Mat flagMatrix = cv::Mat::zeros(imgSize.second, imgSize.first, CV_32SC1);
+	cv::Mat flagMatrix = cv::Mat::zeros(imgSize.height(), imgSize.width(), CV_32SC1);
 
 	for (int r = 0; r < bitmap.size().height(); ++r)
 	{
@@ -269,26 +198,3 @@ std::vector<cv::Mat> HDF5Exporter::exportImageTags_Merged(const ImageTagsBitmap&
 
 	return {flagMatrix};
 }
-
-H5::DataSpace HDF5Exporter::createDataSpace(unsigned int channels) const
-{
-	// Our dataspace is 4D: Index, number of channels, Y and X
-	auto imgSize = getImageSize();
-	hsize_t dataSpaceDims[] = {_imageReferences.size(), channels, imgSize.second, imgSize.first};
-	return H5::DataSpace{4, dataSpaceDims};
-}
-
-H5::DataSet HDF5Exporter::createDataSet(const H5::H5File& h5File, const H5::DataSpace& dataSpace, QString name, H5::PredType predType) const
-{
-	H5::DSetCreatPropList propList{H5::DSetCreatPropList::DEFAULT};
-
-#if defined(H5_HAVE_FILTER_DEFLATE)
-	// Enable compression on the data
-	auto imgSize = getImageSize();
-	hsize_t chunkDims[] = {1, 1, imgSize.second, imgSize.first};
-	propList.setChunk(4, chunkDims);
-	propList.setDeflate(5);
-#endif
-
-	return h5File.createDataSet(name.toLatin1(), predType, dataSpace, propList);
-}
diff --git a/Grinder/project/exporters/HDF5Exporter.h b/Grinder/project/exporters/HDF5Exporter.h
index cd44cb350c7ad085d5a268881b74ccb9152dee2d..db42237dfb1f50bdd528181e233a067832ccd5c7 100644
--- a/Grinder/project/exporters/HDF5Exporter.h
+++ b/Grinder/project/exporters/HDF5Exporter.h
@@ -6,9 +6,9 @@
 #ifndef HDF5EXPORTER_H
 #define HDF5EXPORTER_H
 
-#include <H5Cpp.h>
 #include <opencv2/core.hpp>
 
+#include "HDF5File.h"
 #include "project/ProjectExporter.h"
 #include "project/ImageReferenceSelection.h"
 
@@ -22,20 +22,7 @@ namespace grndr
 	class HDF5Exporter : public ProjectExporter
 	{
 	public:
-		enum class ExportFlag : unsigned int
-		{
-			None = 0x0000,
-
-			ExportAsGrayscale = 0x0001,
-			ExportTags = 0x0002,
-
-			MergeTags = 0x0100,
-		};
-
-		Q_DECLARE_FLAGS(ExportFlags, ExportFlag)
-
-	public:
-		HDF5Exporter(Label* label = nullptr, const Block* canvasBlock = nullptr, const ImageReferenceSelection& imageReferences = {}, ExportFlags exportFlags = ExportFlag::ExportTags);
+		HDF5Exporter(Label* label = nullptr, const Block* canvasBlock = nullptr, const ImageReferenceSelection& imageReferences = {}, HDF5File::ExportFlags exportFlags = HDF5File::ExportFlag::ExportTags);
 
 	public:
 		virtual bool invokeUi(const Project* project, QWidget* parent) override;
@@ -44,29 +31,25 @@ namespace grndr
 	private:
 		void verifyImageReferences(const Project* project) const;
 
-		std::pair<hsize_t, hsize_t> getImageSize() const;
+		QSize getImageSize() const;
+		unsigned int getImageTagsCount() const;
 
 	private:
-		void exportImageReferences(const Project* project, H5::H5File& h5File) const;
-		void exportImageReference(const Project* project, const ImageReference* imgRef, unsigned int index, const H5::DataSpace& dataSpace, const H5::DataSet& dataSet) const;
+		void exportImageReferences(const Project* project, const HDF5File& h5File) const;
+		void exportImageReference(const Project* project, const ImageReference* imgRef, const HDF5File& h5File) const;
 		void exportImageTags(const Project* project, H5::H5File& h5File) const;
 		void exportImageTags(const Project* project, const ImageReference* imgRef, unsigned int tagCount, unsigned int index, const H5::DataSpace& dataSpace, const H5::DataSet& dataSet) const;
 
 		std::vector<cv::Mat> exportImageTags_Individually(const ImageTagsBitmap& imageTagsBitmap, unsigned int tagCount) const;
 		std::vector<cv::Mat> exportImageTags_Merged(const ImageTagsBitmap& imageTagsBitmap, unsigned int tagCount) const;
 
-		H5::DataSpace createDataSpace(unsigned int channels) const;
-		H5::DataSet createDataSet(const H5::H5File& h5File, const H5::DataSpace& dataSpace, QString name, H5::PredType predType) const;
-
 	private:
 		Label* _label{nullptr};
 		const Block* _canvasBlock{nullptr};
 		ImageReferenceSelection _imageReferences;
 
-		ExportFlags _exportFlags{ExportFlag::None};
+		HDF5File::ExportFlags _exportFlags{HDF5File::ExportFlag::None};
 	};
 }
 
-Q_DECLARE_OPERATORS_FOR_FLAGS(grndr::HDF5Exporter::ExportFlags)
-
 #endif
diff --git a/Grinder/project/exporters/HDF5File.cpp b/Grinder/project/exporters/HDF5File.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7863a878048b64f89fe3c9af6f4bfc02f1a65970
--- /dev/null
+++ b/Grinder/project/exporters/HDF5File.cpp
@@ -0,0 +1,167 @@
+/******************************************************************************
+ * File: HDF5File.cpp
+ * Date: 26.8.2019
+ *****************************************************************************/
+
+#include "Grinder.h"
+#include "HDF5File.h"
+#include "project/ProjectExceptions.h"
+
+#include <opencv2/imgproc.hpp>
+
+#define HDF5_DATASET_DATA	"data"
+#define HDF5_DATASET_TAGS	"label"
+
+HDF5File::HDF5File(QString filename, bool truncate)
+{
+	H5::Exception::dontPrint();
+
+	try {
+		// Create/open the H5 file
+		_h5File = H5::H5File{filename.toLatin1(), truncate ? H5F_ACC_TRUNC : 0};
+	} catch (H5::Exception& e) {
+		throwH5Exception(e);
+	}
+}
+
+void HDF5File::initExport(QSize imageSize, unsigned int imageCount, unsigned int tagsCount, ExportFlags flags)
+{
+	if (imageSize.isNull())
+		throw ExportException{nullptr, _EXCPT("Image size may not be 0x0")};
+
+	if (imageCount == 0)
+		throw ExportException{nullptr, _EXCPT("Image count may not be 0")};
+
+	_imageSize = {imageSize.width(), imageSize.height()};
+	_imageCount = imageCount;
+
+	if (flags.testFlag(ExportFlag::ExportTags))
+		_tagsCount = flags.testFlag(ExportFlag::MergeTags) ? 1 : tagsCount;
+	else
+		_tagsCount = 0;
+
+	_exportFlags = flags;
+
+	try {
+		// Create the H5 objects
+		_h5SpaceData = createDataSpace(_exportFlags.testFlag(ExportFlag::ExportAsGrayscale) ? 1 : 3);
+		_h5SetData = createDataSet(_h5SpaceData, HDF5_DATASET_DATA, H5::PredType::NATIVE_FLOAT);
+
+		if (_exportFlags.testFlag(ExportFlag::ExportTags))
+		{
+			_h5SpaceTags = createDataSpace(_tagsCount);
+			_h5SetTags = createDataSet(_h5SpaceTags, HDF5_DATASET_TAGS, H5::PredType::NATIVE_INT32);
+		}
+
+		_currentImage = 0;
+	} catch (H5::Exception& e) {
+		throwH5Exception(e);
+	}
+}
+
+void HDF5File::exportImageEx(const cv::Mat& image, const std::vector<cv::Mat>& tagMatrices) const
+{
+	// Verify parameters
+	if (static_cast<hsize_t>(image.rows) != _imageSize.second || static_cast<hsize_t>(image.cols) != _imageSize.first)
+		throw ExportException{nullptr, _EXCPT(QString{"All exported images must be of the correct size (%1x%2)"}.arg(_imageSize.first).arg(_imageSize.second))};
+
+	for (auto tagMatrix : tagMatrices)
+	{
+		if (static_cast<hsize_t>(tagMatrix.rows) != _imageSize.second || static_cast<hsize_t>(tagMatrix.cols) != _imageSize.first)
+			throw ExportException{nullptr, _EXCPT(QString{"All exported image tags must be of the correct size (%1x%2)"}.arg(_imageSize.first).arg(_imageSize.second))};
+	}
+
+	if (_currentImage >= _imageCount)
+		throw ExportException{nullptr, _EXCPT(QString{"Tried to export more than %1 image(s)"}.arg(_imageCount))};
+
+	cv::Mat imgData;
+
+	// Convert the image data to float and map intensities to [0,1]
+	image.convertTo(imgData, CV_32F);
+	imgData /= 255.0f;
+
+	// Convert colors if necessary
+	if (imgData.channels() == 1 && !_exportFlags.testFlag(ExportFlag::ExportAsGrayscale))
+		cv::cvtColor(imgData, imgData, cv::COLOR_GRAY2BGR);
+	else if (imgData.channels() == 3 && _exportFlags.testFlag(ExportFlag::ExportAsGrayscale))
+		cv::cvtColor(imgData, imgData, cv::COLOR_BGR2GRAY);
+
+	// Write each channel individually; this works for single-channel images as well
+	cv::Mat imageChannels[3];	// BGR order
+	cv::split(imgData, imageChannels);
+
+	hsize_t imgDims[] = {_imageSize.second, _imageSize.first};
+	hsize_t count[] = {1, 1, _imageSize.second, _imageSize.first};
+
+	try {
+		for (unsigned int chan = 0; chan < static_cast<unsigned int>(imgData.channels()); ++chan)
+		{
+			// Select the proper hyperslab to write the data to
+			hsize_t start[] = {_currentImage, chan, 0, 0};
+			_h5SpaceData.selectHyperslab(H5S_SELECT_SET, count, start);
+
+			// Write the image data to the dataset
+			_h5SetData.write(imageChannels[chan].data, H5::PredType::NATIVE_FLOAT, H5::DataSpace{2, imgDims}, _h5SpaceData);
+		}
+	} catch (H5::Exception& e) {
+		throwH5Exception(e);
+	}
+
+	if (_exportFlags.testFlag(ExportFlag::ExportTags))
+		exportImageTags(tagMatrices);
+
+	_currentImage += 1;
+}
+
+void HDF5File::exportImageTags(const std::vector<cv::Mat>& tagMatrices) const
+{
+	if (tagMatrices.size() > _tagsCount)
+		throw ExportException{nullptr, _EXCPT(QString{"Tried to export more than %1 image tag(s)"}.arg(_tagsCount))};
+
+	// Export all tags matrices
+	hsize_t imgDims[] = {_imageSize.second, _imageSize.first};
+	hsize_t count[] = {1, 1, _imageSize.second, _imageSize.first};
+
+	try {
+		for (unsigned int i = 0; i < tagMatrices.size(); ++i)
+		{
+			if (static_cast<hsize_t>(tagMatrices[i].rows) != _imageSize.second || static_cast<hsize_t>(tagMatrices[i].cols) != _imageSize.first)
+				throw ExportException{nullptr, _EXCPT(QString{"All exported tag bitmaps must be of the correct size (%1x%2)"}.arg(_imageSize.first).arg(_imageSize.second))};
+
+			// Select the proper hyperslab to write the data to
+			hsize_t start[] = {_currentImage, i, 0, 0};
+			_h5SpaceTags.selectHyperslab(H5S_SELECT_SET, count, start);
+
+			// Write the tags data to the dataset
+			_h5SetTags.write(tagMatrices[i].data, H5::PredType::NATIVE_INT32, H5::DataSpace{2, imgDims}, _h5SpaceTags);
+		}
+	} catch (H5::Exception& e) {
+		throwH5Exception(e);
+	}
+}
+
+H5::DataSpace HDF5File::createDataSpace(unsigned int channels) const
+{
+	// Our dataspace is 4D: Index, number of channels, Y and X
+	hsize_t dataSpaceDims[] = {_imageCount, channels, _imageSize.second, _imageSize.first};
+	return H5::DataSpace{4, dataSpaceDims};
+}
+
+H5::DataSet HDF5File::createDataSet(const H5::DataSpace& dataSpace, QString name, H5::PredType predType) const
+{
+	H5::DSetCreatPropList propList{H5::DSetCreatPropList::DEFAULT};
+
+#if defined(H5_HAVE_FILTER_DEFLATE)
+	// Enable compression on the data
+	hsize_t chunkDims[] = {1, 1, _imageSize.second, _imageSize.first};
+	propList.setChunk(4, chunkDims);
+	propList.setDeflate(5);
+#endif
+
+	return _h5File.createDataSet(name.toLatin1(), predType, dataSpace, propList);
+}
+
+void HDF5File::throwH5Exception(H5::Exception& e) const
+{
+	throw ExportException{nullptr, _EXCPT(QString{"HDF5 error: %1"}.arg(e.getDetailMsg().data()))};
+}
diff --git a/Grinder/project/exporters/HDF5File.h b/Grinder/project/exporters/HDF5File.h
new file mode 100644
index 0000000000000000000000000000000000000000..95ea8d3ff5bb94d0d5ee92d6808ebdaca9597249
--- /dev/null
+++ b/Grinder/project/exporters/HDF5File.h
@@ -0,0 +1,72 @@
+/******************************************************************************
+ * File: HDF5File.h
+ * Date: 26.8.2019
+ *****************************************************************************/
+
+#ifndef HDF5FILE_H
+#define HDF5FILE_H
+
+#include <H5Cpp.h>
+#include <opencv2/core.hpp>
+
+#include <QSize>
+
+namespace grndr
+{
+	class HDF5File
+	{
+	public:
+		enum class ExportFlag : unsigned int
+		{
+			None = 0x0000,
+
+			ExportAsGrayscale = 0x0001,
+			ExportTags = 0x0002,
+
+			MergeTags = 0x0100,
+		};
+
+		Q_DECLARE_FLAGS(ExportFlags, ExportFlag)
+
+	public:
+		HDF5File(QString filename, bool truncate = true);
+
+	public:
+		void initExport(QSize imageSize, unsigned int imageCount, unsigned int tagsCount, ExportFlags flags = ExportFlag::ExportTags);
+
+		void exportImage(const cv::Mat& image) const { exportImageEx(image, {}); }
+		void exportImageEx(const cv::Mat& image, const std::vector<cv::Mat>& tagMatrices) const;
+
+	private:
+		void exportImageTags(const std::vector<cv::Mat>& tagMatrices) const;
+
+	private:
+		H5::DataSpace createDataSpace(unsigned int channels) const;
+		H5::DataSet createDataSet(const H5::DataSpace& dataSpace, QString name, H5::PredType predType) const;
+
+	private:
+		void throwH5Exception(H5::Exception& e) const;
+
+	private:
+		std::pair<hsize_t, hsize_t> _imageSize;
+		unsigned int _imageCount{0};
+		unsigned int _tagsCount{0};
+
+		ExportFlags _exportFlags{ExportFlag::None};
+
+	private:
+		H5::H5File _h5File;
+
+		H5::DataSpace _h5SpaceData;
+		H5::DataSet _h5SetData;
+		H5::DataSpace _h5SpaceTags;
+		H5::DataSet _h5SetTags;
+
+	private:
+		mutable unsigned int _currentImage{0};
+	};
+}
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(grndr::HDF5File::ExportFlags)
+
+#endif