14.图像增强与图像去噪,图像分割之边缘检测(傅里叶变换)--- OpenCV从零开始到图像(人脸 + 物体)识别系列

14.图像增强与图像去噪,图像分割之边缘检测(傅里叶变换)— OpenCV从零开始到图像(人脸 + 物体)识别系列


本文作者:小嗷

微信公众号:aoxiaoji


1.前言

1.前沿内容

2. 简介及用途

2.1 傅里叶变换简单说:

二维图片等于无数个正弦或者余弦组合而成 -> 而每个正弦或余弦函数,都有一个振幅值。我们单独抽出来搞成一副振幅频图(相位频图也一样),这就是傅里叶变换的表达。

2.2 图像傅里叶变换dft

在第4篇文章里面可能对傅里叶变换有了一定的了解。我们知道傅里叶变换是把一个信号从时域变换到其对应的频域进行分析。如果有小伙伴还对傅里叶变换处于很迷糊的状态,请添加微信公众号aoxiaoji,第13篇文章,非常通俗易懂。

而在图像处理中也有傅里叶分析的概念,官方指导文件opencv_tutorials中给出的解释:

傅里叶变换可以将一幅图片分解为正弦和余弦两个分量,换而言之,他可以将一幅图像从其空间域(spatial domain)转换为频域(frequency domain)。这种变换的思想是任何函数可以很精确的接近无穷个sin()函数和cos()函数的和。傅里叶变换提供了这种方法来达到这种效果。对于二维图像其傅里叶变换公式如下:

2.傅里叶变换公式

一维图如下:

2.2 傅里叶一维图

大家在上章理解到每张图都可以通过时域转换成频域,而我们的目标就是去除或者说过滤掉频域上(图片上),不需要正弦或余弦波(就是图片中影响我们识别【噪音】。当然也可以故意添加【噪音】,这涉及密码学及模拟测试环境,有机会一起讨论)。

2.3傅里叶波形图

式中f(i, j)是图像空间域的值而F是频域的值。傅里叶转换的结果是复数,这也显示出了傅里叶变换是一副实数图像(real image)和虚数图像(complex image)叠加或者是幅度图像(magitude image)和相位图像(phase image)叠加的结果。在实际的图像处理算法中仅有幅度图像(magnitude image)图像能够用到,因为幅度图像包含了我们所需要的所有图像几何结构的信息。但是,如果想通过修改幅度图像或者相位图像来间接修改原空间图像,需要保留幅度图像和相位图像来进行傅里叶逆变换,从而得到修改后图像。

注意:通过修改幅度图像或者相位图像来间接修改原空间图像。

幅度图像(改过) + 相位图像 = 修改后图像(通过傅里叶逆变换)

图像 = 幅度图像 + 相位图像(傅里叶变换)

2.3 傅里叶变换函数dft()

用途:执行一维或二维浮点数组的正向或逆离散傅里叶变换。

C++: void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0);

参数解释:

  1. InputArray src - 输入图像,可以是实数或虚数
  2. OutputArray dst - 输出图像,其大小和类型取决于第三个参数flags
  3. int flags = 0 - 转换的标识符,有默认值0.其可取的值如下所示:

(1) DFT_INVERSE: 用一维或二维逆变换取代默认的正向变换

(2) DFT_SCALE: 缩放比例标识符,根据数据元素个数平均求出其缩放结果,如有N个元素,则输出结果以1/N缩放输出,常与DFT_INVERSE搭配使用。

(3) DFT_ROWS: 对输入矩阵的每行进行正向或反向的傅里叶变换;此标识符可在处理多种适量的的时候用于减小资源的开销,这些处理常常是三维或高维变换等复杂操作。

(4) DFT_COMPLEX_OUTPUT: 对一维或二维的实数数组进行正向变换,这样的结果虽然是复数阵列,但拥有复数的共轭对称性(CCS),可以以一个和原数组尺寸大小相同的实数数组进行填充,这是最快的选择也是函数默认的方法。你可能想要得到一个全尺寸的复数数组(像简单光谱分析等等),通过设置标志位可以使函数生成一个全尺寸的复数输出数组。

(5) DFT_REAL_OUTPUT: 对一维二维复数数组进行逆向变换,这样的结果通常是一个尺寸相同的复数矩阵,但是如果输入矩阵有复数的共轭对称性(比如是一个带有DFT_COMPLEX_OUTPUT标识符的正变换结果),便会输出实数矩阵。

4.int nonzeroRows = 0 - 当这个参数不为0,函数会假设只有输入数组(没有设置DFT_INVERSE)的第一行或第一个输出数组(设置了DFT_INVERSE)包含非零值。这样的话函数就可以对其他的行进行更高效的处理节省一些时间,这项技术尤其是在采用DFT计算矩阵卷积时非常有效。

其实,具体详述参照opencv2refman.pdf如下:
(在第133页)

3.dft的英文解析

(在第三、四篇有PDF链接,也可以关注公众号输入opencv2refman弹出百度分享)

2.4 getOptimalDFTSize()函数

用途:返回给定向量尺寸经过DFT变换后结果的最优尺寸大小。

C++: int getOptimalDFTSize(int vecsize);

参数解释:

int vecsize - 输入向量尺寸大小(vector size) ,即图片的rows、cols

(就是矩阵数组的列和行。如不懂图片为啥称为矩阵数组,是啥?建议从头到尾看一次小嗷文章吧。这个是基础,Are you OK?)

DFT变换在一个向量尺寸上不是一个单调函数,当计算两个数组卷积或对一个数组进行光学分析,它常常会用0扩充一些数组来得到稍微大点的数组以达到比原来数组计算更快的目的。一个尺寸是2阶指数(2,4,8,16,32…)的数组计算速度最快,一个数组尺寸是2、3、5的倍数(例如:300 = 5*5*3*2*2)同样有很高的处理效率。

2阶指数是啥玩意?指数是啥子?明天单独讲(反正小嗷每次理解过后几个月,就忘了,这玩意生活中用到个鸡,O(∩_∩)O哈哈~)

getOptimalDFTSize()函数返回大于或等于vecsize的最小数值N,这样尺寸为N的向量进行DFT变换能得到更高的处理效率。在当前N通过p,q,r等一些整数得出N = 2^p*3^q*5^r.

这个函数不能直接用于DCT(离散余弦变换)最优尺寸的估计,可以通过getOptimalDFTSize((vecsize+1)/2)*2得到。

2.5 magnitude()函数

用途:计算二维矢量的幅值

C++: void magnitude(InputArray x, InputArray y, OutputArray magnitude);

参数解释:

InputArray x: 浮点型数组的x坐标矢量,也就是实部

InputArray y: 浮点型数组的y坐标矢量,必须和x尺寸相同

OutputArray magnitude: 与x类型和尺寸相同的输出数组

其计算公式如下:

4.矢量的幅值

估计单独讲指数没啥意义,再加上什么是矢量,(:з」∠)(矢量或者向量特征识别会用到)

2.6 copyMakeBorder()函数

用途:围绕图像形成边框,扩充图像边界

C++: void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar& value=Scalar() );

参数解释:

  1. InputArray src: 输入图像
  2. OutputArray dst: 输出图像,与src图像有相同的类型,其尺寸应为Size(src.cols+left+right, src.rows+top+bottom)
  3. int类型的top、bottom、left、right: 在图像的四个方向上扩充像素的值
  4. int borderType: 边界类型,由borderInterpolate()来定义,常见的取值为BORDER_CONSTANT
  5. const Scalar& value = Scalar(): 如果边界类型为BORDER_CONSTANT则表示为边界值

2.7 normalize()函数

重点:归一化就是把要处理的数据经过某种算法的处理限制在所需要的范围内。首先归一化是为了后面数据处理的方便,其次归一化能够保证程序运行时收敛加快。归一化的具体作用是归纳同意样本的统计分布性,归一化在0-1之间是统计的概率分布,归一化在某个区间上是统计的坐标分布,在机器学习算法的数据预处理阶段,归一化也是非常重要的步骤。

看看明天抽不抽时间具体写写归一化,这里简单讲讲

参数解释:

  1. InputArray src: 输入图像
  2. OutputArray dst: 输出图像,尺寸大小和src相同
  3. double alpha = 1: range normalization模式的最小值
  4. double beta = 0: range normalization模式的最大值,不用于norm normalization(范数归一化)模式
  5. int norm_type = NORM_L2: 归一化的类型,主要有

NORM_INF: 归一化数组的C-范数(绝对值的最大值)

NORM_L1: 归一化数组的L1-范数(绝对值的和)

NORM_L2: 归一化数组的L2-范数(欧几里得)

欧几里得,呵呵呵. 算了堆到明天讲讲,讲不完后天

NORM_MINMAX: 数组的数值被平移或缩放到一个指定的范围,线性归一化,一般较常用。

6.int dtype = -1: 当该参数为负数时,输出数组的类型与输入数组的类型相同,否则输出数组与输入数组只是通道数相同,而depth = CV_MAT_DEPTH(dtype)

7.InputArray mask = noArray(): 操作掩膜版,用于指示函数是否仅仅对指定的元素进行操作。

5.代码

任务:计算以及显示傅里叶变换后的幅度图像。

由于数字图像的离散性,像素值的取值范围也是有限的。比如在一张灰度图像中,像素灰度值一般再0-255之间。因此,我们这里讨论的也仅仅是离散傅里叶变换(DFT),如果需要得到图像中的几何结构信息,那我们就要用到它了(以后将轮廓的时候)

在频域里面,对于一副图像,高频部分代表了图像的细节,纹理信息;低频部分代表了图像的轮廓信息。如果对一副精细的图像使用低通滤波器,那么滤波后结果就只剩下轮廓。这与信号处理的基本思想是相通的。如果图像受到的噪声刚好位于某个特定的“频率”范围内,则可以通过滤波来恢复原来的图像。傅里叶变换在图像处理中可以做到图像增强与图像去噪,图像分割之边缘检测,图像特征提取,图像压缩等。

傅里叶变换在图像处理中可以做到图像增强与图像去噪,图像分割之边缘检测,图像特征提取,图像压缩等。

高频部分代表了图像的细节,纹理信息;低频部分代表了图像的轮廓信息。

//---------------------------------【头文件、命名空间包含部分】-----------------------------
//      描述:包含程序所使用的头文件和命名空间
//-------------------------------------------------------------------------------------------------
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace cv;

//--------------------------------------【main( )函数】-----------------------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-------------------------------------------------------------------------------------------------
int main()
{

    //【1】以灰度模式读取原始图像并显示
    Mat srcImage = imread("D://52.jpg", 0);
    if (!srcImage.data) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); getchar(); return false; }
    imshow("原始图像", srcImage);



    //【2】将输入图像延扩到最佳的尺寸,边界用0补充
    //离散傅里叶变换的运行速度与图片的尺寸有很大关系。
    //当图像尺寸是2,3,5整数倍时,计算速度最快。getOptimalDFTSize就是获取最佳尺寸
    int m = getOptimalDFTSize(srcImage.rows);
    int n = getOptimalDFTSize(srcImage.cols);
    //将添加的像素初始化为0.
    Mat padded;
    //copyMakeBorder()函数的作用是扩充图像边界
    copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols, BORDER_CONSTANT, Scalar::all(0));

    //【3】为傅立叶变换的结果(复数)(实部和虚部)分配存储空间。
    //将planes数组组合合并成一个多通道的数组complexI
    //1.傅里叶结果是复数,这就是说对于每个原图像值,结果会有两个图像值
    //2.频域值范围远远超过空间值范围,因此要将频域存储再float格式中。并多加一个通道储存复数部分
    //3.什么是通道?请翻开第四篇文章谢谢,关注一下小嗷的公众号:aoxiaoji
    Mat planes[] = { Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F) };
    Mat complexI;
    merge(planes, 2, complexI);

    //【4】进行就地离散傅里叶变换
    dft(complexI, complexI);

    //【5】将复数转换为幅值,即=> log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))
    //请翻开第四篇文章有讲通道分离split。谢谢,关注一下小嗷的公众号:aoxiaoji
    split(complexI, planes); // 将多通道数组complexI分离成几个单通道数组,planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
    //计算二维矢量的幅值
    magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude  
    Mat magnitudeImage = planes[0];

    //【6】进行对数尺度(logarithmic scale)缩放
    //将复数转换为幅值范围太大。高值显示为白点,而低值为黑点,高低值的变化无法有效分辨。为了在屏幕上凹显出高低变化的连续性,用对数尺度替代线性尺度
    magnitudeImage += Scalar::all(1);
    log(magnitudeImage, magnitudeImage);//求自然对数

    //【7】剪切和重分布幅度图象限
    //若有奇数行或奇数列,进行频谱裁剪
    //第二步,为了提高处理速度延扩了图像,现在是剔除添加的像素。
    //为了方便显示,也可以重新分布幅度图像象限位置,
    //(注:将第五步得到的幅度图从中间划开,得到4张1/4子图象,将每张子图堪称幅度图的一个象限,重新分布,即将4个角点重叠到图片中心)
    //这样的话原点(0,0)就位移到图像中心了
    magnitudeImage = magnitudeImage(Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2));
    //重新排列傅立叶图像中的象限,使得原点位于图像中心  
    int cx = magnitudeImage.cols / 2;
    int cy = magnitudeImage.rows / 2;
    Mat q0(magnitudeImage, Rect(0, 0, cx, cy));   // ROI区域的左上
    Mat q1(magnitudeImage, Rect(cx, 0, cx, cy));  // ROI区域的右上
    Mat q2(magnitudeImage, Rect(0, cy, cx, cy));  // ROI区域的左下
    Mat q3(magnitudeImage, Rect(cx, cy, cx, cy)); // ROI区域的右下
                                                  //交换象限(左上与右下进行交换)
    Mat tmp;
    q0.copyTo(tmp);
    q3.copyTo(q0);
    tmp.copyTo(q3);
    //交换象限(右上与左下进行交换)
    q1.copyTo(tmp);
    q2.copyTo(q1);
    tmp.copyTo(q2);

    //【8】归一化,用0到1之间的浮点值将矩阵变换为可视的图像格式(部分幅度值仍然超过可显示范围[0,1].幅度归一化到可显示范围)
    //此句代码的OpenCV2版为:
    //normalize(magnitudeImage, magnitudeImage, 0, 1, CV_MINMAX); 
    //此句代码的OpenCV3版为:
    normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX);

    //【9】显示效果图
    imshow("频谱幅值", magnitudeImage);
    waitKey();

    return 0;
}

代码不多,注解多,请慢慢看,切莫复制粘贴,一笔带过。

效果图

6.效果图.PNG

7.png

  1. 本人是抱着玩一玩的心态,学习opencv(其实深度学习没有外界说的这么高深,小嗷是白板,而且有工作在身并且于代码无关)
  2. 大家可以把我的数学水平想象成初中水平,毕竟小嗷既不是代码靠吃饭又不是靠数学吃饭,毕业N年
  3. 写文章主要是为了后人少走点弯路,多交点朋友,一起学习
  4. 如果有好的图像识别群拉我进去QQ:631821577
  5. 就我一个白板,最后还是成的,你们别怕,慢慢来把

7.二维码

分享可以无数次,转载成自己文章QQ邮箱通知一下,未经授权请勿转载。

  • 邮箱:631821577@qq.com
  • QQ群:736854977
  • 有什么疑问公众号提问,下班或者周六日回答,ths

下期估计补补数学,因为毕竟往后深度学习,视频识别,都需要高数。个人原则就是用到再说,谢谢大家体谅 内容: 指数 -> 2阶指数 ->矢量 -> 归一化 -> 欧几里得 -> 对数 -> 象限

如果,哪里写的不好不对尽管提出来,小嗷虚心受教。

展开阅读全文

没有更多推荐了,返回首页