日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

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

發布時間:2023/12/1 Android 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 android 美颜录像,Android 关于美颜/滤镜 利用PBO从OpenGL录制视频 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

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

代碼已上傳至GitHub

提示:工程需要下載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

這樣需要注意一點因為屏幕大小和錄制大小不相同,所以它們的頂點坐標和紋理坐標也不相同,需要重新計算屏幕坐標和錄制坐標。

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

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。

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日。

總結

以上是生活随笔為你收集整理的android 美颜录像,Android 关于美颜/滤镜 利用PBO从OpenGL录制视频的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。