边缘检测效果,是一种用于突出图像中的边缘,使物体的轮廓更加明显的图像处理技术,边缘检测的主要目的是找到图像中亮度变化显著的区域,这些区域通常对应于物体的边界,边缘检测相当于利用 Shader 代码自动给屏幕图像进行描边处理
1、边缘检测基本原理
计算每个像素的灰度值,用灰度值结合卷积核进行卷积运算,得到该像素的梯度值,梯度值越大越靠近边界,越趋近于描边颜色;梯度值越小表明不是边界位置,越趋近于原始颜色
关键知识点:灰度值、卷积、卷积核、梯度值
灰度值
由于人眼对不同颜色的敏感度不同,所以在计算平均值时不会直接使用算数平均(R+G+B)/3
在图形学中我们一般使用加权平均法来计算灰度值
所谓加权平均法就是通过对不同数据分配不同权重,计算出更符合实际情况的平均值
下面是基于 Rec. 709标准 计算的灰度值(高清电视和许多数字图像格式中常用的标准)
灰度值 L = 0.2126*R + 0.7152*G + 0.0722*B
卷积、卷积核、梯度值
卷积是一种数学计算方式,首先通过一个比喻来理解卷积在边缘检测中的作用,它就像是要用一个放大镜(卷积核)在图片上移动,放大镜(卷积核)的作用是帮助我们看到图,片上的细微变化。当我们用这个放大镜(卷积核)扫描整张图片时,它能帮助我们发现图片上哪些地方颜色变化突然,这些突然变化的地方往往就是物体的边缘了
假设我们有一张 5x5 的图像,每一个格子代表一个像素,格子中的数据表示该像素的灰度值,假设我们有一个 3x3 的卷积核(放大镜)
如果你想要求出图中红色格子的梯度值(值越大表示越靠近边缘),如果你想要求出图中红色格子的梯度值(值越大表示越靠近边缘)进行如下计算:
最终算出来的结果就表示该像素的梯度值,我们便可以用该值决定边缘效果了
从卷积的计算方式我们可以得知,其中**卷积核(也被称为边缘检测因子)**是非常重要的一个元素,在图形学中,有三种常用的卷积核(边缘检测因子),他们分别是:
- Roberts 算子:由拉里·罗伯茨(Larry Roberts)于1965年提出
- Prewitt 算子:由约翰·普雷维特(John Prewitt)于1970年提出
- Sobel 算子:由欧文·索贝尔(Irwin Sobel)于1968年提出
图形学中最常用的还是Sobel算子,因为它更适合高精度的边缘检测
可以看到三种算子都包含了两个方向的卷积核,他们分别用来检测水平和竖直方向上的边缘信息
在边缘检测的卷积计算 时,只需要对每个像素进行两次卷积计算 即可,这样就可以得到两个方向的梯度值 Gx 和 Gy, 而该像素的整体梯度值 G = abs(Gx) + abs(Gy)
已经知道了通过卷积获取梯度值的基本原理那么我们只需要定义一个描边颜色,利用Shader中的内置函数lerp,在原始颜色和描边颜色之间利用梯度值进行插值即可
最终颜色 = lerp(原始颜色,描边颜色,梯度值)
梯度值越大表明越接近边缘,则颜色越接近描边颜色;反之越接近原始颜色
2、如何得到当前像素周围8个像素位置
想要获取当前像素周围8个像素的位置,我们需要Unity 提供给我们用于访问纹理对应的每个纹素(像素)的大小 的变量------float4 纹理名_TexelSize
类似 纹理名_ST(用于获取纹理缩放偏移的变量)
其中的xyzw分别代表(假设纹理宽高为 1024 * 768)
- x:1/纹理宽度 = 1/1024
- y:1/纹理高度 = 1/768
- z:纹理宽度 = 1024
- w:纹理高度 = 768
可以利用 float4 纹理名_TexelSize 纹素 信息得到当前像素周围8个像素位置,可以进行uv坐标偏移计算,在 顶点着色器函数 或者 片元着色器函数 中计算都行,**但是建议在顶点着色器函数中计算,可以节约计算量,**片元着色器中直接使用插值的结果也不会影响纹理坐标的计算结果
3、边缘检测的实现
cs
Shader "Unlit/EdgeDetection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_EdgeColor ("EdgeColor", Color) = (0, 0, 0, 0)
_EdgeFactor ("EdgeFactor", Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
half2 uv[9] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _MainTex_TexelSize;
fixed4 _EdgeColor;
fixed _EdgeFactor;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
// 计算灰度值
fixed4 calcuminance(fixed4 color)
{
return 0.2126 * color.r + 0.71522 * color.g + 0.0722 * color.b;
}
half Sobel(v2f o)
{
half Gx[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half Gy[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
half L;
half edgeX = 0;
half edgeY = 0;
for (int i = 0; i < 9; i++)
{
// 采样后获取灰度值
L = calcuminance(tex2D(_MainTex, o.uv[i]));
edgeX += L * Gx[i];
edgeY += L * Gy[i];
}
return abs(edgeX) + abs(edgeY);
}
fixed4 frag (v2f i) : SV_Target
{
half edge = Sobel(i);
return lerp(tex2D(_MainTex, i.uv[4]), _EdgeColor, edge * _EdgeFactor);
}
ENDCG
}
}
}
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EdgeDetection : PostEffectBase
{
public Color EdgeColor;
[Range(0, 1)]
public float EdgeFactor;
protected override void UpdateProperty() {
if (material != null) {
material.SetFloat("_EdgeFactor", EdgeFactor);
material.SetColor("_EdgeColor", EdgeColor);
}
}
}
4、边缘检测的纯色背景功能
边缘描边时,有时只想保留描边的边缘线,不想要显示原图的背景颜色,比如把整个背景变为白色、黑色、等等自定义颜色,而抛弃掉原本图片的颜色信息,效果就像是一张描边图片
需要做以下:
(1)新属性声明
- 添加 背景颜色程度变量 _BackgroundExtent 0表示保留图片原始颜色,1表示完全抛弃图片原始颜色,0~1之间可以自己控制保留程度
- 添加自定义背景颜色 _BackgroundColor,定义用于替换图片原始颜色的颜色
(2)修改片元着色器
- 利用插值运算,记录纯色背景中像素描边颜色
- 利用插值运算,在 原始图片描边 和 纯色图片描边 之间用程度变量进行控制
cs
Shader "Unlit/EdgeDetection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_EdgeColor ("EdgeColor", Color) = (0, 0, 0, 0)
_EdgeFactor ("EdgeFactor", Float) = 1
_BackgroundExtent ("BackgroundExtent", Range(0, 1)) = 0
_BackgroundColor ("BackgroundColor", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
half2 uv[9] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
half4 _MainTex_TexelSize;
fixed4 _EdgeColor;
fixed _EdgeFactor;
fixed _BackgroundExtent;
fixed4 _BackgroundColor;
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
// 计算灰度值
fixed4 calcuminance(fixed4 color)
{
return 0.2126 * color.r + 0.71522 * color.g + 0.0722 * color.b;
}
half Sobel(v2f o)
{
half Gx[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half Gy[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
half L;
half edgeX = 0;
half edgeY = 0;
for (int i = 0; i < 9; i++)
{
// 采样后获取灰度值
L = calcuminance(tex2D(_MainTex, o.uv[i]));
edgeX += L * Gx[i];
edgeY += L * Gy[i];
}
return abs(edgeX) + abs(edgeY);
}
fixed4 frag (v2f i) : SV_Target
{
half edge = Sobel(i);
fixed4 withEdgeColor = lerp(tex2D(_MainTex, i.uv[4]), _EdgeColor, edge * _EdgeFactor);
fixed4 onlyEdgeColor = lerp(_BackgroundColor, _EdgeColor, edge * _EdgeFactor);
return lerp(withEdgeColor, onlyEdgeColor, _BackgroundExtent);
}
ENDCG
}
}
}
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EdgeDetection : PostEffectBase
{
public Color EdgeColor;
public Color BackgroundColor;
[Range(0, 1)]
public float EdgeFactor;
[Range(0, 1)]
public float BackgroundExtent;
protected override void UpdateProperty() {
if (material != null) {
material.SetFloat("_EdgeFactor", EdgeFactor);
material.SetFloat("_BackgroundExtent", BackgroundExtent);
material.SetColor("_EdgeColor", EdgeColor);
material.SetColor("_BackgroundColor", BackgroundColor);
}
}
}