上节我们初始化了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代码,自己上手写一个从注册完成到视频流识别的代码。
希望能帮助到各位使用者~
授权在demo里看的了。跑一下就知道环境正常情况了
返回-1是因为离线采集SDk授权过期了,返回0是采集到人脸
在线的是免费的呀
要是免费的就好了
个人玩玩在线API就行了啊。离线SDK当然是针对商业的
这么说个人玩家就用不了了,是吧
现在还要企业认证么?
别人那里考来的
这个sdk应该是必须要企业认证了才开放的。不晓得你之前说有问题的sdk是怎么来的。。
研究研究,学习学习
这些本身就是项目成本 不用自己出
是啊 你不下载demo你咋开发的呀
不过要商用的话,价格就比较高了
800块,就是那个价格最高的? 也不是很贵了。
SDK下载需要企业认证...
文档中心默认下载下来的就是demo喔
我没有demo版本的代码,你可以给我发一份吗?谢谢啦
我印象里0是没人。
-1我感觉是个错误码
建议跑一下demo版本的代码。
检查下模型有没有成功加载进来
public void onDetectFace(int retCode, FaceInfo[] infos, ImageFrame frame)中的retcode,每次检测返回都是-1,应该是没检测人脸吧
这方法是void诶,应该没返回值的