Opengl ES之LUT滤镜(上)

简介: Opengl ES之连载系列

什么是LUT滤镜

从今天开始我们开始进入Opengl ES的滤镜专题,提到滤镜就不得不提用得最多的LUT滤镜了。

LUT全称LookUpTable,也称为颜色查找表,它代表的是一种映射关系,通过LUT可以将输入的像素数组通过映射关系转换输出成另外的像素数组。
比如一个像素的颜色值分别是 R1 G1 B1,经过一次LUT操作后变为R2 G2 B2:

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

通过这个映射关系就可以将一个像素的颜色转换为另外一种颜色。

为什么使用LUT滤镜

在正常情况下,8位的RGB颜色模式可以表示的颜色数量为256X256X256种,如果要完全记录这种映射关系,设备需要耗费大量的内存,并且可能在计算时因为计算量大而产生性能问题,
为了简化计算量,降低内存占用,可以将相近的n种颜色采用一条映射记录并存储,(n通常为4)这样只需要64X64X64种就可以表示原来256X256X256的颜色数量,我们也将4称为采样步长。

要想熟练使用LUT滤镜,我们首先要了解它是怎么建立颜色映射关系的, 我们看下以下这张图,这张图展示了在LUT中RBG颜色是如何实现映射关系的:

lut映射关系图

首先这张图的大小是512X512,在横竖方向上这张图都被分成了8个小方格,每个小方格的大小是64X64,也就是一张512X512的图被分割成64个小方格,每个小方格的大小是64X64,这64个小方格就代表了64种B通道的颜色映射,
然后每个B通道的小方格上又是一个64X64像素大小的图像,这个小图像的横坐标代表R分量的64种映射情况,纵坐标代表了G分量的64种映射情况,这样就刚好这就和采样步长是4的映射表对应上了。

在使用上面这张LUT表的时候首选需要找对B分量对应的小格子,然后在找到的小个子上再计算出R分量和G分量的映射结果即可得到完整的RGB映射结果。

如何在Opengl中使用LUT滤镜

如果我们想要对图像进行LUT滤镜处理比较常用的有两种方式,一种是使用OpenCV而另外一种就是使用Opengl了,今天我们就用opengl来实现一个LUT滤镜的小demo。

理论总是枯燥的,下面我们结合一个512X512的LUT滤镜片元着色器讲解以下映射过程:

#version 300 es
precision mediump float;
in vec2 TexCoord;
uniform sampler2D ourTexture;
uniform sampler2D textureLUT;
out vec4 FragColor;
vec4 lookupTable(vec4 color){
    float blueColor = color.b * 63.0;
    //取与 B 分量值最接近的 2 个小方格的坐标
    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()
{
    // 原始图像的RGBA值
    vec4 tmpColor = texture(ourTexture, TexCoord);
    // 通过原始图像LUT映射
    FragColor = lookupTable(tmpColor);
}

读懂上面的着色器程序需要了解一些Opengl的内置函数的意思,例如floor(x)是表示向下取整,返回小于等于x的整数,而ceil(x)表示向上取整,返回大于等于x的整数。
这部分的内容大家自行搜索学习即可。

注意上面这个着色器的算法仅仅适合与512X512的LUT图,没有适配到各种尺寸的LUT图。

  • 获取B分量

首先我们注意看第8行float blueColor = color.b * 63.0; 经过main行数中的texture采样出来的RGB值都在0到1之间,因此乘以63即可得到B分量所在的小方格,
但是因为会出现浮点误差,为了减少误差,所以取两个B分量,也就是与下一步的B分量值最接近的2个小方格的坐标,最后根据小数点进行插值运算。

其中第11到第12行的核心意思就是计算B分量的小格子在LUT的8X8各种的行列序号,quad1的x和y的值应该在0到7之间。

  • 获取RG分量

这部分的核心代码是16到21行,这个计算结果是归一化后的纹理坐标,所以x和y的值应该是在0到1之间,首先解析下(quad1.x * 0.125),在512的LUT图中,横竖都被分成了8个小方格,因此在归一化坐标中,
每个小方格所占的比例就是1/8=0.125,因此(quad1.x * 0.125)的意思就是当前格子的左上角在纹理坐标系中的横坐标的具体坐标点。

因为上面(quad1.x * 0.125)得出的是当前格子的左上角在纹理坐标系中的横坐标的具体坐标点,那么再加上0.5/512.0就是当前格子的中心点在纹理坐标系中的横坐标的具体坐标点。

最后看下((0.125 - 1.0/512.0) * color.r),其实这是一个经过优化后的计算,原始的计算应该是((0.125 - 1.0/512.0) * textureColor.r) = ((64-1)* textureColor.r)/512

(64-1)* textureColor.r 意思是首先将当前实际像素的r值映射到0-63的范围内,再除以512是转化为纹理坐标中实际的点,因为我们的LUT纹理图的分辨率为512*512。

同理G通道的计算也是一样的。

下面附上完整核心的demo渲染代码:

Lut2DOpengl.cpp


#include "Lut2DOpengl.h"
#include "../utils/Log.h"

// 顶点着色器
static const char *ver = "#version 300 es\n"
                         "in vec4 aPosition;\n"
                         "in vec2 aTexCoord;\n"
                         "out vec2 TexCoord;\n"
                         "void main() {\n"
                         "  TexCoord = aTexCoord;\n"
                         "  gl_Position = aPosition;\n"
                         "}";
// 片元着色器
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "in vec2 TexCoord;\n"
                              "uniform sampler2D ourTexture;\n"
                              "uniform sampler2D textureLUT;\n"
                              "out vec4 FragColor;\n"
                              "vec4 lookupTable(vec4 color){\n"
                              "    float blueColor = color.b * 63.0;\n"
                              "    vec2 quad1;\n"
                              "    quad1.y = floor(floor(blueColor) / 8.0);\n"
                              "    quad1.x = floor(blueColor) - (quad1.y * 8.0);\n"
                              "    vec2 quad2;\n"
                              "    quad2.y = floor(ceil(blueColor) / 8.0);\n"
                              "    quad2.x = ceil(blueColor) - (quad2.y * 8.0);\n"
                              "    vec2 texPos1;\n"
                              "    texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);\n"
                              "    texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);\n"
                              "    vec2 texPos2;\n"
                              "    texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);\n"
                              "    texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);\n"
                              "    vec4 newColor1 = texture(textureLUT, texPos1);\n"
                              "    vec4 newColor2 = texture(textureLUT, texPos2);\n"
                              "    vec4 newColor = mix(newColor1, newColor2, fract(blueColor));\n"
                              "    return vec4(newColor.rgb, color.w);\n"
                              "}\n"
                              "\n"
                              "void main()\n"
                              "{\n"
                              "    vec4 tmpColor = texture(ourTexture, TexCoord);\n"
                              "    FragColor = lookupTable(tmpColor);\n"
                              "}";

const static GLfloat VERTICES_AND_TEXTURE[] = {
        0.5f, -0.5f, // 右下
        // 纹理坐标
        1.0f,1.0f,
        0.5f, 0.5f, // 右上
        // 纹理坐标
        1.0f,0.0f,
        -0.5f, -0.5f, // 左下
        // 纹理坐标
        0.0f,1.0f,
        -0.5f, 0.5f, // 左上
        // 纹理坐标
        0.0f,0.0f
};

// 真正的纹理坐标在图片的左下角
const static GLfloat FBO_VERTICES_AND_TEXTURE[] = {
        1.0f, -1.0f, // 右下
        // 纹理坐标
        1.0f,0.0f,
        1.0f, 1.0f, // 右上
        // 纹理坐标
        1.0f,1.0f,
        -1.0f, -1.0f, // 左下
        // 纹理坐标
        0.0f,0.0f,
        -1.0f, 1.0f, // 左上
        // 纹理坐标
        0.0f,1.0f
};

// 使用byte类型比使用short或者int类型节约内存
const static uint8_t indices[] = {
        // 注意索引从0开始!
        // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
        // 这样可以由下标代表顶点组合成矩形
        0, 1, 2, // 第一个三角形
        1, 2, 3  // 第二个三角形
};

Lut2DOpengl::Lut2DOpengl() {
    initGlProgram(ver,fragment);
    positionHandle = glGetAttribLocation(program,"aPosition");
    textureHandle = glGetAttribLocation(program,"aTexCoord");
    textureSampler = glGetUniformLocation(program,"ourTexture");
    lut_textureSampler = glGetUniformLocation(program,"textureLUT");

    LOGD("program:%d",program);
    LOGD("positionHandle:%d",positionHandle);
    LOGD("textureHandle:%d",textureHandle);
    LOGD("textureSample:%d",textureSampler);
    // VAO
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    // vbo
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES_AND_TEXTURE), VERTICES_AND_TEXTURE, GL_STATIC_DRAW);

    // stride 步长 每个顶点坐标之间相隔4个数据点,数据类型是float
    glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) 0);
    // 启用顶点数据
    glEnableVertexAttribArray(positionHandle);
    // stride 步长 每个颜色坐标之间相隔4个数据点,数据类型是float,颜色坐标索引从2开始
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
                          (void *) (2 * sizeof(float)));
    // 启用纹理坐标数组
    glEnableVertexAttribArray(textureHandle);

    // EBO
    glGenBuffers(1,&ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);

    // 这个顺序不能乱啊,先解除vao,再解除其他的,不然在绘制的时候可能会不起作用,需要重新glBindBuffer才生效
    // vao解除
    glBindVertexArray(0);
    // 解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 解除绑定
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
}

Lut2DOpengl::~Lut2DOpengl() noexcept {
    glDeleteBuffers(1,&ebo);
    glDeleteBuffers(1,&vbo);
    glDeleteVertexArrays(1,&vao);
    // ... 删除其他,例如fbo等
}

void Lut2DOpengl::setPixel(void *data, int width, int height, int length) {
    imageWidth = width;
    imageHeight = height;
    glGenTextures(1, &imageTextureId);

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);
    // 为当前绑定的纹理对象设置环绕、过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    // 生成mip贴图
    glGenerateMipmap(GL_TEXTURE_2D);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);
}

void Lut2DOpengl::setLutPixel(void *data, int width, int height, int length) {
    glGenTextures(1, &lut_imageTextureId);

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, lut_imageTextureId);
    // 为当前绑定的纹理对象设置环绕、过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    // 生成mip贴图
    glGenerateMipmap(GL_TEXTURE_2D);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);
}

void Lut2DOpengl::onDraw() {
    // 恢复绘制屏幕宽高
    glViewport(0,0,eglHelper->viewWidth,eglHelper->viewHeight);

    // 绘制到屏幕
    // 清屏
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);

    // 激活纹理
    glActiveTexture(GL_TEXTURE1);
    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);
    glUniform1i(textureSampler, 1);

    // 激活纹理 lut
    glActiveTexture(GL_TEXTURE2);
    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, lut_imageTextureId);
    glUniform1i(lut_textureSampler, 2);

    checkError(program);

    // VBO与VAO配合绘制
    // 使用vao
    glBindVertexArray(vao);
    // 使用EBO
// 使用byte类型节省内存
    glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_BYTE,(void *)0);
    glUseProgram(0);
    // vao解除绑定
    glBindVertexArray(0);
    // 禁用顶点
    glDisableVertexAttribArray(positionHandle);
    if (nullptr != eglHelper) {
        eglHelper->swapBuffers();
    }

    glBindTexture(GL_TEXTURE_2D, 0);
}

demo运行结果图

以下这张是需要增加滤镜的原图:

需要增加滤镜的原图

以下这张是所使用的LUT纹理图:

所使用的LUT纹理图

以下这张是运行结果图:

运行结果图

思考

如果要在Opengl中给LUT加上滤镜强度调整参数该怎么处理呢?

系列教程源码

https://github.com/feiflyer/NDK_OpenglES_Tutorial

Opengl ES系列入门介绍

Opengl ES之EGL环境搭建
Opengl ES之着色器
Opengl ES之三角形绘制
Opengl ES之四边形绘制
Opengl ES之纹理贴图
Opengl ES之VBO和VAO
Opengl ES之EBO
Opengl ES之FBO
Opengl ES之PBO
Opengl ES之YUV数据渲染
YUV转RGB的一些理论知识
Opengl ES之RGB转NV21
Opengl ES之踩坑记
Opengl ES之矩阵变换(上)
Opengl ES之矩阵变换(下)
Opengl ES之水印贴图
Opengl ES之纹理数组
OpenGL ES之多目标渲染(MRT

关注我,一起进步,人生不止coding!!!

目录
相关文章
|
7月前
|
XML 小程序 Java
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
137 0
|
数据安全/隐私保护 开发者
OpenGL ES 多目标渲染(MRT)
Opengl ES连载系列
329 0
|
数据安全/隐私保护 索引
Opengl ES之纹理数组
Opengl ES连载系列
263 0
|
数据安全/隐私保护
Opengl ES之水印贴图
Opengl ES之连载系列
157 0
|
Java 数据安全/隐私保护 Android开发
Opengl ES之矩阵变换(下)
Opengl ES连载系列
136 0
|
Java API 数据安全/隐私保护
Opengl ES之矩阵变换(上)
Opengl ES连载系列
152 0
|
缓存 C++
Opengl ES之FBO
Opengl ES连载系列
157 0
|
存储
Opengl ES之踩坑记
Opengl ES之连载系列
146 0
|
存储 编解码 算法
Opengl ES之RGB转NV21
Opengl ES连载系列
159 0
|
并行计算 C++
Opengl ES之YUV数据渲染
Opengl ES连载系列
183 0