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