最近研究了一下比较热门的人脸识别技术,经过一番调研,选取了虹软AI开放平台。

人脸识别的应用也是相对广泛,借用官网一张图来说明。


应用场景.png

在Android App也有不少应用场景,比如:刷脸打卡、身份验证等。

为什么选择虹软人脸识别?

  • 虹软公司是一家具有硅谷背景的图像处理公司,虹软在人脸相关领域里的研究成果及技术应用不论是其深度和广度,都是全球领先者。虹软的人脸相关技术广泛应用于智能手机、DSC、平板、IP Camera, 机器人、智能家居、智能终端等领域,超过十亿台的设备在使用该技术。此外虹软人脸相关技术更被广泛用于专业的垂直领域比如公安,安防和医疗等。

  • 虹软人脸识别等SDK是免费的,且支持离线环境使用

虹软功能简介

人脸检测
对传入图像数据进行人脸检测,返回人脸位置信息和人脸在图像中的朝向信息,可用于后续的人脸分析、人脸比对操作,支持图像模式和视频流模式。支持单人脸、多人脸检测,最多支持检测人脸数为50

人脸追踪
捕捉视频流中的人脸信息,精确定位并跟踪面部区域位置

人脸比对
将两个人脸进行比对,来判断是否为同一个人,返回比对相似度值,如果相似度值在0.8以上,一般可认为是同一个人

人脸查找
在人脸库中查找相似的人脸

人脸属性
对检测到的人脸进行属性分析,支持性别、年龄,3d等属性分析,支持图像模式和视频流模式。

活体检测
检测是否为真人,防止恶意***。针对视频流/图片,通过采集人像的破绽来判断目标对象是否为活体,可有效防止照片、屏幕二次翻拍等作弊***。只有视频模式下才能检测成功。

人脸三维角度检测
检测输入图像数据指定区域人脸的三维角度信息,包含人脸三个空间角度:俯仰角(pitch), 横滚角(roll), 偏航角(yaw), 支持图像模式和视频流模式。

三维


人脸注册和识别的流程如下

人脸识别流程


准备工作

首先去虹软官网的新手帮助查看SDK配置和使用的方法,也可以下载官方Demo来体验一下。

V2.0相较于V1.0的区别

1、按照官方的说法,V2.0比V1.0在速度上有所提升,具体多少,我没测过;
2、V2.0在人脸检测中增加了人脸3D角度值和活体检测值,以前活体检测是单独的一项,是收费的,在V2.0中变成免费的了(亲测可行)
3、SDK变化很大,整体更简洁了,很多操作需要通过FaceEngine对象来操作,初始化也只需要1个就行,比以前简单了。

SDK的调用流程图


Step1:首先调用 FaceEngine 的 active 方法激活设备,一个设备安装后仅需激活一次,卸载重 新安装后需要重新激活。

Step2:调用 FaceEngine 的 init 方法初始化 SDK,初始化成功后才能进一步使用 SDK 的功 能。

Step3:调用 FaceEngine 的 detectFaces 方法进行图像数据或预览数据的人脸检测,若检 测成功,则可得到一个人脸列表。(初始化时 combineMask 需要 ASF_FACE_DETECT)

Step4:调用 FaceEngine 的 extractFaceFeature 方法可对图像中指定的人脸进行特征提 取。(初始化时 combineMask 需要 ASF_FACE_RECOGNITION)

Step5:调用 FaceEngine 的 compareFaceFeature 方法可对传入的两个人脸特征进行比对, 获取相似度。(初始化时 combineMask 需要 ASF_FACE_RECOGNITION)通常相似度在0.8以上可认为是同一个人。

Step6:调用 FaceEngine 的 process 方法,传入不同的 combineMask 组合可对 Age、 Gender、Face3Dangle、Liveness 进行检测,传入的 combineMask 的任一属性都需要在 init 时进行初始化。

Step7:调用 FaceEngine 的 getAgegetGendergetFace3DanglegetLiveness 方法可获 取年龄、性别、三维角度、活体检测结果,且每个结果在获取前都需要在 process 中进行 处理。

Step8:调用 FaceEngine 的unInit 方法销毁引擎。在 init 成功后如不 unInit 会导致内存 泄漏。

注意:调用FaceEngine中的任何一个方法时都会返回一个int的结果信息。如果成功的话会返回ErrorInfo.MOK(0),否则就是失败。你可以根据返回的errorCode去官网查询。

激活人脸识别引擎SDK

mFaceEngine = new FaceEngine();
        boolean isActive = SharedPrefUtils.getBoolean(mContext, Constants.IS_ACTIVE, false);
        if (!isActive) {
            int activeCode = mFaceEngine.active(this,
                    Constants.APP_ID,
                    Constants.SDK_KEY);
            if (activeCode == ErrorInfo.MOK) {
                Log.i(TAG, "人脸引擎激活成功");
                SharedPrefUtils.writeBoolean(mContext, Constants.IS_ACTIVE, true);
            } else {
                Log.i(TAG, "人脸引擎激活失败 " + activeCode);
            }
        }

注意:激活引擎的active()方法,需要将APP_ID和SDK_KEY替换成你自己的(官网获取)。一个设备安装后仅需激活一次,卸载重 新安装后需要重新激活。

初始化人脸识别引擎

/**
     * 初始化图片识别引擎(视频、拍照)
     * 调用FaceEngine的init方法初始化SDK,初始化成功后才能进一步使用SDK的功能。
     *
     * @param context
     * @param detectFaceMaxNum 最大检测人数
     */
    public boolean initVideoEngine(Context context, int detectFaceMaxNum) {
        mFaceEngine = new FaceEngine();
        int faceEngineCode = mFaceEngine.init(context,
                FaceEngine.ASF_DETECT_MODE_VIDEO,
                FaceEngine.ASF_OP_270_ONLY,
                16,
                detectFaceMaxNum,
                FaceEngine.ASF_FACE_RECOGNITION | FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_AGE | FaceEngine.ASF_FACE3DANGLE | FaceEngine.ASF_GENDER | FaceEngine.ASF_LIVENESS);
        if (faceEngineCode == ErrorInfo.MOK) {
            Log.i(TAG, "人脸引擎初始化成功");
        } else {
            Log.i(TAG, "人脸引擎初始化失败 " + faceEngineCode);
        }
        return faceEngineCode == ErrorInfo.MOK;
    }

V2.0的初始化还是简洁了不少,以前是多个对象初始化,2.0只需要FaceEngine 初始化就行了。

参数2:检测模式,分别是视频模式ASF_DETECT_MODE_VIDEO和图像模式ASF_DETECT_MODE_IMAGE,如果需要活体检测的话只能使用视频模式。

参数3:人脸检测方向,如果使用前置摄像头的话设置为270度即可;如果是检测图片中的人脸的话要使用ASF_OP_0_HIGHER_EXT 。

参数4:人脸相对于所在图片的长边的占比,在视频模式ASF_DETECT_MODE_VIDEO下有效值范围[2,16],在图像模式ASF_DETECT_MODE_IMAGE下有效值范围[2,32]

参数5:人脸引擎最多能检测出的人脸数,有效值范围[1,50]

参数6:初始化引擎所需的功能,可以是以下单个或者多个,用 | 运算符拼接
ASF_NONE
ASF_FACE_DETECT
ASF_FACE_RECOGNITION
ASF_AGE
ASF_GENDER
ASF_FACE3DANGLE
ASF_LIVENESS

人脸识别

通过照片或者相机获取头像,图片不用说了,打开相册获取图片数据即可,相机的话,需要打开前置摄像头,在相机预览回调的方法中获取头像数据

Camera.PreviewCallback mPreViewCallback = new Camera.PreviewCallback() {
        @Override
        public void onPreviewFrame(final byte[] data, Camera camera) {
            if (startFaceCheck) {
              int detectFacesCode = faceEngine.detectFaces(nv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList);
if (detectFacesCode== ErrorInfo.MOK&&faceInfoList!=null&&faceInfoList.size()>0){
                    Log.i(TAG, "onPreview 人脸检测成功 : ");
                }else {
                    Log.i(TAG, "onPreview 人脸检测失败 : "+detectFacesCode);
                }
            }
        }
    };

在相机预览的回调方法中的data数据就是NV21格式,可以在这里处理人脸检测

注意:只有code为ErrorInfo.MOK且检测到的人脸数>=1才算检测成功,否则就是失败。

参数1:图像数据,也就是Camera中onPreviewFrame回调的数据
参数2:图像的宽度,也就是Camera的setPreviewSize的宽度
参数3:图像的高度,也就是Camera的setPreviewSize的高度
参数4:图像的颜色空间格式,支持NV21(CP_PAF_NV21)、BGR24(CP_PAF_BGR24)
参数5:人脸列表,List<FaceInfo> faceInfoList,如果调用detectFaces成功之后,它就会有数据。

根据相机获取人脸数据成功以后,需要绘制一个人脸框的自定义View来把脸的轮廓给框起来。这一步你以为很复杂,其实一点也不难,因为虹软SDK已经帮你获取到了人脸位置坐标了,你只需要简单调用一下绘制一个矩形框就好。
简单一点的,只画一个纯实线的矩形,你可以调用这个方法。

 /**
     * 绘制人脸框
     *
     * @param bitmap
     */
    public Bitmap drawFaceRect(Bitmap bitmap) {
        //绘制bitmap
        bitmap = bitmap.copy(Bitmap.Config.RGB_565, true);
        Canvas canvas = new Canvas(bitmap);
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStrokeWidth(10);
        paint.setColor(Color.YELLOW);

        for (int i = 0; i < faceInfoList.size(); i++) {
            //绘制人脸框
            paint.setStyle(Paint.Style.STROKE);
            canvas.drawRect(faceInfoList.get(i).getRect(), paint);
            //绘制人脸序号
            paint.setStyle(Paint.Style.FILL_AND_STROKE);
            paint.setTextSize(faceInfoList.get(i).getRect().width() / 2);
            canvas.drawText("" + i, faceInfoList.get(i).getRect().left, faceInfoList.get(i).getRect().top, paint);
        }
        return bitmap;
    }

如果需要更复杂的自定义人脸框,你可以参考demo中的DrawHelper类更改

检测人脸成功以后,你可以继续检测人脸年龄、性别等信息和提取人脸特征。

检测人脸年龄、性别等信息

注意:这一步调用前需要进行人脸检测detectFaces才行。

/**
     * 检测人脸年龄、性别等信息
     *
     * @param bgr24
     * @param width
     * @param height
     * @param format             图像的格式
     * @param onFaceDetectResult
     */
    public void detectFaceInfo(byte[] bgr24, int width, int height, int format, IOnFaceDetectResult onFaceDetectResult) {
        /**
         * 4.上一步已获取到人脸位置和角度信息,传入给process函数,进行年龄、性别、三维角度检测
         */
        int faceProcessCode = mFaceEngine.process(bgr24, width, height, format, faceInfoList, FaceEngine.ASF_AGE | FaceEngine.ASF_GENDER | FaceEngine.ASF_FACE3DANGLE | FaceEngine.ASF_LIVENESS);
        if (faceProcessCode != ErrorInfo.MOK) {
            onFaceDetectResult.detectFaceInfos(faceProcessCode, "获取人脸年龄、性别等信息失败", null);
        } else {
            Log.i(TAG, "processImage 获取年龄、性别等信息成功 : ");
        }


        //年龄信息结果
        List<AgeInfo> ageInfoList = new ArrayList<>();
        //性别信息结果
        List<GenderInfo> genderInfoList = new ArrayList<>();
        //人脸三维角度结果
        List<Face3DAngle> face3DAngleList = new ArrayList<>();
        //活体检测结果
        List<LivenessInfo> livenessInfoList = new ArrayList<>();
        //获取年龄、性别、三维角度、活体结果
        int ageCode = mFaceEngine.getAge(ageInfoList);
        int genderCode = mFaceEngine.getGender(genderInfoList);
        int face3DAngleCode = mFaceEngine.getFace3DAngle(face3DAngleList);
        int livenessCode = mFaceEngine.getLiveness(livenessInfoList);

        if ((ageCode | genderCode | face3DAngleCode | livenessCode) != ErrorInfo.MOK) {
            Log.i(TAG, "detectFaceInfo 获取部分信息失败: ageCode: " + ageCode + " genderCode:" + genderCode + " face3DAngleCode:" + face3DAngleCode + " livenessCode:" + livenessCode);
            return;
        }


        /**
         * 5.年龄、性别、三维角度已获取成功,添加信息到提示文字中
         */
        List<FaceDetectInfo> mFaceDetectInfos = new ArrayList<>();
        for (int i = 0; i < faceInfoList.size(); i++) {
            FaceInfo faceInfo = faceInfoList.get(i);
            FaceFeature faceFeature = extractFaceFeature(bgr24, width, height, format, faceInfo);

            FaceDetectInfo faceDetectInfo = new FaceDetectInfo();
            faceDetectInfo.setFaceAge(ageInfoList.get(i).getAge());
            faceDetectInfo.setFaceGender(genderInfoList.get(i).getGender());
            faceDetectInfo.setFace3DAngle(face3DAngleList.get(i));
            faceDetectInfo.setLivenessInfo(livenessInfoList.get(i));
            faceDetectInfo.setFaceInfo(faceInfo);
            faceDetectInfo.setFaceFeature(faceFeature);
            mFaceDetectInfos.add(faceDetectInfo);
        }

        onFaceDetectResult.detectFaceInfos(faceProcessCode, "获取人脸年龄、性别等信息失败", mFaceDetectInfos);

    }

人脸特征提取

注意:这一步调用前需要进行人脸检测detectFaces才行。

/**
     * 提取人脸特征
     *
     * @param bgr24
     * @param width
     * @param height
     * @param faceInfo
     * @param format   图像的格式
     */
    public FaceFeature extractFaceFeature(byte[] bgr24, int width, int height, int format, FaceInfo faceInfo) {
        FaceFeature faceFeature = new FaceFeature();
     
        int extractFaceFeatureCode = mFaceEngine.extractFaceFeature(bgr24, width, height, format, faceInfo, faceFeature);
        if (extractFaceFeatureCode != ErrorInfo.MOK) {
            Log.i(TAG, "extractFaceFeature:extract face feature failed,code= " + extractFaceFeatureCode);
            faceFeature = null;
        } else {
            Log.i(TAG, "extractFaceFeature 提取人脸特征成功 : " + faceFeature.toString());
        }
        return faceFeature;
    }

FaceFeature就是人脸特征对象,它可以用来比对2张人脸的相似度。如果提取成功,这个FaceFeature对象就有数据,否则为空。

人脸比对

这一步思路比较简单,通过照相机获取人脸特征对象FaceFeature,再从数据库中查询要对比的人的FaceFeature,如果相似度在0.8以上就可以认为是同一个人。

/**
     * 人脸比对
     *
     * @param faceFeature1 第一张人脸特征
     * @param faceFeature2 第二张人脸特征
     * @return
     */
    public float compare(FaceFeature faceFeature1, FaceFeature faceFeature2) {
        if (faceFeature1 == null || faceFeature2 == null) {
            return 0;
        }
        FaceSimilar faceSimilar = new FaceSimilar();
        //比对两个人脸特征获取相似度信息
        int compareFaceFeature = mFaceEngine.compareFaceFeature(faceFeature1, faceFeature2, faceSimilar);
        if (compareFaceFeature == ErrorInfo.MOK) {
            //获取相似度
            float score = faceSimilar.getScore();
            Log.d(TAG, "人脸比对成功 " + faceSimilar.getScore());
            return score;
        } else {
            Log.d(TAG, "人脸比对失败 " + compareFaceFeature);
        }
        return 0;
    }

人脸识别系统的思路大致如下:打开前置摄像头获取人脸数据,如果获取成功,则从数据库中去查询匹配这个人的人脸相似度,如果没有查询到数据或者相似度特别低可以认为人脸库中没有这个人,那就可以走注册流程;如果相似度在0.8以上可以视为查找到本人,那就是打卡成功。注意:打卡必须要检测到这个人是活体才能进行下一步比较,否则用一张照片也可以蒙混过关。

虹软底活体检测,我测过多次,真心nb,基本别想用其他手段代替真人。


欢迎大家批评指正!

有兴趣的加入Android工程师交流企鹅群:7520 16839 进群与大牛们一起讨论,还可获取Android高级架构资料、源码、笔记、视频

高级UI、Gradle、RxJava、小程序、Hybrid、移动架构、React Native、性能优化等全面的Android高级实践技术讲解性能优化架构思维导图,和BATJ面试题及答案!

免费分享给有需要的朋友,希望能够帮助一些在这个行业发展迷茫的,或者想系统深入提升以及困于瓶颈的朋友,在网上博客论坛等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我在这免费分享一些架构资料及给大家。希望在这些资料中都有你需要的内容。