[OpenGL]未來視覺4-Native層濾鏡新增

天星技術團隊發表於2019-01-28

作者:蒼王 時間:2019.1.28

上一節介紹了攝像頭的幀採集,這一節將要介紹採集回來的攝像頭資料如何顯示到螢幕,以及對資料進行濾鏡新增。

現在demo中提供了大概有三十幾種濾鏡,其原來MagicCamera大致一樣的。 首先說一下采坑遇到的問題

image.png

####1.圖片格式不同的問題 濾鏡效果比較簡單的理解就是原來的影象的基礎上,混合上紋理顯示出來的效果,而Opengl中紋理可以圖片,也可以資料的形式來載入。一般是有png,jpg,bmp三種。 移動端很少使用bmp圖,因為圖片比較壓縮度低,一般bmp圖片容量對比png,jpg大很多。png圖和jpg圖最大的不同是,png是RGBA的編碼,而jpg是RGB編碼,就是透明通道的區別,如果填寫錯誤,我遇到過現實的濾鏡會有黑條的問題(不是顯示不出來,是黑條)。而Opengl很多都需要填寫通道的格式和通道長度的,這點尤為重要。 我一開始複寫這個框架的時候就是因為對圖片解碼等問題不夠深入,耽誤了很長的時間(大概兩週,才在這個坑口爬出來,所以入門的時候一定要認清這些圖片格式確切的區別)

###2.圖片載入的問題 (1)如果你單純用Android的OpenGL來讀取圖片紋理,非常簡單,Android的OpenGL直接支援bitmap的輸入作為紋理轉換。 (2)如果你單純使用jni來讀取圖片,Android獨有的android/bitmap.h框架可以直接從java層傳遞二進位制圖片資料到native層,而且能讀取到圖片的一些基本資訊。 (3)如果你想單純通過native層來完成圖片紋理讀取解碼以及native 的opengl載入,這是最高自由度的,可以根本上考慮程式碼的複用性,以及圖片載入機制的瞭解。當然,我就選擇了這種,途中遇到複雜的問題拆分為: ####如何讓native讀取到Android內建圖片的地址? 在native讀取到內建圖片的地址,這裡要藉助到android種asset這個資料夾,用來存放紋理圖片,以及shader檔案。(當然你也可以將shader檔案也複製到程式碼中,這樣更加壓縮會更加好,但是維護比較麻煩就是了)。通過asset來讀取到圖片檔案資訊,這樣做的好處在於圖片的處理都在native層,當圖片釋放,可以立刻釋放資源,不會想java那張圖片可能還在快取中等待回收。asset_manager_jni.h中提供了一套完整的讀取asset資料夾的方法。

      //開啟asset資料夾中的資料夾
        AAssetDir *dir = AAssetManager_openDir(manager,"filter");
        //初始化檔名指標
        const char *file = nullptr;
        //迴圈遍歷
        while ((file =AAssetDir_getNextFileName(dir))!= nullptr) {
            //對比檔名
            if (strcmp(file, fileName) == 0) {
                //拼接檔案路徑,以asset資料夾為起始
                std::string *name = new std::string("filter/");
                name->append(file);
                //以流的方式開啟檔案
                AAsset *asset = AAssetManager_open(manager, name->c_str(), AASSET_MODE_STREAMING);
複製程式碼

####如果在native中讀取到圖片的資訊? 讀取圖片檔案的資訊,包括長寬,圖片大小,通道大小,這裡當然只能使用C的框架來讀取影象資料了,這裡使用了一個比較簡便,且可接入性較高的影象解析庫stb

//引用stb庫
#define STB_IMAGE_IMPLEMENTATION
#include "src/main/cpp/utils/stb_image.h"

                    int len = AAsset_getLength(asset);
                    int width=0,height=0,n=0;
                    //讀取資原始檔流
                    unsigned char* buff = (unsigned char *) AAsset_getBuffer(asset);
                    //讀取圖片長寬以及通道資料
                    unsigned char* data = stbi_load_from_memory(buff, len, &width, &height, &n, 0);
                    ALOGV("loadTextureFromAssets fileName = %s,width = %d,height=%d,n=%d,size = %d",fileName,width,height,n,len);
                    //關閉資源
                    AAsset_close(asset);
                    //關閉asset資料夾
                    AAssetDir_close(dir);
複製程式碼

這裡一定要注意,引入stb_image.h的方式,請一定要遵循我這種引用方式,不然將會出現某些影象無法解析的可能。我就是在這裡卡了幾天,一度懷疑人生,想要換庫操作。

image.png

####讀取完成後怎麼載入圖片的紋理資訊? 這裡其實很明確使用glTexImage2D來將圖片的資料生成一個2D紋理,但是有人會考慮使用mipmaps貼圖,但是濾鏡一般都不需要使用mimpas貼圖的。千萬別誤用了mipmaps貼圖,mipmaps貼圖是2的次冪方,而且長和寬大小是一樣的正方形,然而我真的是除錯的時候自己傻傻的新增了,只怪我還是太年輕了。

                       if(data!=NULL) {
                        GLint format = GL_RGB;
                        if (n==3) { //RGB三通道,例如jpg格式
                            format = GL_RGB;
                        } else if (n==4) {  //RGBA四通道,例如png格式
                            format = GL_RGBA;
                        }
                        //將圖片資料生成一個2D紋理
                        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
                    } else{
                        LOGE("load texture from assets is null,fileName = %s",fileName);
                    }
複製程式碼

做完這些後可以從Native直接讀取到圖片紋理,那麼接下來就是將攝像頭採集過來的幀圖和濾鏡紋理疊加,然後將其顯示到螢幕當中,如何獲取到幀緩衝圖,請檢視上一節的內容。

image.png

濾鏡新增的流程圖

濾鏡新增.png

這裡是繪製的公共程式碼。

if (cameraInputFilter != nullptr){
//        cameraInputFilter->onDrawFrame(mTextureId,matrix,VERTICES,TEX_COORDS);
        //獲取幀緩衝id
        GLuint id = cameraInputFilter->onDrawToTexture(mTextureId,matrix);
        if (filter != nullptr)
            //通過濾鏡filter繪製
            filter->onDrawFrame(id,matrix);
        //緩衝區交換
        glFlush();
        mEGLCore->swapBuffer();
    }

int GPUImageFilter::onDrawFrame(const GLuint textureId, GLfloat *matrix,const float *cubeBuffer,
                                const float *textureBuffer) {
    onDrawPrepare();
    glUseProgram(mGLProgId);
    if (!mIsInitialized) {
        ALOGE("NOT_INIT");
        return NOT_INIT;
    }

    //載入矩陣
//    glUniformMatrix4fv(mMatrixLoc,1,GL_FALSE,matrix);
    glVertexAttribPointer(mGLAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, cubeBuffer);
    glEnableVertexAttribArray(mGLAttribPosition);
    glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GL_FLOAT, GL_FALSE, 0, textureBuffer);
    glEnableVertexAttribArray(mGLAttribTextureCoordinate);

    if(textureId !=NO_TEXTURE){
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D,textureId);
        //載入紋理
        glUniform1i(mGLUniformTexture,0);
    }
    //濾鏡引數載入
    onDrawArraysPre();
    glDrawArrays(GL_TRIANGLE_STRIP,0,4);
    //濾鏡引數釋放
    onDrawArraysAfter();
    //釋放頂點繫結
    glDisableVertexAttribArray(mGLAttribPosition);
    glDisableVertexAttribArray(mGLAttribTextureCoordinate);

    if(textureId !=NO_TEXTURE) //啟用回到預設紋理
        glBindTexture(GL_TEXTURE_2D,0);
    return ON_DRAWN;
}
複製程式碼

用一個簡單的Amaro濾鏡來說明一下使用。

//構造時載入program,以及shader
MagicAmaroFilter::MagicAmaroFilter(AAssetManager *assetManager)
    : GPUImageFilter(assetManager,readShaderFromAsset(assetManager,"nofilter_v.glsl"), readShaderFromAsset(assetManager,"amaro.glsl")){

}

//初始化shader頂點和紋理引數
void MagicAmaroFilter::onInit() {
    GPUImageFilter::onInit();
    inputTextureUniformLocations[0] = glGetUniformLocation(mGLProgId,"inputImageTexture2");
    inputTextureUniformLocations[1] = glGetUniformLocation(mGLProgId,"inputImageTexture3");
    inputTextureUniformLocations[2] = glGetUniformLocation(mGLProgId,"inputImageTexture4");
    mGLStrengthLocation = glGetUniformLocation(mGLProgId,"strength");
}

//從多個圖中生成紋理
void MagicAmaroFilter::onInitialized() {
    GPUImageFilter::onInitialized();
    inputTextureHandles[0] = loadTextureFromAssets(mAssetManager,"brannan_blowout.png");
    inputTextureHandles[1] = loadTextureFromAssets(mAssetManager,"overlaymap.png");
    inputTextureHandles[2] = loadTextureFromAssets(mAssetManager,"amaromap.png");
}
複製程式碼

在繪製的時候onDrawArraysPre()加入繫結紋理,以及onDrawArraysAfter()取消繫結

void MagicAmaroFilter::onDrawArraysPre() {
    glUniform1f(mGLStrengthLocation, 1.0f);
    if (inputTextureHandles[0] != 0) {
        glActiveTexture(GL_TEXTURE3);
        glBindTexture(GL_TEXTURE_2D, inputTextureHandles[0]);
        glUniform1i(inputTextureUniformLocations[0], 3);
    }
……
}

void MagicAmaroFilter::onDrawArraysAfter() {
    if (inputTextureHandles[0] != 0) {
        glActiveTexture(GL_TEXTURE3);
        glBindTexture(GL_TEXTURE_2D, inputTextureHandles[0]);
        glActiveTexture(GL_TEXTURE0);
    }
……
}
複製程式碼

這裡關鍵效果的疊加是Fragment Shader中計算繪製來完成。

void main()
 {
     //從取樣器中程式紋理取樣
     vec4 originColor = texture(inputImageTexture, textureCoordinate);
     vec4 texel = texture(inputImageTexture, textureCoordinate);
     vec3 bbTexel = texture(inputImageTexture2, textureCoordinate).rgb;
     
     texel.r = texture(inputImageTexture3, vec2(bbTexel.r, texel.r)).r;
     texel.g = texture(inputImageTexture3, vec2(bbTexel.g, texel.g)).g;
     texel.b = texture(inputImageTexture3, vec2(bbTexel.b, texel.b)).b;

     //按比例分別混合RGB
     vec4 mapped;
     mapped.r = texture(inputImageTexture4, vec2(texel.r, .16666)).r;
     mapped.g = texture(inputImageTexture4, vec2(texel.g, .5)).g;
     mapped.b = texture(inputImageTexture4, vec2(texel.b, .83333)).b;
     mapped.a = 1.0;
     //mix(x, y, a): x, y的線性混疊, x(1-a) + y*a;
     mapped.rgb = mix(originColor.rgb, mapped.rgb, strength);

     gl_FragColor = mapped;
 }
複製程式碼

基本的載入濾鏡的模板程式碼就到這裡了。需要注意的是,如果你看原版的MagicCamera很多程式碼使用android的訊息傳輸以及執行緒的簡便,拋到到繪製onDraw的時候執行。而C++編寫的時候,還是好好的嚴格遵循繪製的順序來編寫和初始化。

image.png

[OpenGL]未來視覺-MagicCamera3實用開源庫

相關文章