人脸识别在Android端的实现

整个八月,一直忙于校招的笔试、面试,以及研究生课题。这个人脸识别APP,是我在不多的空闲时间中参与的一个小项目。现总结如下:

项目介绍

  • 人脸识别算法已封装成库,供客户端使用;
  • 客户端实现了动态人脸识别、静态人脸比对、人脸管理等功能。

开发环境

  • Mac OS X 10.11.4
  • Android Studio 2.1.2
  • Huawei Honor 6 / Android 6.0

APP效果图

所谓无图无真相,所以先放几张APP的实际效果图压阵;代码随后就到。

face-recognition

manifest配置

1
2
3
4
5
6
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.front" />
<uses-feature android:name="android.hardware.camera.autofocus" />

Camera+SurfaceView实现摄像预览

继承SurfaceView的CameraPreview

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
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {

private SurfaceHolder mHolder;
private Camera mCamera;

@SuppressWarnings("deprecation")
public CameraPreview(Context context, Camera camera) {
super(context);
mCamera = camera;

mHolder = getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}

public void surfaceCreated(SurfaceHolder holder) {
try {
mCamera.setPreviewDisplay(holder);
mCamera.setDisplayOrientation(90);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}

public void surfaceDestroyed(SurfaceHolder holder) {
mCamera.release();
}

public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mCamera.setDisplayOrientation(90);

if (mHolder.getSurface() == null) {
return;
}

try {
mCamera.stopPreview();
} catch (Exception e) {
e.printStackTrace();
}

try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
}

DetectActivity用于显示摄像头画面

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
public class DetectActivity extends Activity {

private FrameLayout previewLayout;
private Camera mCamera;
private CameraPreview mPreview;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detect);

if (!checkCamera()) {
finish();
}

initView();
initList();

try {
mCamera = openFrontCamera();
} catch (Exception e) {
e.printStackTrace();
}

mPreview = new CameraPreview(this, mCamera);
previewLayout.addView(mPreview);
}

private void initView() {
previewLayout = (FrameLayout) findViewById(R.id.camera_preview);
}

private Camera openFrontCamera() {
int cameraCount = 0;
Camera cam = null;

Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
cameraCount = Camera.getNumberOfCameras(); // get cameras number

for (int camIdx = 0; camIdx < cameraCount; camIdx++) {
Camera.getCameraInfo(camIdx, cameraInfo); // get camerainfo
// 代表摄像头的方位,目前有定义值两个分别为CAMERA_FACING_FRONT前置和CAMERA_FACING_BACK后置
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
try {
cam = Camera.open(camIdx);
} catch (RuntimeException e) {
Log.e("DetectActivity", "Camera failed to open: " + e.getLocalizedMessage());
}
}
}
return cam;
}

private boolean checkCamera() {
return DetectActivity.this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
}

@Override
public void onBackPressed() {
super.onBackPressed();
onDestroy();
}

@Override
public void onDestroy() {
super.onDestroy();
handler.removeCallbacks(runnable);
}

}

DetectActivity的布局文件activity_detect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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">


<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="match_parent"
android:layout_height="match_parent">


</FrameLayout>

</FrameLayout>

动态人脸检测的实现

获取实时图片流

动态人脸检测的前提是获取到实时的图片流,可通过如下的方式实现:

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
private void takeSnapPhoto() {
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Camera.Parameters parameters = camera.getParameters();
int width = parameters.getPreviewSize().width;
int height = parameters.getPreviewSize().height;

Bitmap baseBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Allocation bmData = renderScriptNV21ToRGBA888(DetectActivity.this, width, height, data);
bmData.copyTo(baseBitmap);

Matrix matrix = new Matrix();
switch (currentCameraType) {
case FRONT://前
matrix.preRotate(DIGREE_270);
break;
case BACK://后
matrix.preRotate(DIGREE_90);
break;
}
globalBitmap = Bitmap.createBitmap(baseBitmap, 0, 0, width, height, matrix, true);
}
});
}

public Allocation renderScriptNV21ToRGBA888(Context context, int width, int height, byte[] nv21) {
RenderScript rs = RenderScript.create(context);
ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));

Type.Builder yuvType = new Type.Builder(rs, Element.U8(rs)).setX(nv21.length);
Allocation in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);

Type.Builder rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height);
Allocation out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);

in.copyFrom(nv21);

yuvToRgbIntrinsic.setInput(in);
yuvToRgbIntrinsic.forEach(out);
return out;
}

Handler+Runnable实现定时检测与刷新操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
handler = new Handler();
runnable = new Runnable() {
@Override
public void run() {
//获取相机预览图
takeSnapPhoto();
if (globalBitmap != null) {
//人脸检测
//get rgb data buffer from bitmap
byte[] rgbData = Util.getRGBByBitmap(globalBitmap);
//detect face return face rect(x,y,width,height) in picture
int[] ret = application.detectFace(rgbData, globalBitmap.getWidth(), globalBitmap.getHeight());
//人脸标记
int face_size = ret[0];
if (face_size > 0) {
//相似度计算
//更新界面
}
}
handler.postDelayed(this, 500);
}
};
handler.postDelayed(runnable, 500);

人脸标记

通过上面的步骤,已经实现了人脸检测与刷新页面的功能;通过如下方式,用矩形框将人脸标记出来,并可以实时刷新标记状态。

自定义DrawImageView用于标记人脸

实现原理:

  1. 通过postInvalidate定时刷新页面,在onDraw中利用Canvas绘制矩形框。
  2. 通过setParam方法,将DetectActivity中计算出的人脸参数传递到DrawImageView中。
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
public class DrawImageView extends ImageView {

private Paint paint;
private int left, top, right, bottom = 0;

public DrawImageView(Context context, AttributeSet attrs) {
super(context, attrs);

paint = new Paint();
paint.setAntiAlias(true); // 反锯齿
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3.5f);//设置线宽
paint.setAlpha(100);

new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
Thread.sleep(500);
postInvalidate();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}

public void setParam(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect rect = new Rect(left, top, right, bottom);
canvas.drawRect(rect, paint);//绘制矩形
}

}

从DetectActivity向DrawImageView传递人脸参数

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
drawImageView = new DrawImageView(DetectActivity.this, null);
drawImageView.setParam(1, 1, 10, 10);
previewLayout.addView(drawImageView);

handler = new Handler();
runnable = new Runnable() {
@Override
public void run() {
//获取相机预览图
takeSnapPhoto();
if (globalBitmap != null) {
//人脸检测
//get rgb data buffer from bitmap
byte[] rgbData = Util.getRGBByBitmap(globalBitmap);
//detect face return face rect(x,y,width,height) in picture
int[] ret = application.detectFace(rgbData, globalBitmap.getWidth(), globalBitmap.getHeight());
//人脸标记
int face_size = ret[0];
if (face_size > 0) {
int ret_x = ret[1];
int ret_y = ret[2];
int ret_width = ret[3];
int ret_height = ret[4];
drawImageView.setParam(detectRetLeft(ret_x), detectRetTop(ret_y), detectRetRight(ret_x, ret_width), detectRetBottom(ret_y, ret_height));
}
}
handler.postDelayed(this, 500);
counter++;
}
};
handler.postDelayed(runnable, 500);

人脸匹配

从Android本地数据库SQLite中读取所有人脸数据,分别计算与当前检测到的人脸的特征相似度;并匹配出最佳结果。

读取数据库中的人脸数据

1
2
dbManager = new DBManager(this);
persons = dbManager.query();

人脸特征数组转化

1
2
3
4
5
6
7
8
private float[] StringToFloatArray(String feaStr) {
String[] strArray = feaStr.split(",");
float[] floatArray = new float[strArray.length];
for (int i = 0; i < strArray.length; i++) {
floatArray[i] = Float.parseFloat(strArray[i]);
}
return floatArray;
}

计算人脸相似度

1
2
3
4
5
6
7
8
9
10
11
private void similarPerson(float[] feature) {
for (int i = 0; i < persons.size(); i++) {
Person person = persons.get(i);
float score = application.compare2Feature(feature, StringToFloatArray(person.feature));
if (score >= 0.5 && score > globalMaxScore) {
globalMaxScore = score;
globalMaxName = person.name;
globalMaxImage = person.image;
}
}
}

切换摄像头

人脸识别的时候,会有自拍(前置摄像头)和拍摄他人(后置摄像头)的需求。

定义导航栏菜单点击事件(切换摄像头)

detect.xml

1
2
3
4
5
6
7
8
9
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<item
android:id="@+id/camera_switch"
android:orderInCategory="100"
android:showAsAction="always"
android:title="@string/camera_switch" />
</menu>

DetectActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.detect, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.camera_switch:
//切换摄像头
Toast.makeText(DetectActivity.this, R.string.camera_switch, Toast.LENGTH_SHORT).show();
try {
changeCamera();
} catch (IOException e) {
e.printStackTrace();
}
break;
default:
break;
}
return true;
}

实现前后摄像头的切换操作

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
private void changeCamera() throws IOException {
mCamera.stopPreview();
mCamera.release();
mCamera = null;

if (currentCameraType == FRONT) {
mCamera = openCamera(BACK);
} else if (currentCameraType == BACK) {
mCamera = openCamera(FRONT);
}
try {
mCamera.setPreviewDisplay(mPreview.getHolder());
mCamera.setDisplayOrientation(getPreviewDegree(DetectActivity.this));
} catch (IOException e) {
e.printStackTrace();
}
Camera.Parameters parameters = mCamera.getParameters(); // 获取各项参数
parameters.setPictureFormat(PixelFormat.JPEG); // 设置图片格式
parameters.setJpegQuality(50); // 设置照片质量
mCamera.startPreview();
}

private Camera openCamera(int type) {
int frontIndex = -1;
int backIndex = -1;
int cameraCount = Camera.getNumberOfCameras();
Camera.CameraInfo info = new Camera.CameraInfo();
for (int cameraIndex = 0; cameraIndex < cameraCount; cameraIndex++) {
Camera.getCameraInfo(cameraIndex, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
frontIndex = cameraIndex;
} else if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
backIndex = cameraIndex;
}
}

currentCameraType = type;
if (type == FRONT && frontIndex != -1) {
return Camera.open(frontIndex);
} else if (type == BACK && backIndex != -1) {
return Camera.open(backIndex);
}
return null;
}

//用于根据手机方向获得相机预览画面旋转的角度
public int getPreviewDegree(Activity activity) {
// 获得手机的方向
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int degree = 0;
// 根据手机的方向计算相机预览画面应该选择的角度
switch (rotation) {
case Surface.ROTATION_0:
degree = 90;
break;
case Surface.ROTATION_90:
degree = 0;
break;
case Surface.ROTATION_180:
degree = 270;
break;
case Surface.ROTATION_270:
degree = 180;
break;
}
return degree;
}

人脸管理功能的实现

1.DBHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "face.db";
private static final int DATABASE_VERSION = 1;

public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS person" +
"(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR, image BLOB, feature VARCHAR)");
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("ALTER TABLE person ADD COLUMN other STRING");
}
}

2.DBManager

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
public class DBManager {

private DBHelper helper;
private SQLiteDatabase db;

public DBManager(Context context) {
helper = new DBHelper(context);
db = helper.getWritableDatabase();
}

public void addPersons(List<Person> persons) {
db.beginTransaction(); //开始事务
try {
for (Person person : persons) {
db.execSQL("INSERT INTO person VALUES(null, ?, ?, ?)", new Object[]{person.name, person.image, person.feature});
}
Log.i(DBMANAGER, "批量添加多条记录");
db.setTransactionSuccessful(); //设置事务成功完成
} finally {
db.endTransaction(); //结束事务
}
}

public void addPerson(Person person) {
db.execSQL("INSERT INTO person VALUES(null, ?, ?, ?)", new Object[]{person.name, person.image, person.feature});
}

public void deletePerson(Person person) {
db.delete("person", "id=?", new String[]{String.valueOf(person.id)});
}

public List<Person> query() {
ArrayList<Person> persons = new ArrayList<Person>();
Cursor c = queryTheCursor();
while (c.moveToNext()) {
Person person = new Person();
person.id = c.getInt(c.getColumnIndex("id"));
person.name = c.getString(c.getColumnIndex("name"));
person.image = c.getBlob(c.getColumnIndex("image"));
person.feature = c.getString(c.getColumnIndex("feature"));
persons.add(person);
}
c.close();
return persons;
}

public Cursor queryTheCursor() {
Cursor c = db.rawQuery("SELECT * FROM person", null);
return c;
}

public void closeDB() {
db.close();
}
}

导航栏后退按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private ActionBar actionBar;

actionBar.setDisplayHomeAsUpEnabled(true);
actionBar = getActionBar();

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
break;
default:
break;
}
return true;
}

参考资料

八宝粥 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!