From 3768bd94f24a655e448a2afb21d236adba4a0b4a Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Sat, 28 Feb 2026 10:03:04 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=86=E5=88=AB=E6=88=90=E5=8A=9F=E6=9C=AA?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E5=8F=8A=E9=87=87=E9=9B=86=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E9=95=BF=E9=97=AE=E9=A2=98=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sw/face/collect/MainActivity.kt | 495 ++++++++---------- .../java/com/sw/face/collect/ext/Common.kt | 31 +- .../sw/face/collect/utils/CountDownManager.kt | 135 +++++ .../sw/face/collect/utils/CountDownUtil.kt | 88 ++++ .../sw/face/collect/utils/FlowCountDown.kt | 21 +- .../viewmodel/FaceRecognizeViewModel.kt | 75 +++ .../sw/plate/utils/arcface/FaceRectView.java | 4 + .../arcface/viewmodel/RecognizeViewModel.java | 2 +- 8 files changed, 558 insertions(+), 293 deletions(-) create mode 100644 app/src/main/java/com/sw/face/collect/utils/CountDownManager.kt create mode 100644 app/src/main/java/com/sw/face/collect/utils/CountDownUtil.kt create mode 100644 app/src/main/java/com/sw/face/collect/viewmodel/FaceRecognizeViewModel.kt diff --git a/app/src/main/java/com/sw/face/collect/MainActivity.kt b/app/src/main/java/com/sw/face/collect/MainActivity.kt index 0f965ca..183a9e3 100644 --- a/app/src/main/java/com/sw/face/collect/MainActivity.kt +++ b/app/src/main/java/com/sw/face/collect/MainActivity.kt @@ -2,28 +2,31 @@ package com.sw.face.collect import android.Manifest import android.content.Intent -import android.content.pm.PackageManager import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.PixelFormat import android.graphics.Point import android.hardware.Camera +import android.media.ImageReader import android.os.Build -import android.os.CountDownTimer -import android.util.DisplayMetrics import android.util.Log import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.widget.FrameLayout -import android.widget.Toast import androidx.activity.viewModels import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.arcsoft.face.ErrorInfo -import com.google.gson.Gson +import com.arcsoft.face.FaceEngine +import com.arcsoft.face.FaceFeature +import com.arcsoft.face.FaceInfo +import com.arcsoft.face.enums.DetectFaceOrientPriority +import com.arcsoft.face.enums.DetectMode +import com.arcsoft.face.enums.ExtractType import com.sw.face.collect.base.BaseActivity -import com.sw.face.collect.base.GlobalData import com.sw.face.collect.databinding.ActivityMainBinding import com.sw.face.collect.ext.clickWithDebounce import com.sw.face.collect.ext.dp @@ -31,15 +34,16 @@ import com.sw.face.collect.ext.gone import com.sw.face.collect.ext.toast import com.sw.face.collect.ext.visible import com.sw.face.collect.model.UserFaceModel -import com.sw.face.collect.utils.Base64 import com.sw.face.collect.utils.BitmapSaver import com.sw.face.collect.utils.BitmapUtils +import com.sw.face.collect.utils.CountDownManager +import com.sw.face.collect.utils.CountDownUtil import com.sw.face.collect.utils.Debouncer import com.sw.face.collect.utils.FaceEngineUtils -import com.sw.face.collect.utils.IntervalExecutor import com.sw.face.collect.utils.PermissionHelper import com.sw.face.collect.utils.RegisterCallbackHandler import com.sw.face.collect.utils.SoundPoolUtil +import com.sw.face.collect.viewmodel.FaceRecognizeViewModel import com.sw.face.collect.viewmodel.MainViewModel import com.sw.inbound.utils.DateTimeUtils import com.sw.plate.utils.L @@ -54,6 +58,7 @@ 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 @@ -62,19 +67,16 @@ 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 kotlinx.coroutines.Job -import kotlinx.coroutines.runBlocking -import org.json.JSONObject import java.io.File +import java.util.Arrays import java.util.concurrent.atomic.AtomicBoolean import kotlin.getValue +import kotlin.math.log import kotlin.system.exitProcess class MainActivity : BaseActivity(), ViewTreeObserver.OnGlobalLayoutListener { - public val recognizeViewModel by viewModels() -// public val userViewModel by viewModels() - + public val recognizeViewModel by viewModels() companion object { private const val TAG = "LoginByFaceActivity" @@ -94,56 +96,12 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater) -// private var logList: MutableList = mutableListOf() -// private val logAdapter by lazy { LogAdapter(logList) } - override fun initialize() { FaceEngineUtils.activeEngine() -// addSocketListener() - initCountTime() initArcViewModel() initArcView() openRectInfoDraw = true recognizeViewModel.setDrawRectInfoTextValue(true) - //viewModel.generateToken() - -// binding.llToPwd.setOnClickListener { -// val intent = Intent(this, LoginByPwdActivity::class.java) -// startActivity(intent) -// } - -// TaskManager.startTask() - -// EventBus.getDefault().register(this) - -// binding.btnCollectFace.setOnClickListener { collectFace() } - - -// binding.rvLogInfo.let { -// it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) -// it.adapter = logAdapter -// } -// -// binding.btnLogState.let { -// it.tag = false -// it.setImageResource(R.drawable.ic_log_show) -// it.setOnClickListener { v -> -// binding.rvLogInfo.run { -// val state = it.tag.toString().toBooleanStrictOrNull() ?: false -// if (state) visible() else gone() -// } -// val state = it.tag.toString().toBooleanStrictOrNull() ?: false -// it.tag = state -// it.setImageResource( -// if (state) R.drawable.ic_log_hide else R.drawable.ic_log_show -// ) -// } -// } - - //开启人脸增量数据定时任务 -// startFaceTask { -// loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}") -// } val soundMap = hashMapOf() soundMap[COLLECT_SUCCESS] = R.raw.collect_success @@ -157,6 +115,7 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo //采集数据按钮 retryFailCount = 0 isCollectFace = true + isCollecting.set(true) collectFace() } @@ -166,7 +125,6 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo } } - private fun checkCameraPermission() { //Timber.i("checkCameraPermission") val permissionHelper = @@ -211,6 +169,7 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo private val livenessType = LivenessType.IR private var openRectInfoDraw = false private val regDebouncer = Debouncer(3000) + private val leftDebouncer = Debouncer(30*1000) private var registerHandler: RegisterCallbackHandler? = null private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean { @@ -229,7 +188,7 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo recognizeViewModel.prepareRegister() recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo -> registerHandler?.setCallback { - if (!isCollectFace) { + if (!isCollectFace || !isCollecting.get()) { return@setCallback } if (this.userFaceInfo == null && isFitData(userFaceInfo)) { @@ -301,6 +260,20 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo recognizeViewModel.drawRectInfoText.observe(this, Observer { info -> //Timber.i("drawRectInfoText observe info = $info") }) + + recognizeViewModel.recognitionTriggered.observe(this) { recognitionResult -> + // recognitionResult 为空时跳过(比如重置状态后) + recognitionResult ?: return@observe + + // 识别事件触发(无论是否是同一人,都会走到这里) + val userId = recognitionResult.userId + if (recognitionResult.isNewRecognition) { + // 新识别:执行业务逻辑(如跳转、授权) + recognizeSuccess(userId) + } else { + // 防抖内的重复识别:仅更新UI(不重复执行业务) + } + } } /** @@ -332,6 +305,28 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo private val openPlateDebouncer = Debouncer(5000) private val isCollecting = AtomicBoolean(false) + 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 { + openPlate(userId) + } + } + private fun loadFaceRecognizeResult3(result: CompareResult?) { val similar = result?.similar ?: return @@ -341,29 +336,11 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo ) if (similar >= faceSuccessThreshold) { val userId = result.faceEntity?.userName - 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 { - openPlate(userId) - } + recognizeSuccess(userId) return } - if (isCollectFace) { + if (isCollectFace || isCollecting.get()) { loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar") return } @@ -377,6 +354,7 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo loadLogInfo("collectFace 已在采集中,忽略重复触发") return } + loadLogInfo("recognizeFail---maxFailCount") recognizeFail() return } @@ -386,7 +364,7 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo } private fun recognizeFail() { - countDownTimer?.cancel() + CountDownManager.cancelCountDown() binding.tvToInit.text = "" binding.llCollectButton.visible() @@ -394,102 +372,9 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo binding.btnCollect.run { visible() text = "新用户采集" -// if (tag == "1") { -// binding.btnRetry.gone() -// text = "重新采集" -// tag = "1" -// } else { -// binding.btnRetry.visible() -// text = "新用户采集" -// tag = "1" -// } } } - /** - * 根据相似度similar>设定阈值0.8作为识别成功判断, - * 如果similar<0.3作为识别失败判断, - * 如果similar>=0.3&&similar<=0.8时,暂时不作为失败情况,再识别5次如果similar<0.8则作为识别失败处理 - */ - private fun loadFaceRecognizeResult2(result: CompareResult?) { - // 无有效结果直接忽略 - val similar = result?.similar ?: return - - binding.tvSimilarValue.text = "$similar,${retryFailCount}次" - loadLogInfo( - "collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, " + "fail=$faceFailThreshold, similar=$similar, retry=$retryFailCount" - ) - - if (similar >= faceSuccessThreshold) { - val userId = result.faceEntity?.userName - 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 { - openPlate(userId) - } - return - } - - if (isCollectFace) { - loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar") - return - } - - if (similar <= faceFailThreshold || retryFailCount >= maxFailCount) { - loadLogInfo( - "collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " + "retry=$retryFailCount, time=${DateTimeUtils.getDateTimeString()}" - ) - if (!isCollecting.compareAndSet(false, true)) { - loadLogInfo("collectFace 已在采集中,忽略重复触发") - return - } - - retryFailCount = 0 - isCollectFace = true - collectFace()//采集人脸 - return - } - - retryFailCount++ - loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount") - } - - /** - * 根据相似度similar>设定阈值0.8作为识别是否成功来加载人脸识别结果 - */ - private fun loadFaceRecognizeResult(result: CompareResult?) { - val similar = result?.similar ?: 0f - binding.tvSimilarValue.text = "$similar" - val similarPass = result?.isSimilarPass ?: false - if (similarPass.not()) { - loadLogInfo("collectFace,recognizeUserId 未识别到人脸信息,开始采集") - isCollectFace = true - collectFace() - return - } - currentUserId = result?.faceEntity?.userName - if (tempUserId != currentUserId) { - SoundPoolUtil.getInstance().play("success", 0) - } -// plateEnable = true - Debouncer(5000).debounce { openPlate(currentUserId) } - } - - private var plateEnable = false /** @@ -498,7 +383,8 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo private fun openPlate(userId: String?) { if (plateEnable.not()) return SoundPoolUtil.getInstance().play(RECOGNIZE_SUCCESS, 0) - countDownTimer?.cancel() +// countDownTimer?.cancel() + CountDownManager.cancelCountDown() goInitActivity() // loadLogInfo("collectFace,openPlate: userId=$userId") // getPlateData { items -> @@ -670,11 +556,13 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo } L.e("摄像头正常") } + binding.dualCameraFaceRectView.clearFaceInfo() facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true) if (facePreviewInfoList != null && rgbFaceRectTransformer != null) { drawPreviewInfo(facePreviewInfoList) } + recognizeViewModel.clearLeftFace(facePreviewInfoList) if (facePreviewInfoList.isNullOrEmpty()) { // currentPlateInfo = null currentUserId = null @@ -682,8 +570,41 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo binding.btnCollect.tag = "" binding.tvSimilarValue.text = "" retryFailCount = 0 + + if (binding.llCollectButton.isVisible) { + leftDebouncer.debounce { leftCountDown() } + } + return } - recognizeViewModel.clearLeftFace(facePreviewInfoList) +// val point = recognizeViewModel.loadPreviewSize() +// val faceInfoList = ArrayList() +// // 1. 检测人脸 +// recognizeViewModel.ftEngine?.detectFaces(nv21, point.x, point.y, FaceEngine.CP_PAF_NV21, faceInfoList) +// if (faceInfoList.isEmpty()) { +// recognizeViewModel.resetRecognitionState() +// return +// } +// val faceInfo = faceInfoList[0] +// // 2. 提取人脸特征 +// val faceFeature = FaceFeature(ByteArray(2056)) +//// val faceFeature = FaceFeature() +// // 手动初始化特征数组(满足源码里length≥2056的校验) +//// faceFeature.featureData = ByteArray(2056) +// var extractCode = recognizeViewModel.ftEngine?.extractFaceFeature(nv21, point.x, point.y, FaceEngine.CP_PAF_NV21, faceInfo, ExtractType.RECOGNIZE, FaceEngine.ASF_FACE_RECOGNITION, faceFeature) +// if (extractCode != ErrorInfo.MOK) { +// loadLogInfo("extractCode=$extractCode") +// return +// } +// +// +// // 3. 匹配人脸库(识别成功) +//// val matchedUserId = matchFaceFeature(faceFeature) // 你的人脸库匹配逻辑 +// val searchResult = recognizeViewModel.ftEngine.searchFaceFeature(faceFeature) +// val matchedUserId = searchResult?.faceFeatureInfo?.searchId +// if (matchedUserId != null) { +// // 核心:调用ViewModel的处理方法,强制触发通知 +// recognizeViewModel.handleRecognitionSuccess(faceInfo, matchedUserId.toString()) +// } } override fun onCameraClosed() { @@ -718,6 +639,27 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo rgbCameraHelper!!.start() } + // 第一步:查看相机输出格式(以Camera2 API为例) + fun getCameraImageFormat(point: Point): Int { + // 相机配置时记录输出格式 + val imageReader = ImageReader.newInstance(point.x, point.y, ImageFormat.NV21, 2) + Log.d("Camera", "相机输出格式:${imageReader.imageFormat}") // 打印格式值 + return imageReader.imageFormat + } + + // 第二步:匹配虹软SDK的格式常量(关键!) + fun getArcSoftImageFormat(cameraFormat: Int): Int { + return when (cameraFormat) { + ImageFormat.NV21 -> FaceEngine.CP_PAF_NV21 // 对应 + ImageFormat.YUV_420_888 -> FaceEngine.CP_PAF_NV21 // YUV420_888可转NV21 +// PixelFormat.RGBA_8888 -> FaceEngine.CP_PAF_RGBA // 若相机输出RGBA + else -> { + Log.e("Camera", "不支持的图像格式:$cameraFormat") + -1 // 无效格式 + } + } + } + /** * 绘制RGB、IR画面的实时人脸信息 * @@ -747,15 +689,48 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo } private fun resumeCamera() { - isRecognition = true +// isRecognition = true if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) { + isRecognition = true loadLogInfo("collectFace,resumeCamera: ") rgbCameraHelper!!.start() } else { - recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) + //recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) + + recognizeViewModel.onPreviewFrame(emptyFrame, true) + binding.dualCameraFaceRectView.clearFaceInfo() + recognizeViewModel.clearLeftFace(facePreviewInfoList) + binding.root.postDelayed({ + isRecognition = true + }, 600) } } + + /** + * 构造符合分辨率的 NV21 格式全黑帧 + * @param width 图像宽度(如 1920) + * @param height 图像高度(如 1080) + * @return 合法的全黑帧字节数组 + */ + 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()) + // 2. 填充 UV 分量(后 frameSize/2 个字节) + Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte()) + + return nv21Data + } + +// // 调用示例:构造 1920×1080 的全黑帧(长度 1382400) + val emptyFrame = createBlackNV21Frame(1920, 1080) +// // 传递“空帧” +// recognizeViewModel.onPreviewFrame(emptyFrame, true) + /** * 临时用户id */ @@ -780,6 +755,7 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo * 重置识别状态 */ private fun resetRecognizeState() { + binding.tvToInit.text = "15" binding.llCollectButton.gone() retryFailCount = 0 binding.tvSimilarValue.text = "" @@ -790,10 +766,26 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo tempUserId = "${System.currentTimeMillis()}" resumeCamera() registerHandler = RegisterCallbackHandler() - countDownTimer?.let { - it.cancel() - it.start() - } + + CountDownManager.startCountDown( + totalSeconds = 15, + 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 @@ -801,35 +793,31 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo protected override fun onPause() { pauseCamera() super.onPause() - countDownTimer?.cancel() + CountDownManager.cancelCountDown() +// countDownTimer?.cancel() } private fun pauseCamera() { isRecognition = false - recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) +// currentUserId = null +// userFaceInfo = null +// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) } - - private var countDownTimer: CountDownTimer? = null - private val totalTimeInMillis = 15 * 1000L - fun initCountTime() { - countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) { - override fun onTick(millisUntilFinished: Long) { - loadLogInfo("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}") - binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}" - } - - override fun onFinish() { - if (facePreviewInfoList.isNullOrEmpty()) { - goInitActivity() - } else { - recognizeFail() - } + 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() { + loadLogInfo("------------goInitActivity----------") val intent = Intent(context, InitActivity::class.java) startActivity(intent) } @@ -867,12 +855,6 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo uploadFaceDebouncer.debounce { addUserFace(faceData, imageFile) } -// // 给某个客户端发送 -// val jsonObject = JSONObject().also { -// it.put("type", "faceFeature") -// it.put("content", faceData) -// } -// tcpClient?.send(jsonObject) } private val mainViewModel by viewModels() @@ -944,7 +926,10 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") recognizeViewModel.refreshFaceList() loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------") - runOnUiThread { recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) } + runOnUiThread { +// recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) + recognizeViewModel.onPreviewFrame(emptyFrame, true) + } loadLogInfo("---------------------------------") collectCount = 0 } catch (e: Exception) { @@ -958,78 +943,6 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo private var collectCount = 0 -// private var tcpClient: TcpClient? = null -// -// private fun addSocketListener() { -//// val ipAddress = NetworkUtils.getIPAddress(true) -//// loadLogInfo( "addSocketListener,ipAddress: $ipAddress"); -// tcpClient = TcpClient( -// "192.168.1.95", -// 5000, -// GlobalData.deviceId, // clientId -// 5000, // connectTimeoutMs -// 10 * 1000, // heartbeatIntervalMs -// 30 * 1000 // heartbeatTimeoutMs -// ) -// -// tcpClient?.setListener(object : TcpClientListenerImpl() { -// override fun onSendSuccess(json: JSONObject?) { -// super.onSendSuccess(json) -// loadLogInfo("collectFace,TcpClientListener发送成功:$json") -//// toast("发送成功") -// } -// -// override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) { -// super.onSendFailed(json, e) -// ToastUtils.showToast("发送失败${e?.message}") -// loadLogInfo("collectFace,TcpClientListener人脸数据发送失败:${e?.message}") -// } -// -// override fun onMessage(json: JSONObject?) { -// super.onMessage(json) -// loadLogInfo("collectFace,TcpClientListener收到消息:$json") -// val type = json?.getInt("type") -// // 根据服务端消息类型处理删除新增临时用户人脸数据--------------------- -// when (type) { -// LanServer.TYPE_ADD_FACE_DATA -> { -// //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据 -// loadLogInfo("collectFace,TcpClientListener收到消息,新增用户:采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据") -// } -// -// LanServer.TYPE_CLEAR_FACE_DATA -> { -// clearFaceData() -// } -// -// else -> {} -// } -// } -// }) -// tcpClient?.start() -// -// try { -// LanServer.getInstance().let { -// it.setListener(object : LanServerListenerImpl() { -// override fun onMessageReceived(clientId: String?, message: JSONObject?) { -// super.onMessageReceived(clientId, message) -// ToastUtils.showToast("收到消息clientId=$clientId") -// } -// }) -// it.start() -// } -// } catch (e: Exception) { -// e.printStackTrace() -// } -// } - - private fun clearFaceData() { - Thread { - val faceDao = FaceDatabase.getInstance(this).faceDao() - faceDao.deleteTempUserFaceData() - recognizeViewModel.refreshFaceList() - loadLogInfo("collectFace,TcpClientListener收到消息,已删除并刷新人脸数据") - }.start() - } - private var isCheckCamera = false private var isCollectFace = false private val nv21ToBitmap by lazy { NV21ToBitmap(this) } @@ -1169,4 +1082,26 @@ class MainActivity : BaseActivity(), ViewTreeObserver.OnGlo finish() exitProcess(0) } + + private fun leftCountDown() { + CountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { + loadLogInfo("onPreview-----------leftCountDown-------------") + }, onTick = { + loadLogInfo("leftCountDown.onTick:$it") + if (facePreviewInfoList.isNullOrEmpty().not()) { + loadLogInfo("leftCountDown.onTick-cancel()") + CountDownUtil.cancelCountDown() + return@startCountDown + } + }, onFinish = { + if (facePreviewInfoList.isNullOrEmpty().not()) { + loadLogInfo("leftCountDown.onFinish-cancel()") + CountDownUtil.cancelCountDown() + return@startCountDown + } + CountDownUtil.cancelCountDown() + goInitActivity() + }) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/ext/Common.kt b/app/src/main/java/com/sw/face/collect/ext/Common.kt index d0ad511..a633225 100644 --- a/app/src/main/java/com/sw/face/collect/ext/Common.kt +++ b/app/src/main/java/com/sw/face/collect/ext/Common.kt @@ -155,13 +155,34 @@ fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) { } +//fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) { +// var job: Job? = null +// setOnClickListener { +// job?.cancel() +// job = CoroutineScope(Dispatchers.Main).launch { +// delay(delay) +// action() +// } +// } +//} + +/** + * 极简版防重复点击扩展函数 + * @param delay 防抖时间(默认300ms) + * @param action 点击执行逻辑 + */ fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) { - var job: Job? = null setOnClickListener { - job?.cancel() - job = CoroutineScope(Dispatchers.Main).launch { - delay(delay) - action() + // 用View的tag存储是否可点击的状态(默认可点击) + if (tag as? Boolean ?: true) { + tag = false // 标记为不可点击 + action() // 立即执行点击逻辑 + + // 启动协程,延迟后恢复可点击状态 + CoroutineScope(Dispatchers.Main).launch { + delay(delay) + tag = true // 恢复可点击 + } } } } diff --git a/app/src/main/java/com/sw/face/collect/utils/CountDownManager.kt b/app/src/main/java/com/sw/face/collect/utils/CountDownManager.kt new file mode 100644 index 0000000..2b146ae --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/CountDownManager.kt @@ -0,0 +1,135 @@ +package com.sw.face.collect.utils + +import android.os.CountDownTimer +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.util.Log + +/** + * 无状态残留的CountDownTimer封装(解决时间残留问题) + * @param millisInFuture 总时长(毫秒) + * @param countDownInterval 倒计时间隔(毫秒) + * @param onTick 倒计时回调(剩余毫秒) + * @param onFinish 结束回调 + */ +class SafeCountDownTimer( + private val millisInFuture: Long, + private val countDownInterval: Long, + private val onTick: (Long) -> Unit, + private val onFinish: () -> Unit +) { + // 独立Handler,避免与其他CountDownTimer复用消息队列 + private val handler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + synchronized(this@SafeCountDownTimer) { + // 仅当timer未取消时处理消息,避免残留消息触发 + if (!isCancelled) { + super.handleMessage(msg) + } + } + } + } + + // 核心:标记是否已取消,避免残留消息触发 + @Volatile + private var isCancelled = false + // 实际的CountDownTimer实例 + private var timer: CountDownTimer? = null + + /** + * 启动倒计时(启动前自动清空旧状态) + */ + fun start() { + // 第一步:强制取消旧实例+清空所有残留消息(核心!) + cancel() + + // 第二步:重置取消标记 + isCancelled = false + + // 第三步:新建CountDownTimer实例,绑定独立Handler + timer = object : CountDownTimer(millisInFuture, countDownInterval) { + override fun onTick(millisUntilFinished: Long) { + // 双重校验:仅未取消时触发回调 + if (!isCancelled) { + onTick.invoke(millisUntilFinished) + } + } + + override fun onFinish() { + if (!isCancelled) { + onFinish.invoke() + // 结束后主动清空,避免残留 + cancel() + } + } + } + + // 第四步:启动新实例 + timer?.start() + Log.d("SafeCountDownTimer", "倒计时启动,总时长:$millisInFuture 毫秒") + } + + /** + * 取消倒计时(彻底清空所有状态) + */ + fun cancel() { + // 标记为已取消,拦截后续消息 + isCancelled = true + + // 取消timer实例 + timer?.cancel() + timer = null + + // 清空Handler所有残留消息(核心:解决旧消息触发问题) + handler.removeCallbacksAndMessages(null) + + Log.d("SafeCountDownTimer", "倒计时已取消,清空所有残留状态") + } +} + +// ========== 封装成工具类,方便全局调用 ========== +object CountDownManager { + // 保存当前倒计时实例,避免重复启动 + private var currentTimer: SafeCountDownTimer? = null + + /** + * 启动/重启倒计时(适配人脸识别防抖场景) + * @param totalSeconds 总秒数(如15) + * @param onTick 每秒回调剩余秒数 + * @param onFinish 结束回调 + */ + fun startCountDown( + totalSeconds: Int = 15, + onTick: (Int) -> Unit = {}, + onFinish: () -> Unit + ) { + // 先取消旧倒计时 + cancelCountDown() + + // 新建安全倒计时实例 + currentTimer = SafeCountDownTimer( + millisInFuture = totalSeconds * 1000L, + countDownInterval = 1000L, + onTick = { millis -> + // 转换为秒数回调 + val sec = (millis / 1000).toInt() + 1 + onTick.invoke(sec) + }, + onFinish = { + onFinish.invoke() + } + ) + + // 启动倒计时 + currentTimer?.start() + } + + /** + * 取消当前倒计时 + */ + fun cancelCountDown() { + currentTimer?.cancel() + currentTimer = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/CountDownUtil.kt b/app/src/main/java/com/sw/face/collect/utils/CountDownUtil.kt new file mode 100644 index 0000000..846f44d --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/utils/CountDownUtil.kt @@ -0,0 +1,88 @@ +package com.sw.face.collect.utils + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import android.util.Log +import java.util.concurrent.atomic.AtomicBoolean + +/** + * 人脸识别防抖倒计时工具类(单例+防重复+无死循环) + */ +object CountDownUtil { + // 原子布尔值:标记是否正在倒计时(防重复启动) + private val isCounting = AtomicBoolean(false) + + // 当前倒计时Job(用于取消) + private var currentJob: Job? = null + + /** + * 启动/重启防抖倒计时(同一时间仅一个倒计时运行) + * @param totalSeconds 倒计时总秒数(如15) + * @param lifecycleScope Activity/Fragment的lifecycleScope(必须) + * @param onTick 每秒回调剩余秒数(主线程) + * @param onFinish 倒计时结束回调(主线程) + */ + fun startCountDown( + total: Int = 15, + lifecycleScope: CoroutineScope, + onStart: () -> Unit = {}, + onTick: (Int) -> Unit = {}, + onFinish: () -> Unit + ) { + // 1. 防重复:如果正在倒计时,先取消旧任务 + if (isCounting.get()) { + //cancelCountDown() + return + } + + // 2. 标记为正在倒计时 + isCounting.set(true) + + // 3. 启动新倒计时 + currentJob = flow { + Log.d("CountDownUtil", "倒计时启动,总时长:$total 秒") + for (i in total downTo 1) { + emit(i) + delay(1000) // IO线程等待1秒,无死循环 + } + } + .flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程 + .onStart { + withContext(Dispatchers.Main) { + onStart.invoke() + } + } + .onEach { remaining -> + // 主线程回调剩余时间 + withContext(Dispatchers.Main) { + onTick.invoke(remaining) + } + } + .onCompletion { + // 倒计时结束:重置状态+回调 + withContext(Dispatchers.Main) { + isCounting.set(false) // 重置标记 + currentJob = null // 置空Job + Log.d("CountDownUtil", "倒计时正常结束") + onFinish.invoke() + } + } + .catch { e -> + // 捕获异常:避免崩溃,重置状态 + Log.e("CountDownUtil", "倒计时异常:${e.message}", e) + isCounting.set(false) + currentJob = null + } + .launchIn(lifecycleScope) + } + + /** + * 取消倒计时(手动停止) + */ + fun cancelCountDown() { + currentJob?.cancel() // 取消Job + currentJob = null // 置空,避免残留 + isCounting.set(false) // 重置标记 + Log.d("CountDownUtil", "倒计时被手动取消") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt b/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt index 57f9d44..5de1df3 100644 --- a/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt +++ b/app/src/main/java/com/sw/face/collect/utils/FlowCountDown.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext // 倒计时Flow扩展函数 fun countDownByFlow( @@ -20,14 +21,20 @@ fun countDownByFlow( onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null ): Job { + require(total > 0) { "倒计时总秒数必须大于0" } return flow { - for (i in total downTo 0) { + for (i in total downTo 1) { emit(i) - if (i != 0) delay(1000) +// if (i != 0) + delay(1000) } - }.flowOn(Dispatchers.Main) - .onStart { onStart?.invoke() } - .onCompletion { onFinish?.invoke() } - .onEach { onTick.invoke(it) } - .launchIn(scope) + } + .flowOn(Dispatchers.IO) +// .flowOn(Dispatchers.Main) + .onStart { withContext(Dispatchers.Main) { onStart?.invoke() } } + .onCompletion { + withContext(Dispatchers.Main) { { onFinish?.invoke() } } + } + .onEach { withContext(Dispatchers.Main) { onTick.invoke(it) } } + .launchIn(CoroutineScope(scope.coroutineContext + Dispatchers.Main)) } diff --git a/app/src/main/java/com/sw/face/collect/viewmodel/FaceRecognizeViewModel.kt b/app/src/main/java/com/sw/face/collect/viewmodel/FaceRecognizeViewModel.kt new file mode 100644 index 0000000..aeffa5b --- /dev/null +++ b/app/src/main/java/com/sw/face/collect/viewmodel/FaceRecognizeViewModel.kt @@ -0,0 +1,75 @@ +package com.sw.face.collect.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.arcsoft.face.FaceInfo +import com.sw.plate.utils.arcface.face.model.CompareResult +import com.sw.plate.utils.arcface.face.model.FacePreviewInfo +import com.sw.plate.utils.arcface.facedb.entity.FaceEntity +import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel + +class FaceRecognizeViewModel: RecognizeViewModel() { + + // 2. 新增:识别事件触发标记(即使userId不变,也能通知) + private val _recognitionTriggered = MutableLiveData() + val recognitionTriggered: LiveData = _recognitionTriggered + + // 辅助:存储当前锁定的trackId(防止重复识别) + private var lockedTrackId: Int? = null + // 防抖时间(可配置,比如3秒) + private val ANTI_SHAKE_TIME = 3000L + // 存储最后一次识别时间,用于防抖 + private var lastRecognitionTime = 0L + + // 识别结果数据类:包含userId+是否新触发 + data class RecognitionResult( + val userId: String, + val isNewRecognition: Boolean = true + ) + + /** + * 核心:处理识别结果(强制触发通知) + */ + fun handleRecognitionSuccess(faceInfo: FaceInfo, userId: String) { + val currentTime = System.currentTimeMillis() +// val currentTrackId = faceInfo.trackId + val currentTrackId = faceInfo.faceId + + // 1. 防抖判断:同一人脸3秒内不重复触发(可根据需求调整) + if (currentTrackId == lockedTrackId && currentTime - lastRecognitionTime < ANTI_SHAKE_TIME) { + // 可选:即使防抖,也强制通知(仅更新事件,不重复执行业务) + _recognitionTriggered.postValue(RecognitionResult(userId, isNewRecognition = false)) + return + } + + // 2. 重置锁定状态 + lockedTrackId = currentTrackId + lastRecognitionTime = currentTime + + // 3. 更新userId(触发原有observe) + val entity = FaceEntity(userId, null, faceInfo.faceData) + recognizeUserId.postValue(CompareResult(entity, 9.0f)) + // 4. 强制触发识别事件通知(核心:即使userId不变,也通知) + _recognitionTriggered.postValue(RecognitionResult(userId)) + } + /** + * 重置识别状态(人脸离开/重新进屏时调用) + */ + fun resetRecognitionState() { + lockedTrackId = null + lastRecognitionTime = 0L + // 清空LiveData,为下次识别做准备 + recognizeUserId.postValue(null) + _recognitionTriggered.postValue(null) + } + + /** + * 手动触发重新识别(比如按钮点击) + */ + fun triggerReRecognition() { + resetRecognitionState() + // 可选:重置虹软SDK跟踪上下文 + //faceEngine?.faceEngineReset() + } + +} \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java index c58ba6e..8ffa074 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java @@ -47,6 +47,10 @@ public class FaceRectView extends View { } } + public int getRectColor() { + return paint.getColor(); + } + public void clearFaceInfo() { drawInfoList.clear(); postInvalidate(); diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java index 10200c9..81f0e16 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/viewmodel/RecognizeViewModel.java @@ -141,7 +141,7 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback { /** * VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测 */ - private FaceEngine ftEngine; + public FaceEngine ftEngine; /** * 用于特征提取的引擎 */