@file:Suppress("DEPRECATION") package com.sw.platecabinet.activity import android.Manifest import android.content.Intent import android.graphics.Bitmap import android.graphics.Point import android.hardware.Camera import android.net.Uri import android.os.Build import android.provider.Settings import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.arcsoft.face.ErrorInfo import com.bumptech.glide.Glide import com.google.gson.Gson import com.sw.inbound.utils.DateTimeUtils import com.sw.plate.utils.Base64 import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.arcface.ConfigUtil import com.sw.plate.utils.arcface.ErrorCodeUtil import com.sw.plate.utils.arcface.FaceRectTransformer import com.sw.plate.utils.arcface.FaceRectView import com.sw.plate.utils.arcface.FaceRectView.DrawInfo import com.sw.plate.utils.arcface.PreviewConfig import com.sw.plate.utils.arcface.camera.CameraListener import com.sw.plate.utils.arcface.camera.DualCameraHelper import com.sw.plate.utils.arcface.face.constants.LivenessType import com.sw.plate.utils.arcface.face.constants.RecognizeColor import com.sw.plate.utils.arcface.face.model.CompareResult import com.sw.plate.utils.arcface.face.model.FacePreviewInfo import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration import com.sw.plate.utils.arcface.facedb.FaceDatabase import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.model.UserFaceInfo import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_READY import com.sw.platecabinet.R import com.sw.platecabinet.databinding.ActivityLoginFaceBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.dialog.CustomDialog import com.sw.platecabinet.ext.clickWithDebounce import com.sw.platecabinet.ext.gone import com.sw.platecabinet.ext.visible import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.utils.BitmapSaver import com.sw.platecabinet.utils.CountDownUtil import com.sw.platecabinet.utils.Debouncer import com.sw.platecabinet.utils.LogFileUtil import com.sw.platecabinet.utils.RegisterCallbackHandler import com.sw.platecabinet.utils.SoundPoolUtil import com.sw.platecabinet.utils.mego.PlcCallback import com.sw.platecabinet.utils.mego.PlcHelper import com.sw.platecabinet.utils.mego.PlcStatus import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import timber.log.Timber import java.io.File import java.util.concurrent.atomic.AtomicBoolean /** * 人脸识别 */ class LoginByFaceActivity : BaseActivity(), ViewTreeObserver.OnGlobalLayoutListener { override fun inflateViewBinding(): ActivityLoginFaceBinding { return ActivityLoginFaceBinding.inflate(layoutInflater) } override fun inflateTitleBinding(): ItemTitleTimeBinding { return binding.includeHeader } companion object { private const val TAG = "LoginByFaceActivity" private const val COLLECT_SUCCESS = "collect_success" private const val RECOGNIZE_SUCCESS = "recognize_success" private const val NO_PLATE_REMIND = "no_plate_remind" } override fun initialize() { userViewModel.activeEngine() PlcHelper.getInstance(this).openSerialPort(object : PlcCallback { override fun onSuccess(result: String) { ToastUtils.showToast("打开串口 → 成功: ${result}") } override fun onError(message: String) { ToastUtils.showToast("打开串口 → 失败: $message") } }) initArcViewModel() initArcView() openRectInfoDraw = true recognizeViewModel.setDrawRectInfoTextValue(true) EventBus.getDefault().register(this) //开启人脸增量数据定时任务 startFaceTask { loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}") } val soundMap = hashMapOf() soundMap[COLLECT_SUCCESS] = R.raw.collect_success soundMap[RECOGNIZE_SUCCESS] = R.raw.recognize_success soundMap[NO_PLATE_REMIND] = R.raw.no_plate_remind SoundPoolUtil.getInstance().loadR(this, soundMap) binding.btnCollect.clickWithDebounce { //先重置,后采集 resetRecognizeState() //采集数据按钮 retryFailCount = 0 isCollectFace = true isCollecting.set(true) collectFace() } binding.btnRetry.clickWithDebounce { //重新识别按钮 resetRecognizeState() } } // 检查相机权限 private fun checkCameraPermission() { Timber.i("checkCameraPermission") val hasCameraPermission = ContextCompat.checkSelfPermission( this, Manifest.permission.CAMERA ) == android.content.pm.PackageManager.PERMISSION_GRANTED if (hasCameraPermission) { // 权限已授予 loadLogInfo("checkCameraPermission 权限已允许") recognizeViewModel.init() initRgbCamera() resumeCamera() } else { // 无权限,请求相机权限 requestCameraPermission() } } private fun requestCameraPermission() { requestPermission(Manifest.permission.CAMERA) { isGranted -> if (isGranted) { // 权限已授予 loadLogInfo("checkCameraPermission 权限已允许") recognizeViewModel.init() initRgbCamera() resumeCamera() } else { // 权限被拒绝 loadLogInfo("checkCameraPermission 权限被拒绝") CustomDialog( activity = this, content = "请在设置中开启相机权限", onConfirm = { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.fromParts("package", context.packageName, null) } startActivity(intent) { checkCameraPermission() } }, onCancel = { }).show() } } } @Subscribe(threadMode = ThreadMode.MAIN) fun getFaceEntity(insertEntity: FaceEntity) { Timber.tag("performSync").e("-time=%s", insertEntity.registerTime) recognizeViewModel.addFace(insertEntity) Timber.tag("performSync over").e("-time=%s", insertEntity.registerTime) } override fun onLeftDoubleClick() { finish() } override fun onRightDoubleClick() { PlcSettingActivity.start(this) } private var isRecognition = false private var rgbCameraHelper: DualCameraHelper? = null private var rgbFaceRectTransformer: FaceRectTransformer? = null private val livenessType = LivenessType.RGB private var openRectInfoDraw = false private val leftDebouncer = Debouncer(30 * 1000) /** 识别超时倒计时(15秒无操作跳回初始页) */ private val recognizeCountDownUtil = CountDownUtil() /** 用户离开画面后的倒计时(llCollectButton 显示状态下无人脸时触发) */ private val leftCountDownUtil = CountDownUtil() private var registerHandler: RegisterCallbackHandler? = null private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean { if (userFaceInfo == null) return false val headBmp = userFaceInfo.headBmp if (headBmp == null) return false // if (headBmp.width <= 200 || headBmp.height <= 200) { // loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小-----------") // return false // } return true } private fun initArcViewModel() { recognizeViewModel.setLiveType(livenessType) recognizeViewModel.prepareRegister() recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo -> registerHandler?.setCallback { if (!isCollectFace || !isCollecting.get()) { return@setCallback } if (this.userFaceInfo == null && isFitData(userFaceInfo)) { val headBmp = userFaceInfo.headBmp ?: run { loadLogInfo("collectFace,isFitData:headBmp 为 null,跳过") return@setCallback } if (headBmp.width <= 200 || headBmp.height <= 200) { loadLogInfo("collectFace,isFitData:拿到人脸提取特征数据照片过小w:${headBmp.width},h:${headBmp.height}-----------") binding.llCollectButton.visible() binding.btnRetry.gone() binding.btnCollect.text = "重新采集" return@setCallback } recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE) this.userFaceInfo = userFaceInfo getFaceDataBlock() } } } recognizeViewModel.ftInitCode.observe(this) { ftInitCode: Int? -> if (ftInitCode != null && ftInitCode != ErrorInfo.MOK) { val error: String? = getString( R.string.specific_engine_init_failed, "ftEngine", ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode) ) Timber.e("ftInitCode observe = $error") ToastUtils.showToast(error) } } recognizeViewModel.frInitCode.observe(this) { frInitCode: Int? -> if (frInitCode != null && frInitCode != ErrorInfo.MOK) { val error: String? = getString( R.string.specific_engine_init_failed, "frEngine", frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode) ) Timber.e("frInitCode observe = $error") ToastUtils.showToast(error) } } recognizeViewModel.flInitCode.observe(this) { flInitCode: Int? -> if (flInitCode != null && flInitCode != ErrorInfo.MOK) { val error: String? = getString( R.string.specific_engine_init_failed, "flEngine", flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode) ) Timber.e("flInitCode observe = $error") ToastUtils.showToast(error) } } recognizeViewModel.recognizeConfiguration .observe(this) { recognizeConfiguration: RecognizeConfiguration? -> Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}") } recognizeViewModel.recognizeNotice.observe(this) { notice: String? -> Timber.i("recognizeNotice observe notice = $notice") } recognizeViewModel.recognizeUserId.observe(this) { result: CompareResult? -> loadLogInfo("collectFace,recognizeUserId observe userId=${result?.faceEntity?.userName},similar=${result?.similar},userType=${result?.faceEntity?.userType}") loadFaceRecognizeResult3(result) } recognizeViewModel.drawRectInfoText.observe(this) { info -> Timber.i("drawRectInfoText observe info = $info") } } /** * 识别失败重试次数 */ private var retryFailCount = 0 /** * 识别失败最大次数 */ private val maxFailCount = 5 /** * 配置阈值,默认值为0.8 */ private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) } private val openPlateDebouncer = Debouncer(3000) private val isCollecting = AtomicBoolean(false) private fun loadFaceRecognizeResult3(result: CompareResult?) { val similar = result?.similar ?: return binding.tvSimilarValue.text = "$similar,${retryFailCount}次" loadLogInfo( "collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, similar=$similar, retry=$retryFailCount" ) if (similar >= faceSuccessThreshold) { val userId = result.faceEntity?.userName recognizeSuccess(userId) return } if (isCollectFace || isCollecting.get()) { loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar") return } if (retryFailCount >= maxFailCount) { loadLogInfo( "collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " + "retry=$retryFailCount, time=${DateTimeUtils.getDateTimeString()}" ) if (!isCollecting.compareAndSet(false, true)) { loadLogInfo("collectFace 已在采集中,忽略重复触发") return } recognizeFail() return } retryFailCount++ loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount") } private fun recognizeSuccess(userId: String?) { if (userId.isNullOrBlank()) { loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略") return } loadLogInfo("collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}") // 成功后必须重置状态 retryFailCount = 0 isCollecting.set(false) isCollectFace = false currentUserId = userId // 防抖,防止重复开闸 openPlateDebouncer.debounce("openPlateDebouncer") { openPlate(userId) } } private fun recognizeFail() { recognizeCountDownUtil.cancelCountDown() binding.tvToInit.text = "" binding.tvSimilarValue.text = "" binding.llCollectButton.visible() binding.btnRetry.visible() binding.btnCollect.run { visible() text = "新用户采集" } binding.ivCollectPhoto.visible() } private fun newUserCollect() { recognizeCountDownUtil.cancelCountDown() binding.tvToInit.text = "" binding.tvSimilarValue.text = "" binding.llCollectButton.visible() binding.btnRetry.gone() binding.btnCollect.run { visible() text = "新用户采集" } binding.ivCollectPhoto.gone() openPlateDebouncer.reset() plateEnable = true resumeCamera() } private var plateEnable = false /** * 打开餐盘柜:先获取 PLC 状态,有盘则出盘,无盘则播放提示音 */ private fun openPlate(userId: String?) { loadLogInfo("collectFace,openPlate: userId=$userId,plateEnable=$plateEnable") if (plateEnable.not()) return PlcHelper.getInstance(this@LoginByFaceActivity) .outPlate(object : PlcCallback { override fun onSuccess(result: String) { if(result.equals("true")){ SoundPoolUtil.getInstance().play(RECOGNIZE_SUCCESS, 0) } plateEnable = false recognizeCountDownUtil.cancelCountDown() loadLogInfo("collectFace,openPlate: 出盘成功,result=$result") goInitActivity() } override fun onError(message: String) { ToastUtils.showToast("出盘失败,message=$message") loadLogInfo("collectFace,openPlate: 出盘失败,message=$message") goInitActivity() } }) // showWaitingDialog("检测餐盘中,请稍候……") // PlcHelper.getInstance(this).getStatus(object : PlcCallback { // override fun onSuccess(result: PlcStatus) { // hideWaitingDialog() // if (result.isEmptyPlate) { // // 无餐盘,播放提示音并返回首页 // loadLogInfo("collectFace,openPlate: 暂无餐盘,isEmptyPlate=true,isCollectFace=$isCollectFace") // SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0) // goInitActivity() // return // } // // 有餐盘,播放识别成功音效并调用出盘 // loadLogInfo("collectFace,openPlate: 检测到餐盘,开始出盘") // // // } // // override fun onError(message: String) { // hideWaitingDialog() // // 状态获取失败,保底播放无盘提示 // loadLogInfo("collectFace,openPlate: 获取PLC状态失败,message=$message") // SoundPoolUtil.getInstance().play(NO_PLATE_REMIND, 0) // goInitActivity() // } // }) } private val openDebouncer by lazy { Debouncer(5000) } private fun initArcView() { //在布局结束后才做初始化操作 binding.dualCameraTexturePreviewRgb.viewTreeObserver.addOnGlobalLayoutListener(this) val list = recognizeViewModel.getCompareResultList().getValue() loadLogInfo("initArcView:list.size=${list?.size}") } override fun onDestroy() { loadLogInfo("onDestroy") rgbCameraHelper?.release() rgbCameraHelper = null recognizeViewModel.destroy() EventBus.getDefault().unregister(this) super.onDestroy() } /** * 调整View的宽高,使预览显示正常且采集框固定为380x380 * * @param rgbPreview RGB预览View * @param previewView 显示预览数据的view * @param faceRectView 画框的view (380x380) * @param previewSize 预览大小 * @param displayOrientation 相机旋转角度 * @param scale 缩放比例 * @return 调整后的LayoutParams */ private fun adjustPreviewViewSize( rgbPreview: View, previewView: View, faceRectView: FaceRectView, previewSize: Camera.Size, displayOrientation: Int, scale: Float ): ViewGroup.LayoutParams { // val layoutParams = previewView.layoutParams // val measuredWidth = previewView.measuredWidth // val measuredHeight = previewView.measuredHeight // var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat() // if (ratio > 1) { // ratio = 1 / ratio // } // if (displayOrientation % 180 == 0) { // layoutParams.width = measuredWidth // layoutParams.height = (measuredWidth * ratio).toInt() // } else { // layoutParams.height = measuredHeight // layoutParams.width = (measuredHeight * ratio).toInt() // } // if (scale < 1f) { // val rgbParam = rgbPreview.layoutParams // layoutParams.width = (rgbParam.width * scale).toInt() // layoutParams.height = (rgbParam.height * scale).toInt() // } else { // layoutParams.width = (layoutParams.width * scale).toInt() // layoutParams.height = (layoutParams.height * scale).toInt() // } // // val metrics = DisplayMetrics() // windowManager.defaultDisplay.getMetrics(metrics) // // if (layoutParams.width >= metrics.widthPixels) { // val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat()) // layoutParams.width = (layoutParams.width / viewRatio).toInt() // layoutParams.height = (layoutParams.height / viewRatio).toInt() // } // if (layoutParams.height >= metrics.heightPixels) { // val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat()) // layoutParams.width = (layoutParams.width / viewRatio).toInt() // layoutParams.height = (layoutParams.height / viewRatio).toInt() // } val layoutParams = FrameLayout.LayoutParams(776, 582); Timber.d("adjustPreviewViewSize,${layoutParams.width},${layoutParams.height}}") previewView.setLayoutParams(layoutParams) faceRectView.setLayoutParams(layoutParams) return layoutParams } private fun initRgbCamera() { val cameraListener: CameraListener = object : CameraListener { override fun onCameraOpened( camera: Camera, cameraId: Int, displayOrientation: Int, isMirror: Boolean ) { loadLogInfo("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation") runOnUiThread { val previewSizeRgb = camera.parameters.previewSize val layoutParams = adjustPreviewViewSize( binding.dualCameraTexturePreviewRgb, binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView, previewSizeRgb, displayOrientation, 0.7f //0.6f ) loadLogInfo("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}") loadLogInfo("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}") loadLogInfo( "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${ ConfigUtil.isDrawRgbRectHorizontalMirror( context ) }, isDrawRgbRectVerticalMirror = ${ ConfigUtil.isDrawRgbRectVerticalMirror( context ) }" ) rgbFaceRectTransformer = FaceRectTransformer( previewSizeRgb.width, previewSizeRgb.height, layoutParams.width, layoutParams.height, 0, cameraId, isMirror, false, false ) recognizeViewModel.onRgbCameraOpened(camera) recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer) } } @RequiresApi(api = Build.VERSION_CODES.Q) override fun onPreview(nv21: ByteArray?, camera: Camera?) { if (!isRecognition) { return } binding.dualCameraFaceRectView.clearFaceInfo() facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true) if (facePreviewInfoList != null && rgbFaceRectTransformer != null) { drawPreviewInfo(facePreviewInfoList) } if (facePreviewInfoList.isNullOrEmpty()) { currentUserId = null userFaceInfo = null binding.btnCollect.tag = "" binding.tvSimilarValue.text = "" retryFailCount = 0 // 人脸离开时,发送空帧清除 ViewModel 内部识别缓存,防止再次进入时使用旧数据 recognizeViewModel.onPreviewFrame(emptyFrame, true) if (binding.llCollectButton.isVisible) { leftDebouncer.debounce("leftDebouncer") { leftCountDown() } } } recognizeViewModel.clearLeftFace(facePreviewInfoList) } override fun onCameraClosed() { Timber.i("initRgbCamera onCameraClosed: ") } override fun onCameraError(e: java.lang.Exception) { Timber.i("initRgbCamera onCameraError: %s", e.message) e.printStackTrace() } override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) { Timber.i("initRgbCamera onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}") if (rgbFaceRectTransformer != null) { rgbFaceRectTransformer?.cameraDisplayOrientation = displayOrientation } Timber.i("initRgbCamera onCameraConfigurationChanged: $cameraID $displayOrientation") } } val previewConfig: PreviewConfig = recognizeViewModel.previewConfig rgbCameraHelper = DualCameraHelper.Builder() .previewViewSize( Point( binding.dualCameraTexturePreviewRgb.measuredWidth, binding.dualCameraTexturePreviewRgb.measuredHeight ) ) .rotation(windowManager.defaultDisplay.rotation) .additionalRotation(0) // 角度 .previewSize(recognizeViewModel.loadPreviewSize()) .specificCameraId(previewConfig.rgbCameraId) // .isMirror(true) .isMirror(true) .previewOn(binding.dualCameraTexturePreviewRgb) .cameraListener(cameraListener) .build() rgbCameraHelper?.init() rgbCameraHelper?.start() } /** * 绘制RGB、IR画面的实时人脸信息 * * @param facePreviewInfoList RGB画面的实时人脸信息 */ private fun drawPreviewInfo(facePreviewInfoList: MutableList?) { if (rgbFaceRectTransformer != null) { val rgbDrawInfoList: MutableList? = recognizeViewModel.getDrawInfo( facePreviewInfoList, LivenessType.RGB, openRectInfoDraw ) // 识别成功 binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList) } } // fun openRectInfoDraw(view: View?) { // openRectInfoDraw = !openRectInfoDraw // recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw) // } override fun onGlobalLayout() { loadLogInfo("onGlobalLayout") binding.dualCameraTexturePreviewRgb.viewTreeObserver.removeOnGlobalLayoutListener(this) // 请求权限 checkCameraPermission() } private fun resumeCamera() { // isRecognition = true val helper = rgbCameraHelper if (helper != null && helper.isStopped) { loadLogInfo("collectFace,resumeCamera: ") isRecognition = true helper.start() } else { //recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) recognizeViewModel.onPreviewFrame(emptyFrame, true) binding.dualCameraFaceRectView.clearFaceInfo() recognizeViewModel.clearLeftFace(facePreviewInfoList) binding.root.postDelayed({ isRecognition = true }, 500) } } /** * 构造符合分辨率的 NV21 格式全黑帧 * @param width 图像宽度(如 1920) * @param height 图像高度(如 1080) * @return 合法的全黑帧字节数组 */ private fun createBlackNV21Frame(width: Int, height: Int): ByteArray { val frameSize = width * height val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5 // Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值) // 1. 填充 Y 分量(前 frameSize 个字节) // Arrays.fill(nv21Data, 0, frameSize, 0.toByte()) nv21Data.fill(0, 0, frameSize) // 2. 填充 UV 分量(后 frameSize/2 个字节) // Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte()) nv21Data.fill(128.toByte(), frameSize, nv21Data.size) return nv21Data } // 调用示例:构造 1920×1080 的全黑帧(长度 1382400) // private val emptyFrame = createBlackNV21Frame(1920, 1080) private val emptyFrame = createBlackNV21Frame(1280, 720) // 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true) // /** // * 临时用户id // */ // private var tempUserId: String = "" override fun onResume() { super.onResume() plateEnable = true try { if (isFirstOpen) { isFirstOpen = false goInitActivity() return } lifecycleScope.launch { try { val faceDao = FaceDatabase.getInstance(this@LoginByFaceActivity).faceDao() val faceCount = withContext(Dispatchers.IO) { faceDao.faceCount } if (faceCount == 0) { newUserCollect() } else { resetRecognizeState() } } catch (e: Exception) { e.printStackTrace() } } } catch (e: Exception) { e.printStackTrace() } } /** * 重置识别状态 */ private fun resetRecognizeState() { binding.ivCollectPhoto.gone() binding.ivCollectPhoto.setImageDrawable(null) binding.tvToInit.text = "15" binding.llCollectButton.gone() retryFailCount = 0 binding.tvSimilarValue.text = "" isCollecting.set(false) isCollectFace = false plateEnable = true collectCount = 0 // tempUserId = "${System.currentTimeMillis()}" openPlateDebouncer.reset() // 清空上次识别结果和 FaceHelper 内部状态,防止短时间内再次识别无法触发 recognizeViewModel.resetFaceState() resumeCamera() registerHandler = RegisterCallbackHandler() recognizeCountDownUtil.startCountDown( total = 15, lifecycleScope = lifecycleScope, onTick = { remaining -> loadLogInfo("resetRecognizeState剩余时间:$remaining 秒") // if (judgeRecognizeState()) return@startCountDown loadLogInfo("initCountTime onTick = $remaining") binding.tvToInit.text = "$remaining" }, onFinish = { loadLogInfo("resetRecognizeState倒计时结束") // if (judgeRecognizeState()) return@startCountDown if (facePreviewInfoList.isNullOrEmpty()) { goInitActivity() } else { loadLogInfo("recognizeFail---initCountTime") recognizeFail() } } ) } private var isFirstOpen = true override fun onPause() { pauseCamera() super.onPause() recognizeCountDownUtil.cancelCountDown() } private fun pauseCamera() { isRecognition = false recognizeViewModel.onPreviewFrame(emptyFrame, true) } // private fun judgeRecognizeState(): Boolean { // val rectColor = binding.dualCameraFaceRectView.rectColor // if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank() // .not() && facePreviewInfoList.isNullOrEmpty().not() // ) { // //人未离开,再次识别 // loadLogInfo("userId=$currentUserId") // openPlateDebouncer.debounce { // openPlate(currentUserId) // } // return true // } // return false // } fun goInitActivity() { val intent = Intent(context, InitActivity::class.java) startActivity(intent) } /** * 采集人脸 */ private fun collectFace() { loadLogInfo("collectFace: -----------1,开始采集人脸,开始时间:${DateTimeUtils.getDateTimeString()}----------") if (isFirstOpen) return loadLogInfo("collectFace: ----------2,isFirstOpen=false-----------") loadLogInfo("collectFace: ----------4,检测到人脸信息-----------") currentUserId = null userFaceInfo = null recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY) loadLogInfo("collectFace: ----------5,人脸采集中-----------") } private val uploadFaceDebouncer = Debouncer(10000) /** * 改为上传人脸数据,不需要发生人脸数据到收银端 */ private fun sendFaceData(faceData: String, headBmp: Bitmap) { loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------") val imageFile = BitmapSaver.saveToAppFilesDir( headBmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg" ) loadLogInfo("collectFace,sendFaceData:${imageFile?.absolutePath}") if (imageFile == null) { ToastUtils.showToast("获取人脸照片失败") return } uploadFaceDebouncer.debounce("uploadFaceDebouncer") { addUserFace(faceData, imageFile) } } private fun addUserFace(faceData: String, imageFile: File) { userViewModel.addUserFace(faceData, imageFile) { status, faceModel -> loadLogInfo("collectFace,saveFaceInfo: ----------12,人脸数据发送完成时间:${DateTimeUtils.getDateTimeString()},结果:$status-----------") if (status.not()) { ToastUtils.showToast("新增数据失败") loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据失败,faceData=${faceData}") return@addUserFace } if (faceModel == null) { ToastUtils.showToast("新增数据成功,但未拿到接口数据") loadLogInfo("collectFace,sendFaceData: ----------12,发送人脸数据成功但接口返回为null,faceData=${faceData}") return@addUserFace } lifecycleScope.launch(Dispatchers.IO) { saveFaceInfo(faceModel) { runOnUiThread { Glide.with(this@LoginByFaceActivity).load(imageFile) .into(binding.ivCollectPhoto) } } uploadFaceDebouncer.reset() } } } private fun getFaceDataBlock() { if (userFaceInfo?.faceFeature?.featureData == null) { loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据为null-----------") return } val faceData: String? = Base64.encode(userFaceInfo?.faceFeature?.featureData) if (faceData.isNullOrBlank()) { loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据Base64编码为空") return } loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------") val headBmp = userFaceInfo!!.headBmp //block(faceData, headBmp) isCollecting.set(false) isCollectFace = false sendFaceData(faceData, headBmp) } /** * 加载日志 * @param msg 日志内容 */ private fun loadLogInfo(msg: String) { Timber.tag(TAG).d(msg) LogFileUtil.saveLog(msg) } private var facePreviewInfoList: MutableList? = null private var userFaceInfo: UserFaceInfo? = null private var currentUserId: String? = null private fun saveFaceInfo(faceModel: UserFaceModel, block: () -> Unit = {}) { try { loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------") val faceDao = FaceDatabase.getInstance(this).faceDao() val faceEntity = FaceEntity( faceModel.userId, null, Base64.decode(faceModel.faceFeatureStr) ).also { //1-会员、2-临时用户 it.userType = if (faceModel.member) "1" else "2" } val faceId = faceDao.insert(faceEntity) faceEntity.faceId = faceId loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") recognizeViewModel.refreshFaceList() loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------") runOnUiThread { //采集完成,不再识别,直接提示成功 isRecognition = false recognizeSuccess("${System.currentTimeMillis()}") } loadLogInfo("---------------------------------") collectCount = 0 block() } catch (e: Exception) { e.printStackTrace() loadLogInfo("collectFace,saveFaceInfo: ----------13,人脸采集异常:${e.message}-----------") collectCount++ loadLogInfo("collectFace,saveFaceInfo: ----------14,${collectCount}次人脸采集异常:${e.message}-----------") ToastUtils.showToast("人脸采集异常:${e.message}") } } private var collectCount = 0 private var isCollectFace = false private fun leftCountDown() { leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { loadLogInfo("onPreview-----------leftCountDown-------------") }, onTick = { loadLogInfo("leftCountDown.onTick:$it") if (facePreviewInfoList.isNullOrEmpty().not()) { loadLogInfo("leftCountDown.onTick-cancel()") leftCountDownUtil.cancelCountDown() return@startCountDown } }, onFinish = { if (facePreviewInfoList.isNullOrEmpty().not()) { loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转") return@startCountDown } goInitActivity() }) } }