VS2026+QT6.9+opencv图像增强(多帧平均降噪)(CLAHE对比度增强)(边缘增强)(图像超分辨率)

目录

一、前言

二、代码详解

三、gitee完整项目下载


一、前言

本方案使用于医疗设备X射线穿透,探测器获取16位灰度图像产生的随机噪声,方法是一次获取多帧图像并求平均来消除这种随机噪声,并做简单的预处理。

图像处理的流程如下:

1、主线程中获取5张raw原图,在子线程中进行图像的处理(注意:这5张raw图是5张连续的帧,每帧的噪声是随机的,这是X射线图的特性,帧数越多,去噪效果理应越好,视情况而定)

  • 当 N=1(单帧):σₙₑw=σ(无去噪效果);
  • 当 N=5:σₙₑw≈σ/2.24(噪声强度降低约 55%);
  • 当 N=10:σₙₑw≈σ/3.16(噪声强度降低约 68%);
  • 当 N=100:σₙₑw=σ/10(噪声强度降低 90%)

2、多帧ORB对齐

3、多帧平均去噪

4、CLAHE对比度增强

5、Sobel边缘提取

6、ESPCN超分

准备工作:

1、本次项目所用到的opencv头文件需要使用cmake编译opencv_contrib拓展库

cmake版本:cmake-4.1.1-windows-x86_64

opencv版本:opencv-4.12.0-windows

opencv拓展库版本:opencv_contrib-4.12.0

cmake & opencv & opencv_contrib百度网盘下载

编译教程:有空更新

cpp 复制代码
#include <opencv2/opencv.hpp>       // 引入opencv头文件
#include <opencv2/dnn_superres.hpp> // 引入超分辨率头文件
#include <opencv2/features2d.hpp>   // 引入特征检测头文件

2、本次项目所用到的超分辨率模型为ESPCN_x4.pb模型(提供x2 x3 x4的EDSR模型文件试验)

ESPCN & EDSR 超分辨率模型文件百度网盘下载

完整项目中自带espcn_x4模型

3、本次项目所用到的5张RAW原图,1536x1184,16位灰度图

HFW_RAW五张原图百度网盘下载

图像处理效果如下:

raw原图:

1_avg_result:多帧平均后

2_display_8u:归一化8位

3_clahe:对比度增强

4_Sobel:边缘提取

5_morph_grad:形态学梯度增强

6_fused:多尺度融合

7_gamma_corrected:伽马亮度增强

8_ESPCN_output:超分辨率

二、代码详解

本次项目的UI界面:

解决方案:

opencv_raw.h

cpp 复制代码
#pragma once

#include <QtWidgets/QWidget>
#include "ui_opencv_raw.h"
#include <QFile>
#include <QFileDialog>
#include <QThread>
#include <QImage>
#include <QMessageBox>
#include <vector>
#include <iostream>

#include <opencv2/opencv.hpp>       // 引入opencv头文件
#include <opencv2/dnn_superres.hpp> // 引入超分辨率头文件
#include <opencv2/features2d.hpp>   // 引入特征检测头文件

using namespace std;                // 引入标准命名空间
using namespace cv;                 // 引入opencv命名空间
using namespace dnn_superres;       // 引入超分辨率命名空间

class raw_thread;                   // 向前声明

class opencv_raw : public QWidget
{
	Q_OBJECT

public:
	opencv_raw(QWidget* parent = nullptr);
	~opencv_raw();

	raw_thread* raw;    // 对象
	QThread* thread;    // 线程
	int width = 1536;   // 图像宽度
	int height = 1184;  // 图像高度
	QFile file;         // 打开raw文件
	Mat clone;          // 深拷贝图像

signals:
	void send_raw_5files(const vector<Mat>&);
public slots:
	void read_image(const Mat&);
private:
	Ui::opencv_rawClass ui;
};

class raw_thread : public QObject
{
	Q_OBJECT

public slots:
	void read_raw_5files(const vector<Mat>&);
	// Gamma校正
	Mat gammaCorrection(const Mat& src, double gamma);
	// ORB特征对齐
	cv::Mat alignWithORB(const cv::Mat& ref_8u, const cv::Mat& frame_8u, cv::Mat& H);
	// 多帧平均函数
	cv::Mat multiFrameAverage(const std::vector<cv::Mat>& aligned_frames);
signals:
	void send_image(const Mat&);
};

opencv_raw.cpp

cpp 复制代码
#include "opencv_raw.h"

opencv_raw::opencv_raw(QWidget* parent)
	: QWidget(parent)
{
	ui.setupUi(this);

	thread = new QThread(this);         // 创建线程
	raw = new raw_thread();             // 创建线程对象
	raw->moveToThread(thread);          // 将线程对象移动到线程中
	thread->start();                    // 启动线程

	connect(raw, &raw_thread::send_image, this, &opencv_raw::read_image);
	connect(this, &opencv_raw::send_raw_5files, raw, &raw_thread::read_raw_5files);

	// 多帧平均按钮
	connect(ui.pushButton_3, &QPushButton::clicked, [this]() {
		QString folderPath = QFileDialog::getExistingDirectory(this, "选择包含5张RAW的文件夹", QDir::currentPath());
		if (folderPath.isEmpty()) return;

		QDir dir(folderPath);
		QStringList filters;
		filters << "*.raw";
		QFileInfoList fileInfoList = dir.entryInfoList(filters, QDir::Files);

		if (fileInfoList.size() < 5) {
			QMessageBox::warning(this, "警告", "文件夹中RAW文件不足5个");
			return;
		}

		// 只取前5个文件
		vector<Mat> rawImages;
		for (int i = 0; i < 5; ++i) {
			QFile file(fileInfoList[i].filePath());
			if (!file.open(QIODevice::ReadOnly)) {
				qWarning() << "无法打开文件:" << fileInfoList[i].filePath();
				continue;
			}

			Mat img(height, width, CV_16UC1);
			qint64 bytesRead = file.read(reinterpret_cast<char*>(img.data), width * height * 2);
			if (bytesRead != width * height * 2) {
				qWarning() << "文件大小不匹配:" << fileInfoList[i].filePath();
				continue;
			}

			rawImages.push_back(img);
			file.close();
		}

		if (rawImages.size() == 5) {
			QMessageBox::information(this, "提示", "成功加载5张RAW文件,开始BM3D处理");
			emit send_raw_5files(rawImages);
		}
		else {
			QMessageBox::warning(this, "警告", "实际成功加载的RAW文件不足5个");
		}
		});

}

opencv_raw::~opencv_raw()
{
	thread->quit();
	thread->wait(); // 等待线程结束
}

void opencv_raw::read_image(const Mat& src)
{
	// 深拷贝
	clone = src.clone();

	// 转换为QImage
	QImage qImg(clone.data, clone.cols, clone.rows, clone.step, QImage::Format_Grayscale8);
	qImg.save("9_output.png");

	// 显示在label中
	QPixmap pixmap = QPixmap::fromImage(qImg);
	if (!pixmap.isNull()) {
		QSize labelSize = ui.label->size();
		QSize scaledSize = pixmap.size().scaled(labelSize, Qt::KeepAspectRatio);
		ui.label->setPixmap(pixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
		ui.label->setAlignment(Qt::AlignCenter);
	}
}

raw_thread.cpp

cpp 复制代码
#include "opencv_raw.h"

void raw_thread::read_raw_5files(const std::vector<cv::Mat>& raw_frames) {
	// 1. 多帧ORB对齐
	std::vector<cv::Mat> aligned_frames;
	aligned_frames.push_back(raw_frames[0]);
	for (size_t i = 1; i < raw_frames.size(); ++i) {
		cv::Mat ref_8u, frame_8u;
		cv::normalize(raw_frames[0], ref_8u, 0, 255, cv::NORM_MINMAX, CV_8UC1);
		cv::normalize(raw_frames[i], frame_8u, 0, 255, cv::NORM_MINMAX, CV_8UC1);

		cv::Mat aligned_8u, H;
		aligned_8u = alignWithORB(ref_8u, frame_8u, H);
		if (H.empty()) {
			qWarning() << "第" << i << "帧对齐失败,使用原图";
			aligned_frames.push_back(raw_frames[i].clone());
			continue;
		}

		cv::Mat aligned_16u;
		cv::warpPerspective(raw_frames[i], aligned_16u, H, raw_frames[0].size(),
			cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(0));
		aligned_frames.push_back(aligned_16u);
		qDebug() << "第" << i << "帧16位对齐完成";
	}

	// 2. 多帧平均去噪
	cv::Mat avg_result = multiFrameAverage(aligned_frames);
	cv::imwrite("1_avg_result.png", avg_result);

	// 3. 16位转8位进行图像处理
	cv::Mat display_8u;
	double min_val, max_val;
	cv::minMaxLoc(avg_result, &min_val, &max_val);
	if (max_val - min_val < 30000) {
		cv::normalize(avg_result, display_8u, 0, 255, cv::NORM_MINMAX, CV_8UC1);
	}
	else {
		avg_result.convertTo(display_8u, CV_8UC1, 255.0 / 65535.0);
	}
	cv::imwrite("2_display_8u.png", display_8u);

	// 4. CLAHE对比度增强
	cv::Mat clahe_img;
	cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(4, 4));
	clahe->apply(display_8u, clahe_img);
	cv::imwrite("3_clahe.png", clahe_img);

	// 5. Sobel边缘提取
	cv::Mat sobel_x, sobel_y, edge;
	cv::Sobel(clahe_img, sobel_x, CV_16S, 1, 0, 3);
	cv::Sobel(clahe_img, sobel_y, CV_16S, 0, 1, 3);
	cv::convertScaleAbs(sobel_x, sobel_x);
	cv::convertScaleAbs(sobel_y, sobel_y);
	cv::addWeighted(sobel_x, 0.5, sobel_y, 0.5, 0, edge);
	cv::imwrite("4_Sobel.png", edge);

	// 6. 形态学梯度增强
	cv::Mat morph_grad;
	cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
	cv::morphologyEx(clahe_img, morph_grad, cv::MORPH_GRADIENT, kernel);
	cv::imwrite("5_morph_grad.png", morph_grad);

	// 7. 多尺度融合
	cv::Mat fused;
	cv::addWeighted(clahe_img, 0.6, edge, 0.15, 0, fused);
	cv::addWeighted(fused, 1.0, morph_grad, 0.15, 0, fused);
	cv::imwrite("6_fused.png", fused);

	// 8. 伽马亮度增强
	double gamma = 0.6;
	cv::Mat gamma_corrected = gammaCorrection(fused, gamma);
	cv::imwrite("7_gamma_corrected.png", gamma_corrected);

	// 9. 转换为3通道(适配ESPCN输入)
	cv::Mat src3c;
	cv::cvtColor(gamma_corrected, src3c, cv::COLOR_GRAY2BGR);

	// 10. ESPCN超分
	cv::Mat sr_img;
	bool sr_success = true;
	std::string model_path = "ESPCN_x4.pb";  // 模型路径
	try {
		cv::dnn_superres::DnnSuperResImpl sr;
		sr.readModel(model_path);
		sr.setModel("espcn", 4);
		sr.upsample(src3c, sr_img);
		cv::cvtColor(sr_img, sr_img, cv::COLOR_BGR2GRAY); // 转回单通道
		qDebug() << "ESPCN超分后尺寸:" << sr_img.cols << "x" << sr_img.rows;

		if (!cv::imwrite("8_ESPCN_output.png", sr_img)) {
			qWarning() << "超分结果保存失败";
		}
	}
	catch (const cv::Exception& e) {
		qCritical() << "超分失败:" << e.what();
		sr_img = gamma_corrected.clone();
		sr_success = false;
	}

	if (!sr_success) {
		emit send_image(gamma_corrected);
		return;
	}

	// 发送最终结果在label中显示
	emit send_image(sr_img);
}

// ORB特征对齐(输入8位图像,返回对齐后的8位图像和单应性矩阵H)
cv::Mat raw_thread::alignWithORB(const cv::Mat& ref_8u, const cv::Mat& frame_8u, cv::Mat& H) {
	H = cv::Mat(); // 初始化H为空
	if (ref_8u.empty() || frame_8u.empty()) {
		qCritical() << "ORB对齐失败:输入图像为空";
		return frame_8u.clone();
	}

	// 初始化ORB检测器
	cv::Ptr<cv::ORB> orb = cv::ORB::create(8000, 1.2f, 8);

	// 提取特征点和描述符
	std::vector<cv::KeyPoint> ref_kp, frame_kp;
	cv::Mat ref_desc, frame_desc;
	orb->detectAndCompute(ref_8u, cv::noArray(), ref_kp, ref_desc);
	orb->detectAndCompute(frame_8u, cv::noArray(), frame_kp, frame_desc);

	qDebug() << "ORB特征点数量(参考帧:" << ref_kp.size() << ",当前帧:" << frame_kp.size() << ")";

	// 检查特征点数量
	if (ref_kp.size() < 10 || frame_kp.size() < 10) {
		qWarning() << "特征点不足,返回原图";
		return frame_8u.clone();
	}

	// 匹配描述符
	cv::BFMatcher matcher(cv::NORM_HAMMING);
	std::vector<cv::DMatch> matches;
	matcher.match(ref_desc, frame_desc, matches);

	// 筛选优质匹配
	if (matches.size() < 10) {
		qWarning() << "有效匹配点不足(" << matches.size() << "),返回原图";
		return frame_8u.clone();
	}
	std::sort(matches.begin(), matches.end(), [](const cv::DMatch& a, const cv::DMatch& b) {
		return a.distance < b.distance;
		});
	int keep = std::max(10, (int)(matches.size() * 0.2));
	matches.resize(keep);

	// 提取匹配点坐标
	std::vector<cv::Point2f> ref_pts, frame_pts;
	for (const auto& m : matches) {
		ref_pts.push_back(ref_kp[m.queryIdx].pt);
		frame_pts.push_back(frame_kp[m.trainIdx].pt);
	}

	// 计算单应性矩阵H
	H = cv::findHomography(frame_pts, ref_pts, cv::RANSAC, 5.0);
	if (H.empty()) {
		qWarning() << "单应性矩阵计算失败,返回原图";
		return frame_8u.clone();
	}

	// 对齐8位图像
	cv::Mat aligned_8u;
	cv::warpPerspective(
		frame_8u,
		aligned_8u,
		H,
		ref_8u.size(),
		cv::INTER_LINEAR,
		cv::BORDER_CONSTANT,
		cv::Scalar(0)
	);

	return aligned_8u;
}

// 多帧平均
cv::Mat raw_thread::multiFrameAverage(const std::vector<cv::Mat>& aligned_frames) {
	if (aligned_frames.empty()) {
		qCritical() << "多帧平均失败:输入帧为空";
		return cv::Mat();
	}

	// 校验帧尺寸和类型
	int rows = aligned_frames[0].rows;
	int cols = aligned_frames[0].cols;
	for (const auto& frame : aligned_frames) {
		if (frame.rows != rows || frame.cols != cols || frame.type() != CV_16UC1) {
			qCritical() << "多帧平均失败:帧尺寸或类型不匹配";
			return cv::Mat();
		}
	}

	// 32位整数累加避免溢出
	cv::Mat sum_mat(rows, cols, CV_32SC1, cv::Scalar(0));
	for (const auto& frame : aligned_frames) {
		cv::Mat frame_32s;
		frame.convertTo(frame_32s, CV_32SC1);
		sum_mat += frame_32s;
	}

	// 计算平均值并转回16位
	cv::Mat avg_mat;
	sum_mat.convertTo(avg_mat, CV_16UC1, 1.0 / aligned_frames.size(), 0.5); // 四舍五入

	return avg_mat;
}

// 伽马亮度增强
cv::Mat raw_thread::gammaCorrection(const cv::Mat& src, double gamma) {
	cv::Mat lut(1, 256, CV_8UC1);
	uchar* ptr = lut.ptr();
	for (int i = 0; i < 256; ++i) {
		ptr[i] = cv::saturate_cast<uchar>(255.0 * pow(i / 255.0, gamma));
	}
	cv::Mat dst;
	cv::LUT(src, lut, dst);
	return dst;
}

程序运行效果如下:

三、gitee完整项目下载

https://gitee.com/zjq11223344/opencv_rawhttps://gitee.com/zjq11223344/opencv_raw

相关推荐
Blossom.1183 小时前
用一颗MCU跑通7B大模型:RISC-V+SRAM极致量化实战
人工智能·python·单片机·嵌入式硬件·opencv·机器学习·risc-v
mit6.8243 小时前
[GazeTracking] 摄像头交互与显示 | OpenCV
人工智能·opencv·交互
Algebraaaaa4 小时前
Qt中的字符串宏 | 编译期检查和运行期检查 | Qt信号与槽connect写法
开发语言·c++·qt
友友马4 小时前
『 QT 』Hello World控件实现指南
开发语言·qt
誰能久伴不乏4 小时前
如何在 Linux_Ubuntu 上安装 Qt 5:详细教程
linux·qt·ubuntu
Predestination王瀞潞9 小时前
IO操作(Num22)
开发语言·c++
星期天要睡觉10 小时前
计算机视觉(opencv)——基于 OpenCV DNN 的实时人脸检测 + 年龄与性别识别
opencv·计算机视觉·dnn
宋恩淇要努力10 小时前
C++继承
开发语言·c++
feiyangqingyun12 小时前
有难度哦/Qt基于通用地图组件实现航迹规划和模拟/动态标注轨迹线/带序号和方向箭头指示
qt·航迹规划和模拟