安卓中有个类是用来检测人脸的api,能实现一些基本功能:FaceDetector,该api在Android1.0的时候就有了,最近工作的业务中需要用到人脸检测的功能,刚开始我以为简单的调取api检测一下人脸,如果检测出来了就执行后面的业务流程,后来实际开发中遇到的坑一个比一个多,一个比一个深,真是令人绝望。 不过后来都是把这些问题都逐一解决了,现在把所遇到的问题全部分享出来吧,以免后人再踩一遍。
首先说一下业务需求,公司的后台有一个人脸识别的接口,从前端上传图片给后台,然后后台将识别结果返回给前端展示。由于后台能承担的压力不是很大,所以需要前端对图片进行过滤,,当前端检测到一个稳定的人脸图片,没有虚影的时候,才进行上传给后台,安卓端的需求就是通过摄像头来检测人脸,并且人脸的位置稳定在一个地方几秒后,判断为有效图片,这时就可以进行上传图片了。
经过多次摸索,终于将功能实现了,现在将经历过的坑逐一分享,这里不涉及到业务以及功能实现。
基础坑:基础坑应该算是一些开发过程中遇到的小问题吧,安卓端打开摄像头分Camera和Camera2,均位于android.hardware下,Android 5.0(SDK 21)以下用的是Camera,5.0以上包括5.0用的是Camera2,开发过程中需要对不同的api level进行适配,关于相机的开发这里不做赘述,随便搜索一下是有很多相应的资料,另外别忘了先检查有没有相应的相机权限,然后再进行相机操作。相机也是可能打开失败的,相机被占用的时候是不能被其他应用使用,另外因为开发的平台是平板硬件rk3288,然后在上面接入usb摄像头,除此之外没有其他摄像头了,所以开启的时候要先判断是否有摄像头,然后再进行后续操作。预览会有旋转角度的问题,假如看到的预览方向不对,那就应该设置一下预览方向,Camera.setDisplayOrientaion(int),参数只能取0,90,180,270其中之一,默认为0。也有可能会遇到左右翻转的问题,左右翻转的问题是因为前置摄像头和后置摄像头引起的,如果需要左右翻转可通过bitmap的左右翻转功能,代码如下
Bitmap bitmap = null;
Matrix matrix = new Matrix();
//镜像水平翻转
matrix.postScale(-1, 1);
Bitmap.createBitmap( bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,false );
这里会用到bitmap的原因是因为预览照片是圆形的,预览是通过接受预览数据,转为bitmap,左右翻转,然后将bitmap裁成圆形显示在imageview上。实际需要实现的界面如下,目前就只想到通过bitmap来展示此效果,在这里顺便说一下,通过自定义一个surfaceview,重写ondraw方法也可以实现,代码如下:
@Override
public void draw(Canvas canvas) {
Log.e("onDraw", "draw: test");
Path path = new Path();
//设置裁剪的圆心,半径
path.addCircle(height / 2, height / 2, height / 2, Path.Direction.CCW);
//裁剪画布,并设置其填充方式
canvas.clipPath(path, Region.Op.REPLACE);
super.draw(canvas);
}
这里我没用此方法是因为我还要用bitmap进行人脸检测,保存成文件上传给后台,调试界面等,所以用了第一种方法。
中级坑:在上面的业务中因为对bitmap做了比较多的操作,而且最后预览出来的效果是十分卡顿的,掉帧十分严重,所以后面将人脸翻转以及检测人脸的功能搬到子线程去了,刚开始是在预览回调里,将数据存放在一个列表里,然后子线程从头开始读取一张图片,进行人脸检测,检测完成之后就将其从列表移除,我还自以为聪明的用LinkList来进行存储,最后发现这是不可行的,因为每次进行人脸检测需要一定的时间,虽然说是在子线程,但是如果任务耗时过长,会导致后续任务堆积越来越多,其现象就类似于用tcp来实现视频通话吧,其结果就是任务迟滞会越来越严重,导致与需求不符。发现此问题后,改进了下,就是将预览的bitmap还是放在一个List里面,然后每次取最后一个进行人脸检测,并且将检测信息存储起来,作为业务的处理。
上面一个方法一开始以为是可以用全局变量实现的,但是后来发现我错了,因为随着时间的推移,出现了oom的问题,bitmap虽然是内存大户,但是java不是自动垃圾回收的么,没用的全局变量所占用的内存不是会自动释放么?经过多次查看内存占用情况,debug,发现我对bitmap的了解仅仅只是会拿来用用而已,因为bitmap源码里面根本就没有多少内容,而主要的实现都是用在native里面,而c或者c++实现的是需要手动进行回收的,所以需要调用bitmap.recycle()方法,查看该源码,的确会调用到native的一个方法来释放其内存。所以我最后还是选择用一个list来存储bitmap对象,新建一个线程专门用来回收内存的,当确认不再使用该对象的时候就调用recycle()方法并且移出list,不再维护.
另外oom还有一个原因,那就是FaceDetector的新建也会占用很多内存的,我
在子线程就new了很多该对象,同样会导致oom,不过此问题比较好解决,就是直接变为全局变量就行了,该对象释放native是在finalize()方法中,FaceDetector释放的相关源码如下:
/* no user serviceable parts here ... */
@Override
protected void finalize() throws Throwable {
fft_destroy();
}
native private void fft_destroy();
可见,该类也是需要释放native的内存的.
坑中坑:在上面的解决方案中,因为用到了回收线程,所以多线程间的冲突必不可少,尤其是在回收之后,经常导致对象被回收了,用不了,或者是空指针异常,此时就需要做一些线程间同步的工作了,这里也是各种头疼,因为摄像头预览的时候会不断产生数据进来,线程间同步又不能让回收线程等待太久,如果太久了,内存会马上满掉,oom再次出现,所以同步的问题也是做了很多工作。另外回收线程我一开始也是不断轮询的,这样其实并不是最好的,因为回收一些内存是很快的,但是产生一张预览图片确实每秒25张左右,很多情况下,并没有回收任务,会导致白白占用CPU时间,所以可以将该线程sleep一下。
以上就是对此次开发功能的总结,或许我采用上述的办法来实现需求不是最好的一种,鉴于业务的一些实际处理以及个人水平的限制,暂时觉得以上应该是一个可行并且不会出现太多bug的方案,如果有更好的实现或者解决问题的方法,欢迎留言交流。