434 lines
14 KiB
C++
434 lines
14 KiB
C++
#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 ¶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<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"
|