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

语言: CN / TW / HK

theme: juejin highlight: a11y-dark


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

项目背景: 门禁打卡系统

定制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中。 正式环境应该是下载服务器的人脸图片注册到人脸库。 ```kotlin 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) {
                    [email protected]
                }

                // 旋转角度创建新的图片
                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) {
                    //添加到失败文件夹中去
                    [email protected]
                }

                // 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) {
                    [email protected]
                }

                //使用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 预览与识别

注册成功之后进入人脸识别页面。 先初始化摄像头展示预览页面,然后开启人脸检测。 其核心是三个线程池的互相交互 kotlin private lateinit var ftEngine: FaceEngine //人脸检测引擎,用于预览帧人脸追踪 private lateinit var frEngine: FaceEngine //用于特征提取的引擎 private lateinit var flEngine: FaceEngine //活体检测引擎,用于预览帧人脸活体检测 大致流程为,初始化预览Canera页面,这个大家应该都没什么问题,然后初始化camearHelper和faceHelper,在预览页面获取的nv21数据中查找是否有人脸,然后判断人脸是否是活体,并判断是否匹配到人脸库,内部加入重试机制,如果双方都返回true的情况下,才算识别成功。
核心代码如下: ```kotlin 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内的方法。 ```kotlin private fun drawPreviewInfo(facePreviewInfoList: List, faceRectView: FaceRectView): Rect? { val drawInfoList: MutableList = 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,大家不需要可以自行删除。 ```kotlin /* * 在已经注册的待检测人脸中搜索指定人脸 / 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")
            [email protected] compareResult
        }

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

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

            retryRecognizeDelayed(requestId)
            [email protected]
        }

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

            //排查重复数据
            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完结