每次等知识真的用上了才会想为啥当初不再认真点听课,卷积暂时也没有整的很明白,尽力去理解和表达一下。
卷积?卷鸭!
边缘检测 边缘检测本质是高通滤波器 ,下面的图用于说明一张图片上高通和低通部分的区别。
先说说卷积和卷积核 卷积就是用卷积核对图像中每个像素进行一系列操作,而卷积核的中心是放在当前要处理的像素上,对于每一个当前像素,计算领域像素和滤波器矩阵对应元素的乘积,然后求和就得到了当前像素的值。
边缘检测的原理就是找到有着明显差别的颜色、亮度或纹理等属性的相邻像素,对应到数学知识上,这种差值可以用梯度 (gradient)来表示,即边缘处的梯度是比较大的。进而我们引入了边缘检测算子的概念,而我们常用的几个算子都包含了两个方向上的卷积核,即水平和数值;在进行边缘检测时,我们对每个像素进行一次卷积计算,得到两个方向上的梯度值Gx 和Gy ,然后通过下面的公式计算整体梯度
有时候出于性能考虑,可以使用绝对值代替开根号的操作:
梯度方向:
得到梯度G后,我们可以来判断哪些像素对应了边缘,即梯度值越大越可能是边界。
几种常见的边缘检测算子 Roberts算子 在进行Roberts算子的实现的时候,《Unity Shader入门精要》上给出的矩阵是
而在ITMO的Image Processing课程上给出的是符号和位置不同的
我们直接在OpenCV-Python中看一下三种形式算子得到的结果,可以看出第一张图和后两张的边界清晰度是不同的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def roberts (): G_x = np.array([[1 , -1 ], [0 , 0 ]]) G_y = np.array([[1 , 0 ], [-1 , 0 ]]) G_x = np.array([[-1 , 0 ], [0 , 1 ]]) G_y = np.array([[0 , -1 ], [1 , 0 ]]) G_x = np.array([[1 , 0 ], [0 , -1 ]]) G_y = np.array([[0 , 1 ], [-1 , 0 ]]) if img.dtype == np.uint8: img_filter = img.astype(np.float32) / 255 else : img_filter = np.copy(img) I_x = cv.filter2D(img_filter, -1 , G_x) I_y = cv.filter2D(img_filter, -1 , G_y) img_filter = cv.magnitude(I_x, I_y) if img.dtype == np.uint8: img_filter = (255 * img_filter).clip(0 , 255 ).astype(np.uint8) cv.imshow("Image1" , img) cv.imshow("Result1" , img_filter) cv.waitKey()
为了探究Roberts算子的值的位置所造成的不同结果,我们先要明白下这个二阶算子的原理(按第一种矩阵来说明)
Roberts算子是基于交叉差分的梯度算法;本质是计算左上角和右下角的差值,乘以右上角和左下角的差值,作为评估边缘的依据;常用于处理具有陡峭低噪声的图像,当边缘比较接近45度的时候算法处理比较理想,缺点是对边缘的定位不准确,提取的边缘线条比较粗。
因此我们谈到Roberts算子,一般使用的是具有对角值的矩阵,而非对角值矩阵所产生的边界会没有那么明显。
Prewitt算子及效果图 Prewitt算子对噪声有抑制作用,通过先对图像进行一个方向的归一化均值平滑,然后进行该方向的差分;但是Prewitt对于边缘的定位没有Roberts算子准确。
Sobel算子及效果图
在Unity中实现Sobel算子 以Sobel算子为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 using System.Collections; using System.Collections.Generic; using UnityEngine; 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); } } }
PostEffectBase
基类提供检测相关shader和材质的函数,实现 OnRenderImage
获取当前屏幕渲染的纹理,将参数设置为相应的数值,调用Graphics.Blit
使用特定的unity shader对当前图像进行处理,再把返回的渲染纹理显示到屏幕上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 Shader "Unity Shaders Book/EdgeDetection" { Properties { _MainTex ("Albedo (RGB)", 2 D) = "white" {} _EdgeOnly ("Edge Only", Float) = 1.0 _EdgeColor ("Edge Color", Color) = (0 , 0 , 0 , 1 ) _BackgroundColor ("Background Color", Color) = (1 , 1 , 1 , 1 ) } SubShader { Pass { ZTest Always Cull Off ZWrite Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; fixed _EdgeOnly; fixed4 _EdgeColor; fixed4 _BackgroundColor; 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; 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.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; } half Sobel(v2f i) { const half Gx[9 ] = {-1 , -2 , -1 , 0 , 0 , 0 , 1 , 2 , 1 }; const half Gy[9 ] = {-1 , 0 , 1 , -2 , 0 , 2 , -1 , 0 , 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 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4 ]), edge); fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge); return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); } ENDCG } } FallBack Off }
_MainTex_TexelSize
是Unity为提供的访问某纹理对应的每个纹素的大小:如一张512x512的纹理,值为1/512;因为卷积需要对邻近区域内的纹理进行采样,所以需要用到这个值来计算各个相邻区域的纹理坐标
edgesOnly
参数用于调整边缘线强度:当值为0时,边缘会叠加在原渲染图像上;当值为1时,只会显示边缘,不显示原渲染图像
EdgesOnly为0时:
EdgesOnly为1时:
在Unity中实现Roberts算子
脚本代码在Sobel算子的基础上添加一些新的属性,用于控制采样距离以及对深度和法线进行边缘检测时的灵敏度参数,并在后续OnRenderImage
函数中将参数传递给材质
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [Range(0.0f, 1.0f)] public float edgesOnly = 0.0f; public Color edgeColor = Color.black; public Color backgroundColor = Color.white; public float sampleDistance = 1.0f; public float sensitivityDepth = 1.0f; public float sensitivityNormals = 1.0f; void OnEnable() { GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals; } [ImageEffectOpaque] void OnRenderImage (RenderTexture src, RenderTexture dest) { if (material != null) { material.SetFloat("_EdgeOnly", edgesOnly); material.SetColor("_EdgeColor", edgeColor); material.SetColor("_BackgroundColor", backgroundColor); material.SetFloat("_SampleDistance", sampleDistance); material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f)); Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } }
[ImageEffectOpaque]属性使OnRenderImage函数在不透明的pass执行完毕后调用,而不对不透明物体产生影响;因此在对不透明物体描边而不希望透明物体也被描边时 添加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 Shader "Unity Shaders Book/Edge Detection Normals And Depth" { Properties { _MainTex ("Base (RGB)", 2 D) = "white" {} _EdgeOnly ("Edge Only", Float) = 1.0 _EdgeColor ("Edge Color", Color) = (0 , 0 , 0 , 1 ) _BackgroundColor ("Background Color", Color) = (1 , 1 , 1 , 1 ) _SampleDistance ("Sample Distance", Float) = 1.0 _Sensitivity ("Sensitivity", Vector) = (1 , 1 , 1 , 1 ) } SubShader { CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; fixed _EdgeOnly; fixed4 _EdgeColor; fixed4 _BackgroundColor; float _SampleDistance; half4 _Sensitivity; sampler2D _CameraDepthNormalsTexture; struct v2f { float4 pos : SV_POSITION; half2 uv[5 ]: TEXCOORD0; }; v2f vert(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0 ] = uv; #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0 ) uv.y = 1 - uv.y; #endif o.uv[1 ] = uv + _MainTex_TexelSize.xy * half2(1 ,1 ) * _SampleDistance; o.uv[2 ] = uv + _MainTex_TexelSize.xy * half2(-1 ,-1 ) * _SampleDistance; o.uv[3 ] = uv + _MainTex_TexelSize.xy * half2(-1 ,1 ) * _SampleDistance; o.uv[4 ] = uv + _MainTex_TexelSize.xy * half2(1 ,-1 ) * _SampleDistance; return o; } half CheckSame(half4 center, half4 sample ) { half2 centerNormal = center.xy; float centerDepth = DecodeFloatRG(center.zw); half2 sampleNormal = sample .xy; float sampleDepth = DecodeFloatRG(sample .zw); half2 diffNormal = abs (centerNormal - sampleNormal) * _Sensitivity.x; int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1 ; float diffDepth = abs (centerDepth - sampleDepth) * _Sensitivity.y; int isSameDepth = diffDepth < 0.1 * centerDepth; return isSameNormal * isSameDepth ? 1.0 : 0.0 ; } fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target { half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1 ]); half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2 ]); half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3 ]); half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4 ]); half edge = 1.0 ; edge *= CheckSame(sample1, sample2); edge *= CheckSame(sample3, sample4); fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0 ]), edge); fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge); return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); } ENDCG Pass { ZTest Always Cull Off ZWrite Off CGPROGRAM #pragma vertex vert #pragma fragment fragRobertsCrossDepthAndNormal ENDCG } } FallBack Off }
高斯模糊 高斯模糊本质也是卷积计算,使用的卷积核为高斯核——一个正方形大小的滤波器,每个元素基于以下方程计算,σ为标准差(一般取1),x和y分别对应当前位置到卷积核中心的整数距离。
计算得到卷积核中各个位置的高斯值后,为保证滤波后图像不会变暗,需要将核中的权重进行归一化——让每个权重除以所有权重的和,保证所有权重的和为1。高斯方程模拟了邻域像素离当前像素越近影响越大,因此高斯核维数越多,模糊程度越大 。使用一个NxN的高斯核对图像进行卷积滤波,就需要NxNxWxH次纹理采样,因此当维数越多,采样次数会十分巨量。因此,可以将二维的高斯函数拆分成两个一维函数,即使用两个一维高斯核先后对图像进行滤波,采样次数减少为2xNxWxH。
在OpenCV-Python中实现 1 img_gauss = cv2.GaussianBlur(img_gray, (5 , 5 ), 0 )
效果图:
在Unity中实现高斯模糊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 using System.Collections; using System.Collections.Generic; using UnityEngine; public class GaussianBlur : PostEffectsBase { public Shader gaussianBlurShader; private Material gaussianBlurMaterial = null; public Material material { get { gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial); return gaussianBlurMaterial; } } //三个参数分别对应高斯模糊迭代次数、模糊范围和缩放系数的参数 [Range(0, 4)] public int iterations = 3; [Range(0.2f, 3.0f)] public float blurSpread = 0.6f; [Range(1, 8)] public int downSample = 2; void OnRenderImage (RenderTexture src, RenderTexture dest) { if (material != null) { int rtW = src.width/downSample; int rtH = src.height/downSample; //GetTemporary函数分配了一块小于屏幕图像尺寸的缓冲区,因为高斯模糊的shader中需要调用两个pass,使用一块中间缓存来存储第一个pass执行完毕后得到的模糊结果 RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0); //将临时渲染纹理的滤波模式设置为双线性 buffer.filterMode = FilterMode.Bilinear; //第一次调用使用shader中第一个pass(即使用竖直方向的一维高斯核进行滤波)对src进行处理,并将结果存储在bufffer中 Graphics.Blit(src, buffer, material, 0); //第二次调用是使用shader中的第二个pass(即使用水平方向的一维高斯核进行滤波)对buffer进行处理,返回最终屏幕的图像 Graphics.Blit(buffer, dest, material, 1); RenderTexture.ReleaseTemporary(buffer); } else { Graphics.Blit(src, dest); } } }
高斯核维数相同的情况下,_BlurSize
越大模糊程度越高,但采样数不会受到影响;但过大的BlurSize
值会造成虚影;而downSample
越大,需要处理的像素数越少(性能越好),也能进一步提高模糊程度,但若这个值过大会使图像像素化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 Shader "Unity Shaders Book/GaussianBlur" { Properties { _MainTex ("Base (RGB)", 2 D) = "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; }; v2f vertBlurVertical(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0 ] = uv; o.uv[1 ] = uv + float2(0.0 , _MainTex_TexelSize.y * 1.0 ) * _BlurSize; o.uv[2 ] = uv - float2(0.0 , _MainTex_TexelSize.y * 1.0 ) * _BlurSize; o.uv[3 ] = uv + float2(0.0 , _MainTex_TexelSize.y * 2.0 ) * _BlurSize; o.uv[4 ] = uv - float2(0.0 , _MainTex_TexelSize.y * 2.0 ) * _BlurSize; return o; } v2f vertBlurHorizontal(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0 ] = uv; o.uv[1 ] = uv + float2(_MainTex_TexelSize.x * 1.0 , 0.0 ) * _BlurSize; o.uv[2 ] = uv - float2(_MainTex_TexelSize.x * 1.0 , 0.0 ) * _BlurSize; o.uv[3 ] = uv + float2(_MainTex_TexelSize.x * 2.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[3 ] = {0.4026 , 0.2442 , 0.0545 }; fixed3 sum = tex2D(_MainTex, i.uv[0 ]).rgb * weight[0 ]; for (int it = 1 ; it < 3 ; it++) { sum += tex2D(_MainTex, i.uv[it*2 -1 ]).rgb * weight[it]; sum += tex2D(_MainTex, i.uv[it*2 ]).rgb * weight[it]; } 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 "Diffuse" }