美颜重磅技术之GPUImage源码分析

说到基于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>();
    }

    @Override
    public void init() {
        for (AbsFilter filter : filters) {
            filter.init();
        }
        isRunning=true;
    }

    @Override
    public void onPreDrawElements() {
    }

    @Override
    public void destroy() {
        destroyFrameBuffers();
        for (AbsFilter filter : filters) {
            filter.destroy();
        }
        isRunning=false;
    }

    @Override
    public 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);
            }
        }
    }

    @Override
    public 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 execution
        if (!isRunning){
            filters.add(filter);
        }
        else
            addPreDrawTask(new Runnable() {
            @Override
            public 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 execution
        if (!isRunning){
            for(AbsFilter filter:filterList){
                filters.add(filter);
            }
        }
        else
            addPreDrawTask(new Runnable() {
                @Override
                public void run() {
                    for(AbsFilter filter:filterList){
                        filter.init();
                        filters.add(filter);
                    }
                    onFilterChanged(surfaceWidth,surfaceHeight);
                }
            });
    }
}

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

原文发布于微信公众号 - 何俊林(DriodDeveloper)

原文发表时间:2018-09-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏性能与架构

将 Redis 作为图数据库

1. 简介 Redis 在 4.0 中正式支持了Module模块系统,使其可以进行丰富的扩展 图数据库的应用越来越广泛,RedisGraph 就是一个 Redi...

4666
来自专栏Alice

ios 继承UITableViewController,更改tableview样式

// 继承UITableViewController,更改tableview样式 - (instancetype)initWithStyle:(UITableV...

3586
来自专栏腾讯AlloyTeam的专栏

构建流式应用:RxJS 详解

本文结合作者自己对 RxJS 理解,通过 RxJS 的实现原理、基础实现及实例来一步步分析,提供 RxJS 较为全面的指引,感受下使用 RxJS 编码是怎样的体...

2.9K2
来自专栏静晴轩

Vue 各类数据绑定

『天下武功,唯快不破』√,这一直是对武学造诣方面的追捧,虽然对于这个丝毫不会;更是对待现实工作不懈渴求,乃至苛求。因为这已不是遁隐修行,而是职场卖命,唯有先快速...

3997
来自专栏hightopo

原 荐 基于HTML5 Canvas的工控S

1283
来自专栏Android干货

环信easeui集成:坑总结2018(二)

环信EaseUI 集成,集成不做描述,看文档即可,下面主要谈一些对easeui的个性化需求修改。

1102
来自专栏C语言及其他语言

【程序源码】猜拳游戏

关注我们 今天来给大家来一段游戏源码(猜拳游戏) ? 程序截图如上。话不多说,上源码!! #include <stdio.h> #include <stdlib...

3416
来自专栏慎独

AVPlayer初体验之视频解纹理

6564
来自专栏HT

基于HTML5 Canvas的工控SCADA模拟飞机飞行

昨天看到一篇文章说是学习如何开飞机的,然后我就想,如果我也可以开飞机那就好玩了,每个人小时候都想做飞行员!中国飞行员太难当了,再说也不轻易让你开飞机!后来我就想...

2349
来自专栏机器学习养成记

R语言爬虫与文本分析

之前用python做过简单的爬虫与分析,今天尝试一下用R完成相应的功能。首先用R爬取了《了不起的麦瑟尔夫人》豆瓣短评作为语料,然后进行了词云绘制、关键词提取的基...

46314

扫码关注云+社区

领取腾讯云代金券