package com.sw.platecabinet.activity import android.Manifest import android.content.Intent import android.graphics.Point import android.hardware.Camera import android.os.Build import android.os.CountDownTimer import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.lifecycle.Observer import com.arcsoft.face.ErrorInfo 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.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.faceserver.FaceServer import com.sw.plate.utils.arcface.facedb.entity.FaceEntity import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.platecabinet.utils.Debouncer import com.sw.platecabinet.member.R import com.sw.platecabinet.member.databinding.ActivityLoginFaceBinding import com.sw.platecabinet.member.databinding.ItemTitleTimeBinding import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.network.task.TaskManager import com.sw.platecabinet.utils.PermissionHelper import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import timber.log.Timber /** * 人脸识别 */ class LoginByFaceActivity : BaseActivity(), ViewTreeObserver.OnGlobalLayoutListener { private var countDownTimer: CountDownTimer? = null private val CAMERA_PERMISSION_REQUEST_CODE = 100 private val REQUIRED_PERMISSIONS: Array = arrayOf( Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE ) override fun inflateViewBinding(): ActivityLoginFaceBinding { return ActivityLoginFaceBinding.inflate(layoutInflater) } override fun inflateTitleBinding(): ItemTitleTimeBinding { return binding.includeHeader } companion object { private var instance: LoginByFaceActivity? = null fun goInitActivity() { instance?.goInitActivity() } } override fun initialize() { instance = this // V2 引擎激活(appId/sdkKey/activeKey 来自 V2 设备配置) netViewModelV2.activeEngine() initCountTime() initArcViewModel() initArcView() openRectInfoDraw = true recognizeViewModel.setDrawRectInfoTextValue(true) //开启人脸增量数据定时任务(V2) startFaceTask() binding.llToPwd.setOnClickListener { val intent = Intent(this, LoginByPwdActivity::class.java) startActivity(intent) } // TODO: 测试请求 TaskManager.startTask() EventBus.getDefault().register(this) } private fun checkCameraPermission() { Timber.i("checkCameraPermission") val permissionHelper = PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE) .onGranted { Timber.d("checkCameraPermission 权限已允许") // 权限已授予 recognizeViewModel.init() initRgbCamera() resumeCamera() } .build() registerPermissionHelper(permissionHelper) permissionHelper.checkAndRequest() } @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() } private var isRecognition = false private var rgbCameraHelper: DualCameraHelper? = null private var rgbFaceRectTransformer: FaceRectTransformer? = null private val livenessType = LivenessType.RGB private var openRectInfoDraw = false private fun initArcViewModel() { recognizeViewModel.setLiveType(livenessType) recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? -> if (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, Observer { frInitCode: Int? -> if (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, Observer { flInitCode: Int? -> if (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, Observer { recognizeConfiguration: RecognizeConfiguration? -> Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}") }) recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? -> Timber.i("recognizeNotice observe notice = $notice") }) recognizeViewModel.recognizeUserId.observe(this, Observer { result: CompareResult? -> Timber.i( "recognizeUserId observe userId=${result?.faceEntity?.userName}," + "similar=${result?.similar},userType=${result?.faceEntity?.userType}" ) loadFaceRecognizeResult(result) }) recognizeViewModel.drawRectInfoText.observe(this, Observer { info -> Timber.i("drawRectInfoText observe info = $info") }) } private fun initArcView() { //在布局结束后才做初始化操作 binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this) recognizeViewModel.getCompareResultList().getValue() } /** * 识别阈值,默认值为0.8 */ private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) } /** * 识别成功后开柜(查询用户信息)防抖,防止重复触发 */ private val loginDebouncer = Debouncer(3000) /** * 处理识别结果:达到阈值走登录流程,未达阈值仅记录(本版本无采集模式) */ private fun loadFaceRecognizeResult(result: CompareResult?) { val similar = result?.similar ?: return Timber.i( "loadFaceRecognizeResult 阈值=$faceSuccessThreshold, similar=$similar" ) if (similar >= faceSuccessThreshold) { val userId = result.faceEntity?.userName recognizeSuccess(userId) return } // 未达阈值:暂不处理(无采集模式),等待下一次识别 } /** * 识别成功:防抖后查询用户信息,走开柜门业务流程 */ private fun recognizeSuccess(userId: String?) { if (userId.isNullOrBlank()) { Timber.i("recognizeSuccess 识别成功但 userId 为空,忽略") return } Timber.i("recognizeSuccess userId=$userId") loginDebouncer.debounce { viewModel.getUserInfoById(memberId = userId) } } override fun onDestroy() { Timber.d("onDestroy") instance = null if (rgbCameraHelper != null) { 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.getLayoutParams() 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() } 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 ) { Timber.d("initRgbCamera Rgb onCameraOpened: cameraId = $cameraId, displayOrientation = $displayOrientation") runOnUiThread({ val previewSizeRgb = camera.getParameters().getPreviewSize() val layoutParams = adjustPreviewViewSize( binding.dualCameraTexturePreviewRgb, binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView, previewSizeRgb, displayOrientation, 0.7f ) Timber.d("initRgbCamera previewSizeRgb = ${previewSizeRgb.width}-${previewSizeRgb.height}") Timber.d("initRgbCamera layoutParams = ${layoutParams.width}-${layoutParams.height}") Timber.d( "initRgbCamera isMirror = ${isMirror}, isDrawRgbRectHorizontalMirror = ${ ConfigUtil.isDrawRgbRectHorizontalMirror( context ) }, isDrawRgbRectVerticalMirror = ${ ConfigUtil.isDrawRgbRectVerticalMirror( context ) }" ) rgbFaceRectTransformer = FaceRectTransformer( previewSizeRgb.width, previewSizeRgb.height, layoutParams.width, layoutParams.height, 90, cameraId, isMirror, true, true ) 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() val facePreviewInfoList: MutableList? = recognizeViewModel.onPreviewFrame(nv21, true) if (facePreviewInfoList != null && rgbFaceRectTransformer != null) { drawPreviewInfo(facePreviewInfoList) } 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) .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() { Timber.d("onGlobalLayout") binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this) // 请求权限 checkCameraPermission() } private fun resumeCamera() { val helper = rgbCameraHelper if (helper != null && helper.isStopped) { isRecognition = true helper.start() } else { // 相机未停止时,发送黑帧清除 ViewModel 内部识别缓存,防止恢复后使用旧数据 recognizeViewModel.onPreviewFrame(emptyFrame, true) binding.dualCameraFaceRectView.clearFaceInfo() binding.root.postDelayed({ isRecognition = true }, 500) } } override fun onResume() { super.onResume() // 清空上次识别结果和 FaceHelper 内部状态,防止短时间内再次识别无法触发 recognizeViewModel.resetFaceState() resumeCamera() viewModel.resetUserInfo() countDownTimer?.let { it.cancel() it.start() } } protected override fun onPause() { pauseCamera() super.onPause() countDownTimer?.cancel() } private fun pauseCamera() { isRecognition = false recognizeViewModel.onPreviewFrame(emptyFrame, true) } /** * 构造符合分辨率的 NV21 格式全黑帧 * @param width 图像宽度 * @param height 图像高度 * @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(默认值) nv21Data.fill(0, 0, frameSize) nv21Data.fill(128.toByte(), frameSize, nv21Data.size) return nv21Data } private val emptyFrame = createBlackNV21Frame(1280, 720) override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) { super.handleLoginSuccess(equipmentUserInfo, isAdmin) // goInitActivity() } fun initCountTime() { val totalTimeInMillis = 30L * 1000 countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) { override fun onTick(millisUntilFinished: Long) { Timber.d("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}") binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}" } override fun onFinish() { goInitActivity() } }.start() } fun goInitActivity() { val intent = Intent(context, InitActivity::class.java) startActivity(intent) } }