移动端滤镜实现(基于 LUT)

移动端滤镜实现(基于 LUT)

1、主要实现方式

  • ** 像素变换 ** 包括亮度、对比度、饱和度、色调、灰色度等
  • ** 卷积变换 ** 浮雕化、模糊、锐化等
  • ** 矩阵变换 ** 缩放、旋转、扭曲、液化等
  • ** 图像合成 **

目前最简单的方式是像素变换,分别对每个像素的 RGBA 做变换,这种方式是像素独立的。

2、LUT(Look Up Table)

全称 Look Up Table,也叫颜色查找表,它代表输入的像素数组跟输出的像素数组的映射关系。比如一个像素的颜色值分别是 R1,G1,B1, 经过一次 LUT 操作:

1D LUT
LUT(R1) = R2
LUT(G1) = G2
LUT(B1) = B2

3D LUT
LUT(R1, G1, B1) = (R2, G2, B2)

这个像素就从颜色 A(R1,G1,B1)变成了 B(R2,G2,B2),LUT 其实就是做一个颜色映射的操作。在 RGB 色彩空间中,RGB 三个值的取值范围为 [0,255],我们可以通过预先将目标滤镜效果保存为映射表,在需要进行滤镜变换时直接查找对应的颜色映射表即可。

2.1、1DLUT

LUT 可以分一维 LUT 和三维 LUT,对于一维 LUT,假设存在映射关系 LUT1,则:

LUT(R1) = R2
LUT(G1) = G2
LUT(B1) = B2

R1,G1,B1 为原始像素值,R2,G2,B2 为映射后的像素值,通过上面的关系可以看出 1DLUT 的颜色映射值仅与原始像素对应的分量相关,1DLUT 用图像表示如下:

一维 LUT

通过上面的介绍可以得知,一维 LUT 能实现的色彩控制十分的局限,一般用于控制画面的曝光伽马值等。而如果需要对色彩进行精准的映射,则需要用到 3DLUT。

2.2、2DLUT

众所周知,无论是使用相机拍照,还是预览照片,手机保存和处理的都是位图,对于 3DLUT,假设存在 LUT3,则

LUT3(R1, G1, B1) = (R2, G2, B2)

3DLUT 示意图如下:
三维 LUT

不难发现,3DLUT 相较与 1DLUT,几乎可以实现全立体色彩空间的控制,当然,缺点也很明显,3DLUT 相较于 1DLUT,所需要的空间变大了 N 倍(1DLUT:256 + 256 + 256,3DLUT:256 * 256 * 256)大约需要 48MB 的空间,如此大小的空间对移动端来说数据量就十分庞大了,所以通常会采用采样的方式来降低数据量,比如可以按照 4 为采样区间进行采样计算,即可以得到一个 64 * 64 * 64 大小的数据关系,对于不在表内的颜色值进行差值法获取对应的映射值。

2.3、3DLUT 存储方式

从 2.2 可以看到,3DLUT 是一个立体的三维图像空间,但是我们可以通过以 B 轴(蓝色值,从 0 点开始)为切面,将这个 3 维立方体切成 64 份,这样子每一份就是一张二维图片,并且此图片的规律为:从左到右红色值逐渐变大,从上到下绿色值逐渐变大,而蓝色值是从左上角到右下角逐渐变大。转换后的二维图片如下:
三维转二维
通过上面的方法,就可以将所有颜色数据存储到一张二维表格中,后续设计师只需要在一张标准 RGB 映射格式的图片上面做其他颜色滤镜的映射,APP 通过代码来加载不同的映射表格进行图片的二次处理,即可以输出不同效果的照片或者视频。

LUT 实现

其实 LUT 的实现原理都是通过 B 的值去定位小格子的具体位置,然后在定位到的格子中通过 R 和 G 的值找到对应的坐标,常见的处理框架有 GPUImage (Github Android)(Github IOS)、OpenGL-ES 等。

Android 端已经基于 GPUImage 实现了一个简易的滤镜 Demo

GPUImage 是一个基于 GPU 的很强大的开源图像处理库,性能十分优秀。而且 GPUImage 框架自身也提供了一个 GPUImageLookupFilter 滤镜,只需要传入处理好的 LUT 图片即可以实现 LUT 映射效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 // GPUImageView 源码
public void setFilter(GPUImageFilter filter) {
this.filter = filter;
gpuImage.setFilter(filter);
requestRender();
}

// GPUImageRenderer 源码
public void setFilter(final GPUImageFilter filter) {
runOnDraw(new Runnable() {

@Override
public void run() {
final GPUImageFilter oldFilter = GPUImageRenderer.this.filter;
GPUImageRenderer.this.filter = filter;
if (oldFilter != null) {
oldFilter.destroy();
}
GPUImageRenderer.this.filter.ifNeedInit();
// 底层依旧是基于 OpenGL-ES 处理的
GLES20.glUseProgram(GPUImageRenderer.this.filter.getProgram());
GPUImageRenderer.this.filter.onOutputSizeChanged(outputWidth, outputHeight);
}
});
}

// Activity
val gpuImageFilter = GPUImageLookupFilter().apply {
bitmap = BitmapFactory.decodeResource(resources, R.drawable.lut)
}
gpuImageView.filter = gpuImageFilter

GPUImage 分析

graph TD
Tips [GPUImage 处理数据流程图]
B[Bitmap] --> G[GPUImage]
Y [YUV 数组] --> G [GPUImage]
G[GPUImage] --> R[GPUImageRender -> GPUImageFilter]
R[GPUImageRender->GPUImageFilter] --> OutB[Bitmap]
R[GPUImageRender->GPUImageFilter] --> OutSV[GLSurcafeView/GLTextureView]

类图

类图

GPUImage 可以看作是模块对外的接口,它封装了 GPUImageFilter、GPUImageRenderer 及其渲染的一些属性,而 GLSurfaceView/GLTextureView 均由外部传入,并与 GPUImageRenderer、GPUImageFilter 建立起联系。
GPUImageRenderer 其继承自 Render 类,主要负责调用 GPUImageFilter 进行图像的处理(GPU),再渲染到 GLSurfaceView 中。
GPUImageFilter 是所有 filter 的基类,其默认实现是不带任何滤镜效果。而其子类可以直接继承自 GPUImageFilter 从而实现单一的滤镜效果。我们需要使用的是 GPUImageLookupFilter 类(继承自 GPUImageTwoInputFilter)。