Android 關(guān)于美顏/濾鏡 利用PBO從OpenGL錄制視頻

前言


上次我寫了一遍文章《Android 關(guān)于美顏/濾鏡 從OpenGl錄制視頻的一種方案》,里面利用ImageReader來從獲取Surface上獲取數(shù)據(jù),但是經(jīng)過@熊皮皮的提醒,我發(fā)現(xiàn)多PBO的確可以實(shí)現(xiàn)跟ImageReader一樣的效果,并且版本要求僅為Android4.3。

代碼已上傳至GitHub

濾鏡部分來源于《Android圖像處理之實(shí)時濾鏡》

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

正文


1.原理

什么是PBO?PBO就是PixelBufferObject(像素緩存對象),它跟VBO很相似,只不過一個存像素數(shù)據(jù),一個存頂點(diǎn)數(shù)據(jù),你可以通過《OpenGL像素緩沖區(qū)對象(PBO)》了解。

其實(shí)上篇文章里我列舉的幾個方法里面已經(jīng)有PBO了,但是因?yàn)槲抑坝玫氖菃蝹€PBO,結(jié)果測試發(fā)現(xiàn)效率不行就放棄了。

單PBO獲取像素信息如下:

//綁定到PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(0));
//從FBO中讀取數(shù)據(jù)寫入到PBO中
GLES30.glReadPixels(0, 0, 480, 640, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE,0);
//將OpenGL緩存區(qū)映射到客戶端內(nèi)存
ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 480 * 640 * 4, GLES30.GL_MAP_READ_BIT);
//取消內(nèi)存映射
GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
//解除PBO綁定
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);

這上面代碼其實(shí)沒有什么問題,包括GLES30.glReadPixels()時間都已經(jīng)降為0,但就是在執(zhí)行函數(shù) GLES30.glMapBufferRange()映射內(nèi)存的時候非常慢。

后來經(jīng)過提醒后我重新翻看了《OpenGL像素緩沖區(qū)對象(PBO)》后發(fā)現(xiàn)我之前忽略了二點(diǎn)。

第一個問題是 GLES30.glMapBufferRange()這個函數(shù)實(shí)際會等待GPU完成了對相應(yīng)緩沖區(qū)對象的操作后才會返回,所以我使用單個PBO并不能顯著的提高傳輸效率,而PBO的主要優(yōu)點(diǎn)在于可以通過DMA(Direct Memory Access)進(jìn)行異步傳輸數(shù)據(jù),從而不影響CPU的時鐘周期,所以使用2個PBO, 一個PBO拷貝數(shù)據(jù)、一個PBO映射內(nèi)存,交替使用,效率將大大提高。

第二個問題就是字節(jié)對齊問題,OpenGLES默認(rèn)以4字節(jié)對齊,也就是說我取得的rowStride應(yīng)該是4的整數(shù)倍,計算公式如下:

int align = 4;//4字節(jié)對齊
int rowStride = (width * pixelStride + (align - 1)) & ~(align - 1);

而我在GLES30.glReadPixels()中使用的參數(shù)是GLES30.GL_RGBA,pixelStride應(yīng)該等于4,那么就有(width * 4 + (4 - 1)) & ~(4 - 1) == width * 4,從這個道理上來講,我的width無論取得什么應(yīng)該都是內(nèi)存對齊的,效率不應(yīng)該會降低,事實(shí)上大部分機(jī)子都沒有問題,但是在索尼Z2上效率下降了。

經(jīng)過我實(shí)驗(yàn)后發(fā)現(xiàn)如果我是128字節(jié)對齊,那么效率不會降低,代碼如下:

int align = 128;//128字節(jié)對齊
int rowStride = (width * mPixelStride + (align - 1)) & ~(align - 1);

事實(shí)上這里我很奇怪,理論上GLES20.glPixelStore()最大值應(yīng)該是8,怎么都不可能是128,我懷疑這個值應(yīng)該跟硬件和屏幕分辨率有關(guān),因?yàn)镮mageReader計算出來的rowStride和我計算出來的值不一樣,但是我沒有在網(wǎng)上找到相關(guān)的資料,如果有誰知道請留言告知我下,謝謝。

關(guān)于內(nèi)存對齊你可以通過《關(guān)于內(nèi)存對齊的那些事》了解。

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

//綁定到第一個PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboIndex));
//從FBO中讀取數(shù)據(jù)寫入到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緩存區(qū)映射到客戶端內(nèi)存
ByteBuffer byteBuffer = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, 480 * 640 * 4, GLES30.GL_MAP_READ_BIT);
//取消內(nèi)存映射
GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER);
//解除PBO綁定
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0);
//交換索引
mPboIndex = (mPboIndex + 1) % 2;
mPboNewIndex = (mPboNewIndex + 1) % 2;

經(jīng)過修改后,2個PBO輪流交替使用,就完全可以滿足需求。

2.實(shí)現(xiàn)

實(shí)際上面講完,這篇文章就可以結(jié)束了,但是我怎么會滿足呢!所以我對MagicCamera進(jìn)行了一些修改。

1.去除grafika方法

在使用PBO之后,grafika方法就已經(jīng)失去作用了,并且在MagicCamera的寫法中過了2次濾鏡(繪制到本地窗口一次,繪制到Surface一次),所以開啟錄制后OpenGL的計算量將加倍。

這里直接刪除encoder文件夾。

2.修改原來的繪制方案

原來的繪制方案是先將攝像頭數(shù)據(jù)繪制到FBO,然后將返回的紋理經(jīng)過濾鏡后繪制到本地窗口。

但是因?yàn)橐褂肞BO,所以我先將攝像頭數(shù)據(jù)過濾鏡后繪制到FBO,然后以屏幕大小繪制到本地窗口,和以錄制大小繪制到另一個FBO在通過PBO獲取數(shù)據(jù)。

這樣做的好處就是3個大小,屏幕大小、攝像頭大小、錄制大小可以各不相同。

流程圖.png

這樣需要注意一點(diǎn)因?yàn)槠聊淮笮『弯浿拼笮〔幌嗤?,所以它們的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)也不相同,需要重新計算屏幕坐標(biāo)錄制坐標(biāo)。

修改后代碼請看CameraGlSurfaceView

3.開始繪制

接下來就可以開始繪制了,首先將攝像頭數(shù)據(jù)經(jīng)過濾鏡后繪制到FBO。

1.初始化FBO,完整代碼請看GPUImageFilter
//生成FBO
GLES20.glGenFramebuffers(1, mFrameBuffers, 0);
//生成紋理
GLES20.glGenTextures(1, mFrameBufferTextures, 0);
//綁定到紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0]);

//...省略設(shè)置紋理參數(shù)

//將紋理關(guān)聯(lián)到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);

上面將紋理關(guān)聯(lián)到FBO,這樣就可以直接繪制到紋理上。

2.將攝像頭數(shù)據(jù)經(jīng)過濾鏡后繪制到FBO,完整代碼請看GPUImageFilter
//設(shè)定為攝像頭大小
GLES20.glViewport(0, 0, 480, 640);
//綁定到FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);

//...省略其他代碼

//設(shè)置矩陣,該矩陣從攝像頭獲得
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);
//設(shè)定為屏幕大小
GLES20.glViewport(0, 0, 1080, 1920);

上面的矩陣通過mSurfaceTexture.getTransformMatrix(mtx)獲得,頂點(diǎn)著色器需要添加參數(shù)。

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必須要注意,當(dāng)我們使用mSurfaceTexture.updateTexImage()時,圖像會被隱式的綁定到GL_TEXTURE_EXTERNAL_OES,所以這里跟我們一般使用的紋理GL_TEXTURE_2D不同。

所以片段著色器也必須要修改,下面是沒有濾鏡的實(shí)現(xiàn),其他的看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);

這里的頂點(diǎn)著色器片段著色器需要去除矩陣和OES參數(shù)。

4.如果開始錄制將返回的紋理繪制到FBO然后通過PBO獲得數(shù)據(jù),完整代碼請看MagicRecordFilter

1.初始化PBO,完整代碼請看MagicRecordFilter

final int align = 128;//128字節(jié)對齊
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));
//設(shè)置內(nèi)存大小
GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, mPboSize, null,GLES30.GL_STATIC_READ);

//綁定到第而個PBO
GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(1));
//設(shè)置內(nèi)存大小
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

//設(shè)定為錄制大小
GLES20.glViewport(0, 0, 240, 320);
//綁定到FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);

//...省略其他代碼

//設(shè)置矩陣
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);
//設(shè)定為屏幕大小
GLES20.glViewport(0, 0, 1080, 1920);

這里也需要設(shè)置矩陣,但是這個矩陣不是從攝像頭獲取的,而是我自己把它垂直翻轉(zhuǎn)了下。

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

為什么我要垂直翻轉(zhuǎn)呢,因?yàn)镽GB圖像在內(nèi)存中存儲的時候是從下到上的,如果你直接把數(shù)據(jù)賦值給Bitmap,那么你將得到一張倒置的并且顏色為BGRA的圖像,這也可以解釋為什么我們最終要將BGRA轉(zhuǎn)換為ARGB,因?yàn)锽itmap需要的是Bitmap.Config.ARGB_8888

這里你可以通過 《Image Stride(內(nèi)存圖像行跨度)》了解。

3.PBO獲取數(shù)據(jù),完整代碼請看MagicRecordFilter

private void bindPixelBuffer() {
    //綁定到第一個PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboIndex));
    //調(diào)用glReadPixels獲取數(shù)據(jù),這里需要注意原生的Java里面沒有與PBO配合的glReadPixels方法
    MagicJni.glReadPixels(0, 0, mRowStride, mInputHeight, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE);

    //第一幀沒有數(shù)據(jù)跳出
    if (mInitRecord) {
        unbindPixelBuffer();
        mInitRecord = false;
        return;
    }

    //綁定到第二個PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, mPboIds.get(mPboNewIndex));

    //glMapBufferRange會等待DMA傳輸完成,所以需要交替使用pbo
    //映射內(nèi)存
    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()最后一個參數(shù)必須為0,但是原生Java層的glReadPixels()最后一個參數(shù)是Buffer,而最后參數(shù)為int的glReadPixels()24版本才有,所以這里需要使用jni去調(diào)用原生的glReadPixels()方法,代碼在MagicJni。

關(guān)于RecordHelper我就不講了,跟上篇一樣,這里可以用libyuv代替,我這只是作為測試瀏覽用。

我這里JNI采用CMake編譯,編譯指令在CMakeLists.txt,更多可以參考谷歌官方文檔《向您的項目添加 C 和 C++ 代碼》。

結(jié)尾


其實(shí)在篇文章我早就寫完了,但是一直搞不清楚rowStride的計算方式,最終我決定還是不拖了,直接發(fā)布希望有誰知道的能指點(diǎn)下,謝謝。

最后,如果它有解決你的問題的話,請下點(diǎn)個贊,謝謝。

這是我個人的第四篇文章,發(fā)布于2017年5月15日。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 這篇文章是有感而發(fā),從一開始做實(shí)時美顏視頻錄制到現(xiàn)在大概能真正開始用,找了無數(shù)資料,也經(jīng)歷了很長一段時間,真...
    某金閱讀 26,277評論 57 110
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,650評論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評論 25 708
  • 時,吾心而語"累否?" 吾曰"否,何為累者?" 心言道"實(shí)不可虛,虛卻可謂實(shí)!" 吾問"那何為虛與實(shí)?" 心著一眼...
    小施是個大可愛閱讀 331評論 0 1
  • 有一個來自安徽的姑娘,她叫林特特。在2013年,她曾經(jīng)出版過一本書,叫做《以自己喜歡的方式過一生》。我讀過這本書后...
    作家格格閱讀 361評論 0 2

友情鏈接更多精彩內(nèi)容