首先,我们提出几个关键词:百度人脸API、人脸图像连续采集、注册人脸库、图像编码、face_token、人脸检测接口、人脸对比接口
需要先注入两个库:
implementation 'com.google.code.gson:gson:2.8.0'
implementation 'com.github.zcolin:ZFrame:2.0.1'
登录百度人脸的官网会有教程教你如何创建应用并获取两个重要的key:ApiKey和SecretKey,每个新应用的这两个key都是不一样的,用这两个key可以请求获取开发者权限access_token,这个token是请求人脸检测和人脸对比接口的必要参数,所以很重要,获取access_token的代码如下:
private static void getAuth(ZParamSubmitListeneraccessTokenListener) { // 获取token地址 String authHost = "https://aip.baidubce.com/oauth/2.0/token?"; String getAccessTokenUrl = authHost // 1. grant_type为固定参数 + "grant_type=client_credentials" // 2. 官网获取的 API Key + "&client_id=" + ApiKey // 3. 官网获取的 Secret Key + "&client_secret=" + SecretKey; ZHttp.get(getAccessTokenUrl, new ZResponse (AccessTokenReply.class) { @Override public void onSuccess(Response response, AccessTokenReply resObj) { if (resObj != null) { if (accessTokenListener != null) { accessTokenListener.submit(resObj.access_token); } } } @Override public void onError(int code, String error) { super.onError(code, error); if (accessTokenListener != null) { accessTokenListener.submit(""); } } }); }
AccessTokenReply.java
public class AccessTokenReply implements ZReply { @Override public boolean isSuccess() { return access_token != null; } @Override public int getReplyCode() { return 0; } @Override public String getErrorMessage() { return "token获取失败"; } public String access_token; }
需要注意的是这个access_token有使用期限(一个月),切记需要每30天进行定期更换,或者每次请求都拉取新的token
一般app上的人脸扫描预览界面都是一个圆形框,我们也不例外,继承自SurfaceView,重写draw方法绘制圆形区域:
public class CircleSurfaceView extends SurfaceView { private int radius;//预览框半径偏移,默认为屏幕高度的四分之一 private int widthSize; private int heightSize; public CircleSurfaceView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(); } public CircleSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public CircleSurfaceView(Context context) { super(context); initView(); } public void setRadius(int radius) { this.radius = radius; } private void initView() { this.setFocusable(true); this.setFocusableInTouchMode(true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); widthSize = MeasureSpec.getSize(widthMeasureSpec); heightSize = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(widthSize, heightSize); } @Override public void draw(Canvas canvas) { Path path = new Path(); path.addCircle(widthSize / 2, heightSize / 2, heightSize / 4 + radius, Path.Direction.CCW); canvas.clipPath(path, Region.Op.REPLACE); super.draw(canvas); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { int screenWidth = ScreenUtil.getScreenWidth(getContext()); int screenHeight = ScreenUtil.getScreenHeight(getContext()); w = screenWidth; h = screenHeight; super.onSizeChanged(w, h, oldw, oldh); } }
人脸图像的连续采集要设置一堆的camera参数:
private Camera getMcamera() { Camera camera; try { camera = Camera.open(1);//开前置摄像头 if (camera != null && !isPreview) { try { Camera.Parameters parameters = camera.getParameters(); parameters.setPreviewSize(screenWidth, screenHeight); // 设置预览照片的大小 parameters.setPreviewFpsRange(20, 30); // 每秒显示20~30帧 parameters.setPictureFormat(ImageFormat.NV21); // 设置图片格式 parameters.setPictureSize(screenWidth, screenHeight); // 设置照片的大小 parameters.setPreviewFrameRate(1);// 每秒从摄像头里面获得1个画面 帧率过高可能会导致内存溢出 camera.setPreviewDisplay(mHolder); // 通过SurfaceView显示取景画面 camera.setPreviewCallback((data, camera1) -> { Camera.Size size = camera1.getParameters().getPreviewSize(); try { // 调用image.compressToJpeg()将YUV格式图像数据data转为jpg格式 YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null); if (image != null) { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, outStream); 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()); String pictureName = ""; if (type == 0) {//人脸录入操作 pictureName = "answer.jpg"; } else if (type == 1) {//身份验证操作 pictureName = "confirm.jpg"; } System.out.println(pictureName); saveBitmap(bmp, pictureName);//轮流保存每一张图片,这个方法中包括了录入和验证的功能逻辑 outStream.flush(); } } catch (Exception ex) { Log.e("Sys", "Error:" + ex.getMessage()); } }); camera.autoFocus(null); // 自动对焦 camera.setDisplayOrientation(getAngel()); camera.startPreview(); // 开始预览 } catch (Exception e) { e.printStackTrace(); } isPreview = true; } } catch (Exception e) { camera = null; e.printStackTrace(); System.out.println("无法获取摄像头"); } return camera; }
这个方法中的部分变量是成员变量:
int type = 0; CircleSurfaceView mPreview; SurfaceHolder mHolder; int screenWidth, screenHeight;//640,480 boolean isPreview = false; // 是否在预览中
每一帧的图片名都是一样的,只有检测完一张图片并且失败之后才会检测下一张图片,status是一个成员变量,1代表继续请求接口,2代表停止请求接口,如果录入失败或者校验失败status会重新赋值为1进行下一张图片的数据请求。而有一种例外的情况就是我们请求人脸比对接口返回的相似度值在0-100,取90以上代表为同一个人即身份验证成功,若返回值小于90则不为同一个人即身份校验失败,这种情况下status不会重新赋值为1,它会回调校验失败的接口进行“非用户本人”的身份错误提示信息。录入失败的情况不需要处理,因为录入失败说明没有检测到人脸,失败了就继续检测,直到检测出人脸即为成功,当然你也可以设置时间,检测录入时间过长时你可以判定为超时操作,回调接口就由你自己去写喽
private void saveBitmap(Bitmap bitmap, final String pictureName) { final File imgFile = new File(getContext().getCacheDir().getPath() + "/" + pictureName); if (imgFile.exists() && imgFile.isFile()) { imgFile.delete(); } FileOutputStream out; try { out = new FileOutputStream(imgFile); if (bitmap.compress(Bitmap.CompressFormat.PNG, 60, out)) { out.flush(); out.close(); } } catch (IOException e) { e.printStackTrace(); } if (type == 0 && status == 1) { status = 2; tvMsg.setText("录入中,请摇摇头眨眨眼..."); FaceMgr.detect(getContext().getCacheDir().getPath() + "/" + pictureName, detectResult -> { if (detectResult != null) { String num = detectResult.get("num"); String faceToken = detectResult.get("face_token"); if (Integer.valueOf(detectResult.get("code")) == 0 && num.equals("1")) { SPUtil.putString("face_token", faceToken); FaceMgr.add(faceToken, isSuccess -> false); sendMessage(0, faceToken); releaseCamera(); } else { status = 1; } } else { status = 1; } return false; }); } else if (type == 1 && status == 1) { tvMsg.setText("验证中,请摇摇头眨眨眼..."); status = 2; FaceMgr.detect(getContext().getCacheDir().getPath() + "/" + pictureName, detectResult -> { String num = detectResult.get("num"); String faceToken = detectResult.get("face_token"); if (Integer.valueOf(detectResult.get("code")) == 0 && num.equals("1")) { FaceMgr.match(SPUtil.getString("face_token", ""), faceToken, matchResult -> { String score = matchResult.get("score"); if (Integer.valueOf(matchResult.get("code")) == 0) { if (Double.valueOf(score) >= 90) { sendMessage(1, score); releaseCamera(); } else { sendMessage(2, null); releaseCamera(); } } else { status = 1; } return false; }); } else { status = 1; } return false; }); } }
sendMessage方法就是异步实现成功或者失败的响应回调:
private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: faceEventListener.settingSuccess(msg.obj.toString()); break; case 1: faceEventListener.matchingSuccess(msg.obj.toString()); break; case 2: faceEventListener.matchingFail(); break; default: break; } } };
private void sendMessage(int code, String value) { Message msg = new Message(); msg.what = code; msg.obj = value; mHandler.sendMessage(msg); }
事件接口:
private FaceEventListener faceEventListener = new FaceEventListener() { @Override public void settingSuccess(String token) { Toast.makeText(getContext(), "录入成功!", Toast.LENGTH_LONG).show(); } @Override public void matchingSuccess(String score) { Toast.makeText(getContext(), "验证成功!", Toast.LENGTH_LONG).show(); } @Override public void matchingFail() { Toast.makeText(getContext(), "非用户本人!", Toast.LENGTH_LONG).show(); } };
public interface FaceEventListener { /** * 录入成功后 回调 */ void settingSuccess(String token); /** * 验证成功后 回调 */ void matchingSuccess(String score); /** * 验证失败后 回调 */ void matchingFail(); }
这个类下面的操作主要有人脸检测、人脸库注册、人脸比对以及access_token的获取:
public class FaceMgr { //从百度人脸API官网创建应用并获取这两个key,这俩key比较隐私,自己去百度官网获取就好了 private static String ApiKey = "*****************"; private static String SecretKey = "*****************************"; private static void getAuth(ZParamSubmitListeneraccessTokenListener) { // 获取token地址 String authHost = "https://aip.baidubce.com/oauth/2.0/token?"; String getAccessTokenUrl = authHost // 1. grant_type为固定参数 + "grant_type=client_credentials" // 2. 官网获取的 API Key + "&client_id=" + ApiKey // 3. 官网获取的 Secret Key + "&client_secret=" + SecretKey; ZHttp.get(getAccessTokenUrl, new ZResponse (AccessTokenReply.class) { @Override public void onSuccess(Response response, AccessTokenReply resObj) { if (resObj != null) { if (accessTokenListener != null) { accessTokenListener.submit(resObj.access_token); } } } @Override public void onError(int code, String error) { super.onError(code, error); if (accessTokenListener != null) { accessTokenListener.submit(""); } } }); } /** * 注册到人脸库 否则facetoken会失效 */ public static void add(String token, ZParamSubmitListener onAddResultListener) { getAuth(accessToken -> { String url = "https://aip.baidubce.com/rest/2.0/face/v3/faceset/user/add?access_token=" + accessToken; try { Map map = new HashMap<>(); map.put("image", token); map.put("image_type", "FACE_TOKEN"); map.put("group_id", "group_demo"); map.put("user_id", "user1"); map.put("liveness_control", "NORMAL"); map.put("quality_control", "LOW"); ZHttp.post(url, map, new ZResponse (FaceAddReply.class) { @Override public void onSuccess(Response response, FaceAddReply resObj) { onAddResultListener.submit(true); } @Override public void onError(int code, String error) { super.onError(code, error); onAddResultListener.submit(false); } }); } catch (Exception e) { e.printStackTrace(); } return false; }); } /** * 人脸对比 */ public static void match(String token1, String token2, ZParamSubmitListener
源码下载地址:https://download.csdn.net/download/qq_37159335/10929905