开箱即用 Android人脸识别与比对功能封装

基于虹软算法实现人脸识别与对比功能

项目背景: 门禁打卡系统

定制Android设备带红外温度传感器,手机App + 后端服务 +门禁App。

1.手机App申请指定的工作之后需上传对应的人脸头像,经过处理图片,压缩,旋转,传递给后端校验人脸和分辨率,校验通过之后,以推送的形式发给门禁App.

2.门禁App读取待打卡人脸信息,下载全部人脸图片,以BGR的方式注册到人脸库,同时记录,同步人脸库的成功与失败,如果失败以推送的形式发给手机App,提示用户重新录制人脸。

3.员工到时间来门禁App打卡上班,需要识别匹配人脸,并测温通过之后,算一次成功的Check in。把员工打卡信息同步到服务器生成报表。

为什么选虹软是因为它的免费SDK版本很符合我们的情况,一年10000次免费注册额度,我们的设备总共不超过百台,感觉很适应于这些不上线应用市场,自定义设备的场景。

一. 虹软人脸算法库介绍

具体的文档,其实大家可以看官网的文档,这里介绍几个重点类对象与方法。

FaceServer:核心功能,用于注册人脸,检测人脸,比对人脸。 DrawHelper:绘制相关的人脸框和文本信息。 CameraHelper:用于相机的预览封装。

注册人脸的方式分为nv21和BGR,真正线上项目应该都是BGR吧。
项目的核心类如下:

二. 封装与使用

2.1 注册人脸

Demo中使用的本地Drawable中的图片,大家可以替换为自己的图片放入Drawable中。 正式环境应该是下载服务器的人脸图片注册到人脸库。

    private fun doRegister() {

        launchOnUI {
//            val failurePath = commContext().filesDir.absolutePath + File.separator + "failed"

            var successCount = 0
            val memberList = listOf(
                UserInfo("1", "chengxiao", R.drawable.a),
                UserInfo("2", "chenlu", R.drawable.b),
                UserInfo("3", "liukai", R.drawable.c),
                UserInfo("4", "leyunying", R.drawable.d),
                UserInfo("5", "fangjun", R.drawable.e),
                UserInfo("6", "huyu", R.drawable.f)
            )
            withContext(Dispatchers.IO) {
                memberList.forEachIndexed { index, bean ->

                    // 获取原始Bitmap
                    var bitmap = BitmapFactory.decodeResource(commContext().resources, bean.userAvatar)
                    if (bitmap == null) {
                        return@forEachIndexed
                    }

                    // 旋转角度创建新的图片
                    val width = bitmap.width
                    val height = bitmap.height
                    if (width > height) {
                        val matrix = Matrix()
                        matrix.postRotate(90F)
                        bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
                    }

                    // 图像对齐
                    bitmap = ArcSoftImageUtil.getAlignedBitmap(bitmap, true)
                    if (bitmap == null) {
                        //添加到失败文件夹中去
                        return@forEachIndexed
                    }

                    // bitmap转bgr24
                    val bgr24 = ArcSoftImageUtil.createImageData(bitmap.width, bitmap.height, ArcSoftImageFormat.BGR24)
                    val transformCode = ArcSoftImageUtil.bitmapToImageData(bitmap, bgr24, ArcSoftImageFormat.BGR24)
                    if (transformCode != ArcSoftImageUtilError.CODE_SUCCESS) {
                        return@forEachIndexed
                    }

                    //使用bgr24注册人脸信息
                    val success = FaceServer.getInstance().registerBgr24(
                        commContext(), bgr24,
                        bitmap.width, bitmap.height,
                        bean.userId + "-" + bean.userName  //保存到Face文件夹的文件名
                    )

                    if (!success) {
                        //添加到失败文件夹中去
                        bean.isRegistSuccess = false

                    } else {
                        bean.isRegistSuccess = true
                        successCount++
                    }

                    YYLogUtils.w("current register index :$index")
                }

            }

            val failureList = memberList.filter { !it.isRegistSuccess }
            toast("resigter success :$successCount failed:$failureList")

        }

    }

复制代码
2.2 预览与识别

注册成功之后进入人脸识别页面。 先初始化摄像头展示预览页面,然后开启人脸检测。 其核心是三个线程池的互相交互

    private lateinit var ftEngine: FaceEngine      //人脸检测引擎,用于预览帧人脸追踪
    private lateinit var frEngine: FaceEngine      //用于特征提取的引擎
    private lateinit var flEngine: FaceEngine      //活体检测引擎,用于预览帧人脸活体检测
复制代码

大致流程为,初始化预览Canera页面,这个大家应该都没什么问题,然后初始化camearHelper和faceHelper,在预览页面获取的nv21数据中查找是否有人脸,然后判断人脸是否是活体,并判断是否匹配到人脸库,内部加入重试机制,如果双方都返回true的情况下,才算识别成功。
核心代码如下:

扫描二维码关注公众号,回复: 14145795 查看本文章
    fun initCamera(activity: Activity, previewView: TextureView, faceRectView: FaceRectView) {

        /*
         * 人脸处理的监听回调,用于找出人脸,判断活体,对比人脸,共分三个引擎
         *  FT Engine  预览画面中查找出人脸
         *  FL Engine  判断指定的数据是否是活体
         *  FR Engine  人脸比对是否通过
         */
        val faceListener = object : FaceListener {
            override fun onFail(e: Exception?) {
                YYLogUtils.e("faceListener-onFail: " + e?.message)
            }

            //FR Engine -> 人脸比对完成结果回调
            override fun onFaceFeatureInfoGet(
                faceFeature: FaceFeature?, requestId: Int, errorCode: Int?,
                orignData: ByteArray?, faceInfo: FaceInfo?, width: Int, height: Int
            ) {
                //如果提取到了指定的人脸特征
                if (faceFeature != null) {

                    val liveness = livenessMap[requestId]
                    //不做活体检测的情况,直接搜索
                    if (!livenessDetect) {
                        searchFace(faceFeature, requestId, orignData, faceInfo, width, height)
                    } else if (liveness != null && liveness == LivenessInfo.ALIVE) {
                        searchFace(faceFeature, requestId, orignData, faceInfo, width, height)
                    } else {
                        if (requestFeatureStatusMap.containsKey(requestId)) {
                            //延时发射
                            Observable.timer(WAIT_LIVENESS_INTERVAL, TimeUnit.MILLISECONDS)
                                .subscribe(object : Observer<Long?> {
                                    var disposable: Disposable? = null

                                    override fun onSubscribe(d: Disposable) {
                                        disposable = d
                                        getFeatureDelayedDisposables.add(disposable!!)
                                    }

                                    override fun onNext(t: Long) {
                                        onFaceFeatureInfoGet(faceFeature, requestId, errorCode, orignData, faceInfo, width, height)
                                    }

                                    override fun onError(e: Throwable) {
                                    }

                                    override fun onComplete() {
                                        getFeatureDelayedDisposables.remove(disposable!!)
                                    }
                                })
                        }
                    }
                }
                //如果没有提取到特征表示特征提取失败
                else {
                    if (increaseAndGetValue(extractErrorRetryMap, requestId) > MAX_RETRY_TIME) {
                        extractErrorRetryMap[requestId] = 0
                        // 传入的FaceInfo在指定的图像上无法解析人脸,此处使用的是RGB人脸数据,一般是人脸模糊
                        val msg: String = if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) {
                            commContext().getString(R.string.low_confidence_level)
                        } else {
                            "ExtractCode:$errorCode"
                        }
                        faceHelper?.setName(requestId, commContext().getString(R.string.recognize_failed_notice, msg))
                        // 在尝试最大次数后,特征提取仍然失败,则认为识别未通过
                        requestFeatureStatusMap[requestId] = RequestFeatureStatus.FAILED
                        retryRecognizeDelayed(requestId)
                    } else {
                        requestFeatureStatusMap[requestId] = RequestFeatureStatus.TO_RETRY
                    }
                }
            }

            //FL Engine -> 是否是活体的回调处理
            override fun onFaceLivenessInfoGet(livenessInfo: LivenessInfo?, requestId: Int, errorCode: Int?) {
                if (livenessInfo != null) {
                    val liveness = livenessInfo.liveness
                    //有结果之后,重新储存这个人脸的活体状态
                    livenessMap[requestId] = liveness

                    // 非活体,重试
                    if (liveness == LivenessInfo.NOT_ALIVE) {
                        faceHelper!!.setName(requestId, commContext().getString(R.string.recognize_failed_notice, "NOT_ALIVE"))
                        // 延迟 FAIL_RETRY_INTERVAL 后,将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测
                        retryLivenessDetectDelayed(requestId)
                    }
                } else {
                    if (increaseAndGetValue(livenessErrorRetryMap, requestId) > MAX_RETRY_TIME) {
                        livenessErrorRetryMap[requestId] = 0
                        // 传入的FaceInfo在指定的图像上无法解析人脸,此处使用的是RGB人脸数据,一般是人脸模糊
                        val msg: String = if (errorCode != null && errorCode == ErrorInfo.MERR_FSDK_FACEFEATURE_LOW_CONFIDENCE_LEVEL) {
                            commContext().getString(R.string.low_confidence_level)
                        } else {
                            "ProcessCode:$errorCode"
                        }
                        faceHelper!!.setName(requestId, commContext().getString(R.string.recognize_failed_notice, msg))
                        retryLivenessDetectDelayed(requestId)
                    } else {
                        livenessMap[requestId] = LivenessInfo.UNKNOWN
                    }
                }
            }
        }

        //自定义相机监听器 - 开启相机监听 -预览数据nv21获取
        val cameraListener = object : CameraListener {
            override fun onCameraOpened(camera: Camera, cameraId: Int, displayOrientation: Int, isMirror: Boolean) {
                val lastPreviewSize = previewSize
                previewSize = camera.parameters.previewSize

                //绘制人脸框与文本的工具类初始化
                drawHelper = DrawHelper(
                    previewSize?.width ?: 0, previewSize?.height ?: 0, previewView.width,
                    previewView.height, displayOrientation, cameraId, isMirror, false, false
                )
                YYLogUtils.d("onCameraOpened: " + drawHelper.toString())

                // 切换相机的时候可能会导致预览尺寸发生变化
                if (faceHelper == null || lastPreviewSize == null || lastPreviewSize.width != previewSize?.width
                    || lastPreviewSize.height != previewSize?.height
                ) {
                    var trackedFaceCount: Int? = null

                    // 记录切换时的人脸序号
                    if (faceHelper != null) {
                        trackedFaceCount = faceHelper!!.trackedFaceCount
                        faceHelper!!.release()
                    }

                    //人脸处理工具类初始化,用于找出人脸,判断活体,对比人脸
                    faceHelper = FaceHelper.Builder()
                        .ftEngine(ftEngine)
                        .frEngine(frEngine)
                        .flEngine(flEngine)
                        .frQueueSize(MAX_DETECT_NUM)
                        .flQueueSize(MAX_DETECT_NUM)
                        .previewSize(previewSize)
                        .faceListener(faceListener)
                        .trackedFaceCount(trackedFaceCount ?: ConfigUtil.getTrackedFaceCount(CommUtils.getContext()))
                        .build()
                }
            }

            //摄像头画面的预览 - 获取到预览页面的nv21数据
            override fun onPreview(nv21: ByteArray, camera: Camera) {
                var startCheck = false

                faceRectView.clearFaceInfo()
                //人脸工具类处理数据流获取到人脸数据
                val facePreviewInfoList: List<FacePreviewInfo>? = faceHelper?.onPreviewFrame(nv21)
                if (!CheckUtil.isEmpty(facePreviewInfoList) && drawHelper != null) {
                    //如果有人脸,开始绘制人脸框与文本
                    val showRect = drawPreviewInfo(facePreviewInfoList!!, faceRectView)
                    showRect?.let {
                        val width = it.width()
                        if (width > 300) startCheck = true
                    }

                    //开启白色补光灯
                    openWhiteLight()
                    showNormalState()
                }

                //删除人脸数据,处理一些Map
                clearLeftFace(facePreviewInfoList)

                //限制人脸距离,比较近的时候开始检测
                if (!startCheck) return
                //开始检测活体与提取特征-内部加入一些状态判断
                if (!CheckUtil.isEmpty(facePreviewInfoList) && previewSize != null) {
                    for (i in facePreviewInfoList!!.indices) {
                        val status = requestFeatureStatusMap[facePreviewInfoList[i].trackId]
                        /**
                         * 在活体检测开启,在人脸识别状态不为成功或人脸活体状态不为处理中(ANALYZING)
                         * 且不为处理完成(ALIVE、NOT_ALIVE)时重新进行活体检测
                         */
                        if (livenessDetect && (status == null || status != RequestFeatureStatus.SUCCEED)) {
                            val liveness = livenessMap[facePreviewInfoList[i].trackId]
                            if (liveness == null || liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE
                                && liveness != RequestLivenessStatus.ANALYZING
                            ) {
                                //开始分析活体,先储存状态为分析中
                                livenessMap[facePreviewInfoList[i].trackId] = RequestLivenessStatus.ANALYZING
                                //人脸工具类调用方法开始分析活体,结果在Face回调中
                                faceHelper!!.requestFaceLiveness(
                                    nv21,
                                    facePreviewInfoList[i].faceInfo,
                                    previewSize!!.width,
                                    previewSize!!.height,
                                    FaceEngine.CP_PAF_NV21,
                                    facePreviewInfoList[i].trackId,
                                    LivenessType.RGB
                                )
                            }
                        }

                        /**
                         * 对于每个人脸,若状态为空或者为失败,则请求特征提取(可根据需要添加其他判断以限制特征提取次数),
                         * 特征提取回传的人脸特征结果在[FaceListener.onFaceFeatureInfoGet]中回传
                         */
                        if (status == null || status == RequestFeatureStatus.TO_RETRY) {
                            //开启分析人脸特征,先存储状态为搜索中
                            requestFeatureStatusMap[facePreviewInfoList[i].trackId] = RequestFeatureStatus.SEARCHING
                            //人脸工具类调用方法开启提前人脸特征
                            faceHelper!!.requestFaceFeature(
                                nv21,
                                facePreviewInfoList[i].faceInfo,
                                previewSize!!.width,
                                previewSize!!.height,
                                FaceEngine.CP_PAF_NV21,
                                facePreviewInfoList[i].trackId
                            )
                        }
                    }
                }
            }

            override fun onCameraClosed() {
                YYLogUtils.w("onCameraClosed: ")
            }

            override fun onCameraError(e: java.lang.Exception?) {
                YYLogUtils.e("onCameraError: " + e?.message)
            }

            override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
                drawHelper?.cameraDisplayOrientation = displayOrientation
                YYLogUtils.w("onCameraConfigurationChanged: $cameraID  $displayOrientation")
            }
        }

        cameraHelper = CameraHelper.Builder()
            .previewViewSize(Point(previewView.measuredWidth, previewView.measuredHeight))  //预览的宽高 最佳相机比例时用到
            .rotation(activity.windowManager.defaultDisplay.rotation)     //指定旋转角度 固定写法
            .specificCameraId(rgbCameraID)   //指定相机ID,这里指定前置
            .isMirror(false)         //是否开启前置镜像
            .previewOn(previewView) //预览容器 推荐TextureView
            .cameraListener(cameraListener) //设置自定义的监听器
            .build()
        cameraHelper?.init()
        cameraHelper?.start()
    }

复制代码

注意: 上面一个重要点是我通过人脸框的绘制大小,来判断人脸距离屏幕,因为需要红外测温,如果不在指定的距离,那么红外测温就不准确,体温会太高或者太低,如果大家不需要这个逻辑可以自行去掉。

2.3 绘制信息

如果大家有绘制人脸框方面的自定义需求,可以修改绘制的信息,或修改DrawHelp内的方法。

    private fun drawPreviewInfo(facePreviewInfoList: List<FacePreviewInfo>, faceRectView: FaceRectView): Rect? {
        val drawInfoList: MutableList<DrawInfo> = ArrayList()
        var rect: Rect? = null
        for (i in facePreviewInfoList.indices) {
            val name = faceHelper?.getName(facePreviewInfoList[i].trackId)
            val liveness = livenessMap[facePreviewInfoList[i].trackId]
            val recognizeStatus = requestFeatureStatusMap[facePreviewInfoList[i].trackId]

            // 根据识别结果和活体结果设置颜色
            var color: Int = RecognizeColor.COLOR_UNKNOWN
            if (recognizeStatus != null) {
                if (recognizeStatus == RequestFeatureStatus.FAILED) {
                    color = RecognizeColor.COLOR_FAILED
                }
                if (recognizeStatus == RequestFeatureStatus.SUCCEED) {
                    color = RecognizeColor.COLOR_SUCCESS
                }
            }
            if (liveness != null && liveness == LivenessInfo.NOT_ALIVE) {
                color = RecognizeColor.COLOR_FAILED
            }
            rect = drawHelper?.adjustRect(facePreviewInfoList[i].faceInfo.rect)
            //添加需要绘制的人脸信息
            drawInfoList.add(
                DrawInfo(
                    rect, GenderInfo.UNKNOWN, AgeInfo.UNKNOWN_AGE, liveness ?: LivenessInfo.UNKNOWN, color,
                    name ?: (facePreviewInfoList[i].trackId).toString()
                )
            )
        }

        //开启绘制
        drawHelper?.draw(faceRectView, drawInfoList)

        return rect
    }
复制代码
3.3 人脸的比对

内部包含一些比对成功或失败之后硬件控件的Api,大家不需要可以自行删除。

    /**
     * 在已经注册的待检测人脸中搜索指定人脸
     */
    private fun searchFace(
        frFace: FaceFeature, requestId: Int,
        orignData: ByteArray?, faceInfo: FaceInfo?, width: Int, height: Int
    ) {

        launchOnUI {

            val compareResult = withContext(Dispatchers.IO) {
                //直接调用Server方法获取比对之后的人脸,内部实现是SDK方法compareFaceFeature
                YYLogUtils.w("find FaceFeature :$frFace")
                val compareResult: CompareResult? = FaceServer.getInstance().getTopOfFaceLib(frFace)
                YYLogUtils.w("find compare result :$compareResult")
                return@withContext compareResult
            }

            if (compareResult?.userName == null) {
                requestFeatureStatusMap[requestId] = RequestFeatureStatus.FAILED
                faceHelper?.setName(requestId, "VISITOR1-$requestId")

                //开启红灯-代表失败
                openRedLight()

                retryRecognizeDelayed(requestId)
                return@launchOnUI
            }

            if (compareResult.similar > SIMILAR_THRESHOLD) {
                //满足相似度
                var isAdded = false
                if (compareResultList == null) {
                    requestFeatureStatusMap[requestId] = RequestFeatureStatus.FAILED
                    faceHelper?.setName(requestId, "VISITOR2-$requestId")
                    //开启红灯-代表失败
                    openRedLight()
                    return@launchOnUI
                }

                //排查重复数据
                for (compareResult1 in compareResultList) {
                    if (compareResult1.trackId == requestId) {
                        isAdded = true
                        break
                    }
                }

                if (!isAdded) {
                    //对于多人脸搜索,假如最大显示数量为 MAX_DETECT_NUM 且有新的人脸进入,则以队列的形式移除
                    if (compareResultList.size >= MAX_DETECT_NUM) {
                        compareResultList.removeAt(0)
//                      adapter.notifyItemRemoved(0)
                    }
                    //添加显示人员时,保存其trackId
                    compareResult.trackId = requestId
                    compareResultList.add(compareResult)
//                  adapter.notifyItemInserted(compareResultList.size - 1)
                }
                requestFeatureStatusMap[requestId] = RequestFeatureStatus.SUCCEED
                faceHelper?.setName(requestId, commContext().getString(R.string.recognize_success_notice, compareResult.userName))
                //开启绿灯-代表成功
                openGreenLight()

                //成功之后跳转新页面
                jumpSuccessPage(compareResult, orignData, faceInfo, width, height)

            } else {
                //相似度小于0.8不是一个人
                faceHelper?.setName(requestId, commContext().getString(R.string.recognize_failed_notice, "NOT_REGISTERED"))
                //开启红灯-代表失败
                openRedLight()

                retryRecognizeDelayed(requestId)
            }

        }

    }
复制代码

这里也是做了一些自定义操作,成功之后会把当前打卡的人脸的NV21数据存储起来,转换为bitmap,保存到file中,同步给服务器。让服务器知道当前打卡的人脸,这一点也是我们业务的需求,如果有自定义要求,也是可以自行删除。
Demo的两个页面:

点击本地人员注册,成功之后,再进入首页:

总结:

主要是CamearHelper的初始化,监听预览页面的人脸,然后使用drawhelper绘制相应的人脸框,查看人脸距离大小等数据满足条件之后,判断并启动活体检测与人脸匹配,成功之后把成功的nv21数据帧传输给服务器。

这里也只是放出了核心的一些类和方法,具体的推荐大家看看源码,开箱即用

注: 由于公司项目涉及到具体页面,这里放出的Demo只涉及到人脸注册,识别,匹配,活体,等相关的核心功能,具体的业务不方便开源。相信对各位高工来说也不是什么问题啦!OK完结

猜你喜欢

转载自juejin.im/post/7096350990151974943