增量人脸数据查询优化

This commit is contained in:
2025-12-31 11:53:35 +08:00
parent b5daf0f5df
commit 2656e89244
12 changed files with 242 additions and 66 deletions
+10
View File
@@ -30,6 +30,16 @@
android:usesCleartextTraffic="true"
android:theme="@style/Theme.FaceCollect">
<!-- 注册BootReceiver,监听开机完成广播 -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -88,6 +88,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
private val mainViewModel by viewModels<MainViewModel>()
private var currentUserId: String? = null
private var lastFaceTrackId = -1
override fun inflateViewBinding() = ActivityMainBinding.inflate(layoutInflater)
@@ -102,7 +103,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
super.initialize()
mainViewModel.getUserFaceCache(pageNo = pageNo)
FaceEngineUtils.activeEngine()
setupArcCamera()
binding.btnCollectFace.setOnClickListener {
requestSinglePermissionResult(Manifest.permission.CAMERA) { granted ->
if (granted.not()) {
@@ -121,7 +122,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
binding.tvFaceTip.visible()
binding.layoutState.gone()
resumeCamera()
// resumeCamera()
startFaceTask()
}
@@ -173,6 +174,31 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
it.put("content", faceData)
}
tcpClient?.send(jsonObject)
// 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
@@ -199,6 +225,22 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
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();
@@ -219,6 +261,11 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
// private var imageBitmap: Bitmap? = null
private fun collectFace() {
//toast("currentUserId=$currentUserId")
if (currentUserId != null) {
toast("您已采集过人脸信息")
return
}
currentUserId = null
userFaceInfo = null
recognizeViewModel.updateRegisterStatus(REGISTER_STATUS_READY)
@@ -248,10 +295,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
private fun getFaceData(block: (String) -> Unit) {
runBlocking {
job = executor.startIntervalTask(5) {
if (currentUserId != null && currentUserId!!.isNotBlank()) {
toast("您已采集过人脸信息")
return@startIntervalTask
}
// if (currentUserId != null && currentUserId!!.isNotBlank()) {
// toast("您已采集过人脸信息")
// return@startIntervalTask
// }
if (userFaceInfo == null) {
return@startIntervalTask
}
@@ -353,13 +400,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
TAG,
"recognizeUserId observe compareResult = ${compareResult.trackId}, userId = ${compareResult.faceEntity.userName}"
)
val lastFaceTrackId = compareResult.trackId
lastFaceTrackId = compareResult.trackId
val faceEntity = compareResult.faceEntity
currentUserId = faceEntity.userName
// if (!currentUserId.isNullOrBlank()) {
//// toast("您已采集过人脸信息")
// return@Observer
// }
if (!currentUserId.isNullOrBlank()) {
toast("您已采集过人脸信息")
return@Observer
}
// //未识别到,拍摄照片
})
@@ -663,29 +710,19 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
// val listIsEmpty = facePreviewInfoList.isEmpty()
// val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null
// Log.d(TAG,"listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId")
// if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) {
// if (lastFaceTrackId != -1 && currentUserId != null) {
// mealPickupMode = SPUtil.getInstance().get(GlobalKey.KEY_PICKUP_MODE, 0) ?: 0
// Timber.tag(com.sw.dualscreen.presentation.MainScreenPresentation.Companion.TAG).i("$lastFaceTrackId 用户离开")
// lastFaceTrackId = -1
// Timber.tag(com.sw.dualscreen.presentation.MainScreenPresentation.Companion.TAG)
// .d("postUserData userNutritionData是否null ${userNutrition == null}, currentFood是否null: ${currentFood == null}currentUserId是否null${currentUserId == null}")
// if (userNutrition == null || currentFood == null || currentUserId == null) {
// return
// }
// postUserData()
// if (mealPickupMode == 1 || isKeepFaceState) {
// step2FaceRecognizing(currentFood!!)
// isKeepFaceState = false
// } else {
// activity.isAnalyzing = false
// step1FoodRecognizing()
// }
// }
// }
val listIsEmpty = facePreviewInfoList.isEmpty()
val listFirstTrackId = if (listIsEmpty.not()) facePreviewInfoList[0].trackId else null
Log.d(
TAG,
"listIsEmpty=$listIsEmpty,lastFaceTrackId=$lastFaceTrackId,listFirstTrackId=$listFirstTrackId"
)
if (listIsEmpty || (lastFaceTrackId != listFirstTrackId)) {
if (lastFaceTrackId != -1 && currentUserId != null) {
Log.d(TAG, "$lastFaceTrackId 用户离开")
lastFaceTrackId = -1
currentUserId = null
}
}
}
fun switchCamera() {
@@ -757,14 +794,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
private val intervalExecutor by lazy { IntervalExecutor() }
private var faceTaskJob: Job? = null
// private val initialDelay = 5 * 60 * 1000L
// private val dealyMillis = 10 * 60 * 1000L
private val initialDelay = 60 * 1000L
private val dealyMillis = 30 * 1000L
private var taskPageNo = 1
fun startFaceTask() {
faceTaskJob =
intervalExecutor.startIntervalTaskWithInitialDelay(initialDelay, dealyMillis) {
intervalExecutor.startIntervalTaskWithInitialDelay(60 * 1000L, 30 * 1000L) {
val timestamp = SpTool.getLastFaceTimestamp()
if (timestamp == 0L) {
return@startIntervalTaskWithInitialDelay
@@ -774,7 +807,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), ViewTreeObserver.OnGlo
timestamp = timestamp,
onAllQueryFinished = {
taskPageNo = 1
SpTool.setLastFaceTimestamp(System.currentTimeMillis())
},
onPageQueryFinished = { list ->
runOnUiThread {
@@ -822,4 +854,34 @@ 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()
faceDao.deleteTempUserFaceData()
recognizeViewModel.refreshFaceList();
}.start()
}
override fun onResume() {
super.onResume()
setupArcCamera()
resumeCamera()
}
override fun onPause() {
super.onPause()
pauseCamera()
}
}
@@ -15,7 +15,7 @@ object GlobalData {
/**
* 具体业务baseurl
*/
var appBaseUrl: String = ""
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 横排数量
@@ -20,7 +20,7 @@ data class UserFaceModel(
// @SerializedName("userId")
val userId: String? = "",
val faceFeatureStr: String? = "",
val faceUpdateTimestamp: Long?=null,
val faceDeleted: Boolean? = false
)
// : Parcelable
@@ -41,3 +41,13 @@ data class FaceData(
val pages: Int,
val records: List<UserFaceModel2>? = null
)
data class DinnerType(
val id: String? = "",
//餐次
val dinnerType: String? = "",
//餐次收费模式(0称重,1固定,2不收费)
val chargeType: Int = 0,
//固定收费(元) 仅charge_type = 1 时
val fixedAmount: Double?=null
)
@@ -1,8 +1,8 @@
package com.sw.dualscreen.network
package com.sw.face.collect.network
import android.util.Log
import com.sw.face.collect.network.api.ApiService
import com.sw.dualscreen.network.interceptor.RequestInterceptor
import com.sw.face.collect.network.interceptor.RequestInterceptor
import com.sw.face.collect.MyApp
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@@ -11,7 +11,8 @@ import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object ApiClient {
private const val BASE_URL = "http://device.shuziweidao.com:8889/"
// private const val BASE_URL = "http://device.shuziweidao.com:8889/"
private const val BASE_URL = "https://dev.yixiong-tech.com:8081"
//private const val TIME_OUT = 30L // 超时时间(秒)
//todo 临时测试改为5秒
@@ -2,6 +2,7 @@ package com.sw.face.collect.network.api
import com.sw.face.collect.base.GlobalData
import com.sw.face.collect.model.ApiResponse
import com.sw.face.collect.model.DinnerType
import com.sw.face.collect.model.UserFaceModel
import okhttp3.MultipartBody
import okhttp3.RequestBody
@@ -33,4 +34,12 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
* 获取当前餐点类型
*/
@GET
suspend fun getDinnerType(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/getRegionRule",
): ApiResponse<DinnerType?>
}
@@ -1,9 +1,8 @@
package com.sw.dualscreen.network.interceptor
package com.sw.face.collect.network.interceptor
import android.text.TextUtils
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.utils.SPUtil
import com.sw.face.collect.base.GlobalData
import com.sw.plate.App
import okhttp3.Interceptor
import okhttp3.Request
@@ -30,12 +29,12 @@ class RequestInterceptor : Interceptor {
return chain.proceed(newRequest)
}
private fun getToken(originRequest: Request): String {
val tokenParam = originRequest.header("X-Access-Token")
if (!TextUtils.isEmpty(tokenParam)) return tokenParam!!
// 从本地获取token的逻辑
val spUtil = SPUtil.getInstance(context = App.getContext())
return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
}
// private fun getToken(originRequest: Request): String {
// val tokenParam = originRequest.header("X-Access-Token")
// if (!TextUtils.isEmpty(tokenParam)) return tokenParam!!
// // 从本地获取token的逻辑
// val spUtil = SPUtil.getInstance(context = App.getContext())
// return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
//// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
// }
}
@@ -0,0 +1,19 @@
package com.sw.face.collect.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.sw.face.collect.MainActivity
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
Log.d("BootReceiver","设备启动完成,开始执行自启动逻辑")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}
@@ -2,6 +2,7 @@ package com.sw.face.collect.repository
import com.sw.dualscreen.repository.BaseRepository
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
@@ -28,7 +29,7 @@ class RemoteRepository constructor(
)
}
}
/**
/**
* 获取人脸数据
*/
suspend fun getFaceIncrementList(
@@ -47,4 +48,13 @@ class RemoteRepository constructor(
}
}
/**
* 获取当前餐点类型
*/
suspend fun getDinnerType(): ApiResponse<DinnerType?> {
return safeApiCall {
apiService.getDinnerType()
}
}
}
@@ -1,7 +1,8 @@
package com.sw.face.collect.socket;
public class LanServer {
public static final int TYPE_ADD_FACE_DATA = 1;
public static final int TYPE_CLEAR_FACE_DATA = 2;
private static volatile LanCommunicationManager instance;
public static LanCommunicationManager getInstance() {
@@ -18,4 +19,17 @@ public class LanServer {
}
return instance;
}
void a() {
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_ADD_FACE_DATA);
//data.put("faceId", tempUserId);//收到采集信息,每次生成新的ID
//data.put("faceFeature", faceFeatureString);
//lanServer.broadcast(new JSONObject(data));
//
//HashMap<String, Object> data = new HashMap<>();
//data.put("type", LanServer.TYPE_CLEAR_FACE_DATA);
//lanServer.broadcast(new JSONObject(data));
}
}
@@ -2,10 +2,11 @@ package com.sw.face.collect.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.dualscreen.network.ApiClient
import com.sw.face.collect.network.ApiClient
import com.sw.dualscreen.utils.SPUtil
import com.sw.face.collect.base.GlobalKey
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.repository.RemoteRepository
import com.sw.face.collect.utils.SpTool
@@ -25,6 +26,8 @@ class MainViewModel() : ViewModel() {
private val faceApi: FaceApi = FaceApi()
protected val repository = RemoteRepository(ApiClient.apiService)
private var lastFaceTimestamp = 0L
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
val code = response.code
if (code == "00000" || code == "200" || code == "0") {
@@ -43,14 +46,15 @@ class MainViewModel() : ViewModel() {
var currentPageNo = pageNo
// Timber.tag(TAG).d("getUserFaceCache index = $currentPageNo")
viewModelScope.launch {
//获取全量数据时,时间戳改为0
SpTool.setLastFaceTimestamp(0)
val response = repository.getUserFaceCache(currentPageNo)
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
if (list.isEmpty()) {
SpTool.setLastFaceTimestamp(System.currentTimeMillis())
if (pageNo == 1 && list.isEmpty()) {
ToastUtils.showToast("查询人脸数据为空")
return@withContext
}
@@ -59,11 +63,17 @@ class MainViewModel() : ViewModel() {
}
faceApi.updateFaceData(currentPageNo, faceEntity)
if (list.size >= pageSize) {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
currentPageNo++
getUserFaceCache(currentPageNo)
} else {
SpTool.setLastFaceTimestamp(System.currentTimeMillis())
return@withContext
}
//查询完成所有数据,保存时间戳
if (list.isNotEmpty()) {
//非空最后一条数据的是时间戳,空使用上次list最后一条时间戳
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.setLastFaceTimestamp(lastFaceTimestamp)
}
}
}
@@ -76,7 +86,7 @@ class MainViewModel() : ViewModel() {
pageNo: Int = 1,
pageSize: Int = PAGE_SIZE,
timestamp: Long,
onAllQueryFinished:()->Unit,
onAllQueryFinished: () -> Unit,
onPageQueryFinished: (List<UserFaceModel>) -> Unit
) {
viewModelScope.launch {
@@ -89,7 +99,11 @@ class MainViewModel() : ViewModel() {
withContext(Dispatchers.Default) {
val list: List<UserFaceModel> = response.data ?: emptyList()
onPageQueryFinished(list)
if (pageNo == 1 && list.isEmpty()) {
return@withContext
}
if (list.size >= pageSize) {
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
getFaceIncrementList(
pageNo = pageNo + 1,
pageSize = pageSize,
@@ -97,8 +111,30 @@ class MainViewModel() : ViewModel() {
onAllQueryFinished = onAllQueryFinished,
onPageQueryFinished = onPageQueryFinished
)
} else {
onAllQueryFinished()
return@withContext
}
//查询完成所有数据,保存时间戳
if (list.isNotEmpty()) {
//非空最后一条数据的是时间戳,空使用上次list最后一条时间戳
lastFaceTimestamp = list.last().faceUpdateTimestamp ?: 0L
}
SpTool.setLastFaceTimestamp(lastFaceTimestamp)
onAllQueryFinished()
}
}
}
}
/**
* 获取当前餐点类型
*/
fun getDinnerType(block: (DinnerType) -> Unit) {
viewModelScope.launch {
val response = repository.getDinnerType()
if (parseResponse(response)) {
withContext(Dispatchers.Default) {
response.data?.let {
block(it)
}
}
}
@@ -88,4 +88,10 @@ public interface FaceDao {
@Query("UPDATE sqlite_sequence SET seq = 0 WHERE name ='face'")
void resetId();
/**
* @return 删除临时用户人脸
*/
@Query("DELETE from face WHERE user_type = '2'")
int deleteTempUserFaceData();
}