Files
chengnan/MainWindow.cpp
2026-05-31 12:10:10 +08:00

434 lines
14 KiB
C++
Raw Permalink 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 <QCheckBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QPixmap>
#include <QScrollArea>
#include <QSlider>
#include <QStatusBar>
#include <QThread>
#include <QVBoxLayout>
#include <algorithm>
#include <cmath>
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<int>(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<double>(0, 2) += bounds.width / 2.0 - image.cols / 2.0;
matrix.at<double>(1, 2) += bounds.height / 2.0 - image.rows / 2.0;
cv::Mat rotated;
const cv::Size outputSize(static_cast<int>(bounds.width), static_cast<int>(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_<double>(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<std::vector<cv::Point>> 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 &params)
{
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<std::vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
targetCount = static_cast<int>(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>("cv::Mat");
qRegisterMetaType<ProcessParams>("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"