图像处理之LUT

  • 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 = 101.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 = 101.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 inputTexture [[texture(0)]],
                              texture2d 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特效滤镜的实现原理,我们就明白了。

参考:

落影大神:Metal图像处理——颜色查找表(Color Lookup Table) - 简书

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

你可能感兴趣的:(opengl,图像处理,计算机视觉,人工智能)