高斯模糊(Gaussian Blur)
概述
高斯模糊(Gaussian Blur),也叫高斯平滑,在photoshop中也有高斯模糊滤镜,通常用它来减少图像噪声以及降低细节层次。从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积,由于正态分布又叫做高斯分布,所以这项技术又叫做高斯模糊。
原理
高斯模糊和均值模糊一样,也是取每个像素以及周边像素的平均值,只不过高斯模糊在取值是离原像素越远的像素权重越低,而均值模糊则所有像素的权重相等,因此从计算量上来说,采用相同阶数的高斯模糊计算量要比均值模糊要大,但是模糊效果要更好,由于没有明显的边界,不会出现均值模糊会出现的方块化效果。高斯模糊的权重计算时根据正态分布来计算的,它在N维空间的正态分布方程为:
这个函数省略了位置参数
,因为处理像素都以原像素为中心点,
默认为0了,通常我们只用到正态分布函数的一维函数G(x)
或者二维函数G(x,y),其中r是模糊半径(r²=x²+y²)
其对应的图像分别为:
从图像我们也可以看出,正态分布是一种很不错的权重分布方式;计算平均值的时候,我们只需将中心点作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。下面还有当σ有取不同值时的曲线图像,从图可知,当σ越小时,曲线越高越尖,当σ越大时,曲线就越低越平缓,对应的结果就是图像越模糊。
在实际应用中,如果只是针对图像进行预处理(如photoshop的高斯滤镜),可以比较精确的使用高斯函数(虽然也有优化如把二维计算转化成一维计算),权重都会随着模糊半径变化而变化,因此对应的结果也是最精确的,效果也比较好,而在游戏开发中,因为考虑到实时处理,效率优先一般会采用近似的高斯模板,优化效率。
游戏中对高斯模糊的优化
在游戏中里的模糊通常都是近似高斯模糊,只要保证权重采样是一条类似高斯模糊的钟形曲线就行,即从中心到边缘是平滑渐变的。一般来说对高斯模糊优化有几点:
- 降低阶数:降低采样的阶数,比如5阶的滤波器就比7阶的滤波器效率高,实际上一般不超过7阶。
- 迭代计算:采用低阶采样的同时,可以将进行迭代计算,就是把用上一次的模糊结果,再进行同样的采样模糊,以达到更好的效果。在上一篇均值模糊的文章里也采用了这种方式。
- 固定权重:权重预先计算好,并且归一化固定下来,不在游戏过程中实时计算。
- 降维计算:因为高斯模糊在二维图像上是线性可分的,可以二维计算拆分正两次一维计算。具体就是先在水平方向做一维高斯矩阵变换后,将其结果再进行垂直方向的一维高斯矩阵变换。从下图来看对于一个9阶的二维运算来说,需要9X9=81次采样,但是如果拆分成一维运算的话,只需要9+9=18次采样,只不过需要缓存第一次计算的结果,以空间换时间还是划算的,也可以得出阶数越高,效率相差也越大的结论。
shader代码实现
在本例中将和上篇均值模糊一样,只开放一个参数给外部,本例使用了一个7阶的权重矩阵,由于考虑到函数的对称型我们只需要定义一个包含四个权重的数组,权重已经提前计算好并做了归一化处理【(0.0205+0.0855+ 0.232)*2+ 0.324=1】,因此不会出现图像变暗的问题:
uniform half4 _MainTex_TexelSize;
uniform float _blurSize;
static const half curve[4] = { 0.0205, 0.0855, 0.232, 0.324};
static const half4 coordOffs = half4(1.0h,1.0h,-1.0h,-1.0h);
定义一个顶点结构体,包含一个四维数数组用来存储采样坐标,这样可以存储6个采样坐标再加上一个二维数 uv共七个:
struct v2f_withBlurCoordsSGX
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half4 offs[3] : TEXCOORD1;
};
v2f_withBlurCoordsSGX vertBlurHorizontalSGX (appdata_img v)
{
v2f_withBlurCoordsSGX o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;
half2 netFilterWidth = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _blurSize;
half4 coords = -netFilterWidth.xyxy * 3.0;
o.offs[0] = v.texcoord.xyxy + coords * coordOffs;
coords += netFilterWidth.xyxy;
o.offs[1] = v.texcoord.xyxy + coords * coordOffs;
coords += netFilterWidth.xyxy;
o.offs[2] = v.texcoord.xyxy + coords * coordOffs;
return o;
}
v2f_withBlurCoordsSGX vertBlurVerticalSGX (appdata_img v)
{
v2f_withBlurCoordsSGX o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;
half2 netFilterWidth = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _blurSize;
half4 coords = -netFilterWidth.xyxy * 3.0;
o.offs[0] = v.texcoord.xyxy + coords * coordOffs;
coords += netFilterWidth.xyxy;
o.offs[1] = v.texcoord.xyxy + coords * coordOffs;
coords += netFilterWidth.xyxy;
o.offs[2] = v.texcoord.xyxy + coords * coordOffs;
return o;
}
Frag函数比较简单只需要用顶点函数计算的坐标采样图片乘以权重进行累加:
half4 fragBlurSGX ( v2f_withBlurCoordsSGX i ) : SV_Target
{
half2 uv = i.uv;
half4 color = tex2D(_MainTex, i.uv) * curve[3];
for( int l = 0; l < 3; l++ )
{
half4 tapA = tex2D(_MainTex, i.offs[l].xy);
half4 tapB = tex2D(_MainTex, i.offs[l].zw);
color += (tapA + tapB) * curve[l];
}
return color;
需要注意的是,该shader里面需要定义两个pass,分别是水平方向和垂直方向的模糊计算,方便C#脚本调用。
shader完整代码
Shader "PengLu/ImageEffect/Unlit/GaussianBlur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
uniform half4 _MainTex_TexelSize;
uniform float _blurSize;
// weight curves
static const half curve[4] = { 0.0205, 0.0855, 0.232, 0.324};
static const half4 coordOffs = half4(1.0h,1.0h,-1.0h,-1.0h);
struct v2f_withBlurCoordsSGX
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
half4 offs[3] : TEXCOORD1;
};
v2f_withBlurCoordsSGX vertBlurHorizontalSGX (appdata_img v)
{
v2f_withBlurCoordsSGX o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;
half2 netFilterWidth = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _blurSize;
half4 coords = -netFilterWidth.xyxy * 3.0;
o.offs[0] = v.texcoord.xyxy + coords * coordOffs;
coords += netFilterWidth.xyxy;
o.offs[1] = v.texcoord.xyxy + coords * coordOffs;
coords += netFilterWidth.xyxy;
o.offs[2] = v.texcoord.xyxy + coords * coordOffs;
return o;
}
v2f_withBlurCoordsSGX vertBlurVerticalSGX (appdata_img v)
{
v2f_withBlurCoordsSGX o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;
half2 netFilterWidth = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _blurSize;
half4 coords = -netFilterWidth.xyxy * 3.0;
o.offs[0] = v.texcoord.xyxy + coords * coordOffs;
coords += netFilterWidth.xyxy;
o.offs[1] = v.texcoord.xyxy + coords * coordOffs;
coords += netFilterWidth.xyxy;
o.offs[2] = v.texcoord.xyxy + coords * coordOffs;
return o;
}
half4 fragBlurSGX ( v2f_withBlurCoordsSGX i ) : SV_Target
{
half2 uv = i.uv;
half4 color = tex2D(_MainTex, i.uv) * curve[3];
for( int l = 0; l < 3; l++ )
{
half4 tapA = tex2D(_MainTex, i.offs[l].xy);
half4 tapB = tex2D(_MainTex, i.offs[l].zw);
color += (tapA + tapB) * curve[l];
}
return color;
}
ENDCG
SubShader {
ZTest Off ZWrite Off Blend Off
Pass {
ZTest Always
CGPROGRAM
#pragma vertex vertBlurVerticalSGX
#pragma fragment fragBlurSGX
ENDCG
}
Pass {
ZTest Always
CGPROGRAM
#pragma vertex vertBlurHorizontalSGX
#pragma fragment fragBlurSGX
ENDCG
}
}
FallBack Off
}
C#脚本完整代码
C#脚本同样比较简单,和均值模糊的脚本类似,将模糊的迭代的次数固定为2,只开放了模糊半径参数,由于将二维换成了两次一维计算,因此多调用了两次pass,drawcall也比上个例子均值模糊多两个。这里只放出关键代码;
void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture)
{
if(BlurSize != 0 && GaussianBlurShader != null){
int rtW = sourceTexture.width/8;
int rtH = sourceTexture.height/8;
RenderTexture rtTempA = RenderTexture.GetTemporary (rtW, rtH, 0, sourceTexture.format);
rtTempA.filterMode = FilterMode.Bilinear;
Graphics.Blit (sourceTexture, rtTempA);
for(int i = 0; i < 2; i++){
float iteraionOffs = i * 1.0f;
material.SetFloat("_blurSize",BlurSize+iteraionOffs);
//vertical blur
RenderTexture rtTempB = RenderTexture.GetTemporary (rtW, rtH, 0, sourceTexture.format);
rtTempB.filterMode = FilterMode.Bilinear;
Graphics.Blit (rtTempA, rtTempB, material,0);
RenderTexture.ReleaseTemporary(rtTempA);
rtTempA = rtTempB;
//horizontal blur
rtTempB = RenderTexture.GetTemporary (rtW, rtH, 0, sourceTexture.format);
rtTempB.filterMode = FilterMode.Bilinear;
Graphics.Blit (rtTempA, rtTempB, material,1);
RenderTexture.ReleaseTemporary(rtTempA);
rtTempA = rtTempB;
}
Graphics.Blit(rtTempA, destTexture);
RenderTexture.ReleaseTemporary(rtTempA);
}
else{
Graphics.Blit(sourceTexture, destTexture);
}
}
这里需要注意的下面这个函数,函数中的最后那个参数0,表示是取shader的第1个pass,依此类推;默认是-1,则表示取shader所有的pass。
Graphics.Blit (rtTempA, rtTempB, material,0);
本例实现效果如下:
总结
从图可以看出本例的效果比之前的均值模糊效果要好太多,之所以有这样的结果,主要是因为本例图像滤波器的采样阶数达到了7阶,相当于每个像素采样了49个顶点(但只花了14次采样,不包括迭代),而上篇只是一个3阶的缩水版的图像滤波器(实际只采样了四次,不包括迭代),因此效果差也成了必然,实际上在移动平台上,如果要求不太高,可以将7阶采样降为5阶就足够了,取消掉迭代,效果也可以达到上篇均值模糊的效果,甚至还要稍微好一些,而且计算量相差无几,,一个14次采样,一个8次(迭代了两次)采样,而drawcall可以降到3次。本例也不是严格的高斯模糊,只能算是近似高斯模糊,实际上,权重曲线我们也可以换成其他的曲线,,只要按照离原像素越远,权重越低的原则即可,为了效率经常会使用一些近似权重矩阵采样计算,如:
[1 2 1]
1/16* [2 4 2]
[1 2 1]
[1 2 3 2 1]
[2 3 4 3 2]
1/65*[3 4 5 4 3]
[2 3 4 3 2]
[1 2 3 2 1]