说到基于GPU的图像处理和实时滤镜,大家肯定会想到鼎鼎大名的GPUImage,这个项目确实为后续开发提供了很多方便,基本的图像处理工具一应俱全。但是学习借鉴GPUImage的项目结构,可以为我们提供不小的帮助。

GPUImage项目结构

GPUImage的项目结构其实很简单,Android版本就更是简陋,结构如下:

  • 一堆滤镜(shader以及配套设置参数的代码)
  • FilterGroup(利用FBO进行同一副图像的多次处理)
  • EGL管理类(主要用来做离屏渲染)

虽然GPUImage的主要价值在那堆滤镜上,但是我们主要来分析后面两个,这是GPUImage的框架,而滤镜就像插件一样,想插就插:D,我们也可以依葫芦画瓢定制自己的滤镜。

为什么要离屏渲染

离屏渲染的主要目的是在后台处理数据,做过Camera应用的都知道,如果用SurfaceView进行预览,那么就不得不把相机数据显示出来,为了不显示,就要把SurfaceView缩到很小,麻烦又浪费资源。Android 3.0后有了SurfaceTexture和GLSurfaceView,之后又有了TextureView,可以自由处理相机数据不显示出来了,但是依然有一个显示和绘制的过程。换句话说,TextureView和GLSurfaceView还不够听话,不能完成我们的所有要求。

如果我们只是想要利用GPU处理一张图片,但是不把他显示出来呢?

举个栗子
我们来看一下Camera360 Lite版的界面:

这些图片都是打开以后选择滤镜就能看到的,不用联网也可以,他们是APK自带的吗?为什么都是同一个人呢? 然而找了一圈以后,我只能在APK中找到这些:

不同颜色的大姐姐去哪了?
这就说明,这些不同的滤镜效果,其实是APK在第一次运行时,在用户手机上生成的。(可以自行查看Camera360 的data文件夹) 这样有很多好处呀,例如说大大减小了APK体积,同一套代码还可以用来完成不同的功能等。 当然,这只是离屏渲染的一个优点。

之前使用GLSurfaceView时,GLSurfaceView帮我们完成了EGL的环境配置,现在不使用GLSurfaceView,我们就要自行管理了,看看GPUImage是怎么做的吧:

GPUImage参考了GLSurfaceView,自己进行了OpenGL的环境配置(好像什么都没说啊,逃…

后面我们在分析GLSurfaceView的代码时,会再来说离屏渲染应该怎么做(毕竟环境配置什么的都是套路)

滤镜组与帧缓存对象(FBO)

GPUImage的滤镜组可以说是对这些滤镜的最好复用。借助于FrameBufferObject(FBO,帧缓存),我们可以在一幅图像上使用不同的滤镜组合来得到想要的结果。

再举个栗子:
我写了一个灰度滤镜,可以把图片转成黑白效果,代码如下:

precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
void main() {vec3 centralColor = texture2D(sTexture, vTextureCoord).rgb;gl_FragColor = vec4(0.299*centralColor.r+0.587*centralColor.g+0.114*centralColor.b);
}

有一天我闲的没事干,又写了一个反色滤镜:

precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
void main() {vec4 centralColor = texture2D(sTexture, vTextureCoord);gl_FragColor = vec4((1.0 - centralColor.rgb), centralColor.w);
}

现在Boss要求我对于视频流先进行黑白处理,再进行反色。
这点小事怎么难得到我呢,然后我花了10分钟写出了下面的代码:

precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
void main() {vec4 centralColor = texture2D(sTexture, vTextureCoord);gl_FragColor =vec4(0.299*centralColor.r+0.587*centralColor.g+0.114*centralColor.b);gl_FragColor = vec4((1.0 - gl_FragColor.rgb), gl_FragColor.w);
}

这两个滤镜比较简单(只有一行),如果每个滤镜都很复杂呢?如果组合很多呢?

我们将两个功能写到了同一个滤镜里面,这样每次都要修改shader,一点都不优雅,一点都没有体现大学老师辛辛苦苦灌输的OO理念。

在GPUImage中,帧缓存对象就是用来解决这个问题的,之前我们都是一次性处理完就绘制到屏幕上了,现在不,我们可以将结果保存在帧缓存当中,然后再拿绘制结果作为下一次的输入数据来进行处理,于是我的代码就变成了:

filterGroup.addFilter(new GrayScaleShaderFilter(context));
filterGroup.addFilter(new InvertColorFilter(context));

如果还要有第三步处理怎么办?
再new一个呀!是不是很方便?

FBO的创建与绘制流程

首先我们需要两个数组,用来保存FBO的ID和绘制结果的纹理ID。

protected int[] frameBuffers = null;
protected int[] frameBufferTextures = null;

没错,FBO也像纹理一样,用一个数字表示。

if (frameBuffers == null) {frameBuffers = new int[size-1];frameBufferTextures = new int[size-1];for (int i = 0; i < size-1; i++) {GLES20.glGenFramebuffers(1, frameBuffers, i);GLES20.glGenTextures(1, frameBufferTextures, i);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameBufferTextures[i]);GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,filters.get(i).surfaceWidth, filters.get(i).surfaceHeight, 0,GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,GLES20.GL_TEXTURE_2D, frameBufferTextures[i], 0);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);}
}

这里的代码比较长,但是和我们之前生成纹理的代码很相似(没有OpenGL ES基础的同学可以看这个)

  • GLES20.glGenFramebuffers用来生成帧缓存对象
  • 下面的一大段其实就是生成一个纹理并且用我们当前要绘制的长和宽对其进行配置,并且指定边界的处理情况,放大缩小的策略等
  • 关键来了:我们用GLES20.glFramebufferTexture2D来把一幅纹理图像关联到一个帧缓存对象,告诉OpenGL这个FBO是用来关联一个2D纹理的,frameBufferTextures[i]就是和这个FBO关联的纹理
  • 为什么是size-1呢,因为我们最后一个纹理是直接绘制到屏幕上的呀~

绘制

生成了FBO以后,我们就可以这样改写我们的绘制代码

if (i < size - 1) {GLES20.glViewport(0, 0, filter.surfaceWidth, filter.surfaceHeight);GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);filter.onDrawFrame(previousTexture);GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);previousTexture = frameBufferTextures[i];
}else{GLES20.glViewport(0, 0 ,filter.surfaceWidth, filter.surfaceHeight);filter.onDrawFrame(previousTexture);
}
  • 每次绘制之前使用glBindFramebuffer绑定FBO,然后这次我们绘制的结果就不会显示在屏幕上,而是变成了一个刚才和FBO绑定的纹理对象,然后再用这个纹理给下一个滤镜作为输入
  • 第一个滤镜的输入就是我们的相机或者播放器对应的纹理
  • 最后一个滤镜不需要再输出到FBO了,因此直接绘制出来就好。

滤镜组完整代码

package com.martin.ads.omoshiroilib.filter.base;import android.opengl.GLES20;
import android.util.Log;import java.util.ArrayList;
import java.util.List;/*** Created by Ads on 2016/11/19.*/public class FilterGroup extends AbsFilter {private static final String TAG = "FilterGroup";protected int[] frameBuffers = null;protected int[] frameBufferTextures = null;protected List<AbsFilter> filters;protected boolean isRunning;public FilterGroup() {super("FilterGroup");filters=new ArrayList<AbsFilter>();}@Overridepublic void init() {for (AbsFilter filter : filters) {filter.init();}isRunning=true;}@Overridepublic void onPreDrawElements() {}@Overridepublic void destroy() {destroyFrameBuffers();for (AbsFilter filter : filters) {filter.destroy();}isRunning=false;}@Overridepublic void onDrawFrame(int textureId) {runPreDrawTasks();if (frameBuffers == null || frameBufferTextures == null) {return ;}int size = filters.size();int previousTexture = textureId;for (int i = 0; i < size; i++) {AbsFilter filter = filters.get(i);Log.d(TAG, "onDrawFrame: "+i+" / "+size +" "+filter.getClass().getSimpleName()+" "+filter.surfaceWidth+" "+filter.surfaceHeight);if (i < size - 1) {GLES20.glViewport(0, 0, filter.surfaceWidth, filter.surfaceHeight);GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);filter.onDrawFrame(previousTexture);GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);previousTexture = frameBufferTextures[i];}else{GLES20.glViewport(0, 0 ,filter.surfaceWidth, filter.surfaceHeight);filter.onDrawFrame(previousTexture);}}}@Overridepublic void onFilterChanged(int surfaceWidth, int surfaceHeight) {super.onFilterChanged(surfaceWidth, surfaceHeight);int size = filters.size();for (int i = 0; i < size; i++) {filters.get(i).onFilterChanged(surfaceWidth, surfaceHeight);}if(frameBuffers != null){destroyFrameBuffers();}if (frameBuffers == null) {frameBuffers = new int[size-1];frameBufferTextures = new int[size-1];for (int i = 0; i < size-1; i++) {GLES20.glGenFramebuffers(1, frameBuffers, i);GLES20.glGenTextures(1, frameBufferTextures, i);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameBufferTextures[i]);GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,filters.get(i).surfaceWidth, filters.get(i).surfaceHeight, 0,GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,GLES20.GL_TEXTURE_2D, frameBufferTextures[i], 0);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);}}}private void destroyFrameBuffers() {if (frameBufferTextures != null) {GLES20.glDeleteTextures(frameBufferTextures.length, frameBufferTextures, 0);frameBufferTextures = null;}if (frameBuffers != null) {GLES20.glDeleteFramebuffers(frameBuffers.length, frameBuffers, 0);frameBuffers = null;}}public void addFilter(final AbsFilter filter){if (filter==null) return;//If one filter is added multiple times,//it will execute the same times//BTW: Pay attention to the order of executionif (!isRunning){filters.add(filter);}elseaddPreDrawTask(new Runnable() {@Overridepublic void run() {filter.init();filters.add(filter);onFilterChanged(surfaceWidth,surfaceHeight);}});}public void addFilterList(final List<AbsFilter> filterList){if (filterList==null) return;//If one filter is added multiple times,//it will execute the same times//BTW: Pay attention to the order of executionif (!isRunning){for(AbsFilter filter:filterList){filters.add(filter);}}elseaddPreDrawTask(new Runnable() {@Overridepublic void run() {for(AbsFilter filter:filterList){filter.init();filters.add(filter);}onFilterChanged(surfaceWidth,surfaceHeight);}});}
}

原创作者:Martin,原文链接:https://blog.csdn.net/Martin20150405/article/details/55520358

欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。

美颜重磅技术之GPUImage源码分析相关推荐

  1. Eclipse Theia技术揭秘——脚手架源码分析

    在之前的文章中,我们介绍了Theia的构建,其中用到了很多theia的命令,这些命令来自于@theia/cli这个库,本篇文章我们就对@theia/cli以及相关联的库进行分析.本篇文章是继构建桌面I ...

  2. 音视频技术之ffplay源码分析-音视频同步

    音视频同步的目的是为了使播放的声音和显示的画面保持一致.视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧:音频按采样点播放,声音播放设备每次播放一个采样点,声 ...

  3. 擎创技术流 | Flink源码分析-JobDispatcher

    背景介绍 最近一直在阅读Flink基于Yarn的资源管理相关的代码,牵扯的流程比较长,主要包含以下几个环节: 客户端环节:命令参数解析,定位到作业入口,生成JobGraph,翻译成启动对应的Yarn集 ...

  4. FFmpeg简述,源码分析,录制/压缩/水印/剪切/旋转/滤镜/美颜/上传视频等(CPU软编码和解码)

    > ffmpeg源码分析 ffmpeg源码简析(一)结构总览- https://blog.csdn.net/Louis_815/article/details/79621056 FFmpeg的库 ...

  5. 技术社区分享|添加FUSD源码分析

    本文由Flow的技术大使FOU编写.他在学习Flow链上稳定币FUSD的过程中总结得出,希望能够给学习FUSD代码的小伙伴一定的启发和帮助.​ 摘要     1. FUSD基础源码分析 2. 重点分析 ...

  6. 【技术分享篇】Linux内核——手把手带你实现一个Linux内核文件系统丨Linux内核源码分析

    手把手带你实现一个Linux内核文件系统 1. 内核文件系统架构分析 2. 行行珠玑,代码实现 [技术分享篇]Linux内核--手把手带你实现一个Linux内核文件系统丨Linux内核源码分析 更多L ...

  7. [前沿技术] AMD FSR 1.0源码分析(一)

    文章目录 FSR技术分析 1. SIGGRAPH课程分析 1.1 源码介绍 1.2 EASU简单介绍 2. EASU源码分析 2.1 FsrEasuCon分析 2.2 FidelityFXSuperR ...

  8. [前沿技术] AMD FSR 1.0源码分析(二)

    FSR技术分析 前文:[前沿技术] AMD FSR 1.0源码分析(一) 2. EASU源码分析 2.3 FsrEasuF分析 1️⃣首先,就参数而言,主要是: void FsrEasuF(out A ...

  9. 推荐一个 React 技术揭秘的项目,自顶向下的 React 源码分析

    大家好,我是你们的 猫哥,那个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ just-react 这本书的宗旨是打造一本严谨.易懂的 React 源码分析教程. 为了达到这个目标,在行文上,本书会遵循: 不预 ...

  10. MyBatis 源码分析-技术分享

    2019独角兽企业重金招聘Python工程师标准>>> MyBatis 源码分析 MyBatis的主要成员 Configuration MyBatis所有的配置信息都保存在Confi ...

最新文章

  1. C# 制作外挂常用的API
  2. JAVA虚拟机之垃圾收集与内存分配策略
  3. ip数据报首部校验和的计算
  4. 怎么找不到JAVA9_在Java 9上运行应用程序时获取错误“找不到模块”:log4j.core
  5. php下的原生ajax请求
  6. android 打开移动开关,教你一个让安卓手机运行更流畅的小技巧:打开这个开关即可...
  7. php5.6 mongo 扩展,PHP5.6的安装及redis、memcache、mongo扩展
  8. 当try和finally都包含return时
  9. 【Divided Two】cpp
  10. Python(一)缺点
  11. php离线地图,如何发布百度离线地图及二次开发API
  12. R 语言之数据分析高级方法「GLM 广义线性模型」
  13. VC++ Call Stack调试
  14. android反编译干嘛,安卓反编译流程大解析 看完你就懂了!
  15. vue中点击打开新的页面window.open()
  16. 关于Django响应速度慢的问题
  17. 如何在Linux系统上装jdk1.8以及环境配置的方法步骤
  18. Isaac Sim 使用指南(一)
  19. (memcpy,memmove...)内存函数还不会??别怕,我来助你一臂之力
  20. 用粒子群解决有约束的最优解问题

热门文章

  1. mybatis mysql upsert_SpringBoot2.3.4+Mybatis+Phoenix操作HBase2.0.6
  2. 设计模式(12)——状态模式
  3. Java中的几种设计模式:创建型模式,结构型模式
  4. 2019年最流行的50款开源软件
  5. LIO-SAM探秘第三章之代码解析(一) --- utility.h + imageProjection.cpp
  6. 【图论】拉普拉斯矩阵(Laplacian matrix)
  7. [转]JS将图片转为base64编码
  8. HBase 数据模型(Data Model)
  9. OS开发(Objective-C)常用库索引
  10. github项目提交失败 master - master (non-fast-forward)