GPUImage 是 iOS 上一個基于 OpenGL 進行圖像處理的開源框架,后來有人借鑒它的想法實現(xiàn)了一個 Android 版本的 GPUImage 瑟捣,本文也主要對 Android 版本的 GPUImage 進行分析。
概要
在 GPUImage 中既有對圖像進行處理的食呻,也有對相機內(nèi)容進行處理的搞糕,這里主要以相機處理為例進行分析。
大致會分為三個部分:
- 相機數(shù)據(jù)的采集
- OpenGL 對圖像的處理與顯示
- 相機的拍攝
相機數(shù)據(jù)采集
相機數(shù)據(jù)采集實際上就是把相機的圖像數(shù)據(jù)轉(zhuǎn)換成 OpenGL 中的紋理辫愉。
在相機的業(yè)務(wù)開發(fā)中,會給相機設(shè)置 PreviewCallback
回調(diào)方法将硝,只要相機處于預覽階段恭朗,這個回調(diào)就會被重復調(diào)用屏镊,返回當前預覽幀的內(nèi)容。
camera.setPreviewCallback(GPUImageRenderer.this);
camera.startPreview();
默認情況下痰腮,相機返回的數(shù)據(jù)是 NV21
格式而芥,也就是 YCbCr_420_SP
格式,而 OpenGL 使用的紋理是 RGB 格式膀值,所以在每一次的回調(diào)方法中需要將 YUV
格式的數(shù)據(jù)轉(zhuǎn)換成 RGB
格式數(shù)據(jù)棍丐。
GPUImageNativeLibrary.YUVtoRBGA(data, previewSize.width, previewSize.height,
mGLRgbBuffer.array());
有了圖像的 RGB 數(shù)據(jù),就可以使用 glGenTextures
生成紋理沧踏,并用 glTexImage2D
方法將圖像數(shù)據(jù)作為紋理歌逢。
另外,如果紋理已經(jīng)生成了翘狱,當再有圖像數(shù)據(jù)過來時秘案,只需要更新數(shù)據(jù)就好了,無需重復創(chuàng)建紋理潦匈。
// 根據(jù)圖像數(shù)據(jù)加載紋理
public static int loadTexture(final IntBuffer data, final Size size, final int usedTexId) {
int textures[] = new int[1];
if (usedTexId == NO_TEXTURE) {
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
// 省略部分代碼
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, size.width, size.height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
} else {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, usedTexId);
// 更新紋理數(shù)據(jù)就好阱高,無需重復創(chuàng)建紋理
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, size.width,
size.height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
textures[0] = usedTexId;
}
return textures[0];
}
通過在 PreviewCallback
回調(diào)方法中的操作,就完成了將圖像數(shù)據(jù)轉(zhuǎn)換為 OpenGL 的紋理茬缩。
接下來就是如何將紋理數(shù)據(jù)進行處理赤惊,并且顯示到屏幕上。
在相機數(shù)據(jù)采集中凰锡,還有一些小的細節(jié)問題荐捻,比如相機前置與后置攝像頭的左右鏡像翻轉(zhuǎn)問題。
對于前置攝像頭寡夹,再把傳感器內(nèi)容作為紋理顯示時,前置攝像頭要做一個左右的翻轉(zhuǎn)處理厂置,因為我們看到的是一個鏡像內(nèi)容菩掏,符合正常的自拍流程。
在 GPUImage 的 TextureRotationUtil 類中有定義了紋理坐標昵济,這些紋理坐標系的原點不是位于左下角進行定義的智绸,而是位于左上角。
如果以左下角為紋理坐標系的坐標原點访忿,那么除了要將紋理坐標向右順時針旋轉(zhuǎn) 90° 之外瞧栗,還需要進行上下翻轉(zhuǎn)才行,至于為什么要向右順時針旋轉(zhuǎn) 90° 海铆,參考這篇文章:
當我們把紋理坐標以左上角為原點迹恐,并相對于頂點坐標順時針旋轉(zhuǎn) 90 ° 之后,才能夠正常的顯示圖像:
// 頂點坐標
static final float CUBE[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
// 以左上角為原點卧斟,并相對于頂點坐標順時針旋轉(zhuǎn) 90° 后的紋理坐標
public static final float TEXTURE_ROTATED_90[] = {
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 1.0f,
0.0f, 0.0f,
};
圖像處理與顯示
在有了紋理之后殴边,需要明確的是憎茂,這個紋理就是相機采集到的圖像內(nèi)容,我們要將紋理繪制到屏幕上锤岸,實際上是繪制一個矩形竖幔,然后紋理是貼在這個矩形上的。
所以是偷,這里可以回顧一下 OpenGL 是如何繪制矩形的拳氢,并且將紋理貼到矩形上。
在 GPUImage 中蛋铆,GPUImageFilter
類就完成了上述的操作馋评,它是 OpenGL 中所有濾鏡的基類。
解析 GPUImageFilter 的代碼實現(xiàn):
在 GPUImageFilter 的構(gòu)造方法中會確定好需要使用的頂點著色器和片段著色器腳本內(nèi)容戒职。
在 init
方法中會調(diào)用 onInit
方法和 onInitialized
方法栗恩。
-
onInit
方法會創(chuàng)建 OpenGL 中的 Program,并且會綁定到著色器腳本中聲明的attribute
和uniform
變量字段洪燥。 -
onInitialized
方法會給一些uniform
字段變量賦值磕秤,在 GPUImageFilter 類中還對不同類型的變量賦值進行了對應(yīng)的方法,比如對float
變量:
protected void setFloat(final int location, final float floatValue) {
runOnDraw(new Runnable() {
@Override
public void run() {
GLES20.glUniform1f(location, floatValue);
}
});
}
在 onDraw 方法中就是執(zhí)行具體的繪制了捧韵,在繪制的時候會執(zhí)行 runPendingOnDrawTasks
方法市咆,這是因為我們在 init
方法去中給著色器語言中的變量賦值,并沒有立即生效再来,而是添加到了一個鏈表中蒙兰,所以需要把鏈表中的任務(wù)執(zhí)行完了才接著執(zhí)行繪制。
public void onDraw(final int textureId, final FloatBuffer cubeBuffer,
final FloatBuffer textureBuffer) {
GLES20.glUseProgram(mGLProgId);
// 執(zhí)行賦值的任務(wù)
runPendingOnDrawTasks();
// 頂點和紋理坐標數(shù)據(jù)
GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);
GLES20.glEnableVertexAttribArray(mGLAttribPosition);
GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
// 在繪制前的最后一波操作
onDrawArraysPre();
// 最終繪制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
在繪制時芒篷,還需要給頂點坐標賦值搜变,給紋理坐標賦值,GPUImageFilter 并沒有去管理頂點坐標和紋理坐標针炉,而是通過傳遞參數(shù)的形式挠他,這樣就不用去處理在前置攝像頭與后置前攝像頭、手機豎立放置與橫屏放置時的關(guān)系了篡帕。
在執(zhí)行具體的 glDrawArrays
方法之前殖侵,還提供了一個 onDrawArraysPre
方法,在這個方法里面還可以執(zhí)行繪制前的最后一波操作镰烧,在某些濾鏡的實現(xiàn)中有用到了拢军。
最后才是 glDrawArrays
方法去完成繪制。
當我們不需要 GPUImageFilter 進行繪制時怔鳖,需要將它銷毀掉茉唉,在 destroy 方法去進行銷毀,并且提供 onDestory 方法去為某些濾鏡提供自定義的銷毀。
public final void destroy() {
mIsInitialized = false;
GLES20.glDeleteProgram(mGLProgId);
onDestroy();
}
public void onDestroy() {
}
在 GPUImageFilter 方法中定義了片段著色器腳本赌渣,這個腳本是將圖像內(nèi)容原樣貼到了矩形上魏铅,并沒有做特殊的圖像處理操作。
而其他濾鏡中坚芜,更改了著色器腳本览芳,也就會對圖像進行其他的處理,在整個 GPUImage 項目中鸿竖,最精華的也就是那些著色器腳本內(nèi)容了沧竟,如何通過著色器去做圖像處理又是一門高深的學問了~~~
解析 GPUImageFilterGroup 的代碼實現(xiàn)
當想要對圖像進行多次處理時,就得考慮使用 GPUImageFilterGroup 了缚忧。
GPUImageFilterGroup 繼承自 GPUImageFilter悟泵, 顧名思義就是一系列 GPUImageFilter 濾鏡的組合,可以把它類比為 ViewGroup 闪水,ViewGroup 即可以包含 View 糕非,也可以包含 ViewGroup ,同樣 GPUImageFilterGroup 即可以包含 GPUImageFilter球榆,也可以包含 GPUImageFilterGroup朽肥。
在用 GPUImageFilterGroup 進行繪制時,需要把所有的濾鏡內(nèi)容都進行一遍繪制持钉,而對于 GPUImageFilterGroup 包含 GPUImageFilterGroup 的情況衡招,就需要把子 GPUImageFilterGroup 內(nèi)的濾鏡內(nèi)容拆分出來,最終是用 mMergedFilters
變量表示所有非 GPUImageFilterGroup 類型的 GPUImageFilter 每强。
// 拿到所有非 GPUImageFilterGroup 的 GPUImageFilter
public void updateMergedFilters() {
List<GPUImageFilter> filters;
for (GPUImageFilter filter : mFilters) {
// 如果濾鏡是 GPUImageFilterGroup 類型始腾,就把它拆分了
if (filter instanceof GPUImageFilterGroup) {
// 遞歸調(diào)用 updateMergedFilters 方法去拆分
((GPUImageFilterGroup) filter).updateMergedFilters();
// 拿到所有非 GPUImageFilterGroup 的 GPUImageFilter
filters = ((GPUImageFilterGroup) filter).getMergedFilters();
if (filters == null || filters.isEmpty())
continue;
// 把 GPUImageFilter 添加到 mMergedFilters 中
mMergedFilters.addAll(filters);
continue;
}
// 如果是非 GPUImageFilterGroup 直接添加了
mMergedFilters.add(filter);
}
}
在 GPUImageFilterGroup 執(zhí)行具體的繪制之前,還創(chuàng)建了和濾鏡數(shù)量一樣多的 FrameBuffer 幀緩沖和 Texture 紋理空执。
// 遍歷時浪箭,選擇 mMergedFilters 的長度,因為 mMergedFilters 里面才是保存的所有的 濾鏡的長度辨绊。
if (mMergedFilters != null && mMergedFilters.size() > 0) {
size = mMergedFilters.size();
// FrameBuffer 幀緩沖數(shù)量
mFrameBuffers = new int[size - 1];
// 紋理數(shù)量
mFrameBufferTextures = new int[size - 1];
for (int i = 0; i < size - 1; i++) {
// 生成 FrameBuffer 幀緩沖
GLES20.glGenFramebuffers(1, mFrameBuffers, i);
// 生成紋理
GLES20.glGenTextures(1, mFrameBufferTextures, i);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[i]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
// 省略部分代碼
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[i]);
// 紋理綁定到幀緩沖上
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, mFrameBufferTextures[i], 0);
// 省略部分代碼
}
}
如果對 FrameBuffer 的使用不熟悉的話奶栖,請參考這篇文章:
if (mMergedFilters != null) {
int size = mMergedFilters.size();
// 相機原始圖像轉(zhuǎn)換的紋理 ID
int previousTexture = textureId;
for (int i = 0; i < size; i++) {
GPUImageFilter filter = mMergedFilters.get(i);
boolean isNotLast = i < size - 1;
// 如果不是最后一個濾鏡,繪制到 FrameBuffer 上邢羔,如果是最后一個,就繪制到了屏幕上
if (isNotLast) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[i]);
GLES20.glClearColor(0, 0, 0, 0);
}
// 濾鏡繪制代碼
if (i == 0) {
// 第一個濾鏡繪制使用相機的原始圖像紋理 ID 和參數(shù)傳遞過來的頂點以及紋理坐標
filter.onDraw(previousTexture, cubeBuffer, textureBuffer);
} else if (i == size - 1) {
//
filter.onDraw(previousTexture, mGLCubeBuffer, (size % 2 == 0) ? mGLTextureFlipBuffer : mGLTextureBuffer);
} else {
// 中間的濾鏡繪制在之前紋理基礎(chǔ)上繼續(xù)繪制桑孩,使用 mGLTextureBuffer 紋理坐標
filter.onDraw(previousTexture, mGLCubeBuffer, mGLTextureBuffer);
}
if (isNotLast) {
// 綁定到屏幕上
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
previousTexture = mFrameBufferTextures[i];
}
}
}
在執(zhí)行具體的繪制時拜鹤,只要不是最后一個濾鏡,那么就會先綁定到 FrameBuffer 上流椒,然后在 FrameBuffer 上進行繪制敏簿,這時繪制是繪制到了 FrameBuffer 綁定的紋理上,繪制結(jié)束后再接著解綁,綁定到屏幕上惯裕。
如果是最后一個濾鏡温数,那么就不用綁定到 FrameBuffer 上了,直接繪制到屏幕上即可蜻势。
在這里有個細節(jié)撑刺,就是如下的代碼:
filter.onDraw(previousTexture, mGLCubeBuffer, (size % 2 == 0) ? mGLTextureFlipBuffer : mGLTextureBuffer);
如果是最后一個濾鏡,并且濾鏡個數(shù)為偶數(shù)握玛,則使用 mGLTextureFlipBuffer 的紋理坐標够傍,否則使用 mGLTextureBuffer 的紋理坐標。
// 對應(yīng)的紋理坐標為 TEXTURE_NO_ROTATION
mGLTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);
// 對應(yīng)的紋理坐標為 TEXTURE_NO_ROTATION挠铲,并且 true 的參數(shù)表示進行垂直上下翻轉(zhuǎn)
float[] flipTexture = TextureRotationUtil.getRotation(Rotation.NORMAL, false, true);
mGLTextureFlipBuffer.put(flipTexture).position(0);
在第一個濾鏡繪制時冕屯,使用的是參數(shù)傳遞過來的頂點坐標和紋理坐標,中間部分的濾鏡使用的是 mGLTextureBuffer 紋理坐標拂苹,它對應(yīng)的紋理坐標數(shù)組為 TEXTURE_NO_ROTATION
安聘。
在前面講到過,GPUImage 的紋理坐標原點是位于左上角的瓢棒,所以使用 TEXTURE_NO_ROTATION
的紋理坐標實質(zhì)上是將圖像進行了上下翻轉(zhuǎn)浴韭,兩次調(diào)用TEXTURE_NO_ROTATION
紋理坐標時,又將圖像復原了音羞,這也就可以解釋為什么濾鏡個數(shù)為偶數(shù)時囱桨,需要使用 mGLTextureFlipBuffer
紋理坐標將圖像再進行一次翻轉(zhuǎn),而 mGLTextureBuffer
紋理坐標不需要了嗅绰。
當明白了 GPUImageFilter 和 GPUImageFilterGroup 的實現(xiàn)之后舍肠,再去看具體的 Renderer 的代碼就明了多了。
在 onSurfaceCreated
和 onSurfaceChanged
方法中分別對濾鏡進行初始化以及設(shè)定寬窘面、高翠语,在 onDrawFrame 方法中調(diào)用具體的繪制。
當切換濾鏡時财边,先將上一個濾鏡銷毀掉肌括,然后初始化新的濾鏡并設(shè)定寬、高酣难。
final GPUImageFilter oldFilter = mFilter;
mFilter = filter;
if (oldFilter != null) {
oldFilter.destroy();
}
mFilter.init();
GLES20.glUseProgram(mFilter.getProgram());
mFilter.onOutputSizeChanged(mOutputWidth, mOutputHeight);
圖像拍攝保存處理
在 GPUImage 中相機的拍攝是調(diào)用 Camera 的 takePicture
方法谍夭,在該方法中返回相機采集的原始圖像數(shù)據(jù),然后再對該數(shù)據(jù)進行一遍濾鏡處理后并保存憨募。
調(diào)用的最后都是通過 glReadPixels
方法將處理后的圖像讀取出來紧索,并保存為 Bitmap 。
private void convertToBitmap() {
int[] iat = new int[mWidth * mHeight];
IntBuffer ib = IntBuffer.allocate(mWidth * mHeight);
mGL.glReadPixels(0, 0, mWidth, mHeight, GL_RGBA, GL_UNSIGNED_BYTE, ib);
int[] ia = ib.array();
// glReadPixels 讀取的內(nèi)容是上下翻轉(zhuǎn)的菜谣,要處理一下
for (int i = 0; i < mHeight; i++) {
for (int j = 0; j < mWidth; j++) {
iat[(mHeight - i - 1) * mWidth + j] = ia[i * mWidth + j];
}
}
mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
mBitmap.copyPixelsFromBuffer(IntBuffer.wrap(iat));
}
小結(jié)
對 GPUImage 的分析以及濾鏡架構(gòu)的設(shè)計大致就是這樣了珠漂,這些都還不是它的精華啦晚缩,重要的還是它的那些著色器腳本,從那些著色器腳本中學會如果通過 GLSL
去實現(xiàn)圖像處理算法媳危。
一起交流學習荞彼,答疑解惑,有問題鸣皂,我們星球見~~~
對 OpenGL 感興趣的朋友签夭,歡迎關(guān)注微信公眾號:【紙上淺談】第租,獲得最新文章推送~~~