美顏重磅技術之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

歡迎關注我的微信公衆號「碼農突圍」,分享Python、Java、大數據、機器學習、人工智能等技術,關注碼農技術提升•職場突圍•思維躍遷,20萬+碼農成長充電第一站,陪有夢想的你一起成長。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章