[OpenGL]未來視覺2-Android攝像頭幀採集

天星技術團隊發表於2018-12-30

作者:蒼王 時間:2018.12.29

[OpenGL]未來視覺2-Android攝像頭幀採集

這節相機的渲染的介紹,只涉及到二維平面的渲染,所以不需要關注三維變數。 先看一下總圖

OpenGL採集總圖.png

下面是相機採集初始化處理

JNIEXPORT jint JNICALL
Java_com_cangwang_magic_util_OpenGLJniLib_magicBaseInit(JNIEnv *env, jobject obj,
                                                        jobject surface,jint width,jint height,jobject assetManager) {
    std::unique_lock<std::mutex> lock(gMutex);
    if(glCamera){
        glCamera->stop();
        delete glCamera;
    }
    //建立window
    ANativeWindow *window = ANativeWindow_fromSurface(env,surface);
    //獲取檔案管理
    AAssetManager *manager = AAssetManager_fromJava(env,assetManager);
    //初始化相機引擎
    glCamera = new CameraEngine(window);
    glCamera->setAssetManager(manager);
    glCamera->resize(width,height);
    //建立相機採集
    return glCamera->create();
}
複製程式碼

需要先確定好相機的變換矩陣,這個初始化並不重要。 { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}

CameraEngine::CameraEngine(ANativeWindow *window): mWindow(window),mEGLCore(new EGLCore()),
                                                   mAssetManager(nullptr),mTextureId(0),mTextureLoc(0),
                                                   mMatrixLoc(0){
    //清空mMatrix陣列
    memset(mMatrix,0, sizeof(mMatrix));
    mMatrix[0] = 1;
    mMatrix[5] = 1;
    mMatrix[10] = 1;
    mMatrix[15] = 1;
}
複製程式碼

初始化採集相機紋理的引數。這裡需要了解,採集相機是需要GL_TEXTURE_EXTERNAL_OES來繫結相機紋理,因為相機資料是YUV的編碼格式,而顯示到螢幕上應該是RGB或者RGBA格式,那麼就需要轉換,如果我們採集後再自己手動轉換,將非常耗費GPU和CPU資源,Opengl提供了更加底層的採集解析才能平穩的將資料轉化,這裡就需要引用到GLES2/gl2ext.h,這是獨有的擴充套件紋理庫。

int CameraEngine::create() {
    //這裡面需要初始化EGL
    if (!mEGLCore->buildContext(mWindow)){
        return -1;
    }
    //讀取頂點著色器
    std::string *vShader = readShaderFromAsset(mAssetManager,"camera.vert");
    //讀取片段著色器
    std::string *fShader = readShaderFromAsset(mAssetManager,"camera.frag");
    //載入程式
    mProgram = loadProgram(vShader->c_str(),fShader->c_str());

    //生成紋理貼圖
    glGenTextures(1,&mTextureId);
    //繫結紋理,這裡面使用GL_TEXTURE_EXTERNAL_OES用於採集相機紋理
    glBindTexture(GL_TEXTURE_EXTERNAL_OES,mTextureId);
    //設定縮小過濾為使用紋理中座標最接近的一個畫素的顏色作為需要繪製的畫素顏色,少量計算,渲染比較快,但是效果差
    glTexParameterf(GL_TEXTURE_EXTERNAL_OES,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
    //設定放大過濾為使用紋理中座標最接近的若干個顏色,通過加權平均演算法得到需要繪製的畫素顏色,需要演算法計算,用時相對變長,效果好
    glTexParameterf(GL_TEXTURE_EXTERNAL_OES,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    //這裡GL_TEXTURE_WRAP_S 紋理座標是以S軸方向與T軸方向紋理(對應平面座標x,y方向)
    glTexParameterf(GL_TEXTURE_EXTERNAL_OES,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_EXTERNAL_OES,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);

    //初始化矩陣繫結
    mMatrixLoc = glGetUniformLocation(mProgram,"mMatrix");
    //初始化紋理繫結
    mTextureLoc = glGetUniformLocation(mProgram,"sTexture");
    //使用白色清屏
    glClearColor(1.0f,1.0f,1.0f,1.0f);

    delete vShader;
    delete fShader;
    return mTextureId;
}
複製程式碼

EGL是介於諸如OpenGL 或OpenVG的Khronos渲染API與底層本地平臺視窗系統的介面。它被用於處理圖形管理、表面/緩衝捆綁、渲染同步及支援使用其他Khronos API進行的高效、加速、混合模式2D和3D渲染。這裡用於做離屏渲染(緩衝渲染)。介紹一下EGL初始化過程

GLboolean EGLCore::buildContext(ANativeWindow *window) {
    //與本地視窗通訊
    mDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (mDisplay == EGL_NO_DISPLAY){
        ALOGE("eglGetDisplay failed: %d",eglGetError());
        return GL_FALSE;
    }

    GLint majorVersion;
    GLint minorVersion;
    //獲取支援最低和最高版本
    if (!eglInitialize(mDisplay,&majorVersion,&minorVersion)){
        ALOGE("eglInitialize failed: %d",eglGetError());
        return GL_FALSE;
    }

    EGLConfig config;
    EGLint numConfigs = 0;
    //顏色使用565,讀寫型別需要egl擴充套件
    EGLint attribList[] = {
            EGL_RED_SIZE,5, //指定RGB中的R大小(bits)
            EGL_GREEN_SIZE,6, //指定G大小
            EGL_BLUE_SIZE,5,  //指定B大小
            EGL_RENDERABLE_TYPE,EGL_OPENGL_ES3_BIT_KHR, //渲染型別,為相機擴充套件型別
            EGL_SURFACE_TYPE,EGL_WINDOW_BIT,  //繪圖型別,
            EGL_NONE
    };

    //讓EGL推薦匹配的EGLConfig
    if(!eglChooseConfig(mDisplay,attribList,&config,1,&numConfigs)){
        ALOGE("eglChooseConfig failed: %d",eglGetError());
        return GL_FALSE;
    }

    //找不到匹配的
    if (numConfigs <1){
        ALOGE("eglChooseConfig get config number less than one");
        return GL_FALSE;
    }

    //建立渲染上下文
    //只使用opengles3
    GLint contextAttrib[] = {EGL_CONTEXT_CLIENT_VERSION,3,EGL_NONE};
    // EGL_NO_CONTEXT表示不向其它的context共享資源
    mContext = eglCreateContext(mDisplay,config,EGL_NO_CONTEXT,contextAttrib);
    if (mContext == EGL_NO_CONTEXT){
        ALOGE("eglCreateContext failed: %d",eglGetError());
        return GL_FALSE;
    }

    EGLint format = 0;
    if (!eglGetConfigAttrib(mDisplay,config,EGL_NATIVE_VISUAL_ID,&format)){
        ALOGE("eglGetConfigAttrib failed: %d",eglGetError());
        return GL_FALSE;
    }
    ANativeWindow_setBuffersGeometry(window,0,0,format);

    //建立On-Screen 渲染區域
    mSurface = eglCreateWindowSurface(mDisplay,config,window,0);
    if (mSurface == EGL_NO_SURFACE){
        ALOGE("eglCreateWindowSurface failed: %d",eglGetError());
        return GL_FALSE;
    }

    //把EGLContext和EGLSurface關聯起來
    if (!eglMakeCurrent(mDisplay,mSurface,mSurface,mContext)){
        ALOGE("eglMakeCurrent failed: %d",eglGetError());
        return GL_FALSE;
    }

    ALOGD("buildContext Succeed");
    return GL_TRUE;
}

複製程式碼

初始化之後,等待相機回撥後,才可以開始開啟繪製。

 fun initOpenGL(surface: Surface, width: Int, height: Int){
        mExecutor.execute {
            //獲取紋理id
            val textureId = OpenGLJniLib.magicBaseInit(surface,width,height,BaseApplication.context.assets)
            if (textureId < 0){  //返回紋理繫結是否正常
                Log.e(TAG, "surfaceCreated init OpenGL ES failed!")
                return@execute
            }
            //需要使用surfaceTexture來做紋理裝載
            mSurfaceTexture = SurfaceTexture(textureId)
            //新增紋理變化回撥
            mSurfaceTexture?.setOnFrameAvailableListener { 
                    //開始畫圖
                    drawOpenGL() 
            }
            ……省略程式碼……
        }
    }
複製程式碼

這裡需要讀取到相機的變換矩陣紋理,矩陣中包括相機圖片的影象是是否正對你,是否有角度偏移。我們都可以通過這個矩陣來調整

JNIEXPORT void JNICALL
Java_com_cangwang_magic_util_OpenGLJniLib_magicBaseDraw(JNIEnv *env, jobject obj,jfloatArray matrix_) {
    //獲取矩陣,陣列轉指標
    jfloat *matrix = env->GetFloatArrayElements(matrix_,NULL);

    std::unique_lock<std::mutex> lock(gMutex);
    if (!glCamera){
        ALOGE("draw error, glCamera is null");
        return;
    }
    //啟動畫圖
    glCamera->draw(matrix);
    //釋放矩陣記憶體
    env->ReleaseFloatArrayElements(matrix_,matrix,0);
}
複製程式碼

使用GL_TEXTURE_EXTERNAL_OES紋理繪製,matrix是相機採集時傳入的矩陣引數。

void CameraEngine::draw(GLfloat *matrix) {
    glViewport(0,0,mWidth,mHeight);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(mProgram);
    //啟用紋理
    glActiveTexture(GL_TEXTURE0);
    //繫結紋理
    glBindTexture(GL_TEXTURE_EXTERNAL_OES,mTextureId);
    //載入紋理
    glUniform1i(mTextureLoc,0);
    //載入矩陣
    glUniformMatrix4fv(mMatrixLoc,1,GL_FALSE,matrix);
    //開啟頂點陣列緩衝區,第0個
    glEnableVertexAttribArray(ATTRIB_POSITION);
    //引數1:頂點陣列索引,引數2:每次取的數量 引數3:資料格式 引數4:是否需要浮點轉換 引數5:跨距取值,引數6:儲存頂點屬性資料的緩衝區指標
    glVertexAttribPointer(ATTRIB_POSITION,VERTEX_POS_SIZE,GL_FLOAT,GL_FALSE,0,VERTICES);
    //開啟頂點陣列緩衝區 第1個
    glEnableVertexAttribArray(ATTRIB_TEXCOORD);
    glVertexAttribPointer(ATTRIB_TEXCOORD,TEX_COORD_POS_SZIE,GL_FLOAT,GL_FALSE,0,TEX_COORDS);
    //畫方形
    glDrawArrays(GL_TRIANGLE_STRIP,0,VERTEX_NUM);

    //關閉緩衝區
    glDisableVertexAttribArray(ATTRIB_POSITION);
    //關閉緩衝區
    glDisableVertexAttribArray(ATTRIB_TEXCOORD);

    //清空緩衝區,將指令送往硬體立刻執行
    glFlush();
    //緩衝區交換
    mEGLCore->swapBuffer();
}
複製程式碼

說一下離屏渲染之後,交換到前臺顯示的原理

void EGLCore::swapBuffer() {
    //雙緩衝繪圖,原來是檢測出前臺display和後臺緩衝的差別的dirty區域,然後再區域替換buffer
    //1)首先計算非dirty區域,然後將非dirty區域資料從上一個buffer拷貝到當前buffer;
    //2)完成buffer內容的填充,然後將previousBuffer指向buffer,同時queue buffer。
    //3)Dequeue一塊新的buffer,並等待fence。如果等待超時,就將buffer cancel掉。
    //4)按需重新計算buffer
    //5)Lock buffer,這樣就實現page flip,也就是swapbuffer
    eglSwapBuffers(mDisplay,mSurface);
}
複製程式碼

glsl分析.png

頂點著色器輸入相機轉換矩陣,來調整xy方向顯示。

#version 300 es

layout(location=0) in vec4 aPosition;
layout(location=1) in vec4 aTexCoord;
//相機轉換矩陣
uniform mat4 mMatrix;

out vec2 vTexCoord;

void main() {
   //矩陣調整相機顯示,相機畫素的座標
    vTexCoord = (mMatrix * aTexCoord).xy;
    gl_Position = aPosition;
}
複製程式碼

這裡需要注意的是片段著色器,需要宣告使用opengl擴充套件庫,才能使用samperExternalOES提示相機採集資料,並提示片段著色器採集之後轉換。

#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require

precision highp float;
//相機採集紋理
uniform samplerExternalOES sTexture;
//座標
in vec2 vTexCoord;

out vec4 fragColor;

void main() {
    /texture/將與紋理座標對應的紋理值從記憶體中取出來,畫素座標和畫素紋理關聯
    //輸出片段著色器的色值
    fragColor = texture(sTexture, vTexCoord);
}
複製程式碼

Native底層的相機影象採集介紹就到這,下一節會介紹,相機採幀轉換介紹。 例子在(MagicCamera3)[github.com/cangwang/Ma…]的CameraActivity當中。

相關文章