美文网首页Android开发部落Android Dev收藏夹
Android 关于美颜/滤镜 利用PBO从OpenGL录制视频

Android 关于美颜/滤镜 利用PBO从OpenGL录制视频

作者: 某金 | 来源:发表于2017-05-15 11:51 被阅读5467次

    前言


    上次我写了一遍文章《Android 关于美颜/滤镜 从OpenGl录制视频的一种方案》,里面利用ImageReader来从获取Surface上获取数据,但是经过@熊皮皮的提醒,我发现多PBO的确可以实现跟ImageReader一样的效果,并且版本要求仅为Android4.3。

    代码已上传至GitHub

    滤镜部分来源于《Android图像处理之实时滤镜》

    提示:工程需要下载NDK和CMake

    正文


    1.原理

    什么是PBO?PBO就是PixelBufferObject(像素缓存对象),它跟VBO很相似,只不过一个存像素数据,一个存顶点数据,你可以通过《OpenGL像素缓冲区对象(PBO)》了解。

    其实上篇文章里我列举的几个方法里面已经有PBO了,但是因为我之前用的是单个PBO,结果测试发现效率不行就放弃了。

    单PBO获取像素信息如下:

    //绑定到PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(0));
    //从FBO中读取数据写入到PBO中
    GLES30.glReadPixels(0, 0, 480, 640, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,0);
    //将OpenGL缓存区映射到客户端内存
    ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 480 * 640 * 4, GLES30.GL_MAP_READ_BIT);
    //取消内存映射
    GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
    //解除PBO绑定
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
    

    这上面代码其实没有什么问题,包括GLES30.glReadPixels()时间都已经降为0,但就是在执行函数 GLES30.glMapBufferRange()映射内存的时候非常慢。

    后来经过提醒后我重新翻看了《OpenGL像素缓冲区对象(PBO)》后发现我之前忽略了二点。

    第一个问题是 GLES30.glMapBufferRange()这个函数实际会等待GPU完成了对相应缓冲区对象的操作后才会返回,所以我使用单个PBO并不能显著的提高传输效率,而PBO的主要优点在于可以通过DMA(Direct Memory Access)进行异步传输数据,从而不影响CPU的时钟周期,所以使用2个PBO, 一个PBO拷贝数据、一个PBO映射内存,交替使用,效率将大大提高。

    第二个问题就是字节对齐问题,OpenGLES默认以4字节对齐,也就是说我取得的rowStride应该是4的整数倍,计算公式如下:

    int align = 4;//4字节对齐
    int rowStride = (width * pixelStride + (align - 1)) & ~(align - 1);
    

    而我在GLES30.glReadPixels()中使用的参数是GLES30.GL_RGBA,pixelStride应该等于4,那么就有(width * 4 + (4 - 1)) & ~(4 - 1) == width * 4,从这个道理上来讲,我的width无论取得什么应该都是内存对齐的,效率不应该会降低,事实上大部分机子都没有问题,但是在索尼Z2上效率下降了。

    经过我实验后发现如果我是128字节对齐,那么效率不会降低,代码如下:

    int align = 128;//128字节对齐
    int rowStride = (width * mPixelStride + (align - 1)) & ~(align - 1);
    

    事实上这里我很奇怪,理论上GLES20.glPixelStore()最大值应该是8,怎么都不可能是128,我怀疑这个值应该跟硬件和屏幕分辨率有关,因为ImageReader计算出来的rowStride和我计算出来的值不一样,但是我没有在网上找到相关的资料,如果有谁知道请留言告知我下,谢谢

    关于内存对齐你可以通过《关于内存对齐的那些事》了解。

    修改后多PBO获取像素信息如下:

    //绑定到第一个PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboIndex));
    //从FBO中读取数据写入到PBO中
    GLES30.glReadPixels(0, 0, 480, 640, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,0);
    //绑定到第二个PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboNewIndex));
    //将OpenGL缓存区映射到客户端内存
    ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 480 * 640 * 4, GLES30.GL_MAP_READ_BIT);
    //取消内存映射
    GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
    //解除PBO绑定
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
    //交换索引
    mPboIndex = (mPboIndex + 1) % 2;
    mPboNewIndex = (mPboNewIndex + 1) % 2;
    

    经过修改后,2个PBO轮流交替使用,就完全可以满足需求。

    2.实现

    实际上面讲完,这篇文章就可以结束了,但是我怎么会满足呢!所以我对MagicCamera进行了一些修改。

    1.去除grafika方法

    在使用PBO之后,grafika方法就已经失去作用了,并且在MagicCamera的写法中过了2次滤镜(绘制到本地窗口一次,绘制到Surface一次),所以开启录制后OpenGL的计算量将加倍。

    这里直接删除encoder文件夹。

    2.修改原来的绘制方案

    原来的绘制方案是先将摄像头数据绘制到FBO,然后将返回的纹理经过滤镜后绘制到本地窗口。

    但是因为要使用PBO,所以我先将摄像头数据过滤镜后绘制到FBO,然后以屏幕大小绘制到本地窗口,和以录制大小绘制到另一个FBO在通过PBO获取数据。

    这样做的好处就是3个大小,屏幕大小、摄像头大小、录制大小可以各不相同。

    流程图.png

    这样需要注意一点因为屏幕大小和录制大小不相同,所以它们的顶点坐标和纹理坐标也不相同,需要重新计算屏幕坐标录制坐标

    修改后代码请看CameraGlSurfaceView

    3.开始绘制

    接下来就可以开始绘制了,首先将摄像头数据经过滤镜后绘制到FBO。

    1.初始化FBO,完整代码请看GPUImageFilter
    //生成FBO
    GLES20.glGenFramebuffers(1, mFrameBuffers, 0);
    //生成纹理
    GLES20.glGenTextures(1, mFrameBufferTextures, 0);
    //绑定到纹理
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0]);
    
    //...省略设置纹理参数
    
    //将纹理关联到FBO
    GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0], 0);
    //解除绑定纹理
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    //解除绑定FBO
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
    

    上面将纹理关联到FBO,这样就可以直接绘制到纹理上。

    2.将摄像头数据经过滤镜后绘制到FBO,完整代码请看GPUImageFilter
    //设定为摄像头大小
    GLES20.glViewport(0, 0, 480, 640);
    //绑定到FBO
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
    
    //...省略其他代码
    
    //设置矩阵,该矩阵从摄像头获得
    GLES20.glUniformMatrix4fv(mTextureTransformMatrixLocation, 1, false, mTextureTransformMatrix, 0);
    
    //选择活跃纹理
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    //绑定到纹理,这里需要注意GL_TEXTURE_EXTERNAL_OES是特殊的
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
    GLES20.glUniform1i(mGLUniformTexture, 0);
    
    //...省略其他代码
    
    //解除绑定纹理
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
    //解除绑定FBO
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
    //设定为屏幕大小
    GLES20.glViewport(0, 0, 1080, 1920);
    

    上面的矩阵通过mSurfaceTexture.getTransformMatrix(mtx)获得,顶点着色器需要添加参数。

    attribute vec4 position;
    attribute vec4 inputTextureCoordinate;
    
    varying vec2 textureCoordinate;
    
    uniform mat4 textureTransform;
    
    void main() {
        textureCoordinate = (textureTransform * inputTextureCoordinate).xy;
        gl_Position = position;
    }
    

    这里的GL_TEXTURE_EXTERNAL_OES必须要注意,当我们使用mSurfaceTexture.updateTexImage()时,图像会被隐式的绑定到GL_TEXTURE_EXTERNAL_OES,所以这里跟我们一般使用的纹理GL_TEXTURE_2D不同。

    所以片段着色器也必须要修改,下面是没有滤镜的实现,其他的看Raw

    #extension GL_OES_EGL_image_external : require
    
    varying highp vec2 textureCoordinate;
    
    uniform samplerExternalOES inputImageTexture;
    
    void main(){
        gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
    }`
    
    3.将返回的纹理绘制到本地窗口,完整代码请看GPUImageFilter
    //...省略其他代码
    
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    //绑定纹理,这里的纹理是GL_TEXTURE_2D
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
    GLES20.glUniform1i(mGLUniformTexture, 0);
    
    //...省略其他代码
    
    //解除绑定纹理
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    

    这里的顶点着色器片段着色器需要去除矩阵和OES参数。

    4.如果开始录制将返回的纹理绘制到FBO然后通过PBO获得数据,完整代码请看MagicRecordFilter

    1.初始化PBO,完整代码请看MagicRecordFilter

    final int align = 128;//128字节对齐
    mRowStride = (width * mPixelStride + (align - 1)) & ~(align - 1);
    
    mPboIds = IntBuffer.allocate(2);
    //生成2个PBO
    GLES30.glGenBuffers(2, mPboIds);
    
    //绑定到第一个PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(0));
    //设置内存大小
    GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, mPboSize, null,GLES30.GL_STATIC_READ);
    
    //绑定到第而个PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(1));
    //设置内存大小
    GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, mPboSize, null,GLES30.GL_STATIC_READ);
    
    //解除绑定PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
    

    2.绘制2D纹理到FBO,完整代码请看MagicRecordFilter

    //设定为录制大小
    GLES20.glViewport(0, 0, 240, 320);
    //绑定到FBO
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
    
    //...省略其他代码
    
    //设置矩阵
    GLES20.glUniformMatrix4fv(mTextureTransformMatrixLocation, 1, false, mTextureTransformMatrix, 0);
    
    //选择活跃纹理
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    //绑定到纹理
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
    GLES20.glUniform1i(mGLUniformTexture, 0);
    
    //...省略其他代码
    
    //解除绑定纹理
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    //解除绑定FBO
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
    //设定为屏幕大小
    GLES20.glViewport(0, 0, 1080, 1920);
    

    这里也需要设置矩阵,但是这个矩阵不是从摄像头获取的,而是我自己把它垂直翻转了下。

    mTextureTransformMatrix = new float[]{
                    -1f, 0f, 0f, 0f,
                    0f, 1f, 0f, 0f,
                    0f, 0f, 1f, 0f,
                    1f, 0f, 0f, 1f});
    

    为什么我要垂直翻转呢,因为RGB图像在内存中存储的时候是从下到上的,如果你直接把数据赋值给Bitmap,那么你将得到一张倒置的并且颜色为BGRA的图像,这也可以解释为什么我们最终要将BGRA转换为ARGB,因为Bitmap需要的是Bitmap.Config.ARGB_8888

    这里你可以通过 《Image Stride(内存图像行跨度)》了解。

    3.PBO获取数据,完整代码请看MagicRecordFilter

    private void bindPixelBuffer() {
        //绑定到第一个PBO
        GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboIndex));
        //调用glReadPixels获取数据,这里需要注意原生的Java里面没有与PBO配合的glReadPixels方法
        MagicJni.glReadPixels(0, 0, mRowStride, mInputHeight, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE);
    
        //第一帧没有数据跳出
        if (mInitRecord) {
            unbindPixelBuffer();
            mInitRecord = false;
            return;
        }
    
        //绑定到第二个PBO
        GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboNewIndex));
    
        //glMapBufferRange会等待DMA传输完成,所以需要交替使用pbo
        //映射内存
        ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, mPboSize, GLES30.GL_MAP_READ_BIT);
    
        //解除映射
        GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
        unbindPixelBuffer();
    
        //交给mRecordHelper录制
        mRecordHelper.onRecord(byteBuffer, mInputWidth, mInputHeight, mRowStride, mLastTimestamp);
    }
    
    //解绑pbo
    private void unbindPixelBuffer() {
        //解除绑定PBO
        GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
    
        //交换索引
        mPboIndex = (mPboIndex + 1) % 2;
        mPboNewIndex = (mPboNewIndex + 1) % 2;
    }
    

    这里必须要注意,要与PBO配合使用glReadPixels()最后一个参数必须为0,但是原生Java层的glReadPixels()最后一个参数是Buffer,而最后参数为int的glReadPixels()24版本才有,所以这里需要使用jni去调用原生的glReadPixels()方法,代码在MagicJni

    关于RecordHelper我就不讲了,跟上篇一样,这里可以用libyuv代替,我这只是作为测试浏览用。

    我这里JNI采用CMake编译,编译指令在CMakeLists.txt,更多可以参考谷歌官方文档《向您的项目添加 C 和 C++ 代码》

    结尾


    其实在篇文章我早就写完了,但是一直搞不清楚rowStride的计算方式,最终我决定还是不拖了,直接发布希望有谁知道的能指点下,谢谢。

    最后,如果它有解决你的问题的话,请下点个赞,谢谢。

    这是我个人的第四篇文章,发布于2017年5月15日。

    相关文章

      网友评论

      • bc2b8e1c1298:这里测试的分辨率是640*480,效率的确是很快,可如果把分辨率调整到1080*720,效率会立马下降到跟readpixel差不多,这里有尝试过720p以上的视频录制吗?:heart:
      • 899e2d74bdeb:你好,我在我手机上测试你的项目的时候,发现在ImageView上显示的视频很卡(很久才显示下一帧),这是转换视频数据造成的吧,与pbo的效率应该没有关系吧
        某金:是的,不过你也可以打印下,有可能某些机子会有问题
      • 899e2d74bdeb:请教一个问题,能否直接获取纹理数据,进行编码,而不需要再写入fbo中,再从pbo中获取数据进行编码。
        某金:@哎疯 你可以试下glReadPixels,但是效率是跟不上的
        899e2d74bdeb:@某金 你好,我一直纠结一个问题,能否把已经绘制到屏幕的图像数据获取到,然后直接编码,这样就不用写到fbo中。我这个想法究竟可不可行,是不是对的。大概流程就是:摄像头数据->滤镜->屏幕->录制。
        某金:直接获取?不用pbo应该不行效率跟不上
      • vincent_leo:为什么我这边使用PBO后 glReadPixels效率没有得到明显改善,倒是glMapBufferRange总是几乎为0,不管使用单PBO还是双PBO
        某金:@vincent_leo 直接映射到问题?
        vincent_leo:@某金 你好 我确保我确实绑定了PBO 。还有一个问题 我想请教下 我如何把pbo数据直接映射到问题。而不是现在拿到pix像素数组来自己处理呢
        某金:你绑定到pbo后,glReadPixels的操作将会被交给dma处理,所以glReadPixels会直接返回,如果还卡住就说明你没有绑定到pbo
      • 1c72477e2e9c:文中:BGRA转换为ARGB,是BGRA ? 还是RGBA ?
        某金:@逍遥无心 libyuv试试
        1c72477e2e9c:@某金 有什么比较快的方法把BGRA转成yuv?
        某金:是BGRA,看http://blog.csdn.net/g0ose/article/details/52116453
      • 笨鱼_7dac:能否解析一下使用FBO的时候为什么没有使用
        glGenRenderbuffers(1, mRenderbuffers);这些函数呢?
      • 1c72477e2e9c:厉害啊
      • zzx_ac85:怎么才能在取出数据后对帧数据进行镜像加旋转。
        某金:对矩阵坐下处理,可以参照下上面的
      • jionkang:GLES30.glReadPixels(0, 0, 480, 640, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,0); 7.0手机才行为什么说4。3呢
        某金:4.3开始支持OPENGLES3.0
      • 567dacd0f6dd:录制的视频文件在哪呢,大神。刚接触这块
        某金: @音符跳跃思念每天 并没有录制成视频,录制你可以用mediacodec
      • 迷途小书童nb:有空研究下pbo, 效率咋样?内存对齐好像用16或32多些吧。
        某金: @CAT1024 额,因为那不是我关注的点,而且写起来也麻烦😅
        CAT1024:为什么不处理成本地视频,只能得到一帧帧的bitmap
        某金:PBO效率很快,目前测试了十几台机子效率都很不错,关于对齐问题,32位的cpu4字节对齐,64的cpu应该是8字节对齐,但是在索尼z2上我设置了128字节对齐才没有降低效率,这里我很疑惑。

      本文标题:Android 关于美颜/滤镜 利用PBO从OpenGL录制视频

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