android基于虹软的人脸识别+测温+道闸项目实现
前言:
最近应防疫要求,开发一套人脸识别+腕部测温+身份证+健康码通行的道闸项目,人脸识别采用的是虹软人脸识别算法。以人脸识别+测温、刷身份证+测温、刷健康码+测温为开门条件。(文章末尾附源码)
软硬件环境
平台为Android平台,采用kotlin+java混编
虹软SDK版本为最新的4.0可以戴口罩识别
终端摄像头采用双目摄像头模组IR活体识别
扫码头、测温头、身份证读卡器皆为本公司设备,就不一一介绍了
UI界面和机器展示
![](https://img.haomeiwen.com/i4411166/61aac11e754ebd45.png)
使用说明
人脸识别通过后自动测温,然后向后台上传温度和人员信息,后台判断温度是否异常,并且保存人员通行记录
项目总体流程
人脸注册:
人脸注册采用另一种终端和小程序注册两种方式,这里只说小程序。
用户使用小程序采集人脸照片上传至服务器-->人脸终端起服务定时向服务端请求终端没有注册过的人脸-->终端拿到人脸照片之后注册至本地。另外定时请求需要删除和更改的人脸信息,然后本地做删除更改操作。(直接同步人脸照片而不是特征值是因为虹软目前没有小程序的人脸识别sdk)
开门条件
以人脸识别+测温、刷身份证+测温、刷健康码+测温为开门条件。
本文主要讲解人脸+测温
项目主要类介绍
PullDataServerHelper
拉取人脸信息帮助类,实现了拿到信息之后注册人脸、删除人脸、更改信息的操作
DataSyncService
数据同步服务,此类为server,主要功能是定时调用PullDataServerHelper做网络请求
facedb包
此包中为数据库操作相关文件,本项目数据操作使用greendao,不了解的可以了解一下,非常好用。
项目的一些东西就先说这么多,文章最后会附上源码,接下来着重讲一些虹软SDK的使用
人脸识别部分(核心代码)
1.sdk的激活
SDK为一次激活永久使用,不可多次激活,本文使用在线激活的方式,后端录入终端绑定激活码,app带着终端唯一标识向后端请求激活码。
激活之前先判断是否已经激活,没有激活才继续激活操作,下面为代码:
fun Active() {
//获取激活文件
val activeFileInfo = ActiveFileInfo()
val code = FaceEngine.getActiveFileInfo(mContext, activeFileInfo)
if (code == ErrorInfo.MOK) {
//已经激活
isActive.value = true
return
} else {
//未激活 读取本地存储的激活码
var sdkKey = readString(
mContext,
Constants.APP_SDK_KEY
)
var appId = readString(
mContext,
Constants.APP_ID_KEY
)
var activeKey = readString(
mContext,
Constants.APP_ACTIVE_KEY
)
if (sdkKey.isNullOrEmpty()) {
//本地无激活码 从网络获取
getSdkInfo()
} else {
val code1 = FaceEngine.activeOnline(
mContext,
activeKey,
appId,
sdkKey
)
if (code1 == ErrorInfo.MOK) {
isActive.value = true
return
} else {
getSdkInfo()
}
}
}
}
private fun getSdkInfo() {
RetrofitManager.getInstance().createReq(ApiServer::class.java)
.getSdkInfo(AppUtils.getMac())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : BaseObserver<SdkInfoResult>() {
override fun onSuccees(data: SdkInfoResult) {
if (data.code == 200 && null != data.data) {
write(mContext, Constants.APP_SDK_KEY, data.data.SdkKey)
write(mContext, Constants.APP_ID_KEY, data.data.AppId)
write(mContext, Constants.APP_ACTIVE_KEY, data.data.ActiveKey)
val code1 = FaceEngine.activeOnline(
mContext,
data.data.activeKey,
data.data.appId,
data.data.sdkKey
)
if (code1 == ErrorInfo.MOK) {
isActive.value = true
return
} else {
isActive.value = false
}
}
}
override fun onFailure(message: String?) {
isActive.value = false
}
})
}
2、sdk初始化
初始化的各个属性官方文档都有详细讲解,这里就不赘述了
public void init() {
Context context = CustomApplication.Companion.getMContext();
FaceServer.getInstance().init(context);
ftEngine = new FaceEngine();
int ftEngineMask = FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT;
int ftCode = ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, DetectFaceOrientPriority.ASF_OP_90_ONLY, FaceConfig.RECOGNIZE_MAX_DETECT_FACENUM, ftEngineMask);
ftInitCode.postValue(ftCode);
frEngine = new FaceEngine();
int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION;
if (FaceConfig.ENABLE_FACE_QUALITY_DETECT) {
frEngineMask |= FaceEngine.ASF_IMAGEQUALITY;
}
int frCode = frEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_90_ONLY,
10, frEngineMask);
frInitCode.postValue(frCode);
//启用活体检测时,才初始化活体引擎
int flCode = -1;
if (FaceConfig.ENABLE_LIVENESS) {
flEngine = new FaceEngine();
int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT));
if (needUpdateFaceData) {
flEngineMask |= FaceEngine.ASF_UPDATE_FACEDATA;
}
flCode = flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE,
DetectFaceOrientPriority.ASF_OP_90_ONLY, FaceConfig.RECOGNIZE_MAX_DETECT_FACENUM, flEngineMask);
flInitCode.postValue(flCode);
LivenessParam livenessParam = new LivenessParam(FaceConfig.RECOMMEND_RGB_LIVENESS_THRESHOLD, FaceConfig.RECOMMEND_IR_LIVENESS_THRESHOLD);
flEngine.setLivenessParam(livenessParam);
}
if (ftCode == ErrorInfo.MOK && frCode == ErrorInfo.MOK && flCode == ErrorInfo.MOK) {
Constants.isInitEnt = true;
}
}
人脸注册
public FaceEntity registerJpeg(Context context, FaceImageResult.DataBean data) throws RegisterFailedException {
if (faceRegisterInfoList != null && faceRegisterInfoList.size() >= MAX_REGISTER_FACE_COUNT) {
Log.e(TAG, "registerJpeg: registered face count limited " + faceRegisterInfoList.size());
// 已达注册上限,超过该值会影响识别率
throw new RegisterFailedException("registered face count limited");
}
Bitmap bitmap = ImageUtil.jpegToScaledBitmap( Base64.decode(data.getImage(), Base64.DEFAULT), ImageUtil.DEFAULT_MAX_WIDTH, ImageUtil.DEFAULT_MAX_HEIGHT);
bitmap = ArcSoftImageUtil.getAlignedBitmap(bitmap, true);
byte[] imageData = ArcSoftImageUtil.createImageData(bitmap.getWidth(), bitmap.getHeight(), ArcSoftImageFormat.BGR24);
int code = ArcSoftImageUtil.bitmapToImageData(bitmap, imageData, ArcSoftImageFormat.BGR24);
if (code != ArcSoftImageUtilError.CODE_SUCCESS) {
throw new RuntimeException("bitmapToImageData failed, code is " + code);
}
return registerBgr24(context, imageData, bitmap.getWidth(), bitmap.getHeight(), data);
}
/**
* 用于注册照片人脸
*
* @param context 上下文对象
* @param bgr24 bgr24数据
* @param width bgr24宽度
* @param height bgr24高度
* @param name 保存的名字,若为空则使用时间戳
* @return 注册成功后的人脸信息
*/
public FaceEntity registerBgr24(Context context, byte[] bgr24, int width, int height, String name,String idCard) {
if (faceEngine == null || context == null || bgr24 == null || width % 4 != 0 || bgr24.length != width * height * 3) {
Log.e(TAG, "registerBgr24: invalid params");
return null;
}
//人脸检测
List<FaceInfo> faceInfoList = new ArrayList<>();
int code;
synchronized (faceEngine) {
code = faceEngine.detectFaces(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList);
}
if (code == ErrorInfo.MOK && !faceInfoList.isEmpty()) {
code = faceEngine.process(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList,
FaceEngine.ASF_MASK_DETECT);
if (code == ErrorInfo.MOK) {
List<MaskInfo> maskInfoList = new ArrayList<>();
faceEngine.getMask(maskInfoList);
if (!maskInfoList.isEmpty()) {
int isMask = maskInfoList.get(0).getMask();
if (isMask == MaskInfo.WORN) {
/*
* 注册照要求不戴口罩
*/
Log.e(TAG, "registerBgr24: maskInfo is worn");
return null;
}
}
}
FaceFeature faceFeature = new FaceFeature();
/*
* 特征提取,注册人脸时参数extractType值为ExtractType.REGISTER,参数mask的值为MaskInfo.NOT_WORN
*/
synchronized (faceEngine) {
code = faceEngine.extractFaceFeature(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList.get(0),
ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature);
}
String userName = name == null ? String.valueOf(System.currentTimeMillis()) : name;
//保存注册结果(注册图、特征数据)
if (code == ErrorInfo.MOK) {
//为了美观,扩大rect截取注册图
Rect cropRect = getBestRect(width, height, faceInfoList.get(0).getRect());
if (cropRect == null) {
Log.e(TAG, "registerBgr24: cropRect is null");
return null;
}
cropRect.left &= ~3;
cropRect.top &= ~3;
cropRect.right &= ~3;
cropRect.bottom &= ~3;
String imgPath = getImagePath(userName);
// 创建一个头像的Bitmap,存放旋转结果图
Bitmap headBmp = getHeadImage(bgr24, width, height, faceInfoList.get(0).getOrient(), cropRect, ArcSoftImageFormat.BGR24);
try {
FileOutputStream fos = new FileOutputStream(imgPath);
headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
fos.close();
} catch (IOException e) {
e.printStackTrace();
return null;
}
// 内存中的数据同步
if (faceRegisterInfoList == null) {
faceRegisterInfoList = new ArrayList<>();
}
FaceEntity faceEntity = new FaceEntity(name,idCard, imgPath, faceFeature.getFeatureData(),0L);
//判断是否存在这个人,如果存在覆盖,否则新增(解决 因重置人脸删除和注册同事进行问题)
if (faceRegisterInfoList.contains(faceEntity)) {
faceRegisterInfoList.remove(faceEntity);
List<FaceEntity> faceEntities = GreendaoUtils.Companion.getGreendaoUtils().searchFaceForIdcard(idCard);
if (faceEntities == null || faceEntities.isEmpty()) {
long faceId = GreendaoUtils.Companion.getGreendaoUtils().insert(faceEntity);
faceEntity.setFaceId(faceId);
}else {
faceEntities.get(0).setFeatureData(faceFeature.getFeatureData());
GreendaoUtils.Companion.getGreendaoUtils().update(faceEntities.get(0));
}
} else {
long faceId = GreendaoUtils.Companion.getGreendaoUtils().insert(faceEntity);
faceEntity.setFaceId(faceId);
}
faceRegisterInfoList.add(faceEntity);
return faceEntity;
} else {
Log.e(TAG, "registerBgr24: extract face feature failed, code is " + code);
return null;
}
} else {
Log.e(TAG, "registerBgr24: no face detected, code is " + code);
return null;
}
}
人脸搜索
/**
* 在特征库中搜索
*
* @param faceFeature 传入特征数据
* @return 比对结果
*/
public CompareResult getTopOfFaceLib(FaceFeature faceFeature) {
if (faceEngine == null || faceFeature == null || faceRegisterInfoList == null || faceRegisterInfoList.isEmpty()) {
return null;
}
long start = System.currentTimeMillis();
FaceFeature tempFaceFeature = new FaceFeature();
FaceSimilar faceSimilar = new FaceSimilar();
float maxSimilar = 0;
int maxSimilarIndex = -1;
int code = ErrorInfo.MOK;
synchronized (searchLock) {
for (int i = 0; i < faceRegisterInfoList.size(); i++) {
tempFaceFeature.setFeatureData(faceRegisterInfoList.get(i).getFeatureData());
code = faceEngine.compareFaceFeature(faceFeature, tempFaceFeature, faceSimilar);
if (faceSimilar.getScore() > maxSimilar) {
maxSimilar = faceSimilar.getScore();
maxSimilarIndex = i;
}
}
}
if (maxSimilarIndex != -1) {
return new CompareResult(faceRegisterInfoList.get(maxSimilarIndex), maxSimilar, code, System.currentTimeMillis() - start);
}
return null;
}
测温部分(核心代码)
测温头我们使用usb连接测温头,采用简单的usb 模拟键盘的方式,测温头测到温度模拟键盘输入到终端的文本框中,代码监听键盘输入读取温度。当然也可以通过串口连接测温头,主动发指令操作测温头测温,这里我采用的是模拟键盘的方式。
public class ReadTemperatureHelper {
private StringBuffer mStringBufferResult; //扫描内容
private boolean mCaps;
private boolean isCtrl;//大小写
private OnReadSuccessListener onReadSuccessListener;
public ReadTemperatureHelper(OnReadSuccessListener onReadSuccessListener) {
this.onReadSuccessListener = onReadSuccessListener;
mStringBufferResult = new StringBuffer();
}
/**
* 事件解析
*
* @param event
*/
public void analysisKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
//判断字母大小写
checkLetterStatus(event);
checkInputEnt(event);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
char aChar = getInputCode(event);
if (aChar != 0) {
mStringBufferResult.append(aChar);
}
Log.i("123123", "keyCode:" + keyCode);
if (keyCode == KeyEvent.KEYCODE_ENTER) {
//回车键 返回
Log.i("123123", "dispatchKeyEvent:" + mStringBufferResult.toString());
String s = mStringBufferResult.toString();
// int i = s.lastIndexOf(":");
// String substring = s.substring(i);
// String[] s1 = substring.split(" ");
Log.i("123123", "体温为:" + s);
onReadSuccessListener.onReadSuccess(s.trim());
mStringBufferResult.setLength(0);
}
}
}
/**
* ctrl
*/
private void checkInputEnt(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_CTRL_LEFT) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
isCtrl = true;
} else {
isCtrl = false;
}
}
}
/**
* shift键
*
* @param keyEvent
*/
private void checkLetterStatus(KeyEvent keyEvent) {
int keyCode = keyEvent.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
//按住shift键 大写
mCaps = true;
} else {
//小写
mCaps = false;
}
}
}
/**
* 获取扫描内容
*
* @param keyEvent
* @return
*/
private char getInputCode(KeyEvent keyEvent) {
char aChar;
int keyCode = keyEvent.getKeyCode();
Log.i("TAGKEYCODE", keyCode + "");
if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= keyEvent.KEYCODE_Z)//29< keycode <54
{
//字母
aChar = (char) ((mCaps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A);//
} else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
//数字
if (mCaps)//是否按住了shift键
{
//按住了 需要将数字转换为对应的字符
switch (keyCode) {
case KeyEvent.KEYCODE_0:
aChar = ')';
break;
case KeyEvent.KEYCODE_1:
aChar = '!';
break;
case KeyEvent.KEYCODE_2:
aChar = '@';
break;
case KeyEvent.KEYCODE_3:
aChar = '#';
break;
case KeyEvent.KEYCODE_4:
aChar = '$';
break;
case KeyEvent.KEYCODE_5:
aChar = '%';
break;
case KeyEvent.KEYCODE_6:
aChar = '^';
break;
case KeyEvent.KEYCODE_7:
aChar = '&';
break;
case KeyEvent.KEYCODE_8:
aChar = '*';
break;
case KeyEvent.KEYCODE_9:
aChar = '(';
break;
default:
aChar = ' ';
break;
}
} else {
aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
}
} else {
//其他符号
switch (keyCode) {
case KeyEvent.KEYCODE_PERIOD:
aChar = '.';
break;
case KeyEvent.KEYCODE_MINUS:
aChar = mCaps ? '_' : '-';
break;
case KeyEvent.KEYCODE_SLASH:
aChar = '/';
break;
case KeyEvent.KEYCODE_STAR:
aChar = '*';
break;
case KeyEvent.KEYCODE_POUND:
aChar = '#';
break;
case KeyEvent.KEYCODE_SEMICOLON:
aChar = mCaps ? ':' : ';';
break;
case KeyEvent.KEYCODE_AT:
aChar = '@';
break;
case KeyEvent.KEYCODE_BACKSLASH:
aChar = mCaps ? '|' : '\\';
break;
default:
aChar = ' ';
break;
}
}
return aChar;
}
public interface OnReadSuccessListener {
void onReadSuccess(String temperature);
}
}
在activity的dispatchKeyEvent方法,监听键盘输入事件
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
if (isReadTemp) {
read.analysisKeyEvent(event)
if (event!!.keyCode == KeyEvent.KEYCODE_ENTER) {
return true
}
}
return super.dispatchKeyEvent(event)
}
开门(继电器方式 核心代码)
public void openG() {
String status = "1";
try {
FileOutputStream fos = new FileOutputStream("/sys/exgpio/relay1");
fos.write(status.getBytes());
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
String status1 = "0";
SystemClock.sleep(200);
try {
FileOutputStream fos = new FileOutputStream("/sys/exgpio/relay1");
fos.write(status1.getBytes());
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
常见问题
本项目的开发和使用中也遇到了很多问题,我认为比较值得注意的有两个
1、室外复杂环境下,存在人脸识别久久不通过问题
这个问题不是偶发的问题,经过反复测试,室外较为昏暗的光线下,因为开启了ir红外活体检测,存在热源光不足导致活体检测不通过
2、室外环境导致测温不准确
这个问题是红外测温技术原理导致的,因为室外温度过高或者过低无法保证测温准确率,或者测不到温度。目前没有解决方案,后期会测量整个人脸框这以区域每个点的温度,作一定补偿取平均值。
补充
上述简单的罗列了一些核心的代码块,后面附源码,源码中有详细的业务代码,包含读身份证和扫码,因为身份证读卡器是公司产品,与其它的身份证读卡器读卡sdk不一样,所以删除了读卡sdk,业务代码保留。
读到身份证后回去后台验证此人健康码状态,然后确定是否开门
读取健康码使用串口读取,代码里有写,读到健康码后,去后台验证此健康码状态确认是否开门
因为测试需要,所以健康码部分代码注释掉了,项目中随机给的温度以便测试
源码中还有很多逻辑和东西没有写,直接获取源码吧
欢迎交流