【OpenGL学习】3DLUT颜色滤镜

2 篇文章 1 订阅
1 篇文章 0 订阅

LUT颜色滤镜

LUT颜色滤镜是指通过LUT的方式来实现的颜色滤镜。也把它叫做LUT滤镜,LUT滤镜是当前各大主流美图/视频软件滤镜的主要实现方案,通过添加不同的LUT滤镜使画面展示出不一样的色彩。下图是项目中使用“湛蓝”LUT滤镜渲染的前后对比。

在这里插入图片描述
在这里插入图片描述

LUT是什么?

LUT(Look Up Table)指的是颜色查找表,是色彩映射关系的管理,例如:当原始R值为0时,输出R值为5;当原始R值为1时,输出R值为6;当原始R值为2时,输出R值为8;当原始R值为3时,输出R值为10;…我们把提前定义好的这种对应关系,存储在一张图中,这个图就叫做LUT,也就是原始颜色通过LUT的颜色查找表会映射到新的色彩上去,对这样一堆映射关系的管理。
LUT滤镜的本质上属于独立像素点替换,让每一个像素点都会对应一个新的颜色值,这个新色值就是最终呈现出来的滤镜色。
LUT分为1DLUT,2DLUT,3DLUT三种,目前视频/图片滤镜技术中应用最广泛的是3DLUT,下图是一张分辨率为 512*512 的LUT图片:
在这里插入图片描述

为什么要使用LUT?

在正常情况下,RGB的颜色模式可以表示的颜色数量为256X256X256种,如果要完全记录这种映射关系,需要大量的内存,并且在计算时工作量巨大,为了简化计算量,降低内存占用,3DLUT以一定的采样间隔,将相近的n种颜色采用一条映射记录并存储,(n通常为4)这样只需要64 X 64 X 64种就可以表示,我们也将4称为采样步长。降低了存储映射关系所用的内存空间,充分利用GPU的计算能力。

##如何使用LUT进行像素点替换?
想要使用它,就必须了解它,先来了解一下LUT图是怎么对颜色做一一对应的映射,首先一张LUT图在横竖方向上被分成了8*8 一共有64个小方格,每一个小方格内的B(blue)分量为一个定值,64个小方格一共就表示了B分量的64种映射取值。B分量的取值分布如下图所示
在这里插入图片描述

这64个小方格内又被分成了横竖64*64的小方格,其中横坐标代表R分量的64种映射情况,纵坐标代表了G分量的64种映射情况,我们以其中一个小方格为例,展示了R分量和G分量的取值分布如下图
在这里插入图片描述

这样,RBG三个分量的64种取值范围,就用这样一张图表示映射出来了。图中Target点是某点的RBG值在LUT图中的颜色值映射点。 通过获取原始图像中某点的像素RGB三分量的取值,定位到Target坐标点,此时获取到的Target坐标点的像素值,即是该点的像素映射值。我们将这个过程放在shader中完成。
网上关于LUT在shader中的应用有一段现成的代码,代码中有大量常数值,难以理解,接下来我会解释算法中每一句,每一个常量数值的含义,做到知其然更知其所以然。
##LUT滤镜算法解析
首先,我们先看一下这段代码:

void main()
{
    //首先原始采样像素的 RGBA 值
    vec4 textureColor =texture2D(uTexture, vTextureCoord);
    //解析点1
    float blueColor = textureColor.b * 63.0;

    //取与 B 分量值最接近的 2 个小方格的坐标
    vec2 quad1; 
    quad1.y = floor(floor(blueColor) / 8.0);   //floor 向下取整 
    quad1.x = floor(blueColor) - (quad1.y * 8.0);
    vec2 quad2;
    quad2.y = floor(ceil(blueColor) / 7.9999);
    quad2.x = ceil(blueColor) - (quad2.y * 8.0);
    
    //解析点2
    vec2 texPos1; 
    texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
    texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
    vec2 texPos2;
    texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
    texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);

    //取目标映射对应的像素值
    vec4 newColor1 = texture2D(s_LutTexture, texPos1);
    vec4 newColor2 = texture2D(s_LutTexture, texPos2);
    
    //解析点3
    vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); 
    
    //解析点4
    gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), adjust);

}

由于以上代码含有大量的常量值,网上大多数博客都没有解释这些常数的含义,也就成了所谓的祖传代码。很难理解到算法是如何运行的,重点看一下这段代码中的几个关键步骤(解析点1-4)。

解析点1:获取B分量

float blueColor = textureColor.b * 63.0;
textureColor.b是一个0-1之间的浮点数,乘以63确定B分量所在位置(0-63)),因为会出现浮点误差,所以才需要取两个B分量,也就是下一步的取与B分量值最接近的2个小方格的坐标,最后根据小数点进行插值运算。

解析点2:计算 R分量和G分量

 vec2 texPos1; 
    texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
    texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
    vec2 texPos2;
    texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
    texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);

在这段代码中有大量的常数出现,例如0.125,0.5 /512,1.0/512,看起来毫无头绪,但其实是是有迹可循的。这四句代码是相同的算法,所以我们只分析其中的第一句

 texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);

先了解到(texPos1.x,texPos1.y)代表的是在0-1的纹理坐标中该点(texPos1)的具体坐标),我们再来一个个来看这段代码是什么含义

  • (quad1.x * 0.125)
    指的是在纹理坐标中的点左上角的归一化坐标,什么意思呢,quad1.x代表当前格子在8X8的格子中横坐标的第几个,这个8X8的格子构成了0-1的纹理空间,所以一个格子代表的纹理坐标长度就是1/8 = 0.125,所以第几个格子就代表了具有几个0.125这样的纹理长度,所以指的是当前格子的左上角在纹理坐标系中的横坐标的具体坐标点,(quad1.x * 0.125)同理指纵坐标的具体坐标点。如下图所示:
    在这里插入图片描述

  • ((0.125 - 1.0/512.0) * textureColor.r)
    这段代码可能是最看不懂的一段了,其实是这里是一个计算步骤的省略如下:
    ((0.125 - 1.0/512.0) * textureColor.r) = ((64-1)* textureColor.r)/512这样就可以理解了,
    (64-1)* textureColor.r 意思是首先将当前实际像素的r值映射到0-63 的范围内。除以512是转化为纹理坐标中实际的点。(纹理的分辨率为512*512) textureColor.G同理,这个步骤结束就在这一个小方格中根据R分量和G分量又确定了更小的一个方格的左上角的坐标点。如下图
    在这里插入图片描述

  • 0.5/512.0
    这个就很好理解了,因为上面算出来的结果都是小方格的左上角坐标,加上小方格横纵的一半坐标就是将该点移动到了小方格中心。

解析点3:使用Mix方法对2个边界像素值进行插值混合

  vec4 newColor = mix(newColor1, newColor2, fract(blueColor));

因为刚刚已经提到了取值会出现浮点误差是因为使用的函数是texture2D,这时候就需要根据浮点数的到所谓整点的距离来计算其对应的颜色值。 fract函数是指取小数部分,mix函数的内部展开是:
mix($color-1,$color-2,$weight)=newColor1*weight+newColor2*(1-weight)

解析点4:将原图与映射后的图进行插值混合

gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), adjust);

经过以上几个步骤就能拿到滤镜完全映射后的结果,最后一步我们将原图与映射后的图进行插值混合,这次的插值混合是实现我们常见的滤镜调节功能,adjust调节范围为(0-1)取0时完全使用原图,取1时完全取映射后的滤镜图。

LUT滤镜算法优化

到此步长为4的LUT算法(分辨率为512X512LUT图)已经解释清楚了,这个时候有的同学就要问了,步长一定是4吗? 答案是:不一定,因为LUT采用了采样(于一个样值序列间隔几个样值取样一次,这样得到新序列就是原序列的下采样)的方式,目的是为了减少数据量。但对于采样间隔并没有规定,所以常见的还有一种采样间隔(步长)为16的3DLUT滤镜,下图是一张分辨率为64X64的图片,很明显 刚刚的算法就不能适用于这种图片了。
在这里插入图片描述

在我们项目中滤镜能力是对外开放的,资源入口暴露在外面的。业务线可以不依赖sdk,根据他们场景滤镜的需求,输入相应的lut图,所以我们也不知道输入的LUT滤镜是以什么为步长的,基于以上不可控的场景 将算法优化为:

// 适配不同的lut滤镜图片 纹理宽度开三次根号的值 例如 4:表示每个通道16位(16*16*16)  8:表示每个通道64位(64*64*64)
uniform mediump float  matchLut;

vec4 lookup(in vec4 textureColor){
    mediump float blueColor = textureColor.b * (pow(matchLut, 2.0)-1.0);
    mediump vec2 quad1;
    quad1.y = floor(floor(blueColor) / matchLut);
    quad1.x = floor(blueColor) - (quad1.y * matchLut);
    mediump vec2 quad2;
    quad2.y = floor(ceil(blueColor) / matchLut);
    quad2.x = ceil(blueColor) - (quad2.y * matchLut);
    highp vec2 texPos1;
    texPos1.x = (quad1.x *(1.0/matchLut)) + 0.5/(pow(matchLut, 3.0))+ ((1.0/matchLut - 1.0/pow(matchLut, 3.0)) * textureColor.r);
    texPos1.y = (quad1.y *(1.0/matchLut)) + 0.5/(pow(matchLut, 3.0)) + ((1.0/matchLut - 1.0/pow(matchLut, 3.0)) * textureColor.g);
    highp vec2 texPos2;
    texPos2.x = (quad2.x *(1.0/matchLut)) + 0.5/(pow(matchLut, 3.0)) + ((1.0/matchLut - 1.0/pow(matchLut, 3.0)) * textureColor.r);
    texPos2.y = (quad2.y *(1.0/matchLut)) + 0.5/(pow(matchLut, 3.0)) + ((1.0/matchLut - 1.0/pow(matchLut, 3.0)) * textureColor.g);
    lowp vec4 newColor1 = texture2D(uTexture2, texPos1);
    lowp vec4 newColor2 = texture2D(uTexture2, texPos2);
    lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
    return newColor;
}

由于原理相同,对于这段代码就不做详细的解释了,可以根据第一段的解释尝试理解,举一反三。

LUT滤镜优缺点小结

就目前而言,几乎所有的图像/摄像类软件,图像处理软件都在使用LUT滤镜,凡是涉及像素调色内容,都是可以使用LUT滤镜完成,使用LUT滤镜的优缺点如下:
优点:

  • 极大的简化了代码,将复杂的算法计算简化为一次LUT操作,有助于算法本身的代码保护。
  • LUT更容易进行OpenGL渲染,可以轻松应用于Camera实时预览和视频实时处理,速度快,效果稳定。
  •  LUT的设计更容易进行资源化配置,开发出一个模版LUT滤镜之后,通过更改输入的资源就可以得到不同的效果,从设计师到效果上线不需要再次开发,节省人力,缩短时效。
    

缺点:

  • 由于LUT是以图像资源的方式存在,而且必须无压缩,因为也会占用内存空间,随着LUT数量的增多,会增加软件包体积的大小。目前主流的优化方式是将资源线上化,用户使用到某个滤镜时再去下载。
  • LUT资源容易被破解,泄密。

以上就是对于颜色滤镜的全部介绍。滤镜包含的分支很多,除了本文介绍的颜色滤镜,还有几何滤镜(也叫变形滤镜)混合滤镜,智能滤镜等,感兴趣的同学欢迎交流学习。

  • 11
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
### 回答1: 3DLUT是一种三维查找表,可以用于色彩矫正。要制作3DLUT,首先需要使用色彩校准工具对摄像机或显示器进行校准,然后使用色彩测量仪器测量不同的色彩样本。接下来,使用专业的软件将这些样本转换为3D LUT文件,最后将其应用于视频或图像中以进行色彩矫正。 ### 回答2: 3DLUT(三维查找表)是一种用于色彩矫正的技术。色彩矫正旨在调整图像的色彩和对比度,以使其更加准确地呈现真实的颜色。 首先,我们需要收集一组已知的色彩参考样本。这可以通过使用色彩卡来完成,其中包含已知颜色的标准样本。这组样本将成为我们色彩矫正的参考。 接下来,通过使用专业的色彩校准仪或固定在显示器上的硬件器件,测量显示器上每个像素的色彩输出。这些测量值将与已知的参考样本进行比较,以确定显示器上每个像素的色彩偏差。 然后,使用色彩校正软件创建3DLUT文件。该文件根据测量的色彩偏差和参考样本之间的差异,提供调整色彩的数学算法。3DLUT包含了色彩校正的预设值,可以将显示器的输出调整到更准确的颜色。 最后,将3DLUT文件加载到图像处理软件或显示设备上。当图像显示时,软件或设备会采用3DLUT文件中的校正值对图像进行处理,以准确呈现真实的颜色。 值得注意的是,为了取得最佳的色彩矫正效果,需要周期性地重新校准显示设备,因为显示器的色彩输出会随着时间和使用而发生变化。重新校准后,需要重新创建和加载新的3DLUT文件。 总之,通过收集参考样本、测量像素的色彩偏差、创建3DLUT文件以及加载到图像处理软件或显示设备中,我们可以进行色彩矫正,从而获得更准确、真实的颜色输出。 ### 回答3: 3DLUT(三维查找表)是一种用于色彩矫正的工具。它通过将一组输入颜色值与目标颜色值进行匹配,从而实现对图像的色彩精细调整。 首先,需要创建一个3DLUT文件。这可以通过将标准图像与目标图像进行比较来完成。标准图像应该是具有准确颜色表示的样本图像,而目标图像则是需要进行色彩矫正的图像。 在创建3DLUT文件之后,可以将其应用于需要进行色彩矫正的图像。通常情况下,图像矫正软件将提供一个选项,允许用户导入3DLUT文件并将其应用于图像。 当3DLUT被应用于图像时,它会根据LUT文件中的映射规则对图像中的每个像素的颜色进行修改。每个像素的颜色值将被映射到LUT文件中对应的目标值。通过这种方式,图像的色彩会被精确地调整以匹配目标颜色。 一旦应用了3DLUT,可以再次对图像进行预览和调整。如果输出结果与预期不符,可以通过修改3DLUT文件中的映射关系来改变颜色调整效果。通过不断的试验和调整,可以达到满意的色彩矫正结果。 总而言之,3DLUT通过将输入图像的颜色值映射到目标图像颜色值,实现对图像的色彩矫正。使用3DLUT需要先创建LUT文件,然后将其应用于待矫正的图像,最后调整映射关系以达到理想的色彩效果。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值