识别成功未提示及采集时间长问题优化

This commit is contained in:
2026-02-28 10:03:04 +08:00
parent dcfb684b66
commit 3768bd94f2
8 changed files with 558 additions and 293 deletions
@@ -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;
/**
* 用于特征提取的引擎
*/