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

OpenCV 常用算法学习

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

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

函数原型

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
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为源图像
  cv::cvtColor(img_mat_root, img_mat_gray, cv::COLOR_BGRA2GRAY);

高斯模糊

数学本质

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

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

函数原型

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方向上的标准差,标准差越大,边缘越模糊

使用示例

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

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

算法步骤

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

函数原型

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即默认值)

使用示例

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

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

函数原型

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,高于或等于时不变

使用示例

  • 通过一个按钮实现对图像的二进制二值化与反二值化
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)为输入像素值

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

-说明

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

代码示例

/* 亮度对比度调节 */
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

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的默认算法
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()
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)));
}

图像融合

算法原理

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

函数原型

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    // 输出数据类型(默认同输入)
)
  • 注意:两张图像融合前,需保证两张图像通道、位深均一致

使用示例

/* 融合 */
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)));
}

旋转与镜像

旋转函数原型

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

旋转模式

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

翻转函数模型

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

翻转模式

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

代码示例

/* 旋转 */
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)));
}

霍夫圆检测

函数原型

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个圆的情况(类似下图)
  • 这里对圆点次序做了排序,采用中心点叉积方式,详情可去源码中了解
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 @ 2025-03-06 17:27  风陵南  阅读(264)  评论(0)    收藏  举报