前言:
最近應防疫要求,開發一套人臉識別+腕部測溫+身份證+健康碼通行的道閘項目,人臉識別採用的是虹軟人臉識別算法。以人臉識別+測溫、刷身份證+測溫、刷健康碼+測溫爲開門條件。(文章末尾附源碼)
軟硬件環境
平臺爲Android平臺,採用kotlin+java混編 虹軟SDK版本爲最新的4.0可以戴口罩識別 終端攝像頭採用雙目攝像頭模組IR活體識別 掃碼頭、測溫頭、身份證讀卡器皆爲本公司設備,就不一一介紹了
UI界面和機器展示
使用說明
人臉識別通過後自動測溫,然後向後臺上傳溫度和人員信息,後臺判斷溫度是否異常,並且保存人員通行記錄
項目總體流程
人臉註冊: 人臉註冊採用另一種終端和小程序註冊兩種方式,這裏只說小程序。 用戶使用小程序採集人臉照片上傳至服務器-->人臉終端起服務定時向服務端請求終端沒有註冊過的人臉-->終端拿到人臉照片之後註冊至本地。另外定時請求需要刪除和更改的人臉信息,然後本地做刪除更改操作。(直接同步人臉照片而不是特徵值是因爲虹軟目前沒有小程序的人臉識別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,業務代碼保留。
讀到身份證後回去後臺驗證此人健康碼狀態,然後確定是否開門
讀取健康碼使用串口讀取,代碼裏有寫,讀到健康碼後,去後臺驗證此健康碼狀態確認是否開門
因爲測試需要,所以健康碼部分代碼註釋掉了,項目中隨機給的溫度以便測試
源碼中還有很多邏輯和東西沒有寫,直接獲取源碼吧
瞭解更多人臉識別產品相關內容請到虹軟視覺開放平臺哦