本文已参与「新人创作礼」活动,一起开启掘金创作之路。
基于虹软算法实现人脸识别与对比功能
项目背景: 门禁打卡系统,
定制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的情况下,才算识别成功。
核心代码如下:
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完结
今天的文章开箱即用 Android人脸识别与比对功能封装分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23471.html