美文网首页Android开发经验谈Android开发Android技术知识
[OpenGL]未来视觉2-Android摄像头帧采集

[OpenGL]未来视觉2-Android摄像头帧采集

作者: CangWang | 来源:发表于2018-12-24 15:55 被阅读109次

    大家好,我系苍王。
    以下是我这个系列的相关文章,有兴趣可以参考一下,可以给个喜欢或者关注我的文章。

    [Android]如何做一个崩溃率少于千分之三噶应用app--章节列表

    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)[https://github.com/cangwang/MagicCamera3]的CameraActivity当中。

    群号是316556016,也可以扫码进群。我在这里期待你们的加入!!!

    Android组件化群1 Android组件化群2

    相关文章

      网友评论

        本文标题:[OpenGL]未来视觉2-Android摄像头帧采集

        本文链接:https://www.haomeiwen.com/subject/dhyakqtx.html