#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"