Initial Linux Qt OpenCV project
This commit is contained in:
11
app/include/ImageConverter.h
Normal file
11
app/include/ImageConverter.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
class ImageConverter final
|
||||
{
|
||||
public:
|
||||
static QImage matToQImage(const cv::Mat &mat);
|
||||
static cv::Mat qImageToMat(const QImage &image);
|
||||
};
|
||||
23
app/include/ImageProcessor.h
Normal file
23
app/include/ImageProcessor.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QMetaType>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
struct ProcessResult
|
||||
{
|
||||
cv::Mat image;
|
||||
QString description;
|
||||
};
|
||||
|
||||
class ImageProcessor final
|
||||
{
|
||||
public:
|
||||
static ProcessResult rotate(const cv::Mat &source, double angleDegrees);
|
||||
static ProcessResult scale(const cv::Mat &source, double factor);
|
||||
static ProcessResult translate(const cv::Mat &source, int dx, int dy);
|
||||
static ProcessResult extractTargets(const cv::Mat &source);
|
||||
static ProcessResult classifySimple(const cv::Mat &source);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ProcessResult)
|
||||
35
app/include/ImageView.h
Normal file
35
app/include/ImageView.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
|
||||
class QGraphicsPixmapItem;
|
||||
class QGraphicsScene;
|
||||
class QWheelEvent;
|
||||
class QMouseEvent;
|
||||
|
||||
class ImageView final : public QGraphicsView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ImageView(QWidget *parent = nullptr);
|
||||
|
||||
void setImage(const QImage &image);
|
||||
void clearImage();
|
||||
void fitImageToView();
|
||||
bool hasImage() const;
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
void resetSceneRect();
|
||||
|
||||
QGraphicsScene *scene_ = nullptr;
|
||||
QGraphicsPixmapItem *pixmapItem_ = nullptr;
|
||||
QPixmap pixmap_;
|
||||
double zoomFactor_ = 1.0;
|
||||
};
|
||||
33
app/include/ImageWorker.h
Normal file
33
app/include/ImageWorker.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "ImageProcessor.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QMetaType>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
Q_DECLARE_METATYPE(cv::Mat)
|
||||
|
||||
class ImageWorker final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Operation {
|
||||
Rotate,
|
||||
Scale,
|
||||
Translate,
|
||||
ExtractTargets,
|
||||
ClassifySimple
|
||||
};
|
||||
Q_ENUM(Operation)
|
||||
|
||||
public Q_SLOTS:
|
||||
void process(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy);
|
||||
|
||||
Q_SIGNALS:
|
||||
void finished(ProcessResult result);
|
||||
void failed(QString message);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ImageWorker::Operation)
|
||||
72
app/include/MainWindow.h
Normal file
72
app/include/MainWindow.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "ImageWorker.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QThread>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
class QAction;
|
||||
class QLabel;
|
||||
class QSpinBox;
|
||||
class QDoubleSpinBox;
|
||||
class QPushButton;
|
||||
class ImageView;
|
||||
|
||||
class MainWindow final : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void processRequested(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy);
|
||||
|
||||
private Q_SLOTS:
|
||||
void openImage();
|
||||
void saveImage();
|
||||
void saveImageAs();
|
||||
void resetToOriginal();
|
||||
void onProcessFinished(ProcessResult result);
|
||||
void onProcessFailed(const QString &message);
|
||||
void rotateLeft();
|
||||
void rotateRight();
|
||||
void scaleUp();
|
||||
void scaleDown();
|
||||
void translateUp();
|
||||
void translateDown();
|
||||
void translateLeft();
|
||||
void translateRight();
|
||||
void extractTargets();
|
||||
void classifyImage();
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
void setupWorkerThread();
|
||||
void updateViews();
|
||||
void setBusy(bool busy);
|
||||
bool ensureHasImage() const;
|
||||
void requestProcess(ImageWorker::Operation operation, double value = 0.0, int dx = 0, int dy = 0);
|
||||
bool writeCurrentImageTo(const QString &path);
|
||||
|
||||
ImageView *originalView_ = nullptr;
|
||||
ImageView *processedView_ = nullptr;
|
||||
QLabel *infoLabel_ = nullptr;
|
||||
QLabel *statusLabel_ = nullptr;
|
||||
QDoubleSpinBox *scaleSpin_ = nullptr;
|
||||
QDoubleSpinBox *angleSpin_ = nullptr;
|
||||
QSpinBox *translateSpin_ = nullptr;
|
||||
|
||||
QAction *saveAction_ = nullptr;
|
||||
QAction *saveAsAction_ = nullptr;
|
||||
|
||||
QThread workerThread_;
|
||||
ImageWorker *worker_ = nullptr;
|
||||
|
||||
cv::Mat originalImage_;
|
||||
cv::Mat processedImage_;
|
||||
QString currentPath_;
|
||||
bool busy_ = false;
|
||||
};
|
||||
47
app/src/ImageConverter.cpp
Normal file
47
app/src/ImageConverter.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "ImageConverter.h"
|
||||
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
QImage ImageConverter::matToQImage(const cv::Mat &mat)
|
||||
{
|
||||
if (mat.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (mat.type()) {
|
||||
case CV_8UC1: {
|
||||
QImage image(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_Grayscale8);
|
||||
return image.copy();
|
||||
}
|
||||
case CV_8UC3: {
|
||||
cv::Mat rgb;
|
||||
cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB);
|
||||
QImage image(rgb.data, rgb.cols, rgb.rows, static_cast<int>(rgb.step), QImage::Format_RGB888);
|
||||
return image.copy();
|
||||
}
|
||||
case CV_8UC4: {
|
||||
QImage image(mat.data, mat.cols, mat.rows, static_cast<int>(mat.step), QImage::Format_ARGB32);
|
||||
return image.copy();
|
||||
}
|
||||
default: {
|
||||
cv::Mat converted;
|
||||
mat.convertTo(converted, CV_8U);
|
||||
return matToQImage(converted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cv::Mat ImageConverter::qImageToMat(const QImage &image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QImage converted = image.convertToFormat(QImage::Format_RGB888);
|
||||
cv::Mat rgb(converted.height(), converted.width(), CV_8UC3,
|
||||
const_cast<uchar *>(converted.bits()),
|
||||
static_cast<size_t>(converted.bytesPerLine()));
|
||||
cv::Mat bgr;
|
||||
cv::cvtColor(rgb, bgr, cv::COLOR_RGB2BGR);
|
||||
return bgr.clone();
|
||||
}
|
||||
149
app/src/ImageProcessor.cpp
Normal file
149
app/src/ImageProcessor.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include "ImageProcessor.h"
|
||||
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
ProcessResult ImageProcessor::rotate(const cv::Mat &source, double angleDegrees)
|
||||
{
|
||||
if (source.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const cv::Point2f center(static_cast<float>(source.cols) / 2.0F, static_cast<float>(source.rows) / 2.0F);
|
||||
cv::Mat matrix = cv::getRotationMatrix2D(center, angleDegrees, 1.0);
|
||||
|
||||
const double absCos = std::abs(matrix.at<double>(0, 0));
|
||||
const double absSin = std::abs(matrix.at<double>(0, 1));
|
||||
const int boundW = static_cast<int>(source.rows * absSin + source.cols * absCos);
|
||||
const int boundH = static_cast<int>(source.rows * absCos + source.cols * absSin);
|
||||
|
||||
matrix.at<double>(0, 2) += boundW / 2.0 - center.x;
|
||||
matrix.at<double>(1, 2) += boundH / 2.0 - center.y;
|
||||
|
||||
cv::Mat output;
|
||||
cv::warpAffine(source, output, matrix, cv::Size(boundW, boundH), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(30, 30, 30));
|
||||
return {output, QStringLiteral("旋转 %1° 完成").arg(angleDegrees)};
|
||||
}
|
||||
|
||||
ProcessResult ImageProcessor::scale(const cv::Mat &source, double factor)
|
||||
{
|
||||
if (source.empty() || factor <= 0.0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
cv::Mat output;
|
||||
cv::resize(source, output, cv::Size(), factor, factor, factor >= 1.0 ? cv::INTER_CUBIC : cv::INTER_AREA);
|
||||
return {output, QStringLiteral("缩放 %1 倍完成,尺寸:%2 × %3").arg(factor).arg(output.cols).arg(output.rows)};
|
||||
}
|
||||
|
||||
ProcessResult ImageProcessor::translate(const cv::Mat &source, int dx, int dy)
|
||||
{
|
||||
if (source.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const cv::Mat matrix = (cv::Mat_<double>(2, 3) << 1, 0, dx, 0, 1, dy);
|
||||
cv::Mat output;
|
||||
cv::warpAffine(source, output, matrix, source.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(30, 30, 30));
|
||||
return {output, QStringLiteral("平移完成:dx=%1, dy=%2").arg(dx).arg(dy)};
|
||||
}
|
||||
|
||||
ProcessResult ImageProcessor::extractTargets(const cv::Mat &source)
|
||||
{
|
||||
if (source.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
cv::Mat gray;
|
||||
if (source.channels() == 1) {
|
||||
gray = source.clone();
|
||||
} else {
|
||||
cv::cvtColor(source, gray, cv::COLOR_BGR2GRAY);
|
||||
}
|
||||
|
||||
cv::Mat blurred;
|
||||
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
|
||||
|
||||
cv::Mat binary;
|
||||
cv::threshold(blurred, 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);
|
||||
|
||||
cv::Mat output;
|
||||
if (source.channels() == 1) {
|
||||
cv::cvtColor(source, output, cv::COLOR_GRAY2BGR);
|
||||
} else {
|
||||
output = source.clone();
|
||||
}
|
||||
|
||||
int validCount = 0;
|
||||
double totalArea = 0.0;
|
||||
const double minArea = std::max(80.0, source.cols * source.rows * 0.0005);
|
||||
|
||||
for (const auto &contour : contours) {
|
||||
const double area = cv::contourArea(contour);
|
||||
if (area < minArea) {
|
||||
continue;
|
||||
}
|
||||
++validCount;
|
||||
totalArea += area;
|
||||
const cv::Rect box = cv::boundingRect(contour);
|
||||
cv::rectangle(output, box, cv::Scalar(0, 0, 255), 2);
|
||||
cv::drawContours(output, std::vector<std::vector<cv::Point>>{contour}, -1, cv::Scalar(0, 255, 0), 2);
|
||||
const cv::Moments moments = cv::moments(contour);
|
||||
if (moments.m00 != 0.0) {
|
||||
const int cx = static_cast<int>(moments.m10 / moments.m00);
|
||||
const int cy = static_cast<int>(moments.m01 / moments.m00);
|
||||
cv::circle(output, cv::Point(cx, cy), 4, cv::Scalar(255, 0, 0), -1);
|
||||
}
|
||||
}
|
||||
|
||||
return {output, QStringLiteral("目标提取完成:检测到 %1 个目标,总面积约 %2 像素")
|
||||
.arg(validCount)
|
||||
.arg(static_cast<int>(totalArea))};
|
||||
}
|
||||
|
||||
ProcessResult ImageProcessor::classifySimple(const cv::Mat &source)
|
||||
{
|
||||
if (source.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
cv::Mat hsv;
|
||||
if (source.channels() == 1) {
|
||||
cv::cvtColor(source, hsv, cv::COLOR_GRAY2BGR);
|
||||
cv::cvtColor(hsv, hsv, cv::COLOR_BGR2HSV);
|
||||
} else {
|
||||
cv::cvtColor(source, hsv, cv::COLOR_BGR2HSV);
|
||||
}
|
||||
|
||||
const cv::Scalar meanHsv = cv::mean(hsv);
|
||||
const double hue = meanHsv[0];
|
||||
const double saturation = meanHsv[1];
|
||||
const double value = meanHsv[2];
|
||||
|
||||
QString type;
|
||||
if (value < 70) {
|
||||
type = QStringLiteral("偏暗图像");
|
||||
} else if (saturation < 45) {
|
||||
type = QStringLiteral("低饱和 / 灰度感图像");
|
||||
} else if (hue < 15 || hue > 160) {
|
||||
type = QStringLiteral("红色系图像");
|
||||
} else if (hue < 45) {
|
||||
type = QStringLiteral("黄色 / 橙色系图像");
|
||||
} else if (hue < 85) {
|
||||
type = QStringLiteral("绿色系图像");
|
||||
} else if (hue < 130) {
|
||||
type = QStringLiteral("蓝色系图像");
|
||||
} else {
|
||||
type = QStringLiteral("紫色系图像");
|
||||
}
|
||||
|
||||
cv::Mat output = source.clone();
|
||||
return {output, QStringLiteral("简单分类:%1(平均 H=%2, S=%3, V=%4)")
|
||||
.arg(type)
|
||||
.arg(hue, 0, 'f', 1)
|
||||
.arg(saturation, 0, 'f', 1)
|
||||
.arg(value, 0, 'f', 1)};
|
||||
}
|
||||
89
app/src/ImageView.cpp
Normal file
89
app/src/ImageView.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "ImageView.h"
|
||||
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScrollBar>
|
||||
#include <QWheelEvent>
|
||||
|
||||
ImageView::ImageView(QWidget *parent)
|
||||
: QGraphicsView(parent), scene_(new QGraphicsScene(this))
|
||||
{
|
||||
setScene(scene_);
|
||||
pixmapItem_ = scene_->addPixmap(QPixmap());
|
||||
pixmapItem_->setTransformationMode(Qt::SmoothTransformation);
|
||||
|
||||
setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
|
||||
setDragMode(QGraphicsView::ScrollHandDrag);
|
||||
setViewportUpdateMode(QGraphicsView::SmartViewportUpdate);
|
||||
setBackgroundBrush(QColor(36, 36, 36));
|
||||
setFrameShape(QFrame::StyledPanel);
|
||||
setAlignment(Qt::AlignCenter);
|
||||
}
|
||||
|
||||
void ImageView::setImage(const QImage &image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
clearImage();
|
||||
return;
|
||||
}
|
||||
|
||||
pixmap_ = QPixmap::fromImage(image);
|
||||
pixmapItem_->setPixmap(pixmap_);
|
||||
resetSceneRect();
|
||||
fitImageToView();
|
||||
}
|
||||
|
||||
void ImageView::clearImage()
|
||||
{
|
||||
pixmap_ = QPixmap();
|
||||
pixmapItem_->setPixmap(QPixmap());
|
||||
scene_->setSceneRect(QRectF());
|
||||
resetTransform();
|
||||
zoomFactor_ = 1.0;
|
||||
}
|
||||
|
||||
void ImageView::fitImageToView()
|
||||
{
|
||||
if (!hasImage()) {
|
||||
return;
|
||||
}
|
||||
resetTransform();
|
||||
fitInView(pixmapItem_, Qt::KeepAspectRatio);
|
||||
zoomFactor_ = transform().m11();
|
||||
}
|
||||
|
||||
bool ImageView::hasImage() const
|
||||
{
|
||||
return !pixmap_.isNull();
|
||||
}
|
||||
|
||||
void ImageView::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
if (!hasImage()) {
|
||||
QGraphicsView::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
const int delta = event->angleDelta().y();
|
||||
#else
|
||||
const int delta = event->delta();
|
||||
#endif
|
||||
const double factor = delta > 0 ? 1.15 : 1.0 / 1.15;
|
||||
zoomFactor_ *= factor;
|
||||
scale(factor, factor);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void ImageView::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
fitImageToView();
|
||||
}
|
||||
|
||||
void ImageView::resetSceneRect()
|
||||
{
|
||||
scene_->setSceneRect(pixmap_.rect());
|
||||
}
|
||||
35
app/src/ImageWorker.cpp
Normal file
35
app/src/ImageWorker.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "ImageWorker.h"
|
||||
|
||||
void ImageWorker::process(cv::Mat image, ImageWorker::Operation operation, double value, int dx, int dy)
|
||||
{
|
||||
if (image.empty()) {
|
||||
Q_EMIT failed(QStringLiteral("没有可处理的图片"));
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessResult result;
|
||||
switch (operation) {
|
||||
case Operation::Rotate:
|
||||
result = ImageProcessor::rotate(image, value);
|
||||
break;
|
||||
case Operation::Scale:
|
||||
result = ImageProcessor::scale(image, value);
|
||||
break;
|
||||
case Operation::Translate:
|
||||
result = ImageProcessor::translate(image, dx, dy);
|
||||
break;
|
||||
case Operation::ExtractTargets:
|
||||
result = ImageProcessor::extractTargets(image);
|
||||
break;
|
||||
case Operation::ClassifySimple:
|
||||
result = ImageProcessor::classifySimple(image);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.image.empty()) {
|
||||
Q_EMIT failed(QStringLiteral("图像处理失败"));
|
||||
return;
|
||||
}
|
||||
|
||||
Q_EMIT finished(result);
|
||||
}
|
||||
369
app/src/MainWindow.cpp
Normal file
369
app/src/MainWindow.cpp
Normal file
@@ -0,0 +1,369 @@
|
||||
#include "MainWindow.h"
|
||||
|
||||
#include "ImageConverter.h"
|
||||
#include "ImageView.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QBoxLayout>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QMenuBar>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QSplitter>
|
||||
#include <QStatusBar>
|
||||
#include <QToolBar>
|
||||
#include <QWidget>
|
||||
#include <opencv2/imgcodecs.hpp>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
{
|
||||
qRegisterMetaType<ProcessResult>("ProcessResult");
|
||||
qRegisterMetaType<ImageWorker::Operation>("ImageWorker::Operation");
|
||||
qRegisterMetaType<cv::Mat>("cv::Mat");
|
||||
|
||||
buildUi();
|
||||
setupWorkerThread();
|
||||
setWindowTitle(QStringLiteral("chengnan - Qt/OpenCV 图像处理"));
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
workerThread_.quit();
|
||||
workerThread_.wait();
|
||||
}
|
||||
|
||||
void MainWindow::buildUi()
|
||||
{
|
||||
auto *openAction = new QAction(QStringLiteral("打开图片"), this);
|
||||
saveAction_ = new QAction(QStringLiteral("保存图片"), this);
|
||||
saveAsAction_ = new QAction(QStringLiteral("另存图片"), this);
|
||||
auto *resetAction = new QAction(QStringLiteral("还原原图"), this);
|
||||
auto *exitAction = new QAction(QStringLiteral("退出"), this);
|
||||
|
||||
connect(openAction, &QAction::triggered, this, &MainWindow::openImage);
|
||||
connect(saveAction_, &QAction::triggered, this, &MainWindow::saveImage);
|
||||
connect(saveAsAction_, &QAction::triggered, this, &MainWindow::saveImageAs);
|
||||
connect(resetAction, &QAction::triggered, this, &MainWindow::resetToOriginal);
|
||||
connect(exitAction, &QAction::triggered, qApp, &QApplication::quit);
|
||||
|
||||
auto *fileMenu = menuBar()->addMenu(QStringLiteral("文件"));
|
||||
fileMenu->addAction(openAction);
|
||||
fileMenu->addAction(saveAction_);
|
||||
fileMenu->addAction(saveAsAction_);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(exitAction);
|
||||
|
||||
auto *editMenu = menuBar()->addMenu(QStringLiteral("处理"));
|
||||
editMenu->addAction(resetAction);
|
||||
|
||||
auto *toolbar = addToolBar(QStringLiteral("主工具栏"));
|
||||
toolbar->addAction(openAction);
|
||||
toolbar->addAction(saveAction_);
|
||||
toolbar->addAction(saveAsAction_);
|
||||
toolbar->addAction(resetAction);
|
||||
|
||||
originalView_ = new ImageView(this);
|
||||
processedView_ = new ImageView(this);
|
||||
|
||||
auto *leftBox = new QGroupBox(QStringLiteral("处理前图像"), this);
|
||||
auto *leftLayout = new QVBoxLayout(leftBox);
|
||||
leftLayout->addWidget(originalView_);
|
||||
|
||||
auto *rightBox = new QGroupBox(QStringLiteral("处理后图像"), this);
|
||||
auto *rightLayout = new QVBoxLayout(rightBox);
|
||||
rightLayout->addWidget(processedView_);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Horizontal, this);
|
||||
splitter->addWidget(leftBox);
|
||||
splitter->addWidget(rightBox);
|
||||
splitter->setStretchFactor(0, 1);
|
||||
splitter->setStretchFactor(1, 1);
|
||||
|
||||
scaleSpin_ = new QDoubleSpinBox(this);
|
||||
scaleSpin_->setRange(0.1, 5.0);
|
||||
scaleSpin_->setSingleStep(0.1);
|
||||
scaleSpin_->setValue(1.25);
|
||||
scaleSpin_->setSuffix(QStringLiteral(" 倍"));
|
||||
|
||||
angleSpin_ = new QDoubleSpinBox(this);
|
||||
angleSpin_->setRange(-360.0, 360.0);
|
||||
angleSpin_->setSingleStep(5.0);
|
||||
angleSpin_->setValue(90.0);
|
||||
angleSpin_->setSuffix(QStringLiteral("°"));
|
||||
|
||||
translateSpin_ = new QSpinBox(this);
|
||||
translateSpin_->setRange(1, 1000);
|
||||
translateSpin_->setValue(40);
|
||||
translateSpin_->setSuffix(QStringLiteral(" px"));
|
||||
|
||||
auto *scaleUpButton = new QPushButton(QStringLiteral("放大"), this);
|
||||
auto *scaleDownButton = new QPushButton(QStringLiteral("缩小"), this);
|
||||
auto *rotateLeftButton = new QPushButton(QStringLiteral("逆时针旋转"), this);
|
||||
auto *rotateRightButton = new QPushButton(QStringLiteral("顺时针旋转"), this);
|
||||
auto *upButton = new QPushButton(QStringLiteral("上移"), this);
|
||||
auto *downButton = new QPushButton(QStringLiteral("下移"), this);
|
||||
auto *leftButton = new QPushButton(QStringLiteral("左移"), this);
|
||||
auto *rightButton = new QPushButton(QStringLiteral("右移"), this);
|
||||
auto *extractButton = new QPushButton(QStringLiteral("目标提取"), this);
|
||||
auto *classifyButton = new QPushButton(QStringLiteral("简单分类"), this);
|
||||
|
||||
connect(scaleUpButton, &QPushButton::clicked, this, &MainWindow::scaleUp);
|
||||
connect(scaleDownButton, &QPushButton::clicked, this, &MainWindow::scaleDown);
|
||||
connect(rotateLeftButton, &QPushButton::clicked, this, &MainWindow::rotateLeft);
|
||||
connect(rotateRightButton, &QPushButton::clicked, this, &MainWindow::rotateRight);
|
||||
connect(upButton, &QPushButton::clicked, this, &MainWindow::translateUp);
|
||||
connect(downButton, &QPushButton::clicked, this, &MainWindow::translateDown);
|
||||
connect(leftButton, &QPushButton::clicked, this, &MainWindow::translateLeft);
|
||||
connect(rightButton, &QPushButton::clicked, this, &MainWindow::translateRight);
|
||||
connect(extractButton, &QPushButton::clicked, this, &MainWindow::extractTargets);
|
||||
connect(classifyButton, &QPushButton::clicked, this, &MainWindow::classifyImage);
|
||||
|
||||
auto *controlBox = new QGroupBox(QStringLiteral("图像处理操作"), this);
|
||||
auto *controlLayout = new QHBoxLayout(controlBox);
|
||||
controlLayout->addWidget(new QLabel(QStringLiteral("缩放比例:"), this));
|
||||
controlLayout->addWidget(scaleSpin_);
|
||||
controlLayout->addWidget(scaleUpButton);
|
||||
controlLayout->addWidget(scaleDownButton);
|
||||
controlLayout->addSpacing(12);
|
||||
controlLayout->addWidget(new QLabel(QStringLiteral("旋转角度:"), this));
|
||||
controlLayout->addWidget(angleSpin_);
|
||||
controlLayout->addWidget(rotateLeftButton);
|
||||
controlLayout->addWidget(rotateRightButton);
|
||||
controlLayout->addSpacing(12);
|
||||
controlLayout->addWidget(new QLabel(QStringLiteral("平移距离:"), this));
|
||||
controlLayout->addWidget(translateSpin_);
|
||||
controlLayout->addWidget(upButton);
|
||||
controlLayout->addWidget(downButton);
|
||||
controlLayout->addWidget(leftButton);
|
||||
controlLayout->addWidget(rightButton);
|
||||
controlLayout->addSpacing(12);
|
||||
controlLayout->addWidget(extractButton);
|
||||
controlLayout->addWidget(classifyButton);
|
||||
controlLayout->addStretch(1);
|
||||
|
||||
infoLabel_ = new QLabel(QStringLiteral("请打开一张图片开始处理。提示:鼠标滚轮可缩放视图,双击图片可适配窗口。"), this);
|
||||
infoLabel_->setWordWrap(true);
|
||||
|
||||
auto *central = new QWidget(this);
|
||||
auto *mainLayout = new QVBoxLayout(central);
|
||||
mainLayout->addWidget(splitter, 1);
|
||||
mainLayout->addWidget(controlBox);
|
||||
mainLayout->addWidget(infoLabel_);
|
||||
setCentralWidget(central);
|
||||
|
||||
statusLabel_ = new QLabel(QStringLiteral("就绪"), this);
|
||||
statusBar()->addPermanentWidget(statusLabel_, 1);
|
||||
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
void MainWindow::setupWorkerThread()
|
||||
{
|
||||
worker_ = new ImageWorker();
|
||||
worker_->moveToThread(&workerThread_);
|
||||
|
||||
connect(&workerThread_, &QThread::finished, worker_, &QObject::deleteLater);
|
||||
connect(this, &MainWindow::processRequested, worker_, &ImageWorker::process, Qt::QueuedConnection);
|
||||
connect(worker_, &ImageWorker::finished, this, &MainWindow::onProcessFinished, Qt::QueuedConnection);
|
||||
connect(worker_, &ImageWorker::failed, this, &MainWindow::onProcessFailed, Qt::QueuedConnection);
|
||||
|
||||
workerThread_.start();
|
||||
}
|
||||
|
||||
void MainWindow::openImage()
|
||||
{
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
QStringLiteral("打开图片"),
|
||||
QString(),
|
||||
QStringLiteral("图片文件 (*.png *.jpg *.jpeg *.bmp *.tif *.tiff);;所有文件 (*.*)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string localPath = QFile::encodeName(path).toStdString();
|
||||
cv::Mat image = cv::imread(localPath, cv::IMREAD_COLOR);
|
||||
if (image.empty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("打开失败"), QStringLiteral("无法读取图片:%1").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
originalImage_ = image;
|
||||
processedImage_ = image.clone();
|
||||
currentPath_ = path;
|
||||
updateViews();
|
||||
infoLabel_->setText(QStringLiteral("已打开:%1,尺寸:%2 × %3")
|
||||
.arg(QFileInfo(path).fileName())
|
||||
.arg(image.cols)
|
||||
.arg(image.rows));
|
||||
statusLabel_->setText(QStringLiteral("已打开图片"));
|
||||
}
|
||||
|
||||
void MainWindow::saveImage()
|
||||
{
|
||||
if (!ensureHasImage()) {
|
||||
return;
|
||||
}
|
||||
if (currentPath_.isEmpty()) {
|
||||
saveImageAs();
|
||||
return;
|
||||
}
|
||||
writeCurrentImageTo(currentPath_);
|
||||
}
|
||||
|
||||
void MainWindow::saveImageAs()
|
||||
{
|
||||
if (!ensureHasImage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
QStringLiteral("另存图片"),
|
||||
currentPath_.isEmpty() ? QStringLiteral("processed.png") : currentPath_,
|
||||
QStringLiteral("PNG 图片 (*.png);;JPEG 图片 (*.jpg *.jpeg);;BMP 图片 (*.bmp);;所有文件 (*.*)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (writeCurrentImageTo(path)) {
|
||||
currentPath_ = path;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::resetToOriginal()
|
||||
{
|
||||
if (!ensureHasImage()) {
|
||||
return;
|
||||
}
|
||||
processedImage_ = originalImage_.clone();
|
||||
updateViews();
|
||||
infoLabel_->setText(QStringLiteral("已还原为原始图片"));
|
||||
}
|
||||
|
||||
void MainWindow::onProcessFinished(ProcessResult result)
|
||||
{
|
||||
processedImage_ = result.image;
|
||||
updateViews();
|
||||
infoLabel_->setText(result.description);
|
||||
statusLabel_->setText(QStringLiteral("处理完成"));
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
void MainWindow::onProcessFailed(const QString &message)
|
||||
{
|
||||
QMessageBox::warning(this, QStringLiteral("处理失败"), message);
|
||||
statusLabel_->setText(QStringLiteral("处理失败"));
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
void MainWindow::rotateLeft()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Rotate, -angleSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::rotateRight()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Rotate, angleSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::scaleUp()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Scale, scaleSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::scaleDown()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Scale, 1.0 / scaleSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::translateUp()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Translate, 0.0, 0, -translateSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::translateDown()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Translate, 0.0, 0, translateSpin_->value());
|
||||
}
|
||||
|
||||
void MainWindow::translateLeft()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Translate, 0.0, -translateSpin_->value(), 0);
|
||||
}
|
||||
|
||||
void MainWindow::translateRight()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::Translate, 0.0, translateSpin_->value(), 0);
|
||||
}
|
||||
|
||||
void MainWindow::extractTargets()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::ExtractTargets);
|
||||
}
|
||||
|
||||
void MainWindow::classifyImage()
|
||||
{
|
||||
requestProcess(ImageWorker::Operation::ClassifySimple);
|
||||
}
|
||||
|
||||
void MainWindow::updateViews()
|
||||
{
|
||||
originalView_->setImage(ImageConverter::matToQImage(originalImage_));
|
||||
processedView_->setImage(ImageConverter::matToQImage(processedImage_));
|
||||
}
|
||||
|
||||
void MainWindow::setBusy(bool busy)
|
||||
{
|
||||
busy_ = busy;
|
||||
saveAction_->setEnabled(!busy && !processedImage_.empty());
|
||||
saveAsAction_->setEnabled(!busy && !processedImage_.empty());
|
||||
if (busy) {
|
||||
statusLabel_->setText(QStringLiteral("处理中..."));
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::ensureHasImage() const
|
||||
{
|
||||
if (processedImage_.empty()) {
|
||||
QMessageBox::information(const_cast<MainWindow *>(this), QStringLiteral("提示"), QStringLiteral("请先打开图片。"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::requestProcess(ImageWorker::Operation operation, double value, int dx, int dy)
|
||||
{
|
||||
if (busy_) {
|
||||
return;
|
||||
}
|
||||
if (!ensureHasImage()) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
Q_EMIT processRequested(processedImage_.clone(), operation, value, dx, dy);
|
||||
}
|
||||
|
||||
bool MainWindow::writeCurrentImageTo(const QString &path)
|
||||
{
|
||||
if (processedImage_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string localPath = QFile::encodeName(path).toStdString();
|
||||
if (!cv::imwrite(localPath, processedImage_)) {
|
||||
QMessageBox::warning(this, QStringLiteral("保存失败"), QStringLiteral("无法保存图片:%1").arg(path));
|
||||
return false;
|
||||
}
|
||||
|
||||
statusLabel_->setText(QStringLiteral("已保存图片"));
|
||||
infoLabel_->setText(QStringLiteral("已保存处理后图片:%1").arg(path));
|
||||
return true;
|
||||
}
|
||||
20
app/src/main.cpp
Normal file
20
app/src/main.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include <QApplication>
|
||||
#include <QMetaType>
|
||||
|
||||
#include "MainWindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
QApplication::setApplicationName("chengnan");
|
||||
QApplication::setOrganizationName("luochen570");
|
||||
QApplication::setApplicationVersion(CHENGNAN_APP_VERSION);
|
||||
|
||||
qRegisterMetaType<cv::Mat>("cv::Mat");
|
||||
|
||||
MainWindow window;
|
||||
window.resize(1280, 760);
|
||||
window.show();
|
||||
|
||||
return QApplication::exec();
|
||||
}
|
||||
Reference in New Issue
Block a user