识别成功未提示及采集时间长问题优化
This commit is contained in:
@@ -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<ActivityMainBinding>(), ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
public val recognizeViewModel by viewModels<RecognizeViewModel>()
|
||||
// public val userViewModel by viewModels<UserViewModel>()
|
||||
|
||||
public val recognizeViewModel by viewModels<FaceRecognizeViewModel>()
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LoginByFaceActivity"
|
||||
@@ -94,56 +96,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
|
||||
override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater)
|
||||
|
||||
// private var logList: MutableList<String> = 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<String, Int>()
|
||||
soundMap[COLLECT_SUCCESS] = R.raw.collect_success
|
||||
@@ -157,6 +115,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
//采集数据按钮
|
||||
retryFailCount = 0
|
||||
isCollectFace = true
|
||||
isCollecting.set(true)
|
||||
collectFace()
|
||||
}
|
||||
|
||||
@@ -166,7 +125,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun checkCameraPermission() {
|
||||
//Timber.i("checkCameraPermission")
|
||||
val permissionHelper =
|
||||
@@ -211,6 +169,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
loadLogInfo("collectFace 已在采集中,忽略重复触发")
|
||||
return
|
||||
}
|
||||
loadLogInfo("recognizeFail---maxFailCount")
|
||||
recognizeFail()
|
||||
return
|
||||
}
|
||||
@@ -386,7 +364,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
}
|
||||
|
||||
private fun recognizeFail() {
|
||||
countDownTimer?.cancel()
|
||||
CountDownManager.cancelCountDown()
|
||||
binding.tvToInit.text = ""
|
||||
|
||||
binding.llCollectButton.visible()
|
||||
@@ -394,102 +372,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<FaceInfo>()
|
||||
// // 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), ViewTreeObserver.OnGlo
|
||||
* 重置识别状态
|
||||
*/
|
||||
private fun resetRecognizeState() {
|
||||
binding.tvToInit.text = "15"
|
||||
binding.llCollectButton.gone()
|
||||
retryFailCount = 0
|
||||
binding.tvSimilarValue.text = ""
|
||||
@@ -790,10 +766,26 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<MainViewModel>()
|
||||
@@ -944,7 +926,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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<ActivityMainBinding>(), 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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 // 恢复可点击
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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", "倒计时被手动取消")
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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<RecognitionResult?>()
|
||||
val recognitionTriggered: LiveData<RecognitionResult?> = _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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -47,6 +47,10 @@ public class FaceRectView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
public int getRectColor() {
|
||||
return paint.getColor();
|
||||
}
|
||||
|
||||
public void clearFaceInfo() {
|
||||
drawInfoList.clear();
|
||||
postInvalidate();
|
||||
|
||||
@@ -141,7 +141,7 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
|
||||
/**
|
||||
* VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测
|
||||
*/
|
||||
private FaceEngine ftEngine;
|
||||
public FaceEngine ftEngine;
|
||||
/**
|
||||
* 用于特征提取的引擎
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user