最近公司的项目需要用到人脸检测的功能,我花了一些时间整理了一下,以此作为记录,这也是我的第一篇博文,有什么不正确的地方,希望大家指正
介绍
引用百度知道人脸识别词条中的介绍
人脸识别技术是基于人的脸部特征,对输入的人脸图像或者视频流 . 首先判断其是否存在人脸 , 如果存在人脸,则进一步的给出每个脸的位置、大小和各个主要面部器官的位置信息。并依据这些信息,进一步提取每个人脸中所蕴涵的身份特征,并将其与已知的人脸进行对比,从而识别每个人脸的身份。
人脸识别在Android平台的应用已经不是什么新鲜事了,从最初的4.0系统的人脸解锁屏幕,现在各个相机应用动画贴图效果,都是依靠人脸识别来实现的。Android中人脸相关的API,在Level 1的时候就已经存在,但是他能做到的也只是人脸的检测,而非通常上讲的人脸识别。接下来就来实现一个人脸检测的应用。
流程
首先用Camera来预览图像,那么就需要用到SurfaceView来进行显示,接下来就有两种方式来进行检测人脸
- 直接为Camera添加人脸检测的监听器,这种方式是在系统底层实现的,我们只要负责回调就可以了。
- 我们自己处理图像。拿到预览的每一帧图像,然后在调用人脸检测API来找到人脸。
最后我们要将人脸的位置信息绘制到屏幕上去,又需要用到SurfaceView。
实现
由于SurfaceView的Canvas在绘制时锁定的,我们不能在已经预览Camera的SurfaceView上进行绘制,所以就要用到两个重叠的SurfaceView
布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.ycxu.facedemo.MainActivity">
<SurfaceView android:id="@+id/after_view" android:layout_width="match_parent" android:layout_height="match_parent" />
<SurfaceView android:id="@+id/before_view" android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>
|
Camera如何进行预览以及在预览过程中所遇到的问题(比如预览画面的拉伸),在这里就不做叙述,Google和百度上面有很多这方面的内容。**(此处有个巨大的Bug,会导致无法获取正确的图像,具体跳到最后)
**代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback { private Camera mCamera; private SurfaceView mAfterView, mBeforeView; private SurfaceHolder mAfterHolder, mBeforeHolder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION ); setContentView(R.layout.activity_main); getSupportActionBar().hide(); Display defaultDisplay = getWindow().getWindowManager().getDefaultDisplay(); int width = defaultDisplay.getWidth(); int height = width/3*4; findViewById(R.id.container).setLayoutParams(new FrameLayout.LayoutParams(width,height));
mAfterView = (SurfaceView) findViewById(R.id.after_view); mBeforeView = (SurfaceView) findViewById(R.id.before_view);
mAfterHolder = mAfterView.getHolder(); mBeforeHolder = mBeforeView.getHolder();
mBeforeView.setZOrderOnTop(true);
mBeforeHolder.setFormat(PixelFormat.TRANSPARENT);
mAfterHolder.addCallback(this); }
@Override public void surfaceCreated(SurfaceHolder holder) { mCamera = Camera.open(); try { mCamera.setPreviewDisplay(holder); } catch (IOException e) { e.printStackTrace(); } Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(0, info); mCamera.setDisplayOrientation(info.orientation);
mCamera.startPreview(); }
@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Camera.Parameters parameters = mCamera.getParameters(); List<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes(); Camera.Size size = previewSizes.get(previewSizes.size() - 1); parameters.setPreviewSize(size.width, size.height); mCamera.setParameters(parameters); }
@Override public void surfaceDestroyed(SurfaceHolder holder) { mCamera.stopPreview(); mCamera.release(); }
}
|
运行效果如下:

官方实现的人脸检测
一般来讲,如果设备是手机,那么都会支持人脸检测功能,但是还是有个别手机或者其他的Android定制系统不支持这个功能。如何验证系统是否支持这个功能,可以通过以下代码来判断:
1 2 3 4 5 6 7 8 9
| int maxNumDetectedFaces = mCamera.getParameters().getMaxNumDetectedFaces() if (maxNumDetectedFaces > 0) { mCamera.setFaceDetectionListener(this); mCamera.startPreview(); mCamera.startFaceDetection(); return; }
|
这里说一下人脸检测开启关闭的时机,开启时必须在开启预览之后,关闭时必须在关闭预览之前。否则会抛出异常
拿到Face以后,就可以开始进行绘制了。然而并没这么简单。通过Debug发现,除了rect参数有值以外,其他的几个关键参数却为null,而且rect坐标也有点诡异,居然有负数,这就无法直接拿来用了。

通过多种各种测试和Google,得出两个结论:
- 由于摄像头的安装方向的问题(一般都是横屏安装的),预览画面都是通过旋转后的画面,而用于人脸检测的图像是没有经过旋转的。
- 坐标系的不同,这里得到的rect是来自Camera.Area,而他的坐标系跟屏幕坐标系完全不是一个概念。
在 Camera.Area 对象中的 Rect 属性描述一个映射 2000*2000 单元格子的正方形。坐标 -1000,-1000 代表相机图片的左上角,坐标 1000,1000 代表相机图片右下角,如下图文所示:

红线表示在相机预览中为Camera.Area指定坐标系统。蓝色方框展示使用值为333,333,667,667的Rect的相机区域位置和形状。
该坐标系的边界总是与相机预览中可见的图像的外边缘相一致,且不会随缩放级别而缩小或扩展。同样,使用 Camera.setDisplayOrientation() 旋转预览的图片,不会重新映射坐标系统。
Camera.Area 官方API
Camera 中文 API介绍
知道了原因,但是我们无法干涉他的检测过程,那么就只有通过已有的结果,逆向推算出符合屏幕坐标系的的结果。做法就是进行各种变换,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation, int viewWidth, int viewHeight) { matrix.setScale(mirror ? -1 : 1, 1); matrix.postRotate(displayOrientation); matrix.postScale(viewWidth / 2000f, viewHeight / 2000f); matrix.postTranslate(viewWidth / 2f, viewHeight / 2f); }
|
剩下的就是绘制,这点没什么说的,需要注意的都在代码中注释了。具体直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Override public void onFaceDetection(Camera.Face[] faces, Camera camera) { Canvas canvas = mBeforeHolder.lockCanvas(); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); if (faces.length < 1) { mBeforeHolder.unlockCanvasAndPost(canvas); return; } Matrix matrix = new Matrix();
prepareMatrix(matrix, false, mOrientation, mViewWidth, mViewHeight);
for (int i = 0; i < faces.length; i++) { RectF rect = new RectF(faces[i].rect); matrix.mapRect(rect); canvas.drawRect(rect,mPaint); } mBeforeHolder.unlockCanvasAndPost(canvas);
}
|
效果如下:

以上就是Camera自己实现的人脸检测。但是有的设备没有这个功能,那么就必须要我们自己来实现整个过程,这就要用到Google官方为我们提供了一个FaceDetector类。
自己动手实现的人脸检测
FaceDetector类是在Android level 1 就已经添加到Android系统,它能够在Bitmap中检测出人脸,并标记处相对Bitmap坐标的两眼之间的中心点。通过这个点,就可以进行各种操作了。那么如何获得Bitmap呢? 这个可以为Camera添加监听器获取到预览的每一帧的数据,再进行转换就能得到。首先要做的就是为Camera添加PreviewCallback:
1 2 3 4 5 6 7 8 9
|
mCamera.setPreviewCallback(new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) {
} });
|
可以看到返回的并不是一个Bitmap,接下来要做的就是解析数据为Bitmap,这要用到Yuvimage这个类。
1 2 3 4 5 6 7 8
| Camera.Size size = camera.getParameters().getPreviewSize(); YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21,size.width,size.Height,null); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0,0,size.width,size.Height),80,outputStream); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; Bitmap bitmap = BitmapFactory.decodeByteArray(outputStream.toByteArray(), 0, outputStream.toByteArray().length, options);
|
基本就是这个套路,有何疑问可以查看源码的注释。有了Bitmap就可以开始人脸检测了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| Matrix matrix = new Matrix(); matrix.postRotate(mOrientation); matrix.postScale(0.25f, 0.25f);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
Canvas canvas = mBeforeHolder.lockCanvas(); canvas.drawBitmap(bitmap, new Matrix(), mPaint); mBeforeHolder.unlockCanvasAndPost(canvas);
FaceDetector detector = new FaceDetector(bitmap.getWidth(), bitmap.getHeight(), 5);
FaceDetector.Face[] faces = new FaceDetector.Face[5]; detector.findFaces(bitmap, faces); for (FaceDetector.Face face : faces) { canvas = mBeforeHolder.lockCanvas(); if (face == null) { canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); mBeforeHolder.unlockCanvasAndPost(canvas); return; } Log.e(TAG, "run: 已检测到人脸"); PointF pointF = new PointF();
face.getMidPoint(pointF);
pointF.x = mViewWidth * (pointF.x / bitmap.getWidth()); pointF.y = mViewHeight * (pointF.y / bitmap.getHeight()); float v = face.eyesDistance();
canvas.drawRect(pointF.x - v * 2, pointF.y - v * 2, pointF.x + v * 2, pointF.y + v * 2, mPaint);
mBeforeHolder.unlockCanvasAndPost(canvas);
}
|
到这里基本就完成了。
总结
官方实现的人脸检测方式调用简单,获取到坐标后稍加转换就可以用,自己实现的方式,过程复杂,之间还会涉及到图片格式等问题。所以一般还是用官方实现的方式。
修正&补充
由于之前Camera设置的代码是新写的代码,与其他的代码不是一起完成,差点就铸成大错了。问题出在设置预览大小上面,先看问题:

左上角那块无法描述的东西是在检测时画上去的,造成这个问题的原因是使用YUVImage转换格式时,传入的宽高不正确。因此导致转换失败。但是参考其他的代码,都是通过获取Camera的预览大小传入的。那么问题是出在Camera身上,重新梳理了一下逻辑。终于发现了问题所在。
在设置Camera预览大小时为了让预览画面不产生拉伸。取巧的为布局设置3:4的宽高,但是预览大小设置的是却是手机兼容的最大分辨率。由此造成了看到的是正确的画面。而用于人脸检测的图像却是错误的。解决问题的办法很简单,只要尽量保持图像比例的一致就可以了。我这里是这么做的
1 2 3 4 5 6 7 8 9 10
| Camera.Parameters parameters = mCamera.getParameters(); List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
for (Size size : sizes) { if (size.width / 3 == size.height / 4) { parameters.setPreviewSize(size.width, size.height); break; } } mCamera.setParameters(parameters);
|
参考这里
