From c658fa4265490c9c702788cd5a5ed86e34e8619a Mon Sep 17 00:00:00 2001 From: luochen570 <1160510664@qq.com> Date: Sun, 31 May 2026 12:10:10 +0800 Subject: [PATCH] Initial Qt OpenCV image tool --- CMakeLists.txt | 26 +++ MainWindow.cpp | 433 ++++++++++++++++++++++++++++++++++++++++++ MainWindow.h | 76 ++++++++ QtOpenCVImageTool.zip | Bin 0 -> 6624 bytes main.cpp | 14 ++ readme.md | 39 ++++ 6 files changed, 588 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 MainWindow.cpp create mode 100644 MainWindow.h create mode 100644 QtOpenCVImageTool.zip create mode 100644 main.cpp create mode 100644 readme.md diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3d1d11f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.16) + +project(QtOpenCVImageTool LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) +find_package(OpenCV REQUIRED) + +add_executable(QtOpenCVImageTool + main.cpp + MainWindow.h + MainWindow.cpp +) + +target_link_libraries(QtOpenCVImageTool PRIVATE + Qt${QT_VERSION_MAJOR}::Widgets + ${OpenCV_LIBS} +) + +target_include_directories(QtOpenCVImageTool PRIVATE ${OpenCV_INCLUDE_DIRS}) diff --git a/MainWindow.cpp b/MainWindow.cpp new file mode 100644 index 0000000..c82e610 --- /dev/null +++ b/MainWindow.cpp @@ -0,0 +1,433 @@ +#include "MainWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ +QImage matToQImage(const cv::Mat &mat) +{ + if (mat.empty()) { + return {}; + } + + cv::Mat rgb; + if (mat.channels() == 1) { + cv::cvtColor(mat, rgb, cv::COLOR_GRAY2RGB); + } else if (mat.channels() == 3) { + cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); + } else if (mat.channels() == 4) { + cv::cvtColor(mat, rgb, cv::COLOR_BGRA2RGBA); + } else { + return {}; + } + + const QImage::Format format = rgb.channels() == 4 ? QImage::Format_RGBA8888 : QImage::Format_RGB888; + return QImage(rgb.data, rgb.cols, rgb.rows, static_cast(rgb.step), format).copy(); +} + +cv::Mat rotateWithBounds(const cv::Mat &image, double angle) +{ + if (image.empty() || std::abs(angle) < 0.01) { + return image.clone(); + } + + const cv::Point2f center(image.cols / 2.0F, image.rows / 2.0F); + cv::Mat matrix = cv::getRotationMatrix2D(center, angle, 1.0); + const cv::Rect2f bounds = cv::RotatedRect(cv::Point2f(), image.size(), angle).boundingRect2f(); + matrix.at(0, 2) += bounds.width / 2.0 - image.cols / 2.0; + matrix.at(1, 2) += bounds.height / 2.0 - image.rows / 2.0; + + cv::Mat rotated; + const cv::Size outputSize(static_cast(bounds.width), static_cast(bounds.height)); + cv::warpAffine(image, rotated, matrix, outputSize, cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(30, 30, 30)); + return rotated; +} + +cv::Mat translateImage(const cv::Mat &image, int dx, int dy) +{ + if (image.empty() || (dx == 0 && dy == 0)) { + return image.clone(); + } + + cv::Mat translated; + cv::Mat matrix = (cv::Mat_(2, 3) << 1, 0, dx, 0, 1, dy); + cv::warpAffine(image, translated, matrix, image.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(30, 30, 30)); + return translated; +} + +cv::Mat extractTargets(const cv::Mat &image, int &targetCount) +{ + targetCount = 0; + if (image.empty()) { + return {}; + } + + cv::Mat gray; + cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY); + cv::GaussianBlur(gray, gray, cv::Size(5, 5), 0); + + cv::Mat binary; + cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU); + if (cv::countNonZero(binary) > binary.rows * binary.cols / 2) { + cv::bitwise_not(binary, binary); + } + + const cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5)); + cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernel); + cv::morphologyEx(binary, binary, cv::MORPH_CLOSE, kernel); + + std::vector> contours; + cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); + + cv::Mat extracted = cv::Mat::zeros(image.size(), image.type()); + image.copyTo(extracted, binary); + + for (const auto &contour : contours) { + const double area = cv::contourArea(contour); + if (area < 300.0) { + continue; + } + + ++targetCount; + cv::rectangle(extracted, cv::boundingRect(contour), cv::Scalar(0, 255, 0), 2); + } + + return extracted; +} + +QString classifyImage(const cv::Mat &image, int targetCount) +{ + if (image.empty()) { + return QStringLiteral("未载入图片"); + } + + cv::Scalar mean = cv::mean(image); + const double brightness = (mean[0] + mean[1] + mean[2]) / 3.0; + const QString light = brightness >= 170.0 ? QStringLiteral("偏亮") + : brightness <= 85.0 ? QStringLiteral("偏暗") + : QStringLiteral("亮度适中"); + + QString color = QStringLiteral("颜色均衡"); + if (mean[2] > mean[1] + 18.0 && mean[2] > mean[0] + 18.0) { + color = QStringLiteral("红色占优"); + } else if (mean[1] > mean[2] + 18.0 && mean[1] > mean[0] + 18.0) { + color = QStringLiteral("绿色占优"); + } else if (mean[0] > mean[2] + 18.0 && mean[0] > mean[1] + 18.0) { + color = QStringLiteral("蓝色占优"); + } + + return QStringLiteral("分类结果:%1,%2;目标数量:%3").arg(light, color).arg(targetCount); +} + +class ImageProcessorWorker : public QObject +{ + Q_OBJECT + +public slots: + void process(const cv::Mat &source, const ProcessParams ¶ms) + { + if (source.empty()) { + emit processed(cv::Mat(), QStringLiteral("未载入图片")); + return; + } + + cv::Mat output = source.clone(); + + if (std::abs(params.scale - 1.0) > 0.001) { + cv::resize(output, output, cv::Size(), params.scale, params.scale, cv::INTER_LINEAR); + } + + output = translateImage(output, params.translateX, params.translateY); + output = rotateWithBounds(output, params.angle); + + int targetCount = 0; + if (params.extractTarget) { + output = extractTargets(output, targetCount); + } else { + cv::Mat gray; + cv::cvtColor(output, gray, cv::COLOR_BGR2GRAY); + cv::Mat binary; + cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU); + std::vector> contours; + cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); + targetCount = static_cast(std::count_if(contours.begin(), contours.end(), [](const auto &contour) { + return cv::contourArea(contour) >= 300.0; + })); + } + + const QString classText = params.classifyImage ? classifyImage(output, targetCount) : QStringLiteral("处理完成"); + emit processed(output, classText); + } + +signals: + void processed(const cv::Mat &image, const QString &classText); +}; +} + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + qRegisterMetaType("cv::Mat"); + qRegisterMetaType("ProcessParams"); + + buildUi(); + + auto *worker = new ImageProcessorWorker; + workerThread = new QThread(this); + worker->moveToThread(workerThread); + connect(this, &MainWindow::processRequested, worker, &ImageProcessorWorker::process); + connect(worker, &ImageProcessorWorker::processed, this, &MainWindow::onProcessed); + connect(workerThread, &QThread::finished, worker, &QObject::deleteLater); + workerThread->start(); +} + +MainWindow::~MainWindow() +{ + if (workerThread) { + workerThread->quit(); + workerThread->wait(); + } +} + +void MainWindow::buildUi() +{ + setWindowTitle(QStringLiteral("Qt + OpenCV 图像显示与处理")); + + auto *central = new QWidget(this); + auto *rootLayout = new QVBoxLayout(central); + + auto *buttonLayout = new QHBoxLayout; + openButton = new QPushButton(QStringLiteral("打开图片")); + saveButton = new QPushButton(QStringLiteral("保存图片")); + saveAsButton = new QPushButton(QStringLiteral("另存图片")); + resetButton = new QPushButton(QStringLiteral("重置处理")); + buttonLayout->addWidget(openButton); + buttonLayout->addWidget(saveButton); + buttonLayout->addWidget(saveAsButton); + buttonLayout->addStretch(); + buttonLayout->addWidget(resetButton); + + auto *controlBox = new QGroupBox(QStringLiteral("图像处理")); + auto *controlLayout = new QHBoxLayout(controlBox); + + scaleSlider = new QSlider(Qt::Horizontal); + scaleSlider->setRange(25, 200); + scaleSlider->setValue(100); + rotateSlider = new QSlider(Qt::Horizontal); + rotateSlider->setRange(-180, 180); + translateXSlider = new QSlider(Qt::Horizontal); + translateXSlider->setRange(-200, 200); + translateYSlider = new QSlider(Qt::Horizontal); + translateYSlider->setRange(-200, 200); + extractCheckBox = new QCheckBox(QStringLiteral("目标提取")); + classifyCheckBox = new QCheckBox(QStringLiteral("图像分类")); + + auto addSlider = [controlLayout](const QString &name, QSlider *slider) { + auto *box = new QWidget; + auto *layout = new QVBoxLayout(box); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(new QLabel(name)); + layout->addWidget(slider); + controlLayout->addWidget(box, 1); + }; + + addSlider(QStringLiteral("缩放"), scaleSlider); + addSlider(QStringLiteral("旋转"), rotateSlider); + addSlider(QStringLiteral("水平平移"), translateXSlider); + addSlider(QStringLiteral("垂直平移"), translateYSlider); + controlLayout->addWidget(extractCheckBox); + controlLayout->addWidget(classifyCheckBox); + + auto *imageLayout = new QHBoxLayout; + originalLabel = new QLabel(QStringLiteral("原图")); + resultLabel = new QLabel(QStringLiteral("处理后图像")); + originalLabel->setAlignment(Qt::AlignCenter); + resultLabel->setAlignment(Qt::AlignCenter); + originalLabel->setMinimumSize(480, 420); + resultLabel->setMinimumSize(480, 420); + originalLabel->setStyleSheet("QLabel { background:#202124; color:white; border:1px solid #555; }"); + resultLabel->setStyleSheet("QLabel { background:#202124; color:white; border:1px solid #555; }"); + + auto *originalScroll = new QScrollArea; + auto *resultScroll = new QScrollArea; + originalScroll->setWidget(originalLabel); + resultScroll->setWidget(resultLabel); + originalScroll->setWidgetResizable(true); + resultScroll->setWidgetResizable(true); + + imageLayout->addWidget(originalScroll, 1); + imageLayout->addWidget(resultScroll, 1); + + rootLayout->addLayout(buttonLayout); + rootLayout->addWidget(controlBox); + rootLayout->addLayout(imageLayout, 1); + setCentralWidget(central); + + statusLabel = new QLabel(QStringLiteral("请打开一张图片")); + statusBar()->addWidget(statusLabel, 1); + + connect(openButton, &QPushButton::clicked, this, &MainWindow::openImage); + connect(saveButton, &QPushButton::clicked, this, &MainWindow::saveImage); + connect(saveAsButton, &QPushButton::clicked, this, &MainWindow::saveImageAs); + connect(resetButton, &QPushButton::clicked, this, &MainWindow::resetParams); + + connect(scaleSlider, &QSlider::valueChanged, this, &MainWindow::scheduleProcessing); + connect(rotateSlider, &QSlider::valueChanged, this, &MainWindow::scheduleProcessing); + connect(translateXSlider, &QSlider::valueChanged, this, &MainWindow::scheduleProcessing); + connect(translateYSlider, &QSlider::valueChanged, this, &MainWindow::scheduleProcessing); + connect(extractCheckBox, &QCheckBox::toggled, this, &MainWindow::scheduleProcessing); + connect(classifyCheckBox, &QCheckBox::toggled, this, &MainWindow::scheduleProcessing); +} + +void MainWindow::openImage() +{ + const QString fileName = QFileDialog::getOpenFileName( + this, + QStringLiteral("打开图片"), + QString(), + QStringLiteral("Images (*.png *.jpg *.jpeg *.bmp *.tif *.tiff)")); + + if (fileName.isEmpty()) { + return; + } + + originalImage = cv::imread(fileName.toLocal8Bit().constData(), cv::IMREAD_COLOR); + if (originalImage.empty()) { + QMessageBox::warning(this, QStringLiteral("打开失败"), QStringLiteral("无法读取该图片文件。")); + return; + } + + savePath.clear(); + updateOriginalView(); + scheduleProcessing(); + statusLabel->setText(QStringLiteral("已打开:%1").arg(QFileInfo(fileName).fileName())); +} + +void MainWindow::saveImage() +{ + if (savePath.isEmpty()) { + saveImageAs(); + return; + } + + if (writeResultToFile(savePath)) { + statusLabel->setText(QStringLiteral("已保存:%1").arg(QFileInfo(savePath).fileName())); + } +} + +void MainWindow::saveImageAs() +{ + if (resultImage.empty()) { + QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("请先打开并处理图片。")); + return; + } + + const QString fileName = QFileDialog::getSaveFileName( + this, + QStringLiteral("另存图片"), + QStringLiteral("processed.png"), + QStringLiteral("PNG (*.png);;JPEG (*.jpg *.jpeg);;BMP (*.bmp);;TIFF (*.tif *.tiff)")); + + if (fileName.isEmpty()) { + return; + } + + if (writeResultToFile(fileName)) { + savePath = fileName; + statusLabel->setText(QStringLiteral("已另存为:%1").arg(QFileInfo(fileName).fileName())); + } +} + +void MainWindow::resetParams() +{ + scaleSlider->setValue(100); + rotateSlider->setValue(0); + translateXSlider->setValue(0); + translateYSlider->setValue(0); + extractCheckBox->setChecked(false); + classifyCheckBox->setChecked(false); + scheduleProcessing(); +} + +void MainWindow::scheduleProcessing() +{ + if (originalImage.empty()) { + return; + } + + emit processRequested(originalImage.clone(), currentParams()); +} + +void MainWindow::onProcessed(const cv::Mat &image, const QString &classText) +{ + resultImage = image.clone(); + updateResultView(); + statusLabel->setText(classText); +} + +void MainWindow::updateOriginalView() +{ + setImageOnLabel(originalLabel, originalImage); +} + +void MainWindow::updateResultView() +{ + setImageOnLabel(resultLabel, resultImage); +} + +void MainWindow::setImageOnLabel(QLabel *label, const cv::Mat &mat) +{ + const QImage image = matToQImage(mat); + if (image.isNull()) { + label->setText(QStringLiteral("无图像")); + return; + } + + label->setPixmap(QPixmap::fromImage(image).scaled(label->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + +ProcessParams MainWindow::currentParams() const +{ + ProcessParams params; + params.scale = scaleSlider->value() / 100.0; + params.angle = rotateSlider->value(); + params.translateX = translateXSlider->value(); + params.translateY = translateYSlider->value(); + params.extractTarget = extractCheckBox->isChecked(); + params.classifyImage = classifyCheckBox->isChecked(); + return params; +} + +bool MainWindow::writeResultToFile(const QString &fileName) +{ + if (resultImage.empty()) { + QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("没有可保存的处理结果。")); + return false; + } + + if (!cv::imwrite(fileName.toLocal8Bit().constData(), resultImage)) { + QMessageBox::warning(this, QStringLiteral("保存失败"), QStringLiteral("无法写入该图片文件。")); + return false; + } + + return true; +} + +#include "MainWindow.moc" diff --git a/MainWindow.h b/MainWindow.h new file mode 100644 index 0000000..dfbe70d --- /dev/null +++ b/MainWindow.h @@ -0,0 +1,76 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include + +#include + +class QLabel; +class QPushButton; +class QSlider; +class QCheckBox; +class QThread; + +struct ProcessParams +{ + double scale = 1.0; + int translateX = 0; + int translateY = 0; + double angle = 0.0; + bool extractTarget = false; + bool classifyImage = false; +}; + +Q_DECLARE_METATYPE(cv::Mat) +Q_DECLARE_METATYPE(ProcessParams) + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +signals: + void processRequested(const cv::Mat &image, const ProcessParams ¶ms); + +private slots: + void openImage(); + void saveImage(); + void saveImageAs(); + void resetParams(); + void scheduleProcessing(); + void onProcessed(const cv::Mat &image, const QString &classText); + +private: + void buildUi(); + void updateOriginalView(); + void updateResultView(); + void setImageOnLabel(QLabel *label, const cv::Mat &mat); + ProcessParams currentParams() const; + bool writeResultToFile(const QString &fileName); + + QLabel *originalLabel = nullptr; + QLabel *resultLabel = nullptr; + QLabel *statusLabel = nullptr; + QPushButton *openButton = nullptr; + QPushButton *saveButton = nullptr; + QPushButton *saveAsButton = nullptr; + QPushButton *resetButton = nullptr; + QSlider *scaleSlider = nullptr; + QSlider *rotateSlider = nullptr; + QSlider *translateXSlider = nullptr; + QSlider *translateYSlider = nullptr; + QCheckBox *extractCheckBox = nullptr; + QCheckBox *classifyCheckBox = nullptr; + + cv::Mat originalImage; + cv::Mat resultImage; + QString savePath; + + QThread *workerThread = nullptr; +}; + +#endif diff --git a/QtOpenCVImageTool.zip b/QtOpenCVImageTool.zip new file mode 100644 index 0000000000000000000000000000000000000000..116fd4d50bb41fcd5dfdf110ccd1d49654fa5d89 GIT binary patch literal 6624 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 literal 0 HcmV?d00001 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..d0ee6f2 --- /dev/null +++ b/main.cpp @@ -0,0 +1,14 @@ +#include "MainWindow.h" + +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + MainWindow window; + window.resize(1180, 760); + window.show(); + + return app.exec(); +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..422bfad --- /dev/null +++ b/readme.md @@ -0,0 +1,39 @@ +# Qt + OpenCV 图像显示与处理 + +本工程使用 Qt Widgets 设计界面,使用 OpenCV 完成图像处理。程序支持打开图片、保存图片、另存图片,支持缩放、平移、旋转,并提供目标提取和简单图像分类功能。图像显示使用 Qt 的 `QLabel`/`QWidget`,没有使用 `cv::imshow`。 + +## 文件说明 + +- `CMakeLists.txt`:CMake 工程配置。 +- `main.cpp`:程序入口。 +- `MainWindow.h`:主窗口类和处理参数声明。 +- `MainWindow.cpp`:界面、图像读写、Qt 线程和 OpenCV 处理实现。 + +## 功能 + +- 打开图片:读取 `png/jpg/bmp/tif` 等常见格式。 +- 保存图片:保存当前处理结果。 +- 另存图片:将处理结果保存为新文件。 +- 显示对比:左侧显示处理前图像,右侧显示处理后图像。 +- 基本处理:缩放、水平平移、垂直平移、旋转。 +- 目标提取:使用灰度化、Otsu 阈值、形态学处理和轮廓检测提取前景目标,并用绿色矩形框标记目标。 +- 图像分类:根据平均亮度和主色通道给出简单分类,同时显示目标数量。 +- 线程处理:图像处理工作对象运行在 `QThread` 中,避免阻塞界面。 + +## 编译运行 + +需要先安装 Qt5 或 Qt6、OpenCV、CMake 和 C++17 编译器。 + +```bash +mkdir build +cd build +cmake .. +cmake --build . +./QtOpenCVImageTool +``` + +Windows 下可以使用 Qt Creator 打开本目录的 `CMakeLists.txt`,配置好 Qt 和 OpenCV 后直接构建运行。 + +## 说明 + +本示例中的分类属于简单规则分类,适合作为课程作业演示:根据图像整体亮度判断“偏亮/偏暗/亮度适中”,根据 BGR 平均值判断主色。若需要更高精度分类,可替换为训练好的 OpenCV DNN 或传统机器学习模型。