通過一個小Trick實現shader的影象識別/影象統計操作
阿新 • • 發佈:2018-12-22
1.簡介
在日常開發中會遇到諸如判斷某張圖的紅色百分比佔多少的問題,由於gpu運算並行的原因並不能對其進行累加操作。網上一些針對此類問題
的做法是將一張大圖分成多個小塊逐步處理並逐步合併:
但我在思考一種更簡便的方法,於是想到在頂點shader裡做判斷檢測,在畫素shader裡獲取結果這樣一個形式:
用一組頂點去讀單個畫素,判斷失敗的頂點座標提交到螢幕外,而判斷成功的頂點座標放在螢幕內。
最後在CPU中獲取是否有螢幕內頂點這樣一個結果,來進行簡單的識別操作。
而在開啟透明之後,還可以用透明度疊加來獲取更復雜的結果。
2.實踐
首先實踐結果並沒有想象的那麼好,因為如果純用三角面來做頂點部分的判斷未免太費效率了。
所以我改成了傳入頂點判斷並生成面的方式,並且縮小了傳入圖片的畫素大小。
畢竟更多的運用場合是用來做刮刮卡或者擦除的識別。只需要檢測mask圖片。
上程式碼:
Shader "Hidden/FooShader" { Properties { } SubShader { Blend One One tags { "Queue" = "Transparent" "RenderType" = "Transparent" } Pass { CGPROGRAMFooShader.shader#pragma target 4.0 #pragma vertex vert #pragma geometry geom #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 color : COLOR; float4 vertex : SV_POSITION; }; v2f vert(uint vid : SV_VertexID) { v2f o = (v2f)0; o.vertex = 0; int roll = vid % 4; if(roll == 0) o.color = float4(0.05, 0, 0, 0); if (roll == 1) o.color = float4(0, 0.05, 0, 0); if (roll == 2) o.color = float4(0, 0, 0.05, 0); if (roll == 3) o.color = float4(0, 0, 0, 0.05); return o; } [maxvertexcount(4)] void geom(point v2f vertElement[1], inout TriangleStream<v2f> triStream) { float size = 10; float4 v1 = vertElement[0].vertex + float4(-size, -size, 0, 0); float4 v2 = vertElement[0].vertex + float4(-size, size, 0, 0); float4 v3 = vertElement[0].vertex + float4(size, -size, 0, 0); float4 v4 = vertElement[0].vertex + float4(size, size, 0, 0); v2f r = (v2f)0; r.vertex = mul(UNITY_MATRIX_VP, v1); r.color = vertElement[0].color; triStream.Append(r); r.vertex = mul(UNITY_MATRIX_VP, v2); r.color = vertElement[0].color; triStream.Append(r); r.vertex = mul(UNITY_MATRIX_VP, v3); r.color = vertElement[0].color; triStream.Append(r); r.vertex = mul(UNITY_MATRIX_VP, v4); r.color = vertElement[0].color; triStream.Append(r); } fixed4 frag(v2f i) : SV_Target { return i.color; } ENDCG } } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; namespace Hont { public class Foo : MonoBehaviour { void Start() { var mat = new Material(Shader.Find("Hidden/FooShader")); mat.SetPass(0); var tempRT = RenderTexture.GetTemporary(16, 16, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB, 1); tempRT.filterMode = FilterMode.Point; tempRT.autoGenerateMips = false; tempRT.anisoLevel = 0; tempRT.wrapMode = TextureWrapMode.Clamp; var cacheRT = RenderTexture.active; RenderTexture.active = tempRT; Graphics.DrawProcedural(MeshTopology.Points, 10, 1); var tex2D = new Texture2D(16, 16, TextureFormat.ARGB32, false, false); tex2D.wrapMode = TextureWrapMode.Clamp; tex2D.anisoLevel = 0; tex2D.filterMode = FilterMode.Point; tex2D.ReadPixels(new Rect(0, 0, 16, 16), 0, 0); var firstPixel = tex2D.GetPixel(0, 0); Debug.Log("firstPixel: " + firstPixel); RenderTexture.active = cacheRT; RenderTexture.ReleaseTemporary(tempRT); } } }Foo.cs
跑了一下程式碼之後我發現了兩個問題,也是沒解決的問題,一個是計算結果有誤差
o.color = float4(0.05, 0, 0, 0);
特別是當返回顏色小於0.1之後,我嘗試改變影象格式或者RT等引數依舊沒能解決
第二個問題是開啟透明後,透明圖片的疊加是有上限的,畢竟深度有限。
對於第一個問題,目前還不需要太精確所以沒解決但也能用。第二個問題可以用一些方法來緩解
比如在頂點shader中增加運算量,把返回值分散到rgba四個通道上去。
int roll = vid % 4; if(roll == 0) o.color = float4(0.05, 0, 0, 0); if (roll == 1) o.color = float4(0, 0.05, 0, 0); if (roll == 2) o.color = float4(0, 0, 0.05, 0); if (roll == 3) o.color = float4(0, 0, 0, 0.05); return o;
把更多的畫素遍歷放入頂點中,這樣頂點shader處理的圖片大小會小n/1:
v2f vert(uint vid : SV_VertexID) { v2f o = (v2f)0; o.vertex = 0; half2 image_size = half2(_SampleFilter.x * _LoopImageSize.x, _SampleFilter.y * _LoopImageSize.y); half y = floor(vid / _LoopImageSize.x); half x = (vid - y * _LoopImageSize.x) / _LoopImageSize.x; y = y / _LoopImageSize.y; for (half rx = 0; rx < _SampleFilter.x; rx++) { for (half ry = 0; ry < _SampleFilter.y; ry++) { half xx = x + rx; half yy = y + ry; float4 r = statistics_sample(_Image, _Rec_Color, half4(xx, yy, 0, 0), image_size); o.color += r; } } return o; }
3.優化再測試
最終達到了一個比較不錯的結果,我把相關函式封裝成了一個類。
我寫了一個塗抹效果demo來測試一下,它通過識別白色畫素的數量來判斷是否為全部塗完:
通過這個小Trick其實可以在畫素裡返回更多的資訊,簡單的場合這麼還是比較方便的,當然一些複雜的情況分塊+computer shader來做其實更合適。