高斯模糊的细枝末节

目录

1.概述

2.数学推导

2-1正态分布

2-2 正态分布试验

2-3 正态分布的数学描述

2-4 从正态分布到高斯模糊

2-5 采样计算

 2-6 二维高斯模糊

 2-7 简化二维采样

 3.代码部分

3-1 程序思路

 3-2 C#部分

 3-3 shader部分

4.参考文献 


1.概述

高斯模糊是非常常见的一种图像模糊方式,简单说就是将每个像素点和周围像素点,以某种方式进行混合, 实现既不改变原图的大致形态,看上去还是原来的样子,但却模糊了的效果,如图1-1所示. 查阅很多资料,发现大多数都是对高斯模糊侃侃而谈,但是没有讲到数学计算的细节,算子到底是如何得到的. 高斯模糊涉及到了数学概率的正态分布, 有一点点的复杂度. 在此我们就是要从“0”开始说清楚这个高斯模糊的来龙去脉,前世今生.

2.数学推导

2-1正态分布

 2-1是人类的智商分布比例,大多数人的智商集中在85-115,超高和超低智商的人只占很小的比例,柱状图可用一条曲线拟合,如图中红色曲线所示这个钟形曲线就是正态分布曲线. 正态分布曲线体现了宇宙中很多事物的分布规律,比如全国身高的分布,年级考试成绩的分布,气体分子运动速度的分布,抛10个硬币1000次正面向上的次数分布,等等都符合正态分布.

2-2 正态分布试验

 正态分布,可描述某事件大量发生时的分布规律,或某事件单独发生的可能性,也就是概率. 事件发生后,可用统计解释,发生前可用概率预测2-2的装置叫做高尔顿钉板,可验证大量重复试验的结果符合正态分布小球从顶部落下,会经过很多钉子,每碰到一个钉子,小球弹向左边或右边的概率都是1/2,落在最左边或者最右边的概率非常低,落在中间的概率最高,所以可以用正态分布描述小球落入区间的分布.

2-3 正态分布的数学描述

 

正态分布用数学公式表达就是

 曲线表示概率密度变化,x轴数值表示试验结果,函数值是这个结果对应的概率, 比如某人智商是x=130的概率是f(x),或者说人类中智商是x=130的所占的比例是𝑓𝑥.. 曲线积分(曲线和x轴围成的面积)等于1,即所有可能的情况(x的取值值)的概率和(求积分)是1,比如某人的智商是

x∈[0-150](全部可能性)的概率是1.

𝑥=μ,对应钟形曲线最高点,μx的概率加权平均值,叫期望E(X),期望就是平均值,比如智商统计的曲线中μ=100,那么人类的平均智商就是100.  μ=0时,平均值就是0,如图.小明和小红的智商是90110,平均值是:(90+110)/2=100

𝜎2σ^2的平方是方差,即

 即所有x与平均值的差,平方,求和,最后平均方差表示样本的平均波动大小,比如小明和小红的智商是90110,平均值是100,方差是[(90-100)^2+(110-100)^2 ]/2=100,再比如小明和小红的智商是50150,平均值也是100,但是方差(波动性)要大得多, [(50-100)^2+(150-100)^2 ]/2=2500

2-4 从正态分布到高斯模糊

图像的模糊算法有很多,比如有:

均值模糊:将中心像素和周围像素颜色数值加起来求平均,作为中心像素的模糊结果

中值模糊:把中心像素和周围像素的颜色排个顺序,取中间像素的颜色数值作为模糊结果

高斯模糊:中间像素和周围像素对模糊的权重(重要性)不一样,且权重遵循正态分布,进行加权平均. 换句话说,高斯模糊就是参与模糊的像素(中间像素和周围像素)的重要性,符合正态分布,所以按照正态分布的规律加权平均就是最后的模糊数值,中间像素权重(百分比)最大,越往边上,权重(百分比)越小,且所有的权重(百分比)的和应该是1,即100%. 正态分布又叫做高斯分布,所以这种模糊又叫高斯模糊. 中间像素的权重最大,对应钟形曲线最高点,左右像素的权重对称相等,所以𝜇μ=0 ,曲线关于y轴对称;𝜎σ决定了曲线的陡峭程度,𝜎=1σ=1的曲线相对平滑,那么模糊也会相对平滑高斯模糊的像素权重分布,采用的正态分布函数如下:

2-5 采样计算

 

正态分布是连续的,纹理采样点是离散问题有限的,对于每个纹素,不可能采样整张图像,按照正态分布加权平均,最后生成该纹素点的模糊值出于效率考虑,假设在目标纹素左右,以Δx为间隔,各采样2个点,包括中心的目标纹素共5个点,如图2-5.  那么应该将这5个点,映射在正态分布多大的范围内,来取得权重呢?正态分布的样本空间是[-∞,+∞],  图2-5中,当 x∈[μ-2σ,μ+2σ] 时,概率(权重)总和已经达到95%, 接近100%,再靠边缘的样本对最后结果的贡献非常非常小, 所以仅需要在正态分布的[μ-2σ,μ+2σ]区间中计算权重即可.

       当分布函数为

μ=0σ=1时,区间-2,+2]权重占到95%. 那么5个纹理采样点,映射到正态分布的区间[-2,+2]上,得到正态分布曲线的x值为-2-1012,得到5个纹理采样点对应的5个权重值,或者说这5个纹素对于目标纹素点模糊的贡献比例为:

 

但这些权重加起来不是1,而是

 

[-2,+2]区间,权重总和不是应该占95%吗,怎么是99%呢?这是因为,这个95%是积分计算的结果,而此处的结果是离散数据计算的.如图2-5-2

 

 

 2-6 二维高斯模糊

图像是二维的,那么模糊权重也应该是二维的,所以将正态分布从一维扩展到二维,公式为:

 3D 图像如图2-6所示. 二维的高斯算子,是x,y两个参数共同决定的, 离远点越近,权重值越大如果在目标纹素周围5*5的范围采样,需要把这25个纹素采样点映射到x∈[-2,+2],y∈[-2,+2]的区间内的25个(x,y)坐标,计算得到25个权重,再对25个纹理采样颜色分别乘以权重,得到加权求平均值,即最终的高斯模糊颜色,如图2-7所示.

 

 2-7 简化二维采样

如果一张图像的尺寸是w*h,上述二维高斯核需要25*w*h次采样并计算,如何才能降低计算成本,又不必大幅度损失效果呢? 可以简化采样方式,采用2个方向的一维高斯核采样,如图2-8,这样仅需要5*w*h+5*w*h次采样,垂直水平方向分别进行一次高斯模糊的计算,既提高了效率,又不至于效果太差后续的代码实现中也将采用这种简化的二维高斯核计算权重注意,此处的一维权重是使用图2-5中的归一化的一维权重,因为此时是用2个方向的一维权重(不是二维权重图中9/25的那些部分),分别执行一次独立运算(但效果是叠加的).

 3.代码部分

3-1 程序思路

高斯模糊的实现分成两个部分,c#shader.  因为这是一种图像后处理,意思就是先要渲染场景,等到场景渲染结束,变成一张纹理(图像),然后再对这张图象进行处理.  场景渲染结束后会调用我们的c#函数,该函数再调用shader中的pass,进行真正的模糊处理. 因为pass是用GPU计算的,速度不是一个等级,这也是调来调去的原因所在.

具体到实现中,此处使用Unity提供的C#接口:

  MonoBehaviour.OnRenderImage(RenderTextrue src,RenderTextrue dest)

该函数需要在继承MonoBehaviour的脚本中定义,并且必须将脚本绑定到摄像机. OnRenderImage函数会在所有不透明和透明Pass执行完毕后调用如果希望不透明Pass(即渲染你队列<=2500Pass,包括内置Background,Geometry,AlphaTest)执行完毕立刻调用函数,可在函数定义中加[ImageEffectOpaque]特性来实现.

       屏幕图像将传入OnRenderImage的参数src,再通过Graphics.Blit函数调用特定Shader来处理,再把处理的结果放在dest参数中,最后显示到屏幕上, 在一些复杂的屏幕特效中,可能会多次调用Graphics.Blit函数.调用过程如图3-1

using UnityEngine;

[ExecuteInEditMode]//编辑模式也可以看到效果
[RequireComponent(typeof(Camera))]//只能绑定到有Camera脚本的GameObject上
public class GuassBlur : MonoBehaviour
{
	public Shader edgeDetectShader; //调用的材质球
	private Material edgeDetectMaterial = null;//该材质球动态生成

	//高斯模糊的迭代次数,次数越大越模糊.
	[Range(0, 4)]
	public int iterations = 3;
	//模糊范围,每次模糊处理的采样范围为  1+ 迭代次数 * blurSpread
	[Range(0.2f, 3.0f)]
	public float blurSpread = 0.6f;
	//降采样数,图像尺寸变为原来的(1/downSample,1/downSample)进行模糊,降低太多容易有马赛克感
	[Range(1, 8)]
	public int downSample = 2;
	public Material material
	{
		get
		{
			edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
			return edgeDetectMaterial;
		}
	}
	void OnRenderImage(RenderTexture src, RenderTexture dest)
	{
		if (material != null)
		{
			//计算过降低后的图像尺寸
			int rtW = src.width / downSample;
			int rtH = src.height / downSample;
			//new缓存
			RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
			//原图像和降采样后图像之间使用双线性滤波转换,本身也会带来一定的模糊效果
			buffer0.filterMode = FilterMode.Bilinear;
			//原图-> 小图
			Graphics.Blit(src, buffer0);
			//进行iterations次高斯模糊处理
			for (int i = 0; i < iterations; i++)
			{
				//设置采样间隔
				material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
				RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

				//调用第0个pass,进行垂直方向的高斯模糊
				Graphics.Blit(buffer0, buffer1, material, 0);

				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
				buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

				//调用第1个pass,进行垂直方向的高斯模糊
				Graphics.Blit(buffer0, buffer1, material, 1);

				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
			}
			//高斯模糊的小图 ->  大图
			Graphics.Blit(buffer0, dest);
			RenderTexture.ReleaseTemporary(buffer0);
		}
		else
		{
			Graphics.Blit(src, dest);
		}
	}
	//检查、生成、返回材质球
	protected Material CheckShaderAndCreateMaterial(Shader shader, Material material)
	{
		if (shader == null)
		{
			return null;
		}
		if (shader.isSupported && material && material.shader == shader)
			return material;
		if (!shader.isSupported)
		{
			return null;
		}
		else
		{
			material = new Material(shader);
			material.hideFlags = HideFlags.DontSave;
			if (material)
				return material;
			else
				return null;
		}
	}
}

 3-2 C#部分

 3-3 shader部分

Shader "MyShader/PostEffect/GuassBlur" {
	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_BlurSize("Blur Size", Float) = 1.0
	}
		SubShader{
			CGINCLUDE

			#include "UnityCG.cginc"

			sampler2D _MainTex;
			half4 _MainTex_TexelSize;//单位纹素大小
			float _BlurSize;//采样间隔

			struct v2f {
				float4 pos : SV_POSITION;
				half2 uv[5]: TEXCOORD0;
			};
			//顶点着色-垂直模糊Pass
			v2f vertBlurVertical(appdata_img v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);

				half2 uv = v.texcoord;
				//设置垂直方向的采样坐标
				o.uv[0] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
				o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
				o.uv[2] = uv;
				o.uv[3] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
				o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
				return o;
			}
			//顶点着色-水平模糊Pass
			v2f vertBlurHorizontal(appdata_img v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);

				half2 uv = v.texcoord;
				//设置水平方向的采样坐标
				o.uv[0] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
				o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
				o.uv[2] = uv;
				o.uv[3] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
				o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;

				return o;
			}
			//片元着色
			fixed4 fragBlur(v2f i) : SV_Target {
				//高斯模糊权重
				float weight[5] = { 0.0545,0.2442,0.4026, 0.2442, 0.0545};

				//中心纹理*权重
				fixed3 sum = 0;
				//上下/左右 的2个纹理*权重
				for (int n = 0; n < 5; n++) {
					sum += tex2D(_MainTex, i.uv[n]).rgb * weight[n];
				}
				return fixed4(sum, 1.0);
			}

			ENDCG

			ZTest Always Cull Off ZWrite Off

			Pass {
				NAME "GAUSSIAN_BLUR_VERTICAL"

				CGPROGRAM

				#pragma vertex vertBlurVertical  
				#pragma fragment fragBlur

				ENDCG
			}

			Pass {
				NAME "GAUSSIAN_BLUR_HORIZONTAL"

				CGPROGRAM

				#pragma vertex vertBlurHorizontal  
				#pragma fragment fragBlur

				ENDCG
			}
		}
			FallBack Off
}

4.参考文献 

[1] 《UnityShader 入门精要

[2]   A First Course in Probility (9e) 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值