OpenCV常用算法 —— 基于Qt C++开发图片工具

风陵南·2025-03-06 17:27·12 次阅读

OpenCV常用算法 —— 基于Qt C++开发图片工具

OpenCV 常用算法学习

使用Qt界面开发结合OpenCV进行学习,学习过程中也逐渐完善了一个图片处理工具
工具中还有很多这里没有介绍的内容,包括图像在Qt中的拖拽放大以及很多细节处理等等,这些无关OpenCV,就不放这里了
源码:GitHub-Qt-imgToolForRaw
界面:

图像格式转换(颜色空间转换)#

函数原型#

Copy
void cv::cvtColor(
InputArray src, // 输入图像(CV_8U, CV_16U或CV_32F类型)
OutputArray dst, // 输出图像
int code, // 颜色空间转换代码
int dstCn = 0 // 输出通道数(0表示自动推断)
);

常用的颜色空间转换类型#

转换类型 代码常量 应用场景
BGR ↔ 灰度 COLOR_BGR2GRAY 图像简化处理
BGR ↔ RGB COLOR_BGR2GRAY Qt显示cv处理的图像
BGR ↔ HSV COLOR_BGR2HSV 颜色追踪、阈值分割
BGR ↔ Lab COLOR_BGR2Lab 色彩一致性检查
BGR ↔ YUV COLOR_BGR2YUV_I420 视频编码/解码
灰度 ↔ BGR COLOR_GRAY2BGR 单通道转三通道显示

代码应用#

在Qt中QImage显示只支持RGB格式,而OpenCV默认读取的图片是BGR格式,OpenCV处理的图片通常需要通过格式转换成RGB后才能通过Qt显示出来。

  • 通过以下函数即实现了将cv::Mat类型,转为了可以使用Qt显示的QImage
Copy
QImage Widget::cvMatToQImage(const cv::Mat &mat){
if (mat.empty()) {
qDebug() << "error : mat is empty!";
return QImage();
}
// 处理灰度图(8UC1)
if(mat.type() == CV_8UC1){
return QImage(
mat.data, // 数据指针
mat.cols, // 宽度
mat.rows, // 高度
mat.step, // 每行字节数
QImage::Format_Grayscale8 // 8位灰度格式
).copy(); // 深拷贝避免悬空指针
}
// 处理BGR彩色图
else if (mat.type() == CV_8UC3) {
// 将BGR转换为RGB
cv::Mat rgbMat;
cv::cvtColor(mat, rgbMat, cv::COLOR_BGR2RGB);
return QImage(
rgbMat.data, // 数据指针
rgbMat.cols, // 宽度
rgbMat.rows, // 高度
rgbMat.step, // 每行字节数
QImage::Format_RGB888 // RGB888格式
).copy(); // 深拷贝
}
// 不支持的类型返回空图像
else {
qWarning("Unsupported Mat type: must be CV_8UC1 or CV_8UC3");
return QImage();
}
}
  • 通过cv::COLOR_BGRA2GRAY实现将图片转为灰度图功能
    • img_mat_root为源图像
Copy
cv::cvtColor(img_mat_root, img_mat_gray, cv::COLOR_BGRA2GRAY);

高斯模糊#

数学本质#

采用二维高斯函数生成卷积核:

Copy
G(x,y) = (1/(2πσ²)) * e^(-(x²+y²)/(2σ²))

函数原型#

Copy
void cv::GaussianBlur(
InputArray src, // 输入图像
OutputArray dst, // 输出图像(与输入图像具有相同的尺寸和类型)
Size ksize, // 高斯内核大小(宽和高都必须为正奇数)
double sigmaX, // X方向上的标准差(如果设置为0,则根据ksize计算)
double sigmaY = 0 // Y方向上的标准差(如果设置为0,则等于sigmaX)
);
  • 关键参数:
    • 核尺寸(ksize):高斯内核的大小,边越长,模糊范围越大,通常指定宽高都是奇数的Size对象,如Size(5, 5)表示一个5x5的内核。
    • 标准差sigmaXsigmaY分别表示X和Y方向上的标准差,标准差越大,边缘越模糊

使用示例#

Copy
// 降噪(高斯模糊)
cv::GaussianBlur(img_mat_root, img_mat_gaussian, cv::Size(5, 5), 2, 2);

Canny边缘检测(输入为单通道灰度图像)#

算法步骤#

  1. 高斯滤波降噪
  2. 计算梯度幅值和方向
  3. 非极大值抑制
  4. 双阈值边缘连接

函数原型#

Copy
void cv::Canny(
InputArray image, // 输入图像(8位单通道)
OutputArray edges, // 输出边缘图(8位单通道二值图像)
double threshold1, // 低阈值
double threshold2, // 高阈值
int apertureSize = 3, // Sobel算子孔径大小(3, 5, 7)
bool L2gradient = false // 梯度计算方式(true使用L2范数,false用L1)
);
  • 关键参数
    • threshold1:低阈值,低于此值的边缘将被丢弃
    • threshold2:高阈值,高于此值的边缘将被保留为强边缘
    • apertureSize:Sobel算子的窗口尺寸(推荐值为3即默认值)

使用示例#

Copy
cv::Canny(img_mat_gaussian, img_mat_canny, 100, 200); // 使用相同阈值
  • 注意这里使用了img_mat_gaussian,是已经高斯处理过后的图像,此时在Canny函数内部则会跳过高斯滤波部分

图像二值化(输入为单通道灰度图像8位或32位浮点)#

函数原型#

Copy
double cv::threshold(
InputArray src, // 输入图像(单通道,8位或32位浮点)
OutputArray dst, // 输出图像(与src同尺寸和类型)
double thresh, // 阈值
double maxval, // 最大值(用于二进制/反二进制模式)
int type // 阈值类型(见下方详解)
);

阈值类型(输入为单通道灰度图像8位或32位浮点)#

类型标志 类型 说明
THRESH_BINARY 二进制阈值 像素值低于参数thresh的会被置为0,高于的置为参数中的maxval最大值
THRESH_BINARY_INV 反二进制阈值 与二进制阈值相反
THRESH_TRUNC 截断阈值 像素值高于参数阈值时直接被置为阈值的值,低于或等于则保持不变
THRESH_TOZERO 零阈值 像素值低于参数阈值时被置为0,高于或等于时不变

使用示例#

  • 通过一个按钮实现对图像的二进制二值化与反二值化
Copy
void Widget::on_pushButton_threshold_clicked()
{
threshType = (threshType + 1) % 2;
cv::Mat thresh_Mat;
int t_type = 0;
if(threshType){
// 二进制二值化
t_type = cv::THRESH_BINARY;
ui->pushButton_threshold->setText("反二值化");
}
else{
// 二进制反二值化
t_type = cv::THRESH_BINARY_INV;
ui->pushButton_threshold->setText("二值化");
}
// cv::threshold(输入图像, 输出图像, 阈值, 最大值, 阈值类型)
// 输入图像必须单通道,最大值通常设置为255
cv::threshold(img_mat_gray, thresh_Mat, 100, 255, t_type);
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(thresh_Mat)));
}

亮度对比度调节#

算法原理#

使用线性变换公式
g(x)为输出像素值,f(x)为输入像素值

Copy
g(x) = α·f(x) + β

-说明

  • α>1增强对比度,0<α<1降低对比度
  • β>0增加亮度,β<0降低亮度

代码示例#

Copy
/* 亮度对比度调节 */
void Widget::changeGain(double contrast, int brightness)
{
cv::Mat changedGainMat;
// 计算公式是 对比度 * 像素值 + 亮度
img_mat_root.convertTo(changedGainMat, -1, contrast, brightness);
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(changedGainMat)));
}

图像缩放#

插值方法对比#

算法类型 计算复杂度 使用场景
最近邻插值 O(1) 实时视频处理
双线性插值 O(4) 通用图像缩放
图像金字塔 多级处理(高内存消耗) 多尺度特征提取

最近邻插值#

函数:cv::resize()
参数:cv::INTER_NEAREST

Copy
void Widget::on_pushButton_nearest_clicked()
{
// 缩放倍数
double scale = ui->doubleSpinBox_resize->value();
cv::Mat scaleMat;
cv::Size scaleSize(width * scale, height * scale);
// cv::resize -- cv::INTER_NEAREST近邻算法填充像素值(附近的像素按原本位置的像素填充)
cv::resize(img_mat_root, scaleMat, scaleSize, 0, 0, cv::INTER_NEAREST);
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(scaleMat)));
ui->label_scale_width->setText(QString::number(scaleSize.width));
ui->label_scale_height->setText(QString::number(scaleSize.height));
}

双线性插值#

  • 由原图像位置在它附近的2*2区域4个临近像素的值通过加权平均计算得出
  • cv::resize的默认算法
Copy
void Widget::on_pushButton_linear_clicked()
{
// 缩放倍数
double scale = ui->doubleSpinBox_resize->value();
cv::Mat scaleMat;
cv::Size scaleSize(width * scale, height * scale);
// cv::resize -- 双线性内插值-cv::INTER_LINEAR -- 是resize默认算法 resize(mat, outputMat, scaleSize);
cv::resize(img_mat_root, scaleMat, scaleSize, 0, 0, cv::INTER_LINEAR);
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(scaleMat)));
ui->label_scale_width->setText(QString::number(scaleSize.width));
ui->label_scale_height->setText(QString::number(scaleSize.height));
}

图像金字塔#

  • 高斯金字塔(向下采样,图像缩小)
    • 执行一次长宽各缩小二分之一
    • 函数:cv::pyrDown(src, dst)
  • 拉普拉斯金字塔(向上采样,图像放大)
    • 执行一次长宽各放大2倍
    • 函数:cv::pyrUp()
Copy
void Widget::on_pushButton_pyramid_clicked()
{
int pyramidType = ui->comboBox_pyramid_type->currentIndex();
double scale = ui->doubleSpinBox_resize->value();
cv::Mat pyMat = img_mat_root.clone();
if(pyramidType == 0){
// 高斯金字塔缩小 缩小倍数必须是2的n次方分之1,即0.5,0,25等
if(scale != 0.5 && scale != 0.25){
QMessageBox::warning(nullptr, "高斯金字塔缩小", "缩小倍数必须是2的n次方分之1,即0.5,0,25等");
return;
}
ui->label_scale_width->setText(QString::number(width * scale));
ui->label_scale_height->setText(QString::number(height * scale));
while(scale <= 0.5){
// 高斯金字塔 执行一次缩小两倍
cv::pyrDown(pyMat, pyMat);
scale *= 2;
}
}else {
int py_scale = static_cast<int>(scale);
// 拉普拉斯金字塔放大 放大倍数必须是2的整数倍
if(py_scale % 2 != 0){
QMessageBox::warning(nullptr, "拉普拉斯金字塔放大", "放大倍数必须是2的整数倍");
return;
}
ui->doubleSpinBox_resize->setValue(py_scale);
ui->label_scale_width->setText(QString::number(width * py_scale));
ui->label_scale_height->setText(QString::number(height * py_scale));
while(py_scale > 1){
// 拉普拉斯金字塔 执行一次放大两倍
cv::pyrUp(pyMat, pyMat);
py_scale /= 2;
}
}
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(pyMat)));
}

图像融合#

算法原理#

  • 加权融合公式
Copy
dst = α·src1 + β·src2 + γ
  • 保证α+β≤1防止过曝,通常取β=1-α

函数原型#

Copy
void cv::addWeighted(
InputArray src1, // 输入图像1
double alpha, // 图像1权重 (0.0-1.0)
InputArray src2, // 输入图像2
double beta, // 图像2权重 (0.0-1.0)
double gamma, // 亮度调节量
OutputArray dst, // 输出图像
int dtype = -1 // 输出数据类型(默认同输入)
)
  • 注意:两张图像融合前,需保证两张图像通道、位深均一致

使用示例#

Copy
/* 融合 */
void Widget::on_pushButton_blend_clicked()
{
double alpha = ui->doubleSpinBox_blend_alpha->value();
double gain = ui->spinBox_blend_gain->value();
cv::Mat blendMat = qImageToCVMat(blendImg);
// 大小改为一样再融合
cv::resize(blendMat, blendMat, img_mat_root.size());
// 类型统一处理
if (blendMat.type() != img_mat_root.type()) {
// 处理通道差异
if (blendMat.channels() != img_mat_root.channels()) {
if (blendMat.channels() == 1) {
cv::cvtColor(blendMat, blendMat, cv::COLOR_GRAY2BGR); // 单通道转三通道
} else {
cv::cvtColor(blendMat, blendMat, cv::COLOR_BGR2GRAY); // 多通道转单通道示例
}
}
// 处理位深差异
if (blendMat.depth() != img_mat_root.depth()) {
if (blendMat.depth() == CV_32F) { // 浮点转8位
blendMat.convertTo(blendMat, CV_8UC3, 255.0); // 假设原数据范围0~1
} else if (blendMat.depth() == CV_16U) { // 16位转8位
blendMat.convertTo(blendMat, CV_8UC3, 1.0/256.0);
}
}
}
cv::Mat res_mat;
// 保证类型一致再进行融合
cv::addWeighted(blendMat, alpha, img_mat_root, 1 - alpha, gain, res_mat);
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(res_mat)));
}

旋转与镜像#

旋转函数原型#

Copy
void cv::rotate(
InputArray src, // 输入图像
OutputArray dst, // 输出图像
int rotateCode // 旋转模式
)

旋转模式#

枚举值 旋转角度
ROTATE_90_CLOCKWISE 顺时针90度
ROTATE_180 180度
ROTATE_90_COUNTERCLOCKWISE 逆时针90度

翻转函数模型#

Copy
void cv::flip(
InputArray src, // 输入图像
OutputArray dst, // 输出图像
int flipCode // 翻转模式控制码
)

翻转模式#

参数值 翻转方向 效果说明
1 水平翻转(X轴对称) 镜像翻转,左右互换
0 垂直翻转(Y轴对称) 上下颠倒
-1 双向翻转 同时水平和垂直翻转

代码示例#

Copy
/* 旋转 */
void Widget::on_pushButton_rotate_clicked()
{
int flag = ui->comboBox_rotate->currentIndex();
int type = 0;
switch(flag){
case 0:
type = cv::ROTATE_90_CLOCKWISE; // 顺指针90
break;
case 1:
type = cv::ROTATE_90_COUNTERCLOCKWISE; // 逆时针90
break;
case 2:
type = cv::ROTATE_180; // 旋转180
break;
default:
break;
}
cv::rotate(img_mat_root, img_mat_root, type);
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(img_mat_root)));
}
/* 镜像 */
void Widget::on_pushButton_flip_clicked()
{
int flag = ui->comboBox_flip->currentIndex();
int type = 0;
switch(flag){
case 0:
type = 1; // 水平翻转
break;
case 1:
type = 0; // 垂直翻转
break;
case 2:
type = -1; // 同时翻转
break;
default:
break;
}
cv::flip(img_mat_root, img_mat_root, type);
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(img_mat_root)));
}

霍夫圆检测#

函数原型#

Copy
void cv::HoughCircles(
InputArray image, // 输入图像(必须为8位单通道)
OutputArray circles, // 输出圆向量(x,y,radius)
int method, // 检测方法(目前仅支持HOUGH_GRADIENT)
double dp, // 累加器分辨率倒数(1=与输入同分辨率)
double minDist, // 圆之间的最小中心距离
double param1 = 100, // Canny边缘检测高阈值/累加器阈值
double param2 = 100, // 圆心检测阈值
int minRadius = 0, // 最小圆半径
int maxRadius = 0 // 最大圆半径
)

参数详解#

参数 典型值范围 作用说明
dp 1.0-2.0 值越小检测越精细,但会增加计算量
minDist 图像宽度的1/10 防止相邻圆重复检测
param1 50-200 Canny边缘检测的高阈值(低阈值为高阈值的一半)
param2 20-100 值越小检测到的假圆越多,值越大检测越严格
minRadius 0-50 过滤过小的圆
maxRadius 100-图像宽度1/2 限制最大检测半径

代码示例#

  • 这里只处理了对图像上有4个圆的情况(类似下图)
  • 这里对圆点次序做了排序,采用中心点叉积方式,详情可去源码中了解
Copy
void Widget::on_pushButton_circle_clicked()
{
// 只需要执行一次
if(circles.empty()){
// 绘制圆形的mat
img_mat_circle = img_mat_root.clone();
// 高斯处理灰度图
cv::Mat gaussion_gray;
cv::GaussianBlur(img_mat_gray, gaussion_gray, cv::Size(5, 5), 2, 2);
// 霍夫圆检测
cv::HoughCircles(
gaussion_gray, // 输入灰度图像
circles, // 输出结果(x, y, radius)
cv::HOUGH_GRADIENT, // 检测方法(目前仅支持梯度法)
1, // 累加器分辨率(与图像尺寸的倒数,通常为1)
gaussion_gray.rows/64, // 圆之间的最小距离(避免重复检测)
200, // Canny边缘检测的高阈值
100, // 累加器阈值(越小检测越多假圆)
0, // 最小圆半径(0表示不限制)
0); // 最大圆半径(0表示不限制)
if(circles.empty()){
QMessageBox::warning(nullptr, "不存在圆点", "未找到圆点,请检查图片");
img_mat_circle = cv::Mat();
return;
}
// 更新圆点顺序(逆时针排序)
sortCirclePoint();
// 设置圆点信息
setCircleInfo();
std::vector<cv::Point> centers;
// 绘制检测结果
for (size_t i = 0; i < circles.size(); i++) {
qDebug() << circles[i][0] << "," << circles[i][1] << "半径:" << circles[i][2];
cv::Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
centers.push_back(center);
int radius = cvRound(circles[i][2]);
// 绘制实心圆(红色填充)
cv::circle(
img_mat_circle, // 目标图像
center, // 圆心坐标
radius, // 半径
cv::Scalar(0, 0, 255), // 颜色(BGR格式,红色)
-1 // 线宽:-1 表示填充
);
// 绘制圆心
cv::circle(img_mat_circle, center, 3, cv::Scalar(0, 255, 0), -1);
cv::Point text_pos(center.x + 60, center.y);
// 绘制圆序号
cv::putText(
img_mat_circle,
QString::number(i + 1).toStdString(),
text_pos,
cv::FONT_HERSHEY_SIMPLEX,
1,
cv::Scalar(0, 0, 255), // 红色
5
);
}
double dis13;
double dis24;
cvLine(centers[0], centers[2], dis13);
cvLine(centers[1], centers[3], dis24);
ui->lineEdit_circle13_dis->setText(QString::number(dis13));
ui->lineEdit_circle24_dis->setText(QString::number(dis24));
}
// 显示绘制图
ui->label->setPixmap(QPixmap::fromImage(cvMatToQImage(img_mat_circle)));
}
posted @   风陵南  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示
目录