GPUImage 是 iOS 上一個基於 OpenGL 進行圖像處理的開源框架,後來有人借鑑它的想法實現了一個 Android 版本的 GPUImage ,本文也主要對 Android 版本的 GPUImage 進行分析。java
在 GPUImage 中既有對圖像進行處理的,也有對相機內容進行處理的,這裏主要以相機處理爲例進行分析。android
大體會分爲三個部分:git
相機數據採集實際上就是把相機的圖像數據轉換成 OpenGL 中的紋理。github
在相機的業務開發中,會給相機設置 PreviewCallback
回調方法,只要相機處於預覽階段,這個回調就會被重複調用,返回當前預覽幀的內容。算法
camera.setPreviewCallback(GPUImageRenderer.this);
camera.startPreview();
複製代碼
默認狀況下,相機返回的數據是 NV21
格式,也就是 YCbCr_420_SP
格式,而 OpenGL 使用的紋理是 RGB 格式,因此在每一次的回調方法中須要將 YUV
格式的數據轉換成 RGB
格式數據。數組
GPUImageNativeLibrary.YUVtoRBGA(data, previewSize.width, previewSize.height,
mGLRgbBuffer.array());
複製代碼
有了圖像的 RGB 數據,就可使用 glGenTextures
生成紋理,並用 glTexImage2D
方法將圖像數據做爲紋理。微信
另外,若是紋理已經生成了,當再有圖像數據過來時,只須要更新數據就行了,無需重複建立紋理。架構
// 根據圖像數據加載紋理
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);
// 更新紋理數據就好,無需重複建立紋理
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
回調方法中的操做,就完成了將圖像數據轉換爲 OpenGL 的紋理。框架
接下來就是如何將紋理數據進行處理,而且顯示到屏幕上。ide
在相機數據採集中,還有一些小的細節問題,好比相機前置與後置攝像頭的左右鏡像翻轉問題。
對於前置攝像頭,再把傳感器內容做爲紋理顯示時,前置攝像頭要作一個左右的翻轉處理,由於咱們看到的是一個鏡像內容,符合正常的自拍流程。
在 GPUImage 的 TextureRotationUtil 類中有定義了紋理座標,這些紋理座標系的原點不是位於左下角進行定義的,而是位於左上角。
若是以左下角爲紋理座標系的座標原點,那麼除了要將紋理座標向右順時針旋轉 90° 以外,還須要進行上下翻轉才行,至於爲何要向右順時針旋轉 90° ,參考這篇文章:
當咱們把紋理座標以左上角爲原點,並相對於頂點座標順時針旋轉 90 ° 以後,纔可以正常的顯示圖像:
// 頂點座標
static final float CUBE[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
// 以左上角爲原點,並相對於頂點座標順時針旋轉 90° 後的紋理座標
public static final float TEXTURE_ROTATED_90[] = {
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 1.0f,
0.0f, 0.0f,
};
複製代碼
在有了紋理以後,須要明確的是,這個紋理就是相機採集到的圖像內容,咱們要將紋理繪製到屏幕上,其實是繪製一個矩形,而後紋理是貼在這個矩形上的。
因此,這裏能夠回顧一下 OpenGL 是如何繪製矩形的,而且將紋理貼到矩形上。
在 GPUImage 中,GPUImageFilter
類就完成了上述的操做,它是 OpenGL 中全部濾鏡的基類。
在 GPUImageFilter 的構造方法中會肯定好須要使用的頂點着色器和片斷着色器腳本內容。
在 init
方法中會調用 onInit
方法和 onInitialized
方法。
onInit
方法會建立 OpenGL 中的 Program,而且會綁定到着色器腳本中聲明的 attribute
和 uniform
變量字段。onInitialized
方法會給一些 uniform
字段變量賦值,在 GPUImageFilter 類中還對不一樣類型的變量賦值進行了對應的方法,好比對 float
變量:protected void setFloat(final int location, final float floatValue) {
runOnDraw(new Runnable() {
@Override
public void run() {
GLES20.glUniform1f(location, floatValue);
}
});
}
複製代碼
在 onDraw 方法中就是執行具體的繪製了,在繪製的時候會執行 runPendingOnDrawTasks
方法,這是由於咱們在 init
方法去中給着色器語言中的變量賦值,並無當即生效,而是添加到了一個鏈表中,因此須要把鏈表中的任務執行完了才接着執行繪製。
public void onDraw(final int textureId, final FloatBuffer cubeBuffer, final FloatBuffer textureBuffer) {
GLES20.glUseProgram(mGLProgId);
// 執行賦值的任務
runPendingOnDrawTasks();
// 頂點和紋理座標數據
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 並無去管理頂點座標和紋理座標,而是經過傳遞參數的形式,這樣就不用去處理在前置攝像頭與後置前攝像頭、手機豎立放置與橫屏放置時的關係了。
在執行具體的 glDrawArrays
方法以前,還提供了一個 onDrawArraysPre
方法,在這個方法裏面還能夠執行繪製前的最後一波操做,在某些濾鏡的實現中有用到了。
最後纔是 glDrawArrays
方法去完成繪製。
當咱們不須要 GPUImageFilter 進行繪製時,須要將它銷燬掉,在 destroy 方法去進行銷燬,而且提供 onDestory 方法去爲某些濾鏡提供自定義的銷燬。
public final void destroy() {
mIsInitialized = false;
GLES20.glDeleteProgram(mGLProgId);
onDestroy();
}
public void onDestroy() {
}
複製代碼
在 GPUImageFilter 方法中定義了片斷着色器腳本,這個腳本是將圖像內容原樣貼到了矩形上,並無作特殊的圖像處理操做。
而其餘濾鏡中,更改了着色器腳本,也就會對圖像進行其餘的處理,在整個 GPUImage 項目中,最精華的也就是那些着色器腳本內容了,如何經過着色器去作圖像處理又是一門高深的學問了~~~
當想要對圖像進行屢次處理時,就得考慮使用 GPUImageFilterGroup 了。
GPUImageFilterGroup 繼承自 GPUImageFilter, 顧名思義就是一系列 GPUImageFilter 濾鏡的組合,能夠把它類比爲 ViewGroup ,ViewGroup 便可以包含 View ,也能夠包含 ViewGroup ,一樣 GPUImageFilterGroup 便可以包含 GPUImageFilter,也能夠包含 GPUImageFilterGroup。
在用 GPUImageFilterGroup 進行繪製時,須要把全部的濾鏡內容都進行一遍繪製,而對於 GPUImageFilterGroup 包含 GPUImageFilterGroup 的狀況,就須要把子 GPUImageFilterGroup 內的濾鏡內容拆分出來,最終是用 mMergedFilters
變量表示全部非 GPUImageFilterGroup 類型的 GPUImageFilter 。
// 拿到全部非 GPUImageFilterGroup 的 GPUImageFilter
public void updateMergedFilters() {
List<GPUImageFilter> filters;
for (GPUImageFilter filter : mFilters) {
// 若是濾鏡是 GPUImageFilterGroup 類型,就把它拆分了
if (filter instanceof GPUImageFilterGroup) {
// 遞歸調用 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 執行具體的繪製以前,還建立了和濾鏡數量同樣多的 FrameBuffer 幀緩衝和 Texture 紋理。
// 遍歷時,選擇 mMergedFilters 的長度,由於 mMergedFilters 裏面纔是保存的全部的 濾鏡的長度。
if (mMergedFilters != null && mMergedFilters.size() > 0) {
size = mMergedFilters.size();
// FrameBuffer 幀緩衝數量
mFrameBuffers = new int[size - 1];
// 紋理數量
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();
// 相機原始圖像轉換的紋理 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 和參數傳遞過來的頂點以及紋理座標
filter.onDraw(previousTexture, cubeBuffer, textureBuffer);
} else if (i == size - 1) {
//
filter.onDraw(previousTexture, mGLCubeBuffer, (size % 2 == 0) ? mGLTextureFlipBuffer : mGLTextureBuffer);
} else {
// 中間的濾鏡繪製在以前紋理基礎上繼續繪製,使用 mGLTextureBuffer 紋理座標
filter.onDraw(previousTexture, mGLCubeBuffer, mGLTextureBuffer);
}
if (isNotLast) {
// 綁定到屏幕上
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
previousTexture = mFrameBufferTextures[i];
}
}
}
複製代碼
在執行具體的繪製時,只要不是最後一個濾鏡,那麼就會先綁定到 FrameBuffer 上,而後在 FrameBuffer 上進行繪製,這時繪製是繪製到了 FrameBuffer 綁定的紋理上,繪製結束後再接着解綁,綁定到屏幕上。
若是是最後一個濾鏡,那麼就不用綁定到 FrameBuffer 上了,直接繪製到屏幕上便可。
在這裏有個細節,就是以下的代碼:
filter.onDraw(previousTexture, mGLCubeBuffer, (size % 2 == 0) ? mGLTextureFlipBuffer : mGLTextureBuffer);
複製代碼
若是是最後一個濾鏡,而且濾鏡個數爲偶數,則使用 mGLTextureFlipBuffer 的紋理座標,不然使用 mGLTextureBuffer 的紋理座標。
// 對應的紋理座標爲 TEXTURE_NO_ROTATION
mGLTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);
// 對應的紋理座標爲 TEXTURE_NO_ROTATION,而且 true 的參數表示進行垂直上下翻轉
float[] flipTexture = TextureRotationUtil.getRotation(Rotation.NORMAL, false, true);
mGLTextureFlipBuffer.put(flipTexture).position(0);
複製代碼
在第一個濾鏡繪製時,使用的是參數傳遞過來的頂點座標和紋理座標,中間部分的濾鏡使用的是 mGLTextureBuffer 紋理座標,它對應的紋理座標數組爲 TEXTURE_NO_ROTATION
。
在前面講到過,GPUImage 的紋理座標原點是位於左上角的,因此使用 TEXTURE_NO_ROTATION
的紋理座標實質上是將圖像進行了上下翻轉,兩次調用TEXTURE_NO_ROTATION
紋理座標時,又將圖像復原了,這也就能夠解釋爲何濾鏡個數爲偶數時,須要使用 mGLTextureFlipBuffer
紋理座標將圖像再進行一次翻轉,而 mGLTextureBuffer
紋理座標不須要了。
當明白了 GPUImageFilter 和 GPUImageFilterGroup 的實現以後,再去看具體的 Renderer 的代碼就明瞭多了。
在 onSurfaceCreated
和 onSurfaceChanged
方法中分別對濾鏡進行初始化以及設定寬、高,在 onDrawFrame 方法中調用具體的繪製。
當切換濾鏡時,先將上一個濾鏡銷燬掉,而後初始化新的濾鏡並設定寬、高。
final GPUImageFilter oldFilter = mFilter;
mFilter = filter;
if (oldFilter != null) {
oldFilter.destroy();
}
mFilter.init();
GLES20.glUseProgram(mFilter.getProgram());
mFilter.onOutputSizeChanged(mOutputWidth, mOutputHeight);
複製代碼
在 GPUImage 中相機的拍攝是調用 Camera 的 takePicture
方法,在該方法中返回相機採集的原始圖像數據,而後再對該數據進行一遍濾鏡處理後並保存。
調用的最後都是經過 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 讀取的內容是上下翻轉的,要處理一下
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));
}
複製代碼
對 GPUImage 的分析以及濾鏡架構的設計大體就是這樣了,這些都還不是它的精華啦,重要的仍是它的那些着色器腳本,從那些着色器腳本中學會若是經過 GLSL
去實現圖像處理算法。
對 OpenGL 感興趣的朋友,歡迎關注微信公衆號:【紙上淺談】,得到最新文章推送~~~