Initial Qt OpenCV image tool

This commit is contained in:
luochen570
2026-05-31 12:10:10 +08:00
commit c658fa4265
6 changed files with 588 additions and 0 deletions

433
MainWindow.cpp Normal file
View File

@@ -0,0 +1,433 @@
#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"