From ab6018d99deb49714bce697e9a25d84277930870 Mon Sep 17 00:00:00 2001 From: zvezdochiot Date: Sat, 12 Aug 2023 13:11:40 +0300 Subject: [PATCH] 1.0.19: colors: wiener denoiser --- .../filters/output/ColorCommonOptions.cpp | 25 ++- src/core/filters/output/ColorCommonOptions.h | 20 +++ src/core/filters/output/OptionsWidget.cpp | 22 +++ src/core/filters/output/OptionsWidget.h | 4 + src/core/filters/output/OptionsWidget.ui | 40 +++++ src/core/filters/output/OutputGenerator.cpp | 11 ++ src/imageproc/CMakeLists.txt | 3 +- src/imageproc/WienerFilter.cpp | 160 ++++++++++++++++++ src/imageproc/WienerFilter.h | 54 ++++++ 9 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 src/imageproc/WienerFilter.cpp create mode 100644 src/imageproc/WienerFilter.h diff --git a/src/core/filters/output/ColorCommonOptions.cpp b/src/core/filters/output/ColorCommonOptions.cpp index 6e53bf14f..e3d4bfdd0 100644 --- a/src/core/filters/output/ColorCommonOptions.cpp +++ b/src/core/filters/output/ColorCommonOptions.cpp @@ -7,14 +7,28 @@ namespace output { ColorCommonOptions::ColorCommonOptions() - : m_fillOffcut(true), m_fillMargins(true), m_normalizeIllumination(false), m_fillingColor(FILL_BACKGROUND) {} + : m_fillOffcut(true), + m_fillMargins(true), + m_normalizeIllumination(false), + m_fillingColor(FILL_BACKGROUND), + m_wienerCoef(0.0), + m_wienerWindowSize(5) {} ColorCommonOptions::ColorCommonOptions(const QDomElement& el) : m_fillOffcut(el.attribute("fillOffcut") == "1"), m_fillMargins(el.attribute("fillMargins") == "1"), m_normalizeIllumination(el.attribute("normalizeIlluminationColor") == "1"), m_fillingColor(parseFillingColor(el.attribute("fillingColor"))), - m_posterizationOptions(el.namedItem("posterization-options").toElement()) {} + m_posterizationOptions(el.namedItem("posterization-options").toElement()), + m_wienerCoef(el.attribute("wienerCoef").toDouble()), + m_wienerWindowSize(el.attribute("wienerWinSize").toInt()) { + if (m_wienerCoef < 0.0 || m_wienerCoef > 1.0) { + m_wienerCoef = 0.0; + } + if (m_wienerWindowSize < 3) { + m_wienerWindowSize = 5; + } +} QDomElement ColorCommonOptions::toXml(QDomDocument& doc, const QString& name) const { QDomElement el(doc.createElement(name)); @@ -23,13 +37,16 @@ QDomElement ColorCommonOptions::toXml(QDomDocument& doc, const QString& name) co el.setAttribute("normalizeIlluminationColor", m_normalizeIllumination ? "1" : "0"); el.setAttribute("fillingColor", formatFillingColor(m_fillingColor)); el.appendChild(m_posterizationOptions.toXml(doc, "posterization-options")); + el.setAttribute("wienerCoef", m_wienerCoef); + el.setAttribute("wienerWinSize", m_wienerWindowSize); return el; } bool ColorCommonOptions::operator==(const ColorCommonOptions& other) const { return (m_normalizeIllumination == other.m_normalizeIllumination) && (m_fillMargins == other.m_fillMargins) && (m_fillOffcut == other.m_fillOffcut) && (m_fillingColor == other.m_fillingColor) - && (m_posterizationOptions == other.m_posterizationOptions); + && (m_posterizationOptions == other.m_posterizationOptions) && (m_wienerCoef == other.m_wienerCoef) + && (m_wienerWindowSize == other.m_wienerWindowSize); } bool ColorCommonOptions::operator!=(const ColorCommonOptions& other) const { @@ -91,4 +108,4 @@ bool ColorCommonOptions::PosterizationOptions::operator==(const ColorCommonOptio bool ColorCommonOptions::PosterizationOptions::operator!=(const ColorCommonOptions::PosterizationOptions& other) const { return !(*this == other); } -} // namespace output \ No newline at end of file +} // namespace output diff --git a/src/core/filters/output/ColorCommonOptions.h b/src/core/filters/output/ColorCommonOptions.h index 9882ab81f..2adeb52d8 100644 --- a/src/core/filters/output/ColorCommonOptions.h +++ b/src/core/filters/output/ColorCommonOptions.h @@ -68,6 +68,11 @@ class ColorCommonOptions { void setNormalizeIllumination(bool val); + double wienerCoef() const; + void setWienerCoef(double val); + int wienerWindowSize() const; + void setWienerWindowSize(int val); + FillingColor getFillingColor() const; void setFillingColor(FillingColor fillingColor); @@ -89,6 +94,8 @@ class ColorCommonOptions { bool m_fillOffcut; bool m_fillMargins; bool m_normalizeIllumination; + double m_wienerCoef; + int m_wienerWindowSize; FillingColor m_fillingColor; PosterizationOptions m_posterizationOptions; }; @@ -118,6 +125,19 @@ inline void ColorCommonOptions::setNormalizeIllumination(bool val) { m_normalizeIllumination = val; } +inline double ColorCommonOptions::wienerCoef() const { + return m_wienerCoef; +} +inline void ColorCommonOptions::setWienerCoef(double val) { + m_wienerCoef = val; +} +inline int ColorCommonOptions::wienerWindowSize() const { + return m_wienerWindowSize; +} +inline void ColorCommonOptions::setWienerWindowSize(int val) { + m_wienerWindowSize = val; +} + inline const ColorCommonOptions::PosterizationOptions& ColorCommonOptions::getPosterizationOptions() const { return m_posterizationOptions; } diff --git a/src/core/filters/output/OptionsWidget.cpp b/src/core/filters/output/OptionsWidget.cpp index cd19356e2..0ece0a63e 100644 --- a/src/core/filters/output/OptionsWidget.cpp +++ b/src/core/filters/output/OptionsWidget.cpp @@ -634,6 +634,8 @@ void OptionsWidget::updateColorsDisplay() { posterizeCB->setEnabled(true); posterizeOptionsWidget->setEnabled(colorCommonOptions.getPosterizationOptions().isEnabled()); } + wienerCoef->setValue(colorCommonOptions.wienerCoef()); + wienerWindowSize->setValue(colorCommonOptions.wienerWindowSize()); colorSegmentationCB->setChecked(blackWhiteOptions.getColorSegmenterOptions().isEnabled()); reduceNoiseSB->setValue(blackWhiteOptions.getColorSegmenterOptions().getNoiseReduction()); redAdjustmentSB->setValue(blackWhiteOptions.getColorSegmenterOptions().getRedThresholdAdjustment()); @@ -764,6 +766,24 @@ void OptionsWidget::originalBackgroundToggled(bool checked) { emit reloadRequested(); } +void OptionsWidget::wienerCoefChanged(double value) { + ColorCommonOptions colorCommonOptions = m_colorParams.colorCommonOptions(); + colorCommonOptions.setWienerCoef(value); + m_colorParams.setColorCommonOptions(colorCommonOptions); + m_settings->setColorParams(m_pageId, m_colorParams); + + m_delayedReloadRequest.start(750); +} + +void OptionsWidget::wienerWindowSizeChanged(int value) { + ColorCommonOptions colorCommonOptions = m_colorParams.colorCommonOptions(); + colorCommonOptions.setWienerWindowSize(value); + m_colorParams.setColorCommonOptions(colorCommonOptions); + m_settings->setColorParams(m_pageId, m_colorParams); + + m_delayedReloadRequest.start(750); +} + void OptionsWidget::colorSegmentationToggled(bool checked) { BlackWhiteOptions blackWhiteOptions = m_colorParams.blackWhiteOptions(); BlackWhiteOptions::ColorSegmenterOptions segmenterOptions = blackWhiteOptions.getColorSegmenterOptions(); @@ -908,6 +928,8 @@ void OptionsWidget::setupUiConnections() { CONNECT(pictureShapeSensitivitySB, SIGNAL(valueChanged(int)), this, SLOT(pictureShapeSensitivityChanged(int))); CONNECT(higherSearchSensitivityCB, SIGNAL(clicked(bool)), this, SLOT(higherSearchSensivityToggled(bool))); + CONNECT(wienerCoef, SIGNAL(valueChanged(double)), this, SLOT(wienerCoefChanged(double))); + CONNECT(wienerWindowSize, SIGNAL(valueChanged(int)), this, SLOT(wienerWindowSizeChanged(int))); CONNECT(colorSegmentationCB, SIGNAL(clicked(bool)), this, SLOT(colorSegmentationToggled(bool))); CONNECT(reduceNoiseSB, SIGNAL(valueChanged(int)), this, SLOT(reduceNoiseChanged(int))); CONNECT(redAdjustmentSB, SIGNAL(valueChanged(int)), this, SLOT(redAdjustmentChanged(int))); diff --git a/src/core/filters/output/OptionsWidget.h b/src/core/filters/output/OptionsWidget.h index 9e73891d7..c828a61bf 100644 --- a/src/core/filters/output/OptionsWidget.h +++ b/src/core/filters/output/OptionsWidget.h @@ -94,6 +94,10 @@ class OptionsWidget : public FilterOptionsWidget, private Ui::OptionsWidget { void higherSearchSensivityToggled(bool checked); + void wienerCoefChanged(double value); + + void wienerWindowSizeChanged(int value); + void colorSegmentationToggled(bool checked); void reduceNoiseChanged(int value); diff --git a/src/core/filters/output/OptionsWidget.ui b/src/core/filters/output/OptionsWidget.ui index f936a67b1..9c77e168b 100644 --- a/src/core/filters/output/OptionsWidget.ui +++ b/src/core/filters/output/OptionsWidget.ui @@ -456,6 +456,46 @@ 5 + + + + + + Wiener denoiser + + + + + + + Value is 0.0 .. 1.0.. + + + 0.0 + + + 1.0 + + + 0.01 + + + + + + + The dimensions of a pixel neighborhood to consider. + + + 3 + + + 9999 + + + + + diff --git a/src/core/filters/output/OutputGenerator.cpp b/src/core/filters/output/OutputGenerator.cpp index b9a241010..be45f02cd 100644 --- a/src/core/filters/output/OutputGenerator.cpp +++ b/src/core/filters/output/OutputGenerator.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -1267,6 +1268,11 @@ std::unique_ptr OutputGenerator::Processor::processWithoutDewarping m_dbg->add(maybeNormalized, "maybeNormalized"); } + const ColorCommonOptions& colorCommonOptions = m_colorParams.colorCommonOptions(); + wienerColorFilterInPlace(maybeNormalized, + QSize(colorCommonOptions.wienerWindowSize(), colorCommonOptions.wienerWindowSize()), + colorCommonOptions.wienerCoef()); + if (m_renderParams.normalizeIllumination()) { m_outsideBackgroundColor = BackgroundColorCalculator::calcDominantBackgroundColor(maybeNormalized, m_outCropAreaInWorkingCs); @@ -1554,6 +1560,11 @@ std::unique_ptr OutputGenerator::Processor::processWithDewarping(Zo m_status.throwIfCancelled(); + const ColorCommonOptions& colorCommonOptions = m_colorParams.colorCommonOptions(); + wienerColorFilterInPlace(normalizedOriginal, + QSize(colorCommonOptions.wienerWindowSize(), colorCommonOptions.wienerWindowSize()), + colorCommonOptions.wienerCoef()); + // Picture mask (white indicate a picture) in the same coordinates as // warpedGrayOutput. Only built for Mixed mode. BinaryImage warpedBwMask; diff --git a/src/imageproc/CMakeLists.txt b/src/imageproc/CMakeLists.txt index a3c712f90..23035be5d 100644 --- a/src/imageproc/CMakeLists.txt +++ b/src/imageproc/CMakeLists.txt @@ -25,6 +25,7 @@ set(sources PolygonRasterizer.cpp PolygonRasterizer.h HoughLineDetector.cpp HoughLineDetector.h GaussBlur.cpp GaussBlur.h + WienerFilter.cpp WienerFilter.h Sobel.h MorphGradientDetect.cpp MorphGradientDetect.h PolynomialLine.cpp PolynomialLine.h @@ -57,4 +58,4 @@ add_library(imageproc STATIC ${sources}) target_link_libraries(imageproc PUBLIC foundation math) target_include_directories(imageproc PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") -add_subdirectory(tests) \ No newline at end of file +add_subdirectory(tests) diff --git a/src/imageproc/WienerFilter.cpp b/src/imageproc/WienerFilter.cpp new file mode 100644 index 000000000..7fdf10f88 --- /dev/null +++ b/src/imageproc/WienerFilter.cpp @@ -0,0 +1,160 @@ +/* + Scan Tailor - Interactive post-processing tool for scanned pages. + Copyright (C) 2015 Joseph Artsimovich + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "WienerFilter.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "GrayImage.h" +#include "IntegralImage.h" + +namespace imageproc { + +GrayImage wienerFilter(GrayImage const& image, QSize const& window_size, double const noise_sigma) { + GrayImage dst(image); + wienerFilterInPlace(dst, window_size, noise_sigma); + return dst; +} + +void wienerFilterInPlace(GrayImage& image, QSize const& window_size, double const noise_sigma) { + if (window_size.isEmpty()) { + throw std::invalid_argument("wienerFilter: empty window_size"); + } + if (noise_sigma < 0) { + throw std::invalid_argument("wienerFilter: negative noise_sigma"); + } + if (image.isNull()) { + return; + } + + int const w = image.width(); + int const h = image.height(); + double const noise_variance = noise_sigma * noise_sigma; + + IntegralImage integral_image(w, h); + IntegralImage integral_sqimage(w, h); + + uint8_t* image_line = image.data(); + int const image_stride = image.stride(); + + for (int y = 0; y < h; ++y) { + integral_image.beginRow(); + integral_sqimage.beginRow(); + for (int x = 0; x < w; ++x) { + uint32_t const pixel = image_line[x]; + integral_image.push(pixel); + integral_sqimage.push(pixel * pixel); + } + image_line += image_stride; + } + + int const window_lower_half = window_size.height() >> 1; + int const window_upper_half = window_size.height() - window_lower_half; + int const window_left_half = window_size.width() >> 1; + int const window_right_half = window_size.width() - window_left_half; + + image_line = image.data(); + for (int y = 0; y < h; ++y) { + int const top = ((y - window_lower_half) < 0) ? 0 : (y - window_lower_half); + int const bottom = ((y + window_upper_half) < h) ? (y + window_upper_half) : h; // exclusive + + for (int x = 0; x < w; ++x) { + int const left = ((x - window_left_half) < 0) ? 0 : (x - window_left_half); + int const right = ((x + window_right_half) < w) ? (x + window_right_half) : w; // exclusive + int const area = (bottom - top) * (right - left); + assert(area > 0); // because window_size > 0 and w > 0 and h > 0 + + QRect const rect(left, top, right - left, bottom - top); + double const window_sum = integral_image.sum(rect); + double const window_sqsum = integral_sqimage.sum(rect); + + double const r_area = 1.0 / area; + double const mean = window_sum * r_area; + double const sqmean = window_sqsum * r_area; + double const variance = sqmean - mean * mean; + + if (variance > 1e-6) { + double const src_pixel = (double) image_line[x]; + double const dst_pixel = mean + + (src_pixel - mean) + * (((variance - noise_variance) < 0.0) ? 0.0 : (variance - noise_variance)) + / variance; + image_line[x] = (uint8_t)((dst_pixel < 0.0) ? 0.0 : ((dst_pixel < 255.0) ? dst_pixel : 255.0)); + } + } + image_line += image_stride; + } +} + +QImage wienerColorFilter(QImage const& image, QSize const& window_size, double const coef) { + QImage dst(image); + wienerColorFilterInPlace(dst, window_size, coef); + return dst; +} + +void wienerColorFilterInPlace(QImage& image, QSize const& window_size, double const coef) { + if (image.isNull()) { + return; + } + if (window_size.isEmpty()) { + throw std::invalid_argument("wienerFilter: empty window_size"); + } + + if (coef > 0.0) { + int const w = image.width(); + int const h = image.height(); + uint8_t* image_line = (uint8_t*) image.bits(); + int const image_bpl = image.bytesPerLine(); + unsigned int const cnum = image_bpl / w; + + GrayImage gray = GrayImage(image); + uint8_t* gray_line = gray.data(); + int const gray_bpl = gray.stride(); + GrayImage wiener(wienerFilter(gray, window_size, 255.0 * coef)); + uint8_t* wiener_line = wiener.data(); + int const wiener_bpl = wiener.stride(); + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + float const origin = gray_line[x]; + float color = wiener_line[x]; + + float const colscale = (color + 1.0f) / (origin + 1.0f); + float const coldelta = color - origin * colscale; + for (unsigned int c = 0; c < cnum; ++c) { + int const indx = x * cnum + c; + float origcol = image_line[indx]; + float val = origcol * colscale + coldelta; + val = (val < 0.0f) ? 0.0f : (val < 255.0f) ? val : 255.0f; + image_line[indx] = (uint8_t)(val + 0.5f); + } + } + image_line += image_bpl; + gray_line += gray_bpl; + wiener_line += wiener_bpl; + } + } +} + +} // namespace imageproc diff --git a/src/imageproc/WienerFilter.h b/src/imageproc/WienerFilter.h new file mode 100644 index 000000000..b7cfd9e0f --- /dev/null +++ b/src/imageproc/WienerFilter.h @@ -0,0 +1,54 @@ +/* + Scan Tailor - Interactive post-processing tool for scanned pages. + Copyright (C) 2015 Joseph Artsimovich + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef SCANTAILOR_IMAGEPROC_WIENER_FILTER_H_ +#define SCANTAILOR_IMAGEPROC_WIENER_FILTER_H_ + +#include "GrayImage.h" + +class QImage; +class QSize; + +namespace imageproc { + +class GrayImage; + +/** + * @brief Applies the Wiener filter to a grayscale image. + * + * @param image The image to apply the filter to. A null image is allowed. + * @param window_size The local neighbourhood around a pixel to use. + * @param noise_sigma The standard deviation of noise in the image. + * @return The filtered image. + */ +GrayImage wienerFilter(GrayImage const& image, QSize const& window_size, double noise_sigma); + +/** + * @brief An in-place version of wienerFilter(). + * @see wienerFilter() + */ +void wienerFilterInPlace(GrayImage& image, QSize const& window_size, double noise_sigma); + + +QImage wienerColorFilter(QImage const& image, QSize const& window_size, double coef); + +void wienerColorFilterInPlace(QImage& image, QSize const& window_size, double coef); + +} // namespace imageproc + +#endif