diff --git a/Grinder/Version.h b/Grinder/Version.h
index f6f822355933f3295d615236877d2b6284327466..9e6e54205e8cdb074fe9e7e07f6d6b92e21262f6 100644
--- a/Grinder/Version.h
+++ b/Grinder/Version.h
@@ -17,7 +17,7 @@
 #define GRNDR_VERSION_MAJOR		0
 #define GRNDR_VERSION_MINOR		6
 #define GRNDR_VERSION_REVISION	0
-#define GRNDR_VERSION_BUILD		218
+#define GRNDR_VERSION_BUILD		219
 
 namespace grndr
 {
diff --git a/Grinder/cv/CVUtils.cpp b/Grinder/cv/CVUtils.cpp
index 0f1b2143fefd05bdbe7d14a7f703ecd024504708..69588466feeeda89a65c7a17e3201ceabdff65de 100644
--- a/Grinder/cv/CVUtils.cpp
+++ b/Grinder/cv/CVUtils.cpp
@@ -47,7 +47,11 @@ QColor CVUtils::colorToGrayscale(QColor color)
 
 bool CVUtils::compareColors(QColor clr1, QColor clr2, float tolerance)
 {
-	if (tolerance > 0.0f)
+	if (tolerance >= 1.0f)
+	{
+		return true;
+	}
+	else if (tolerance > 0.0f)
 	{
 		// Based on https://www.compuphase.com/cmetric.htm
 		auto rmean = (clr1.red() + clr2.red() ) / 2;
diff --git a/Grinder/cv/algorithms/FloodFill.cpp b/Grinder/cv/algorithms/FloodFill.cpp
index c66541bbfc6bf7984c01546945371ce245092e74..ac5448d84b70e41e2b01844e12b87cea3d8ac06e 100644
--- a/Grinder/cv/algorithms/FloodFill.cpp
+++ b/Grinder/cv/algorithms/FloodFill.cpp
@@ -20,38 +20,97 @@ void FloodFill::execute()
 	if (_fromColor == _toColor)
 		return;
 
-	// Use a stack instead of recursion to prevent overflows
-	std::stack<QPoint> stack;
-	stack.push(_seedPos);
+	scanlineFill(_seedPos.x(), _seedPos.y());
+}
+
+void FloodFill::scanlineFill(int x, int y)
+{
+	// Based on Paul Heckbert's Seed Fill Algorithm (crude & ugly, but reasonably fast)
+	struct LineSegment { int y, xl, xr, dy; };
+	std::stack<LineSegment> stack;
+
+	auto push = [&stack, this](int y, int xl, int xr, int dy) {
+		if (y + dy >= 0 && y + dy < _matrix.rows)
+			stack.push(LineSegment{y, xl, xr, dy});
+	};
+
+	auto checkColor = [this](QColor pixelColor) {
+		if (pixelColor.rgb() == _toColor.rgb())
+			return false;
+
+		return ((_fromColor.isValid() && CVUtils::compareColors(pixelColor, _fromColor, _tolerance)) || (!_fromColor.isValid() && pixelColor.alpha() == 0));
+	};
+
+	push(y, x, x, 1);
+	push(y + 1, x, x, -1);
 
 	while (!stack.empty())
 	{
-		auto pos = stack.top();
+		auto lineSegment = stack.top();
 		stack.pop();
 
-		auto pixel = _pixels.at(pos);
-		auto pixelColor = pixel.get();
+		int y = lineSegment.y + lineSegment.dy;
+		int l;
 
-		if (((_fromColor.isValid() && CVUtils::compareColors(pixelColor, _fromColor, _tolerance)) || (!_fromColor.isValid() && pixelColor.alpha() == 0)) && pixelColor != _toColor)
+		for (x = lineSegment.xl; x >= 0; --x)
 		{
-			if (_toColor.isValid())
-				pixel = _toColor;
+			auto pixel = _pixels.at(y, x);
+			auto pixelColor = pixel.get();
+
+			if (checkColor(pixelColor))
+				pixel = _toColor.isValid() ? _toColor : QColor{0, 0, 0, 0};
 			else
-				pixel = QColor{0, 0, 0, 0};
+				break;
+		}
+
+		bool skip = false;
+
+		if (x >= lineSegment.xl)
+			skip = true;
+
+		if (!skip)
+		{
+			l = x + 1;
 
-			for (int deltaX = -1; deltaX <= 1; ++deltaX)
+			if (l < lineSegment.xl)
+				push(y, l, lineSegment.xl - 1, -lineSegment.dy);
+
+			x = lineSegment.xl + 1;
+		}
+
+		do
+		{
+			if (!skip)
 			{
-				for (int deltaY = -1; deltaY <= 1; ++deltaY)
+				for (; x < _matrix.cols; ++x)
 				{
-					if (deltaX != 0 || deltaY != 0)
-					{
-						auto newPos = QPoint{pos.x() + deltaX, pos.y() + deltaY};
+					auto pixel = _pixels.at(y, x);
+					auto pixelColor = pixel.get();
 
-						if (newPos.x() >= 0 && newPos.y() >= 0 && newPos.x() < _matrix.cols && newPos.y() < _matrix.rows)
-							stack.push(newPos);
-					}
+					if (checkColor(pixelColor))
+						pixel = _toColor.isValid() ? _toColor : QColor{0, 0, 0, 0};
+					else
+						break;
 				}
+
+				push(y, l, x - 1, lineSegment.dy);
+
+				if (x > lineSegment.xr + 1)
+					push(y, lineSegment.xr + 1, x - 1, -lineSegment.dy);
 			}
-		}
+
+			skip = false;
+
+			for (x++; x <= lineSegment.xr; ++x)
+			{
+				auto pixel = _pixels.at(y, x);
+				auto pixelColor = pixel.get();
+
+				if (checkColor(pixelColor))
+					break;
+			}
+
+			l = x;
+		} while (x <= lineSegment.xr);
 	}
 }
diff --git a/Grinder/cv/algorithms/FloodFill.h b/Grinder/cv/algorithms/FloodFill.h
index 93ee8558f7be5e44fe52c1700401862f9dd15885..a810cf743f4362a88765a53234d9ff183934c638 100644
--- a/Grinder/cv/algorithms/FloodFill.h
+++ b/Grinder/cv/algorithms/FloodFill.h
@@ -8,6 +8,7 @@
 
 #include <QColor>
 #include <QPoint>
+#include <stack>
 
 #include "CVAlgorithm.h"
 #include "cv/Pixel.h"
@@ -22,6 +23,9 @@ namespace grndr
 	public:
 		virtual void execute() override;
 
+	private:
+		void scanlineFill(int x, int y);
+
 	private:
 		QPoint _seedPos{0, 0};
 		QColor _fromColor;
diff --git a/Grinder/image/LayerPixelsData.cpp b/Grinder/image/LayerPixelsData.cpp
index e6907a486a5f44a30ac23427add27da8cdf08278..c1d3d6b7bf292d797a897498ad03a0037fe0ef80 100644
--- a/Grinder/image/LayerPixelsData.cpp
+++ b/Grinder/image/LayerPixelsData.cpp
@@ -181,7 +181,10 @@ void LayerPixelsData::fromImage(const QImage& image, QColor colorKey, float tole
 cv::Mat LayerPixelsData::toMatrix(QSize size, bool includeAlpha) const
 {
 	if (size.isNull())
-		size = bounds().size();
+	{
+		auto pixelsBounds = bounds();
+		size = QSize{pixelsBounds.right() + 1, pixelsBounds.bottom() + 1};
+	}
 
 	cv::Mat matrix = cv::Mat::zeros(size.height(), size.width(), includeAlpha ? CV_8UC4 : CV_8UC3);
 	PixelAccessor pixels{matrix};
@@ -203,7 +206,10 @@ cv::Mat LayerPixelsData::toMatrix(QSize size, bool includeAlpha) const
 QImage LayerPixelsData::toImage(QSize size, QImage* existingImage) const
 {
 	if (size.isNull())
-		size = bounds().size();
+	{
+		auto pixelsBounds = bounds();
+		size = QSize{pixelsBounds.right() + 1, pixelsBounds.bottom() + 1};
+	}
 
 	auto imageConverter = [this](QImage* image) {
 		image->fill(0);	// Erase everything with an invisible color