#include "MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(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(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> 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(cv::countNonZero(edges)) / static_cast(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(viewportSize.width()) / imageSize.width(); const double yRatio = static_cast(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(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(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(std::round(image.width() * zoomFactor_))), std::max(1, static_cast(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 &processor) { if (!ensureImageLoaded(actionName)) { return; } const QImage input = currentImage_; auto result = std::make_shared(); auto error = std::make_shared(); 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; }