OpenCV(二)掩码操作与平滑(均值,高斯模糊)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenCV(二)掩码操作与平滑(均值,高斯模糊)相关的知识,希望对你有一定的参考价值。
参考技术A OpenCV知识总结来到了下一个难度高一点的,掩码操作和模糊效果,这是图像处理里面常见的操作。如果遇到问题请在这里联系我: https://www.jianshu.com/p/67324fb69074
掩码操作实际上思想上很简单:根据一个掩码矩阵(卷积核)重新计算图像中的每一个像素。掩码矩阵中的值表示近邻像素的值(包括自身像素的值)对新像素的值有多大的影响,从数学上观点看来,就是对掩码矩阵内每一个设置好权重,然后对对应的像素领域内做一个权加平均。
卷积是什么?用一个简单的公式来表示:
本质上,卷积就是这种思想。卷积把万事万物看成一个输入,当万事万物的状态出现了变化,则会通过某种系统产生变化,变成另一种输出状态。而这个系统往往就在数学眼里就叫做卷积。
而在深度学习中,往往每一个卷积核是一个奇数的矩阵,做图像识别的时候会通过这个卷积核做一次过滤,筛选出必要的特征信息。
那么掩码操作在数学上是怎么回事?我们平常运用掩码做什么?在OpenCV中掩码最常见的操作就是增加图片对比度。对比度的概念是什么,在上一节聊过,通俗来讲就是能够增强像素之间的细节。我们可以对对每个像素做如下操作:
可能这幅图,理解起来比较困难。实际上流程如此:
举个例子,就以计算出掩码矩阵之后的E的位置,能看到此时是原图中所有的像素都会取出和掩码矩阵一样大小的矩阵。也就是取出原图的红色那一块的领域,分别对E周边包括自己做了一次加权处理,最后赋值回E中。
并且进行如下的权加公式:
这样就能对原来的矩阵进行掩码处理。但是这么做发现没有,如果我们要对A做掩码处理就会发现掩码矩阵对应到原图的位置不存在。现在处理有两种,一种是不对边缘的像素做掩码处理,另一种是为周边的图像做一个padding处理,这种操作在深度学习的图像处理中很常见,通常设置0像素,或者拷贝对边的边缘像素。
能看到这里处理和卷积处理不太一样,只是为了方便,把这种掩码滤波操作称为一种核,是自相关,并不是去计算卷积。
能看到此时这两张图片的对比度有很明显的区别。经过掩码矩阵之后,会发现原图会更加平滑一点,而掩码操作之后会导致整个图片最亮和最暗之间的差距拉大。
从数学公式上来看,当前像素权重为5,周边的点的权重是-1和0.能够发现会对当前的节点加深,同时把周围的像素值减掉,就增加了每一个像素点和周边像素差值,也就是对比度。
当然在OpenCV中,有这么一个函数filter2D,处理掩码操作。
这里创建一个3*3的核。这个核实际上就是上图的那个。这样传递一个掩码矩阵和图像的深度就完成了掩码操作。
平滑也称为模糊,是一项高频率使用的操作。
平滑的作用有很多,其中一项就是降噪音。平滑处理和掩码操作有点相似,也是需要一个滤波器,我们最常用的滤波器就是线性滤波器。线性滤波处理的输出像素值 是输出像素值 的权加和:
其中,h(k,l)成为核,它仅仅只是一个权加系数。图形上的操作和上面的图相似。不妨把核当成一个滑动窗口,不断的沿着原图像素的行列扫动,扫动的过程中,不断的把路过像素都平滑处理。
这里先介绍均值滤波器,它的核心如下:
这里的参数意思是,src:输入的图像,dst:经过均值模糊之后的输出图像,Size:是指这个滤波器的大小,Point是指整个图像模糊绕着哪个原点为半径的进行处理,传入(-1,-1)就是指图像中心,这样就能模糊整个图像。
其计算原理很简单就是,把核里面的所有权重设置为1,最后全部相加求平均值。最后赋值到原来的像素上。
最有用的滤波器 (尽管不是最快的)。 高斯滤波是将输入数组的每一个像素点与 高斯内核 卷积将卷积和当作输出像素值。
高斯模糊实际上是一个二维的高斯核。回顾一下一维的高斯函数:
那么二维实际上就是,就是在原来的x,y轴的情况下,增加一个z轴的纬度,实际上看起来就像一座山一样。
二维的高斯函数可以表示为:
为了达到达到
其OpenCV的调用方式:
这里的参数意思是,src:输入的图像,dst:经过高斯模糊之后的输出图像,Size:是指这个滤波器的大小。sigmaX和sigmaY分别指代的是高斯模糊x轴上和y轴上的二维高斯函数的变化幅度。
换个形象的话说,用上图举个例子,就是确定这个高斯函数这个山的x方向的陡峭程度以及y轴方向的陡峭程度。
下面就高斯模糊,均值模糊和原图的比对
能看到,高斯模糊比起均值模糊保留了图像中相关的形状信息。
为什么会这样呢?原因很简单。因为在计算高斯模糊之前,会根据当前像素区域中所有的像素点进行一次,核的计算,越往中心的权重越高,权重如同小山一下,因此中心的像素权重像素一高了,虽然模糊但是还是保留了原来的形状。
但是当高斯模糊的矩阵大小和sigmaX,sigmaY相似的时候,整个高斯函数就不像山,而是想平原一样平坦。换句话说,整个高斯核中的权重就会,偏向一,就会导致和均值模糊类似效果。
高斯模糊计算流程:
图像中某一段图形的像素是如下分布,
这个时候高斯模糊需要一个核去对每一个位置做滤波。此时不同于均值模糊,没有固定的核矩阵,而是通过上面这个矩阵,计算出高斯的核,最后再计算变化后的矩阵每一个对应的像素。
虽然原理是这样,但是实际上OpenCV为了迅速,在原生实现的时候,内部判断到核是小于7的大小,设置一套固定的高斯模糊矩阵。
这样直接就结束,不是我文章的风格,作为一个程序员,还是有必要探索一下,为什么OpenCV计算速度会比我们自己手写的快。
为了让源码看的不那么辛苦,先聊聊OpenCV底层的设计思想。首先在OpenCV中,内置了几种计算方案,按照效率高低优先度依次的向后执行。
这种设计可以看成我们的平常开发的拦截器设计。当发现优先度高的计算模式发现可以使用的时候,OpenCV将会使用这种模式下的算法进行运算。
一般来说,OpenCV内置如下四个层级计算方案,按照优先顺序依次为:
能看到按照这个优先级不断的向下查找,找到当前OpenCV最快的计算环境。除了最后一个之外,其他三个都是并发计算。
记住这个流程,我们查看OpenCV的源码就很轻松了。
先来看看filter2D的源码。
果不其然,在filter2D实现的第一步,就开始调用CV_OCL_RUN宏去调用OpenCL的显卡并发计算。
能看到,这里面发送了一个condition和一个方法到OpenCL中运行。但是如果,OpenCV在编译的时候,我们没有打开这个OpenCL的选项,没有OpenCL的环境的时候,它实际上就是一个没什么用处的宏:
当有了OpenCL的环境,这个宏就会替换成这个:
能清晰的看到,此时会判断当前的OpenCL是否还在活跃,活跃的状态,并且条件和方法符合规范,就会通过CV_IMPL_ADD,把方法添加到一个vector向量中,让OpenCL读取执行。
在这里面,OpenCV想要使用OpenCL进行计算,就需要这个Mat的类型是UMat,并且是纬度小于等于2.当不符合这两个条件将不会执行OpenCL。
UMat是专门给OpenCL规范计算而使用的矩阵。里面有很多和Mat相似的方法。
此时可能是多线程处理,因此会添加一个智能锁,去保证数据的正确性。
具体的思路,将不作为重点,这边先看看OpenCV是传入了ocl_filter2D的方法,看看这个方法在OpenCL中的执行流程。
OpenCL会把命令最后发送到显卡处理。
实际上这一步和上面的方法有点相似。本质上都是获取需要模糊的区域,如果是(-1,-1),则取中心点,接着判断当前滤波对边缘的处理(BORDER_ISOLATED 不去获取Point为圆心设置的模糊之外的区域)。
能看到这个枚举已经解释很清楚了,默认的边缘处理是复制二个和倒数第二个填充边缘。
最后进入到hal的filter2D进一步操作。
能看到这里有四种方式:
在情况1中,一般的情况replacementFilter2D返回的是一个没有实现的错误码,第二种情况是Intel的并行计算库,没有任何研究,跳过。我们来看看第三种情况和第四种情况
当然这里面判断能够使用dft的判断首先要当前必须要整张图做滤波处理,其次是不能是(0,0)的点为圆心做滤波。最后要判断当前当前的cpu指令是否支持,支持则允许核的宽 高最高为130以内使用原生实现,否则只支持核的宽 高为50以内使用原生实现。
能看到这里面的核心就是调用crossCorr,处理核以及原图的矩阵(使用了快速傅立叶处理相关性计算)。最后从同add添加到目标Mat中,由于add的delta函数为0,因此就和替代的效果一致。
能看到此时,先初始化一个FilterEngine(线性滤波引擎),接着使用apply调用滤波引擎的执行方法。
我们来看看线性引擎的创建:
实际上在这个过程中通过makePtr创建一个sharedptr的指针指向FilterEngine,其原理和Android的智能指针相似。
这个引擎不是关键关键的是getLinearFilter,这个方法创建了一个线性滤波器的实际操作对象。
我们来看看这个结构体:
能看到这里面会根据次数传进来的目标矩阵和原始矩阵的位深创建不同的滤波操作者。
假设,我们现在原图和目标图都是8位位深的矩阵,我们只需要关注下面这个构造函数。
Fliter2D结构体持有着模糊中心点,核,原/目标矩阵, 可以猜测到实际上正在做操作的就是这个结构体。
在preprocess2DKernel方法中,Fliter2D把核的相关信息存储到coords,coeffs中
可以看到此时会判断当前的核矩阵中type是什么,接着再把矩阵中每一个不为0的位置设置进coords,像素数值设置到_coeffs。此时相当于把核矩阵展开成一个向量。
能看到此时滤波引擎会先调用FilterEngine__start,再调用FilterEngine__proceed执行计算。
实际上在FilterEngine__start中计算的是本次循环,需要计算的边界。
FilterEngine__proceed中才是正式计算,做dst循环,最后把具体操作丢给线性引擎生成的Fliter2D的方法中。
了解这两个东西我们直接抽出核心看看fliter是如何运作:
OpenCV---高斯模糊(均值模糊的另一种)
高斯分布:
高斯模糊的原理
一:图像产生高斯噪声循环代码实现(耗时)
def clamp(pv): #使我们的随机值在0-255之间 if pv > 255: return 255 if pv < 0: return 0 return pv import cv2 as cv import numpy as np def gaussian_noise(image): #对图像加上高斯噪声 h,w,c = image.shape for row in range(h): #十分耗时 for col in range(w): s = np.random.normal(0,20,3) #产生3个随机值,符合正态分布,第一个参数是概率分布的均值,对应分布中心,,第二个是概率分布的标准差,越小越瘦高,第三个是输出的值个数 b = image[row,col,0] #blue g = image[row,col,1] #green r = image[row,col,2] #red image[row,col,0] = clamp(b+s[0]) image[row,col,1] = clamp(g+s[1]) image[row,col,2] = clamp(r+s[2]) cv.imshow("noise image",image) src = cv.imread("./1.png") #读取图片 cv.namedWindow("input image",cv.WINDOW_AUTOSIZE) #创建GUI窗口,形式为自适应 cv.imshow("input image",src) #通过名字将图像 gaussian_noise(src) cv.waitKey(0) #等待用户操作,里面等待参数是毫秒,我们填写0,代表是永远,等待用户操作 cv.destroyAllWindows() #销毁所有窗口
推文:从np.random.normal()到正态分布的拟合
二:使用高斯模糊
dst = cv.GaussianBlur(src,(0,0),15) #我们可以通过修改高斯内核(快)和标准差来修改模糊程度 cv.imshow("GaussianBlur",dst)
参数详解如下:
src,输入图像,即源图像,填Mat类的对象即可。它可以是单独的任意通道数的图片,但需要注意,图片深度应该为CV_8U,CV_16U, CV_16S, CV_32F 以及 CV_64F之一。
dst,即目标图像,需要和源图片有一样的尺寸和类型。比如可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。
ksize,高斯内核的大小。其中ksize.width和ksize.height可以不同,但他们都必须为正数和奇数(并不能理解)。或者,它们可以是零的,它们都是由sigma计算而来。
sigmaX,表示高斯核函数在X方向的的标准偏差。
sigmaY,表示高斯核函数在Y方向的的标准偏差。若sigmaY为零,就将它设为sigmaX,如果sigmaX和sigmaY都是0,那么就由ksize.width和ksize.height计算出来。
为了结果的正确性着想,最好是把第三个参数Size,第四个参数sigmaX和第五个参数sigmaY全部指定到。
borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。
ps:
高斯核函数:自己找找看,其实看上去就是普通的正态分布函数形式。
我猜ksize是模糊半径的意思,水平方向和竖直方向的半径不一样,需要指定。
标准偏差就是标准偏差……
三:使用高斯模糊处理高斯噪声(发现高斯噪声的影响不大,高斯模糊对其有抑制作用)
gaussian_noise(src) #修改原图为高斯噪声图 dst = cv.GaussianBlur(src,(5,5),15) cv.imshow("GaussianBlur",dst)
以上是关于OpenCV(二)掩码操作与平滑(均值,高斯模糊)的主要内容,如果未能解决你的问题,请参考以下文章