Files
SmartPlateCabinet/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt
T
lvmeng 95b4f5e8f2 feat(network): 添加API V2版本支持并重构网络层
- 新增ApiServiceV2接口定义,支持人脸缓存、增量数据、设备配置等功能
- 添加NetViewModelV2视图模型,实现新版本API的数据处理逻辑
- 创建RemoteRepositoryV2仓库,封装API V2的网络请求操作
- 新增UserFaceModelV2数据模型,优化人脸特征数据结构
- 添加DeviceConfigV2设备配置模型,支持更多设备参数
- 在ApiClient中注册apiServiceV2实例
- 更新BaseActivity使用NetViewModelV2替代UserViewModel
- 修改DeviceInitActivity中的人脸数据获取和设备配置逻辑
- 重构FaceDao数据库访问对象,优化用户类型查询和删除操作
- 更新FaceEntity实体类,增加更多用户相关字段
- 移除BaseViewModel中的旧版网络基础功能代码
- 优化人脸数据同步和存储流程,提升性能表现
2026-06-30 10:08:10 +08:00

990 lines
39 KiB
Kotlin
Raw Blame History

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