作者:Hohohong
链接:https://www.jianshu.com/p/8d2d93c4229b
來源:简书
图像卷积滤波与高斯模糊
1.1 图像卷积滤波
对于滤波来说,它可以说是图像处理最基本的方法,可以产生很多不同的效果。以下图来说
图中矩阵分别为二维原图像素矩阵,二维的图像滤波矩阵(也叫做卷积核,下面讲到滤波器和卷积核都是同个概念),以及最后滤波后的新像素图。对于原图像的每一个像素点,计算它的领域像素和滤波器矩阵的对应元素的成绩,然后加起来,作为当前中心像素位置的值,这样就完成了滤波的过程了。
可以看到,一个原图像通过一定的卷积核处理后就可以变换为另一个图像了。而对于滤波器来说,也是有一定的规则要求的。
- ① 滤波器的大小应该是奇数,这样它才有一个中心,例如3x3,5x5或者7x7。有中心了,也有了半径的称呼,例如5x5大小的核的半径就是2。
- ② 滤波器矩阵所有的元素之和应该要等于1,这是为了保证滤波前后图像的亮度保持不变。当然了,这不是硬性要求了。
- ③ 如果滤波器矩阵所有元素之和大于1,那么滤波后的图像就会比原图像更亮,反之,如果小于1,那么得到的图像就会变暗。如果和为0,图像不会变黑,但也会非常暗。
- ④ 对于滤波后的结构,可能会出现负数或者大于255的数值。对这种情况,我们将他们直接截断到0和255之间即可。对于负数,也可以取绝对值。
1.2 卷积核一些用法
既然知道滤波器可以用来对原图进行操作,那么,有没有一些比较具体的例子。文中卷积核相关图片来源于网络
1.2.1 空卷积核
可以看到,这个滤波器啥也没有做,得到的图像和原图是一样的。因为只有中心点的值是1。邻域点的权值都是0,对滤波后的取值没有任何影响。
1.2.2 图像锐化滤波器
图像的锐化和边缘检测很像,首先找到边缘,然后把边缘加到原来的图像上面,这样就强化了图像的边缘,使图像看起来更加锐利了。这两者操作统一起来就是锐化滤波器了,也就是在边缘检测滤波器的基础上,再在中心的位置加1,这样滤波后的图像就会和原始的图像具有同样的亮度了,但是会更加锐利。
我们把核加大,就可以得到更加精细的锐化效果
1.2.3 浮雕
浮雕滤波器可以给图像一种3D阴影的效果。只要将中心一边的像素减去另一边的像素就可以了。这时候,像素值有可能是负数,我们将负数当成阴影,将正数当成光,然后我们对结果图像加上128的偏移。这时候,图像大部分就变成灰色了。
下面是45度的浮雕滤波器
我们只要加大滤波器,就可以得到更加夸张的效果了
1.2.4 均值模糊
我们可以将当前像素和它的四邻域的像素一起取平均,然后再除以5,或者直接在滤波器的5个地方取0.2的值即可,如下图:
可以看到,这个模糊还是比较温柔的,我们可以把滤波器变大,这样就会变得粗暴了:注意要将和再除以13.
可以看到均值模糊也可以做到让图片模糊,但是它的模糊不是很平滑,不平滑主要在于距离中心点很远的点与距离中心点很近的所带的权重值相同,产生的模糊效果一样
而想要做到平滑,让权重值跟随中心点位置距离不同而不同,则可以利用正态分布(中间大,两端小)这个特点来实现。
1.3 高斯模糊
有了前面的知识,我们知道如果要想实现高斯模糊的特点,则需要通过构建对应的权重矩阵来进行滤波。
1.3.1 正态分布
![](http://upload-images.jianshu.io/upload_images/4356742-2b8f2992c2cc073b.png)
正态分布中,越接近中心点,取值越大,越远离中心,取值越小。
计算平均值的时候,我们只需要将"中心点"作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。正态分布显然是一种可取的权重分配模式。
1.3.2 高斯函数
如何反映出正态分布?则需要使用高函数来实现。
上面的正态分布是一维的,而对于图像都是二维的,所以我们需要二维的正态分布。
![](http://upload-images.jianshu.io/upload_images/4356742-cd5263b61a23221c.png)
正态分布的密度函数叫做"高斯函数"(Gaussian function)。它的一维形式是:
![](http://upload-images.jianshu.io/upload_images/4356742-885df1569e064b4e.png)
其中,μ是x的均值,σ是x的方差。因为计算平均值的时候,中心点就是原点,所以μ等于0。
![](http://upload-images.jianshu.io/upload_images/4356742-4addde1bf20b0079.png)
根据一维高斯函数,可以推导得到二维高斯函数:
![](http://upload-images.jianshu.io/upload_images/4356742-c522b66c06943404.png)
有了这个函数 ,就可以计算每个点的权重了。
1.3.3 获取权重矩阵
假定中心点的坐标是(0,0),那么距离它最近的8个点的坐标如下:
![](http://upload-images.jianshu.io/upload_images/4356742-219145423be81ca1.png)
更远的点以此类推。
为了计算权重矩阵,需要设定σ的值。假定σ=1.5,则模糊半径为1的权重矩阵如下:
![](http://upload-images.jianshu.io/upload_images/4356742-5f46ce6f2071f840.png)
这9个点的权重总和等于0.4787147,如果只计算这9个点的加权平均,还必须让它们的权重之和等于1,因此上面9个值还要分别除以0.4787147,得到最终的权重矩阵。
![](http://upload-images.jianshu.io/upload_images/4356742-b11500c20d387571.png)
除以总值这个过程也叫做”归一问题“
目的是让滤镜的权重总值等于1。否则的话,使用总值大于1的滤镜会让图像偏亮,小于1的滤镜会让图像偏暗。
1.3.4 计算模糊值
有了权重矩阵,就可以计算高斯模糊的值了。
假设现有9个像素点,灰度值(0-255)如下:
![](http://upload-images.jianshu.io/upload_images/4356742-1aa8845f9ae25f48.png)
每个点乘以自己的权重值:
![](http://upload-images.jianshu.io/upload_images/4356742-b204a24b0fc7cb50.png)
得到
![](http://upload-images.jianshu.io/upload_images/4356742-e9271150c97c5f87.png)
将这9个值加起来,就是中心点的高斯模糊的值。
对所有点重复这个过程,就得到了高斯模糊后的图像。对于彩色图片来说,则需要对RGB三个通道分别做高斯模糊。
1.3.5 边界值问题
既然是根据权重矩阵来进行处理的
![](http://upload-images.jianshu.io/upload_images/4356742-88627bc4ab58b153.png)
如果一个点处于边界,周边没有足够的点,怎么办?
- ① 对称处理,就是把已有的点拷贝到另一面的对应位置,模拟出完整的矩阵。
- ② 赋0,想象图像是无限长的图像的一部分,除了我们给定值的部分,其他部分的像素值都是0
- ③ 赋边界值,想象图像是无限制长,但是默认赋值的不是0而是对应边界点的值
功能:对输入的图像src进行高斯滤波后用dst输出。
参数:src和dst当然分别是输入图像和输出图像。Ksize为高斯滤波器模板大小,sigmaX和sigmaY分别为高斯滤波在横线和竖向的滤波系数(有点晦涩,等下解释)。borderType为边缘点插值类型。
理解:数字图像的滤波可以简单的这么理解,就是对原图像的每一个像素滤波,那么对应这个像素滤波后的值是根据其相邻像素(包括自己那个点)与一个滤波模板进行相乘即可。所以具体到高斯滤波,我们只要知道这个高斯滤波的模板即可。
那怎么确定这个模板呢?首先这个模板的大小为ksize,其每个数字的计算是这样的:
其中 是归一化系数,因为其和要为1.
为了简化,一般在二维图像处理中,ui和uj取0,sigma1和sigma2取相等。所以公式就简化为 :
因此很容易就计算出模板每个位置的数字了,简单吧!
但是要注意2点,第一点就是ksize的宽和高必须是奇数;第二点就是如果参数sigmaX=sigmaY=0,则实际用的是公式sigma = 0.3*((ksize-1)*0.5 - 1) + 0.8 .
其函数声明为:void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT ) ;
功能:对输入的图像src进行高斯滤波后用dst输出。
参数:src和dst当然分别是输入图像和输出图像。Ksize为高斯滤波器模板大小,sigmaX和sigmaY分别为高斯滤波在横线和竖向的滤波系数。borderType为边缘扩展点插值类型。
接下来的工作就是进入GaussianBlur函数内部,跟踪其函数代码,经过分析,在该函数内部调用了很多其他的函数,其调用的函数层次结构如下图所示:
这里我们分析源代码不需要深入到最底层,我们只需分析到函数createSeparableLinearFilter和getGaussianKernel这一层。
那就开始我们的源码分析工作吧!
从函数调用层次结构图可以看出,要分析函数GaussianBlur,必须先分析其调用过的内部函数。
因此首先分析函数getGaussianKernel。
功能:返回一个ksize*1的数组,数组元素满足高斯公式:
其中只有系数alpha和参数sigma未知,sigma的求法为:
如果输入sigma为非正,则计算公式为:sigma = 0.3*((ksize-1)*0.5 - 1) + 0.8 .
如果输入sigma为正,则就用该输入参数sigma。
最后alpha为归一化系数,即计算出的ksize个数之和必须为1,所以后面只需求ksize个数,计算其和并求倒即可。
其源码及注释如下:
cv::Mat cv::getGaussianKernel( int n, double sigma, int ktype )
{
const int SMALL_GAUSSIAN_SIZE = 7;
static const float small_gaussian_tab[][SMALL_GAUSSIAN_SIZE] =
{
{1.f},
{0.25f, 0.5f, 0.25f},
{0.0625f, 0.25f, 0.375f, 0.25f, 0.0625f},
{0.03125f, 0.109375f, 0.21875f, 0.28125f, 0.21875f, 0.109375f, 0.03125f}
};
/*如果sigma小于0,且n为不大于7的奇整数,则核的滤波系数固定了,其固定在数组
small_gaussian_tab中,根据其n的长度来选择具体的值 ,如果不满足上面的,则固定核为0
固定核为0表示自己计算其核*/
const float* fixed_kernel = n % 2 == 1 && n <= SMALL_GAUSSIAN_SIZE && sigma <= 0 ?
small_gaussian_tab[n>>1] : 0;
CV_Assert( ktype == CV_32F || ktype == CV_64F );//确保核元素为32位浮点数或者64位浮点数
Mat kernel(n, 1, ktype);//建立一个n*1的数组kernel,一个Mat矩阵包括一个矩阵头和一个指向矩阵元素的指针
float* cf = (float*)kernel.data;//定义指针cf指向kernel单精度浮点型数据
double* cd = (double*)kernel.data;//定义指针cd指向kernerl双精度浮点型数据
double sigmaX = sigma > 0 ? sigma : ((n-1)*0.5 - 1)*0.3 + 0.8;//当sigma小于0时,采用公式得到sigma(只与n有关)
double scale2X = -0.5/(sigmaX*sigmaX);//高斯表达式后面要用到
double sum = 0;
int i;
for( i = 0; i < n; i++ )
{
double x = i - (n-1)*0.5;
//如果自己算其核的话,就常用公式exp(scale2X*x*x)计算,否则就用固定系数的核
double t = fixed_kernel ? (double)fixed_kernel[i] : std::exp(scale2X*x*x);
if( ktype == CV_32F )
{
cf[i] = (float)t;//单精度要求时存入cf数组中
sum += cf[i];//进行归一化时要用到
}
else
{
cd[i] = t;//双精度时存入cd数组中
sum += cd[i];
}
}
sum = 1./sum;//归一化时核中各元素之和为1
for( i = 0; i < n; i++ )
{
if( ktype == CV_32F )
cf[i] = (float)(cf[i]*sum);//归一化后的单精度核元素
else
cd[i] *= sum;//归一化后的双精度核元素
}
return kernel;//返回n*1的数组,其元素或是单精度或是双精度,且符合高斯分布
}
下面该分析函数createSeparableLinearFilter了。
功能为:创建一个图像滤波其引擎类,其主要处理的是原图像和目标图像数据格式的统以及滤波器核的合成。
其源码及注释如下:
cv::Ptr<cv::FilterEngine> cv::createSeparableLinearFilter(
int _srcType, int _dstType,
InputArray __rowKernel, InputArray __columnKernel,
Point _anchor, double _delta,
int _rowBorderType, int _columnBorderType,
const Scalar& _borderValue )//InputArray是Mat类型,表示的是输入数组
{
//_rowKernel存储其矩阵头,_columnKernel类似
Mat _rowKernel = __rowKernel.getMat(), _columnKernel = __columnKernel.getMat();
_srcType = CV_MAT_TYPE(_srcType);//求矩阵的数组类型,数据类型包过通道数,深度,和数据类型3种
_dstType = CV_MAT_TYPE(_dstType);//类似
int sdepth = CV_MAT_DEPTH(_srcType), ddepth = CV_MAT_DEPTH(_dstType);//求矩阵元素深度
int cn = CV_MAT_CN(_srcType);//求矩阵元素通道
CV_Assert( cn == CV_MAT_CN(_dstType) );//源数组和目标数组的通道数必须相等
int rsize = _rowKernel.rows + _rowKernel.cols - 1;//求行长
int csize = _columnKernel.rows + _columnKernel.cols - 1;//求列长
if( _anchor.x < 0 )//求被滤波点的位置
_anchor.x = rsize/2;
if( _anchor.y < 0 )
_anchor.y = csize/2;
/*getKernelType()这个函数内部就不分析了,宏观上分析一下,其函数声明为:
int getKernelType(InputArray kernel, Point anchor)
功能:根据输入核系数矩阵kernel和被平滑点anchor来分析该核的类型,其类型主要有以下5种。
1.普通核,没什么特点的
2.对称核,anchor点在中心,且中心点2边的系数对称相等
3.反对称核,anchor点也在中心,但中心点2边的系数对称相反
4.平滑核,即每个数都是非负,且所有数相加为1
5.整数核,即核内每个系数都是整数
*/
int rtype = getKernelType(_rowKernel,
_rowKernel.rows == 1 ? Point(_anchor.x, 0) : Point(0, _anchor.x));//返回行矩阵核类型
int ctype = getKernelType(_columnKernel,
_columnKernel.rows == 1 ? Point(_anchor.y, 0) : Point(0, _anchor.y));//返回列矩阵核类型
Mat rowKernel, columnKernel;
/*在源代码types_c.h中有
#define CV_8U 0
#define CV_8S 1
#define CV_16U 2
#define CV_16S 3
#define CV_32S 4
#define CV_32F 5
#define CV_64F 6
*/
int bdepth = std::max(CV_32F,std::max(sdepth, ddepth));//在sdepth,ddepth,CV_32F(即5)中选出一个最大的数
int bits = 0;
if( sdepth == CV_8U &&
((rtype == KERNEL_SMOOTH+KERNEL_SYMMETRICAL &&//行列都是平滑对称核,且类型为8位无符号整型
ctype == KERNEL_SMOOTH+KERNEL_SYMMETRICAL &&
ddepth == CV_8U) ||
((rtype & (KERNEL_SYMMETRICAL+KERNEL_ASYMMETRICAL)) &&
(ctype & (KERNEL_SYMMETRICAL+KERNEL_ASYMMETRICAL)) &&
(rtype & ctype & KERNEL_INTEGER) && //或者行列都是整型对称或反对称核,且目标数组类型为16位有符号型
ddepth == CV_16S)) )
{
bdepth = CV_32S; //重新给bdepth赋值
bits = ddepth == CV_8U ? 8 : 0;//当目标矩阵类型为CV_8U时,位深就为8,否则为0
/*convertTo()函数是源数组线性变换成目标数组,第二个参数为目标数组的类型*/
_rowKernel.convertTo( rowKernel, CV_32S, 1 << bits );//将源行数组变换成32s的目标数组
_columnKernel.convertTo( columnKernel, CV_32S, 1 << bits );//将源列数组变换成32s的目标数组
bits *= 2;//为0或者为16
_delta *= (1 << bits);//起放大作用?
}
else
{
if( _rowKernel.type() != bdepth )
_rowKernel.convertTo( rowKernel, bdepth );//将源行数组深度转换为目的数组深度
else
rowKernel = _rowKernel;
if( _columnKernel.type() != bdepth )
_columnKernel.convertTo( columnKernel, bdepth );//将源列数组深度转换为目的数组深度
else
columnKernel = _columnKernel;
}//到目前这一行为止,也只是做了一个非常简单的工作,即把输入的行列矩阵数据类型统一
int _bufType = CV_MAKETYPE(bdepth, cn);//创建一个缓冲数组类型,有深度和通道数2方面的信息?
/*Ptr<BaseRowFilter> _rowFilter表示创建一个参数为BaseRowFilter的具体类Ptr*/
Ptr<BaseRowFilter> _rowFilter = getLinearRowFilter(
_srcType, _bufType, rowKernel, _anchor.x, rtype);
Ptr<BaseColumnFilter> _columnFilter = getLinearColumnFilter(
_bufType, _dstType, columnKernel, _anchor.y, ctype, _delta, bits );//基本上也是完成数据类型的整理
/*FilterEngine为一个通用的图像滤波类
*/
return Ptr<FilterEngine>( new FilterEngine(Ptr<BaseFilter>(0), _rowFilter, _columnFilter,
_srcType, _dstType, _bufType, _rowBorderType, _columnBorderType, _borderValue ));
//新创建一个Ptr的模板类并用类FilterEngine的构造函数来初始化它
}
接着分析函数createGaussianFilter。
功能:给定滤波核大小和类型,以及2个sigma,就可以得出一个二维滤波核。两个sigma允许输入负数等其他不常用的输入。
其源码及注释如下:
cv::Ptr<cv::FilterEngine> cv::createGaussianFilter( int type, Size ksize,
double sigma1, double sigma2,
int borderType )
{
int depth = CV_MAT_DEPTH(type);//取数组元素的深度
if( sigma2 <= 0 )
sigma2 = sigma1;//当第3个参数为非正时,取其与第二个参数相同的值
// automatic detection of kernel size from sigma
/*一般情况下满足sigma1>0*/
if( ksize.width <= 0 && sigma1 > 0 )//当滤波器核的宽非正时,其宽要重新经过计算
/*根据CV_8U来计算,核宽为接近7*sigma1或者9*sigma1*/
ksize.width = cvRound(sigma1*(depth == CV_8U ? 3 : 4)*2 + 1)|1;
if( ksize.height <= 0 && sigma2 > 0 )
/*同理,核高根据CV_8U来计算,为接近7*sigma2或者9*sigma2*/
ksize.height = cvRound(sigma2*(depth == CV_8U ? 3 : 4)*2 + 1)|1;
CV_Assert( ksize.width > 0 && ksize.width % 2 == 1 &&
ksize.height > 0 && ksize.height % 2 == 1 );//确保核宽和核高为正奇数
sigma1 = std::max( sigma1, 0. );//sigma最小为0
sigma2 = std::max( sigma2, 0. );
Mat kx = getGaussianKernel( ksize.width, sigma1, std::max(depth, CV_32F) );//得到x方向一维高斯核
Mat ky;
if( ksize.height == ksize.width && std::abs(sigma1 - sigma2) < DBL_EPSILON )
ky = kx;//如果核宽和核高相等,且两个sigma相差很小的情况下,y方向的高斯核去与x方向一样,减少计算量
else
ky = getGaussianKernel( ksize.height, sigma2, std::max(depth, CV_32F) );//否则计算y方向的高斯核系数
return createSeparableLinearFilter( type, type, kx, ky, Point(-1,-1), 0, borderType );//返回2维图像滤波引擎
}
最后来看真正的高斯滤波函数GaussianBlur:
功能:对输入图像_src进行滤波得到输出图像_dst,滤波核大小为ksize,滤波参数由sigma1和sigma2计算出,边缘扩展模式为borderType.
其源代码和注释如下:
void cv::GaussianBlur( InputArray _src, OutputArray _dst, Size ksize,
double sigma1, double sigma2,
int borderType )
{
Mat src = _src.getMat();//创建一个矩阵src,利用_src的矩阵头信息
_dst.create( src.size(), src.type() );//构造与输入矩阵同大小的目标矩阵
Mat dst = _dst.getMat();//创建一个目标矩阵
if( ksize.width == 1 && ksize.height == 1 )
{
src.copyTo(dst);//如果滤波器核的大小为1的话,则说明根本就不用滤波,输出矩阵与输入矩阵完全相同
return;
}
if( borderType != BORDER_CONSTANT )//当边缘扩展不是常数扩展时
{
if( src.rows == 1 )
ksize.height = 1;//如果输入矩阵是一个行向量,则滤波核的高强制为1
if( src.cols == 1 )
ksize.width = 1;//如果输入矩阵是一个列向量,则滤波核的宽强制为1
}
/*生成一个高斯滤波器引擎f*/
Ptr<FilterEngine> f = createGaussianFilter( src.type(), ksize, sigma1, sigma2, borderType );
f->apply( src, dst );//调用引擎函数,完成将输入矩阵src高斯滤波为输出矩阵dst
}
至此,函数GaussianBlur源码已经分析结束了,格式排版太累了!欢迎交流!
至此做一个总结:
高斯函数的使用方法:
-参数 InputArray表示输入图像的Mat对象。
-参数OutputArray表示模糊之后输出Mat对象
-参数Size表示卷积和大小,此参数决定模糊程度,Size(x,y)中x,y取值越大表现模糊程度越深,而且x,y必须为奇数。
-参数SigmaX表示高斯方程中x方向的标准方差
-参数SigmaY表示高斯方程中y方向的标准方差
-最后一个参数表示对边缘的处理方法,一般取默认值
使用方法一:Size(x,y)中x,y取正奇数,SigmaX,SigmaY取0,此时SigmaX,SigmaY会自动计算,公式为sigma = ((n-1)*0.5 - 1)*0.3 + 0.8,xy方向上都计算。
使用方法二:Size(x,y) 中的xy取0,SigmaX,SigmaY都不去0,此时xy会自动计算,公式为sigma1*(depth == CV_8U ? 3 : 4)*2 + 1)|1。xy都计算。
使用方法三:Size(x,y)中xy,SigmaX,SigmaY都不为0.
前两中方法总之x是SigmaX,y是SigmaY的6-9倍。