Implement Qt OpenCV image processing app
Some checks failed
Build Windows Release / Build Windows x64 Release on Linux (MinGW + Qt + OpenCV) (push) Failing after 2m15s

This commit is contained in:
luochen570
2026-05-30 23:09:27 +08:00
parent 03b873d4dd
commit 573c9d746c
6 changed files with 456 additions and 383 deletions

View File

@@ -10,12 +10,119 @@
#include <QMenuBar>
#include <QMessageBox>
#include <QMouseEvent>
#include <QScrollBar>
#include <QSlider>
#include <QSplitter>
#include <QStatusBar>
#include <QThread>
#include <QToolBar>
#include <QTransform>
#include <opencv2/imgproc.hpp>
#include <algorithm>
#include <cmath>
#include <exception>
#include <memory>
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<uchar *>(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<int>(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<std::vector<cv::Point>> 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<double>(cv::countNonZero(edges)) / static_cast<double>(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)
@@ -45,8 +152,8 @@ MainWindow::MainWindow(QWidget *parent)
createCentralWidget();
updateActions();
setWindowTitle(tr("chengnan 图片查看器"));
resize(1100, 720);
setWindowTitle(tr("chengnan 图片处理工具"));
resize(1200, 720);
statusBar()->showMessage(tr("就绪"));
}
@@ -101,6 +208,12 @@ void MainWindow::createActions()
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()
@@ -126,6 +239,9 @@ void MainWindow::createMenus()
processMenu->addSeparator();
processMenu->addAction(grayscaleAction_);
processMenu->addAction(invertAction_);
processMenu->addSeparator();
processMenu->addAction(extractAction_);
processMenu->addAction(classifyAction_);
}
void MainWindow::createToolbar()
@@ -152,18 +268,35 @@ void MainWindow::createToolbar()
toolbar->addAction(rotateRightAction_);
toolbar->addAction(grayscaleAction_);
toolbar->addAction(invertAction_);
toolbar->addAction(extractAction_);
toolbar->addAction(classifyAction_);
}
void MainWindow::createCentralWidget()
{
imageLabel_ = new ImageLabel(this);
connect(imageLabel_, &ImageLabel::doubleClicked, this, &MainWindow::fitToWindow);
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);
scrollArea_ = new QScrollArea(this);
scrollArea_->setBackgroundRole(QPalette::Dark);
scrollArea_->setWidget(imageLabel_);
scrollArea_->setWidgetResizable(true);
setCentralWidget(scrollArea_);
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()
@@ -236,7 +369,7 @@ void MainWindow::fitToWindow()
return;
}
const QSize viewportSize = scrollArea_->viewport()->size();
const QSize viewportSize = processedScrollArea_->viewport()->size();
const QSize imageSize = currentImage_.size();
if (imageSize.isEmpty()) {
return;
@@ -290,20 +423,35 @@ void MainWindow::flipVertical()
void MainWindow::grayscale()
{
if (!ensureImageLoaded(tr("灰度化"))) {
return;
}
applyTransformedImage(currentImage_.convertToFormat(QImage::Format_Grayscale8));
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()
{
if (!ensureImageLoaded(tr("反色"))) {
return;
}
QImage image = currentImage_.convertToFormat(QImage::Format_ARGB32);
image.invertPixels(QImage::InvertRgb);
applyTransformedImage(image);
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)
@@ -317,43 +465,55 @@ void MainWindow::updateZoomFromSlider(int value)
void MainWindow::setImage(const QImage &image, const QString &path)
{
currentImage_ = image.convertToFormat(QImage::Format_ARGB32);
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()));
? 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()) {
imageLabel_->setPixmap(QPixmap());
imageLabel_->setText(tr("打开一张图片开始查看"));
originalLabel_->setPixmap(QPixmap());
processedLabel_->setPixmap(QPixmap());
originalLabel_->setText(tr("处理前图像"));
processedLabel_->setText(tr("处理后图像"));
return;
}
const QSize scaledSize = currentImage_.size() * zoomFactor_;
const QPixmap pixmap = QPixmap::fromImage(currentImage_).scaled(
updateSingleView(originalLabel_, originalImage_);
updateSingleView(processedLabel_, currentImage_);
const int sliderValue = static_cast<int>(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<int>(std::round(image.width() * zoomFactor_))),
std::max(1, static_cast<int>(std::round(image.height() * zoomFactor_))));
const QPixmap pixmap = QPixmap::fromImage(image).scaled(
scaledSize,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
imageLabel_->setText(QString());
imageLabel_->setPixmap(pixmap);
imageLabel_->resize(pixmap.size());
if (zoomSlider_->value() != static_cast<int>(zoomFactor_ * 100.0)) {
zoomSlider_->blockSignals(true);
zoomSlider_->setValue(static_cast<int>(zoomFactor_ * 100.0));
zoomSlider_->blockSignals(false);
}
statusBar()->showMessage(tr("缩放:%1%").arg(static_cast<int>(zoomFactor_ * 100.0)));
label->setText(QString());
label->setPixmap(pixmap);
label->resize(pixmap.size());
}
void MainWindow::setZoomFactor(double factor)
@@ -362,12 +522,45 @@ void MainWindow::setZoomFactor(double factor)
updateImageView();
}
void MainWindow::applyTransformedImage(const QImage &image)
void MainWindow::applyTransformedImage(const QImage &image, const QString &message)
{
currentImage_ = image.convertToFormat(QImage::Format_ARGB32);
updateImageView();
updateActions();
statusBar()->showMessage(tr("处理完成,可保存结果"), 3000);
statusBar()->showMessage(message.isEmpty() ? tr("处理完成,可保存结果") : message, 3000);
}
void MainWindow::processImageInThread(const QString &actionName,
const QString &doneMessage,
const std::function<QImage(QImage)> &processor)
{
if (!ensureImageLoaded(actionName)) {
return;
}
const QImage input = currentImage_;
auto result = std::make_shared<QImage>();
auto error = std::make_shared<QString>();
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()
@@ -385,6 +578,8 @@ void MainWindow::updateActions()
flipVerticalAction_->setEnabled(hasImage);
grayscaleAction_->setEnabled(hasImage);
invertAction_->setEnabled(hasImage);
extractAction_->setEnabled(hasImage);
classifyAction_->setEnabled(hasImage);
zoomSlider_->setEnabled(hasImage);
}
@@ -407,7 +602,7 @@ bool MainWindow::writeImageToPath(const QString &path)
}
currentPath_ = path;
setWindowTitle(tr("%1 - chengnan 图片查看器").arg(QFileInfo(path).fileName()));
setWindowTitle(tr("%1 - chengnan 图片处理工具").arg(QFileInfo(path).fileName()));
statusBar()->showMessage(tr("已保存:%1").arg(path), 5000);
return true;
}