入门图形学:图像二值化

      最近工程做图像处理和网格处理,顺便来记几篇博客。
      二值化处理
      在图像处理上,一般的图像(彩图)携带信息过多,用于计算处理方面就不适合,需要将图像进行灰度化->二值化。
      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还有一堆一堆的二值化算法,有需要实现一下即可。后面有时间聊下工程的网格处理。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
本文将介绍计算机图形学中的着色(Shading)技术,包括着色频率、图形管线、纹理映射等知识点,并提供部分代码示例。 ## 着色频率 计算机图形学中的着色可以分为两种频率,分别是顶点着色和像素着色。 顶点着色(Vertex Shading)是在顶点级别对图形进行着色的过程,即在图形的每个顶点上计算颜色值,然后通过插值计算出整个图形的颜色。顶点着色通常用于处理顶点属性,如位置、法向量和颜色等。 像素着色(Pixel Shading)是在像素级别对图形进行着色的过程,即对图形的每个像素计算颜色值。像素着色通常用于处理纹理映射、阴影效果、反射和折射等。 ## 图形管线 图形管线(Graphics Pipeline)是计算机图形学中的一个重要概念,它是将输入的几何形状转化为最终图像的过程,通常包括以下几个阶段: 1. 顶点输入:将输入的顶点数据传入图形管线。 2. 顶点着色:在顶点级别对图形进行着色。 3. 图元装配:将顶点组装成图元,如点、线、三角形等。 4. 光栅化:将图元转化为像素,并计算像素在屏幕上的位置。 5. 像素着色:对图形的每个像素进行着色。 6. 输出合成:将所有像素合成成最终的图像。 以下是一个简单的图形管线示例: ```c++ // 顶点着色器 void vertexShader(in vec3 position, out vec4 color) { // 计算顶点颜色 color = vec4(1.0, 0.0, 0.0, 1.0); // 将顶点位置传递给下一个阶段 gl_Position = vec4(position, 1.0); } // 像素着色器 void pixelShader(in vec4 color, out vec4 fragmentColor) { // 直接输出顶点颜色 fragmentColor = color; } // 主程序 int main() { // 顶点数据 vec3 vertices[] = { vec3(-1.0, -1.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(1.0, -1.0, 0.0) }; // 图元数据 GLuint indices[] = {0, 1, 2}; // 创建着色器程序 GLuint program = createProgram(vertexShader, pixelShader); // 获取顶点着色器输入位置的位置 GLuint positionLocation = glGetAttribLocation(program, "position"); // 创建顶点数组对象 GLuint vao; glGenVertexArrays(1, &vao); glBindVertexArray(vao); // 创建顶点缓冲区对象 GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据传递给顶点着色器 glEnableVertexAttribArray(positionLocation); glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0); // 创建索引缓冲区对象 GLuint ibo; glGenBuffers(1, &ibo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 渲染图元 glUseProgram(program); glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0); // 销毁着色器程序、缓冲区对象和顶点数组对象 glDeleteProgram(program); glDeleteBuffers(1, &vbo); glDeleteBuffers(1, &ibo); glDeleteVertexArrays(1, &vao); return 0; } ``` ## 纹理映射 纹理映射(Texture Mapping)是一种基于图像的着色技术,它可以在三维模型表面上贴上图片,从而增强模型的真实感和细节。 纹理映射通常包括以下几个步骤: 1. 加载纹理图像:从文件中加载纹理图像,并将其存储在计算机内存中。 2. 创建纹理对象:将纹理图像传递给图形硬件,并创建一个纹理对象。 3. 纹理坐标计算:计算每个顶点在纹理图像中对应的位置,通常使用二维纹理坐标表示。 4. 纹理采样:在纹理图像中根据纹理坐标采样像素颜色,并将其作为顶点颜色。 5. 顶点着色:使用顶点颜色进行顶点着色。 以下是一个简单的纹理映射示例: ```c++ // 顶点着色器 void vertexShader(in vec3 position, in vec2 texCoord, out vec2 vTexCoord) { // 将纹理坐标传递给下一个阶段 vTexCoord = texCoord; // 将顶点位置传递给下一个阶段 gl_Position = vec4(position, 1.0); } // 像素着色器 uniform sampler2D texture; void pixelShader(in vec2 vTexCoord, out vec4 fragmentColor) { // 在纹理图像中采样像素颜色 vec4 texel = texture2D(texture, vTexCoord); // 输出纹理像素颜色 fragmentColor = texel; } // 主程序 int main() { // 顶点数据 vec3 vertices[] = { vec3(-1.0, -1.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(1.0, -1.0, 0.0) }; // 纹理坐标数据 vec2 texCoords[] = { vec2(0.0, 0.0), vec2(0.5, 1.0), vec2(1.0, 0.0) }; // 图元数据 GLuint indices[] = {0, 1, 2}; // 加载纹理图像 GLuint texture = loadTexture("texture.png"); // 创建着色器程序 GLuint program = createProgram(vertexShader, pixelShader); // 获取顶点着色器输入位置和纹理坐标的位置 GLuint positionLocation = glGetAttribLocation(program, "position"); GLuint texCoordLocation = glGetAttribLocation(program, "texCoord"); // 创建顶点数组对象 GLuint vao; glGenVertexArrays(1, &vao); glBindVertexArray(vao); // 创建顶点缓冲区对象 GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据传递给顶点着色器 glEnableVertexAttribArray(positionLocation); glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, 0); // 创建纹理坐标缓冲区对象 GLuint tbo; glGenBuffers(1, &tbo); glBindBuffer(GL_ARRAY_BUFFER, tbo); glBufferData(GL_ARRAY_BUFFER, sizeof(texCoords), texCoords, GL_STATIC_DRAW); // 将纹理坐标数据传递给顶点着色器 glEnableVertexAttribArray(texCoordLocation); glVertexAttribPointer(texCoordLocation, 2, GL_FLOAT, GL_FALSE, 0, 0); // 创建索引缓冲区对象 GLuint ibo; glGenBuffers(1, &ibo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 渲染图元 glUseProgram(program); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glUniform1i(glGetUniformLocation(program, "texture"), 0); glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0); // 销毁着色器程序、缓冲区对象、纹理对象和顶点数组对象 glDeleteProgram(program); glDeleteBuffers(1, &vbo); glDeleteBuffers(1, &tbo); glDeleteBuffers(1, &ibo); glDeleteTextures(1, &texture); glDeleteVertexArrays(1, &vao); return 0; } ``` 以上是计算机图形学中着色技术的简单介绍,希望对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值