1. 程式人生 > >通過一個小Trick實現shader的影象識別/影象統計操作

通過一個小Trick實現shader的影象識別/影象統計操作

1.簡介

在日常開發中會遇到諸如判斷某張圖的紅色百分比佔多少的問題,由於gpu運算並行的原因並不能對其進行累加操作。網上一些針對此類問題

的做法是將一張大圖分成多個小塊逐步處理並逐步合併:

但我在思考一種更簡便的方法,於是想到在頂點shader裡做判斷檢測,在畫素shader裡獲取結果這樣一個形式:

用一組頂點去讀單個畫素,判斷失敗的頂點座標提交到螢幕外,而判斷成功的頂點座標放在螢幕內。

最後在CPU中獲取是否有螢幕內頂點這樣一個結果,來進行簡單的識別操作。

而在開啟透明之後,還可以用透明度疊加來獲取更復雜的結果。

2.實踐

首先實踐結果並沒有想象的那麼好,因為如果純用三角面來做頂點部分的判斷未免太費效率了。

所以我改成了傳入頂點判斷並生成面的方式,並且縮小了傳入圖片的畫素大小。

畢竟更多的運用場合是用來做刮刮卡或者擦除的識別。只需要檢測mask圖片。

上程式碼:

Shader "Hidden/FooShader"
{
    Properties
    {
    }
    SubShader
    {
        Blend One One

        tags
        {
            "Queue" = "Transparent"
            "RenderType" = "Transparent"
        }

        Pass
        {
            CGPROGRAM
            
#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 } } }
FooShader.shader
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來做其實更合適。