Issue #107: Conversion algorithm

This commit is contained in:
Jakub Melka 2023-11-17 13:45:49 +01:00
parent cf7d65dc82
commit c5ddb521ed
6 changed files with 434 additions and 96 deletions

View File

@ -100,6 +100,8 @@ add_library(Pdf4QtLib SHARED
sources/pdfobjecteditorwidget_impl.h
sources/pdfdocumentsanitizer.h
sources/pdfdocumentsanitizer.cpp
sources/pdfimageconversion.h
sources/pdfimageconversion.cpp
cmaps.qrc
)

View File

@ -0,0 +1,188 @@
#include "pdfimageconversion.h"
namespace pdf
{
PDFImageConversion::PDFImageConversion()
{
}
void PDFImageConversion::setImage(QImage image)
{
m_image = std::move(image);
m_convertedImage = QImage();
m_automaticThreshold = DEFAULT_THRESHOLD;
}
void PDFImageConversion::setConversionMethod(ConversionMethod method)
{
m_conversionMethod = method;
}
void PDFImageConversion::setThreshold(int threshold)
{
m_manualThreshold = threshold;
}
bool PDFImageConversion::convert()
{
if (m_image.isNull())
{
return false;
}
QImage bitonal(m_image.width(), m_image.height(), QImage::Format_Mono);
bitonal.fill(0);
// Thresholding
int threshold = DEFAULT_THRESHOLD;
switch (m_conversionMethod)
{
case pdf::PDFImageConversion::ConversionMethod::Automatic:
m_automaticThreshold = calculateOtsu1DThreshold();
threshold = m_automaticThreshold;
break;
case pdf::PDFImageConversion::ConversionMethod::Manual:
threshold = m_manualThreshold;
break;
default:
Q_ASSERT(false);
break;
}
for (int y = 0; y < m_image.height(); ++y)
{
for (int x = 0; x < m_image.width(); ++x)
{
QColor pixelColor = m_image.pixelColor(x, y);
int pixelValue = pixelColor.lightness();
bool bit = (pixelValue >= threshold);
bitonal.setPixel(x, y, bit);
}
}
m_convertedImage = std::move(bitonal);
return true;
}
int PDFImageConversion::getThreshold() const
{
switch (m_conversionMethod)
{
case pdf::PDFImageConversion::ConversionMethod::Automatic:
return m_automaticThreshold;
case pdf::PDFImageConversion::ConversionMethod::Manual:
return m_manualThreshold;
default:
Q_ASSERT(false);
break;
}
return DEFAULT_THRESHOLD;
}
QImage PDFImageConversion::getConvertedImage() const
{
return m_convertedImage;
}
int PDFImageConversion::calculateOtsu1DThreshold() const
{
if (m_image.isNull())
{
return 128;
}
// Histogram of lightness occurences
std::array<int, 256> histogram = { };
for (int x = 0; x < m_image.width(); ++x)
{
for (int y = 0; y < m_image.height(); ++y)
{
int lightness = m_image.pixelColor(x, y).lightness();
Q_ASSERT(lightness >= 0 && lightness <= 255);
int clampedLightness = qBound(0, lightness, 255);
histogram[clampedLightness] += 1;
}
}
float factor = 1.0f / float(m_image.width() * m_image.height());
std::array<float, 256> normalizedHistogram = { };
std::array<float, 256> cumulativeProbabilities = { };
std::array<float, 256> interClassVariance = { };
// Compute probabilities
for (size_t i = 0; i < histogram.size(); ++i)
{
normalizedHistogram[i] = histogram[i] * factor;
cumulativeProbabilities[i] = normalizedHistogram[i];
if (i > 0)
{
cumulativeProbabilities[i] += cumulativeProbabilities[i - 1];
}
}
// Calculate the inter-class variance for each threshold. Variables
// with the subscript 0 denote the background, while those with
// subscript 1 denote the foreground.
for (size_t i = 0; i < histogram.size(); ++i)
{
const float w0 = cumulativeProbabilities[i] - normalizedHistogram[i];
const float w1 = 1.0f - w0;
float u0 = 0.0f;
float u1 = 0.0f;
// Calculate mean intensity value of the background.
if (!qFuzzyIsNull(w0))
{
for (size_t j = 0; j < i; ++j)
{
u0 += j * normalizedHistogram[j];
}
u0 /= w0;
}
// Calculate mean intensity value of the foreground.
if (!qFuzzyIsNull(w1))
{
for (size_t j = i; j < histogram.size(); ++j)
{
u1 += j * normalizedHistogram[j];
}
u1 /= w1;
}
const float variance = w0 * w1 * std::powf(u0 - u1, 2);
interClassVariance[i] = variance;
}
// Find maximal value of the variance
size_t maxVarianceIndex = 0;
float maxVarianceValue = 0.0f;
for (size_t i = 0; i < interClassVariance.size(); ++i)
{
if (interClassVariance[i] > maxVarianceValue)
{
maxVarianceValue = interClassVariance[i];
maxVarianceIndex = i;
}
}
return int(maxVarianceIndex);
}
} // namespace pdf

View File

@ -0,0 +1,97 @@
// Copyright (C) 2023 Jakub Melka
//
// This file is part of PDF4QT.
//
// PDF4QT is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// with the written consent of the copyright owner, any later version.
//
// PDF4QT 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with PDF4QT. If not, see <https://www.gnu.org/licenses/>.
#ifndef PDFIMAGECONVERSION_H
#define PDFIMAGECONVERSION_H
#include "pdfglobal.h"
#include <QImage>
namespace pdf
{
/// This class facilitates various image conversions,
/// including transforming colored images into monochromatic (or bitonal) formats.
class PDF4QTLIBSHARED_EXPORT PDFImageConversion
{
public:
PDFImageConversion();
enum class ConversionMethod
{
Automatic, ///< The threshold is determined automatically using an algorithm.
Manual ///< The threshold is manually provided by the user.
};
/// Sets the image to be converted using the specified conversion method.
/// This operation resets any previously converted image and the automatic threshold,
/// thereby erasing all prior image data.
/// \param image The image to be set for conversion.
void setImage(QImage image);
/// Sets the method for image conversion. Multiple methods are available
/// for selection. If the manual method is chosen, an appropriate threshold
/// must also be set by the user.
/// \param method The conversion method to be used.
void setConversionMethod(ConversionMethod method);
/// Sets the manual threshold value. When a non-manual (e.g., automatic) conversion
/// method is in use, this function will retain the manual threshold settings,
/// but the conversion will utilize an automatically calculated threshold for the image.
/// The manually set threshold is preserved and not overwritten. Therefore, if the
/// manual conversion method is later selected, the previously established manual
/// threshold will be applied.
/// \param threshold The manual threshold value to be set.
void setThreshold(int threshold);
/// This method converts the image into a bitonal (monochromatic) format. If
/// the automatic threshold calculation is enabled, it executes Otsu's 1D algorithm
/// to determine the threshold. When the manual conversion method is selected,
/// the automatic threshold calculation is bypassed, and the predefined manual threshold
/// value is utilized instead. This method returns true if the conversion is
/// successful, and false otherwise.
bool convert();
/// Returns the threshold used in image conversion. If the automatic conversion method is
/// selected, this function should be called only after executing the convert() method;
/// otherwise, it may return invalid data. The automatic threshold calculation is
/// performed within the convert() method.
/// \returns The threshold value used in image conversion.
int getThreshold() const;
/// Returns the converted image. This method should only be called after
/// the convert() method has been executed, and additionally, only if the
/// convert() method returns true. If these conditions are not met, the result
/// is undefined.
QImage getConvertedImage() const;
private:
int calculateOtsu1DThreshold() const;
static constexpr int DEFAULT_THRESHOLD = 128;
QImage m_image;
QImage m_convertedImage;
ConversionMethod m_conversionMethod = ConversionMethod::Automatic;
int m_automaticThreshold = DEFAULT_THRESHOLD;
int m_manualThreshold = DEFAULT_THRESHOLD;
};
} // namespace pdf
#endif // PDFIMAGECONVERSION_H

View File

@ -24,6 +24,7 @@
#include "pdfdbgheap.h"
#include "pdfexception.h"
#include "pdfwidgetutils.h"
#include "pdfimageconversion.h"
#include <QCheckBox>
#include <QPushButton>
@ -39,6 +40,64 @@
namespace pdfviewer
{
PDFCreateBitonalDocumentPreviewWidget::PDFCreateBitonalDocumentPreviewWidget(QWidget* parent) :
QWidget(parent)
{
}
PDFCreateBitonalDocumentPreviewWidget::~PDFCreateBitonalDocumentPreviewWidget()
{
}
void PDFCreateBitonalDocumentPreviewWidget::paintEvent(QPaintEvent* event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.fillRect(rect(), Qt::white);
// Caption rect
QRect captionRect = rect();
captionRect.setHeight(painter.fontMetrics().lineSpacing() * 2);
painter.fillRect(captionRect, QColor::fromRgb(0, 0, 128, 255));
if (!m_caption.isEmpty())
{
painter.setPen(Qt::white);
painter.drawText(captionRect, m_caption, QTextOption(Qt::AlignCenter));
}
QRect imageRect = rect();
imageRect.setTop(captionRect.bottom());
imageRect = imageRect.adjusted(16, 16, -32, -32);
if (imageRect.isValid() && !m_image.isNull())
{
QRect imageDrawRect = imageRect;
imageDrawRect.setSize(m_image.size().scaled(imageRect.size(), Qt::KeepAspectRatio));
imageDrawRect.moveCenter(imageRect.center());
painter.drawImage(imageDrawRect, m_image);
}
}
void PDFCreateBitonalDocumentPreviewWidget::setCaption(QString caption)
{
if (m_caption != caption)
{
m_caption = caption;
update();
}
}
void PDFCreateBitonalDocumentPreviewWidget::setImage(QImage image)
{
m_image = std::move(image);
update();
}
class ImagePreviewDelegate : public QStyledItemDelegate
{
public:
@ -148,10 +207,18 @@ PDFCreateBitonalDocumentDialog::PDFCreateBitonalDocumentDialog(const pdf::PDFDoc
m_cms(cms),
m_createBitonalDocumentButton(nullptr),
m_conversionInProgress(false),
m_processed(false)
m_processed(false),
m_leftPreviewWidget(new PDFCreateBitonalDocumentPreviewWidget(this)),
m_rightPreviewWidget(new PDFCreateBitonalDocumentPreviewWidget(this))
{
ui->setupUi(this);
m_leftPreviewWidget->setCaption(tr("ORIGINAL"));
m_rightPreviewWidget->setCaption(tr("BITONIC"));
ui->mainGridLayout->addWidget(m_leftPreviewWidget, 1, 1);
ui->mainGridLayout->addWidget(m_rightPreviewWidget, 1, 2);
m_classifier.classify(document);
m_imageReferences = m_classifier.getObjectsByType(pdf::PDFObjectClassifier::Image);
@ -159,6 +226,10 @@ PDFCreateBitonalDocumentDialog::PDFCreateBitonalDocumentDialog(const pdf::PDFDoc
connect(m_createBitonalDocumentButton, &QPushButton::clicked, this, &PDFCreateBitonalDocumentDialog::onCreateBitonalDocumentButtonClicked);
connect(ui->automaticThresholdRadioButton, &QRadioButton::clicked, this, &PDFCreateBitonalDocumentDialog::updateUi);
connect(ui->manualThresholdRadioButton, &QRadioButton::clicked, this, &PDFCreateBitonalDocumentDialog::updateUi);
connect(ui->automaticThresholdRadioButton, &QRadioButton::clicked, this, &PDFCreateBitonalDocumentDialog::updatePreview);
connect(ui->manualThresholdRadioButton, &QRadioButton::clicked, this, &PDFCreateBitonalDocumentDialog::updatePreview);
connect(ui->imageListWidget, &QListWidget::currentItemChanged, this, &PDFCreateBitonalDocumentDialog::updatePreview);
connect(ui->thresholdEditBox, &QSpinBox::editingFinished, this, &PDFCreateBitonalDocumentDialog::updatePreview);
pdf::PDFWidgetUtils::scaleWidget(this, QSize(640, 380));
updateUi();
@ -167,6 +238,7 @@ PDFCreateBitonalDocumentDialog::PDFCreateBitonalDocumentDialog(const pdf::PDFDoc
ui->imageListWidget->setItemDelegate( new ImagePreviewDelegate(&m_imagesToBeConverted, this));
loadImages();
updatePreview();
}
PDFCreateBitonalDocumentDialog::~PDFCreateBitonalDocumentDialog()
@ -243,6 +315,40 @@ void PDFCreateBitonalDocumentDialog::updateUi()
m_createBitonalDocumentButton->setEnabled(!m_conversionInProgress);
}
void PDFCreateBitonalDocumentDialog::updatePreview()
{
QModelIndex index = ui->imageListWidget->currentIndex();
m_previewImageLeft = QImage();
m_previewImageRight = QImage();
if (index.isValid())
{
const ImageConversionInfo& info = m_imagesToBeConverted.at(index.row());
std::optional<pdf::PDFImage> pdfImage = getImageFromReference(info.imageReference);
Q_ASSERT(pdfImage);
pdf::PDFCMSGeneric cmsGeneric;
pdf::PDFRenderErrorReporterDummy reporter;
QImage image = pdfImage->getImage(&cmsGeneric, &reporter, nullptr);
pdf::PDFImageConversion imageConversion;
imageConversion.setConversionMethod(ui->automaticThresholdRadioButton->isChecked() ? pdf::PDFImageConversion::ConversionMethod::Automatic : pdf::PDFImageConversion::ConversionMethod::Manual);
imageConversion.setThreshold(ui->thresholdEditBox->value());
imageConversion.setImage(image);
if (imageConversion.convert())
{
m_previewImageLeft = image;
m_previewImageRight = imageConversion.getConvertedImage();
}
}
m_leftPreviewWidget->setImage(m_previewImageLeft);
m_rightPreviewWidget->setImage(m_previewImageRight);
}
std::optional<pdf::PDFImage> PDFCreateBitonalDocumentDialog::getImageFromReference(pdf::PDFObjectReference reference) const
{
std::optional<pdf::PDFImage> pdfImage;
@ -285,3 +391,5 @@ std::optional<pdf::PDFImage> PDFCreateBitonalDocumentDialog::getImageFromReferen
}
} // namespace pdfviewer

View File

@ -34,6 +34,24 @@ class PDFCreateBitonalDocumentDialog;
namespace pdfviewer
{
class PDFCreateBitonalDocumentPreviewWidget : public QWidget
{
Q_OBJECT
public:
PDFCreateBitonalDocumentPreviewWidget(QWidget* parent);
virtual ~PDFCreateBitonalDocumentPreviewWidget() override;
virtual void paintEvent(QPaintEvent* event) override;
void setCaption(QString caption);
void setImage(QImage image);
private:
QString m_caption;
QImage m_image;
};
class PDFCreateBitonalDocumentDialog : public QDialog
{
Q_OBJECT
@ -56,6 +74,7 @@ private:
void loadImages();
void updateUi();
void updatePreview();
std::optional<pdf::PDFImage> getImageFromReference(pdf::PDFObjectReference reference) const;
@ -70,6 +89,12 @@ private:
pdf::PDFObjectClassifier m_classifier;
std::vector<pdf::PDFObjectReference> m_imageReferences;
std::vector<ImageConversionInfo> m_imagesToBeConverted;
QImage m_previewImageLeft;
QImage m_previewImageRight;
PDFCreateBitonalDocumentPreviewWidget* m_leftPreviewWidget;
PDFCreateBitonalDocumentPreviewWidget* m_rightPreviewWidget;
};
} // namespace pdfviewer

View File

@ -13,13 +13,23 @@
<property name="windowTitle">
<string>Create Bitonal Document</string>
</property>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,1,0" columnstretch="2,1,1">
<layout class="QGridLayout" name="mainGridLayout" rowstretch="0,1,0" columnstretch="2,1,1">
<item row="2" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QGroupBox" name="createBitonalSettingsGroupBox">
<property name="title">
<string>Color to Bitonal Conversion Options</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<layout class="QGridLayout" name="optionsGridLayout">
<item row="0" column="0">
<widget class="QRadioButton" name="automaticThresholdRadioButton">
<property name="text">
@ -50,99 +60,7 @@
</layout>
</widget>
</item>
<item row="1" column="1" rowspan="2">
<widget class="QLabel" name="originalImageLabel">
<property name="palette">
<palette>
<active>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>240</red>
<green>240</green>
<blue>240</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
</widget>
</item>
<item row="1" column="2" rowspan="2">
<widget class="QLabel" name="convertedImageLabel">
<property name="palette">
<palette>
<active>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>240</red>
<green>240</green>
<blue>240</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="0" column="0" rowspan="3">
<item row="0" column="0" rowspan="2">
<widget class="QListWidget" name="imageListWidget">
<property name="resizeMode">
<enum>QListView::Adjust</enum>