From 4e06c82fd20f911ba7e478b2d32c8dba949e3db0 Mon Sep 17 00:00:00 2001 From: zvezdochiot Date: Sat, 10 Aug 2024 17:08:59 +0300 Subject: [PATCH] 1.0.20: threshold: new method Grad --- src/app/DefaultParamsDialog.cpp | 21 +-- src/core/Utils.h | 2 +- src/core/filters/output/BlackWhiteOptions.cpp | 35 ++--- src/core/filters/output/BlackWhiteOptions.h | 2 +- src/core/filters/output/OptionsWidget.cpp | 17 ++- src/core/filters/output/OutputGenerator.cpp | 22 ++- src/imageproc/Binarize.cpp | 126 ++++++++++++++++++ src/imageproc/Binarize.h | 7 + 8 files changed, 191 insertions(+), 41 deletions(-) diff --git a/src/app/DefaultParamsDialog.cpp b/src/app/DefaultParamsDialog.cpp index c9c1c2f6a..982efd361 100644 --- a/src/app/DefaultParamsDialog.cpp +++ b/src/app/DefaultParamsDialog.cpp @@ -45,13 +45,14 @@ DefaultParamsDialog::DefaultParamsDialog(QWidget* parent) fillingColorBox->addItem(tr("White"), FILL_WHITE); fillingColorBox->addItem(tr("Black"), FILL_BLACK); - thresholdMethodBox->addItem(tr("Otsu"), OTSU); - thresholdMethodBox->addItem(tr("Sauvola"), SAUVOLA); - thresholdMethodBox->addItem(tr("Wolf"), WOLF); - thresholdMethodBox->addItem(tr("Bradley"), BRADLEY); - thresholdMethodBox->addItem(tr("EdgePlus"), EDGEPLUS); - thresholdMethodBox->addItem(tr("BlurDiv"), BLURDIV); - thresholdMethodBox->addItem(tr("EdgeDiv"), EDGEDIV); + thresholdMethodBox->addItem(tr("Otsu"), T_OTSU); + thresholdMethodBox->addItem(tr("Sauvola"), T_SAUVOLA); + thresholdMethodBox->addItem(tr("Wolf"), T_WOLF); + thresholdMethodBox->addItem(tr("Bradley"), T_BRADLEY); + thresholdMethodBox->addItem(tr("Grad"), T_GRAD); + thresholdMethodBox->addItem(tr("EdgePlus"), T_EDGEPLUS); + thresholdMethodBox->addItem(tr("BlurDiv"), T_BLURDIV); + thresholdMethodBox->addItem(tr("EdgeDiv"), T_EDGEDIV); pictureShapeSelector->addItem(tr("Off"), OFF_SHAPE); pictureShapeSelector->addItem(tr("Free"), FREE_SHAPE); @@ -666,10 +667,10 @@ std::unique_ptr DefaultParamsDialog::buildParams() const { blackWhiteOptions.setBinarizationMethod(binarizationMethod); blackWhiteOptions.setThresholdAdjustment(thresholdSlider->value()); blackWhiteOptions.setSauvolaCoef(sauvolaCoef->value()); - if (binarizationMethod == SAUVOLA || binarizationMethod == BRADLEY || binarizationMethod == EDGEPLUS - || binarizationMethod == BLURDIV || binarizationMethod == EDGEDIV) { + if (binarizationMethod == T_SAUVOLA || binarizationMethod == T_BRADLEY || binarizationMethod == T_GRAD + || binarizationMethod == T_EDGEPLUS || binarizationMethod == T_BLURDIV || binarizationMethod == T_EDGEDIV) { blackWhiteOptions.setWindowSize(sauvolaWindowSize->value()); - } else if (binarizationMethod == WOLF) { + } else if (binarizationMethod == T_WOLF) { blackWhiteOptions.setWindowSize(wolfWindowSize->value()); } blackWhiteOptions.setWolfCoef(wolfCoef->value()); diff --git a/src/core/Utils.h b/src/core/Utils.h index 8018f4006..002a049e3 100644 --- a/src/core/Utils.h +++ b/src/core/Utils.h @@ -4,8 +4,8 @@ #ifndef SCANTAILOR_CORE_UTILS_H_ #define SCANTAILOR_CORE_UTILS_H_ -#include #include +#include #include #include diff --git a/src/core/filters/output/BlackWhiteOptions.cpp b/src/core/filters/output/BlackWhiteOptions.cpp index 88e8551bf..f50404ebf 100644 --- a/src/core/filters/output/BlackWhiteOptions.cpp +++ b/src/core/filters/output/BlackWhiteOptions.cpp @@ -21,7 +21,7 @@ BlackWhiteOptions::BlackWhiteOptions() m_wolfLowerBound(1), m_wolfUpperBound(254), m_wolfCoef(0.3), - m_binarizationMethod(OTSU) {} + m_binarizationMethod(T_OTSU) {} BlackWhiteOptions::BlackWhiteOptions(const QDomElement& el) : m_thresholdAdjustment(el.attribute("thresholdAdj").toInt()), @@ -69,44 +69,49 @@ bool BlackWhiteOptions::operator!=(const BlackWhiteOptions& other) const { BinarizationMethod BlackWhiteOptions::parseBinarizationMethod(const QString& str) { if (str == "wolf") { - return WOLF; + return T_WOLF; } else if (str == "sauvola") { - return SAUVOLA; + return T_SAUVOLA; } else if (str == "bradley") { - return BRADLEY; + return T_BRADLEY; + } else if (str == "grad") { + return T_GRAD; } else if (str == "edgeplus") { - return EDGEPLUS; + return T_EDGEPLUS; } else if (str == "blurdiv") { - return BLURDIV; + return T_BLURDIV; } else if (str == "edgediv") { - return EDGEDIV; + return T_EDGEDIV; } else { - return OTSU; + return T_OTSU; } } QString BlackWhiteOptions::formatBinarizationMethod(BinarizationMethod type) { QString str = ""; switch (type) { - case OTSU: + case T_OTSU: str = "otsu"; break; - case SAUVOLA: + case T_SAUVOLA: str = "sauvola"; break; - case WOLF: + case T_WOLF: str = "wolf"; break; - case BRADLEY: + case T_BRADLEY: str = "bradley"; break; - case EDGEPLUS: + case T_GRAD: + str = "grad"; + break; + case T_EDGEPLUS: str = "edgeplus"; break; - case BLURDIV: + case T_BLURDIV: str = "blurdiv"; break; - case EDGEDIV: + case T_EDGEDIV: str = "edgediv"; break; } diff --git a/src/core/filters/output/BlackWhiteOptions.h b/src/core/filters/output/BlackWhiteOptions.h index 171da6ef4..ecf9b5265 100644 --- a/src/core/filters/output/BlackWhiteOptions.h +++ b/src/core/filters/output/BlackWhiteOptions.h @@ -9,7 +9,7 @@ class QDomDocument; class QDomElement; namespace output { -enum BinarizationMethod { OTSU, SAUVOLA, WOLF, BRADLEY, EDGEPLUS, BLURDIV, EDGEDIV }; +enum BinarizationMethod { T_OTSU, T_SAUVOLA, T_WOLF, T_BRADLEY, T_GRAD, T_EDGEPLUS, T_BLURDIV, T_EDGEDIV }; class BlackWhiteOptions { public: diff --git a/src/core/filters/output/OptionsWidget.cpp b/src/core/filters/output/OptionsWidget.cpp index 0ece0a63e..8eb945b10 100644 --- a/src/core/filters/output/OptionsWidget.cpp +++ b/src/core/filters/output/OptionsWidget.cpp @@ -39,13 +39,14 @@ OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelec colorModeSelector->addItem(tr("Color / Grayscale"), COLOR_GRAYSCALE); colorModeSelector->addItem(tr("Mixed"), MIXED); - thresholdMethodBox->addItem(tr("Otsu"), OTSU); - thresholdMethodBox->addItem(tr("Sauvola"), SAUVOLA); - thresholdMethodBox->addItem(tr("Wolf"), WOLF); - thresholdMethodBox->addItem(tr("Bradley"), BRADLEY); - thresholdMethodBox->addItem(tr("EdgePlus"), EDGEPLUS); - thresholdMethodBox->addItem(tr("BlurDiv"), BLURDIV); - thresholdMethodBox->addItem(tr("EdgeDiv"), EDGEDIV); + thresholdMethodBox->addItem(tr("Otsu"), T_OTSU); + thresholdMethodBox->addItem(tr("Sauvola"), T_SAUVOLA); + thresholdMethodBox->addItem(tr("Wolf"), T_WOLF); + thresholdMethodBox->addItem(tr("Bradley"), T_BRADLEY); + thresholdMethodBox->addItem(tr("Grad"), T_GRAD); + thresholdMethodBox->addItem(tr("EdgePlus"), T_EDGEPLUS); + thresholdMethodBox->addItem(tr("BlurDiv"), T_BLURDIV); + thresholdMethodBox->addItem(tr("EdgeDiv"), T_EDGEDIV); fillingColorBox->addItem(tr("Background"), FILL_BACKGROUND); fillingColorBox->addItem(tr("White"), FILL_WHITE); @@ -57,6 +58,7 @@ OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelec QPointer wolfBinarizationOptionsWidget = new WolfBinarizationOptionsWidget(m_settings); QPointer bradleyBinarizationOptionsWidget = new SauvolaBinarizationOptionsWidget(m_settings); + QPointer gradBinarizationOptionsWidget = new SauvolaBinarizationOptionsWidget(m_settings); QPointer edgeplusBinarizationOptionsWidget = new SauvolaBinarizationOptionsWidget(m_settings); QPointer blurdivBinarizationOptionsWidget @@ -71,6 +73,7 @@ OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelec addBinarizationOptionsWidget(sauvolaBinarizationOptionsWidget); addBinarizationOptionsWidget(wolfBinarizationOptionsWidget); addBinarizationOptionsWidget(bradleyBinarizationOptionsWidget); + addBinarizationOptionsWidget(gradBinarizationOptionsWidget); addBinarizationOptionsWidget(edgeplusBinarizationOptionsWidget); addBinarizationOptionsWidget(blurdivBinarizationOptionsWidget); addBinarizationOptionsWidget(edgedivBinarizationOptionsWidget); diff --git a/src/core/filters/output/OutputGenerator.cpp b/src/core/filters/output/OutputGenerator.cpp index be45f02cd..bc44ea9c4 100644 --- a/src/core/filters/output/OutputGenerator.cpp +++ b/src/core/filters/output/OutputGenerator.cpp @@ -2294,14 +2294,14 @@ BinaryImage OutputGenerator::Processor::binarize(const QImage& image) const { BinaryImage binarized; switch (binarizationMethod) { - case OTSU: { + case T_OTSU: { GrayscaleHistogram hist(image); const BinaryThreshold bwThresh(BinaryThreshold::otsuThreshold(hist)); binarized = BinaryImage(image, adjustThreshold(bwThresh)); break; } - case SAUVOLA: { + case T_SAUVOLA: { const double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); const QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); const double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); @@ -2309,7 +2309,7 @@ BinaryImage OutputGenerator::Processor::binarize(const QImage& image) const { binarized = binarizeSauvola(image, windowsSize, thresholdCoef, thresholdDelta); break; } - case WOLF: { + case T_WOLF: { const double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); const QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); const auto lowerBound = (unsigned char) blackWhiteOptions.getWolfLowerBound(); @@ -2319,7 +2319,7 @@ BinaryImage OutputGenerator::Processor::binarize(const QImage& image) const { binarized = binarizeWolf(image, windowsSize, lowerBound, upperBound, thresholdCoef, thresholdDelta); break; } - case BRADLEY: { + case T_BRADLEY: { const double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); const QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); const double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); @@ -2327,7 +2327,15 @@ BinaryImage OutputGenerator::Processor::binarize(const QImage& image) const { binarized = binarizeBradley(image, windowsSize, thresholdCoef, thresholdDelta); break; } - case EDGEPLUS: { + case T_GRAD: { + const double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); + const QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); + const double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); + + binarized = binarizeGrad(image, windowsSize, thresholdCoef, thresholdDelta); + break; + } + case T_EDGEPLUS: { const double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); const QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); const double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); @@ -2335,7 +2343,7 @@ BinaryImage OutputGenerator::Processor::binarize(const QImage& image) const { binarized = binarizeEdgeDiv(image, windowsSize, thresholdCoef, 0.0, thresholdDelta); break; } - case BLURDIV: { + case T_BLURDIV: { const double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); const QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); const double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); @@ -2343,7 +2351,7 @@ BinaryImage OutputGenerator::Processor::binarize(const QImage& image) const { binarized = binarizeEdgeDiv(image, windowsSize, 0.0, thresholdCoef, thresholdDelta); break; } - case EDGEDIV: { + case T_EDGEDIV: { const double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); const QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); const double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); diff --git a/src/imageproc/Binarize.cpp b/src/imageproc/Binarize.cpp index 1ecb5d9bf..b24c6ef44 100644 --- a/src/imageproc/Binarize.cpp +++ b/src/imageproc/Binarize.cpp @@ -32,6 +32,9 @@ BinaryImage binarizeSauvola(const QImage& src, const QSize windowSize, const dou } const QImage gray(toGrayscale(src)); + if (gray.isNull()) { + return BinaryImage(); + } const int w = gray.width(); const int h = gray.height(); @@ -114,6 +117,9 @@ BinaryImage binarizeWolf(const QImage& src, } const QImage gray(toGrayscale(src)); + if (gray.isNull()) { + return BinaryImage(); + } const int w = gray.width(); const int h = gray.height(); @@ -211,6 +217,9 @@ BinaryImage binarizeBradley(const QImage& src, const QSize windowSize, const dou } QImage gray(toGrayscale(src)); + if (gray.isNull()) { + return BinaryImage(); + } const int w = gray.width(); const int h = gray.height(); @@ -268,6 +277,120 @@ BinaryImage binarizeBradley(const QImage& src, const QSize windowSize, const dou return bwImg; } // binarizeBradley +BinaryImage binarizeGrad(const QImage& src, const QSize windowSize, const double k, const double delta) { + if (windowSize.isEmpty()) { + throw std::invalid_argument("binarizeGrad: invalid windowSize"); + } + + if (src.isNull()) { + return BinaryImage(); + } + + QImage gray(toGrayscale(src)); + if (gray.isNull()) { + return BinaryImage(); + } + QImage gmean(toGrayscale(src)); + if (gmean.isNull()) { + return BinaryImage(); + } + const int w = gray.width(); + const int h = gray.height(); + + const uint8_t* grayLine = gray.bits(); + const int grayBpl = gray.bytesPerLine(); + + uint8_t* gmeanLine = gmean.bits(); + const int gmeanBpl = gmean.bytesPerLine(); + + IntegralImage integralImage(w, h); + + for (int y = 0; y < h; ++y) { + integralImage.beginRow(); + for (int x = 0; x < w; ++x) { + const uint32_t pixel = grayLine[x]; + integralImage.push(pixel); + } + grayLine += grayBpl; + } + + const int windowLowerHalf = windowSize.height() >> 1; + const int windowUpperHalf = windowSize.height() - windowLowerHalf; + const int windowLeftHalf = windowSize.width() >> 1; + const int windowRightHalf = windowSize.width() - windowLeftHalf; + + for (int y = 0; y < h; ++y) { + const int top = std::max(0, y - windowLowerHalf); + const int bottom = std::min(h, y + windowUpperHalf); // exclusive + for (int x = 0; x < w; ++x) { + const int left = std::max(0, x - windowLeftHalf); + const int right = std::min(w, x + windowRightHalf); // exclusive + const int area = (bottom - top) * (right - left); + assert(area > 0); // because windowSize > 0 and w > 0 and h > 0 + const QRect rect(left, top, right - left, bottom - top); + const double windowSum = integralImage.sum(rect); + + const double rArea = 1.0 / area; + const double mean = windowSum * rArea + 0.5; + const int imean = (int) ((mean < 0.0) ? 0.0 : (mean < 255.0) ? mean : 255.0); + gmeanLine[x] = imean; + } + gmeanLine += gmeanBpl; + } + + double gvalue = 127.5; + double sum_g = 0.0, sum_gi = 0.0; + grayLine = gray.bits(); + gmeanLine = gmean.bits(); + for (int y = 0; y < h; y++) { + double sum_gl = 0.0; + double sum_gil = 0.0; + for (int x = 0; x < w; x++) { + double gi = grayLine[x]; + double g = gmeanLine[x]; + g -= gi; + g = (g < 0.0) ? -g : g; + gi *= g; + sum_gl += g; + sum_gil += gi; + } + sum_g += sum_gl; + sum_gi += sum_gil; + grayLine += grayBpl; + gmeanLine += gmeanBpl; + } + gvalue = (sum_g > 0.0) ? (sum_gi / sum_g) : gvalue; + + double const meanGrad = gvalue * (1.0 - k); + + BinaryImage bwImg(w, h); + uint32_t* bwLine = bwImg.data(); + const int bwWpl = bwImg.wordsPerLine(); + + grayLine = gray.bits(); + gmeanLine = gmean.bits(); + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + const double origin = grayLine[x]; + const double mean = gmeanLine[x]; + const double threshold = meanGrad + mean * k; + const uint32_t msb = uint32_t(1) << 31; + const uint32_t mask = msb >> (x & 31); + if (origin < (threshold + delta)) { + // black + bwLine[x >> 5] |= mask; + } else { + // white + bwLine[x >> 5] &= ~mask; + } + } + grayLine += grayBpl; + gmeanLine += gmeanBpl; + bwLine += bwWpl; + } + return bwImg; +} // binarizeGrad + BinaryImage binarizeEdgeDiv(const QImage& src, const QSize windowSize, const double kep, @@ -282,6 +405,9 @@ BinaryImage binarizeEdgeDiv(const QImage& src, } QImage gray(toGrayscale(src)); + if (gray.isNull()) { + return BinaryImage(); + } const int w = gray.width(); const int h = gray.height(); diff --git a/src/imageproc/Binarize.h b/src/imageproc/Binarize.h index 0adc332cf..481ff830b 100644 --- a/src/imageproc/Binarize.h +++ b/src/imageproc/Binarize.h @@ -69,6 +69,13 @@ BinaryImage binarizeWolf(const QImage& src, */ BinaryImage binarizeBradley(const QImage& src, QSize windowSize, double k = 0.34, double delta = 0.0); +/** + * \brief Image binarization using Grad local/global thresholding method. + * + * Grad (aka "Gradient snip"), zvezdochiot 2024. "Adaptive/global document image binarization". + */ +BinaryImage binarizeGrad(const QImage& src, QSize windowSize, double k = 0.34, double delta = 0.0); + /** * \brief Image binarization using EdgeDiv (EdgePlus & BlurDiv) local/global thresholding method. *