入门图形学:图像二值化

      最近工程做图像处理和网格处理,顺便来记几篇博客。
      二值化处理
      在图像处理上,一般的图像(彩图)携带信息过多,用于计算处理方面就不适合,需要将图像进行灰度化->二值化。
      1.灰度化
      图像编码分为RGB、YUV两种常用的编码方式,一般我们使用的是RGB编码,但是涉及到网络传输等对数据压缩有较高要求的领域,YUV则是主流,因为YUV在损失一定数据量的前提下依旧保证了较高的画质。
      YUV具体压缩方式就是:
      彩色图像记录的格式,常见的有RGB、YUV、CMYK等。彩色电视最早的构想是使用RGB三原色来同时传输。这种设计方式是原来黑白带宽的3倍,在当时并不是很好的设计。RGB诉求于人眼对色彩的感应,YUV则着重于视觉对于亮度的敏感程度,Y代表的是亮度,UV代表的是彩度(因此黑白电影可省略UV,相近于RGB),分别用Cr和Cb来表示,因此YUV的记录通常以Y:UV的格式呈现。
      由于人眼对彩度不明感,UV彩度分量则使用了降采样(二分之一)的方式处理,则U分量(V分量)是Y分量的四分之一数据量,也就是从RGB的12x降低到了YUV(I420)的6x数据量。
      百度yuv
      铺垫了这么多,主要是因为我们需要将图片的RGB转成YUV(只需要Y),也就是将亮度(灰度)提取出来了,下面就是RGB和YUV转换的计算公式:
在这里插入图片描述      wiki yuv
      接下来我们处理彩图到灰度图的转换:

#pragma kernel CSMain

RWTexture2D<float4> Source;
RWTexture2D<float4> Result;

float4 grayPixel(float4 rgba)
{
    
    
    float gray = 0.299*rgba.x+0.587*rgba.y+0.114*rgba.z;
    float4 px = float4(gray,gray,gray,1);
    return px;
}

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    
    
    Result[id.xy] = grayPixel(Source[id.xy]);
}

      c#调用:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestBinarization : MonoBehaviour
{
    
    
    public Texture2D sourceTex;
    public ComputeShader grayCS;

    private int texWidth;
    private int texHeight;

    public RenderTexture sourceRT;
    public RenderTexture grayRT;

    void Start()
    {
    
    
        texWidth = sourceTex.width;
        texHeight = sourceTex.height;

        GetSourceRT();
        GetGrayRT();
    }

    public void GetSourceRT()
    {
    
    
        sourceRT = new RenderTexture(texWidth, texHeight, 0, RenderTextureFormat.ARGB32);
        sourceRT.enableRandomWrite = true;
        sourceRT.Create();
        Graphics.Blit(sourceTex, sourceRT);
    }

    public void GetGrayRT()
    {
    
    
        grayRT = new RenderTexture(texWidth, texHeight, 0, RenderTextureFormat.ARGB32);
        grayRT.enableRandomWrite = true;
        grayRT.Create();
        int kl = grayCS.FindKernel("CSMain");
        grayCS.SetTexture(kl, "Source", sourceRT);
        grayCS.SetTexture(kl, "Result", grayRT);
        grayCS.Dispatch(kl, texWidth / 8, texHeight / 8, 1);
    }
}

      效果如下:
      原图:
在这里插入图片描述
      灰度图:
在这里插入图片描述
      接下来灰度图进行二值化:

#pragma kernel CSMain

RWTexture2D<float4> Source;
RWTexture2D<float4> Result;

float4 binaryPixel(float4 rgba)
{
    
    
    float r = rgba.x;
    float4 px;
    if(r<0.7)
    {
    
    
        px = float4(0,0,0,1);
    }
    else
    {
    
    
        px = float4(1,1,1,1);    
    }
    return px;
}

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    
    
    Result[id.xy] = binaryPixel(Source[id.xy]);
}

      效果如下:
在这里插入图片描述
      但是我是通过自己目测调整0.7这个threshold达到的效果,并不是自动化处理的二值化的threshold,那么我们怎么才能自动处理这个threshold呢?
      这里有三种常用算法:平均值法、OTSU法、Kittler法。

      平均值法:
      用灰度图所有像素灰度平均值作为threshold,如下:

    public float GetAvgThreshold(RenderTexture rt)
    {
    
    
        int width = rt.width;
        int height = rt.height;
        RenderTexture.active = rt;
        Texture2D tex = new Texture2D(width, height);
        tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
        tex.Apply();
        RenderTexture.active = null;
        float threshold = 0f;
        Color[] cols = tex.GetPixels();
        for (int y = 0; y < height; y++)
        {
    
    
            for (int x = 0; x < width; x++)
            {
    
    
                threshold += cols[y * width + x].r;
            }
        }
        threshold /= (width * height);
#if UNITY_EDITOR
        Debug.LogFormat("GetAvgThreshold threshold = {0}", threshold);
#endif
        return threshold;
    }
    public void GetBinaryRT()
    {
    
    
        binaryRT = new RenderTexture(texWidth, texHeight, 0, RenderTextureFormat.ARGB32);
        binaryRT.enableRandomWrite = true;
        binaryRT.Create();
        int kl = grayCS.FindKernel("CSMain");
        float thre = BinarizationFactory.Instance.GetAvgThreshold(grayRT);
        binaryCS.SetFloat("threshold", thre);
        binaryCS.SetTexture(kl, "Source", grayRT);
        binaryCS.SetTexture(kl, "Result", binaryRT);
        binaryCS.Dispatch(kl, texWidth / 8, texHeight / 8, 1);
    }

      效果如下:
在这里插入图片描述
在这里插入图片描述
      可以看得出来,效果不怎么好。

      OTSU法:
      OTSU法也叫大津法(百度大津法),原理是假设threshold灰度值将灰度图分为前景区域(黑)和背景区域(白),求出前景和背景的最大类间方差,根据步长(0f/255f-255f/255f)进行迭代,求出threshold值,公式如下:
      类间方差 = 前景像素占比比例 x (前景平均灰度值 - 全图平均灰度值) ^ 2 + 背景像素占比比例 x (背景平均灰度值 - 全图平均灰度值) ^ 2
      下面是计算公式:
      1.threshold阈值(t)
      2.前景(黑)像素占比比例(r1)
      3.前景(白)平均灰度(g1)
      4.全图平均灰度(g)
      5.背景像素占比比例(r2)
      6.背景平均灰度(g2)
      7.类间方差(v)
在这里插入图片描述
      得到的结果就是v = r1r2(g1-g2)^ 2,只要根据迭代得到最大的v即可求的迭代值threshold。
      接下来写代码:

    public float GetOTSUThreshold(RenderTexture rt)
    {
    
    
        int width = rt.width;
        int height = rt.height;
        int pxlen = width * height;
        RenderTexture.active = rt;
        Texture2D tex = new Texture2D(width, height);
        tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
        tex.Apply();
        RenderTexture.active = null;
        Color[] cols = tex.GetPixels();
        //以0.02f迭代
        float iter = 0.02f;
        //v = r1*r2*(g1-g2)^2
        float threshold = 0;
        float maxv = float.MinValue;
        for (float t = 0; t < 1f; t += iter)
        {
    
    
            float r1 = 0, r2 = 0, g1 = 0f, g2 = 0f;
            for (int k = 0; k < pxlen; k++)
            {
    
    
                float gray = cols[k].r;
                if (gray <= t)      //前景(黑)
                {
    
    
                    r1++;
                    g1 += gray;
                }
                else                //背景(白)
                {
    
    
                    r2++;
                    g2 += gray;
                }
            }
            g1 /= r1;
            r1 /= (float)pxlen;
            g2 /= r2;
            r2 /= (float)pxlen;
            float v = r1 * r2 * (g1 - g2) * (g1 - g2);
            if (maxv < v)
            {
    
    
                maxv = v;
                threshold = t;
            }
        }
#if UNITY_EDITOR
        Debug.LogFormat("GetOTSUThreshold threshold = {0}", threshold);
#endif
        return threshold;
    }

      效果如下:
在这里插入图片描述
在这里插入图片描述
      这里我没有用1f/255f迭代,而是为了加速计算用0.02f迭代,如果需要高精度的就迭代次数多一点,不过看得出来效果还算可以。

      Kittler法
      kittler法又称最小误差阈值法,从名称上来看很自信。核心思想是计算整幅图像得到梯度灰度的平均值,以此平均值作为threshold阈值。
      具体做法就是逐行迭代像素,得到像素水平或垂直方向的最大梯度,而这个梯度的计算方法就是邻间灰度值之差的绝对值,如下图:
在这里插入图片描述
      然后计算最大梯度g与像素灰度p.r的积gp,迭代完成得到∑gp和∑g,求得除数即得到了阈值threshold,如下:

    public float GetKittlerThreshold(RenderTexture rt)
    {
    
    
        int width = rt.width;
        int height = rt.height;
        int pxlen = width * height;
        RenderTexture.active = rt;
        Texture2D tex = new Texture2D(width, height);
        tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
        tex.Apply();
        RenderTexture.active = null;
        Color[] cols = tex.GetPixels();
        float gp = 0f;              //∑gp
        float gsum = 0f;            //∑g
        for (int y = 1; y < height - 1; y++)
        {
    
    
            for (int x = 1; x < width - 1; x++)
            {
    
    
                int px = y * width + x;
                int left = y * width + x - 1;
                int right = y * width + x + 1;
                int up = (y - 1) * width + x;
                int down = (y + 1) * width + x;
                float gh = Mathf.Abs(cols[left].r - cols[right].r);
                float gv = Mathf.Abs(cols[up].r - cols[down].r);
                //得到最大梯度g
                float g = gh > gv ? gh : gv;
                //累加
                gp += (g * cols[px].r);
                gsum += g;
            }
        }
        float threshold = gp / gsum;
#if UNITY_EDITOR
        Debug.LogFormat("GetKittlerThreshold threshold = {0}", threshold);
#endif
        return threshold;
    }

      效果如下:
在这里插入图片描述
在这里插入图片描述
      可以看得出来效果也不差。
      好了,这也算是常用的几种二值化计算方法,当然百度google还有一堆一堆的二值化算法,有需要实现一下即可。后面有时间聊下工程的网格处理。

猜你喜欢

转载自blog.csdn.net/yinhun2012/article/details/121018541