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

栏目: 编程工具 · 发布时间: 6年前

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

美颜重磅技术之 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版的界面:

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

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

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

不同颜色的大姐姐去哪了?

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

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

美颜重磅技术之 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

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

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

你凭什么做好互联网

你凭什么做好互联网

曹政 / 中国友谊出版公司 / 2016-12 / 42.00元

为什么有人可以预见商机、超越景气,在不确定环境下表现更出色? 在规则之外,做好互联网,还有哪些关键秘诀? 当环境不给机会,你靠什么翻身? 本书为“互联网百晓生”曹政20多年互联网经验的总结,以严谨的逻辑思维分析个人与企业在互联网发展中的一些错误思想及做法,并给出正确解法。 从技术到商业如何实现,每个发展阶段需要匹配哪些能力、分解哪些目标、落实哪些策略都一一点出,并在......一起来看看 《你凭什么做好互联网》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具