上节我们初始化了SDK,其余的像质量检测这些我就不详细的介绍了,文档中心给了一张一看就懂的图。这里我就把一些关于质量检测的表格列举出来供参考使用。人脸识别注重的还是用户与组的管理和1:1、1:N检索的功能~
简单的易懂的功能我就简要点一下就过了,如果不懂翻一翻文档中心和sdk工程就懂啦,很简单~
我想尽快带各位进入更深入的地方,所以这里就不再浪费篇幅讲解功能了,争取在最短篇幅里解决掉相关SDK的基本内容。
有关质量检测相关代码:
(这里光照范围那个值写错了,取值是从50~255)
参数 | 名称 | 默认值 | 取值范围 |
brightnessValue | 图片爆光度 | 40f | 50~255 |
blurnessValue | 图像模糊度 | 0.7f | 0~1.0f |
occlusionValue | 人脸遮挡阀值 | 0.5f | 0~1.0f |
headPitchValue | 低头抬头角度 | 15 | 0~45 |
headYawValue | 左右角度 | 15 | 0~45 |
headRollValue | 偏头角度 | 15 | 0~45 |
minFaceSize | 最小人脸检测值,小于此值的人脸将检测不出来,最小值为80 | 120 | 80~200 |
notFaceValue | 人脸置信度 | 0.8f | 0~1.0f |
isCheckFaceQuality | 是否检测人脸质量 | False | True/Flase |
关于特征模式的切换,在SDK内有PreferencesUtil的工具类可以用于切换特征模式。
特征模式分2种,一种为生活照模式,一种为身份证模式。各模式针对的是注册照片而言所评定的,会基于选择模式走不同的算法进行识别,所以要根据场景来选。
抠出关键的代码:
public static final String TYPE_MODEL = "TYPE_MODEL";
public static final int RECOGNIZE_LIVE = 1;
public static final int RECOGNIZE_ID_PHOTO = 2;
PreferencesUtil.putInt(TYPE_MODEL, RECOGNIZE_LIVE);
PreferencesUtil.putInt(TYPE_MODEL, RECOGNIZE_ID_PHOTO);
活检设置也影响着比对和检索的算法,只有单目可以关闭活检,其余的默认就有活检(其实不用担心,只有单目的活检特别耗时)
活检设置用的也是PreferencesUtil。抠出关键代码如下:
public static final int TYPE_NO_LIVENSS = 1;
public static final int TYPE_RGB_LIVENSS = 2;
public static final int TYPE_RGB_IR_LIVENSS = 3;
public static final int TYPE_RGB_DEPTH_LIVENSS = 4;
public static final int TYPE_RGB_IR_DEPTH_LIVENSS = 5;
// 单目无活检:
PreferencesUtil.putInt(TYPE_LIVENSS, TYPE_NO_LIVENSS);
// 单目活检:
PreferencesUtil.putInt(TYPE_LIVENSS, TYPE_RGB_LIVENSS);
// 彩灰(双目)活检:
PreferencesUtil.putInt(TYPE_LIVENSS, TYPE_RGB_IR_LIVENSS);
// 彩深(结构光)活检:
PreferencesUtil.putInt(TYPE_LIVENSS, TYPE_RGB_DEPTH_LIVENSS);
// 彩灰深(红外结构光)活检:
PreferencesUtil.putInt(TYPE_LIVENSS, TYPE_RGB_IR_DEPTH_LIVENSS);
如果选择了结构光相关的活检策略,那还需要设置摄像机类型
抠出关键代码如下:
public static final String TYPE_CAMERA = "TYPE_CAMERA";
public static final int ORBBEC = 1;
public static final int IMIMECT = 2;
public static final int ORBBECPRO = 3;
PreferencesUtil.putInt(TYPE_CAMERA, ORBBEC);
PreferencesUtil.putInt(TYPE_CAMERA, IMIMECT);
PreferencesUtil.putInt(TYPE_CAMERA, ORBBECPRO);
组注册
建议组名用正则匹配一下:
Pattern pattern = Pattern.compile("^[0-9a-zA-Z_-]{1,}$");
Matcher matcher = pattern.matcher(groupId);
Group group = new Group();
group.setGroupId(groupId);
boolean ret = FaceApi.getInstance().groupAdd(group);
照片/流注册
// 获取组列表
List groupList = DBManager.getInstance().queryGroups(0, 1000);
// 仅抽取ID列表
for (Group group : groupList) {
groupIds.add(group.getGroupId());
}
// 注册时使用人脸图片路径。Intent从android.content中引出
//读写权限请求
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
100);
return;
}
private static final int REQUEST_CODE_PICK_IMAGE = 1000;
private String faceImagePath;
faceImagePath = null;
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
// 从相机识别时使用。
private FaceDetectManager detectManager;
//权限确认
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest
.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.CAMERA}, 100);
return;
}
//获取当前所配置的活检类型
int type = PreferencesUtil.getInt(LivenessSettingActivity.TYPE_LIVENSS, LivenessSettingActivity
.TYPE_NO_LIVENSS);
//如果是单目执行以下
Intent intent = new Intent(RegActivity.this, RgbDetectActivity.class);
intent.putExtra("source", SOURCE_REG);
startActivityForResult(intent, REQUEST_CODE_AUTO_DETECT);
//双目调用RgbIrLivenessActivity
//如果是深度还要再判定一下摄像头厂家
int cameraType = PreferencesUtil.getInt(GlobalFaceTypeModel.TYPE_CAMERA, GlobalFaceTypeModel.ORBBEC);
Intent intent3 = null;
if (cameraType == GlobalFaceTypeModel.ORBBEC) {
intent3 = new Intent(RegActivity.this, OrbbecLivenessDetectActivity.class);
} else if (cameraType == GlobalFaceTypeModel.IMIMECT) {
intent3 = new Intent(RegActivity.this, IminectLivenessDetectActivity.class);
} else if (cameraType == GlobalFaceTypeModel.ORBBECPRO) {
intent3 = new Intent(RegActivity.this, OrbbecProLivenessDetectActivity.class);
}
if (intent3 != null) {
intent3.putExtra("source", SOURCE_REG);
startActivityForResult(intent3, REQUEST_CODE_AUTO_DETECT);
}
这里要着重介绍一下DetectActivity,以单目为例
前端代码剖析如下:
//布局标签
//百度封装的TexturePreView标签,用于预览USB流
//用于绘制人脸框用的TextureView
////显示检测的图片。用于调试,如果人脸sdk检测的人脸需要朝上,可以通过该图片判断
////用于给予一些提示,例如 请正视摄像头
//下方是位于底部的布局,为了在UI中显示活体指数和一些tips信息
后端代码比较重要的抠出如下:
faceDetectManager = new FaceDetectManager(getApplicationContext());
// 从系统相机获取图片帧。
final CameraImageSource cameraImageSource = new CameraImageSource(this);
// 图片越小检测速度越快,闸机场景640 * 480 可以满足需求。实际预览值可能和该值不同。和相机所支持的预览尺寸有关。
// 可以通过 camera.getParameters().getSupportedPreviewSizes()查看支持列表。
cameraImageSource.getCameraControl().setPreferredPreviewSize(1280, 720);
// 设置最小人脸,该值越小,检测距离越远,该值越大,检测性能越好。范围为80-200
previewView.setMirrored(false);
// 设置预览 这里previewView是通过(PreviewView) findViewById(R.id.preview_view);抓取的控件对象
cameraImageSource.setPreviewView(previewView);
// 设置图片源
faceDetectManager.setImageSource(cameraImageSource);
//质检
faceDetectManager.setUseDetect(true);
//可以理解为控件透明。true为不透明
textureView.setOpaque(false);
// 不需要屏幕自动变黑。
textureView.setKeepScreenOn(true);
//获取相机方向
boolean isPortrait = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
//适配朝向
if (isPortrait) {
previewView.setScaleType(PreviewView.ScaleType.FIT_WIDTH);
// 相机坚屏模式
cameraImageSource.getCameraControl().setDisplayOrientation(CameraView.ORIENTATION_PORTRAIT);
} else {
previewView.setScaleType(PreviewView.ScaleType.FIT_HEIGHT);
// 相机横屏模式
cameraImageSource.getCameraControl().setDisplayOrientation(CameraView.ORIENTATION_HORIZONTAL);
}
//选择摄像头
setCameraType(cameraImageSource);
setCameraType 选择摄像头
private void setCameraType(CameraImageSource cameraImageSource) {
// TODO 选择使用前置摄像头
// cameraImageSource.getCameraControl().setCameraFacing(ICameraControl.CAMERA_FACING_FRONT);
// TODO 选择使用usb摄像头
cameraImageSource.getCameraControl().setCameraFacing(ICameraControl.CAMERA_USB);
// 如果不设置,人脸框会镜像,显示不准
// previewView.getTextureView().setScaleX(-1);
// TODO 选择使用后置摄像头
// cameraImageSource.getCameraControl().setCameraFacing(ICameraControl.CAMERA_FACING_BACK);
// previewView.getTextureView().setScaleX(-1);
}
listener 设置回调,回调人脸检测结果。
// 设置回调,回调人脸检测结果。
faceDetectManager.setOnFaceDetectListener(new FaceDetectManager.OnFaceDetectListener() {
@Override
public void onDetectFace(int retCode, FaceInfo[] infos, ImageFrame frame) {
// TODO 显示检测的图片。用于调试,如果人脸sdk检测的人脸需要朝上,可以通过该图片判断
final Bitmap bitmap =
Bitmap.createBitmap(frame.getArgb(), frame.getWidth(), frame.getHeight(), Bitmap.Config.ARGB_8888);
//如果需要debug,解开下头
// handler.post(new Runnable() {
// @Override
// public void run() {
// testView.setImageBitmap(bitmap);
// }
// });
checkFace(retCode, infos, frame);
showFrame(frame, infos);
}
});
解析Detect的返回json
//解析质检返回的json结果,处理相关逻辑。这里其中的filter是一个基于各类阈值相关条件给出的参考函数
private void checkFace(int retCode, FaceInfo[] faceInfos, ImageFrame frame) {
if ( retCode == FaceTracker.ErrCode.OK.ordinal() && faceInfos != null) {
FaceInfo faceInfo = faceInfos[0];
String tip = filter(faceInfo, frame);
displayTip(tip);
} else {
String tip = checkFaceCode(retCode);
displayTip(tip);
}
}
private String filter(FaceInfo faceInfo, ImageFrame imageFrame) {
String tip = "";
if (faceInfo.mConf < 0.6) {
tip = "人脸置信度太低";
return tip;
}
float[] headPose = faceInfo.headPose;
if (Math.abs(headPose[0]) > 20 || Math.abs(headPose[1]) > 20 || Math.abs(headPose[2]) > 20) {
tip = "人脸置角度太大,请正对屏幕";
return tip;
}
int width = imageFrame.getWidth();
int height = imageFrame.getHeight();
// 判断人脸大小,若人脸超过屏幕二分一,则提示文案“人脸离手机太近,请调整与手机的距离”;
// 若人脸小于屏幕三分一,则提示“人脸离手机太远,请调整与手机的距离”
float ratio = (float) faceInfo.mWidth / (float) height;
Log.i("liveness_ratio", "ratio=" + ratio);
if (ratio > 0.6) {
tip = "人脸离屏幕太近,请调整与屏幕的距离";
return tip;
} else if (ratio < 0.2) {
tip = "人脸离屏幕太远,请调整与屏幕的距离";
return tip;
} else if (faceInfo.mCenter_x > width * 3 / 4 ) {
tip = "人脸在屏幕中太靠右";
return tip;
} else if (faceInfo.mCenter_x < width / 4 ) {
tip = "人脸在屏幕中太靠左";
return tip;
} else if (faceInfo.mCenter_y > height * 3 / 4 ) {
tip = "人脸在屏幕中太靠下" ;
return tip;
} else if (faceInfo.mCenter_x < height / 4 ) {
tip = "人脸在屏幕中太靠上";
return tip;
}
int liveType = PreferencesUtil.getInt(TYPE_LIVENSS, .TYPE_NO_LIVENSS);
if (liveType == TYPE_NO_LIVENSS) {
saveFace(faceInfo, imageFrame);
} else if (liveType == TYPE_RGB_LIVENSS) {
if (rgbLiveness(imageFrame, faceInfo) > 0.9) {
saveFace(faceInfo, imageFrame);
} else {
toast("rgb活体分数过低");
}
}
return tip;
}
//保存照片的方法
private void saveFace(FaceInfo faceInfo, ImageFrame imageFrame) {
final Bitmap bitmap = FaceCropper.getFace(imageFrame.getArgb(), faceInfo, imageFrame.getWidth());
if (source == RegActivity.SOURCE_REG) {
// 注册来源保存到注册人脸目录
File faceDir = FileUitls.getFaceDirectory();
if (faceDir != null) {
String imageName = UUID.randomUUID().toString();
File file = new File(faceDir, imageName);
// 压缩人脸图片至300 * 300,减少网络传输时间
ImageUtils.resize(bitmap, file, 300, 300);
Intent intent = new Intent();
intent.putExtra("file_path", file.getAbsolutePath());
setResult(Activity.RESULT_OK, intent);
finish();
} else {
toast("注册人脸目录未找到");
}
} else {
try {
// 其他来源保存到临时目录
final File file = File.createTempFile(UUID.randomUUID().toString() + "", ".jpg");
// 人脸识别不需要整张图片。可以对人脸区别进行裁剪。减少流量消耗和,网络传输占用的时间消耗。
ImageUtils.resize(bitmap, file, 300, 300);Intent intent = new Intent();
intent.putExtra("file_path", file.getAbsolutePath());
setResult(Activity.RESULT_OK, intent);
finish();
} catch (IOException e) {
e.printStackTrace();
}
}
}
绘制人脸框
private void showFrame(ImageFrame imageFrame, FaceInfo[] faceInfos) {
Canvas canvas = textureView.lockCanvas();
if (canvas == null) {
textureView.unlockCanvasAndPost(canvas);
return;
}
if (faceInfos == null || faceInfos.length == 0) {
// 清空canvas
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
textureView.unlockCanvasAndPost(canvas);
return;
}
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
FaceInfo faceInfo = faceInfos[0];
rectF.set(getFaceRect(faceInfo, imageFrame));
// 检测图片的坐标和显示的坐标不一样,需要转换。
previewView.mapFromOriginalRect(rectF);
float yaw = Math.abs(faceInfo.headPose[0]);
float patch = Math.abs(faceInfo.headPose[1]);
float roll = Math.abs(faceInfo.headPose[2]);
if (yaw > 20 || patch > 20 || roll > 20) {
// 不符合要求,绘制黄框
paint.setColor(Color.YELLOW);
String text = "请正视屏幕";
float width = paint.measureText(text) + 50;
float x = rectF.centerX() - width / 2;
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
canvas.drawText(text, x + 25, rectF.top - 20, paint);
paint.setColor(Color.YELLOW);
} else {
// 符合检测要求,绘制绿框
paint.setColor(Color.GREEN);
}
paint.setStyle(Paint.Style.STROKE);
// 绘制框
canvas.drawRect(rectF, paint);
textureView.unlockCanvasAndPost(canvas);
}
以上代码都是截取自demo中的。可选择性使用。
人脸检索 视频流人脸库检索
因为真正投入到项目中的落地应用以视频流检索人脸库为首,所以先贴出这块代码。
之后如果有1:1和M:N相关的功能届时再抽出代码
这里也优先使用单目,我觉得会用单目了,双目和结构光也只是配置和UI调整上的问题了,因为SDK,一切都变的很简单。
大部分都经过Detect,所以在取流、质检都类似
在listener开始有部分不同
private void addListener() {
// 设置回调,回调人脸检测结果。
faceDetectManager.setOnFaceDetectListener(new FaceDetectManager.OnFaceDetectListener() {
@Override
public void onDetectFace(int retCode, FaceInfo[] infos, ImageFrame frame) {
// TODO 显示检测的图片。用于调试,如果人脸sdk检测的人脸需要朝上,可以通过该图片判断
final Bitmap bitmap =
Bitmap.createBitmap(frame.getArgb(), frame.getWidth(), frame.getHeight(), Bitmap.Config
.ARGB_8888);
handler.post(new Runnable() {
@Override
public void run() {
testView.setImageBitmap(bitmap);
}
});
if (retCode == FaceTracker.ErrCode.OK.ordinal() && infos != null) {
asyncIdentity(frame, infos);
}
showFrame(frame, infos);
}
});
}
private void asyncIdentity(final ImageFrame imageFrame, final FaceInfo[] faceInfos) {
//通过一个标识来解决并发问题
if (identityStatus != IDENTITY_IDLE) {
return;
}
es.submit(new Runnable() {
@Override
public void run() {
if (faceInfos == null || faceInfos.length == 0) {
return;
}
int liveType = PreferencesUtil.getInt(LivenessSettingActivity.TYPE_LIVENSS, LivenessSettingActivity
.TYPE_NO_LIVENSS);
if (liveType == LivenessSettingActivity.TYPE_NO_LIVENSS) {
//这里只尝试去找检测出的第一张脸,后续可以修改实现多脸比对
identity(imageFrame, faceInfos[0]);
} else if (liveType == LivenessSettingActivity.TYPE_RGB_LIVENSS) {
if (rgbLiveness(imageFrame, faceInfos[0]) > FaceEnvironment.LIVENESS_RGB_THRESHOLD) {
identity(imageFrame, faceInfos[0]);
} else {
// toast("rgb活体分数过低");
}
}
}
});
}
private void identity(ImageFrame imageFrame, FaceInfo faceInfo) {
float raw = Math.abs(faceInfo.headPose[0]);
float patch = Math.abs(faceInfo.headPose[1]);
float roll = Math.abs(faceInfo.headPose[2]);
// 人脸的三个角度大于20不进行识别
if (raw > 20 || patch > 20 || roll > 20) {
return;
}
identityStatus = IDENTITYING;
long starttime = System.currentTimeMillis();
int[] argb = imageFrame.getArgb();
int rows = imageFrame.getHeight();
int cols = imageFrame.getWidth();
int[] landmarks = faceInfo.landmarks;
int type = PreferencesUtil.getInt(GlobalFaceTypeModel.TYPE_MODEL, GlobalFaceTypeModel.RECOGNIZE_LIVE);
IdentifyRet identifyRet = null;
if (type == GlobalFaceTypeModel.RECOGNIZE_LIVE) {
identifyRet = FaceApi.getInstance().identity(argb, rows, cols, landmarks, groupId);
} else if (type == GlobalFaceTypeModel.RECOGNIZE_ID_PHOTO) {
identifyRet = FaceApi.getInstance().identityForIDPhoto(argb, rows, cols, landmarks, groupId);
}
if (identifyRet != null) {
//这里可以拿到ID和分数。可以再对分数进行筛选一次。demo中<80的都滤掉了。
displayUserOfMaxScore(identifyRet.getUserId(), identifyRet.getScore());
}
identityStatus = IDENTITY_IDLE;
displayTip("特征抽取对比耗时:" + (System.currentTimeMillis() - starttime), featureDurationTv);
}
那在本篇中快速的过了一下如何修改SDK的特征模型(生活照、身份证),如何添加组、用户,如何通过相册注册和利用单目视频流进行注册和1:N检索。
本篇的代码也都是从demo中提炼而来的,省去了大部分的View层UI介绍和Controller层的控制,着重介绍了SDK代码。如果对双目和结构光的可以顺着本篇的逻辑进行分析。
之后在介绍落地场景时也会基于不同的摄像头开分篇呢,届时欢迎参考。
下一篇,我将脱离DEMO代码,自己上手写一个从注册完成到视频流识别的代码。
希望能帮助到各位使用者~