diff --git a/src/app/DefaultParamsDialog.cpp b/src/app/DefaultParamsDialog.cpp index 792a57dc9..a011089ba 100644 --- a/src/app/DefaultParamsDialog.cpp +++ b/src/app/DefaultParamsDialog.cpp @@ -50,6 +50,7 @@ DefaultParamsDialog::DefaultParamsDialog(QWidget* parent) thresholdMethodBox->addItem(tr("Wolf"), WOLF); thresholdMethodBox->addItem(tr("EdgePlus"), EDGEPLUS); thresholdMethodBox->addItem(tr("BlurDiv"), BLURDIV); + thresholdMethodBox->addItem(tr("EdgeDiv"), EDGEDIV); pictureShapeSelector->addItem(tr("Off"), OFF_SHAPE); pictureShapeSelector->addItem(tr("Free"), FREE_SHAPE); @@ -664,7 +665,7 @@ std::unique_ptr DefaultParamsDialog::buildParams() const { blackWhiteOptions.setBinarizationMethod(binarizationMethod); blackWhiteOptions.setThresholdAdjustment(thresholdSlider->value()); blackWhiteOptions.setSauvolaCoef(sauvolaCoef->value()); - if (binarizationMethod == SAUVOLA || binarizationMethod == EDGEPLUS || binarizationMethod == BLURDIV) { + if (binarizationMethod == SAUVOLA || binarizationMethod == EDGEPLUS || binarizationMethod == BLURDIV || binarizationMethod == EDGEDIV) { blackWhiteOptions.setWindowSize(sauvolaWindowSize->value()); } else if (binarizationMethod == WOLF) { blackWhiteOptions.setWindowSize(wolfWindowSize->value()); diff --git a/src/core/filters/output/BlackWhiteOptions.cpp b/src/core/filters/output/BlackWhiteOptions.cpp index 8f88921c5..235789cfe 100644 --- a/src/core/filters/output/BlackWhiteOptions.cpp +++ b/src/core/filters/output/BlackWhiteOptions.cpp @@ -76,6 +76,8 @@ BinarizationMethod BlackWhiteOptions::parseBinarizationMethod(const QString& str return EDGEPLUS; } else if (str == "blurdiv") { return BLURDIV; + } else if (str == "edgediv") { + return EDGEDIV; } else { return OTSU; } @@ -99,6 +101,9 @@ QString BlackWhiteOptions::formatBinarizationMethod(BinarizationMethod type) { case BLURDIV: str = "blurdiv"; break; + case EDGEDIV: + str = "edgediv"; + break; } return str; } diff --git a/src/core/filters/output/BlackWhiteOptions.h b/src/core/filters/output/BlackWhiteOptions.h index 5445f1917..2709b6428 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, EDGEPLUS, BLURDIV }; +enum BinarizationMethod { OTSU, SAUVOLA, WOLF, EDGEPLUS, BLURDIV, EDGEDIV }; class BlackWhiteOptions { public: diff --git a/src/core/filters/output/OptionsWidget.cpp b/src/core/filters/output/OptionsWidget.cpp index d38747a07..9111a7c28 100644 --- a/src/core/filters/output/OptionsWidget.cpp +++ b/src/core/filters/output/OptionsWidget.cpp @@ -44,6 +44,7 @@ OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelec thresholdMethodBox->addItem(tr("Wolf"), WOLF); thresholdMethodBox->addItem(tr("EdgePlus"), EDGEPLUS); thresholdMethodBox->addItem(tr("BlurDiv"), BLURDIV); + thresholdMethodBox->addItem(tr("EdgeDiv"), EDGEDIV); fillingColorBox->addItem(tr("Background"), FILL_BACKGROUND); fillingColorBox->addItem(tr("White"), FILL_WHITE); @@ -57,6 +58,8 @@ OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelec = new SauvolaBinarizationOptionsWidget(m_settings); QPointer blurdivBinarizationOptionsWidget = new SauvolaBinarizationOptionsWidget(m_settings); + QPointer edgedivBinarizationOptionsWidget + = new SauvolaBinarizationOptionsWidget(m_settings); while (binarizationOptions->count() != 0) { binarizationOptions->removeWidget(binarizationOptions->widget(0)); @@ -66,6 +69,7 @@ OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelec addBinarizationOptionsWidget(wolfBinarizationOptionsWidget); addBinarizationOptionsWidget(edgeplusBinarizationOptionsWidget); addBinarizationOptionsWidget(blurdivBinarizationOptionsWidget); + addBinarizationOptionsWidget(edgedivBinarizationOptionsWidget); updateBinarizationOptionsDisplay(binarizationOptions->currentIndex()); pictureShapeSelector->addItem(tr("Off"), OFF_SHAPE); diff --git a/src/core/filters/output/OutputGenerator.cpp b/src/core/filters/output/OutputGenerator.cpp index 969fc0349..f2f9848e3 100644 --- a/src/core/filters/output/OutputGenerator.cpp +++ b/src/core/filters/output/OutputGenerator.cpp @@ -2219,37 +2219,45 @@ BinaryImage OutputGenerator::Processor::binarize(const QImage& image) const { break; } case SAUVOLA: { - double sauvolaDelta = blackWhiteOptions.thresholdAdjustment(); + double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); - double sauvolaCoef = blackWhiteOptions.getSauvolaCoef(); + double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); - binarized = binarizeSauvola(image, windowsSize, sauvolaCoef, sauvolaDelta); + binarized = binarizeSauvola(image, windowsSize, thresholdCoef, thresholdDelta); break; } case WOLF: { - double wolfDelta = blackWhiteOptions.thresholdAdjustment(); + double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); auto lowerBound = (unsigned char) blackWhiteOptions.getWolfLowerBound(); auto upperBound = (unsigned char) blackWhiteOptions.getWolfUpperBound(); - double wolfCoef = blackWhiteOptions.getWolfCoef(); + double thresholdCoef = blackWhiteOptions.getWolfCoef(); - binarized = binarizeWolf(image, windowsSize, lowerBound, upperBound, wolfCoef, wolfDelta); + binarized = binarizeWolf(image, windowsSize, lowerBound, upperBound, thresholdCoef, thresholdDelta); break; } case EDGEPLUS: { - double sauvolaDelta = blackWhiteOptions.thresholdAdjustment(); + double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); - double sauvolaCoef = blackWhiteOptions.getSauvolaCoef(); + double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); - binarized = binarizeEdgePlus(image, windowsSize, sauvolaCoef, sauvolaDelta); + binarized = binarizeEdgePlus(image, windowsSize, thresholdCoef, thresholdDelta); break; } case BLURDIV: { - double sauvolaDelta = blackWhiteOptions.thresholdAdjustment(); + double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); - double sauvolaCoef = blackWhiteOptions.getSauvolaCoef(); + double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); - binarized = binarizeBlurDiv(image, windowsSize, sauvolaCoef, sauvolaDelta); + binarized = binarizeBlurDiv(image, windowsSize, thresholdCoef, thresholdDelta); + break; + } + case EDGEDIV: { + double thresholdDelta = blackWhiteOptions.thresholdAdjustment(); + QSize windowsSize = QSize(blackWhiteOptions.getWindowSize(), blackWhiteOptions.getWindowSize()); + double thresholdCoef = blackWhiteOptions.getSauvolaCoef(); + + binarized = binarizeEdgeDiv(image, windowsSize, thresholdCoef, thresholdDelta); break; } } diff --git a/src/imageproc/Binarize.cpp b/src/imageproc/Binarize.cpp index 1f542c946..08806c9f6 100644 --- a/src/imageproc/Binarize.cpp +++ b/src/imageproc/Binarize.cpp @@ -41,7 +41,7 @@ BinaryImage binarizeSauvola(const QImage& src, const QSize windowSize, const dou const uint8_t* grayLine = gray.bits(); const int grayBpl = gray.bytesPerLine(); - for (int y = 0; y < h; ++y, grayLine += grayBpl) { + for (int y = 0; y < h; ++y) { integralImage.beginRow(); integralSqimage.beginRow(); for (int x = 0; x < w; ++x) { @@ -49,6 +49,7 @@ BinaryImage binarizeSauvola(const QImage& src, const QSize windowSize, const dou integralImage.push(pixel); integralSqimage.push(pixel * pixel); } + grayLine += grayBpl; } const int windowLowerHalf = windowSize.height() >> 1; @@ -92,7 +93,6 @@ BinaryImage binarizeSauvola(const QImage& src, const QSize windowSize, const dou bwLine[x >> 5] &= ~mask; } } - grayLine += grayBpl; bwLine += bwWpl; } @@ -125,7 +125,7 @@ BinaryImage binarizeWolf(const QImage& src, uint32_t minGrayLevel = 255; - for (int y = 0; y < h; ++y, grayLine += grayBpl) { + for (int y = 0; y < h; ++y) { integralImage.beginRow(); integralSqimage.beginRow(); for (int x = 0; x < w; ++x) { @@ -134,6 +134,7 @@ BinaryImage binarizeWolf(const QImage& src, integralSqimage.push(pixel * pixel); minGrayLevel = std::min(minGrayLevel, pixel); } + grayLine += grayBpl; } const int windowLowerHalf = windowSize.height() >> 1; @@ -177,7 +178,7 @@ BinaryImage binarizeWolf(const QImage& src, const int bwWpl = bwImg.wordsPerLine(); grayLine = gray.bits(); - for (int y = 0; y < h; ++y, grayLine += grayBpl, bwLine += bwWpl) { + for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { const float mean = means[y * w + x]; const float deviation = deviations[y * w + x]; @@ -194,6 +195,8 @@ BinaryImage binarizeWolf(const QImage& src, bwLine[x >> 5] &= ~mask; } } + grayLine += grayBpl; + bwLine += bwWpl; } return bwImg; } // binarizeWolf @@ -216,12 +219,13 @@ BinaryImage binarizeEdgePlus(const QImage& src, const QSize windowSize, const do uint8_t* grayLine = gray.bits(); const int grayBpl = gray.bytesPerLine(); - for (int y = 0; y < h; ++y, grayLine += grayBpl) { + 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; @@ -277,12 +281,13 @@ BinaryImage binarizeBlurDiv(const QImage& src, const QSize windowSize, const dou uint8_t* grayLine = gray.bits(); const int grayBpl = gray.bytesPerLine(); - for (int y = 0; y < h; ++y, grayLine += grayBpl) { + 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; @@ -320,6 +325,76 @@ BinaryImage binarizeBlurDiv(const QImage& src, const QSize windowSize, const dou return BinaryImage(gray, (BinaryThreshold::otsuThreshold(gray) + delta)); } // binarizeBlurDiv +BinaryImage binarizeEdgeDiv(const QImage& src, const QSize windowSize, const double k, const double delta) { + if (windowSize.isEmpty()) { + throw std::invalid_argument("binarizeBlurDiv: invalid windowSize"); + } + + if (src.isNull()) { + return BinaryImage(); + } + + QImage gray(toGrayscale(src)); + const int w = gray.width(); + const int h = gray.height(); + + IntegralImage integralImage(w, h); + + uint8_t* grayLine = gray.bits(); + const int grayBpl = gray.bytesPerLine(); + + 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; + + grayLine = gray.bits(); + 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; + const double origin = grayLine[x]; + // EdgePlus + // edge = I / blur (shift = -0.5) {0.0 .. >1.0}, mean value = 0.5 + const double edge = (origin + 1) / (mean + 1) - 0.5; + // edgeplus = I * edge, mean value = 0.5 * mean(I) + const double edgeplus = origin * edge; + // return k * edgeplus + (1 - k) * I + double retval = k * edgeplus + (1.0 - k) * origin; + // BlurDiv + // edge = blur / I (shift = -0.5) {0.0 .. >1.0}, mean value = 0.5 + const double edgeinv = (mean + 1) / (retval + 1) - 0.5; + // edgenorm = edge * k + max * (1 - k), mean value = {0.5 .. 1.0} * mean(I) + const double edgenorm = k * edgeinv + (1.0 - k); + // return I / edgenorm + retval = (edgenorm > 0.0) ? (origin / edgenorm) : origin; + // trim value {0..255} + retval = (retval < 0.0) ? 0.0 : (retval < 255.0) ? retval : 255.0; + grayLine[x] = (int) retval; + } + grayLine += grayBpl; + } + return BinaryImage(gray, (BinaryThreshold::otsuThreshold(gray) + delta)); +} // binarizeBlurDiv + BinaryImage peakThreshold(const QImage& image) { return BinaryImage(image, BinaryThreshold::peakThreshold(image)); } diff --git a/src/imageproc/Binarize.h b/src/imageproc/Binarize.h index 9d98dbf29..33d7d0a5b 100644 --- a/src/imageproc/Binarize.h +++ b/src/imageproc/Binarize.h @@ -68,7 +68,6 @@ BinaryImage binarizeWolf(const QImage& src, */ BinaryImage binarizeEdgePlus(const QImage& src, QSize windowSize, double k = 0.34, double delta = 0.0); - /** * \brief Image binarization using BlurDiv local/global thresholding method. * @@ -76,6 +75,12 @@ BinaryImage binarizeEdgePlus(const QImage& src, QSize windowSize, double k = 0.3 */ BinaryImage binarizeBlurDiv(const QImage& src, QSize windowSize, double k = 0.34, double delta = 0.0); +/** + * \brief Image binarization using EdgeDiv (EdgePlus & BlurDiv) local/global thresholding method. + * + * EdgeDiv, zvezdochiot 2023. "Adaptive/global document image binarization". + */ +BinaryImage binarizeEdgeDiv(const QImage& src, QSize windowSize, double k = 0.34, double delta = 0.0); BinaryImage peakThreshold(const QImage& image); } // namespace imageproc