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的设计更容易进行热更新,设计师设计好效果可以动态发布
缺点:
思考:
对于缺点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)));
}
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个方格,他们不在同一行
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;
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));
至此,LUT特效滤镜的实现原理,我们就明白了。
参考:
落影大神:Metal图像处理——颜色查找表(Color Lookup Table) - 简书
其他:https://www.jianshu.com/p/f39f051595bb