OpenCV_Example/sources/main_window.cpp

820 lines
24 KiB
C++

/****************************************************************************
** Copyright (c) 2025 Evgeny Teterin (nayk) <nayk@nxt.ru>
** All right reserved.
**
** Permission is hereby granted, free of charge, to any person obtaining
** a copy of this software and associated documentation files (the
** "Software"), to deal in the Software without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Software, and to
** permit persons to whom the Software is furnished to do so, subject to
** the following conditions:
**
** The above copyright notice and this permission notice shall be
** included in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
**
****************************************************************************/
#include "main_window.h"
#include "./ui_main_window.h"
#include <QtWidgets/QScrollBar>
#include <QtWidgets/QLabel>
#include <QtCore/QDebug>
#include <QtGui/QPixmap>
#include <QtWidgets/QVBoxLayout>
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QFileDialog>
#include <QtGui/QResizeEvent>
#include <QtWidgets/QMessageBox>
#include <math.h>
#include <float.h>
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/types_c.h>
#include <opencv2/core/types.hpp>
//==============================================================================
// Функции обработки изображений OpenCV
//==============================================================================
cv::Mat qImageToMat(const QImage &image)
{
qDebug() << "qImageToMat";
cv::Mat result;
if (image.isNull()) return result;
QImage img = image.convertToFormat( QImage::Format_RGB888 );
cv::Mat tmp(img.height(), img.width(), CV_8UC3,
const_cast<uchar*>(img.bits()),
static_cast<size_t>(img.bytesPerLine()));
try {
cv::cvtColor(tmp, result, CV_BGR2RGB);
} catch (...) {
return cv::Mat();
}
return result.clone();
}
//==============================================================================
QImage matToQImage(const cv::Mat &mat)
{
qDebug() << "matToQImage";
if(mat.empty()) {
return QImage();
}
cv::Mat temp;
try {
cv::cvtColor(mat, temp, CV_BGR2RGB);
} catch (...) {
return QImage();
}
if(temp.empty()) {
return QImage();
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QImage dest( const_cast<uchar *>(temp.data),
temp.cols, temp.rows,
static_cast<qsizetype>(temp.step), QImage::Format_RGB888);
#else
QImage dest( const_cast<uchar *>(temp.data),
temp.cols, temp.rows,
static_cast<int>(temp.step), QImage::Format_RGB888);
#endif
if(dest.isNull()) return QImage();
return dest.copy();
}
//==============================================================================
cv::Mat applyGain(const cv::Mat &inputImage, double gain)
{
qDebug() << "applyGain";
cv::Mat outputImage;
if (inputImage.empty()) return outputImage;
try {
// Умножаем каждый пиксель на коэффициент усиления
inputImage.convertTo(outputImage, -1, gain, 0);
cv::threshold(outputImage, outputImage, 255, 255, cv::THRESH_TRUNC);
}
catch (...) {
outputImage = cv::Mat();
}
return outputImage;
}
//==============================================================================
cv::Mat adjustBrightnessContrast(const cv::Mat &img, double alpha, int beta)
{
qDebug() << "adjustBrightnessContrast";
cv::Mat adjusted;
if (img.empty()) return adjusted;
try {
img.convertTo(adjusted, -1, alpha, beta); // alpha - контраст, beta - яркость
cv::threshold(adjusted, adjusted, 255, 255, cv::THRESH_TRUNC);
}
catch (...) {
adjusted = cv::Mat();
}
return adjusted;
}
//==============================================================================
cv::Mat equalizeColorHist(const cv::Mat &bgrImg)
{
qDebug() << "equalizeColorHist";
cv::Mat result;
if (bgrImg.empty()) return result;
try {
cv::Mat ycrcb;
cv::cvtColor(bgrImg, ycrcb, cv::COLOR_BGR2YCrCb);
std::vector<cv::Mat> channels;
cv::split(ycrcb, channels);
cv::equalizeHist(channels[0], channels[0]); // Эквализация только по Y (яркость)
cv::merge(channels, ycrcb);
cv::cvtColor(ycrcb, result, cv::COLOR_YCrCb2BGR);
}
catch (...) {
result = cv::Mat();
}
return result;
}
//==============================================================================
cv::Mat sharpenLaplacian(const cv::Mat &img)
{
qDebug() << "sharpenLaplacian";
cv::Mat result;
if (img.empty()) return result;
try {
cv::Mat sharpened;
cv::Laplacian(img, sharpened, CV_8U, 3);
result = img - 0.5 * sharpened; // Можно регулировать силу эффекта
}
catch (...) {
result = cv::Mat();
}
return result;
}
//==============================================================================
cv::Mat unsharpMask(const cv::Mat &img, double sigma, double amount)
{
qDebug() << "unsharpMask";
cv::Mat result;
if (img.empty()) return result;
try {
cv::Mat blurred;
cv::GaussianBlur(img, blurred, cv::Size(0, 0), sigma);
result = img * (1 + amount) - blurred * amount;
}
catch (...) {
result = cv::Mat();
}
return result;
}
//==============================================================================
cv::Mat denoiseMedian(const cv::Mat &img, int kernelSize = 3)
{
qDebug() << "denoiseMedian";
cv::Mat denoised;
if (img.empty()) return denoised;
try {
cv::medianBlur(img, denoised, (kernelSize % 2 == 0) ? kernelSize + 1 : kernelSize);
}
catch (...) {
denoised = cv::Mat();
}
return denoised;
}
//==============================================================================
cv::Mat denoiseNlm(const cv::Mat &img, float h = 10)
{
qDebug() << "denoiseNLM";
cv::Mat denoised;
if (img.empty()) return denoised;
try {
cv::fastNlMeansDenoisingColored(img, denoised, h);
}
catch (...) {
denoised = cv::Mat();
}
return denoised;
}
//==============================================================================
cv::Mat autoExposure(const cv::Mat &img)
{
qDebug() << "autoExposure";
cv::Mat result;
if (img.empty()) return result;
try {
cv::Mat lab;
cv::cvtColor(img, lab, cv::COLOR_BGR2Lab);
std::vector<cv::Mat> channels;
cv::split(lab, channels);
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(2.0);
clahe->apply(channels[0], channels[0]); // Применяем CLAHE к каналу L
cv::merge(channels, lab);
cv::cvtColor(lab, result, cv::COLOR_Lab2BGR);
}
catch (...) {
result = cv::Mat();
}
return result;
}
//==============================================================================
cv::Mat grayWorldWb(const cv::Mat &img)
{
qDebug() << "grayWorldWB";
cv::Mat balanced;
if (img.empty()) return balanced;
try {
cv::Scalar avg = cv::mean(img);
double avgGray = (avg[0] + avg[1] + avg[2]) / 3;
balanced = img.clone();
balanced.forEach<cv::Vec3b>([&](cv::Vec3b &pixel, const int*) {
pixel[0] = cv::saturate_cast<uchar>(pixel[0] * avgGray / avg[0]);
pixel[1] = cv::saturate_cast<uchar>(pixel[1] * avgGray / avg[1]);
pixel[2] = cv::saturate_cast<uchar>(pixel[2] * avgGray / avg[2]);
});
}
catch (...) {
balanced = cv::Mat();
}
return balanced;
}
//==============================================================================
cv::Mat enhanceColors(const cv::Mat &img, float saturationFactor = 1.5)
{
qDebug() << "enhanceColors";
cv::Mat result;
if (img.empty()) return result;
try {
cv::Mat hsv;
cv::cvtColor(img, hsv, cv::COLOR_BGR2HSV);
hsv.forEach<cv::Vec3b>([&](cv::Vec3b &pixel, const int*) {
pixel[1] = cv::saturate_cast<uchar>(pixel[1] * saturationFactor); // Усиливаем насыщенность
});
cv::cvtColor(hsv, result, cv::COLOR_HSV2BGR);
}
catch (...) {
result = cv::Mat();
}
return result;
}
//==============================================================================
cv::Mat removeVignette(const cv::Mat &img, double strength = 0.5)
{
qDebug() << "removeVignette";
cv::Mat result;
if (img.empty()) return result;
try {
cv::Mat mask(img.size(), CV_32F);
int centerX = img.cols / 2;
int centerY = img.rows / 2;
double radius = std::sqrt(centerX * centerX + centerY * centerY);
for (int y = 0; y < img.rows; ++y) {
for (int x = 0; x < img.cols; ++x) {
double dist = std::sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY));
double factor = 1.0 - strength * (dist / radius);
mask.at<float>(y, x) = static_cast<float>( std::max(factor, 0.1) ); // Чтобы не было деления на 0
}
}
cv::Mat imgFloat;
img.convertTo(imgFloat, CV_32F);
std::vector<cv::Mat> channels;
cv::split(imgFloat, channels);
for (auto &channel : channels) {
channel = channel / mask;
}
cv::merge(channels, imgFloat);
imgFloat.convertTo(result, CV_8U);
}
catch (...) {
result = cv::Mat();
}
return result;
}
//==============================================================================
cv::Mat applyClache(const cv::Mat &img)
{
qDebug() << "applyCLAHE";
cv::Mat result;
if (img.empty()) return result;
try {
cv::Mat lab;
cv::cvtColor(img, lab, cv::COLOR_BGR2Lab);
std::vector<cv::Mat> channels;
cv::split(lab, channels);
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
clahe->setClipLimit(2.0);
clahe->apply(channels[0], channels[0]);
cv::merge(channels, lab);
cv::cvtColor(lab, result, cv::COLOR_Lab2BGR);
}
catch (...) {
result = cv::Mat();
}
return result;
}
//==============================================================================
// Форма просмотра изображения
//==============================================================================
ImageViewer::ImageViewer(const QImage &image, QWidget *parent) : QWidget(parent)
{
setWindowFlags( windowFlags() | Qt::Dialog);
setAttribute(Qt::WA_DeleteOnClose, true);
setMinimumSize(300, 200);
labelImage = image;
label = new QLabel(this);
label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
label->setAlignment(Qt::AlignCenter);
label->setMinimumSize(1, 1); // Позволяет уменьшать окно
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(label);
QHBoxLayout *hLayout = new QHBoxLayout();
hLayout->addStretch();
QPushButton *btn = new QPushButton(this);
btn->setText(tr("Сохранить как..."));
connect(btn, &QPushButton::clicked, this, &ImageViewer::save);
hLayout->addWidget(btn);
layout->addLayout(hLayout);
setLayout(layout);
updateImage();
}
//==============================================================================
void ImageViewer::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
updateImage();
}
//==============================================================================
void ImageViewer::updateImage()
{
if (!label) return;
label->clear();
if (!labelImage.isNull()) {
QPixmap pixmap = QPixmap::fromImage(labelImage);
label->setPixmap(pixmap.scaled(label->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
}
//==============================================================================
void ImageViewer::save() {
QString fileName = QFileDialog::getSaveFileName(
this, tr("Сохранить изображение"),
QString(),
QString("Изображения png (*.png)")
);
if (fileName.isEmpty()) return;
if (labelImage.save(fileName)) {
QMessageBox::information(this, tr("Игформация"), tr("Файл сохранён"));
}
else {
QMessageBox::critical(this, tr("Ошибка"), tr("Ошибка при сохранении файла"));
}
}
//==============================================================================
//==============================================================================
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
initialize();
updateControls();
actionsEnabled = true;
}
//==============================================================================
MainWindow::~MainWindow()
{
delete ui;
}
//==============================================================================
void MainWindow::resizeEvent(QResizeEvent *event)
{
QMainWindow::resizeEvent(event);
updateLabelImage();
}
//==============================================================================
void MainWindow::initialize()
{
QString title = qGuiApp->applicationDisplayName().isEmpty()
? qGuiApp->applicationName()
: qGuiApp->applicationDisplayName();
if (!title.isEmpty()) this->setWindowTitle( title );
connect(ui->actionExit, &QAction::triggered, this, &MainWindow::close);
connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::onActionOpen);
connect(ui->actionSave, &QAction::triggered, this, &MainWindow::onActionSaveAs);
connect(ui->actionShowImage, &QAction::triggered, this, &MainWindow::onActionShowImage);
connect(ui->actionUndo, &QAction::triggered, this, &MainWindow::onActionUndo);
connect(ui->actionReset, &QAction::triggered, this, &MainWindow::onActionReset);
connect(ui->pushButtonGain, &QPushButton::clicked, this, &MainWindow::btnGain);
connect(ui->pushButtonBrightnessAndContrast, &QPushButton::clicked, this, &MainWindow::btnBrightnessAndContrast);
connect(ui->pushButtonHistogramEqual, &QPushButton::clicked, this, &MainWindow::btnHistogramEqual);
connect(ui->pushButtonLaplacian, &QPushButton::clicked, this, &MainWindow::btnLaplacian);
connect(ui->pushButtonUnsharpMask, &QPushButton::clicked, this, &MainWindow::btnUnsharpMask);
connect(ui->pushButtonMedian, &QPushButton::clicked, this, &MainWindow::btnMedian);
connect(ui->pushButtonFastNlMeans, &QPushButton::clicked, this, &MainWindow::btnFastNlMeans);
connect(ui->pushButtonAutoExposure, &QPushButton::clicked, this, &MainWindow::btnAutoExposure);
connect(ui->pushButtonWhiteBalance, &QPushButton::clicked, this, &MainWindow::btnWhiteBalance);
connect(ui->pushButtonSaturation, &QPushButton::clicked, this, &MainWindow::btnSaturation);
connect(ui->pushButtonVinete, &QPushButton::clicked, this, &MainWindow::btnVinete);
connect(ui->pushButtonClahe, &QPushButton::clicked, this, &MainWindow::btnClahe);
ui->labelImage->setMinimumSize(1, 1);
ui->scrollArea->verticalScrollBar()->setValue(0);
imgList.reserve(maximumImagesCount);
}
//==============================================================================
cv::Mat MainWindow::prepareImage(const QString &operationName)
{
if (imgList.isEmpty()) return cv::Mat();
qDebug() << operationName;
return qImageToMat(imgList.last());
}
//==============================================================================
void MainWindow::finallyImage(const cv::Mat &output)
{
if (output.empty()) {
qCritical() << "Ошибка при выполнении";
}
else {
imgList.append( matToQImage(output) );
while (imgList.size() > maximumImagesCount) imgList.removeAt(1);
}
updateLabelImage();
updateControls();
if (output.empty()) {
QMessageBox::critical(this, tr("Ошибка"), tr("Ошибка при выполнении"));
}
}
//==============================================================================
void MainWindow::updateLabelImage()
{
ui->labelImage->clear();
QImage currentImage;
if (!imgList.isEmpty()) currentImage = imgList.last();
if (currentImage.isNull()) {
ui->labelImage->setText(tr("Изображение не загружено"));
return;
}
QPixmap pixmap = QPixmap::fromImage(currentImage);
ui->labelImage->setPixmap(
pixmap.scaled(ui->labelImage->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)
);
}
//==============================================================================
void MainWindow::updateControls()
{
bool imageExist = !imgList.isEmpty() && !imgList.last().isNull();
ui->widget->setEnabled( imageExist );
ui->actionShowImage->setEnabled( imageExist );
ui->actionSave->setEnabled( imageExist );
ui->actionUndo->setEnabled( imgList.size() > 1 );
ui->actionReset->setEnabled( imgList.size() > 1 );
}
//==============================================================================
void MainWindow::onActionOpen()
{
QString fileName = QFileDialog::getOpenFileName(
this, tr("Открыть изображение"),
QString(),
QString("Файлы изображений (*.png *.jpg *.jpeg)")
);
if (fileName.isEmpty()) return;
qDebug() << "Загрузка из файла: " << fileName;
ui->labelImage->clear();
imgList.clear();
QImage tmpImage( fileName );
if (tmpImage.isNull()) {
qCritical() << "После загрузки изображение isNull";
}
if (!tmpImage.isNull()) {
imgList.append(tmpImage);
}
updateLabelImage();
updateControls();
if (tmpImage.isNull()) {
qCritical() << "Ошибка при загрузке";
QMessageBox::critical(this, tr("Ошибка"), tr("Ошибка при загрузке файла"));
}
}
//==============================================================================
void MainWindow::onActionSaveAs()
{
if (imgList.isEmpty()) return;
QString fileName = QFileDialog::getSaveFileName(
this, tr("Сохранить изображение"),
QString(),
QString("Изображения png (*.png)")
);
if (fileName.isEmpty()) return;
qDebug() << "Сохранение в файл: " << fileName;
if (imgList.last().save(fileName)) {
QMessageBox::information(this, tr("Информация"), tr("Файл сохранён"));
}
else {
qCritical() << "Ошибка при сохранении";
QMessageBox::critical(this, tr("Ошибка"), tr("Ошибка при сохранении файла"));
}
}
//==============================================================================
void MainWindow::onActionShowImage()
{
if (imgList.isEmpty()) return;
ImageViewer *w = new ImageViewer(imgList.last(), this);
w->show();
}
//==============================================================================
void MainWindow::onActionUndo()
{
if (imgList.size() > 1) imgList.removeLast();
updateLabelImage();
updateControls();
}
//==============================================================================
void MainWindow::onActionReset()
{
while (imgList.size() > 1) imgList.removeLast();
updateLabelImage();
updateControls();
}
//==============================================================================
void MainWindow::btnGain()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Простое усиление яркости");
cv::Mat output = applyGain(input, ui->doubleSpinBoxGain->value());
finallyImage(output);
}
//==============================================================================
void MainWindow::btnBrightnessAndContrast()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Яркость и контраст");
cv::Mat output = adjustBrightnessContrast(input,
ui->doubleSpinBoxBrightness->value(),
ui->spinBoxContrast->value());
finallyImage(output);
}
//==============================================================================
void MainWindow::btnHistogramEqual()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Гистограммная эквализация");
cv::Mat output = equalizeColorHist(input);
finallyImage(output);
}
//==============================================================================
void MainWindow::btnLaplacian()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Фильтр Лапласа");
cv::Mat output = sharpenLaplacian(input);
finallyImage(output);
}
//==============================================================================
void MainWindow::btnUnsharpMask()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Размытие + вычитание");
cv::Mat output = unsharpMask(input, ui->doubleSpinBoxSigma->value(),
ui->doubleSpinBoxAmount->value() );
finallyImage(output);
}
//==============================================================================
void MainWindow::btnMedian()
{
if (imgList.isEmpty()) return;
if (ui->spinBoxMedian->value() % 2 == 0)
ui->spinBoxMedian->setValue( ui->spinBoxMedian->value() + 1 );
cv::Mat input = prepareImage("Медианный фильтр");
cv::Mat output = denoiseMedian(input, ui->spinBoxMedian->value() );
finallyImage(output);
}
//==============================================================================
void MainWindow::btnFastNlMeans()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("OpenCV FastNlMeans");
cv::Mat output = denoiseNlm(input, static_cast<float>(ui->doubleSpinBoxFastNlMeans->value()) );
finallyImage(output);
}
//==============================================================================
void MainWindow::btnAutoExposure()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Автокорректировка экспозиции");
cv::Mat output = autoExposure(input);
finallyImage(output);
}
//==============================================================================
void MainWindow::btnWhiteBalance()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Баланс белого");
cv::Mat output = grayWorldWb(input);
finallyImage(output);
}
//==============================================================================
void MainWindow::btnSaturation()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Цветовая насыщенность");
cv::Mat output = denoiseNlm(input, static_cast<float>(ui->doubleSpinBoxSaturation->value()) );
finallyImage(output);
}
//==============================================================================
void MainWindow::btnVinete()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Коррекция виньетирования");
cv::Mat output = removeVignette(input, ui->doubleSpinBoxVinete->value() );
finallyImage(output);
}
//==============================================================================
void MainWindow::btnClahe()
{
if (imgList.isEmpty()) return;
cv::Mat input = prepareImage("Адаптивное выравнивание гистограммы");
cv::Mat output = applyClache(input);
finallyImage(output);
}
//==============================================================================