采集向量数据接口调试

This commit is contained in:
2026-01-06 18:49:33 +08:00
parent fc04db9268
commit 378e6088e7
20 changed files with 468 additions and 241 deletions
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-12-26T06:22:06.750685100Z">
<DropdownSelection timestamp="2025-12-31T07:06:25.405532800Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.2:5555;connection=bfafe870" />
<DeviceId pluginId="Default" identifier="serial=192.168.1.45:5555;connection=52f5d52f" />
</handle>
</Target>
</DropdownSelection>
+30 -12
View File
@@ -5,7 +5,7 @@
"entities": [
{
"id": "1:594331511073099531",
"lastPropertyId": "4:1528837175750321569",
"lastPropertyId": "9:4394472487596452023",
"name": "Food",
"properties": [
{
@@ -14,22 +14,37 @@
"type": 6,
"flags": 1
},
{
"id": "2:340951913928211738",
"name": "name",
"type": 9
},
{
"id": "3:5484846939472684412",
"name": "foodIdx",
"type": 5
},
{
"id": "4:1528837175750321569",
"name": "foodVector",
"indexId": "1:6010838397086628487",
"type": 28,
"flags": 8
},
{
"id": "5:5561277398349179032",
"name": "collectId",
"type": 9
},
{
"id": "6:8313847979709019172",
"name": "foodId",
"type": 9
},
{
"id": "7:1375536603452792462",
"name": "foodName",
"type": 9
},
{
"id": "8:7942065542192169896",
"name": "version",
"type": 9
},
{
"id": "9:4394472487596452023",
"name": "otherField",
"type": 9
}
],
"relations": []
@@ -43,7 +58,10 @@
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredPropertyUids": [
340951913928211738,
5484846939472684412
],
"retiredRelationUids": [],
"version": 1
}
@@ -20,8 +20,13 @@ object GlobalData {
/**
* 具体业务baseurl
*/
// var appBaseUrl: String = "http://192.168.1.201:14801"
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
var appBaseUrl: String = "http://192.168.1.201:14801"
// var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 菜品识别模型版本号,后边以接口返回为准
*/
var foodModelVersion: String = "1.0.0"
/**
* 横排数量
@@ -4,10 +4,10 @@ import android.annotation.SuppressLint
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.CollectedFoodNewAdapter
import com.sw.dualscreen.databinding.ActivityCollectedFoodBinding
import com.sw.dualscreen.databinding.LayoutEmptySearchBinding
import com.sw.dualscreen.dialog.WarnDialog
import com.sw.dualscreen.ext.addOnActionSearchListener
import com.sw.dualscreen.ext.gone
@@ -15,12 +15,10 @@ import com.sw.dualscreen.ext.hideKeyboard
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.CollectedFoodInfo
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
@@ -42,7 +40,7 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
CollectedFoodNewAdapter(list).apply {
// isStateViewEnable = true
addOnItemChildClickListener(R.id.ivDeleteFood) { _, _, position ->
deleteGoods(position)
loadDeleteDialog(position)
}
}
}
@@ -182,17 +180,22 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
//}
}
private fun deleteGoods(position: Int) {
private fun loadDeleteDialog(position: Int) {
WarnDialog(
context = context,
content = "请确认是否删除菜品:${list[position].foodName}",
confirmBlock = {
deleteGoods(position)
}).show()
}
private fun deleteGoods(position: Int) {
val food = list[position]
showWaitingDialog("加载中……")
viewModel.deleteCollectFood(food.foodId, GlobalData.foodModelVersion) { deleteSuccess ->
if (deleteSuccess) {
Thread {
runOnUiThread {
showWaitingDialog("加载中……")
}
val name = list[position].foodName
val filterIdList = box?.all?.filter { it.name == name }?.map { it.id }
val filterIdList = box?.all?.filter { it.foodName == food.foodName }?.map { it.id }
box?.removeByIds(filterIdList)
runOnUiThread {
hideWaitingDialog()
@@ -203,7 +206,8 @@ class CollectedFoodActivity : BaseActivity<ActivityCollectedFoodBinding>() {
}
}
}.start()
}).show()
}
}
}
@@ -1,37 +1,86 @@
package com.sw.dualscreen.activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.databinding.ActivityInitBinding
import com.sw.dualscreen.utils.QRCodeUtil
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.DeviceViewModel
import com.sw.plate.utils.AppUtil
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import com.sw.dualscreen.viewmodel.UserViewModel
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
class InitActivity : BaseActivity<ActivityInitBinding>() {
private val viewModel by viewModels<DeviceViewModel>()
companion object {
private const val PAGE_SIZE = 100
}
private val userViewModel by viewModels<UserViewModel>()
private var pageNum = 1
override fun getViewModel(): BaseViewModel {
return viewModel
return userViewModel
}
override fun inflateViewBinding(): ActivityInitBinding {
return ActivityInitBinding.inflate(layoutInflater)
}
private fun getCollectedFoodVector(block:()-> Unit) {
userViewModel.getCollectedFoodVector(param = mutableMapOf(
"pageNum" to "$pageNum",
"pageSize" to "$PAGE_SIZE",
"version" to GlobalData.foodModelVersion
)) { items ->
val list = items ?: emptyList()
if (pageNum == 1 && list.isEmpty()) {
block()
return@getCollectedFoodVector
}
if (list.size >= PAGE_SIZE) {
saveFoodVector(list)
pageNum++
getCollectedFoodVector(block)
return@getCollectedFoodVector
}
if (list.isNotEmpty()) {
saveFoodVector(list)
block()
}
}
}
private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) }
private fun saveFoodVector(list: List<FoodVector>) {
val vectorList = list.map { vt ->
val foodVector = vt.foodVector?.removeSurrounding("[", "]")?.split(",")?.map { it.toFloatOrNull()?:0.0f }?.toFloatArray()
Food(
collectId = vt.id,
foodId = vt.foodId,
foodName = vt.foodName,
version = vt.version,
foodVector = foodVector
)
}
box.put(vectorList)
}
override fun initialize() {
super.initialize()
binding.root.postDelayed({
goMainActivity()
}, 1000)
showWaitingDialog("加载中……")
FoodModule.init(this) {
getCollectedFoodVector {
runOnUiThread {
hideWaitingDialog()
binding.root.postDelayed({
goMainActivity()
}, 1000)
}
}
}
// val isSuccess = viewModel.checkEquipmentInfo()
// if (isSuccess) {
// goMainActivity()
@@ -50,12 +99,12 @@ class InitActivity : BaseActivity<ActivityInitBinding>() {
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.deviceInfoResult.drop(1).collect {
if (it != true) return@collect
goMainActivity()
}
}
// lifecycleScope.launch {
// viewModel.deviceInfoResult.drop(1).collect {
// if (it != true) return@collect
// goMainActivity()
// }
// }
}
private fun goMainActivity() {
@@ -137,7 +137,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
super.initialize()
isPageVisible = true
addBackEventListener()
FoodModule.init(this)
//FoodModule.init(this)
initView()
viewModel.getUserFaceCache(pageNo = 1)
//setupSecondaryDisplay()
@@ -353,77 +353,37 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
LightManager.closeRedLight()
LightManager.openGreenLight()
showWaitingDialog("识别中,请稍后……")
takePhoto(successCallback = { photoUri ->
lastPhotoUri = photoUri
Timber.tag(TAG).d("main,takePhoto耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList()
ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap ->
Timber.tag(TAG).d("main,uriToBitmap耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri 拿到bitmap")
// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
Timber.d("registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir(
bitmap,
this,
"IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.tag(TAG)
.d("main,saveToAppFilesDir耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
//val nameList = FoodModule.queryFood(bitmap)
val scoreList = FoodModule.getFoodScoreList(bitmap)
val recDataJson = GsonUtils.toJson(scoreList)
Timber.tag(TAG)
.d("main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri 拿到识别数据:$recDataJson")
val nameList = scoreList.map { it.name }
val foodName = nameList.joinToString(separator = ",")
hideWaitingDialog()
Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
if (TextUtils.isEmpty(foodName)) {
LightManager.closeGreenLight()
LightManager.closeRedLight()
//binding.layoutRescan.visibility = View.VISIBLE
binding.flPay.visible()
binding.tvToSearch.let {
it.text = "未识别到,去手动搜索"
it.visible()
}
} else {
//binding.layoutRescan.visibility = View.GONE
showWaitingDialog("正在查询菜品信息,请稍后……")
viewModel.getFoodInfo(foodName) { list ->
runOnUiThread {
hideWaitingDialog()
Timber.tag(TAG)
.d("main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
updateFoodInfo(list.toMutableList(), scoreList)
}
takePhoto(
successCallback = { photoUri ->
onTakePhotoSuccess(photoUri)
failCount = 0
}, failureCallback = {
runOnUiThread {
if (failCount >= 10) {
ToastUtils.showToast("相机异常,请稍后重试")
shutdownCamera()
setupCamera()
return@runOnUiThread
}
}
}
}, failureCallback = {
hideWaitingDialog()
failCount++
hideWaitingDialog()
// ToastUtils.showToast("拍照异常,请手动放置后重新识别")
shutdownCamera()
setupCamera()
shutdownCamera()
setupCamera()
binding.root.postDelayed({
recognizeFood()
}, 500)
})
binding.root.postDelayed({
recognizeFood()
}, 1000)
})
}
/**
* 识别相机异常次数
*/
private var failCount = 0
@SuppressLint("NotifyDataSetChanged")
private fun updateFoodInfo(list: MutableList<FoodInfo>, scoreList: List<IdNameScore>) {
val queryData = GsonUtils.toJson(list)
@@ -434,12 +394,19 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val score = scoreItem?.score ?: 0.0
foodInfo.score = ((1 - score) * 10000).roundToInt()
}
val list2 = try {
val orderList = scoreList.map { it.name.trim() }
list.sortedBy { orderList.indexOf(it.foodName) }
} catch (e: Exception) {
e.printStackTrace()
list
}
searchFoodList.clear()
searchFoodList.addAll(list)
searchFoodList.addAll(list2)
adapter.notifyDataSetChanged()
//adapter.updateData(list)
if (list.isNotEmpty()) {
checkedItem = list[0]
if (list2.isNotEmpty()) {
checkedItem = list2[0]
checkedItem!!.photoUri = lastPhotoUri
checkedItem!!.isChecked = true
adapter.notifyItemChanged(0)
@@ -616,7 +583,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun onError(exception: ImageCaptureException) {
Timber.e("拍照失败: ${exception.message}")
failureCallback()
ToastUtils.showToast("拍照失败: ${exception.message}")
//ToastUtils.showToast("拍照失败: ${exception.message}")
// 恢复图像分析
isAnalyzing = true
}
@@ -1090,4 +1057,64 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// }.start()
// }
private fun onTakePhotoSuccess(photoUri: Uri) {
lastPhotoUri = photoUri
Timber.tag(TAG).d("main,takePhoto耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList()
ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap ->
Timber.tag(TAG).d("main,uriToBitmap耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri 拿到bitmap")
// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
Timber.d("registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir(
bitmap,
this,
"IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.tag(TAG)
.d("main,saveToAppFilesDir耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
//val nameList = FoodModule.queryFood(bitmap)
val scoreList = FoodModule.getFoodScoreList(bitmap)
val recDataJson = GsonUtils.toJson(scoreList)
Timber.tag(TAG)
.d("main,getFoodScoreList耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
Timber.d("registerDataChange photoUri 拿到识别数据:$recDataJson")
val nameList = scoreList.map { it.name }
val foodName = nameList.joinToString(separator = ",")
hideWaitingDialog()
Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
if (TextUtils.isEmpty(foodName)) {
LightManager.closeGreenLight()
LightManager.closeRedLight()
//binding.layoutRescan.visibility = View.VISIBLE
binding.flPay.visible()
binding.tvToSearch.let {
it.text = "未识别到,去手动搜索"
it.visible()
}
} else {
//binding.layoutRescan.visibility = View.GONE
showWaitingDialog("正在查询菜品信息,请稍后……")
viewModel.getFoodInfo(foodName) { list ->
runOnUiThread {
hideWaitingDialog()
Timber.tag(TAG)
.d("main,getFoodInfo耗时:${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
updateFoodInfo(list.toMutableList(), scoreList)
}
}
}
}
}
}
@@ -8,11 +8,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.camera.view.PreviewView
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.CollectedFoodActivity
import com.sw.dualscreen.activity.SettingActivity
@@ -51,7 +51,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = ""
private var box: Box<Food>? = null
private val box by lazy { ObjectBox.boxStore.boxFor(Food::class) }
private val foodCollectionList = mutableListOf<FoodCollectionBean>().apply {
repeat(MAX_COUNT) {
add(FoodCollectionBean(isShowCamera = true))
@@ -126,7 +126,6 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
// targetWidth = 1300, targetHeight = 900,
// //offsetX = 30, offsetY = 100
// )
initBox()
val imageVector = try {
FoodModule.bitmap2FloatArray(bitmap)
} catch (e: Exception) {
@@ -236,9 +235,12 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
it.layoutManager = GridLayoutManager(context, 2)
it.adapter = searchFoodAdapter
}
searchInfo()
}
@SuppressLint("NotifyDataSetChanged")
private fun upload() {
lifecycleScope.launch {
val totalFileCount = foodCollectionList.count { it.imageFile != null }
@@ -247,31 +249,53 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
//params["placeId"] = restId.toRequestBody()
params["foodId"] = checkedItem!!.foodId.toRequestBody()
params["foodName"] = checkedItem!!.foodName!!.toRequestBody()
//params["foodVector"] = foodVector.toRequestBody()
params["version"] = GlobalData.foodModelVersion.toRequestBody()
ImageUploader(
totalList = foodCollectionList,
uploadImage = { batch ->
val files = batch.map { it.imageFile }
(requireActivity() as SettingActivity).viewModel.uploadCollectFoodPics(
files,
params
)
val foodVectorList =
batch.filter { it.imageVector != null }.map { it.imageVector!!.joinToString(separator = ",", prefix = "[", postfix = "]")}
val foodVectorJson =
foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
Timber.tag(TAG).d("json=$foodVectorJson")
params["foodVector"] = foodVectorJson.toRequestBody()
settingActivity?.viewModel?.uploadCollectFoodPics(files, params)
},
onProgress = { count, batch ->
onProgress = { count, batch, idList->
activity?.runOnUiThread {
settingActivity?.showWaitingDialog2("图片上传中$count/$totalFileCount")
batch.forEach {
it.uploadSuccess = true
}
//batch.forEach {
// it.uploadSuccess = true
//}
Thread {
val foodList = batch.mapIndexed { index, it ->
Food(
collectId = if (index < idList.size) idList[index] else null,
foodId = checkedItem!!.foodId,
foodName = checkedItem!!.foodName,
foodVector = it.imageVector,
version = GlobalData.foodModelVersion
)
}
box.put(foodList)
activity?.runOnUiThread {
batch.forEach { it.uploadSuccess = true }
collectionAdapter.notifyDataSetChanged()
}
}.start()
}
},
onError = {
settingActivity?.hideWaitingDialog()
activity?.runOnUiThread {
ToastUtils.showToast("上传失败,请稍后重试")
}
},
onComplete = {
vectorThread()
//vectorThread()
settingActivity?.hideWaitingDialog()
}
).processUploads()
}
@@ -316,33 +340,34 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
}.start()
}
private fun initBox() {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
}
private fun image2VectorTask(imageVector: FloatArray, position: Int) {
initBox()
// val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!)
//
//// val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector)
//// Timber.tag("mzf1").e(base64Str)
////// viewModel.postImageData(
////// context,
////// foodId = selectedFoodId.toString(),
////// foodName = selectedFoodName.toString(),
////// foodVector = base64Str,
////// uri = item.imageUri!!
////// )
box?.put(Food(name = checkedItem!!.foodName, foodIdx = 0, foodVector = imageVector))
foodCollectionList[position].let {
// it.imageVector = imageVector
it.isFinish = true
}
activity?.runOnUiThread {
collectionAdapter.notifyItemChanged(position)
}
private fun image2VectorTask(imageVector: FloatArray?, position: Int) {
if (imageVector == null) return
//// val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!)
////
////// val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector)
////// Timber.tag("mzf1").e(base64Str)
//////// viewModel.postImageData(
//////// context,
//////// foodId = selectedFoodId.toString(),
//////// foodName = selectedFoodName.toString(),
//////// foodVector = base64Str,
//////// uri = item.imageUri!!
//////// )
// box.put(
// Food(
// collectId = null,
// foodId = checkedItem!!.foodId,
// foodName = checkedItem!!.foodName,
// foodVector = imageVector,
// version = "1.0.0"
// )
// )
// if (position > -1) {
// foodCollectionList[position].isFinish = true
// activity?.runOnUiThread {
// collectionAdapter.notifyItemChanged(position)
// }
// }
}
@@ -119,13 +119,13 @@ data class RespCodeMsg(
)
data class PostEvent(var name:String="")
data class PostEvent(var name: String = "")
data class PaySuccessEvent(
var name: String = ""
)
data class ChargeModeEvent(
var chargeMode:Int = 0
var chargeMode: Int = 0
)
data class ClickBackEvent(
@@ -133,7 +133,15 @@ data class ClickBackEvent(
)
data class DeviceConfig(
var arcsoftAppId:String?=null,
var arcsoftSdkKey:String?=null,
var arcsoftActiveKey:String?=null
var arcsoftAppId: String? = null,
var arcsoftSdkKey: String? = null,
var arcsoftActiveKey: String? = null
)
data class FoodVector(
val id: String? = null,
val foodId: String? = null,
val foodName: String? = null,
val foodVector: String? = null,
val version: String? = null
)
@@ -7,8 +7,9 @@ import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.model.response.UserNutrition
@@ -16,12 +17,16 @@ import com.sw.dualscreen.objbox.CollectedFoodInfo
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.QueryMap
import retrofit2.http.Url
interface ApiService {
@@ -246,11 +251,11 @@ interface ApiService {
@Multipart
@POST
suspend fun uploadCollectFoodPics(
// @Url url: String = "http://192.168.1.201:14801/terminal/neglect/dishCollectionVectorData/add",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/dishCollectionVectorData/add",
@PartMap params: HashMap<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part>
): ApiResponse<String>
): ApiResponse<List<String>?>
/**
* 获取设备配置数据
@@ -268,4 +273,22 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/common/app/faceFeature/increment/list",
@Body param: Map<String, Long>
): ApiResponse<List<UserFaceModel>?>
/**
* 档口机向量数据分页查询
*/
@POST
suspend fun getCollectedFoodVector(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/collectionVectorPage",
@Body param: MutableMap<String, String>
): ApiResponse<List<FoodVector>?>
/**
* 档口机向量数据分页查询
*/
@DELETE
suspend fun deleteCollectFood(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/boothMachine/app/deleteBoothMachineCollection",
@QueryMap param: MutableMap<String, String?>
): ApiResponse<Any?>
}
@@ -28,11 +28,11 @@ data class FoodCollectionBean(
)
data class CollectedFoodInfo(
var id: String? = null,
var placeId: String? = null,
// var id: String? = null,
// var placeId: String? = null,
var foodId: String? = null,
var foodName: String? = null,
var foodPic: Any? = null,
var foodVector: Any? = null,
// var foodPic: Any? = null,
// var foodVector: Any? = null,
var foodCount: Int = 0
)
@@ -8,8 +8,11 @@ import io.objectbox.annotation.VectorDistanceType
@Entity
data class Food(
@Id var id: Long = 0,
var name: String? = null,
var foodIdx: Int = 0,
var collectId: String? = null,
var foodId: String? = null,
var foodName: String? = null,
var version: String? = null,
var otherField: String? = null,
@HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT)
var foodVector: FloatArray? = null
) {
@@ -20,8 +23,11 @@ data class Food(
other as Food
if (id != other.id) return false
if (foodIdx != other.foodIdx) return false
if (name != other.name) return false
if (collectId != other.collectId) return false
if (foodId != other.foodId) return false
if (foodName != other.foodName) return false
if (version != other.version) return false
if (otherField != other.otherField) return false
if (!foodVector.contentEquals(other.foodVector)) return false
return true
@@ -29,9 +35,14 @@ data class Food(
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + foodIdx
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (collectId?.hashCode() ?: 0)
result = 31 * result + (foodId?.hashCode() ?: 0)
result = 31 * result + (foodName?.hashCode() ?: 0)
result = 31 * result + (version?.hashCode() ?: 0)
result = 31 * result + (otherField?.hashCode() ?: 0)
result = 31 * result + (foodVector?.contentHashCode() ?: 0)
return result
}
}
@@ -3,7 +3,6 @@ package com.sw.dualscreen.objbox
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.SparseLongArray
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sw.dualscreen.MyApp
@@ -26,7 +25,7 @@ import java.io.InputStream
object FoodModule {
private const val THRESHOLD = 0.8
private var module_mobile: Module?=null
private var module_mobile: Module? = null
private lateinit var box: Box<Food>
private lateinit var embeddingsList: List<List<Float>>
private lateinit var labelsList: IntArray
@@ -35,16 +34,18 @@ object FoodModule {
val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
val DEFAULT_FOOD_INDEX = -1
fun init(context: Context) {
fun init(context: Context, block: () -> Unit = {}) {
Thread {
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
box = ObjectBox.boxStore.boxFor(Food::class)
//if (box.all.isNotEmpty()) {
// box.removeAll()
//}
//初始化默认重新拉取数据,先清空本地数据
if (box.all.isNotEmpty()) {
box.removeAll()
}
//if (box.all.isEmpty()) {
// initDefFoodData(context)
//}
block()
}.start()
}
@@ -100,16 +101,25 @@ object FoodModule {
val idScoreList = query.findIdsWithScores()
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score))
nameScoreList.add(
IdNameScore(
id = it.id,
name = box.get(it.id).foodName ?: "",
score = it.score
)
)
}
val nameScoreData = GsonUtils.toJson(nameScoreList)
Timber.tag("FoodModule").d("registerDataChange,queryFood,耗时:${System.currentTimeMillis()-startTime},数据:$nameScoreData")
Timber.tag("FoodModule")
.d("registerDataChange,queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
return nameScoreList
}
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val startTime = System.currentTimeMillis()
val floatArray = bitmap2FloatArray(bitmap)
Timber.tag("FoodModule").d("registerDataChange,bitmap2FloatArray,耗时:${System.currentTimeMillis()-startTime}")
Timber.tag("FoodModule")
.d("registerDataChange,bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
if (nameScoreList.isEmpty()) {
return emptyList()
@@ -136,6 +146,7 @@ object FoodModule {
//Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
val query: Query<Food> =
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
@@ -144,10 +155,10 @@ object FoodModule {
val map = mutableMapOf<String, Int>()
query.findIdsWithScores().forEach {
val food = box.get(it.id)
Timber.d("${food.name}|${food.foodIdx}|${it.score}")
Timber.d("${food.foodName}|${food.foodId}|${food.collectId}|${food.version}|${food.otherField}|${it.score}")
if (1 - it.score >= THRESHOLD) {
//FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
food.name?.let { key ->
food.foodName?.let { key ->
val count = map[key] ?: 0
map.put(key, count + 1)
}
@@ -167,16 +178,16 @@ object FoodModule {
}
data class IdNameScore(
val id:Long,
var name:String,
val id: Long,
var name: String,
val score: Double
)
fun initDefFoodData(context: Context, action:()-> Unit={}) {
val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
if (count > 0) {
return
}
fun initDefFoodData(context: Context, action: () -> Unit = {}) {
//val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
//if (count > 0) {
// return
//}
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
@@ -192,7 +203,13 @@ object FoodModule {
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
box.put(
Food(
foodName = foodName,
foodVector = array,
//foodIdx = DEFAULT_FOOD_INDEX
)
)
}
action()
}
@@ -25,6 +25,7 @@ import io.objectbox.android.ObjectBoxLiveData
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.sync.Sync
import timber.log.Timber
import java.io.File
import java.util.Date
import java.util.zip.GZIPOutputStream
@@ -65,7 +66,7 @@ object ObjectBox {
.androidContext(context.applicationContext)
.build()
} catch (e: DbException) {
if (e.javaClass.equals(DbException::class.java) || e is FileCorruptException) {
if (e.javaClass == DbException::class.java || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message;
// checked in NoteListActivity to notify user.
dbExceptionMessage = e.toString()
@@ -77,8 +78,9 @@ object ObjectBox {
}
// if (BuildConfig.DEBUG) {
var syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Log.d(TAG,"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
val syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Timber.tag(TAG)
.d("Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext)
@@ -94,7 +96,7 @@ object ObjectBox {
// Do not copy if database file is still in use.
// If it would be open, the copy will likely get corrupted
// as BoxStore may currently write data to the file.
Log.e(TAG, "Database file is still in use, can not copy.")
Timber.tag(TAG).e("Database file is still in use, can not copy.")
return false
}
@@ -9,6 +9,7 @@ import com.sw.dualscreen.model.response.FaceData
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodSearchReq
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.model.response.UserNutrition
@@ -281,7 +282,7 @@ class RemoteRepository constructor(
suspend fun uploadCollectFoodPics(
fileList: List<File>,
params: HashMap<String, RequestBody>
): ApiResponse<String> {
): ApiResponse<List<String>?> {
// 准备文件参数
val fileParts = mutableListOf<MultipartBody.Part>()
fileList.forEachIndexed { index, file ->
@@ -302,4 +303,19 @@ class RemoteRepository constructor(
suspend fun getDeviceConfig(): ApiResponse<DeviceConfig?> {
return safeApiCall { apiService.getDeviceConfig() }
}
suspend fun getCollectedFoodVector(param: MutableMap<String, String>): ApiResponse<List<FoodVector>?> {
return safeApiCall {
apiService.getCollectedFoodVector(param = param)
}
}
suspend fun deleteCollectFood(foodId:String?, version: String?): ApiResponse<Any?> {
return safeApiCall {
apiService.deleteCollectFood(param = mutableMapOf(
"foodId" to foodId,
"version" to version
))
}
}
}
@@ -5,8 +5,8 @@ import java.util.concurrent.atomic.AtomicInteger
class ImageUploader(
private val totalList: List<FoodCollectionBean>,
private val uploadImage: suspend (List<FoodCollectionBean>)-> Boolean,
private val onProgress: (Int, List<FoodCollectionBean>) -> Unit,
private val uploadImage: suspend (List<FoodCollectionBean>)-> List<String>?,
private val onProgress: (Int, List<FoodCollectionBean>, List<String>) -> Unit,
private val onError:(List<FoodCollectionBean>) -> Unit,
private val onComplete:() -> Unit
) {
@@ -16,18 +16,18 @@ class ImageUploader(
private val uploadedCount = AtomicInteger(0)
suspend fun processUploads() {
val batches = totalList.chunked(BATCH_SIZE)
val batches = totalList.filter { it.imageVector!=null }.chunked(BATCH_SIZE)
for (batch in batches) {
val success = uploadImage(batch)
if (!success) {
val idList = uploadImage(batch)
if (idList.isNullOrEmpty()) {
//println("上传失败,终止流程")
onError(batch)
return
}
val fileCount = batch.count { it.imageFile!=null }
uploadedCount.addAndGet(fileCount)
onProgress(uploadedCount.get(), batch)
onProgress(uploadedCount.get(), batch, idList)
//println("已上传 ${uploadedCount.get()}/$totalImages")
}
@@ -1,20 +1,14 @@
package com.sw.dualscreen.viewmodel
import com.sw.dualscreen.GlobalData
import com.sw.dualscreen.GlobalKey
import com.sw.dualscreen.model.response.EquipmentInfo
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.SPUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
/**
* 初始化的viewmodel
*/
class DeviceViewModel : BaseViewModel() {
private val _deviceInfoResult = MutableStateFlow<Boolean?>(null)
val deviceInfoResult: StateFlow<Boolean?> = _deviceInfoResult
// private val _deviceInfoResult = MutableStateFlow<Boolean?>(null)
// val deviceInfoResult: StateFlow<Boolean?> = _deviceInfoResult
/**
* 获取token
@@ -9,6 +9,7 @@ import com.sw.dualscreen.model.response.DeviceConfig
import com.sw.dualscreen.model.response.DinnerType
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.model.response.FoodOrder
import com.sw.dualscreen.model.response.FoodVector
import com.sw.dualscreen.model.response.MemberInfo
import com.sw.dualscreen.model.response.UserFaceModel
import com.sw.dualscreen.model.response.UserFaceModel2
@@ -539,17 +540,17 @@ class UserViewModel : BaseViewModel() {
suspend fun uploadCollectFoodPics(
files: List<File?>,
params: HashMap<String, RequestBody>
): Boolean {
): List<String>? {
Timber.tag(TAG).d("uploadCollectFoodPics")
val fileList = files.filterNotNull()
if (fileList.isEmpty()) {
return true
return null
}
val resp = repository.uploadCollectFoodPics(fileList, params)
// if (!parseResponse(resp)) {
// return false
// }
return resp.code == "00000"
if (resp.code != "00000") {
return null
}
return resp.data
}
fun getDeviceConfig(block: (DeviceConfig?) -> Unit) {
@@ -563,4 +564,28 @@ class UserViewModel : BaseViewModel() {
}
}
}
fun getCollectedFoodVector(param: MutableMap<String, String>, block: (List<FoodVector>?) -> Unit) {
Timber.tag(TAG).d("getCollectedFoodVector")
launchWithLoading {
val response = repository.getCollectedFoodVector(param)
if (parseResponse(response)) {
block(response.data)
} else {
block(null)
}
}
}
fun deleteCollectFood(foodId:String?, version: String?, block: (Boolean) -> Unit) {
Timber.tag(TAG).d("deleteCollectFood")
launchWithLoading {
val response = repository.deleteCollectFood(foodId, version)
if (parseResponse(response)) {
block(true)
} else {
block(false)
}
}
}
}
+9 -6
View File
@@ -13,22 +13,23 @@
<RadioGroup
android:id="@+id/rgSetting"
android:layout_width="match_parent"
android:layout_height="130dp"
android:gravity="center_vertical"
android:layout_height="90dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbBusiness"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_height="match_parent"
android:layout_marginStart="50dp"
android:layout_marginEnd="25dp"
android:layout_weight="1"
android:button="@null"
tools:checked="true"
android:gravity="center"
android:gravity="center_horizontal"
android:maxLines="1"
android:text="经营设置"
android:paddingTop="6dp"
android:paddingBottom="32dp"
android:textColor="@color/setting_radio"
android:textSize="30sp"
android:drawableBottom="@drawable/line_shape_horizontal"
@@ -37,14 +38,16 @@
<RadioButton
android:id="@+id/rbCollect"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_height="match_parent"
android:layout_marginStart="25dp"
android:layout_marginEnd="50dp"
android:layout_weight="1"
android:button="@null"
android:gravity="center"
android:gravity="center_horizontal"
android:maxLines="1"
android:text="餐品采集"
android:paddingTop="6dp"
android:paddingBottom="32dp"
android:textColor="@color/setting_radio"
android:textSize="30sp"
android:drawableBottom="@drawable/line_shape_horizontal"
+12 -12
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="400dp"
android:layout_height="260dp"
android:layout_width="500dp"
android:layout_height="300dp"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/bg_white_radius12"
@@ -23,7 +23,7 @@
android:gravity="center"
tools:text="存在未收货的物品,请确认是否放弃收货,若放弃则数据不会保存?"
android:textColor="#ff141428"
android:textSize="20sp" />
android:textSize="22sp" />
<LinearLayout
android:layout_width="wrap_content"
@@ -31,26 +31,26 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancel"
android:layout_width="120dp"
android:layout_height="50dp"
android:layout_width="130dp"
android:layout_height="60dp"
android:background="@drawable/bg_white_stroke_blue_ripple"
android:text="取消"
android:gravity="center"
android:textColor="#FF0033CC"
android:textSize="20sp" />
android:textSize="22sp" />
<TextView
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnConfirm"
android:layout_width="120dp"
android:layout_height="50dp"
android:layout_marginStart="30dp"
android:layout_width="130dp"
android:layout_height="60dp"
android:layout_marginStart="40dp"
android:background="@drawable/bg_blue_ripple"
android:text="确认"
android:gravity="center"
android:textColor="@color/white"
android:textSize="20sp" />
android:textSize="22sp" />
</LinearLayout>
</LinearLayout>
@@ -39,12 +39,12 @@
<ImageView
android:id="@+id/ivDeleteFood"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginEnd="75dp"
android:paddingHorizontal="10dp"
android:paddingHorizontal="13dp"
android:src="@drawable/ic_close2"
tools:ignore="ContentDescription"
android:visibility="gone"/>
android:visibility="visible"/>
</LinearLayout>