/**************************************************************************** ** Copyright (c) 2025 Evgeny Teterin (nayk) ** 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include //============================================================================== // Функции обработки изображений 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(img.bits()), static_cast(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(temp.data), temp.cols, temp.rows, static_cast(temp.step), QImage::Format_RGB888); #else QImage dest( const_cast(temp.data), temp.cols, temp.rows, static_cast(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 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 channels; cv::split(lab, channels); cv::Ptr 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 &pixel, const int*) { pixel[0] = cv::saturate_cast(pixel[0] * avgGray / avg[0]); pixel[1] = cv::saturate_cast(pixel[1] * avgGray / avg[1]); pixel[2] = cv::saturate_cast(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 &pixel, const int*) { pixel[1] = cv::saturate_cast(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(y, x) = static_cast( std::max(factor, 0.1) ); // Чтобы не было деления на 0 } } cv::Mat imgFloat; img.convertTo(imgFloat, CV_32F); std::vector 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 channels; cv::split(lab, channels); cv::Ptr 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(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(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); } //==============================================================================