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其实就是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(); } }
验证下效果,抓两张预览照试试:
抓一张普通预览:
抓一张四宫格滤镜预览: