常见模糊算法
图像模糊算法在后处理中有着重要的地位,许多产品级后处理表现都会直接或间接使用模糊算法。在以下项目中将使用Cg代码实现部分模糊算法(均值模糊、高斯模糊、Kawase模糊、双向均值/高斯模糊)。
参考网站: 一、高品质后处理:十种图像模糊算法的总结与实现
均值模糊
图像模糊原理,是使用低通滤波器,过滤图像的高频信息保留低频信息。对图像进行模糊处理。一种常见的方法就是使用卷积滤波器。
算法原理
将数字图像视作一个矩阵,每一个像素都是矩阵上的一点。现在我们使用一个矩阵,获取图像上每一个像素附近的像素,这一个矩阵就称之为卷积核。将卷积核中的像素的颜色值进行加权平均,就能得到一个颜色相对平均的图像,从而达到模糊处理的效果。根据卷积核的不同,可以将均值模糊分为3x3的模糊与2x2的模糊。
2x2卷积核
使用当前像素作为中心的九宫格区域,取左上、左下、右上、右下四格像素的数值进行加权平均,就是2x2的均值模糊的原理。
half4 frag (v2f i) : SV_Target
{
half col = 0;
//uv坐标采样加上像素偏移值。分别向左上、右上、左下、右下方向进行偏移。
//定义变量_BlurOffset控制偏移大小
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(1,1) * _BlurOffset;
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(-1,1) * _BlurOffset;
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(1,-1) * _BlurOffset;
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(-1,-1) * _BlurOffset;
//平均
col *=0.25;
return col;
}
3x3卷积核
原理与2x2卷积核类似,使用3x3大小的卷积核进行卷积操作即可得出。
half4 frag_testTap9 (v2f_img i) : SV_Target
{
half4 col = 0;
//uv坐标采样加上像素偏移值。分别向左上、右上、左下、右下方向进行偏移。
//定义变量_BlurOffset控制偏移大小
col += tex2D(_MainTex, i.uv);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(1,1) * _BlurOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(-1,1) * _BlurOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(1,-1) * _BlurOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(-1,-1) * _BlurOffset);
//分别向右、左、上、下进行偏移
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(1,0) * _BlurOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(-1,0) * _BlurOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(0,-1) * _BlurOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(0,1) * _BlurOffset);
//平均
col = col/9.0;
return col;
}
代码详解
将上述2x2、3x3的模糊算法片段着色器分别放在不同的pass中,绑定材质和脚本文件。通过OnRenderImage()函数,使用Graphics.Blit()分别调用以上着色器实现不同的算法。
public enum PassEnum
{
//定义枚举类用于执行不同的模糊算法
//实现的滤波器
Tap4BoxFilter,
Tap9BoxFilter
}
public Material material;
[Range(1, 10)]
public int _Iteration = 4; //定义迭代次数
[Range(0, 7)]
public float blurOffset = 1.0f; //定义偏移值
public float downSample = 1.0f; //定义图像降采样值
public PassEnum thePass; //定义选用的模糊算法
.....
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
int width = (int)(source.width / downSample);
int height = (int)(source.height / downSample);
//设置shader的参数
material.SetFloat("_BlurOffset", blurOffset);
RenderTexture rt1 = RenderTexture.GetTemporary(width, height);
RenderTexture rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(source, rt1);
//定义枚举类PassEnum,判断使用的模糊算法
if (thePass == PassEnum.Tap4BoxFilter)
{
//迭代
for (int i = 0; i < _Iteration; i++)
{
Graphics.Blit(rt1, rt2, material, 0);
Graphics.Blit(rt2, rt1, material, 0);
}
}
if (thePass == PassEnum.Tap9BoxFilter)
{
for (int i = 0; i < _Iteration; i++)
{
Graphics.Blit(rt1, rt2, material, 1);
Graphics.Blit(rt2, rt1, material, 1);
}
}
//将最后结果传递到destination
Graphics.Blit(rt1, destination);
//释放资源
RenderTexture.ReleaseTemporary(rt1);
RenderTexture.ReleaseTemporary(rt2);
}
高斯模糊
高斯模糊是最经典的模糊算法之一。与均值模糊类似,高斯模糊同样使用卷积核对图像进行卷积处理。只是卷积核换成了高斯卷积核。
算法原理
高斯模糊实际上是使用了正态分布对图像进行卷积。因为正态分布别称高斯分布,故称高斯模糊。高斯模糊使用5x5的矩阵,模拟正态分布进行加权卷积计算。得出的模糊效果比均值模糊更平滑。有关高斯模糊原理,详细请见高斯模糊原理
一个典型的高斯核↑
5x5大小的卷积核太大,大量消耗算力,可以将高斯卷积核进行线性分解,拆解成 和 两个条状卷积核,纵向、横向各进行一次卷积处理即可达成高斯卷积核的效果。
代码详解
half4 frag_HorizontalBlur(v2f i) : SV_Target
{
half2 uv1 = i.uv + _MainTex_TexelSize.xy * _BlurOffset * half2(1, 0) * -2.0;
half2 uv2 = i.uv + _MainTex_TexelSize.xy * _BlurOffset * half2(1, 0) * -1.0;
half2 uv3 = i.uv;
half2 uv4 = i.uv + _MainTex_TexelSize.xy * _BlurOffset * half2(1, 0) * 1.0;
half2 uv5 = i.uv + _MainTex_TexelSize.xy * _BlurOffset * half2(1, 0) * 2.0;
half4 col = 0;
col += tex2D(_MainTex, uv1) * 0.05;
col += tex2D(_MainTex, uv2) * 0.25;
col += tex2D(_MainTex, uv3) * 0.40;
col += tex2D(_MainTex, uv4) * 0.25;
col += tex2D(_MainTex, uv5) * 0.05;
return col;
}
half4 frag_VerticalBlur(v2f i) : SV_Target
{
half2 uv1 = i.uv + _MainTex_TexelSize.xy * _BlurOffset * half2(0, 1) * -2.0;
half2 uv2 = i.uv + _MainTex_TexelSize.xy * _BlurOffset * half2(0, 1) * -1.0;
half2 uv3 = i.uv;
half2 uv4 = i.uv + _MainTex_TexelSize.xy * _BlurOffset * half2(0, 1) * 1.0;
half2 uv5 = i.uv + _MainTex_TexelSize.xy * _BlurOffset * half2(0, 1) * 2.0;
half4 col = 0;
col += tex2D(_MainTex, uv1) * 0.05;
col += tex2D(_MainTex, uv2) * 0.25;
col += tex2D(_MainTex, uv3) * 0.40;
col += tex2D(_MainTex, uv4) * 0.25;
col += tex2D(_MainTex, uv5) * 0.05;
return col;
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
......
if (thePass == PassEnum.GaussainFilter)
{
for (int i = 0; i < _Iteration; i++)
{
//分别调用横向、竖向的两个卷积核进行渲染
Graphics.Blit(rt1, rt2, material, 2);
Graphics.Blit(rt2, rt1, material, 3);
}
}
}
Kawase模糊
相较于高斯模糊、均值模糊等的“恒定卷积核”,使用“动态卷积核”来提升效率是一种很自然朴素的想法。所以,Kawase模糊被提了出来。
Kawase Blur的思路是对距离当前像素越来越远的地方对四个角进行采样,且在两个大小相等的纹理之间进行乒乓式的blit。创新点在于,采用了随迭代次数移动的blur kernel,而不是类似高斯模糊,或box blur一样从头到尾固定的blur kernel。
直接上代码
half4 frag_KawaseBlur(v2f i) : SV_Target
{
half4 col = 0;
col += tex2D(_MainTex, i.uv);
col += tex2D(_MainTex, i.uv + half2(1, 1) * _KawaseRange * _MainTex_TexelSize);
col += tex2D(_MainTex, i.uv + half2(1, -1) * _KawaseRange * _MainTex_TexelSize);
col += tex2D(_MainTex, i.uv + half2(-1, 1) * _KawaseRange * _MainTex_TexelSize);
col += tex2D(_MainTex, i.uv + half2(-1, -1) * _KawaseRange * _MainTex_TexelSize);
col = col*0.2;
return col;
}
if (thePass == PassEnum.KawaseFilter)
{
material.SetFloat("_KawaseRange", 0);
for(int i = 0; i < _Iteration; i++)
{
//设置KawaseRange用于在迭代过程中动态递进采样点
material.SetFloat("_KawaseRange", (i + 1) * kawaseRange);
Graphics.Blit(rt1, rt2, material, 3);
//置换rt1与rt2
RenderTexture temp = RenderTexture.GetTemporary(width, height);
temp = rt2;
rt2 = rt1;
rt1 = temp;
RenderTexture.ReleaseTemporary(temp);
}
}
我们可以看到,相比高斯模糊与均值模糊,Kawase模糊的每一次迭代只需调用一次Blit()函数。通过Unity自带的Frame Debugger进行检查,可以发现Kawase模糊调用Pass的次数更少的情况下,就能达到高斯模糊、均值模糊的模糊效果。也就是说,Kawase模糊计算性能更好。使用Kawase模糊,在一定程度上起到了性能优化的效果。
双重模糊
算法原理
Dual Kawase Blur,简称Dual Blur,是SIGGRAPH 2015上ARM团队提出的一种衍生自Kawase Blur的模糊算法。
相较于Kawase Blur在两个大小相等的纹理之间进行乒乓blit的的思路,Dual Kawase Blur的核心思路在于blit过程中进行降采样和升采样,即对RT进行了降采样以及升采样。
代码详解
还是直接上代码。以下是双重高斯模糊
if (thePass == PassEnum.DoubleGaussain)
{
//降采样
for (int i = 0; i < _Iteration; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width / 2;
height = height / 2;
rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt1, rt2, material, 2);
RenderTexture.ReleaseTemporary(rt1);
width = width / 2;
height = height / 2;
rt1 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt2, rt1, material, 3);
}
//升采样
for (int i = 0; i < _Iteration; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width * 2;
height = height * 2;
rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt1, rt2, material, 2);
RenderTexture.ReleaseTemporary(rt1);
width = width * 2;
height = height * 2;
rt1 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt2, rt1, material, 3);
}
}
双重模糊只是一个模板,他可以是双重高斯模糊、双重均值模糊、双重Kawase模糊等。只需调整Blit()中调用的片段着色器即可。
项目源码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode()]
public class BlurEffect : MonoBehaviour
{
public Material material;
[Range(1, 10)]
public int _Iteration = 4;
[Range(0, 7)]
public float blurOffset = 1.0f;
[Range(1, 15)]
public float downSample = 5.0f;
public PassEnum thePass;
[Range(0, 5)]
public float kawaseRange = 1.0f;
[Range(0, 7)]
public float test = 1.5f;
// Start is called before the first frame update
void Start()
{
if(material == null
|| material.shader == null || material.shader.isSupported == false)
{
enabled = false;
return;
}
}
// Update is called once per frame
void Update()
{
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//use the Screen width + height.
int width = (int)(source.width / downSample);
int height = (int)(source.height / downSample);
//Performance Optimization.set the Vertex Shader caculating(GPU) to C# caculating(CPU)
//性能优化:将在GPU(顶点着色器)中计算的内容放在(CPU)C#脚本上进行计算提升性能
material.SetVector("_BlurOffset", new Vector4(blurOffset / width, blurOffset / height, 0, 0));
material.SetFloat("_TestOffset", test);
RenderTexture rt1 = RenderTexture.GetTemporary(width, height);
RenderTexture rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(source, rt1);
if (thePass == PassEnum.Tap4BoxFilter)
{
for (int i = 0; i < _Iteration; i++)
{
Graphics.Blit(rt1, rt2, material, 0);
Graphics.Blit(rt2, rt1, material, 0);
}
}
if (thePass == PassEnum.Tap9BoxFilter)
{
for (int i = 0; i < _Iteration; i++)
{
Graphics.Blit(rt1, rt2, material, 1);
Graphics.Blit(rt2, rt1, material, 1);
}
}
if (thePass == PassEnum.GaussainFilter)
{
for (int i = 0; i < _Iteration; i++)
{
Graphics.Blit(rt1, rt2, material, 2);
Graphics.Blit(rt2, rt1, material, 3);
}
}
if (thePass == PassEnum.KawaseFilter)
{
material.SetFloat("_KawaseRange", 0);
for(int i = 0; i < _Iteration; i++)
{
material.SetFloat("_KawaseRange", (i + 1) * kawaseRange);
Graphics.Blit(rt1, rt2, material, 4);
RenderTexture temp = RenderTexture.GetTemporary(width, height);
temp = rt2;
rt2 = rt1;
rt1 = temp;
RenderTexture.ReleaseTemporary(temp);
//Graphics.Blit(rt2, rt1, material, 4);
}
}
if (thePass == PassEnum.DoubleTap4)
{
for (int i = 0; i < _Iteration; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width / 2;
height = height / 2;
rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt1, rt2, material, 0);
RenderTexture.ReleaseTemporary(rt1);
width = width / 2;
height = height / 2;
rt1 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt2, rt1, material, 0);
}
for (int i = 0; i < _Iteration; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width * 2;
height = height * 2;
rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt1, rt2, material, 0);
RenderTexture.ReleaseTemporary(rt1);
width = width * 2;
height = height * 2;
rt1 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt2, rt1, material, 0);
}
}
if (thePass == PassEnum.DoubleTap9)
{
for (int i = 0; i < _Iteration; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width / 2;
height = height / 2;
rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt1, rt2, material, 1);
RenderTexture.ReleaseTemporary(rt1);
width = width / 2;
height = height / 2;
rt1 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt2, rt1, material, 1);
}
for (int i = 0; i < _Iteration; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width * 2;
height = height * 2;
rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt1, rt2, material, 1);
RenderTexture.ReleaseTemporary(rt1);
width = width * 2;
height = height * 2;
rt1 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt2, rt1, material, 1);
}
}
if (thePass == PassEnum.DoubleGaussain)
{
//降采样
for (int i = 0; i < _Iteration; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width / 2;
height = height / 2;
rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt1, rt2, material, 2);
RenderTexture.ReleaseTemporary(rt1);
width = width / 2;
height = height / 2;
rt1 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt2, rt1, material, 3);
}
//升采样
for (int i = 0; i < _Iteration; i++)
{
RenderTexture.ReleaseTemporary(rt2);
width = width * 2;
height = height * 2;
rt2 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt1, rt2, material, 2);
RenderTexture.ReleaseTemporary(rt1);
width = width * 2;
height = height * 2;
rt1 = RenderTexture.GetTemporary(width, height);
Graphics.Blit(rt2, rt1, material, 3);
}
}
Graphics.Blit(rt1, destination);
RenderTexture.ReleaseTemporary(rt1);
RenderTexture.ReleaseTemporary(rt2);
}
}
Shader "Hidden/BlurShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_KawaseRange("KawaseRange", Float) = 1
_BlurOffset("BlurOffset", Float) = 1
}
//使用CGINCLUDE,将cg语言放在前面,方便管理
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _BlurOffset;
float _KawaseRange;
float4 _MainTex_TexelSize;
half4 frag_BoxFilter_4Tap (v2f_img i) : SV_Target
{
half4 d = _BlurOffset.xyxy * half4(1,1,-1,-1);
half4 s = 0;
//采样进行像素偏移,偏移值与_BlurOffset挂钩
//计算完毕后做平均
s += tex2D(_MainTex, i.uv + d.xy);
s += tex2D(_MainTex, i.uv + d.zy);
s += tex2D(_MainTex, i.uv + d.xw);
s += tex2D(_MainTex, i.uv + d.zw);
s = s*0.25;
return s;
}
half4 frag_testTap9 (v2f_img i) : SV_Target
{
half4 col = 0;
//uv坐标采样加上像素偏移值。分别向左上、右上、左下、右下方向进行偏移。
//定义变量_BlurOffset控制偏移大小
col += tex2D(_MainTex, i.uv);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(1,1) * _TestOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(-1,1) * _TestOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(1,-1) * _TestOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(-1,-1) * _TestOffset);
//分别向右、左、上、下进行偏移
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(1,0) * _TestOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(-1,0) * _TestOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(0,-1) * _TestOffset);
col += tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy * half2(0,1) * _TestOffset);
//平均
col = col/9.0;
return col;
}
half4 frag_BoxFilter_9Tap (v2f_img i) : SV_Target
{
//half4 d = _BlurOffset.xyxy * half4(1,-1,0,0);
half4 d = _BlurOffset.xyxy * half4(-1, -1, 1, 1);
half4 s = 0;
s = tex2D(_MainTex, i.uv);
s += tex2D(_MainTex, i.uv + d.xy);
s += tex2D(_MainTex, i.uv + d.zy);
s += tex2D(_MainTex, i.uv + d.xw);
s += tex2D(_MainTex, i.uv + d.zw);
s += tex2D(_MainTex, i.uv + half2(0.0, d.w));
s += tex2D(_MainTex, i.uv + half2(0.0, d.y));
s += tex2D(_MainTex, i.uv + half2(d.z, 0.0));
s += tex2D(_MainTex, i.uv + half2(d.x, 0.0));
s = s/9.0;
return s;
}
half4 frag_HorizontalBlur(v2f_img i) : SV_Target
{
half2 uv1 = i.uv + _BlurOffset.xy * half2(1, 0) * -2.0;
half2 uv2 = i.uv + _BlurOffset.xy * half2(1, 0) * -1.0;
half2 uv3 = i.uv;
half2 uv4 = i.uv + _BlurOffset.xy * half2(1, 0) * 1.0;
half2 uv5 = i.uv + _BlurOffset.xy * half2(1, 0) * 2.0;
half4 s = 0;
s += tex2D(_MainTex, uv1) * 0.05;
s += tex2D(_MainTex, uv2) * 0.25;
s += tex2D(_MainTex, uv3) * 0.40;
s += tex2D(_MainTex, uv4) * 0.25;
s += tex2D(_MainTex, uv5) * 0.05;
return s;
}
half4 frag_VerticalBlur(v2f_img i) : SV_Target
{
half2 uv1 = i.uv + _BlurOffset.xy * half2(0, 1) * -2.0;
half2 uv2 = i.uv + _BlurOffset.xy * half2(0, 1) * -1.0;
half2 uv3 = i.uv;
half2 uv4 = i.uv + _BlurOffset.xy * half2(0, 1) * 1.0;
half2 uv5 = i.uv + _BlurOffset.xy * half2(0, 1) * 2.0;
half4 s = 0;
s += tex2D(_MainTex, uv1) * 0.05;
s += tex2D(_MainTex, uv2) * 0.25;
s += tex2D(_MainTex, uv3) * 0.40;
s += tex2D(_MainTex, uv4) * 0.25;
s += tex2D(_MainTex, uv5) * 0.05;
return s;
}
half4 frag_KawaseBlur(v2f_img i) : SV_Target
{
half4 d = half4(1,1,-1,-1);
half4 s = 0;
s += tex2D(_MainTex, i.uv);
s += tex2D(_MainTex, i.uv + d.xy * _KawaseRange * _MainTex_TexelSize.xy);
s += tex2D(_MainTex, i.uv + d.zy * _KawaseRange * _MainTex_TexelSize.xy);
s += tex2D(_MainTex, i.uv + d.xw * _KawaseRange * _MainTex_TexelSize.xy);
s += tex2D(_MainTex, i.uv + d.zw * _KawaseRange * _MainTex_TexelSize.xy);
s = s*0.2;
return s;
}
ENDCG
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass //0号Pass
{
CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag_BoxFilter_4Tap
ENDCG
}
Pass //1号Pass
{
CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag_BoxFilter_9Tap
ENDCG
}
pass //2号Pass
{
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert_img
#pragma fragment frag_HorizontalBlur
ENDCG
}
pass //3号Pass
{
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert_img
#pragma fragment frag_VerticalBlur
ENDCG
}
pass //4号Pass
{
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert_img
#pragma fragment frag_KawaseBlur
ENDCG
}
}
}