2
\$\begingroup\$

I am trying to create a debug tool that would help me visualize an image RGB and luminance histogram. I have created a compute shader where I gather the histogram data

#pragma kernel Clear
#pragma kernel Gather

Texture2D<float4> Input;

struct HIST {
    uint4 color;
};
RWStructuredBuffer<HIST> HistogramBuffer;

[numthreads(64, 1, 1)]
void Clear(uint3 id : SV_DispatchThreadID)
{
    HistogramBuffer[id.x].color = uint4(0, 0, 0, 0);
}

[numthreads(8, 8, 1)]
void Gather(uint3 id : SV_DispatchThreadID)
{
    uint4 col = uint4(255.0 * Input[id.xy]);

    InterlockedAdd(HistogramBuffer[col.r].color.r, 1);
    InterlockedAdd(HistogramBuffer[col.g].color.g, 1);
    InterlockedAdd(HistogramBuffer[col.b].color.b, 1);
    InterlockedAdd(HistogramBuffer[dot(col.rgb, float3(0.2126729, 0.7151522, 0.0721750))].color.a, 1); 
}

And in the c# code after I've gathered the data I create a final output texture.

using UnityEngine;
using UnityEngine.UI;

internal sealed class Histogram : MonoBehaviour
{
    public const int HISTOGRAM_BINS = 256;
    uint[] histogramData;

    [SerializeField]
    ComputeShader shader;
    ComputeBuffer histogramBuffer;

    [SerializeField] RawImage output = null;
    [SerializeField] Texture2D input;

    public void Start()
    {
        //Init kernels
        int clearKernel = shader.FindKernel("Clear");
        int gatherkernel = shader.FindKernel("Gather");

        //Init data
        histogramBuffer = new ComputeBuffer(HISTOGRAM_BINS, sizeof(uint) * 4);
        histogramData = new uint[HISTOGRAM_BINS * 4];

        //Set data
        shader.SetTexture(gatherkernel, "Input", input);
        shader.SetBuffer(gatherkernel, "HistogramBuffer", histogramBuffer);
        shader.SetBuffer(clearKernel, "HistogramBuffer", histogramBuffer);

        //Execute shader
        shader.Dispatch(clearKernel, HISTOGRAM_BINS / 64, 1, 1);
        shader.Dispatch(gatherkernel, input.width / 8, input.height / 8, 1);

        //Get data
        histogramBuffer.GetData(histogramData);

        //Find highest value
        uint highestValue = uint.MinValue;
        for (int i = 0; i < histogramData.Length; i++)
        {
            if (histogramData[i] > highestValue)
            {
                highestValue = histogramData[i];
            }
        }

        //Create final output texture
        Texture2D final = new Texture2D(HISTOGRAM_BINS, input.height)
        {
            filterMode = FilterMode.Bilinear,
            wrapMode = TextureWrapMode.Clamp
        };
        final.Apply();

        //Init the texture pixels array
        Color32[] pixels = new Color32[HISTOGRAM_BINS * input.height];

        //Get all red values from the histogram data array
        uint[] reds = new uint[HISTOGRAM_BINS];
        for (int i = 0, h = 0; i < HISTOGRAM_BINS; i++, h += 4)
        {
            reds[i] = histogramData[h];
        }
        pixels = ComputeChannelHistogram(pixels, in reds, highestValue, new Color32(255, 0, 0, 255));

        //Get all green values from the histogram data array
        uint[] greens = new uint[HISTOGRAM_BINS];
        for (int i = 0, h = 1; i < HISTOGRAM_BINS; i++, h += 4)
        {
            greens[i] = histogramData[h];
        }
        pixels = ComputeChannelHistogram(pixels, in greens, highestValue, new Color32(0, 255, 0, 255));

        //Get all blue values from the histogram data array
        uint[] blues = new uint[HISTOGRAM_BINS];
        for (int i = 0, h = 2; i < HISTOGRAM_BINS; i++, h += 4)
        {
            blues[i] = histogramData[h];
        }
        pixels = ComputeChannelHistogram(pixels, in blues, highestValue, new Color32(0, 0, 255, 255));

        //Get all lum values from the histogram data array
        uint[] lum = new uint[HISTOGRAM_BINS];
        for (int i = 0, h = 3; i < HISTOGRAM_BINS; i++, h += 4)
        {
            lum[i] = histogramData[h];
        }
        pixels = ComputeChannelHistogram(pixels, in lum, highestValue, new Color32(127, 127, 127, 255));

        //Set pixels
        final.SetPixels32(pixels); final.Apply();

        //Set output image
        output.texture = final;
    }
    Color32[] ComputeChannelHistogram(Color32[] pixels, in uint[] data, float max, Color32 color)
    {
        for (int x = 0; x < HISTOGRAM_BINS; x++)
        {
            float p = Mathf.InverseLerp(0F, max, data[x]);
            for (int y = 0; y < input.height * p; y++)
            {
                int index = x + (y * HISTOGRAM_BINS);
                Color32 pixelColor = pixels[index];

                pixelColor.r += color.r;
                pixelColor.g += color.g;
                pixelColor.b += color.b;

                pixels[index] = new Color32(pixelColor.r, pixelColor.g, pixelColor.b, 255);
            }
        }
        return pixels;
    }
}

However, I am getting weird results and not sure why.

E.g. input image (Figure 1)

enter image description here

The histogram that is generated

enter image description here

The histogram that is generated in Photoshop

enter image description here

Can someone please help me understand what am I doing wrong here and how can I fix it to have better results? I just recently started looking at histograms, I understand what they show and are, the vertical is a number of pixels and the horizontal bars that go from left to right are the tones, so left is blacks, the middle is midtones, and right is whites, but for some reason I have a difficult time creating one...

\$\endgroup\$
2
  • \$\begingroup\$ Well, I have solved the issue with the same look graphs. The issue was that the offset was applied not to the histogram data index when the data for each channel was gathered into separate arrays. And that resulted in wrong data being stored. I have added a change in the question. The graphs look much better now, however, it still looks somewhat incorrect. Mine looks a lot more smooth, and in Photoshop, it looks a bit rougher. I have also added the new histogram output image to the question. \$\endgroup\$ Nov 21 at 17:03
  • \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$
    – DMGregory
    Nov 21 at 17:46
1
\$\begingroup\$

The solution to the problem I was having was firstly ensuring that the input image was not compressed in any way, so having it as none.

Moreover, the maxSize in the import settings of the texture had to be set higher than the input original size. Limiting the maximum texture size results in a lack of information in the histogram graph.

It is probably best to load the texture through the script, that way you will not have any issue with size limitation.

I have also changed the highest value for each RGB graph to the highest value in the channel array, instead of the entire histogram data array as I've done before.

The last issue that made the graph lack detail was fixed by dividing the gather compute shader dispatch thread groups, width for x, and height for y by 8, instead of 16.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.