美文网首页
图像处理之LUT

图像处理之LUT

作者: BohrIsLay | 来源:发表于2020-12-06 16:01 被阅读0次

    目录

    • LUT原理

    • LUT滤镜特效Shader解读

    演示demo

    前言:

    平常我们使用的图片编辑app,一般都有这个最基本的LUT滤镜,都是UI设计师设计好风格后,导出LUT效果图,有了这个LUT效果图,在app端,借助GPUImage库,便可以方便的实现这种特效。

    甚至有些开发者,是直接破解别人的app,拿到里面的LUT图,不需要自己专门设计,便能轻易的窃取到别人app里这些好看的风格的滤镜

    LUT在图像处理中的位置

    先从大方向上看LUT在数字图像处理上处于什么位置

    图像处理大致可以分为以下几类:

    • 独立像素点运算

    包括亮度、对比、饱和度、色调、灰色化等

    • 多个像素点运算

    一般是进行卷积变换,求均值,求中值,插值等,包括边缘检测、浮雕化、模糊、锐化

    • 几何变化

    矩阵变换。包括缩放、旋转、倾斜、扭曲、液化等

    • 多图像合成

    多张图像的处理,包括添加水印,贴纸,美妆等。

    LUT归为独立像素点运算这种图像处理

    LUT介绍

    LUT 是 LookUpTable 的简称,也称作颜色查找表

    从名字上看,我们大概可以知道,是用来查找颜色的,他确实就是这样的,是通过一种颜色,查找其映射后的颜色,可以理解为一个函数LUT(R1,G1,B1),带R,G,B三个自变量的函数,输出为其对应映射后的值R2,G2,B2

    LUT(R1, G1, B1) = (R2, G2, B2)
    

    对于RGB颜色空间来说,颜色是由RGB三种基色构成,所有像素点的颜色值,都可以通过RGB组合而成,我们量化颜色时,一般使用8bit来表示RGB中的一个分量,所以每一个分量可以表示为2的8次方,即0~255。

    那么要建立一个颜色映射表,我们就可以使用255* 255* 255种来表示所有颜色映射后的值

    这是非常精准的,每一种颜色都可以查找到

    当我们需要把一幅原始图像的颜色风格转换为一种怀旧风格时,

    上面的这个需求,我们完全可以搞一个三维数组,数组的大小256 * 256 * 256,数组存储所有怀旧风格颜色映射后的值,直接通过查表,就能转换风格了,确实可以的

    但是上面的这种实现存在一些问题:

    • 1.数字太难管理,难于保存,难于复用,容易出错,一不小心修改一个数字,因为什么原因丢失,弄错,效果就变了,定位错误非常困难

    • 2.颜色查找表格过大

    而LUT能很好的解决上面的问题。

    首先解决第一个难以管理,容易出错,出错难以排查的问题。

    如果我们使用一张图存储上述信息,那么就不会容易出错了,图保存和复用非常方便,也不容易出错,图没错,信息就不会有错误。

    没有找到 256 * 256 * 256 的LUT图(即16 * 16个方格的LUT图,每个方格里有16 * 16个小格),因为没有这种设计,所以找不到,但是完全可以使用64 * 64 * 64的图,代替256 * 256 * 256的表格,来保存数据,这样图代替表格完成数据保存,就解决了表格数据容易出错,出错后排查困难的问题。

    我们现在使用64 * 64 * 64颗粒度的LUT图保存数据,即将256归化到64而已。

    那怎么用一张图来代替数据表来保存信息呢,这是一个巧妙的设计。

    下面是一个LUT效果图

    image

    LUT就是用图代替了表,完美的解决了上面表存在的问题

    我们来看下,LUT如何能代替颜色查找表的功能

    比如想查找纯蓝色色(0,0,1)对应的映射值

    LUT(R1, G1, B1) = (?, ?, ?)
    

    我们先看这个颜色查找表的特征:

    粗看的话,我们可以得出一个结论:
    8 * 8个方格,总体上,底部越来越蓝;而对于每一个方格,则越往右越红,越往下越绿;

    哈哈,没错,我猜你会关联到了我们的RGB颜色了。

    这是一个64 * 64 * 64颗粒度的LUT设计,总的方格大小为512 * 512, 8 * 8 64个方格,所以每个方格大小为64 * 64。

    64个方格,每个方格大小为 64 * 64 , 所以叫做64 * 64 * 64颗粒度的设计。因为颜色值的范围为0~255,即256个取值,将256个取值归化到64。

    从左上到右下(可以想作z方向),越来越蓝,蓝色值B从0~255,代表用来查找的B,即LUT(R1,G1,B1) = (R2,G2,B2)中的B1,
    
    每一个方格里,从左往右(x方向),红色值R从0~255,代表用来查找的R,即LUT(R1,G1,B1) = (R2,G2,B2)中的R1;
    
    每一个方格里,从上往下(y方向),绿色值G从0~255,代表用来查找的G,即LUT(R1,G1,B1) = (R2,G2,B2)中的G1;
    
    

    因为一个颜色分量是0~255,所以一个方格表示的蓝色范围为4,比如最左上的方格蓝色为0~4,查找时,如果有某个像素的蓝色值在0~4之间,则一定是在第一个方格里查找其映射后的颜色

    通过颜色找位置,找到的位置对应的点的颜色即是这个颜色映射后的颜色,如下图的五角星⭐️的颜色既是某个颜色映射后的颜色

    image

    这样,我们就完成了使用图代替表格,进行数据的存储,并且对图的数据存储进行了4 * 4 * 4倍的压缩处理

    有了这些了解后,我们来尝试查找像素点归一化后的纯蓝色(0,0,1)的映射后的颜色。

    需求变为:

    我们是想通过颜色S,从LUT图上,查找其映射后的颜色T

    思路是,通过颜色S,找到其在LUT上的位置,位置上对应的颜色即是要找的颜色T

    • 1.使用蓝色B定位方格n
    
    n =  1(蓝色值) * 63(一共64个方格,从第0个算起) = 63
    
    

    故要定位的方格n是第63个

    • 2.定位在方格里的位置,使用R,G定位位置x,y
    x = 0(R值) * 63(每个方格大小为 64 * 64) = 0, y = 0(G值) * 63(每个方格大小为 64 * 64) = 0
    
    

    所以方格的(0,0)位置为要定位的x,y

    • 3.定位在整个图中位置

    在512 * 512的大小上,其坐标为:

    Py = floor(n/8) * 64 + y = 7 * 64 + 0 = 448;
    Px = [n - floor(n/8) * 8] * 64 + x = [63 - 7*8] * 64 + 0 = 448;
    P = (448, 448)
    
    
    其中 floor(n/8)代表位置所在行,每一行的长度为64,y为方格里的G定位的位置;
    [n - floor(n/8) * 8]代表位置所在列数,每一列的长度为64,x为方格里的R定位的位置;
    floor为向下取整,ceil为向上取整。比如2.3, floor(2.3) = 2; ceil(2.3) = 3;
    

    方格大小为512 * 512, 位置为P = (448, 448), 归一化后为(7/8, 7/8),很明显,颜色值(0,0,1)的位置确实在第63个方格的左上角

    4.计算映射后颜色
    // 这里使用GPU采样器对纹理采样
     vec4 newColor = texture(sample, texPos);
     
     其中texPos就是第三步的归一化后的P
    
    

    现在我们已经完成了通过LUT查找颜色的映射值,理解了这个之后,我们后面用代码来实现。

    现在,再回到我们说LUT能够解决前面的颜色查找表存在的问题

    1.数据大小:这里使用的是 512 * 512大小的尺寸的LUT图,比前面的颜色查找表要小

    2.图片很方便存储,移植性非常好,做好一次后,非常方便重复使用

    其他的优点:

    • 所有的算法计算,都是对LUT进行计算位置,取位置处的颜色值,有助于算法本身的保护

    • LUT的设计更容易进行热更新,设计师设计好效果可以动态发布

    缺点:

    • 1.LUT资源容易被破解、泄密(得到LUT图,就相当于得到一个算子)
    • 2.尺寸512 * 512 还是比较大,增加软件包的体积(可以考虑后下发来优化)

    思考:

    对于缺点2,LUT图大小一定要使用512 * 512吗?是否可以再缩小呢?
    

    我们前面论述的时候,已经将256 * 256 * 256 压缩到了 64 * 64 * 64,完全可以再压缩。

    实际上,我们一般设计为方形的,算法也简化,对于R,G,B系数是一样的处理,我们可以将256归化为64,还可以继续归化为16。 即从256 * 256 * 256变为16 * 16 * 16,其对应的LUT如下:

    image

    这个公司的一个项目里是有使用的

    补充

    如何查找颜色A(0.4,0.6,0.2)映射的颜色呢?

    • 1.使用蓝色B定位方格n
    n = b * 63 = 0.2 * 63 = 12.6
    

    要定位的方格n是第12.6个,是个小数,那到底是用第12个,还是用第13个呢?我们采用两个,对两个方格的取色结果进行混合即可,首先使用第12个方格计算,然后再使用第13个方格计算,最后混合两个方格的颜色

    • 2.在方格里,使用R,G定位位置x,y
    x = 0.4 * 63 = 25.2, y = 0.6 * 63 = 37.8
    
    
    • 3.定位两个方格对应的位置

    在512 * 512的大小上,计算其坐标:

    // 先使用第12个方格定位其坐标P1
    Py = floor(n/8) * 64 + y = 1 * 64 + 37.8 = 111.8;
    Px = [n - floor(n/8) * 8] * 64 + x = [12 - 1*8] * 64 + 25.2 = 281.2;
    P1 = (Px, Py)=(281.2, 111.8);
    
    归一化后为P1 = (281.2, 111.8)/512 = (0.549, 0.2184);
    
    // 先使用第13个方格定位其坐标P2
    Py = floor(n/8) * 64 + y = 1 * 64 + 37.8 = 111.8;
    Px = [n - floor(n/8) * 8] * 64 + x = [13 - 1*8] * 64 + 25.2 = 345.2;
    P2 = (Px, Py)=(345.2, 111.8);
    
    归一化后为P2 = (345.2, 111.8)/512 = (0.674, 0.2184);
    
    
    • 4.计算颜色
    // 这里使用GPU采样器对纹理采样
     vec4 newColor1 = texture(sample, texPos1);
     vec4 newColor2 = texture(sample, texPos2);
     
     其中texPos1就是第三步的P1, texPos2是第三步的P2
    
    • 5.混合颜色
    
    resColor = mix(newColor1, newColor2, a);
    
    a =   fract(blueColor);
    
    blueColor 为12.6,小数部分越大,越接近13,所以第13个方格占比越大
    
    ps:
    a = fract(blueColor); //fract(x) 获取x的小数部分
    mix(x, y, a); //取x,y的线性混合,x(1-a)+ya
    

    了解了这个后,再来看LUT滤镜里glsl代码的shader算法部分,就很容易了

    GLSL的LUT滤镜shader解读

    fragment half4 lookupFragment(TwoInputVertexIO fragmentInput [[stage_in]],
                                  texture2d<half> inputTexture [[texture(0)]],
                                  texture2d<half> inputTexture2 [[texture(1)]],
                                  constant IntensityUniform& uniform [[ buffer(0) ]])
    {
        constexpr sampler quadSampler;
        half4 base = inputTexture.sample(quadSampler, fragmentInput.textureCoordinate);
        
        // 获取蓝色
        half blueColor = base.b * 63.0h;
        
        // 通过蓝色计算两个方格quad1,quad2
        half2 quad1;
        quad1.y = floor(floor(blueColor) / 8.0h); 
        quad1.x = floor(blueColor) - (quad1.y * 8.0h);
        
        half2 quad2;
        quad2.y = floor(ceil(blueColor) / 8.0h); //ceil 向下取整,ceil(12.6) = 13, 解决跨行时计算问题,比如blueColor = 7.6,则取第7,8个方格,他们不在同一行
        quad2.x = ceil(blueColor) - (quad2.y * 8.0h);
        
        // 计算映射后颜色所在两个方格的位置的归一化纹理坐标
        float2 texPos1;
        texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * base.r);
        texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * base.g);
        
        float2 texPos2;
        texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * base.r);
        texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * base.g);
        
        // 取出对应坐标的颜色newColor1,newColor2
        constexpr sampler quadSampler3;
        half4 newColor1 = inputTexture2.sample(quadSampler3, texPos1);
        constexpr sampler quadSampler4;
        half4 newColor2 = inputTexture2.sample(quadSampler4, texPos2);
        
        // 混合颜色newColor1,newColor2,得到查找的颜色color_t
        half4 newColor = mix(newColor1, newColor2, fract(blueColor));
        
        // 调节强度时,将color_t和源色color_s进行混合
        return half4(mix(base, half4(newColor.rgb, base.w), half(uniform.intensity)));
    }
    
    
    • 1.通过B分量确定两个方格
        half2 quad1;
        quad1.y = floor(floor(blueColor) / 8.0h); 
        quad1.x = floor(blueColor) - (quad1.y * 8.0h);
        
        half2 quad2;
        quad2.y = floor(ceil(blueColor) / 8.0h); 
        quad2.x = ceil(blueColor) - (quad2.y * 8.0h);
        
       
        比如 base(0.4,0.6,0.2), 先确定第一个方格:
        
        base.b = 0.2,blueColor = 0.2 * 63 = 12.6,(即为第12个,第13个方格),但是我们要计算它坐在行和列, floor(12.6) = 12,  floor(12 / 8.0h) = 1,即第一行;
        floor(blueColor) - (quad1.y * 8.0h) = floor(12.6) - (1 * 8) = 4,即第4列;
        
        同理可以算出第二个方格为第1行,第5列
        
        //ceil 向下取整,ceil(12.6) = 13, 解决跨行时计算问题,比如blueColor = 7.6,则取第7,8个方格,他们不在同一行
    
    • 2.确定方格映射后的坐标
        float2 texPos1;
        texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * base.r);
        texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * base.g);
        
        float2 texPos2;
        texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * base.r);
        texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * base.g);
        
       其中 (quad1.x * 0.125) 表示行归一化的坐标,(quad1.y * 0.125)表示列归一化的坐标,一共8行,每一行的长度为1/8 = 0.125,一共8列,每一列的长度为1/8 = 0.125;
       
       0.125 * base.r表示一个方格里红色的位置,因为一个方格长度为0.125,r从0~1;绿色同理;
       
       需要留意的是这里有个0.5/512, 和 1.0/512.
       
       0.5/512 是为了取点的中间值,一个点长度为1,总长度512,取点的中间值,即为0.5/512;
       
       1.0/512, 是因为计算texPos2.x时,单独对于一个方格来说,是从0~63,所以为63/512,即0.125 - 1.0/512;
        
    
    • 3.取出两个方格里对应坐标的颜色newColor1,newColor2
        constexpr sampler quadSampler3;
        half4 newColor1 = inputTexture2.sample(quadSampler3, texPos1);
        constexpr sampler quadSampler4;
        half4 newColor2 = inputTexture2.sample(quadSampler4, texPos2);
    
    • 4.混合颜色newColor1,newColor2,得到查找的颜色color_t
        // 混合颜色newColor1,newColor2,得到查找的颜色color_t
        half4 newColor = mix(newColor1, newColor2, fract(blueColor));
    

    至此,LUT特效滤镜的实现原理,我们就明白了。

    参考:

    落影大神:https://www.jianshu.com/p/96a61110a5ae

    其他:https://www.jianshu.com/p/f39f051595bb

    相关文章

      网友评论

          本文标题:图像处理之LUT

          本文链接:https://www.haomeiwen.com/subject/gqodiktx.html