Unity3D Shader系列之UI Image灰度化

Unity3D Shader系列之UI Image灰度化

2020年11月14日 0 作者 老王

在《OpenCV for Unity学习笔记(1)——Mat及灰度化图像》我们看到,使用OpenCV进行图像灰度化很简单,直接调用Imgproc.cvtColor更改图像的颜色空间即可。

Imgproc.cvtColor(mat, grayMat, Imgproc.COLOR_BGR2GRAY);

但是这个方法背后的原理是什么呢?
我们今天就来回答这个问题,并在Unity中用Shader来实现。

1.灰度化是什么

灰度化就是将一幅彩色图像转换为灰度图像的过程。
彩色图像通常使用的是RGB(红绿蓝)颜色模型,即图像上每一个像素点的颜色都是由这三种颜色混合而成。
这里会有几个问题:
①为什么是红绿蓝不是橙白紫呢?
因为红绿蓝是计算机中的三原色,就是红绿蓝这三种颜色不能够再分解为其他多种颜色的组合,而其他任何颜色都可以用这三种颜色组合而成。
②我们用多大的数字或者说计算机用多少位来表示RGB每个分量?用8位(0~255),还是16位(0 ~ 65535),还是更高?
RGB各分量的值一般用0~255表示,因为每个分量可表示的颜色为256种,那么三种颜色混合起来就能表示出256 256 256 =16777216 (1600多万) 种颜色,而我们人眼实际上分辨不出来那么细致的颜色,所以完全够用了。
RGB分量这个分量我们又称之为通道
除RGB通道之外,对于透明图片还有个A(alpha,阿尔法)通道,用于表征该像素点的透明度。
如果一张图片只包含RGBA中的一个,我们就说这张图片是单通道图片,也就是说每个像素点只占8位;
如果一张图片只包含RGB,我们就说这张图片是三通道图片,也就是说每个像素点占24位;
如果一张图片包含RGBA,我们就说这张图片是四通道图片,也就是说每个像素点占32位;
当然我们在shader中使用图片时,不一定非要按照RGBA就对应红绿蓝、透明度的格式来使用,在RGBA通道我们可以存储任何我们想存的信息。

比如我们可以把高光反射的强度存储在R通道,把边缘光照的强度存储在G通道,把高光反射的指数部分存储在B通道,最后把自发光的强度存储在A通道。
出处:《Unity Shader入门精要》 7.4.2节 其他遮罩纹理

此外,我们知道了图像的通道数和分辨率就能计算出这张图像占的内存大小。比如下面这张jpg图片,分辨率为474×296,三通道,那此张图片占用内存 = 474×296×3÷1024=411.04Kb。
老爹
我们计算的和Unity中展示的相同。
老爹在Unity中占用的内存大小
但是这张图片保存在硬盘上只占26.5Kb,为什么呢?这是因为图片保存的格式是.jpg的,这种格式是一种压缩格式,所以在硬盘上的大小比内存中会小一些。
老爹硬盘上的大小
说了这么多,跟灰度化没多大关系,捂脸。言归正传,看看灰度化
灰度化其实很简单,把RGB各个通道的值全部变成相同即可。
这个值我们又称之为灰度值
那么问题来了,灰度值该怎么计算?

2.灰度值计算方法

目前常用的方式有如下4种:

  • 分量法,取RGB中仍一分量的值
  • 最大值法,取RGB中的最大值
  • 平均值法,RGB三份量取平均值,(R+G+B)/3
  • 加权平均法,该方法根据重要性及其它指标,将三个分量以不同的权值进行加权平均。由于人眼对绿色的敏感最高,对蓝色敏感最低,因此,按下式对RGB三分量进行加权平均能得到相对合理的灰度图像。灰度值 = 0.299R+0.578G+0.114*B

3.灰度化的目的

前面说到将彩色图像转化为灰度图像的过程是图像的灰度化处理过程。彩色图像中的每个像素的颜色由R,G,B三个分量决定,而每个分量中可取值0-255,这样一个像素点可以有1600多万(256256256=16777216)的颜色的变化范围。而灰度图像是R,G,B三个分量相同的一种特殊的彩色图像,其中一个像素点的变化范围为256种,所以在数字图像处理中一般将各种格式的图像转化为灰度图像,使得后续的图像的计算量少一些。灰度图像的描述与彩色图像一样仍然反映了整副图像的整体和局部的色度和高亮等级的分布与特征。现在大部分的彩色图像都是采用RGB颜色模式,处理图像的时候,要分别对RGB三种分量进行处理,实际上RGB并不能反映图像的形态特征,只是从光学的原理上进行颜色的调配。图像灰度化处理可以作为图像处理的预处理步骤,为之后的图像分割、图像识别和图像分析等上层操作做准备。

4.Shader实现

原理弄清楚了就该写代码了。
我们这里要在UI上实现灰度化,那问题来了,Unity上UI的shader该怎么写?
Unity内置的Shader都是开源的,咱们可以在https://unity.cn/releases下载对应版本的内置Shader源码。
下载内置Shader
那么我们只需要在内置Shader的基础上更改就行了。
在Unity中我们创建一张Image,会发现它使用的默认Shader是UI/Default。
在这里插入图片描述
然后我们打开下载的内置Shader,找到在DefaultResourcesEctra》UI下找到UI-Default这个Shader,用记事本打开。
UI-Default
然后在Unity中新建一个Shader,将UI-Default的内容完整拷贝到我们新建的Shader中。然后开始愉快的灰度化图像。
这里,我们采用加权平均值法。
即,灰度值 = 0.299×R + 0.587×G + 0.114×B。
我们只需要在片元着色器末尾加上以下代码即可:

fixed gray = dot(color.rgb, fixed3(0.299, 0.587, 0.114));
color = half4(gray, gray, gray, color.a);

Scene层次结构
效果如下。
UI灰度化
完整Shader如下.

// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "Custom/UI/Gray"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                OUT.color = v.color * _Color;
                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                // 加权平均法灰度化
                fixed gray = dot(color.rgb, fixed3(0.299, 0.587, 0.114));
                color = half4(gray, gray, gray, color.a);
                return color;
            }
        ENDCG
        }
    }
}

5.参考文章