该原创文章首发于微信公众号:字节流动

最后不少朋友问,“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 种,然后做马赛克,用一个小格子替换一个字符,保证小格子的宽高比与字符相同,确保替换后的字符不被拉伸,这样渲染出来的图像大小与原图一样。

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

字符画滤镜实现

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

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

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

这里使用的字符表图像尺寸 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);
}

灰度图马赛克的效果。

灰度图马赛克完成后,每个小格子替换一个字符,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);//最后做个混合保留一些原图的色彩
}

字符画的效果。

联系与交流

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

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

  1. 安卓学习笔记37:利用OpenGL ES绘制平面图形

    文章目录 零.学习目标 一.OpenGL概述 二.了解三维直角坐标系 三.案例演示 - 绘制三角形 (一)运行效果 (二)实现步骤 1.创建安卓应用[DrawTriangle] 2.建模:创建三角形类 ...

  2. OpenGL ES像素着色器

    OpenGL ES像素着色器 原文   http://www.tairan.com/archives/7509 目 录 准备开始 像素着色器 vs 顶点/片段着色器 像素着色器101:渐变 像素着色器 ...

  3. OpenGL ES像素着色器教程

    OpenGL ES像素着色器教程 时间 2014-08-27 09:54:51   泰然 原文   http://www.tairan.com/archives/7509 主题  OpenGL ES ...

  4. OpenGL ES _ 着色器_片断着色器详解

    OpenGL ES _ 入门_01 OpenGL ES _ 入门_02 OpenGL ES _ 入门_03 OpenGL ES _ 入门_04 OpenGL ES _ 入门_05 OpenGL ES ...

  5. 使用OES纹理+GLSurfaceView+JNI实现基于OpenGL ES的播放器画面处理

    前言: 安卓使用SurfaceView + SurfaceTexture + Surface进行视频播放或者相机预览,只能看到原色画面,但很多场合需求画面可以实现二次加工,例如调整红绿蓝三原色的比例. ...

  6. Opengl ES之着色器

    前言 在前面我们介绍了 OpenglEs之EGL环境搭建 ,在后面的例子中,我们将无可避免地需要使用到着色器.而着色器才是Opengl的灵魂所在,有了着色器才有了Opengl天马行空的世界. 图形渲染 ...

  7. 【OpenGL ES】着色器Shader与程序Program

    在OpenGL ES 3程序中,Shader和Program是两个重要的概念,至少需要创建一个顶点Shader对象.一个片段Shader对象和一个Program对象,才能用着色器进行渲染,理解Shad ...

  8. android相机曝光度调节,Android OpenGL ES - 反相、曝光、对比度、饱和度、色调滤镜...

    经过前几篇的博客,我们应该队OpenGL有了基础的认识,那么我们就该来点实践了,它来了,它来了,真正的滤镜它来了 先放效果图 滤镜的学习是个循序渐进的过程,我们本章先说一下简单的滤镜,也让读者揭开一角 ...

  9. android 图像对比度,Android OpenGL ES - 反相、曝光、对比度、饱和度、色调滤镜

    OpenGL ES - 简单滤镜 默认滤镜 vertex shaderattribute vec4 position; attribute vec4 inputTextureCoordinate; v ...

最新文章

  1. python3.9 执行python3.6生成的随机森林模型model.pkl报错,警告版本不一致
  2. python重复import_Python module重复载入的问题
  3. android内核模块签名,android安装内核module,提示Required key not available
  4. appium+python 操作APP
  5. CSDN公式编辑(latex语言应用)整理
  6. 百练 01 Charm Bracelet
  7. java项目eclipse上tomcat部署后项目添加不进去
  8. 静态类和非静态类的主要差别
  9. iis端口号 linux,Linux 6 修改ssh默认远程端口号的操作步骤
  10. EVEREST工具---检测硬件
  11. WordPress使用腾讯云CDN配置如何实现https访问?
  12. 常见计算机硬件故障维修方法,电脑硬件有哪些常见问题 电脑硬件常见问题维修技巧【详解】...
  13. js大于等于小于等于书写
  14. 区块链DAPP开发 以太坊智能合约框架有哪些
  15. 11张图揭露了程序员的日常生活,看完笑哭(泪奔)!
  16. 研一一整年都在搞深度学习,研二醒悟打算转开发
  17. web浏览器中的base64编码解码
  18. Distilling Object Detectors with Fine-grained Feature Imitation(2019 CVPR KD)
  19. linux内核静态添加sdio设备,Linux下sdio设备扫描过程
  20. 关于说服(一)-反抗机制

热门文章

  1. SQL注入与万能密码登录
  2. Mysql 解决 sum求和有多位小数
  3. Mysql 中 “必知” 的单行处理函数
  4. Python爬虫抓取去哪儿网景点信息告诉你国庆哪儿最堵
  5. 学会python语法后的第一个爬虫
  6. SAP物料与总账科目集成业务
  7. 关于CSAPP的学习:如何与如同机翻的文字搏斗及如何快速理解冗长的说明
  8. 牛客SQL--SQL必知必会刷题记录
  9. 新氧2023年财务业绩预测:退市风险大幅降低,收入增长将放缓
  10. R语言数据分析报错解决办法和有用命令(8.3-8.10)