最近公司的项目需要用到人脸检测的功能,我花了一些时间整理了一下,以此作为记录,这也是我的第一篇博文,有什么不正确的地方,希望大家指正

介绍

引用百度知道人脸识别词条中的介绍
人脸识别技术是基于人的脸部特征,对输入的人脸图像或者视频流 . 首先判断其是否存在人脸 , 如果存在人脸,则进一步的给出每个脸的位置、大小和各个主要面部器官的位置信息。并依据这些信息,进一步提取每个人脸中所蕴涵的身份特征,并将其与已知的人脸进行对比,从而识别每个人脸的身份。

人脸识别在Android平台的应用已经不是什么新鲜事了,从最初的4.0系统的人脸解锁屏幕,现在各个相机应用动画贴图效果,都是依靠人脸识别来实现的。Android中人脸相关的API,在Level 1的时候就已经存在,但是他能做到的也只是人脸的检测,而非通常上讲的人脸识别。接下来就来实现一个人脸检测的应用。

流程

首先用Camera来预览图像,那么就需要用到SurfaceView来进行显示,接下来就有两种方式来进行检测人脸

  1. 直接为Camera添加人脸检测的监听器,这种方式是在系统底层实现的,我们只要负责回调就可以了。
  2. 我们自己处理图像。拿到预览的每一帧图像,然后在调用人脸检测API来找到人脸。

最后我们要将人脸的位置信息绘制到屏幕上去,又需要用到SurfaceView

实现

由于SurfaceViewCanvas在绘制时锁定的,我们不能在已经预览CameraSurfaceView上进行绘制,所以就要用到两个重叠的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);
//隐藏ToolBar
getSupportActionBar().hide();

//为了不产生相机拉伸,在这里把整个布局的款宽高比设置为4:3
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();
// 这两个方法都能让这个SurfaceView处于上层。源码上说这个两个方法会互相覆盖
// mBeforeView.setZOrderMediaOverlay(true);
mBeforeView.setZOrderOnTop(true);
// 让它背景透明,以便显示下面的内容
mBeforeHolder.setFormat(PixelFormat.TRANSPARENT);

mAfterHolder.addCallback(this);
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
mCamera = Camera.open();//可以根据ID使用不同的摄像头
try {
mCamera.setPreviewDisplay(holder);
} catch (IOException e) {
e.printStackTrace();
}
Camera.CameraInfo info = new Camera.CameraInfo();
//获得默认的相机的相关信息,这里主要是拿到系统适配的相机旋转角度。但是这并不一定准确
//http://dev.qq.com/topic/583ba1df25d735cd2797004d
//https://www.qcloud.com/community/article/168
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();//Camera是系统资源,不用了要释放
}

}

运行效果如下:

官方实现的人脸检测

一般来讲,如果设备是手机,那么都会支持人脸检测功能,但是还是有个别手机或者其他的Android定制系统不支持这个功能。如何验证系统是否支持这个功能,可以通过以下代码来判断:

1
2
3
4
5
6
7
8
9
//获取到最大人脸检测数,如果是0,那么就是不支持
int maxNumDetectedFaces = mCamera.getParameters().getMaxNumDetectedFaces()
if (maxNumDetectedFaces > 0) {
mCamera.setFaceDetectionListener(this);
mCamera.startPreview();
mCamera.startFaceDetection();
return;
}

这里说一下人脸检测开启关闭的时机,开启时必须在开启预览之后,关闭时必须在关闭预览之前。否则会抛出异常

拿到Face以后,就可以开始进行绘制了。然而并没这么简单。通过Debug发现,除了rect参数有值以外,其他的几个关键参数却为null,而且rect坐标也有点诡异,居然有负数,这就无法直接拿来用了。

通过多种各种测试和Google,得出两个结论:

  1. 由于摄像头的安装方向的问题(一般都是横屏安装的),预览画面都是通过旋转后的画面,而用于人脸检测的图像是没有经过旋转的。
  2. 坐标系的不同,这里得到的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
/** 该方法出自
* http://blog.csdn.net/yanzi1225627/article/details/38098729/
* http://bytefish.de/blog/face_detection_with_android/
* @param matrix 这个就不用说了
* @param mirror 是否需要翻转,后置摄像头(手机背面)不需要翻转,前置摄像头需要翻转。
* @param displayOrientation 旋转的角度
* @param viewWidth 预览View的宽高
* @param viewHeight
*/
public void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation,
int viewWidth, int viewHeight) {
// Need mirror for front camera.
matrix.setScale(mirror ? -1 : 1, 1);
// This is the value for android.hardware.Camera.setDisplayOrientation.
matrix.postRotate(displayOrientation);
// Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
// UI coordinates range from (0, 0) to (width, height)
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();//锁定Surface 并拿到Canvas
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);

// canvas.save();
// 由于有的时候手机会存在一定的偏移(歪着拿手机)所以在这里需要旋转Canvas 和 matrix,
// 偏移值从OrientationEventListener获得,具体Google
// canvas.rotate(-degrees); 默认是逆时针旋转
// matrix.postRotate(degrees);默认是顺时针旋转

for (int i = 0; i < faces.length; i++) {
RectF rect = new RectF(faces[i].rect);
matrix.mapRect(rect);//应用到rect上
canvas.drawRect(rect,mPaint);
}
mBeforeHolder.unlockCanvasAndPost(canvas);//更新Canvas并解锁

// canvas.restore();
}

效果如下:

以上就是Camera自己实现的人脸检测。但是有的设备没有这个功能,那么就必须要我们自己来实现整个过程,这就要用到Google官方为我们提供了一个FaceDetector类。

自己动手实现的人脸检测

FaceDetector类是在Android level 1 就已经添加到Android系统,它能够在Bitmap中检测出人脸,并标记处相对Bitmap坐标的两眼之间的中心点。通过这个点,就可以进行各种操作了。那么如何获得Bitmap呢? 这个可以为Camera添加监听器获取到预览的每一帧的数据,再进行转换就能得到。首先要做的就是为Camera添加PreviewCallback:

1
2
3
4
5
6
7
8
9
//        以下方法都是添加预览监听,内存占用上有区别,具体Google
// mCamera.setOneShotPreviewCallback(this);
// mCamera.setPreviewCallbackWithBuffer(this);
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;//必须设置为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);//5 代表人脸最大检测数

FaceDetector.Face[] faces = new FaceDetector.Face[5];
detector.findFaces(bitmap, faces);
for (FaceDetector.Face face : faces) {
//获取一个指定范围的canvas,减少绘制的范围
//mBeforeHolder.lockCanvas(new Rect());
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);

//由于这个坐标是进行缩放后的坐标,所以必须计算出正确的坐标
//如果是前置摄像头,那么这里就需要计算翻转后的X坐标,
//pointF.x = mViewWidth - mViewWidth * (pointF.x / bitmap.getWidth());
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);

参考这里