您好,登錄后才能下訂單哦!
本篇內(nèi)容主要講解“android基于虹軟的人臉識(shí)別+測(cè)溫+道閘項(xiàng)目的實(shí)現(xiàn)方法”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“android基于虹軟的人臉識(shí)別+測(cè)溫+道閘項(xiàng)目的實(shí)現(xiàn)方法”吧!
平臺(tái)為Android平臺(tái),采用kotlin+java混編 虹軟SDK版本為最新的4.0可以戴口罩識(shí)別 終端攝像頭采用雙目攝像頭模組IR活體識(shí)別 掃碼頭、測(cè)溫頭、身份證讀卡器皆為本公司設(shè)備,就不一一介紹了
人臉識(shí)別通過(guò)后自動(dòng)測(cè)溫,然后向后臺(tái)上傳溫度和人員信息,后臺(tái)判斷溫度是否異常,并且保存人員通行記錄
人臉注冊(cè): 人臉注冊(cè)采用另一種終端和小程序注冊(cè)兩種方式,這里只說(shuō)小程序。 用戶使用小程序采集人臉照片上傳至服務(wù)器-->人臉終端起服務(wù)定時(shí)向服務(wù)端請(qǐng)求終端沒(méi)有注冊(cè)過(guò)的人臉-->終端拿到人臉照片之后注冊(cè)至本地。另外定時(shí)請(qǐng)求需要?jiǎng)h除和更改的人臉信息,然后本地做刪除更改操作。(直接同步人臉照片而不是特征值是因?yàn)楹畿浤壳皼](méi)有小程序的人臉識(shí)別sdk)
開(kāi)門條件 以人臉識(shí)別+測(cè)溫、刷身份證+測(cè)溫、刷健康碼+測(cè)溫為開(kāi)門條件。 本文主要講解人臉+測(cè)溫
PullDataServerHelper 拉取人臉信息幫助類,實(shí)現(xiàn)了拿到信息之后注冊(cè)人臉、刪除人臉、更改信息的操作
DataSyncService 數(shù)據(jù)同步服務(wù),此類為server,主要功能是定時(shí)調(diào)用PullDataServerHelper做網(wǎng)絡(luò)請(qǐng)求
facedb包 此包中為數(shù)據(jù)庫(kù)操作相關(guān)文件,本項(xiàng)目數(shù)據(jù)操作使用greendao,不了解的可以了解一下,非常好用。
項(xiàng)目的一些東西就先說(shuō)這么多,文章最后會(huì)附上源碼,接下來(lái)著重講一些虹軟SDK的使用
1.sdk的激活
SDK為一次激活永久使用,不可多次激活,本文使用在線激活的方式,后端錄入終端綁定激活碼,app帶著終端唯一標(biāo)識(shí)向后端請(qǐng)求激活碼。 激活之前先判斷是否已經(jīng)激活,沒(méi)有激活才繼續(xù)激活操作,下面為代碼:
fun Active() { //獲取激活文件 val activeFileInfo = ActiveFileInfo() val code = FaceEngine.getActiveFileInfo(mContext, activeFileInfo) if (code == ErrorInfo.MOK) { //已經(jīng)激活 isActive.value = true return } else { //未激活 讀取本地存儲(chǔ)的激活碼 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()) { //本地?zé)o激活碼 從網(wǎng)絡(luò)獲取 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初始化 初始化的各個(gè)屬性官方文檔都有詳細(xì)講解,這里就不贅述了
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); //啟用活體檢測(cè)時(shí),才初始化活體引擎 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; } }
人臉注冊(cè)
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()); // 已達(dá)注冊(cè)上限,超過(guò)該值會(huì)影響識(shí)別率 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); } /** * 用于注冊(cè)照片人臉 * * @param context 上下文對(duì)象 * @param bgr24 bgr24數(shù)據(jù) * @param width bgr24寬度 * @param height bgr24高度 * @param name 保存的名字,若為空則使用時(shí)間戳 * @return 注冊(cè)成功后的人臉信息 */ 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; } //人臉檢測(cè) 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) { /* * 注冊(cè)照要求不戴口罩 */ Log.e(TAG, "registerBgr24: maskInfo is worn"); return null; } } } FaceFeature faceFeature = new FaceFeature(); /* * 特征提取,注冊(cè)人臉時(shí)參數(shù)extractType值為ExtractType.REGISTER,參數(shù)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; //保存注冊(cè)結(jié)果(注冊(cè)圖、特征數(shù)據(jù)) if (code == ErrorInfo.MOK) { //為了美觀,擴(kuò)大rect截取注冊(cè)圖 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); // 創(chuàng)建一個(gè)頭像的Bitmap,存放旋轉(zhuǎn)結(jié)果圖 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; } // 內(nèi)存中的數(shù)據(jù)同步 if (faceRegisterInfoList == null) { faceRegisterInfoList = new ArrayList<>(); } FaceEntity faceEntity = new FaceEntity(name,idCard, imgPath, faceFeature.getFeatureData(),0L); //判斷是否存在這個(gè)人,如果存在覆蓋,否則新增(解決 因重置人臉刪除和注冊(cè)同事進(jìn)行問(wèn)題) 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; } }
人臉?biāo)阉?/p>
/** * 在特征庫(kù)中搜索 * * @param faceFeature 傳入特征數(shù)據(jù) * @return 比對(duì)結(jié)果 */ 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; }
測(cè)溫頭我們使用usb連接測(cè)溫頭,采用簡(jiǎn)單的usb 模擬鍵盤的方式,測(cè)溫頭測(cè)到溫度模擬鍵盤輸入到終端的文本框中,代碼監(jiān)聽(tīng)鍵盤輸入讀取溫度。當(dāng)然也可以通過(guò)串口連接測(cè)溫頭,主動(dòng)發(fā)指令操作測(cè)溫頭測(cè)溫,這里我采用的是模擬鍵盤的方式。
public class ReadTemperatureHelper { private StringBuffer mStringBufferResult; //掃描內(nèi)容 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; } } } /** * 獲取掃描內(nèi)容 * * @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) { //數(shù)字 if (mCaps)//是否按住了shift鍵 { //按住了 需要將數(shù)字轉(zhuǎn)換為對(duì)應(yīng)的字符 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 { //其他符號(hào) 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方法,監(jiān)聽(tīng)鍵盤輸入事件
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(); } }
本項(xiàng)目的開(kāi)發(fā)和使用中也遇到了很多問(wèn)題,我認(rèn)為比較值得注意的有兩個(gè) 1、室外復(fù)雜環(huán)境下,存在人臉識(shí)別久久不通過(guò)問(wèn)題 這個(gè)問(wèn)題不是偶發(fā)的問(wèn)題,經(jīng)過(guò)反復(fù)測(cè)試,室外較為昏暗的光線下,因?yàn)殚_(kāi)啟了ir紅外活體檢測(cè),存在熱源光不足導(dǎo)致活體檢測(cè)不通過(guò)
2、室外環(huán)境導(dǎo)致測(cè)溫不準(zhǔn)確 這個(gè)問(wèn)題是紅外測(cè)溫技術(shù)原理導(dǎo)致的,因?yàn)槭彝鉁囟冗^(guò)高或者過(guò)低無(wú)法保證測(cè)溫準(zhǔn)確率,或者測(cè)不到溫度。目前沒(méi)有解決方案,后期會(huì)測(cè)量整個(gè)人臉框這以區(qū)域每個(gè)點(diǎn)的溫度,作一定補(bǔ)償取平均值。
上述簡(jiǎn)單的羅列了一些核心的代碼塊,后面附源碼,源碼中有詳細(xì)的業(yè)務(wù)代碼,包含讀身份證和掃碼,因?yàn)樯矸葑C讀卡器是公司產(chǎn)品,與其它的身份證讀卡器讀卡sdk不一樣,所以刪除了讀卡sdk,業(yè)務(wù)代碼保留。
讀到身份證后回去后臺(tái)驗(yàn)證此人健康碼狀態(tài),然后確定是否開(kāi)門
讀取健康碼使用串口讀取,代碼里有寫,讀到健康碼后,去后臺(tái)驗(yàn)證此健康碼狀態(tài)確認(rèn)是否開(kāi)門
因?yàn)闇y(cè)試需要,所以健康碼部分代碼注釋掉了,項(xiàng)目中隨機(jī)給的溫度以便測(cè)試
到此,相信大家對(duì)“android基于虹軟的人臉識(shí)別+測(cè)溫+道閘項(xiàng)目的實(shí)現(xiàn)方法”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。