本人正在学习 OpenCV 计算机视觉相关知识,此篇为个人学习笔记,参考书籍为冯振等人的《OpenCV4 快速入门》,主要记录 OpenCV 在 Linux 环境下的编译、运行实操流程与相关知识点总结。

笔记内容均为亲自实操验证,过程中踩过不少编译配置、运行参数的坑,整理出来一方面用于自己复盘巩固,另一方面也希望能帮助到刚入门 OpenCV、在编译运行示例程序时遇到问题的小伙伴。文中内容如有疏漏或错误之处,欢迎各位大佬评论区指正交流,共同进步~

第八章 图像分析与修复

8.1 傅里叶变换

8.1.1 离散傅里叶变换 dft()函数

定义:傅里叶变换在时域和频域上都呈现离散的形式,将时域信号的采样变成离散时间傅里叶变换频域的采样。结果是复数,分成实数图像+虚数图像 或者 幅值图像+相位图像

图像中像素波动较大的是高频区域,体现细节和纹理信息。低频体现轮廓

void cv::dft(InputArray src,                // 输入数组(单通道/双通道,浮点型)
    OutputArray dst,               // 输出结果(尺寸与输入一致)
    int flags = 0,                // 变换标志位(核心参数)
    int nonzeroRows = 0           // 优化参数,默认0即可
);

标志位flags

数值

描述

DFT_COMPLEX_OUTPUT

16

强制输出复数格式(双通道),正向变换常用

DFT_REAL_OUTPUT

32

输入为实数、输出为实数,仅用于逆变换

DFT_INVERSE

1

执行逆傅里叶变换(频域→时域),不设置则默认正向变换

DFT_SCALE

2

逆变换时归一化缩放(匹配原始图像幅值,必加)

DFT_ROWS

4

每一行进行正或逆变换,可以处理三维或更高的离散变换,减少资源利用

DFT_COMPLEX_INPUT

64

输入矩阵必须具有两个通道,函数默认输入数据是复数

注意:执行情况见书p255

  • 离散傅里叶逆变换 idft()函数 (原型同上)
  • 计算最优的输入矩阵尺寸 getOptimalDFTSize()函数

最优尺寸是2、3、5的公倍数

int cv::getOptimalDFTSize(int vecsize)  //vecsize表示矩阵的最佳行数或列数
  • 不对图像进行缩放,在图像周围形成外框 copyMakeBorder()函数
void cv::copyMakeBorder(
    InputArray src,                 // 输入:原始图像/矩阵
    OutputArray dst,                // 输出:添加边框后的目标矩阵,数据类型与src完全一致
    int top,                        // 输入:图像**顶部**填充的边框像素个数,非负整数
    int bottom,                     // 输入:图像**底部**填充的边框像素个数,非负整数
    int left,                       // 输入:图像**左侧**填充的边框像素个数,非负整数
    int right,                      // 输入:图像**右侧**填充的边框像素个数,非负整数
    int borderType,                 // 输入:边框填充模式枚举值,决定边框像素的生成规则
    const Scalar& value = Scalar()  // 输入:可选参数,仅borderType=BORDER_CONSTANT时生效,指定填充的固定像素值
);
  • 计算由两个矩阵组成的二维向量矩阵的幅值矩阵 magnitude()函数
void cv::magnitude(x,y,输出的幅值矩阵)

示例代码:

代码分为两大块:矩阵 DFT 正逆变换验证 + 图像 DFT 变换与频谱可视化。

8.1.2 傅里叶变换进行卷积

可以将两个矩阵的卷积转换成两个矩阵的乘积,提高卷积速度

  • 计算两个复数矩阵的乘积 mulSpectrums()函数
void cv::mulSpectrums(
    InputArray a,                // 输入第一个复数频谱矩阵(双通道CV_32F/CV_64F)
    InputArray b,                // 输入第二个复数频谱矩阵(尺寸、类型必须与a完全一致)
    OutputArray c,               // 输出乘法结果矩阵(尺寸、类型与输入一致)
    int flags,                   // 运算标志位,核心控制共轭运算
    bool conjB = false         // 可选参数,是否对矩阵b先取共轭再相乘(false不进行共轭)
);

8.1.3 离散余弦变换 dct()函数

类似离散傅里叶变换,但只使用实数,用于对信号和图像有损数据压缩中

只支持偶数大小的数组,最佳尺寸通过2*getOptimalDFTSize((N+1)/2)

void cv::dct(输入矩阵(必须是浮点数),输出矩阵,
        转换方法的标志(DCT_INVERSE对一维或二维进行逆变换,,,DCT_ROWS转换多个向量))
  • 离散余弦变换的逆变换idft()函数 (原型同上)

8.2 积分图像

快速计算图像某些区域像素的平均灰度

积分图像比原图像大一圈的图像,原图像是n*n,则积分图像是(n+1)*(n+1)

积分图像像素值=该像素点与原点组成矩形区域内所有像素值的和

分类:标准求和积分图像、平方和积分图像、倾斜求和积分图像

  • 标准求和 integral()函数
void cv::integral(输入图像,输出图像,输出图像数据类型(默认-1))
  • 平方求和 integral()函数
void cv::integral(输入图像,输出标准求和图像,输出平方求和图像,
                输出标准求和数据类型(默认-1),输出平方求和图像标志(默认-1))
  • 倾斜求和 integral()函数
void cv::integral(输入图像,输出标准求和图像,输出平方求和图像,输出倾斜求和图像,
                输出标准求和+倾斜求和数据类型(默认-1),输出平方求和图像标志(默认-1))

8.3 图像分割

将图像中属于某一类的像素点与其他像素点分开

8.3.1 漫水填充法 floodFill()函数

根据像素灰度值之间的差值寻找相同区域以实现分割

种子点按照一定规则向外扩散,形成具有相似特征的独立区域,进而实现图像分割

步骤:1. 选择种子点 2.以种子点为中心,判断4-邻域或8-邻域的像素值与种子点像素值的差值,将差值小于阈值的像素点添加进区域内 3.将新加入的像素点作为新的种子点,反复执行

int floodFill(
    InputOutputArray image,   // 输入/输出图像:支持CV_8U/CV_32F,单通道灰度/三通道彩色
    InputOutputArray mask,         // (可选)掩码图像:单通道CV_8U,尺寸比原图大2像素
    Point seedPoint,               // 种子点:填充起始坐标 (x,y),x=列,y=行
    Scalar newVal,                 // 填充的目标颜色/灰度值,彩色图为Scalar(B,G,R)
    Rect* rect = nullptr,          // 可选输出:返回填充区域的最小外接矩形
    Scalar loDiff = Scalar(),      // 向下差值阈值:允许低于种子点的最大差值
    Scalar upDiff = Scalar(),      // 向上差值阈值:允许高于种子点的最大差值
    int flags = 4                  // 填充模式:4邻域/8邻域,默认4邻域
);

8.3.2 分水岭法 watershed()函数

从全局出发,对全局进行分割

过程:1. 排序过程,对图像灰度级进行排序,确定灰度值较小的像素点 2.淹没过程,对每个最低点开始注水,不断淹没周围像素点,不同的注水出汇集在一起,形成分割线

void cv::watershed(输入图像,输出的标记结果)

8.3.3 Grabcut法

使用高斯混合模型估计目标区域的背景和前景,并且会产生众多较小的区域

通过迭代的方法解决能量函数最小化的问题,使更具有可靠性

void cv::grabCut(
    InputArray img,                  // 输入图像,必须是8位三通道彩色图(BGR)
    InputOutputArray mask,           // 掩码矩阵(CV_8U单通道),标记像素类型
    Rect rect,                       // 包含前景的矩形框(交互式框选区域)
    InputOutputArray bgdModel,       // 算法内部使用的背景模型(无需手动修改)
    InputOutputArray fgdModel,       // 算法内部使用的前景模型(无需手动修改)
    int iterCount,                   // 迭代次数,推荐5~10次,次数越多精度越高
    int mode = GC_EVAL               // 操作模式(分割模式标志)
);

宏定义MASK

数值

含义

GC_BGD

0

确定背景(用户手动标记的绝对背景)

GC_FGD

1

确定前景(用户手动标记的绝对前景)

GC_PR_BGD

2

可能背景(算法自动判定)

GC_PR_FGD

3

可能前景(算法自动判定)

修改为摄像头读取图像示例:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    VideoCapture cap(0);
    // 判断摄像头是否成功打开
    if (!cap.isOpened())
    {
        cout << "摄像头打开失败!" << endl;
        system("pause");
        return -1;
    }

    // 2. 读取一帧摄像头图像(GrabCut适合单帧处理,这里取首帧做分割演示)
    Mat img;
    cap >> img; 
    if (img.empty())
    {
        cout << "未获取到摄像头帧!" << endl;
        cap.release();
        return -1;
    }

    //绘制矩形
    Mat imgRect;
    img.copyTo(imgRect);  //备份图像,方式绘制矩形框对结果产生影响
    Rect rect(80, 30, 340, 390);
    rectangle(imgRect, rect, Scalar(0, 0, 255),2);
    imshow("选择的矩形区域", imgRect);

    //进行分割
    Mat bgdmod = Mat::zeros(1, 65, CV_64FC1);
    Mat fgdmod = Mat::zeros(1, 65, CV_64FC1);
    Mat mask = Mat::zeros(img.size(), CV_8UC1);
    grabCut(img, mask, rect, bgdmod, fgdmod, 5, GC_INIT_WITH_RECT);
    
    //将分割出的前景绘制回来
    Mat result;
    for (int row = 0; row < mask.rows; row++) 
    {
        for (int col = 0; col < mask.cols; col++) 
        {
            int n = mask.at<uchar>(row, col);
            //将明显是前景和可能是前景的区域都保留
            if (n == 1 || n == 3) 
            {
                mask.at<uchar>(row, col) = 255;
            }
            //将明显是背景和可能是背景的区域都删除
            else 
            {
                mask.at<uchar>(row, col) = 0;
            }
        }
    }
    bitwise_and(img, img, result, mask);
    imshow("分割结果", result);
    waitKey(0);
     // 8. 释放资源
    cap.release();
    destroyAllWindows();
    return 0;
}

8.3.4 Mean-Shift法 pyrMeanShiftFiltering()函数

均值漂移法,一种基于颜色空间分布的图像分割法,经过滤色的分色图像,颜色会渐变,细纹纹理会平缓

每个像素点用五维向量(x,y,b,g,r)表示,滑动窗口由半径和颜色幅度构成,半径决定了滑动窗口的范围,颜色幅度决定了半径内像素点分类的标准。

void pyrMeanShiftFiltering(
    InputArray  src,            // 输入图像:8位三通道彩色图(BGR格式,灰度图需转三通道)
    OutputArray dst,           // 输出图像:尺寸、类型与原图一致
    double      sp,             // 滑动窗口半径(Spatial Window)
    double      sr,             // 色彩窗口幅度(Color Window)
    int         maxLevel = 1,   // 金字塔最大层数,默认1层
    TermCriteria termcrit = TermCriteria(      //算法迭代停止的条件
        TermCriteria::MAX_ITER + TermCriteria::EPS, 
        5,                      // 最大迭代次数
        1)                       // 精度阈值
);
  • 算法迭代停止的条件 TermCriteria()结构体
cv::TermCriteria(
    int type,        // 终止条件类型,支持组合取值
    int maxCount,    // 最大迭代次数或者元素数
    double epsilon   // 精度阈值(变化量小于该值则停止)
);

枚举常量 type

数值

含义

TermCriteria::MAX_ITER

1

以最大迭代次数作为终止条件

TermCriteria::EPS

2

以精度收敛阈值作为终止条件

TermCriteria::MAX_ITER + TermCriteria::EPS

3

组合条件,满足任一即停止(最常用)

8.4 图像修复

对水印或污染的图像进行修复 inpaint()函数

void inpaint(
    InputArray  src,        // 输入图像:8位单通道灰度图 / 8位三通道彩色图
InputArray inpaintMask,// 修复掩码:单通道8位图,非0像素表示需要修复的区域,0像素为正常区域
    OutputArray dst,        // 输出修复后的图像
    double      inpaintRadius, // 修复邻域半径:算法参考的周边像素范围
    int    flags       // 修复算法选择:INPAINT_NS基于Navier-Stokes算法修复图像(0);
//INPAINT_TELEA基于Alexandru Telea算法图像
);

8.5 总结

至此,第八章就结束了,望各位读者“一键三连”,再次感谢读到这里的各位读者。有任何问题欢迎评论区留言或者私信。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐