Search Unity

[SOLVED] How can I count the number of pixels above a threshold with a shader?

Discussion in 'Shaders' started by JoePatrick, Oct 18, 2019.

  1. JoePatrick

    JoePatrick

    Joined:
    Nov 3, 2013
    Posts:
    121
    So I have a 1024x1024 render texture that looks something like this, though obviously the red part changes
    upload_2019-10-18_16-55-21.png

    I need to count the number of red pixels
    I tried doing this on the CPU with GetPixel() in a nested for loop but this is too slow.

    How can I do this with a (compute?) shader instead?


    Thanks :)
     
    Last edited: Oct 18, 2019
  2. mouurusai

    mouurusai

    Joined:
    Dec 2, 2011
    Posts:
    350
  3. JoePatrick

    JoePatrick

    Joined:
    Nov 3, 2013
    Posts:
    121
  4. JoePatrick

    JoePatrick

    Joined:
    Nov 3, 2013
    Posts:
    121
    Thanks a lot, I was able to adapt this to work :)

    Here's my shader for anyone else looking:

    Code (HLSL):
    1. #pragma kernel CSMain
    2. #pragma kernel CSInit
    3.  
    4. Texture2D<float4> InputImage;
    5.  
    6. RWStructuredBuffer<int> ResultBuffer;
    7.  
    8. [numthreads(1, 1, 1)]
    9. void CSInit(uint3 id : SV_DispatchThreadID)
    10. {
    11.     ResultBuffer[0] = 0;
    12. }
    13.  
    14.  
    15. [numthreads(8, 8, 1)]
    16. void CSMain(uint3 id : SV_DispatchThreadID)
    17. {
    18.     uint4 col = InputImage[id.xy];
    19.  
    20.  
    21.     if(col.r > 0.4)
    22.     {
    23.         InterlockedAdd(ResultBuffer[0], 1);
    24.     }
    25. }
    and here's the C# script
    Code (CSharp):
    1.  
    2. public ComputeShader cShader;
    3. ComputeBuffer cBuffer;
    4. int[] analysisResult;
    5. int kernalMain, kernalInit;
    6. int AnalyseImage()
    7. {
    8.     kernalMain = cShader.FindKernel("CSMain");
    9.     kernalInit = cShader.FindKernel("CSInit");
    10.     cBuffer = new ComputeBuffer(1, sizeof(int));
    11.     analysisResult = new int[1];
    12.  
    13.     cShader.SetTexture(kernalMain, "InputImage", rt);
    14.     cShader.SetTexture(kernalInit, "InputImage", rt);
    15.     cShader.SetBuffer(kernalMain, "ResultBuffer", cBuffer);
    16.     cShader.SetBuffer(kernalInit, "ResultBuffer", cBuffer);
    17.  
    18.     cShader.Dispatch(kernalInit, 1, 1, 1);
    19.     cShader.Dispatch(kernalMain, rt.width / 8, rt.height / 8, 1);
    20.  
    21.     cBuffer.GetData(analysisResult);
    22.  
    23.     cBuffer.Release();
    24.     cBuffer = null;
    25.  
    26.     return analysisResult[0];
    27. }
    (rt is the RenderTexture with the image)
     
    Rockdrake and Olmi like this.
  5. Przemyslaw_Zaworski

    Przemyslaw_Zaworski

    Joined:
    Jun 9, 2017
    Posts:
    328
    Links to source code in video description. Compute shader calculates area (ratio between counted pixels and total pixels):

     
  6. Rockdrake

    Rockdrake

    Joined:
    Mar 28, 2018
    Posts:
    1
    Thanks for sharing! I had exactly the same problem (count white pixels) and the same idea, but no clue, how to do this with the shader. I had 25% load because of ReadPixels - now no remarkable load anymore.
     
  7. unity_1cewWbGw8rRVxw

    unity_1cewWbGw8rRVxw

    Joined:
    Jun 6, 2018
    Posts:
    9
    i have follow this way and it is working perfectly but i am having a issue in some android Devices
    ArgumentException: Kernel 'CSMain' not found
    This is the exception i am getting
    Help Needed

    This is My Compute Shader


    #pragma kernel CSMain

    Texture2D<float4> image; // The Mask
    float4 reference; // Color reference
    RWStructuredBuffer<uint> compute_buffer; //buffer

    [numthreads(8, 8, 1)]
    void CSMain(uint3 id : SV_DispatchThreadID)
    {
    if (all(reference ==image[id.xy])) {
    InterlockedAdd(compute_buffer[0], 1);
    }
    }



    and here is the script

    Code (CSharp):
    1. public void OnStart(RenderTexture mask)
    2. {
    3.     //ListPercentual = new Dictionary<Colors, string>();
    4.      
    5.     compute_shader = Instantiate(compute_shader);
    6.      
    7.  
    8.    
    9.     render_texture = mask;
    10.  
    11.     try
    12.     {
    13.         handle_main = compute_shader.FindKernel("CSMain");
    14.     }
    15.     catch (Exception e)
    16.     {
    17.         GameAnalytics.NewErrorEvent(GAErrorSeverity.Info , e.Message+"::"+SystemInfo.deviceModel);
    18.         Console.WriteLine(e);
    19.         throw;
    20.     }
    21.      
    22.      
    23.     compute_buffer = new ComputeBuffer(1, sizeof(uint));
    24.     data = new uint[1];
    25.     compute_shader.SetTexture(handle_main, "image", render_texture);
    26.     compute_shader.SetBuffer(handle_main, "compute_buffer", compute_buffer);
    27. }
    28.  
    29.  
    30. public float GetPoints()
    31. {
    32.     compute_shader.SetVector("reference", reference);
    33.     data = new uint[1] { 0 };
    34.     compute_buffer.SetData(data);
    35.     compute_shader.Dispatch(handle_main, render_texture.width / 8, render_texture.height / 8, 1);
    36.     compute_buffer.GetData(data);
    37.     uint result = data[0];
    38.     percent = 100.0f - ((float)result / ((float)render_texture.width * (float)render_texture.height)) * 100.0f;
    39.      
    40.  
    41.     return percent;
    42.  
    43. }
     
    Last edited: Jan 6, 2022
  8. unity_1cewWbGw8rRVxw

    unity_1cewWbGw8rRVxw

    Joined:
    Jun 6, 2018
    Posts:
    9
    There is a huge problem with compute shader that 50% of Android Devices does not support Open GL ES 3.1 which is necessary for compute shaders. Also IOS does not support it altogether.
    Looking for another way of doing this on mobile devices
    Any Suggestions.
     
  9. KSzczech

    KSzczech

    Joined:
    Sep 24, 2020
    Posts:
    30
    You may just write a box downsample shader instead.
    First pass would count pixels passing the test and write output.
    Next pass would simply output a sum of all pixels in a box (because they are counts after first pass).

    And if you really want speed you could make your first pass write to a single channel 8-bit buffer (assuming your first box is no bigger than 255 pixels), then your second pass would write to a 16-bit buffer, and following passes to 32-bit buffers.
    Until your final buffer is just 1x1 pixels.

    Yeah, it's more coding, but takes advantage of parallelism pretty well, so offers good performance and good compatibility (you may even give up on compute shaders and do it with pixel shaders, for as long as you have a 32-bit texture to write to)