利用 OpenGL ES 给视频播放器和相机做个字符画滤镜

简介: 最后不少朋友问,“OpenGL ES 入门后怎么学习写一些滤镜?”,“怎么学习 shader ?”。

作者:字节流动

来源:https://blog.csdn.net/Kennethdroid/article/details/113379112


最后不少朋友问,“OpenGL ES 入门后怎么学习写一些滤镜?”,“怎么学习 shader ?”。

最近请教了一些大佬,他们一致认为正确的做法就是“去模仿”。先去模仿别人的滤镜怎么实现的,比如观察抖音的一些简单的滤镜,然后自己琢磨去实现一个。

当然,最有效率的方法是研究一些相关的开源项目,比如大名鼎鼎的 android-gpuimage 项目,该项目基本上实现了各种常见滤镜,上手容易,学习 shader 、熟悉 GLSL 或者对 OpenGL 滤镜感兴趣的同学,可以研究下。

顺便说下,最近看了一个项目叫 android-gpuimage-plus ,主要讲是 Native 层实现的滤镜,有一些比较不错的思路可以参考下。

之前有一位朋友发了一副表情画滤镜的效果图,就是利用不同的表情去替换不同的像素,生成一副由表情组成的图像。表情画滤镜的原理其实跟字符画相同,只是字符换成了表情。

由于那副效果图不方便展示,这里就介绍下字符画的实现原理,利用一个 shader 来实现字符画效果。

字符画滤镜原理

字符画滤镜其实跟 LUT 滤镜是同一个原理,本质上就是查表,像素替换。

实现字符画滤镜,首先想到的法子是,对图像进行逐像素替换成字符(一个字符实际上是由多个像素组成的小图片)。

逐像素替换会有两个问题

  1. 一个像素有 RGB 24 位三个通道,一共有 256×256×256 种颜色,那么多颜色要与字符表对应起来很麻烦;
  2. 逐像素替换字符,相当于原图一个像素替换成多个像素,比如现在用的字符表,一个字符的大小是 16x23 = 268 像素,那么渲染出来的图像大小变为原来的 268 倍,显然也不合理。

所以字符画滤镜实现的正确思路是先把原图转为灰度图,这样颜色种类只有 256 种,然后做马赛克,用一个小格子替换一个字符,保证小格子的宽高比与字符相同,确保替换后的字符不被拉伸,这样渲染出来的图像大小与原图一样。

image.png

字符画滤镜原理一句话描述就是,原图先做灰度图马赛克,再用小格子替换字符。

字符画滤镜实现

按照上节的原理描述,我们先对原图做灰度图马赛克,获取灰度值就直接对采样后像素点的 RGB 分量进行灰度转换。

//RGB 转灰度公式
Y = 0.299R+0.587G+0.114B

马赛克效果原理就是将图像分割成很多小区域,小区域内取相同的颜色,颜色值可以是该区域某些像素值的加权平均,本文取的是小矩形区域内中心点的像素值。

image.png

这里使用的字符表图像尺寸 128x69 ,一共有 24 个字符,每个字符尺寸 16x23 像素。

灰度图马赛克的实现。

//灰度图马赛克
#version 100
precision highp float;
varying vec2 v_texcoord;
uniform lowp sampler2D s_textureY;
uniform lowp sampler2D s_textureU;
uniform lowp sampler2D s_textureV;
uniform lowp sampler2D s_textureMapping;
uniform vec2 texSize;
vec4 YuvToRgb(vec2 uv) {
    float y, u, v, r, g, b;
    y = texture2D(s_textureY, uv).r;
    u = texture2D(s_textureU, uv).r;
    v = texture2D(s_textureV, uv).r;
    u = u - 0.5;
    v = v - 0.5;
    r = y + 1.403 * v;
    g = y - 0.344 * u - 0.714 * v;
    b = y + 1.770 * u;
    return vec4(r, g, b, 1.0);
}
const vec3  RGB2GRAY_VEC3 = vec3(0.299, 0.587, 0.114);
const float MESH_WIDTH = 16.0;//一个字符的宽
const float MESH_HEIGHT= 23.0;//一个字符的高
const float MESH_ROW_NUM = 100.0;//固定小格子的行数
void main()
{
    float imageMeshWidth = texSize.x / MESH_ROW_NUM;
  //使小格子的宽高比跟字符的宽高比保持一致,防止替换后字符被拉伸
    float imageMeshHeight = imageMeshWidth * MESH_HEIGHT / MESH_WIDTH;
    vec2 imageTexCoord = v_texcoord * texSize;//归一化坐标转像素坐标
  //取小格子中心点的像素
    vec2 midTexCoord;
    midTexCoord.x = floor(imageTexCoord.x / imageMeshWidth) * imageMeshWidth + imageMeshWidth * 0.5;//小格子中心
    midTexCoord.y = floor(imageTexCoord.y / imageMeshHeight) * imageMeshHeight + imageMeshHeight * 0.5;//小格子中心
    vec2 normalizedTexCoord = midTexCoord / texSize;//归一化
    vec4 rgbColor = YuvToRgb(normalizedTexCoord);//采样
    float grayValue = dot(rgbColor.rgb, RGB2GRAY_VEC3);//rgb转灰度值
    gl_FragColor = vec4(vec3(grayValue), rgbColor.a);
}

灰度图马赛克的效果。

image.png

灰度图马赛克完成后,每个小格子替换一个字符,24 个字符将 0~255 的灰度值(归一化后为 0~1.0 )分成 24 个等级,计算出灰度值后根据等级取对应的字符。

然后根据采样坐标在小格子内的偏移计算出字符(包含一个字符的小图片)的采样坐标,最后对字符采样。

字符画实现的完整 shader 。

//字符画
#version 100
precision highp float;
varying vec2 v_texcoord;
uniform lowp sampler2D s_textureY;
uniform lowp sampler2D s_textureU;
uniform lowp sampler2D s_textureV;
uniform lowp sampler2D s_textureMapping;//字符表纹理
uniform float u_offset;
uniform vec2 texSize;//原图尺寸
uniform vec2 asciiTexSize;//字符表尺寸
vec4 YuvToRgb(vec2 uv) {
    float y, u, v, r, g, b;
    y = texture2D(s_textureY, uv).r;
    u = texture2D(s_textureU, uv).r;
    v = texture2D(s_textureV, uv).r;
    u = u - 0.5;
    v = v - 0.5;
    r = y + 1.403 * v;
    g = y - 0.344 * u - 0.714 * v;
    b = y + 1.770 * u;
    return vec4(r, g, b, 1.0);
}
const vec3  RGB2GRAY_VEC3 = vec3(0.299, 0.587, 0.114);
const float MESH_WIDTH = 16.0;//一个字符的宽
const float MESH_HEIGHT= 23.0;//一个字符的高
const float GARY_LEVEL = 24.0;//字符表图上有 24 个字符
const float ASCIIS_WIDTH = 8.0;//字符表列数
const float ASCIIS_HEIGHT = 3.0;//字符表行数
const float MESH_ROW_NUM = 100.0;//固定小格子的行数
void main()
{
    float imageMeshWidth = texSize.x / MESH_ROW_NUM;
  //使小格子的宽高比跟字符的宽高比保持一致,防止替换后字符被拉伸
    float imageMeshHeight = imageMeshWidth * MESH_HEIGHT / MESH_WIDTH;
    vec2 imageTexCoord = v_texcoord * texSize;//归一化坐标转像素坐标
  //取小格子中心点的像素
    vec2 midTexCoord;
    midTexCoord.x = floor(imageTexCoord.x / imageMeshWidth) * imageMeshWidth + imageMeshWidth * 0.5;//小格子中心
    midTexCoord.y = floor(imageTexCoord.y / imageMeshHeight) * imageMeshHeight + imageMeshHeight * 0.5;//小格子中心
    vec2 normalizedTexCoord = midTexCoord / texSize;//归一化
    vec4 rgbColor = YuvToRgb(normalizedTexCoord);//采样
    float grayValue = dot(rgbColor.rgb, RGB2GRAY_VEC3);//rgb转灰度值
    //gl_FragColor = vec4(vec3(grayValue), rgbColor.a);
  //根据采样坐标在小格子内的偏移计算出在字符(包含一个字符的小图片)内的偏移
    float offsetX = mod(imageTexCoord.x, imageMeshWidth) * MESH_WIDTH / imageMeshWidth;
    float offsetY = mod(imageTexCoord.y, imageMeshHeight) * MESH_HEIGHT / imageMeshHeight;
    float asciiIndex = floor((1.0 - grayValue) * GARY_LEVEL);//根据灰度值确定第几个字符
    float asciiIndexX = mod(asciiIndex, ASCIIS_WIDTH);
    float asciiIndexY = floor(asciiIndex / ASCIIS_WIDTH);
  //根据字符的位置和字符内的偏移,计算出字符表纹理的采样点坐标
    vec2 grayTexCoord;
    grayTexCoord.x = (asciiIndexX * MESH_WIDTH + offsetX) / asciiTexSize.x;
    grayTexCoord.y = (asciiIndexY * MESH_HEIGHT + offsetY) / asciiTexSize.y;
    vec4 originColor = YuvToRgb(v_texcoord);//采样原始纹理
    vec4 mappingColor = vec4(texture2D(s_textureMapping, grayTexCoord).rgb, rgbColor.a);//采样字符表纹理
    gl_FragColor = mix(originColor, mappingColor, u_offset);//最后做个混合保留一些原图的色彩
}

字符画的效果。

image.png

联系与交流

技术交流获取源码可以添加我的微信:Byte-Flow


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

阿里云视频云@凡科快图.png

相关文章
|
18天前
|
XML 小程序 Java
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
61 0
|
存储 编解码 算法
Opengl ES之LUT滤镜(上)
Opengl ES之连载系列
350 0
|
数据安全/隐私保护 开发者
OpenGL ES 多目标渲染(MRT)
Opengl ES连载系列
229 0
|
数据安全/隐私保护 索引
Opengl ES之纹理数组
Opengl ES连载系列
190 0
|
数据安全/隐私保护
Opengl ES之水印贴图
Opengl ES之连载系列
93 0
|
Java 数据安全/隐私保护 Android开发
Opengl ES之矩阵变换(下)
Opengl ES连载系列
87 0
|
Java API 数据安全/隐私保护
Opengl ES之矩阵变换(上)
Opengl ES连载系列
90 0
|
存储
Opengl ES之踩坑记
Opengl ES之连载系列
95 0
|
存储 编解码 算法
Opengl ES之RGB转NV21
Opengl ES连载系列
120 0
|
并行计算 C++
Opengl ES之YUV数据渲染
Opengl ES连载系列
133 0