图像平滑处理
原理
Note
以下原理来源于Richard Szeliski 的著作 Computer Vision: Algorithms and Applications 以及 Learning OpenCV
-
平滑 也称 模糊, 是一项简单且使用频率很高的图像处理方法。
-
平滑处理的用途有很多, 但是在本教程中我们仅仅关注它减少噪声的功用 (其他用途在以后的教程中会接触到)。
-
平滑处理时需要用到一个 滤波器 。 最常用的滤波器是 线性 滤波器,线性滤波处理的输出像素值 (i.e. ) 是输入像素值 (i.e. )的加权和 :
称为 核, 它仅仅是一个加权系数。
不妨把 滤波器 想象成一个包含加权系数的窗口,当使用这个滤波器平滑处理图像时,就把这个窗口滑过图像。
-
滤波器的种类有很多, 这里仅仅提及最常用的:
归一化块滤波器 (Normalized Box Filter)
-
最简单的滤波器, 输出像素值是核窗口内像素值的 均值 ( 所有像素加权系数相等)
-
核如下:
高斯滤波器 (Gaussian Filter)
-
最有用的滤波器 (尽管不是最快的)。 高斯滤波是将输入数组的每一个像素点与 高斯内核 卷积将卷积和当作输出像素值。
-
还记得1维高斯函数的样子吗?
假设图像是1维的,那么观察上图,不难发现中间像素的加权系数是最大的, 周边像素的加权系数随着它们远离中间像素的距离增大而逐渐减小。
Note
2维高斯函数可以表达为 :
其中 为均值 (峰值对应位置), 代表标准差 (变量 和 变量 各有一个均值,也各有一个标准差)
中值滤波器 (Median Filter)
中值滤波将图像的每个像素用邻域 (以当前像素为中心的正方形区域)像素的 中值 代替 。
双边滤波 (Bilateral Filter)
- 目前我们了解的滤波器都是为了 平滑 图像, 问题是有些时候这些滤波器不仅仅削弱了噪声, 连带着把边缘也给磨掉了。 为避免这样的情形 (至少在一定程度上 ), 我们可以使用双边滤波。
- 类似于高斯滤波器,双边滤波器也给每一个邻域像素分配一个加权系数。 这些加权系数包含两个部分, 第一部分加权方式与高斯滤波一样,第二部分的权重则取决于该邻域像素与当前像素的灰度差值。
- 详细的解释可以查看 链接
源码
-
本程序做什么?
- 装载一张图像
- 使用4种不同滤波器 (见原理部分) 并显示平滑图像
-
代码一瞥:
解释
-
下面看一看有关平滑的OpenCV函数,其余部分大家已经很熟了。
-
归一化块滤波器:
OpenCV函数 blur 执行了归一化块平滑操作。
我们输入4个实参 (详细的解释请参考 Reference):
- src: 输入图像
- dst: 输出图像
- Size( w,h ): 定义内核大小( w 像素宽度, h 像素高度)
- Point(-1, -1): 指定锚点位置(被平滑点), 如果是负值,取核的中心为锚点。
-
高斯滤波器:
OpenCV函数 GaussianBlur 执行高斯平滑 :
我们输入4个实参 (详细的解释请参考 Reference):
- src: 输入图像
- dst: 输出图像
- Size(w, h): 定义内核的大小(需要考虑的邻域范围)。 和 必须是正奇数,否则将使用 和 参数来计算内核大小。
- : x 方向标准方差, 如果是 则 使用内核大小计算得到。
- : y 方向标准方差, 如果是 则 使用内核大小计算得到。.
-
中值滤波器:
OpenCV函数 medianBlur 执行中值滤波操作:
我们用了3个参数:
- src: 输入图像
- dst: 输出图像, 必须与 src 相同类型
- i: 内核大小 (只需一个值,因为我们使用正方形窗口),必须为奇数。
-
双边滤波器
OpenCV函数 bilateralFilter 执行双边滤波操作:
我们使用了5个参数:
- src: 输入图像
- dst: 输出图像
- d: 像素的邻域直径
- : 颜色空间的标准方差
- : 坐标空间的标准方差(像素单位)
结果
-
程序显示了原始图像( lena.jpg) 和使用4种滤波器之后的效果图。
-
这里显示的是使用 中值滤波 之后的效果图:
1. 简介
图像平滑是一个重要的操作,而且有多种成熟的算法。这里主要简单介绍一下Bilateral方法(双边滤波),这主要是由于前段时间做了SSAO,需要用bilateral blur 算法进行降噪。Bilateral blur相对于传统的高斯blur来说很重要的一个特性即可可以保持边缘(Edge Perseving),这个特点对于一些图像模糊来说很有用。一般的高斯模糊在进行采样时主要考虑了像素间的空间距离关系,但是却并没有考虑像素值之间的相似程度,因此这样我们得到的模糊结果通常是整张图片一团模糊。Bilateral blur的改进就在于在采样时不仅考虑像素在空间距离上的关系,同时加入了像素间的相似程度考虑,因而可以保持原始图像的大体分块进而保持边缘。在于游戏引擎的post blur算法中,bilateral blur常常被用到,比如对SSAO的降噪。
2. 原理
滤波算法中,目标点上的像素值通常是由其所在位置上的周围的一个小局部邻居像素的值所决定。在2D高斯滤波中的具体实现就是对周围的一定范围内的像素值分别赋以不同的高斯权重值,并在加权平均后得到当前点的最终结果。而这里的高斯权重因子是利用两个像素之间的空间距离(在图像中为2D)关系来生成。通过高斯分布的曲线可以发现,离目标像素越近的点对最终结果的贡献越大,反之则越小。其公式化的描述一般如下所述:
其中的c即为基于空间距离的高斯权重,而用来对结果进行单位化。
高斯滤波在低通滤波算法中有不错的表现,但是其却有另外一个问题,那就是只考虑了像素间的空间位置上的关系,因此滤波的结果会丢失边缘的信息。这里的边缘主要是指图像中主要的不同颜色区域(比如蓝色的天空,黑色的头发等),而Bilateral就是在Gaussian blur中加入了另外的一个权重分部来解决这一问题。Bilateral滤波中对于边缘的保持通过下述表达式来实现:
其中的s为基于像素间相似程度的高斯权重,同样用来对结果进行单位化。对两者进行结合即可以得到基于空间距离、相似程度综合考量的Bilateral滤波:
上式中的单位化分部综合了两种高斯权重于一起而得到,其中的c与s计算可以详细描述如下:
且有
且有
上述给出的表达式均是在空间上的无限积分,而在像素化的图像中当然无法这么做,而且也没必要如此做,因而在使用前需要对其进行离散化。而且也不需要对于每个局部像素从整张图像上进行加权操作,距离超过一定程度的像素实际上对当前的目标像素影响很小,可以忽略的。限定局部子区域后的离散化公就可以简化为如下形式:
上述理论公式就构成了Bilateral滤波实现的基础。为了直观地了解高斯滤波与双边滤波的区别,我们可以从下列图示中看出依据。假设目标源图像为下述左右区域分明的带有噪声的图像(由程序自动生成),蓝色框的中心即为目标像素所在的位置,那么当前像素处所对应的高斯权重与双边权重因子3D可视化后的形状如后边两图所示:
左图为原始的噪声图像;中间为高斯采样的权重;右图为Bilateral采样的权重。从图中可以看出Bilateral加入了相似程度分部以后可以将源图像左侧那些跟当前像素差值过大的点给滤去,这样就很好地保持了边缘。为了更加形象地观察两者间的区别,使用Matlab将该图在两种不同方式下的高度图3D绘制出来,如下:
上述三图从左到右依次为:双边滤波,原始图像,高斯滤波。从高度图中可以明显看出Bilateral和Gaussian两种方法的区别,前者较好地保持了边缘处的梯度,而在高斯滤波中,由于其在边缘处的变化是线性的,因而就使用连累的梯度呈现出渐变的状态,而这表现在图像中的话就是边界的丢失(图像的示例可见于后述)。
3. 代码实现
有了上述理论以后实现Bilateral Filter就比较简单了,其实它也与普通的Gaussian Blur没有太大的区别。这里主要包括3部分的操作: 基于空间距离的权重因子生成;基于相似度的权重因子的生成;最终filter颜色的计算。
3.1 Spatial Weight
这就是通常的Gaussian Blur中使用的计算高斯权重的方法,其主要通过两个pixel之间的距离并使用如下公式计算而来:
其中的就表示两个像素间的距离,比如当前像素与其右边紧邻的一个像素之间的距离我们就可以用来计算,也即两个二维向量{0 , 0}以及{0 , 1}之间的欧氏距离。直接计算一个区域上的高斯权重并单位化后就可以进行高斯模糊了。
3.2 Similarity Weight
与基于距离的高斯权重计算类似,只不过此处不再根据两个pixel之间的空间距离,而是根据其相似程度(或者两个pixel的值之间的距离)。
其中的表示两个像素值之间的距离,可以直接使用其灰度值之间的差值或者RGB向量之间的欧氏距离。
3.3 Color Filtering
有了上述两部分所必需的权重因子之后,那么具体的双边滤波的实现即与普通的高斯滤波无异。主要部分代码如下述:
- UCHAR3 BBColor(int posX , int posY)
- {
- int centerItemIndex = posY * picWidth4 + posX * 3 , neighbourItemIndex;
- int weightIndex;
- double gsAccumWeight = 0;
- double accumColor = 0;
- // 计算各个采样点处的Gaussian权重,包括closeness,similarity
- for(int i = -number ; i <= number ; ++i)
- {
- for(int j = -number ; j <= number ; ++j)
- {
- weightIndex = (i + number) * (number * 2 + 1) + (j + number);
- neighbourItemIndex = min(noiseImageHeight - 1 , max(0 , posY + j * radius)) * picWidth4 +
- min(noiseImageWidth - 1 , max(0 , posX + i * radius)) * 3;
- pCSWeight[weightIndex] = LookupGSWeightTable(pSrcDataBuffer[neighbourItemIndex] , pSrcDataBuffer[centerItemIndex]);
- pCSWeight[weightIndex] = pGSWeight[weightIndex] * pGCWeight[weightIndex];
- gsAccumWeight += pCSWeight[weightIndex];
- }
- }
- // 单位化权重因子
- gsAccumWeight = 1 / gsAccumWeight;
- for(int i = -number ; i <= number ; ++i)
- {
- for(int j = -number ; j <= number ; ++j)
- {
- weightIndex = (i + number) * (number * 2 + 1) + (j + number);
- pCSWeight[weightIndex] *= gsAccumWeight;
- }
- }
- // 计算最终的颜色并返回
- for(int i = -number ; i <= number ; ++i)
- {
- for(int j = -number ; j <= number ; ++j)
- {
- weightIndex = (i + number) * (number * 2 + 1) + (j + number);
- neighbourItemIndex = min(noiseImageHeight - 1 , max(0 , posY + j * radius)) * picWidth4 +
- min(noiseImageWidth - 1 , max(0 , posX + i * radius)) * 3;
- accumColor += pSrcDataBuffer[neighbourItemIndex + 0] * pCSWeight[weightIndex];
- }
- }
- return UCHAR3(accumColor , accumColor , accumColor);
- }
其中的相似度分部的权重s主要根据两个Pixel之间的颜色差值计算面来。对于灰度图而言,这个差值的范围是可以预知的,即[-255, 255],因而为了提高计算的效率我们可以将该部分权重因子预计算生成并存表,在使用时快速查询即可。使用上述实现的算法对几张带有噪声的图像进行滤波后的结果如下所示:
上图从左到右分别为:双边滤波;原始图像;高斯滤波。从图片中可以较为明显地看出两种算法的区别,最直观的感受差别就是使用高斯算法后整张图片都是一团模糊的状态;而双边滤波则可以较好地保持原始图像中的区域信息,看起来仍然嘴是嘴、眼是眼(特别是在第一张美女图像上的效果!看来PS是灰常重要啊~~^o^)。
4. 在SSAO中的使用
在上述实现中的边缘判定函数主要是通过两个像素值之间的差异来决定,这也是我们观察普通图片的一种普遍感知方式。当然,也可以根据使用的需求情况来使用其它的方式判断其它定义下的边缘,比如使用场景的depth或是normal。比如在对SSAO进行滤波时可以直接使用Depth值来行边缘判断。首先,设置一个深度的阈值,在作边缘检测时比较两点间的depth差值,如果差值大于阈值,则认为其属于不同的区域,则此处就应为边界。使用此方法得到的效果可见于下图所示:
高斯滤波
双边滤波
在得到滤波之后的SSAO图像之后,与原始图像进行直接的整合就可以得到最终的渲染效果,如下图所示:
SSAO关闭
SSAO开启