Files
chengnan/MainWindow.cpp
luochen570 573c9d746c
Some checks failed
Build Windows Release / Build Windows x64 Release on Linux (MinGW + Qt + OpenCV) (push) Failing after 2m15s
Implement Qt OpenCV image processing app
2026-05-30 23:09:27 +08:00

609 lines
19 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "MainWindow.h"
#include <QAction>
#include <QApplication>
#include <QFileDialog>
#include <QFileInfo>
#include <QImageReader>
#include <QImageWriter>
#include <QKeySequence>
#include <QMenuBar>
#include <QMessageBox>
#include <QMouseEvent>
#include <QScrollBar>
#include <QSlider>
#include <QSplitter>
#include <QStatusBar>
#include <QThread>
#include <QToolBar>
#include <QTransform>
#include <opencv2/imgproc.hpp>
#include <algorithm>
#include <cmath>
#include <exception>
#include <memory>
namespace {
cv::Mat qImageToMat(const QImage &image)
{
const QImage rgba = image.convertToFormat(QImage::Format_RGBA8888);
cv::Mat mat(rgba.height(), rgba.width(), CV_8UC4, const_cast<uchar *>(rgba.constBits()), rgba.bytesPerLine());
return mat.clone();
}
QImage matToQImage(const cv::Mat &mat)
{
if (mat.empty()) {
return {};
}
cv::Mat rgba;
if (mat.channels() == 1) {
cv::cvtColor(mat, rgba, cv::COLOR_GRAY2RGBA);
} else if (mat.channels() == 3) {
cv::cvtColor(mat, rgba, cv::COLOR_BGR2RGBA);
} else {
cv::cvtColor(mat, rgba, cv::COLOR_BGRA2RGBA);
}
return QImage(rgba.data, rgba.cols, rgba.rows, static_cast<int>(rgba.step), QImage::Format_RGBA8888).copy();
}
QImage extractObjectImage(const QImage &image)
{
const cv::Mat rgba = qImageToMat(image);
cv::Mat gray;
cv::cvtColor(rgba, gray, cv::COLOR_RGBA2GRAY);
cv::GaussianBlur(gray, gray, cv::Size(5, 5), 0.0);
cv::Mat edges;
cv::Canny(gray, edges, 60, 160);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(edges, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
cv::Mat output = rgba.clone();
cv::cvtColor(output, output, cv::COLOR_RGBA2BGRA);
int count = 0;
for (const auto &contour : contours) {
const double area = cv::contourArea(contour);
if (area < 120.0) {
continue;
}
const cv::Rect rect = cv::boundingRect(contour);
cv::rectangle(output, rect, cv::Scalar(0, 0, 255, 255), 2);
++count;
}
cv::putText(output,
"objects: " + std::to_string(count),
cv::Point(16, 32),
cv::FONT_HERSHEY_SIMPLEX,
0.9,
cv::Scalar(0, 0, 255, 255),
2);
return matToQImage(output);
}
QImage classifyImageByOpenCv(const QImage &image)
{
const cv::Mat rgba = qImageToMat(image);
cv::Mat gray;
cv::cvtColor(rgba, gray, cv::COLOR_RGBA2GRAY);
cv::Mat edges;
cv::Canny(gray, edges, 80, 180);
const double brightness = cv::mean(gray)[0];
const double edgeRatio = static_cast<double>(cv::countNonZero(edges)) / static_cast<double>(gray.total());
std::string label;
if (brightness < 80.0) {
label = "class: dark image";
} else if (edgeRatio > 0.12) {
label = "class: rich edges";
} else {
label = "class: bright smooth image";
}
cv::Mat output = rgba.clone();
cv::cvtColor(output, output, cv::COLOR_RGBA2BGRA);
const int boxWidth = std::max(1, std::min(520, output.cols - 16));
cv::rectangle(output, cv::Rect(8, 8, boxWidth, 52), cv::Scalar(0, 0, 0, 180), cv::FILLED);
cv::putText(output,
label,
cv::Point(18, 42),
cv::FONT_HERSHEY_SIMPLEX,
0.8,
cv::Scalar(0, 255, 255, 255),
2);
return matToQImage(output);
}
} // namespace
ImageLabel::ImageLabel(QWidget *parent)
: QLabel(parent)
{
setAlignment(Qt::AlignCenter);
setBackgroundRole(QPalette::Base);
setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
setScaledContents(false);
setMinimumSize(320, 240);
setText(tr("打开一张图片开始查看"));
}
void ImageLabel::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
Q_EMIT doubleClicked();
}
QLabel::mouseDoubleClickEvent(event);
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
createActions();
createMenus();
createToolbar();
createCentralWidget();
updateActions();
setWindowTitle(tr("chengnan 图片处理工具"));
resize(1200, 720);
statusBar()->showMessage(tr("就绪"));
}
void MainWindow::createActions()
{
openAction_ = new QAction(tr("打开(&O)"), this);
openAction_->setShortcut(QKeySequence::Open);
connect(openAction_, &QAction::triggered, this, &MainWindow::openImage);
saveAction_ = new QAction(tr("保存(&S)"), this);
saveAction_->setShortcut(QKeySequence::Save);
connect(saveAction_, &QAction::triggered, this, &MainWindow::saveImage);
saveAsAction_ = new QAction(tr("另存为(&A)"), this);
saveAsAction_->setShortcut(QKeySequence::SaveAs);
connect(saveAsAction_, &QAction::triggered, this, &MainWindow::saveImageAs);
exitAction_ = new QAction(tr("退出(&X)"), this);
exitAction_->setShortcut(QKeySequence::Quit);
connect(exitAction_, &QAction::triggered, this, &QWidget::close);
zoomInAction_ = new QAction(tr("放大"), this);
zoomInAction_->setShortcut(QKeySequence::ZoomIn);
connect(zoomInAction_, &QAction::triggered, this, &MainWindow::zoomIn);
zoomOutAction_ = new QAction(tr("缩小"), this);
zoomOutAction_->setShortcut(QKeySequence::ZoomOut);
connect(zoomOutAction_, &QAction::triggered, this, &MainWindow::zoomOut);
fitAction_ = new QAction(tr("适配窗口"), this);
fitAction_->setShortcut(Qt::CTRL | Qt::Key_F);
connect(fitAction_, &QAction::triggered, this, &MainWindow::fitToWindow);
resetAction_ = new QAction(tr("原始大小"), this);
resetAction_->setShortcut(Qt::CTRL | Qt::Key_0);
connect(resetAction_, &QAction::triggered, this, &MainWindow::resetView);
rotateLeftAction_ = new QAction(tr("左旋 90°"), this);
connect(rotateLeftAction_, &QAction::triggered, this, &MainWindow::rotateLeft);
rotateRightAction_ = new QAction(tr("右旋 90°"), this);
connect(rotateRightAction_, &QAction::triggered, this, &MainWindow::rotateRight);
flipHorizontalAction_ = new QAction(tr("水平翻转"), this);
connect(flipHorizontalAction_, &QAction::triggered, this, &MainWindow::flipHorizontal);
flipVerticalAction_ = new QAction(tr("垂直翻转"), this);
connect(flipVerticalAction_, &QAction::triggered, this, &MainWindow::flipVertical);
grayscaleAction_ = new QAction(tr("灰度化"), this);
connect(grayscaleAction_, &QAction::triggered, this, &MainWindow::grayscale);
invertAction_ = new QAction(tr("反色"), this);
connect(invertAction_, &QAction::triggered, this, &MainWindow::invertColors);
extractAction_ = new QAction(tr("目标提取"), this);
connect(extractAction_, &QAction::triggered, this, &MainWindow::extractObjects);
classifyAction_ = new QAction(tr("简单分类"), this);
connect(classifyAction_, &QAction::triggered, this, &MainWindow::classifyImage);
}
void MainWindow::createMenus()
{
QMenu *fileMenu = menuBar()->addMenu(tr("文件(&F)"));
fileMenu->addAction(openAction_);
fileMenu->addAction(saveAction_);
fileMenu->addAction(saveAsAction_);
fileMenu->addSeparator();
fileMenu->addAction(exitAction_);
QMenu *viewMenu = menuBar()->addMenu(tr("查看(&V)"));
viewMenu->addAction(zoomInAction_);
viewMenu->addAction(zoomOutAction_);
viewMenu->addAction(fitAction_);
viewMenu->addAction(resetAction_);
QMenu *processMenu = menuBar()->addMenu(tr("处理(&P)"));
processMenu->addAction(rotateLeftAction_);
processMenu->addAction(rotateRightAction_);
processMenu->addAction(flipHorizontalAction_);
processMenu->addAction(flipVerticalAction_);
processMenu->addSeparator();
processMenu->addAction(grayscaleAction_);
processMenu->addAction(invertAction_);
processMenu->addSeparator();
processMenu->addAction(extractAction_);
processMenu->addAction(classifyAction_);
}
void MainWindow::createToolbar()
{
QToolBar *toolbar = addToolBar(tr("主工具栏"));
toolbar->setMovable(false);
toolbar->addAction(openAction_);
toolbar->addAction(saveAction_);
toolbar->addSeparator();
toolbar->addAction(zoomOutAction_);
zoomSlider_ = new QSlider(Qt::Horizontal, this);
zoomSlider_->setRange(10, 400);
zoomSlider_->setValue(100);
zoomSlider_->setFixedWidth(180);
zoomSlider_->setToolTip(tr("缩放比例"));
connect(zoomSlider_, &QSlider::valueChanged, this, &MainWindow::updateZoomFromSlider);
toolbar->addWidget(zoomSlider_);
toolbar->addAction(zoomInAction_);
toolbar->addAction(fitAction_);
toolbar->addSeparator();
toolbar->addAction(rotateLeftAction_);
toolbar->addAction(rotateRightAction_);
toolbar->addAction(grayscaleAction_);
toolbar->addAction(invertAction_);
toolbar->addAction(extractAction_);
toolbar->addAction(classifyAction_);
}
void MainWindow::createCentralWidget()
{
originalLabel_ = new ImageLabel(this);
processedLabel_ = new ImageLabel(this);
originalLabel_->setText(tr("处理前图像"));
processedLabel_->setText(tr("处理后图像"));
connect(originalLabel_, &ImageLabel::doubleClicked, this, &MainWindow::fitToWindow);
connect(processedLabel_, &ImageLabel::doubleClicked, this, &MainWindow::fitToWindow);
originalScrollArea_ = new QScrollArea(this);
originalScrollArea_->setBackgroundRole(QPalette::Dark);
originalScrollArea_->setWidget(originalLabel_);
originalScrollArea_->setWidgetResizable(true);
processedScrollArea_ = new QScrollArea(this);
processedScrollArea_->setBackgroundRole(QPalette::Dark);
processedScrollArea_->setWidget(processedLabel_);
processedScrollArea_->setWidgetResizable(true);
auto *splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(originalScrollArea_);
splitter->addWidget(processedScrollArea_);
splitter->setStretchFactor(0, 1);
splitter->setStretchFactor(1, 1);
setCentralWidget(splitter);
}
void MainWindow::openImage()
{
const QString fileName = QFileDialog::getOpenFileName(
this,
tr("打开图片"),
QString(),
tr("图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.webp);;所有文件 (*.*)"));
if (fileName.isEmpty()) {
return;
}
QImageReader reader(fileName);
reader.setAutoTransform(true);
const QImage image = reader.read();
if (image.isNull()) {
QMessageBox::warning(this, tr("打开失败"), tr("无法读取图片:%1").arg(reader.errorString()));
return;
}
setImage(image, fileName);
}
void MainWindow::saveImage()
{
if (!ensureImageLoaded(tr("保存"))) {
return;
}
if (currentPath_.isEmpty()) {
saveImageAs();
return;
}
writeImageToPath(currentPath_);
}
void MainWindow::saveImageAs()
{
if (!ensureImageLoaded(tr("另存为"))) {
return;
}
const QString fileName = QFileDialog::getSaveFileName(
this,
tr("另存为"),
currentPath_.isEmpty() ? QStringLiteral("chengnan.png") : currentPath_,
tr("PNG 图片 (*.png);;JPEG 图片 (*.jpg *.jpeg);;BMP 图片 (*.bmp);;所有文件 (*.*)"));
if (!fileName.isEmpty()) {
writeImageToPath(fileName);
}
}
void MainWindow::zoomIn()
{
setZoomFactor(zoomFactor_ * 1.25);
}
void MainWindow::zoomOut()
{
setZoomFactor(zoomFactor_ / 1.25);
}
void MainWindow::fitToWindow()
{
if (!ensureImageLoaded(tr("适配窗口"))) {
return;
}
const QSize viewportSize = processedScrollArea_->viewport()->size();
const QSize imageSize = currentImage_.size();
if (imageSize.isEmpty()) {
return;
}
const double xRatio = static_cast<double>(viewportSize.width()) / imageSize.width();
const double yRatio = static_cast<double>(viewportSize.height()) / imageSize.height();
setZoomFactor(std::min(xRatio, yRatio));
}
void MainWindow::resetView()
{
setZoomFactor(1.0);
}
void MainWindow::rotateLeft()
{
if (!ensureImageLoaded(tr("旋转"))) {
return;
}
QTransform transform;
transform.rotate(-90);
applyTransformedImage(currentImage_.transformed(transform, Qt::SmoothTransformation));
}
void MainWindow::rotateRight()
{
if (!ensureImageLoaded(tr("旋转"))) {
return;
}
QTransform transform;
transform.rotate(90);
applyTransformedImage(currentImage_.transformed(transform, Qt::SmoothTransformation));
}
void MainWindow::flipHorizontal()
{
if (!ensureImageLoaded(tr("翻转"))) {
return;
}
applyTransformedImage(currentImage_.mirrored(true, false));
}
void MainWindow::flipVertical()
{
if (!ensureImageLoaded(tr("翻转"))) {
return;
}
applyTransformedImage(currentImage_.mirrored(false, true));
}
void MainWindow::grayscale()
{
processImageInThread(tr("灰度化"), tr("灰度化完成,可保存结果"), [](QImage image) {
cv::Mat rgba = qImageToMat(image);
cv::Mat gray;
cv::cvtColor(rgba, gray, cv::COLOR_RGBA2GRAY);
return matToQImage(gray);
});
}
void MainWindow::invertColors()
{
processImageInThread(tr("反色"), tr("反色完成,可保存结果"), [](QImage image) {
QImage result = image.convertToFormat(QImage::Format_ARGB32);
result.invertPixels(QImage::InvertRgb);
return result;
});
}
void MainWindow::extractObjects()
{
processImageInThread(tr("目标提取"), tr("目标提取完成,红框为提取到的目标"), [](QImage image) {
return extractObjectImage(image);
});
}
void MainWindow::classifyImage()
{
processImageInThread(tr("简单分类"), tr("简单分类完成,结果已标注在图像上"), [](QImage image) {
return classifyImageByOpenCv(image);
});
}
void MainWindow::updateZoomFromSlider(int value)
{
const double factor = static_cast<double>(value) / 100.0;
if (std::abs(factor - zoomFactor_) > 0.001) {
zoomFactor_ = factor;
updateImageView();
}
}
void MainWindow::setImage(const QImage &image, const QString &path)
{
originalImage_ = image.convertToFormat(QImage::Format_ARGB32);
currentImage_ = originalImage_;
currentPath_ = path;
setZoomFactor(1.0);
updateActions();
const QFileInfo info(path);
setWindowTitle(path.isEmpty()
? tr("chengnan 图片处理工具")
: tr("%1 - chengnan 图片处理工具").arg(info.fileName()));
statusBar()->showMessage(tr("已加载:%1 × %2").arg(currentImage_.width()).arg(currentImage_.height()), 5000);
}
void MainWindow::updateImageView()
{
if (currentImage_.isNull()) {
originalLabel_->setPixmap(QPixmap());
processedLabel_->setPixmap(QPixmap());
originalLabel_->setText(tr("处理前图像"));
processedLabel_->setText(tr("处理后图像"));
return;
}
updateSingleView(originalLabel_, originalImage_);
updateSingleView(processedLabel_, currentImage_);
const int sliderValue = static_cast<int>(zoomFactor_ * 100.0);
if (zoomSlider_->value() != sliderValue) {
zoomSlider_->blockSignals(true);
zoomSlider_->setValue(sliderValue);
zoomSlider_->blockSignals(false);
}
statusBar()->showMessage(tr("缩放:%1%").arg(sliderValue));
}
void MainWindow::updateSingleView(ImageLabel *label, const QImage &image)
{
const QSize scaledSize(
std::max(1, static_cast<int>(std::round(image.width() * zoomFactor_))),
std::max(1, static_cast<int>(std::round(image.height() * zoomFactor_))));
const QPixmap pixmap = QPixmap::fromImage(image).scaled(
scaledSize,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
label->setText(QString());
label->setPixmap(pixmap);
label->resize(pixmap.size());
}
void MainWindow::setZoomFactor(double factor)
{
zoomFactor_ = std::clamp(factor, 0.10, 4.00);
updateImageView();
}
void MainWindow::applyTransformedImage(const QImage &image, const QString &message)
{
currentImage_ = image.convertToFormat(QImage::Format_ARGB32);
updateImageView();
updateActions();
statusBar()->showMessage(message.isEmpty() ? tr("处理完成,可保存结果") : message, 3000);
}
void MainWindow::processImageInThread(const QString &actionName,
const QString &doneMessage,
const std::function<QImage(QImage)> &processor)
{
if (!ensureImageLoaded(actionName)) {
return;
}
const QImage input = currentImage_;
auto result = std::make_shared<QImage>();
auto error = std::make_shared<QString>();
QThread *thread = QThread::create([input, result, error, processor]() {
try {
*result = processor(input);
} catch (const cv::Exception &ex) {
*error = QString::fromLocal8Bit(ex.what());
} catch (const std::exception &ex) {
*error = QString::fromLocal8Bit(ex.what());
}
});
statusBar()->showMessage(tr("正在处理:%1 ...").arg(actionName));
connect(thread, &QThread::finished, this, [this, thread, result, error, doneMessage]() {
thread->deleteLater();
if (!error->isEmpty() || result->isNull()) {
QMessageBox::warning(this, tr("处理失败"), error->isEmpty() ? tr("处理结果为空。") : *error);
return;
}
applyTransformedImage(*result, doneMessage);
});
thread->start();
}
void MainWindow::updateActions()
{
const bool hasImage = !currentImage_.isNull();
saveAction_->setEnabled(hasImage);
saveAsAction_->setEnabled(hasImage);
zoomInAction_->setEnabled(hasImage);
zoomOutAction_->setEnabled(hasImage);
fitAction_->setEnabled(hasImage);
resetAction_->setEnabled(hasImage);
rotateLeftAction_->setEnabled(hasImage);
rotateRightAction_->setEnabled(hasImage);
flipHorizontalAction_->setEnabled(hasImage);
flipVerticalAction_->setEnabled(hasImage);
grayscaleAction_->setEnabled(hasImage);
invertAction_->setEnabled(hasImage);
extractAction_->setEnabled(hasImage);
classifyAction_->setEnabled(hasImage);
zoomSlider_->setEnabled(hasImage);
}
bool MainWindow::ensureImageLoaded(const QString &actionName) const
{
if (!currentImage_.isNull()) {
return true;
}
QMessageBox::information(nullptr, actionName, tr("请先打开一张图片。"));
return false;
}
bool MainWindow::writeImageToPath(const QString &path)
{
QImageWriter writer(path);
if (!writer.write(currentImage_)) {
QMessageBox::warning(this, tr("保存失败"), tr("无法保存图片:%1").arg(writer.errorString()));
return false;
}
currentPath_ = path;
setWindowTitle(tr("%1 - chengnan 图片处理工具").arg(QFileInfo(path).fileName()));
statusBar()->showMessage(tr("已保存:%1").arg(path), 5000);
return true;
}