前言
闲谈
因为最近公司在做有关摄像头的项目(人脸识别、皮肤测试)。涉及到了usb摄像头和原生的摄像头,我们usb摄像头用的UVC的库来预览的,其实用Camera来预览也是可以的。开发期间查阅了一些资料,也走了很多弯路。所以现在项目上线了,写了这篇文章,希望能够帮到在Camera迷茫的小伙伴们……
Camera和Camera2
Android5.0以前,相机框架是Camera,Android5.0以后Google 引入了一套全新的相机框架 Camera2,相比之下,它更具灵活性,也增加了新的功能,如设置对焦模式、曝光模式、快门等。不过,很多文章里介绍,国内厂商对系统的定制导致对Camera2的支持不尽相同,但是我觉得还是值得一试的,下篇文章我也去探索一下Camera2(所以呢,本文总结的是Camera)。
选择Camera还是Camera2除了跟手机硬件的关系外,还有个需求是是否要适配5.0以下。Camera2是支持5.0+的。
SurfaceView
Surface
我们先看一下Surface
1 | /** |
Surface是用来处理屏幕显示内容合成器所管理的原始缓冲区的。
原始缓冲区用来保存窗口的像素数据。
简单的说Surface对应了一块屏幕缓冲区,每个window对应一个Surface,任何View都要画在Surface的Canvas上。传统的view共享一块屏幕缓冲区。
可以认为Surface用来管理数据的。
SurfaceView
1 | /** |
SurfaceView提供了嵌入视图层级中的专用surface。你可以控制surface的格式或大小。SurfaceView负责把surface显示在屏幕的正确位置。
SurfaceView是一个View,我们再实际开发中是在布局文件里写的,但是又比较特殊。原因有二:
- 普通的View都是共享一个屏幕缓冲区的(Surface),SurfaceView有单独的Surface。
- 普通的View只能在UI线程更新,SurfaceView没有限制。
SurfaceHolder
SurfaceHolder是个接口,具体的实现在SurfaceView里。实现了很多方法,比如添加回调的方法addCallback(Callback callback)、得到Surface的画布的方法lockCanvas()等等。大概可以说SurfaceHolder用来控制Surface的尺寸、格式与像素以及监听Surface的状态的。
监听Surface的状态回调在SurfaceHolder.Callback里。我们需要在自己去监听这几个回调,实现具体的业务逻辑。
surfaceCreated(SurfaceHolder holder);
当Surface被创建后调用
surfaceChanged(SurfaceHolder holder, int format, int width, int height);
当Surface的size、format等发生变化的时候调用
surfaceDestroyed(SurfaceHolder holder);
当Surface被销毁的时候调用
权限
1 | <uses-permission android:name="android.permission.CAMERA" /> |
Android6.0+需要动态申请权限,这里不多说了。
Camera的实践
SurfaceView
1 | public class CameraFragment extends BaseFragment implements SurfaceHolder.Callback { |
打开与预览
1 | /** |
没错,现在我们已经成功打开前置摄像头并预览到画面了,因为我们没有设置Camera的参数配置,所以现在画面方向、比例都有问题。这个先不着急,这里有几个问题需要谈一下:
Camera.open(index)
我总有种错觉,觉得打开前置就open(1),打开后置就open(0)。其实不是这样,index指的是numCameras中的第几个。比如魔镜系统只有一个前置摄像头(numCameras为1),打开的话还是open(0)。
预览摄像头应该在Surface创建之后。
setPreviewDisplay应该在startPreview之前。
一定记得要release释放资源。
设置一些参数
1 | private void setupCameraParameters() { |
调整预览的方向
图像的Sensor方向:手机Camera的图像数据都是来自于摄像头硬件的图像传感器(Image Sensor),这个Sensor被固定到手机之后是有一个默认的取景方向的,也就是手机横放Home键朝右这个方向,坐标原点位于手机横放时的左上角。
Camera的预览方向:预览方向默认情况下与图像的Sensor方向一致,所以我们看到的画面方向不正常。Google提供了setDisplayOrientation来让我们调整预览的方向。需要注意的是setDisplayOrientation只改变预览的方向,不改变拍照后图片的方向、回调的数据流方向。
Camera的拍照方向:与图像的Sensor方向一致。
1 | /** |
拍照
1 | camera.takePicture(null, null, new Camera.PictureCallback() { |
拍照可以使用takePicture方法,拍照完要重新预览,拍照时有瞬间画面停顿。如果想不断获取图片,这个方法显然是不可取的,可以在数据流回调的方法里生成bitmap。
1 | //视频流的回调 |
可以发现生成的图片方向还是不正确的,因为照片保存的方向还是由Camera的图像Sensor决定的,我们需要进行调整。可以拍完照片后进行旋转处理,也可以使用setRotation来设置角度。
1 | private void setPictureOrientation() { |
这里值得提一下的是,前置摄像头拍的照片可能和预览的左右不一致,也就是镜像。这个好像是正常的,但是根据习惯来说可能感觉怪怪的吧。所以手机自带相机一般有个设置让我们自己决定到底拍照的图片什么样子。
那么自定义相机怎么办?如果我们就是不想要镜像效果呢?那只能对生成的图片修改了。
1 | Matrix matrix = new Matrix(); |
预览尺寸
1 | parameters.setPreviewSize(x, y); |
我们可以用上述方法设置预览尺寸,但是参数不能随便写,如果Camera不支持我们设置的参数恐怕就要崩溃了……而又为了不使预览拉伸变形,我们应该找出Camera支持的预览尺寸里最接近我们SurfaceView宽高比例的。
1 | public final class CameraPreviewUtils { |
人脸识别
这个小节本来不打算写的,因为我也没打算展开写。但是还是谈几句吧…..原生的api提供了人脸识别的方法。
一种是在Camera开启人脸识别,直接回调结果。但是要注意startFaceDetection()需要在startPreview()之后。
1 | private void faceDetch() { |
另一种则是直接检测Bitmap。所以可以直接检测本地图片,也可以将Camera回调中onPreviewFrame(byte[] data, Camera camera)的data转为bitmap来检测。
1 | FaceDetector faceDetector = new FaceDetector(bm.getWidth(), bm.getHeight(), MAX_FACE_NUM); |
不管是原生api的哪种方法,看起来效果并不是特别的好,要是商用的话恐怕不能满足。所以,可能要使用诸如虹软、百度、中科等的sdk了(绝对不是打广告!)。具体不多少了,只是基本上都是涉及到了onPreviewFrame(byte[] data, Camera camera)这个回调中的data数据。
释放
1 | private void stopPreview() { |
Camera与子线程
我们知道,Camera的打开是耗时的,我们可以放在子线程里。那么我们既然要在onPreviewFrame(byte[] data, Camera camera)回调中进行人脸识别比对等等,还要自己再开子线程,我们想这个回调就是子线程回调的要怎么办?
假如我们在子线程打开的Camera,那么onPreviewFrame(byte[] data, Camera camera)就会在子线程?恐怕想得美。
1 | public class HandlerThreadActivity2 extends Activity implements Callback { |
Camera维护了一个EventHandler。 Handler.handleMessage的执行一定在它的Looper所在线程中。 onPreviewFrame的执行在Camera所持有的Looper所在线程中执行。
结束语
- 参考文章
Android开发实践:掌握Camera的预览方向和拍照方向
- 文章同步发布在黑白了落夜