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录制视频的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 鹅肉多少钱啊?
- 下一篇: android媒体播放框架,Androi