目录
前言
经过几天对SeetaFace2的研究,逐渐熟悉了SeetaFace2的简单使用,本着一切为了实战的原则就做了这么一个人脸登录的案例,希望自己以后可以用到也希望能够帮助有需要的朋友。
在看本篇之前希望大家先将我的前几篇文章看看这样有助于加强基础:
●Android NDK开发:SeetaFace2人脸识别算法简介
●Android NDK开发:SeetaFace2实现人脸检测
●Android NDK开发:SeetaFace2实现人脸特征点检测
●Android NDK开发:SeetaFace2实现人脸匹配
效果展示
实现原理
首先需要将人脸信息录入到SeetaFace2的人脸数据库中,然后在登录的时候将数据库中的人脸与登录时获取的人脸进行匹配,如果匹配值大于0.7的话则匹配成功。
实现步骤
●自定义相机
代码如下:
public class FaceCameraView extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback {
private Camera mCamera;//相机
private boolean isSupportAutoFocus;//是否支持自动对焦
private int screenHeight;//屏幕的高度
private int screenWidth;//屏幕的宽度
private boolean isPreviewing;//是否在预览
private PreviewCallback previewCallback;//相机预览的回调
public FaceCameraView(Context context) {
super(context);
init();
}
public FaceCameraView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FaceCameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
screenWidth = dm.heightPixels;
screenHeight = dm.widthPixels;
isSupportAutoFocus = getContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_CAMERA_AUTOFOCUS);
getHolder().addCallback(this);
getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
public void setPreviewCallback(PreviewCallback previewCallback) {
this.previewCallback = previewCallback;
}
/**
* Camera帧数据回调用
*/
@Override
public void onPreviewFrame(final byte[] data, final Camera camera) {
camera.addCallbackBuffer(data);
if(previewCallback!=null){
previewCallback.onPreview(data,camera);
}
}
/**
* 摄像头自动聚焦
*/
Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() {
public void onAutoFocus(boolean success, Camera camera) {
postDelayed(doAutoFocus, 500);
}
};
private Runnable doAutoFocus = new Runnable() {
public void run() {
if (mCamera != null) {
try {
mCamera.autoFocus(autoFocusCB);
} catch (Exception e) {
}
}
}
};
/**
* 打开指定摄像头
*/
public void openCamera() {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); cameraId++) {
Camera.getCameraInfo(cameraId, cameraInfo);
//打开前置摄像头
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
try {
mCamera = Camera.open(cameraId);
} catch (Exception e) {
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
break;
}
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
releaseCamera();
openCamera();
} catch (Exception e) {
mCamera = null;
}
}
/**
* 加载相机配置
*/
private void initCamera() {
try {
mCamera.setPreviewDisplay(getHolder());//当前控件显示相机数据
mCamera.setDisplayOrientation(90);//调整预览角度
setCameraParameters();
startPreview();//打开相机
} catch (Exception e) {
releaseCamera();
}
}
/**
* 配置相机参数
*/
private void setCameraParameters() {
Camera.Parameters parameters = mCamera.getParameters();
List sizes = parameters.getSupportedPreviewSizes();
//确定前面定义的预览宽高是camera支持的,不支持取就更大的
for (int i = 0; i < sizes.size(); i++) {
if ((sizes.get(i).width >= screenWidth && sizes.get(i).height >= screenHeight) || i == sizes.size() - 1) {
screenWidth = sizes.get(i).width;
screenHeight = sizes.get(i).height;
break;
}
}
//设置最终确定的预览大小
parameters.setPreviewSize(screenWidth, screenHeight);
mCamera.setParameters(parameters);
}
/**
* 释放相机
*/
private void releaseCamera() {
if (mCamera != null) {
stopPreview();
mCamera.setPreviewCallback(null);
mCamera.release();
mCamera = null;
}
}
/**
* 停止预览
*/
private void stopPreview() {
if (mCamera != null && isPreviewing) {
mCamera.stopPreview();
isPreviewing = false;
}
}
/**
* 开始预览
*/
public void startPreview() {
if (mCamera != null) {
mCamera.addCallbackBuffer(new byte[((screenWidth * screenHeight) * ImageFormat.getBitsPerPixel(ImageFormat.NV21)) / 8]);
mCamera.setPreviewCallbackWithBuffer(this);
mCamera.startPreview();
if (isSupportAutoFocus) {
mCamera.autoFocus(autoFocusCB);
}
isPreviewing = true;
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
stopPreview();
initCamera();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
releaseCamera();
}
//自定义接口用于传递相机的预览数据
public interface PreviewCallback {
/**
* 相机预览的回调接口
* @param data
* @param camera
*/
void onPreview(final byte[] data, final Camera camera);
}
}
●加载模型
这一步我是在效果图所示的主页面进行的,并将模型对象设置成了静态属性。
private void initModule() {
new Thread(new Runnable() {
@Override
public void run() {
FACEDETECTOR = new FaceDetector2(Environment.getExternalStorageDirectory()+ File.separator+"seetaface"+File.separator+"SeetaFaceDetector2.0.ats");
POINTDETECTOR = new PointDetector2(Environment.getExternalStorageDirectory()+ File.separator+"seetaface"+File.separator+"SeetaPointDetector2.0.pts5.ats"); //特征点
FACERECOGNIZER = new FaceRecognizer2(Environment.getExternalStorageDirectory()+ File.separator+"seetaface"+File.separator+"SeetaFaceRecognizer2.0.ats"); //人脸匹配
}
}).start();
}
●录入人脸信息
这里是在前面自定义相机的回调接口中实现的,整个过程中包括了对相机图像的角度校正,在录入成功之后利用handler传递消息给主线程给予提示信息并将Activity关闭。
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Toast.makeText(FaceInitActivity.this, "信息录入成功", Toast.LENGTH_SHORT).show();
finish();
}
};
faceInitCameraView.setPreviewCallback(new FaceCameraView.PreviewCallback() {
@Override
public void onPreview(final byte[] data, final Camera camera) {
if(FaceLoginActivity.FACEDETECTOR!=null&&FaceLoginActivity.FACERECOGNIZER!=null&&FaceLoginActivity.POINTDETECTOR!=null){
new Thread(new Runnable() {
@Override
public void run() {
//识别中不处理其他帧数据
if (!isScanning) {
isScanning = true;
try {
//获取Camera预览尺寸
Camera.Size size = camera.getParameters().getPreviewSize();
//将帧数据转为bitmap
YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
if (image != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
//纠正图像的旋转角度问题
Matrix m = new Matrix();
m.setRotate(-90, (float) bmp.getWidth() / 2, (float) bmp.getHeight() / 2);
Bitmap bm = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, true);
SeetaImageData RegistSeetaImageData = ConvertUtil.ConvertToSeetaImageData(bm);
SeetaRect[] faceRects = FaceLoginActivity.FACEDETECTOR.Detect(RegistSeetaImageData);
if(faceRects.length>0){
//获取人脸区域(这里只有一个所以取0)
SeetaRect faceRect = faceRects[0];
SeetaPointF[] seetaPoints = FaceLoginActivity.POINTDETECTOR.Detect(RegistSeetaImageData, faceRect);//根据检测到的人脸进行特征点检测
FaceLoginActivity.FACERECOGNIZER.Register(RegistSeetaImageData, seetaPoints);//将人脸注册到SeetaFace2数据库
handler.sendEmptyMessage(0);
}else {
//如果检测不到人脸给予如下提示
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(FaceInitActivity.this, "请保持手机不要晃动", Toast.LENGTH_SHORT).show();
}
});
isScanning = false;
}
}
} catch (Exception ex) {
isScanning = false;
}
}
}
}
).start();
}
}
});
●人脸匹配登录
这里的大部分代码与录入时基本一致,主要的差别就在于这里是进行人脸匹配,当匹配值大于0.7的时候提示登录成功,如果匹配5次没有成功的话则提示登录失败。
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 0:
failedCount++;
if(failedCount>=5){
Toast.makeText(LoginActivity.this, "人脸不匹配,登录失败", Toast.LENGTH_SHORT).show();
finish();
}
isScanning = false;
break;
case 1:
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
finish();
break;
}
}
};
faceCameraView.setPreviewCallback(new FaceCameraView.PreviewCallback() {
@Override
public void onPreview(final byte[] data, final Camera camera) {
if(FaceLoginActivity.FACEDETECTOR!=null&&FaceLoginActivity.FACERECOGNIZER!=null&&FaceLoginActivity.POINTDETECTOR!=null){
new Thread(new Runnable() {
@Override
public void run() {
//识别中不处理其他帧数据
if (!isScanning) {
isScanning = true;
try {
//获取Camera预览尺寸
Camera.Size size = camera.getParameters().getPreviewSize();
//将帧数据转为bitmap
YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
if (image != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
//纠正图像的旋转角度问题
Matrix m = new Matrix();
m.setRotate(-90, (float) bmp.getWidth() / 2, (float) bmp.getHeight() / 2);
Bitmap bm = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, true);
SeetaImageData loginSeetaImageData = ConvertUtil.ConvertToSeetaImageData(bm);
SeetaRect[] faceRects = FaceLoginActivity.FACEDETECTOR.Detect(loginSeetaImageData);
if(faceRects.length>0){
//获取人脸区域(这里只有一个所以取0)
SeetaRect faceRect = faceRects[0];
SeetaPointF[] seetaPoints = FaceLoginActivity.POINTDETECTOR.Detect(loginSeetaImageData, faceRect);//根据检测到的人脸进行特征点检测
float[] similarity = new float[1];//用来存储人脸相似度值
FaceLoginActivity.FACERECOGNIZER.Recognize(loginSeetaImageData, seetaPoints, similarity);//匹配
if(similarity[0]>0.7){
handler.sendEmptyMessage(1);
}else {
handler.sendEmptyMessage(0);
}
}else {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(LoginActivity.this, "请保持手机不要晃动", Toast.LENGTH_SHORT).show();
}
});
isScanning = false;
}
}
} catch (Exception ex) {
isScanning = false;
}
}
}
}
).start();
}
}
});
案例源码
https://github.com/myml666/Seetaface2