13.LUT滤镜
LUT全称Look Up Table,也称为颜色查找表。
它代表的是一种映射关系,通过LUT可以将输入的像素数组通过映射关系转换输出成另外的像素数组。
比如一个像素的颜色值分别是R1 G1 B1,经过一次LUT操作后变为R2 G2 B2。
LUT从查找方式上可以分为1D LUT和3D LUT:
- 1D LUT
对于一张RGB图像,每个通道都可以作为一个输入,用公式可以描述如下:
由于颜色值的范围是(0~255),我们可以用256*3
的表来表示一个1D LUT,在实际操作中,我们通常以一张256*3*1
的图片来存储这个映射表,如下,这是一张1D LUT在Mac访达中文件信息图:
由于1D LUT各个通道都是相互独立的,无法对其他通道产生影响,因此1D LUT只能用来调节亮度/伽马/饱和度/色彩平衡等,如果我们希望对其他通道产生影响,就需要了解另一种滤镜查找方式--3D LUT。
- 3D LUT
3D LUT在滤镜中的影响比1D LUT更为深刻,假设上图3D中的红色平面发生移动,其中对应的绿色和蓝色分量也会发生改变,也就是说,一个颜色通道改变可以影响其他的颜色通道。
理论上3D LUT可以在立体色彩空间中描述所有颜色调整行为,所以它可以处理任何显示的非线性属性,从简单的gamma值、颜色范围和追踪错误,到修正高级的非线性属性、颜色串扰(去耦)、色相、饱和度、亮度等,3D LUT都可以胜任。
常见的3D LUT滤镜文件与.cube或者.3dl都是把3D坐标二维化后的数据表现,如下图
我们看到的这些方格子里面的蓝色是固定的,然后每个格子横坐标是红色,纵坐标是绿色,最左上角的格子因为蓝色全无,所以红色和绿色就很明显,而最右下角的那个格子,蓝色色值达到最大,因此整体看上去就非常的蓝(从左上角的格子到右下角的格子,一共是8行8列,正好是64个格子)。
为什么是64x64
对于RGB颜色,每种颜色可以有256(0 ~ 255)种取值,而每个颜色占用3个字节,因此一个3D LUT如果全量表示的话,大小为256*256*256*3
个字节,即48M内存空间。
如果我们有一个大小为256*256*256*3
的3D LUT文件,那么颜色映射将非常简单,只要根据RGB的像素值按照用蓝色找到对应的格子,然后用红色和绿色找到对应格子的横列就可以找到映射的颜色值。
但是通常情况下,我们不会这么干,因为一张图48M存储空间实在是太大了。
如果要完全记录这种映射关系,设备需要耗费大量的内存,并且可能在计算时因为计算量大而产生性能问题, 为了简化计算量,降低内存占用,可以将相近的n种颜色采用一条映射记录并存储,(采样步长,n通常为4),这样只需要64 * 64 * 64 * 3
个字节就可以表示原来256 * 256 * 256 * 3
的颜色数量,我们也将4称为采样步长。
对于忽略的颜色值可以进行插值获得其相似结果。
在专业领域一般认为16 * 16 * 16
的3D LUT足够适用于预览和监看。64 * 64 * 64
或者更大的3D LUT更适合渲染和调色。
3D LUT映射
要想熟练使用LUT滤镜,我们首先要了解它是怎么建立颜色映射关系的, 我们看下以下这张图,这张图展示了在LUT中RBG颜色是如何实现映射关系的:
LUT图在横竖方向上被分成了8 X 8一共64个小方格,每一个小方格内的B(Blue)分量为一个定值,64 个小方格一共表示了B分量的64种取值。
对于每一个小方格,横竖方向又各自分为64个小格:以左上角为原点,横向小格的 R(Red)分量依次增加,纵向小格的 G(Green)分量依次增加
在使用上面这张LUT表的时候首选需要找对B分量对应的小格子,然后在找到的小格子上再计算出R分量和G分量的映射结果即可得到完整的RGB映射结果。
下面我们就以一个64*64*64
的3D LUT为例,来说明一下3D LUT如何查找和使用的(用glsl来实现):
- 整张图从左到右和从上到下为蓝色的渐变,每个小格子从左到右为红色渐变,从上到下为绿色渐变
- 肉眼可见的有8*8个方格,假设整个图的范围我们定义为0.0~1.0 ,也就是说每个格子所表示的范围是1./8. = 0.125
- 我们首先通过蓝色来查找我们在哪个方格,由于涉及到插值,因此通过蓝色进行查找的时候,可能不会那么幸运一定映射到整数的格子上面,因此我们需要通过向下取整和向上进位两种方式来获取蓝色所对应的方格,然后通过蓝色的颜色值来mix这两种的颜色值,从而获取最终的颜色值。
映射示例
1.1 寻找B值的两个格子
B值对应的是0 ~ 63,共64个格子,在计算时,并不一定会是一个整数,对应到某一个格子,大概率会是一个小数,为了能够更精确的进行映射,需要使用两个格子,然后根据小数(因子)进行插值计算,从而能够更精确。
蓝色值用来定位两个相邻的小格子
- 首先是对原图进行像素采样
vec4 textureColor = texture(s_texture, v_texCoord);
textureColor.b是一个0 - 1之间的浮点数,乘以63用来确定B分量所在的格子.
因为会出现浮点误差,所以才需要取两个B分量,也就是下一步的取与B分量值最接近的2个小方格的坐标,最后根据小数点进行插值运算。
- 第一个小格子:
vec4 color = texture(s_texture, v_texCoord);
float blueColor = color.b * 63.0;
vec2 quad1;
// blueColor = 25.4, 第3行的第1个小格子
// floor:向下取整
quad1.y = floor(floor(blueColor) / 8.0);
quad1.x = floor(blueColor) - (quad1.y * 8.0);
- 第二个小格子:
vec2 quad2;
// blueColor = 25.4,第3行的第2个小格子
// ceil:向上取整
quad2.y = floor(ceil(blueColor) / 8.0);
quad2.x = ceil(blueColor) - (quad2.y * 8.0);
确定每个格子内具体点对应的色值
通过R和G分量的值确定小方格内目标映射的RGB组合的坐标,然后归一化,转化为纹理坐标。
每个B格子代表的纹理坐标长度是 1/8 = 0.125 63 textureColor.r是将当前实际像素的r值映射到0 ~ 63,再除以512是转化为纹理坐标中实际的点,LUT图的分辨率为`512512`
红色值和绿色值用来确定相对于整个LUT的纹理坐标:
vec2 texPos1;
texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);
texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);
vec2 texPos2;
texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);
texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);
- 上面texPos1.x,texPos1.y代表的是在0-1的纹理坐标中该点(texPos1)的具体坐标。
(quad1.x * 0.125)
指的是在纹理坐标中的左上角的归一化坐标- quad1.x代表当前格子在
8*8
的格子中横坐标的第几个,这个8*8
的格子构成了0-1的纹理空间,所以一个格子代表的纹理坐标长度就是 1/8 = 0.125 - 所以第几个格子就代表了具有几个0.125这样的纹理长度
所以
(quad1.x * 0.125)
指的就是当前格子的左上角在纹理坐标系中的横坐标的具体坐标点。
- quad1.x代表当前格子在
((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
)
这个步骤结束就在这一个小方格中根据R分量和G分量又确定了更小的一个方格的左上角的坐标点。如下图
- 0.5/512.0
这个就很好理解了,因为上面算出来的结果都是小方格的左上角坐标,加上小方格横纵的一半坐标就是将该点移动到了小方格中心。
通过纹理坐标获取两个新的颜色
vec4 newColor1 = texture(textureLUT, texPos1);
vec4 newColor2 = texture(textureLUT, texPos2);
然后根据蓝色值小数部分作为权重做线性混合,获取最终的颜色输出
// mix(x, y, a) -> x * (1 - a) + y * a
// 使用mix方法对2个边界像素值进行插值混合
// 根据浮点数到整点的距离来计算其对应的颜色值,fract函数是指小数部分。
vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
// 将原图与映射后的图进行插值混合
// 这次的插值混合是实现我们常见的滤镜调节功能,adjust调节的范围是(0-1),取0时完全使用原图,取1时完全使用映射后的滤镜图
gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), adjust);
片元着色器代码:
#version 300 es
precision mediump float;
in vec2 v_texCoord;
out vec4 outColor;
uniform sampler2D s_texture;
uniform sampler2D textureLUT;
//查找表滤镜
vec4 lookupTable(vec4 color){
float blueColor = color.b * 63.0;
vec2 quad1;
quad1.y = floor(floor(blueColor) / 8.0);
quad1.x = floor(blueColor) - (quad1.y * 8.0);
vec2 quad2;
quad2.y = floor(ceil(blueColor) / 8.0);
quad2.x = ceil(blueColor) - (quad2.y * 8.0);
vec2 texPos1;
texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);
texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);
vec2 texPos2;
texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);
texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);
vec4 newColor1 = texture(textureLUT, texPos1);
vec4 newColor2 = texture(textureLUT, texPos2);
vec4 newColor = mix(newColor1, newColor2, fract(blueColor));
return vec4(newColor.rgb, color.w);
}
void main(){
vec4 tmpColor = texture(s_texture, v_texCoord);
outColor = lookupTable(tmpColor);
}
- 邮箱 :[email protected]
- Good Luck!