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图片:
在正常情况下,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)。
float blueColor = textureColor.b * 63.0;
textureColor.b
是一个0-1之间的浮点数,乘以63确定B分量所在位置(0-63)),因为会出现浮点误差,所以才需要取两个B分量,也就是下一步的取与B分量值最接近的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);
在这段代码中有大量的常数出现,例如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
这个就很好理解了,因为上面算出来的结果都是小方格的左上角坐标,加上小方格横纵的一半坐标就是将该点移动到了小方格中心。
vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
因为刚刚已经提到了取值会出现浮点误差是因为使用的函数是texture2D,这时候就需要根据浮点数的到所谓整点的距离来计算其对应的颜色值。 fract函数是指取小数部分,mix函数的内部展开是:
mix($color-1,$color-2,$weight)=newColor1*weight+newColor2*(1-weight)
gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), adjust);
经过以上几个步骤就能拿到滤镜完全映射后的结果,最后一步我们将原图与映射后的图进行插值混合,这次的插值混合是实现我们常见的滤镜调节功能,adjust调节范围为(0-1)取0时完全使用原图,取1时完全取映射后的滤镜图。
到此步长为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滤镜之后,通过更改输入的资源就可以得到不同的效果,从设计师到效果上线不需要再次开发,节省人力,缩短时效。
缺点:
以上就是对于颜色滤镜的全部介绍。滤镜包含的分支很多,除了本文介绍的颜色滤镜,还有几何滤镜(也叫变形滤镜)混合滤镜,智能滤镜等,感兴趣的同学欢迎交流学习。