This commit is contained in:
2026-02-03 19:01:57 +08:00
parent c7cad6d8b5
commit 96e795c749
21 changed files with 1435 additions and 262 deletions
+1
View File
@@ -51,6 +51,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.sw.face.collect.InitActivity" />
</application>
</manifest>
@@ -0,0 +1,14 @@
package com.sw.face.collect
import com.sw.face.collect.base.BaseActivity
import com.sw.face.collect.databinding.ActivityInitBinding
import com.sw.face.collect.databinding.ActivityMainBinding
class InitActivity : BaseActivity<ActivityInitBinding>() {
override fun inflateViewBinding() = ActivityInitBinding.inflate(layoutInflater)
override fun initialize() {
super.initialize()
}
}
@@ -3,8 +3,10 @@ package com.sw.face.collect
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Point
import android.hardware.Camera
import android.os.CountDownTimer
import android.util.Log
import android.view.View
import android.view.ViewGroup
@@ -23,17 +25,15 @@ 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.socket.LanServer
import com.sw.face.collect.socket.TcpClient
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.Debouncer
import com.sw.face.collect.utils.FaceEngineUtils
import com.sw.face.collect.utils.IntervalExecutor
import com.sw.face.collect.utils.SpTool
import com.sw.face.collect.utils.countDownByFlow
import com.sw.face.collect.view.LanServerListenerImpl
import com.sw.face.collect.view.TcpClientListenerImpl
import com.sw.face.collect.utils.SoundPoolUtil
import com.sw.face.collect.viewmodel.MainViewModel
import com.sw.inbound.utils.DateTimeUtils
import com.sw.plate.utils.L
import com.sw.plate.utils.NV21ToBitmap
import com.sw.plate.utils.ToastUtils
@@ -57,6 +57,8 @@ import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel.REGISTER_STATUS_R
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.getValue
import kotlin.system.exitProcess
@@ -65,6 +67,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
companion object {
private const val TAG = "MainActivity"
private const val COLLECT_SUCCESS = "collect_success"
private const val RECOGNIZE_SUCCESS = "recognize_success"
private const val NO_PLATE_REMIND = "no_plate_remind";
}
// 虹软人脸配置 ⬇
@@ -101,20 +107,19 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
private var pageNo = 1
override fun initialize() {
super.initialize()
mainViewModel.getUserFaceCache(pageNo = pageNo)
//mainViewModel.getUserFaceCache(pageNo = pageNo)
FaceEngineUtils.activeEngine()
binding.btnCollectFace.setOnClickListener {
requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
if (granted.not()) {
toast("没有相机权限")
return@requestSinglePermissionResult
}
collectFace()
}
}
// binding.btnCollectFace.setOnClickListener {
// requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
// if (granted.not()) {
// toast("没有相机权限")
// return@requestSinglePermissionResult
// }
// }
// }
binding.tvTitle.setOnClickListener { finish() }
addSocketListener()
//addSocketListener()
binding.flFace.visible()
binding.flRecognizeIr.visible()
@@ -123,9 +128,24 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
binding.layoutState.gone()
// resumeCamera()
startFaceTask()
}
// startFaceTask()
// startActivity(Intent(this, InitActivity::class.java))
initCountTime()
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)
}
// requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
// if (granted.not()) {
// toast("没有相机权限")
// return@requestSinglePermissionResult
// }
// }
private fun openCamera() {
Log.d(TAG, "openCamera")
@@ -167,152 +187,118 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
openRectInfoDraw = true
}
private fun sendFaceData(faceData: String) {
// 给某个客户端发送
val jsonObject = JSONObject().also {
it.put("type", "faceFeature")
it.put("content", faceData)
}
tcpClient?.send(jsonObject)
/**
* 改为上传人脸数据,不需要发生人脸数据到收银端
*/
private fun sendFaceData(faceData: String, headBmp: Bitmap) {
loadLogInfo("collectFace,sendFaceData: ----------11,发送人脸数据-----------")
// TODO: 保存采集的人脸数据,暂时生成id,实际id在结算终端生成并发送到各台设备
saveFaceInfo(faceData)
}
private fun saveFaceInfo(faceData: String) {
Thread {
try {
val tempId = "${System.currentTimeMillis()}-123456789"
val faceEntity = FaceEntity(
tempId,
null,
Base64.decode(faceData)
).also {
it.userType = "2"
}
FaceDatabase.getInstance(this).faceDao().insert(faceEntity)
recognizeViewModel.refreshFaceList();
runOnUiThread {
toast("你的人脸信息已采集")
}
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
private var tcpClient: TcpClient? = null
private fun addSocketListener() {
//val ipAddress = NetworkUtils.getIPAddress(true)
//Log.d(TAG, "addSocketListener,ipAddress: $ipAddress");
tcpClient = TcpClient(
"192.168.1.95",
5000,
GlobalData.deviceId, // clientId
5000, // connectTimeoutMs
10 * 1000, // heartbeatIntervalMs
30 * 1000 // heartbeatTimeoutMs
val imageFile = BitmapSaver.saveToAppFilesDir(
headBmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
tcpClient?.setListener(object : TcpClientListenerImpl() {
override fun onSendSuccess(json: JSONObject?) {
super.onSendSuccess(json)
// toast("发送成功")
}
override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) {
super.onSendFailed(json, e)
toast("发送失败${e?.message}")
}
override fun onMessage(json: JSONObject?) {
super.onMessage(json)
val type = json?.getInt("type")
// 根据服务端消息类型处理删除新增临时用户人脸数据---------------------
when (type) {
LanServer.TYPE_ADD_FACE_DATA -> {
//采集设备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)
toast("收到消息clientId=$clientId")
}
})
it.start()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
// private var imageBitmap: Bitmap? = null
private fun collectFace() {
//toast("currentUserId=$currentUserId")
if (currentUserId != null) {
toast("您已采集过人脸信息")
loadLogInfo("collectFace,sendFaceData:${imageFile?.absolutePath}")
if (imageFile == null) {
ToastUtils.showToast("获取人脸照片失败")
return
}
currentUserId = null
userFaceInfo = null
recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
//showWaitingDialog("人脸信息中……")
uploadFaceDebouncer.debounce {
addUserFace(faceData, imageFile)
}
// // 给某个客户端发送
// val jsonObject = JSONObject().also {
// it.put("type", "faceFeature")
// it.put("content", faceData)
// }
// tcpClient?.send(jsonObject)
}
binding.tvFaceTip.gone()
binding.btnCollectFace.gone()
binding.layoutState.visible()
binding.pbCollectLoading.visible()
binding.tvCollectState.text = "采集中......"
getFaceData { faceData ->
Log.d(TAG, "faceData: $faceData")
runOnUiThread {
//hideWaitingDialog()
//toast("人脸信息已采集")
binding.pbCollectLoading.gone()
//binding.tvCollectState.text = "采集完成"
userFaceInfo = null
countDown()
private var collectCount = 0
private fun saveFaceInfo(faceModel: UserFaceModel) {
try {
loadLogInfo("collectFace,saveFaceInfo: ----------8,保存人脸取特征数据-----------")
val faceDao = FaceDatabase.getInstance(this).faceDao()
val faceEntity = FaceEntity(
faceModel.userId,
null,
com.sw.plate.utils.Base64.decode(faceModel.faceFeatureStr)
).also {
//1-会员、2-临时用户
it.userType = if (faceModel.isMember) "1" else "2"
}
sendFaceData(faceData)
faceDao.insert(faceEntity)
loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------")
recognizeViewModel.refreshFaceList()
loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------")
runOnUiThread { recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) }
loadLogInfo("---------------------------------")
collectCount = 0
} 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 job: Job? = null
private fun getFaceData(block: (String) -> Unit) {
runBlocking {
job = executor.startIntervalTask(5) {
// if (currentUserId != null && currentUserId!!.isNotBlank()) {
// toast("您已采集过人脸信息")
// return@startIntervalTask
// private fun addSocketListener() {
// //val ipAddress = NetworkUtils.getIPAddress(true)
// //Log.d(TAG, "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)
//// toast("发送成功")
// }
//
// override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) {
// super.onSendFailed(json, e)
// toast("发送失败${e?.message}")
// }
//
// override fun onMessage(json: JSONObject?) {
// super.onMessage(json)
//
// val type = json?.getInt("type")
// // 根据服务端消息类型处理删除新增临时用户人脸数据---------------------
// when (type) {
// LanServer.TYPE_ADD_FACE_DATA -> {
// //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据
// }
// LanServer.TYPE_CLEAR_FACE_DATA -> {
// clearFaceData()
// }
// else-> {}
// }
if (userFaceInfo == null) {
return@startIntervalTask
}
val faceData = Base64.encode(userFaceInfo!!.faceFeature!!.featureData)
block(faceData)
job?.cancel()
}
}
}
// }
// })
// tcpClient?.start();
//
// try {
// LanServer.getInstance().let {
// it.setListener(object : LanServerListenerImpl() {
// override fun onMessageReceived(clientId: String?, message: JSONObject?) {
// super.onMessageReceived(clientId, message)
// toast("收到消息clientId=$clientId")
// }
// })
// it.start()
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
// }
val executor = IntervalExecutor()
public override fun onStop() {
rgbCameraHelper?.release()
rgbCameraHelper = null
@@ -339,6 +325,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
private var userFaceInfo: UserFaceInfo? = null
private var isCheckCamera = false
private var isCollectFace = false
private val isCollecting = AtomicBoolean(false)
private fun initArcViewModel() {
if (DualCameraHelper.hasDualCamera()) {
livenessType = LivenessType.IR
@@ -351,8 +339,22 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
recognizeViewModel.prepareRegister()
recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo ->
this.userFaceInfo = userFaceInfo
Log.d(TAG, "initArcViewModel: userFaceInfo == null : ${userFaceInfo == null}")
if (!isCollectFace) {
return@setOnRegisterFinishedCallback
}
if (this.userFaceInfo == null && userFaceInfo != null) {
loadLogInfo("collectFace:开始采集------------")
recognizeViewModel.updateRegisterStatus(RecognizeViewModel.REGISTER_STATUS_DONE)
this.userFaceInfo = userFaceInfo
getFaceDataBlock { faceData, headBmp ->
isCollecting.set(false)
isCollectFace = false
loadLogInfo("collectFace:采集完成,准备上传------------")
sendFaceData(faceData, headBmp)
}
return@setOnRegisterFinishedCallback
}
}
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
@@ -395,20 +397,20 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
recognizeViewModel.recognizeUserId.observe(
this,
Observer { compareResult: CompareResult ->
Log.d(
TAG,
"recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}"
)
lastFaceTrackId = compareResult.trackId
val faceEntity = compareResult.faceEntity
currentUserId = faceEntity.userName
if (!currentUserId.isNullOrBlank()) {
toast("您已采集过人脸信息")
return@Observer
}
// //未识别到,拍摄照片
Observer { compareResult: CompareResult? ->
// Log.d(
// TAG,
// "recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}"
// )
// lastFaceTrackId = compareResult.trackId
// val faceEntity = compareResult.faceEntity
// currentUserId = faceEntity.userName
// if (!currentUserId.isNullOrBlank()) {
// toast("您已采集过人脸信息")
// return@Observer
// }
//// //未识别到,拍摄照片
loadFaceRecognizeResult2(compareResult)
})
recognizeViewModel.drawRectInfoText.observe(this, Observer { info ->
@@ -416,6 +418,112 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
})
}
/**
* 识别失败重试次数
*/
private var retryFailCount = 0
/**
* 识别失败最大次数
*/
private val maxFailCount = 5
/**
* 人脸失败阈值
*/
private val faceFailThreshold = 0.3
/**
* 配置阈值,默认值为0.8
*/
private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) }
/**
* 根据相似度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
// // 防抖,防止重复开闸
Debouncer(5000).debounce {
countDownTimer?.cancel()
binding.tvCountDown.text = ""
SoundPoolUtil.getInstance().play(RECOGNIZE_SUCCESS, 0)
//openPlate(userId)
//toast("打开餐盘,用户id=$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()//采集人脸
// toast("开始采集人脸")
return
}
retryFailCount++
loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount")
}
/**
* 采集人脸
*/
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,人脸采集中-----------")
countDown()
}
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
@@ -545,6 +653,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
) {
drawPreviewInfo(facePreviewInfoList)
}
if (facePreviewInfoList.isNullOrEmpty()) {
binding.tvSimilarValue.text = ""
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
@@ -766,59 +877,58 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
override fun onDestroy() {
countDownJob?.cancel()
faceTaskJob?.cancel()
super.onDestroy()
}
private var countDownJob: Job? = null
private fun countDown() {
countDownJob = countDownByFlow(
total = 3,
scope = lifecycleScope,
onStart = {
binding.tvCollectState.text = "采集完成(3s"
},
onTick = { seconds ->
binding.tvCollectState.text = "采集完成(${seconds}s"
},
onFinish = {
//binding.tvCollectState.text = "采集完成"
currentUserId = null
binding.layoutState.gone()
binding.tvFaceTip.visible()
binding.btnCollectFace.visible()
}
)
}
// private var countDownJob: Job? = null
// private fun countDown() {
// countDownJob = countDownByFlow(
// total = 3,
// scope = lifecycleScope,
// onStart = {
// binding.tvCollectState.text = "采集完成(3s"
// },
// onTick = { seconds ->
// binding.tvCollectState.text = "采集完成(${seconds}s"
// },
// onFinish = {
// //binding.tvCollectState.text = "采集完成"
// currentUserId = null
// binding.layoutState.gone()
// binding.tvFaceTip.visible()
//// binding.btnCollectFace.visible()
// }
// )
// }
private val intervalExecutor by lazy { IntervalExecutor() }
private var faceTaskJob: Job? = null
private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(60 * 1000L, 30 * 1000L) {
val timestamp = SpTool.getLastFaceTimestamp()
if (timestamp == 0L) {
return@startIntervalTaskWithInitialDelay
}
mainViewModel.getFaceIncrementList(
pageNo = taskPageNo,
timestamp = timestamp,
onAllQueryFinished = {
taskPageNo = 1
},
onPageQueryFinished = { list ->
runOnUiThread {
if (list.isEmpty()) {
return@runOnUiThread
}
updateFaceData(list)
}
}
)
}
}
// private var faceTaskJob: Job? = null
//
// private var taskPageNo = 1
// fun startFaceTask() {
// faceTaskJob =
// intervalExecutor.startIntervalTaskWithInitialDelay(60 * 1000L, 30 * 1000L) {
// val timestamp = SpTool.getLastFaceTimestamp()
// if (timestamp == 0L) {
// return@startIntervalTaskWithInitialDelay
// }
// mainViewModel.getFaceIncrementList(
// pageNo = taskPageNo,
// timestamp = timestamp,
// onAllQueryFinished = {
// taskPageNo = 1
// },
// onPageQueryFinished = { list ->
// runOnUiThread {
// if (list.isEmpty()) {
// return@runOnUiThread
// }
// updateFaceData(list)
// }
// }
// )
// }
// }
private fun updateFaceData(list: List<UserFaceModel>) {
Thread {
@@ -854,16 +964,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
}.start()
}
// private var dinnerTypeJob: Job? = null
// fun startDinnerTypeTask() {
// dinnerTypeJob =
// intervalExecutor.startIntervalTaskWithInitialDelay(10 * 60 * 1000L, 15 * 60 * 1000L) {
// mainViewModel.getDinnerType {
//
// }
// }
// }
private fun clearFaceData() {
Thread {
val faceDao = FaceDatabase.getInstance(this).faceDao()
@@ -880,8 +980,96 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
}
override fun onPause() {
countDownTimer?.cancel()
super.onPause()
pauseCamera()
}
private fun addUserFace(faceData: String, imageFile: File) {
mainViewModel.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
}
Thread {
saveFaceInfo(faceModel)
uploadFaceDebouncer.reset()
}.start()
}
}
private val uploadFaceDebouncer = Debouncer(10000)
private fun getFaceDataBlock(block: (String, Bitmap) -> Unit) {
if (userFaceInfo == null || userFaceInfo?.faceFeature?.featureData == null) {
loadLogInfo("collectFace:数据为空------11111------")
return
}
val faceData: String? = com.sw.plate.utils.Base64.encode(userFaceInfo?.faceFeature?.featureData)
if (faceData.isNullOrBlank()) {
loadLogInfo("collectFace:数据为空------22222------")
return
}
loadLogInfo("collectFace,getFaceData: ----------7,拿到人脸提取特征数据时间:${DateTimeUtils.getDateTimeString()}-----------")
block(faceData, userFaceInfo!!.headBmp)
}
/**
* 加载日志
* @param msg 日志内容
*/
private fun loadLogInfo(msg: String) {
// Timber.tag(TAG).d(msg)
// LogFileUtil.saveLog(msg)
Log.d(TAG, "loadLogInfo: $msg")
}
private val total = 30
private var countDownJob: Job? = null
private fun countDown() {
// countDownJob = countDownByFlow(
// total = total,
// scope = lifecycleScope,
// onStart = {
// binding.tvCountDown.text = "${total}s"
// },
// onTick = { seconds ->
// binding.tvCountDown.text = "${seconds}s"
// },
// onFinish = {
// }
// )
if (countDownTimer == null) {
initCountTime()
}
countDownDebounce.debounce {
countDownTimer?.start()
}
}
private val countDownDebounce = Debouncer(10000)
private var countDownTimer:CountDownTimer?=null
private val totalTimeInMillis = 30 * 1000L
fun initCountTime() {
countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
loadLogInfo("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}")
binding.tvCountDown.text = "${(millisUntilFinished / 1000).toInt() + 1}s"
}
override fun onFinish() {
}
}
}
}
@@ -21,7 +21,16 @@ data class UserFaceModel(
val userId: String? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp: Long?=null,
val faceDeleted: Boolean? = false
val faceDeleted: Boolean? = false,
/**
* 会员编号
*/
val cardNo: String,
/**
* 是否会员
*/
val isMember: Boolean
)
// : Parcelable
@@ -7,6 +7,8 @@ import com.sw.face.collect.model.UserFaceModel
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
@@ -42,4 +44,23 @@ interface ApiService {
suspend fun getDinnerType(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getRegionRule",
): ApiResponse<DinnerType?>
@Multipart
@POST
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload",
@Part file: MultipartBody.Part,
): ApiResponse<String?>
/**
* 添加人脸数据
*/
@POST
@FormUrlEncoded
suspend fun addUserFace(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/addUserByFace",
@Field("featureChar") faceData:String,
@Field("url") imageUr:String
): ApiResponse<UserFaceModel?>
}
@@ -5,6 +5,8 @@ import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.DinnerType
import com.sw.face.collect.model.UserFaceModel
import com.sw.face.collect.network.api.ApiService
import com.sw.face.collect.utils.FileUtils
import java.io.File
/**
* 远程数据处理
@@ -57,4 +59,20 @@ class RemoteRepository constructor(
}
}
suspend fun uploadImage(file: File): ApiResponse<String?> {
return safeApiCall {
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
apiService.uploadImage(file = part)
}
}
}
suspend fun addUserFace(faceData: String, imageUr:String): ApiResponse<UserFaceModel?> {
return safeApiCall { apiService.addUserFace(faceData = faceData, imageUr = imageUr) }
}
}
@@ -0,0 +1,59 @@
package com.sw.face.collect.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.io.use
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = context.cacheDir
val cropFile = File(dir, "crop")
if (cropFile.exists().not()) {
cropFile.mkdirs()
}
return saveBitmap(bitmap, File(cropFile, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,122 @@
package com.sw.inbound.utils
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
//import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.to
/**
* 时间格式化工具类
*/
object DateTimeUtils {
/**
* 获取完整中文日期格式(示例:2025年6月11日 星期三)
*/
fun getChineseDateString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
}
/**
* 获取标准时间格式
*/
fun getDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(date)
}
/**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
*/
fun getChineseDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE HH:mm:ss", Locale.CHINA).format(date)
}
// 使用线程安全的日期格式化(避免 SimpleDateFormat 的线程安全问题)
private val dateFormat by lazy {
SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA)
}
private val timeFormat by lazy {
SimpleDateFormat("HH:mm:ss", Locale.CHINA)
}
fun getChineseDateTimePair(date: Date = Date()): Pair<String, String> {
return dateFormat.format(date) to timeFormat.format(date)
}
// /**
// * 实时时间流(每秒更新)
// * @param intervalMillis 更新间隔(默认1秒)
// */
// fun realTimeChineseDateFlow(intervalMillis: Long = 1000) = flow {
// while (true) {
// FlowCollector.emit(getChineseDateTimePair())
// delay(intervalMillis)
// }
// }
/**
* 解析日期时间字符串
* @param timeString 格式为 "yyyy-MM-dd HH:mm:ss" 的字符串
* @return Date 对象,解析失败返回 null
*/
fun parseDateTime(timeString: String?): Date? {
return try {
if (timeString == null) return null
val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
format.parse(timeString)
} catch (e: Exception) {
// e.printStackTrace()
// Timber.e(e.message)
null
}
}
/**
* 判断给定时间是否距离当前时间超过72小时
* @param timeInMillis 时间戳(毫秒)
* @return true 表示超过72小时,false 表示未超过
*/
fun isMoreThanHoursFromNow(timeInMillis: Long): Boolean {
val currentTime = System.currentTimeMillis()
val timeDifference = currentTime - timeInMillis
val hoursDifference = timeDifference / (1000 * 60 * 60) // 毫秒转小时
return hoursDifference >= 72
}
/**
* 获取时间间隔描述
*/
fun getTimeAgo(date: Date?): String {
if (date == null) return "未知时间"
val now = Date()
val diffMillis = now.time - date.time
// 如果是未来时间
if (diffMillis < 0) {
val futureHours = abs(diffMillis) / (1000 * 60 * 60)
return if (futureHours < 24) {
"未来 $futureHours 小时"
} else {
val days = futureHours / 24
"未来 $days"
}
}
// 过去时间
val hours = diffMillis / (1000 * 60 * 60)
return when {
hours < 1 -> "刚刚"
hours < 24 -> "${hours}小时前"
else -> {
val days = hours / 24
"${days}天前"
}
}
}
}
@@ -0,0 +1,163 @@
package com.sw.face.collect.utils
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
// import Timber.log.Timber
import java.io.File
import kotlin.io.copyTo
import kotlin.io.outputStream
import kotlin.io.use
import kotlin.text.equals
object FileUtils {
/**
* 从Uri获取File
* example: file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
*/
private fun getFileFromUri(context: Context, uri: Uri): File? {
//Timber.d("getFileFromUri uri = ${uri.scheme}")
return when (uri.scheme) {
"file" -> File(uri.path ?: return null)
"content" -> {
try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val cacheDir = context.cacheDir
val file = File.createTempFile(
"upload_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
file.outputStream().use { output ->
inputStream.copyTo(output)
}
file
} catch (e: Exception) {
//Timber.e(e)
null
}
}
else -> null
}
}
/**
* 通过uri生成http请求体
*/
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
//Timber.d("genRequestPart imageUri = $imageUri")
// 1. 从Uri获取文件
val file = getFileFromUri(context, imageUri)
return genRequestPart(file)
}
fun genRequestPart(file: File?): MultipartBody.Part? {
if (file == null) {
//Timber.e("getFileFromUri file is null")
return null
}
// 2. 创建请求体
val requestFile = file
.asRequestBody("application/octet-stream".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData(
"file",
file.name,
requestFile
)
return imagePart
}
/**
* 通过Uri删除文件
* @param context 上下文
* @param uri 文件Uri
* @return Boolean 是否删除成功
*/
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
//Timber.d("deleteFileWithUri uri = ${uri.scheme}")
return when {
// 1. 处理 content:// 类型的Uri (MediaStore)
uri.scheme.equals("content", ignoreCase = true) -> {
deleteContentUriFile(context, uri)
}
// 2. 处理 file:// 类型的Uri
uri.scheme.equals("file", ignoreCase = true) -> {
deleteFileUriFile(uri)
}
// 3. 其他情况尝试直接解析路径
else -> {
deleteFileFromPath(uri.path ?: return false)
}
}
}
// 删除Content Uri文件
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
//Timber.d("deleteContentUriFile uri = ${uri.scheme}")
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// Android 10+需要特殊处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
deleteMediaStoreFile(context, uri)
} else {
false
}
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// Android 10+删除MediaStore文件
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
//Timber.d("deleteMediaStoreFile uri = ${uri.scheme}")
val contentResolver = context.contentResolver
val projection = arrayOf(MediaStore.MediaColumns._ID)
return try {
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val contentUri = ContentUris.withAppendedId(uri, id)
contentResolver.delete(contentUri, null, null) > 0
} else {
false
}
} ?: false
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// 删除File Uri文件
private fun deleteFileUriFile(uri: Uri): Boolean {
//Timber.d("deleteFileUriFile uri = $uri")
return try {
File(uri.path ?: return false).delete()
} catch (e: Exception) {
//Timber.e(e)
false
}
}
// 直接通过路径删除文件
private fun deleteFileFromPath(path: String): Boolean {
//Timber.d("deleteFileFromPath path = $path")
return try {
File(path).delete()
} catch (e: Exception) {
//Timber.e(e)
false
}
}
}
@@ -0,0 +1,427 @@
package com.sw.face.collect.utils;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class SoundPoolUtil {
private static final String TAG = SoundPoolUtil.class.getSimpleName();
private static SoundPoolUtil mSound;
private SoundPool mSoundPool;
private boolean isLoadC = false;
private Map<String, Integer> idCache;
private List<Integer> sidCache;
public SoundPoolUtil() {
idCache = new HashMap<>();
sidCache = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes aab = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
mSoundPool = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(aab)
.build();
} else {
mSoundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 8);
}
// mSoundPool = new SoundPool(60, AudioManager.USE_DEFAULT_STREAM_TYPE, 7);
mSoundPool.setOnLoadCompleteListener(new MyOnLoadCompleteListener());
}
public static SoundPoolUtil getInstance() {
synchronized (SoundPoolUtil.class) {
if (mSound == null) {
mSound = new SoundPoolUtil();
}
}
return mSound;
}
private int loadCompleteSize = 0;
private LoadCompletion completionListener;
public void setCompletionListener(LoadCompletion listener) {
this.completionListener = listener;
}
private class MyOnLoadCompleteListener implements SoundPool.OnLoadCompleteListener {
@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
loadCompleteSize++;
//L.e("loadSize" + loadSize + "===" + loadCompleteSize);
if (loadSize == loadCompleteSize) {
isLoadC = true;
if (completionListener != null) {
completionListener.onCompletion();
}
}
}
}
/**
* 加载指定资源
*
* @param name
* @param path
*/
public void loadR(String name, String path) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(path, 1));
}
}
}
private int loadSize = 0;
/**
* 加载指定路径列表的资源
*
* @param map
*/
public void loadR(Map<String, String> map) {
loadSize = map.size();
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue(), 1));
}
}
}
}
/**
* 加载指定AssetFileDescriptor的资源
*
* @param name
* @param afd
*/
public void loadRF(String name, AssetFileDescriptor afd) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), 1));
}
}
}
/**
* 加载指定AssetFileDescriptor列表的资源
*
* @param map
*/
public void loadRF(Map<String, AssetFileDescriptor> map) {
Set<Map.Entry<String, AssetFileDescriptor>> entries = map.entrySet();
for (Map.Entry<String, AssetFileDescriptor> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(entry.getValue().getFileDescriptor(), entry.getValue().getStartOffset(), entry.getValue().getLength(), 1));
}
}
}
}
/**
* 加载指定列表资源
*
* @param context
* @param map
*/
public void loadR(Context context, Map<String, Integer> map) {
loadSize = map.size();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
if (checkSoundPool()) {
if (!idCache.containsKey(key)) {
idCache.put(key, mSoundPool.load(context, entry.getValue(), 1));
}
}
}
}
/**
* 加载单个音频
*
* @param context
* @param name
* @param res
*/
public void loadR(Context context, String name, int res) {
if (checkSoundPool()) {
if (!idCache.containsKey(name)) {
idCache.put(name, mSoundPool.load(context, res, 1));
}
}
}
/**
* 播放指定音频,并返用于停止、暂停、恢复的StreamId
*
* @param name
* @param times
* @return
*/
public int play(String name, int times) {
//L.e(String.format("play %s times=%s", name, times));
return this.play(name, 1, 1, 1, times, 1);
}
/**
* 播放指定音频,并指定播放次数和频率
*
* @param name
* @param times
* @param rate
* @return
*/
public int play(String name, int times, int rate) {
return this.play(name, 1, 1, 1, times, rate);
}
/**
* 播放指定音频,并指定优先级和播放频率
*
* @param name
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, int property, int times, int rate) {
return this.play(name, 1, 1, property, times, rate);
}
/**
* 播放指定音频,并指定左右声道、优先级、播放次数、播放频率
*
* @param name
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public int play(String name, float leftVolume, float rightVolume, int property, int times, int rate) {
int streamId = -1;
if (checkSoundPool()) {
//L.d(TAG, "play: " + name);
if (idCache.containsKey(name) && isLoadC) {
//L.d(TAG, "name:" + idCache.get(name));
streamId = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
//L.d(TAG, "streadmId:" + streamId);
sidCache.add(streamId);
}
}
return streamId;
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表
*
* @param names
* @param times
* @return
*/
public List<Integer> play(List<String> names, int times) {
return this.play(names, 1, 1, 1, times, 1);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定次数和频率
*
* @param names
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int times, int rate) {
return this.play(names, 1, 1, 1, times, rate);
}
/**
* 播放指定列表的音频,并返回并返用于停止、暂停、恢复的StreamId列表,指定所有参数
*
* @param names
* @param leftVolume
* @param rightVolume
* @param property
* @param times
* @param rate
* @return
*/
public List<Integer> play(List<String> names, int leftVolume, int rightVolume, int property, int times, int rate) {
List<Integer> streamIds = new ArrayList<>();
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name) && isLoadC) {
int a = mSoundPool.play(idCache.get(name), leftVolume, rightVolume, property, times, rate);
streamIds.add(a);
sidCache.add(a);
}
}
}
return streamIds;
}
/**
* 停止指定id音频
*/
public void stop(int r) {
if (checkSoundPool()) {
mSoundPool.stop(r);
}
}
/**
* 停止指定列表音频
*/
public void stopAll() {
if (checkSoundPool()) {
for (int r : sidCache) {
mSoundPool.stop(r);
}
}
}
/**
* 暂停指定音效
*
* @param r
*/
public void pause(int r) {
if (checkSoundPool()) {
mSoundPool.pause(r);
}
}
/**
* 暂停指定列表音频
*
* @param list
*/
public void pause(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.pause(r);
}
}
}
/**
* 暂停所有音效
*/
public void pauseAll() {
mSoundPool.autoPause();
}
/**
* 恢复指定音频播放
*
* @param r
*/
public void resume(int r) {
if (checkSoundPool()) {
mSoundPool.resume(r);
}
}
/**
* 恢复指定列表的音频
*
* @param list
*/
public void resume(List<Integer> list) {
if (checkSoundPool()) {
for (int r : list) {
mSoundPool.resume(r);
}
}
}
/**
* 恢复所有暂停的音频
*/
public void resumeAll() {
if (checkSoundPool()) {
mSoundPool.autoResume();
}
}
/**
* 卸载指定音频
*
* @param name
*/
public void unLoad(String name) {
if (checkSoundPool()) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
/**
* 卸载指定列表的音频
*
* @param names
*/
public void unLoad(List<String> names) {
if (checkSoundPool()) {
for (String name : names) {
if (idCache.containsKey(name)) {
mSoundPool.unload(idCache.get(name));
idCache.remove(name);
}
}
}
}
/**
* 释放所有资源,如果想继续播放,需要重新加载资源
*/
public void release() {
if (checkSoundPool()) {
mSound = null;
mSoundPool.release();
idCache.clear();
}
}
private boolean checkSoundPool() {
if (mSoundPool != null) {
return true;
}
return false;
}
public interface LoadCompletion {
void onCompletion();
}
}
@@ -17,6 +17,7 @@ import com.sw.plate.utils.arcface.facedb.entity.FaceEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class MainViewModel() : ViewModel() {
companion object {
@@ -141,4 +142,26 @@ class MainViewModel() : ViewModel() {
}
}
/**
* 添加人脸数据
*/
fun addUserFace(faceData: String, imageFile: File, block: (Boolean, UserFaceModel?) -> Unit) {
viewModelScope.launch {
val imageResp = repository.uploadImage(imageFile)
val imageUrl = imageResp.data?:""
if (imageUrl.isBlank()) {
block(false, null)
return@launch
}
val faceResp = repository.addUserFace(faceData, imageUrl)
val status = parseResponse(faceResp)
if (status.not()) {
block(false, null)
return@launch
}
block(true, faceResp.data)
}
}
}
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/ivFlag"
android:layout_width="453dp"
android:layout_height="430dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="180dp"
android:scaleType="fitCenter"
android:src="@drawable/img_init"
android:visibility="invisible"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="点击取盘"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>
+48 -24
View File
@@ -65,16 +65,28 @@
</androidx.cardview.widget.CardView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:src="@drawable/bg_face_wrap3"
android:visibility="visible" />
</FrameLayout>
<TextView
android:id="@+id/tvSimilarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="1.0" />
<TextView
android:id="@+id/tvFaceTip"
android:layout_width="wrap_content"
@@ -87,6 +99,18 @@
android:textStyle="bold"
android:visibility="visible" />
<TextView
android:id="@+id/tvCountDown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:textColor="@color/white"
android:textSize="60sp"
android:textStyle="bold"
android:visibility="visible"
tools:text="30s" />
<LinearLayout
android:id="@+id/layoutState"
android:layout_width="wrap_content"
@@ -111,25 +135,25 @@
android:textSize="26sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<!-- <LinearLayout-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="30dp"-->
<!-- android:gravity="center_vertical"-->
<!-- android:orientation="horizontal">-->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCollectFace"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/bg_collect_btn"
android:text="开始采集"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="normal" />
<!-- <androidx.appcompat.widget.AppCompatButton-->
<!-- android:id="@+id/btnCollectFace"-->
<!-- android:layout_width="300dp"-->
<!-- android:layout_height="100dp"-->
<!-- android:layout_margin="20dp"-->
<!-- android:background="@drawable/bg_collect_btn"-->
<!-- android:text="开始采集"-->
<!-- android:textColor="@color/white"-->
<!-- android:textSize="40sp"-->
<!-- android:textStyle="normal" />-->
</LinearLayout>
<!-- </LinearLayout>-->
</LinearLayout>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -652,21 +652,53 @@ public class FaceHelper implements FaceListener {
});
}
private int failCount = 0;
private long startTime = 0;
private void searchFace(final FaceFeature faceFeature, final Integer trackId) {
CompareResult compareResult = FaceServer.getInstance().searchFaceFeature(faceFeature, frEngine);
if (compareResult == null || compareResult.getFaceEntity() == null) {
if (startTime == 0) {
failCount = 0;
startTime = System.currentTimeMillis();
}
if (System.currentTimeMillis() - startTime > 10*1000) {
failCount = 0;
startTime = System.currentTimeMillis();
}
failCount++;
float similar;
if (compareResult != null) {
similar = compareResult.getSimilar();
} else {
similar = 0f;
}
// new Handler(Looper.getMainLooper()).post(()-> {
// try {
// Toast.makeText(App.getContext(),"识别失败"+failCount+"次,similar="+similar, Toast.LENGTH_SHORT).show();
// } catch (Exception e) {
// e.printStackTrace();
// }
// });
Log.d(TAG, "collectFace,searchFace查询失败"+failCount+"次,similar="+similar);
if (failCount >= 2) {
recognizeCallback.onRecognized(null, LivenessInfo.UNKNOWN, false);
return;
}
retryRecognizeDelayed(trackId);
return;
}
compareResult.setTrackId(trackId);
boolean pass = compareResult.getSimilar() > recognizeConfiguration.getSimilarThreshold();
compareResult.setSimilarPass(pass);
Log.d(TAG, "collectFace,searchFace: pass="+pass+",similar="+compareResult.getSimilar()+",threshold="+recognizeConfiguration.getSimilarThreshold());
recognizeCallback.onRecognized(compareResult, getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(), pass);
if (pass) {
setName(trackId, "识别通过");
noticeCurrentStatus("识别通过");
changeRecognizeStatus(trackId, RequestFeatureStatus.SUCCEED);
} else {
noticeCurrentStatus("未通过:NOT_REGISTERED");
noticeCurrentStatus("未通过:NOT_REGISTERED"+compareResult.getSimilar());
retryRecognizeDelayed(trackId);
}
}
@@ -10,6 +10,12 @@ public class CompareResult {
private int compareCode;
private long cost;
private boolean similarPass;
public void setSimilarPass(boolean similarPass) {
this.similarPass = similarPass;
}
public CompareResult(FaceEntity faceEntity, float similar) {
this.faceEntity = faceEntity;
this.similar = similar;
@@ -45,10 +45,15 @@ public class FaceEntity implements Parcelable {
@ColumnInfo(name = "register_time")
private long registerTime;
/**
* 用户类型:1-普通会员、2-临时用户、3-内部员工、或者其它待定类型
* 用户类型:1-普通会员、2-临时用户、或者其它待定类型
*/
@ColumnInfo(name = "user_type")
private String userType;
/**
* 会员编号
*/
@ColumnInfo(name = "cardNo")
private String cardNo;
@Ignore
private int trackId;//人脸追踪ID
@@ -66,6 +71,8 @@ public class FaceEntity implements Parcelable {
this.imagePath = faceEntity.imagePath;
this.featureData = faceEntity.featureData;
this.registerTime = faceEntity.registerTime;
this.userType = faceEntity.getUserType();
this.cardNo = faceEntity.getCardNo();
}
@@ -75,6 +82,8 @@ public class FaceEntity implements Parcelable {
userName = in.readString();
imagePath = in.readString();
featureData = in.createByteArray();
userType = in.readString();
cardNo = in.readString();
}
public static final Creator<FaceEntity> CREATOR = new Creator<FaceEntity>() {
@@ -145,6 +154,16 @@ public class FaceEntity implements Parcelable {
this.userType = userType;
}
public String getCardNo() {
return cardNo;
}
public void setCardNo(String cardNo) {
this.cardNo = cardNo;
}
@Override
public int describeContents() {
return 0;
@@ -158,10 +177,9 @@ public class FaceEntity implements Parcelable {
dest.writeString(imagePath);
dest.writeByteArray(featureData);
dest.writeString(userType);
dest.writeString(cardNo);
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -176,12 +194,13 @@ public class FaceEntity implements Parcelable {
TextUtils.equals(this.userName, that.userName) &&
TextUtils.equals(this.imagePath, that.imagePath) &&
Arrays.equals(featureData, that.featureData) &&
TextUtils.equals(this.userType, that.userType);
TextUtils.equals(this.userType, that.userType) &&
TextUtils.equals(this.cardNo, that.cardNo);
}
@Override
public int hashCode() {
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType);
int result = Objects.hash(faceId, registerTime, userName, imagePath, userType, cardNo);
result = 31 * result + Arrays.hashCode(featureData);
return result;
}
@@ -283,8 +283,8 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
);
}
// 填入在设置界面设置好的配置信息
boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable));
// enableLive = false;
// boolean enableLive = !ConfigUtil.getLivenessDetectType(context).equals(context.getString(R.string.value_liveness_type_disable));
boolean enableLive = false;
boolean enableFaceQualityDetect = ConfigUtil.isEnableImageQualityDetect(context);
boolean enableFaceMoveLimit = ConfigUtil.isEnableFaceMoveLimit(context);
boolean enableFaceSizeLimit = ConfigUtil.isEnableFaceSizeLimit(context);
@@ -472,13 +472,20 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
}
}
private String getUserId(CompareResult result) {
if (result != null && result.getFaceEntity() != null) {
return result.getFaceEntity().getUserName();
}
return null;
}
@Override
public void onRecognized(CompareResult compareResult, Integer live, boolean similarPass) {
Disposable disposable = Observable.just(true).observeOn(AndroidSchedulers.mainThread()).subscribe(aBoolean -> {
// TODO: 2026/1/21 测试使用 Observable.just(similarPass)代替Observable.just(true)---
Observable.just(similarPass).observeOn(AndroidSchedulers.mainThread()).subscribe(aBoolean -> {
Log.d(TAG, "collectFace,onRecognized: similarPass=" + similarPass + ",live=" + live + ",userId=" + getUserId(compareResult));
if (similarPass) {
if (recognizeUserId != null) {
recognizeUserId.postValue(compareResult);
}
recognizeUserId.postValue(compareResult);
boolean isAdded = false;
List<CompareResult> compareResults = compareResultList.getValue();
if (compareResults != null && !compareResults.isEmpty()) {
@@ -500,6 +507,8 @@ public class RecognizeViewModel extends ViewModel implements RecognizeCallback {
getFaceItemEventMutableLiveData().postValue(new FaceItemEvent(compareResults.size() - 1, EventType.INSERTED));
}
}
} else {
recognizeUserId.postValue(compareResult);
}
});
}