邊緣檢測(英語:Edge detection)是圖像處理和計算機視覺中的基本問題,邊緣檢測的目的是標識數字圖像中亮度變化明顯的點。圖像屬性中的顯著變化通常反映了屬性的重要事件和變化。這些包括(i)深度上的不連續、(ii)表面方向不連續、(iii)物質屬性變化和(iv)場景照明變化。 邊緣檢測是圖像處理和計算機視覺中,尤其是特徵檢測中的一個研究領域。
原理
邊緣檢測實質上就是通過對圖像的卷積的結果,卷積是數學分析中一種重要的運算,主要區別在於卷積核的選擇,通過卷積操作我們不僅可以做到邊緣檢測,還可以做到模糊圖像、銳化圖像等功能。
邊緣檢測的卷積核 也叫做 邊緣檢測算子
如:Roberts Cross算子, Prewitt算子, Sobel算子, Canny算子,羅盤算子
這裏以Sobel算子爲例子:
代表原始圖像,分別代表經橫向及縱向邊緣檢測的圖像,
通過以上公式就可以分別計算出橫向 和 縱向 的梯度值,即
梯度值越大,邊緣就越明顯。
注意:
- 計算梯度值前需要先把圖像置灰,方便我們計算梯度值
- 橫向的梯度值檢測出來的是縱向的邊緣線,縱向的梯度值檢測出來的是橫向的邊緣線。
以上是理論,可能有些小夥伴看得很懵逼,下面我們舉一個例子來說明一下:
假如上圖是我們在圖像上隨機選取的兩塊3*3的像素塊,以及橫向的Sobel算子
像素塊一的梯度值:
像素塊二的梯度值:
最後我們取他們的絕對值, 分別 187和1, 這裏我們就很明顯的看到了他們的區別,梯度值越大,說明該像素越有可能是邊緣!
Shader 代碼實現:
首先我們需要把圖像置灰,置灰非常簡單,通過灰度心理學公式 Gray = R*0.299 + G*0.587 + B*0.114
轉換即可!
這裏我們分別對橫向和縱向做邊緣檢測,以便於我們做對比,理解。
創建一個C#腳本拖拽到攝像機上:
PS: PostEffectsBase 基類可以在我的這篇文章查看
或者到我的GitHub上獲取
using UnityEngine;
using System.Collections;
//-----------------------------【邊緣檢測】-----------------------------
public class EdgeDetection : PostEffectsBase {
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null;
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f;
public Color edgeColor = Color.black;
public Color backgroundColor = Color.white;
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
shader:(橫向檢測)
//-----------------------------【邊緣檢測】-----------------------------
Shader "lcl/learnShader3/002 Chapter 12/Edge Detection Test" {
//-----------------------------【屬性】-----------------------------
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
//-----------------------------【子着色器】-----------------------------
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
uniform half4 _MainTex_TexelSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0;
};
//-----------------------------【頂點着色器】-----------------------------
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
//計算周圍像素的紋理座標位置,其中4爲原始點,
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0); //原點
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
// 轉換爲灰度
fixed luminance(fixed4 color) {
return 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
}
// sobel算子
half Sobel(v2f i) {
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
// const half Gy[9] = {-1, -2, -1,
// 0, 0, 0,
// 1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it++) {
// 轉換爲灰度值
texColor = luminance(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * Gx[it];
// edgeY += texColor * Gy[it];
}
// half edge = 1 - abs(edgeX) - abs(edgeY);
half edge = abs(edgeX);
return edge;
}
//-----------------------------【片元着色器】-----------------------------
fixed4 frag(v2f i) : SV_Target {
half edge = Sobel(i);
return edge;
}
ENDCG
}
}
FallBack Off
}
最終結果對比
最後我們合併一下橫向和縱向的邊緣及原圖,最終邊緣檢測結果爲:
最終Shader代碼:
//-----------------------------【邊緣檢測】-----------------------------
Shader "lcl/learnShader3/002 Chapter 12/Edge Detection Test" {
//-----------------------------【屬性】-----------------------------
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
// 描邊程度
_EdgeOnly ("Edge Only", Float) = 1.0
// 邊緣顏色
_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
}
//-----------------------------【子着色器】-----------------------------
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
sampler2D _MainTex;
uniform half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0;
};
//-----------------------------【頂點着色器】-----------------------------
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
//計算周圍像素的紋理座標位置,其中4爲原始點,
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0); //原點
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
// 轉換爲灰度
fixed luminance(fixed4 color) {
return 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
}
// sobel算子
half Sobel(v2f i) {
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it++) {
// 轉換爲灰度值
texColor = luminance(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}
// 合併橫向和縱向
half edge = 1 - (abs(edgeX) + abs(edgeY));
return edge;
}
//-----------------------------【片元着色器】-----------------------------
fixed4 frag(v2f i) : SV_Target {
half edge = Sobel(i);
fixed4 edgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
edgeColor = lerp(tex2D(_MainTex, i.uv[4]),edgeColor, _EdgeOnly);
return edgeColor;
}
ENDCG
}
}
FallBack Off
}
最後
有興趣的小夥伴可以來我的GitHub上逛逛,裏面有我學習Shader過程中的一些記錄,實現的一些特效,喜歡的可以點個Star,嘿嘿,謝謝啦!