Android現行的Camera API2機制可以通過onImageAvailable(ImageReader reader)回調從底層獲取到Jpeg、Yuv和Raw三種格式的Image,然后通過保存Image實現拍照功能,但是卻并沒有Api能直接在上層直接拿到實時預覽的數據。
Android Camera預覽的實現是上層下發Surface到CameraHAL,由CameraHAL也就是android.hardware.camera.provider@2.4-service進程往Surface對應的Buffer中填充預覽數據,然后再copy到SurfaceFling中由OpenGL進行渲染顯示。
實際相機開發中,不僅僅只是要實現預覽,還經常需要拿到預覽數據做一些特效處理,那么問題來了,怎么在相機App獲取到實時預覽數據呢?
這跟上層Camera App用于顯示Surface的View控件有關:
- 如果上層使用的是GLSurfaceView,可以直接通過OpenGLES的glReadPixels()獲取到copy到顯存中的預覽數據
- 如果上層使用的不是GLSurfaceView,可以通過自己搭建EGL環境,然后在EGL環境中調用OpenGLES的glReadPixels()獲取到預覽數據。
GLSurfaceView其實就是Android封裝好的EGL+SufaceView控件,Android的所有渲染最終都是通過OpenGL來實現的,所以萬變不離其宗,本質上上層Camera App都只能通過OpenGLES的glReadPixels()實現預覽數據的獲取。
一個Surface在Android EGL中對應一個FrameBuffer,學習過OpenGL的應該都知道,一個FrameBuffer會有多個附著(attachment),其中必須且只能有一個ColorBuffer附著,有一個或多個StencilBuffer、DepthBuffer附著。
glReadPixels()僅限于讀取ColorBuffer,無法讀取DepthBuffer和StencilBuffer,它可以將圖像內容從顯存讀取到內存中,將ColorBuffer中的像素值保存到預分配的內存緩沖區。
前面關于OpenGLES的博文中,有兩篇是使用OpenGLES實現相機的相關功能,一篇是《OpenGLES:GLSurfaceView實現Android Camera預覽》,一篇是《OpenGLES:相機實時濾鏡四宮格、九宮格》,今天就在這兩篇博文基礎上實現相機預覽數據的獲取和保存。
相機實現部分在此不做過多講解,有興趣的可以參看前面兩篇博文,有詳細的講解
本文主要展示glReadPixels()對相機預覽數據獲取的實現
代碼實現其實很簡單
在GLSurfaceView.Renderer實現類的onDrawFrame(GL10 gl)函數中新增如下代碼段:
if (shouldTakePic) {//預覽尺寸int w = 1080;int h = 1440;//預覽數據保存成照片的目錄String savePath = Environment.getExternalStorageDirectory().getPath() + "/DCIM/MyCamera/";int[] iat = new int[w * h];IntBuffer ib = IntBuffer.allocate(w * h);//(0,580)距離屏幕左下角的距離,與glViewport(0, 580,...)保持一致glReadPixels(0, 580, w, h, GL_RGBA, GL_UNSIGNED_BYTE, ib);int[] ia = ib.array();//glReadPixels 讀取的內容是上下翻轉的,要處理一下for (int i = 0; i < h; i++) {for (int j = 0; j < w; j++) {iat[(h - i - 1) * w + j] = ia[i * w + j];}}Bitmap inBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);inBitmap.copyPixelsFromBuffer(IntBuffer.wrap(iat));ByteArrayOutputStream bos = new ByteArrayOutputStream();inBitmap.compress(Bitmap.CompressFormat.JPEG, 90, bos);byte[] bitmapData = bos.toByteArray();File tempDir = new File(savePath);tempDir.mkdirs();String fileName = "temp_" + System.currentTimeMillis() + ".jpeg";File imgFile = new File(savePath, fileName);try {FileOutputStream output = new FileOutputStream(imgFile);output.write(bitmapData);output.flush();output.close();Log.v(TAG, "ImageReader X");} catch (Exception e) {e.printStackTrace();} finally {inBitmap.recycle();}
}
glReadPixels讀取的內容上下翻轉處理還有另外一種實現,
原理都是一樣的,實現起來大同小異
if (shouldTakePic) {//預覽尺寸int w = 1080;int h = 1440;//預覽數據保存成照片的目錄String savePath = Environment.getExternalStorageDirectory().getPath() + "/DCIM/MyCamera/";int b[] = new int[(int) (w * h)];int bt[] = new int[(int) (w * h)];IntBuffer buffer = IntBuffer.wrap(b);buffer.position(0);//(0,580)距離屏幕左下角的距離,與glViewport(0, 580,...)保持一致glReadPixels(0, 580, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer);for (int i = 0; i < h; i++) {for (int j = 0; j < w; j++) {int pix = b[i * w + j];int pb = (pix >> 16) & 0xff;int pr = (pix << 16) & 0x00ff0000;int pix1 = (pix & 0xff00ff00) | pr | pb;bt[(h - i - 1) * w + j] = pix1;}}Bitmap inBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);inBitmap.copyPixelsFromBuffer(buffer);inBitmap = Bitmap.createBitmap(bt, w, h, Bitmap.Config.ARGB_8888);ByteArrayOutputStream bos = new ByteArrayOutputStream();inBitmap.compress(Bitmap.CompressFormat.JPEG, 90, bos);byte[] bitmapData = bos.toByteArray();ByteArrayInputStream fis = new ByteArrayInputStream(bitmapData);String tempPicFile = "temp_" + System.currentTimeMillis() + ".jpeg";File tempDir = new File(savePath);tempDir.mkdirs();try {File tmpFile = new File(tempDir, tempPicFile);FileOutputStream fos = new FileOutputStream(tmpFile);byte[] buf = new byte[1024];int len;while ((len = fis.read(buf)) > 0) {fos.write(buf, 0, len);}fis.close();fos.close();inBitmap.recycle();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}
}
驗證下效果,抓兩張預覽照試試:
抓一張普通預覽:
抓一張四宮格濾鏡預覽:
?