diff --git a/app/objectbox-models/default.json b/app/objectbox-models/default.json new file mode 100644 index 0000000..1b11942 --- /dev/null +++ b/app/objectbox-models/default.json @@ -0,0 +1,49 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:594331511073099531", + "lastPropertyId": "4:1528837175750321569", + "name": "Food", + "properties": [ + { + "id": "1:2317727855243736226", + "name": "id", + "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 + } + ], + "relations": [] + } + ], + "lastEntityId": "1:594331511073099531", + "lastIndexId": "1:6010838397086628487", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [], + "retiredIndexUids": [], + "retiredPropertyUids": [], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt index 73921ba..cbe8e2c 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt @@ -34,8 +34,11 @@ import com.sw.dualscreen.objbox.FoodModule import com.sw.dualscreen.presentation.SecondaryScreenPresentation import com.sw.dualscreen.sdk.SensorScaleUtils import com.sw.dualscreen.utils.AssetsTool +import com.sw.dualscreen.utils.BitmapCropper +import com.sw.dualscreen.utils.BitmapSaver import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.GlideUtils +import com.sw.dualscreen.utils.GsonUtils import com.sw.dualscreen.utils.ImageUtil import com.sw.dualscreen.view.CustomBottomSheetDialog import com.sw.dualscreen.viewmodel.BaseViewModel @@ -170,6 +173,18 @@ class MainActivity : BaseActivity() { override fun registerDataChange() { super.registerDataChange() + lifecycleScope.launch { + viewModel.identifiedFoodInfoList2.collect { list -> + Timber.d("registerDataChange识别后查询接口数据:${GsonUtils.toJson(list)}") + adapter.updateData(list) + if (list.isNotEmpty()) { + checkedItem = list[0] + checkedItem!!.photoUri = lastPhotoUri + updateCurrentFood(checkedItem) + lastPhotoUri = null + } + } + } lifecycleScope.launch { viewModel.identifiedFoodInfoList.collect { list -> adapter.updateData(list) @@ -192,19 +207,17 @@ class MainActivity : BaseActivity() { Timber.d("registerDataChange photoUri = ${photoUri.path}") // viewModel.getIdentifiedFoodList() - AssetsTool.loadImageBitmapFromAssets(this, "data/20251016-091510.png") { bitmap -> - val resultList = FoodModule.queryFood(bitmap) - val name = resultList.joinToString(separator = ",") { it.name.toString() } - Timber.d("registerDataChange 固定图片识别数据名称:$name") - } - ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap -> Timber.d("registerDataChange photoUri 拿到bitmap") - val resultList = FoodModule.queryFood(bitmap) + val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900) + Timber.d("registerDataChange photoUri bitmap裁剪完成") + val file = BitmapSaver.saveToAppFilesDir(bmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg") + Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}") + val resultList = FoodModule.queryFood(bmp) Timber.d("registerDataChange photoUri 拿到识别数据") - val name = resultList.joinToString(separator = ",") { it.name.toString() } - Timber.d("registerDataChange photoUri 识别数据名称:$name") -// Toast.makeText(this, "${res.name}|${res.foodIdx}|${res.score}", Toast.LENGTH_LONG).show() + val foodName = resultList.joinToString(separator = ",") + Timber.d("registerDataChange photoUri 识别数据名称:$foodName") + viewModel.getFoodInfo(foodName) } } } diff --git a/app/src/main/java/com/sw/dualscreen/network/api/ApiService.kt b/app/src/main/java/com/sw/dualscreen/network/api/ApiService.kt index 457f9e4..b8918c5 100644 --- a/app/src/main/java/com/sw/dualscreen/network/api/ApiService.kt +++ b/app/src/main/java/com/sw/dualscreen/network/api/ApiService.kt @@ -99,4 +99,14 @@ interface ApiService { @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/userEatFood/doubleBalance/face/stall", @Body param: List ): ApiResponse + + /** + * 获取菜品信息 + */ + @GET + suspend fun getFoodInfo( + @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall/v2", + @Query("restId") restId: String, + @Query("foodNames") foodName: String, + ): ApiResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/objbox/DataBean.kt b/app/src/main/java/com/sw/dualscreen/objbox/DataBean.kt index c9cb148..70b91d1 100644 --- a/app/src/main/java/com/sw/dualscreen/objbox/DataBean.kt +++ b/app/src/main/java/com/sw/dualscreen/objbox/DataBean.kt @@ -6,9 +6,9 @@ data class FoodClassInfo( var idx_to_class: Map ) -data class FoodQueryResult( - var id: Long, - var name: String?, - var foodIdx: Int, - var score: Double -) \ No newline at end of file +//data class FoodQueryResult( +// var id: Long, +// var name: String?, +// var foodIdx: Int, +// var score: Double +//) diff --git a/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt b/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt index cd5805a..d9f17f0 100644 --- a/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt +++ b/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt @@ -29,15 +29,15 @@ object FoodModule { fun init(context: Context) { 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()) { initFoodData(context) } } - fun queryFood(bitmap: Bitmap, queryCount:Int = 30): MutableList { + fun queryFood(bitmap: Bitmap, queryCount:Int = 15): List { val inputTensor = TensorImageUtils.bitmapToFloat32Tensor( bitmap, NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB @@ -48,23 +48,21 @@ object FoodModule { return queryFood(floatArray,queryCount) } - fun queryFood(floatArray: FloatArray, queryCount:Int = 30): MutableList { + fun queryFood(floatArray: FloatArray, queryCount:Int = 15): List { val query: Query = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build() //查询比较分数 // val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" } - val resultList = mutableListOf() + val map = mutableMapOf() query.findIdsWithScores().forEach { val food = box.get(it.id) - resultList.add( - FoodQueryResult( - id = it.id, - name = food.name, - foodIdx = food.foodIdx, - score = it.score - ) - ) + //FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score) + food.name?.let { key -> + val count = map[key] ?: 0 + map.put(key, count + 1) + } } - return resultList + val list = map.entries.sortedByDescending { it.value }.map { it.key } + return list } diff --git a/app/src/main/java/com/sw/dualscreen/objbox/ObjectBox.kt b/app/src/main/java/com/sw/dualscreen/objbox/ObjectBox.kt index 25d2303..9e8d907 100644 --- a/app/src/main/java/com/sw/dualscreen/objbox/ObjectBox.kt +++ b/app/src/main/java/com/sw/dualscreen/objbox/ObjectBox.kt @@ -42,6 +42,11 @@ import kotlin.jvm.javaClass */ object ObjectBox { +// 确保所有设备/版本使用相同的objectbox-models/default.json +// 避免直接修改自动生成的MyObjectBox类 +// 跨版本升级时使用boxStore.runInTx执行数据迁移 + + private const val TAG = "ObjectBox" lateinit var boxStore: BoxStore diff --git a/app/src/main/java/com/sw/dualscreen/repository/RemoteRepository.kt b/app/src/main/java/com/sw/dualscreen/repository/RemoteRepository.kt index a7d693a..ab8ad53 100644 --- a/app/src/main/java/com/sw/dualscreen/repository/RemoteRepository.kt +++ b/app/src/main/java/com/sw/dualscreen/repository/RemoteRepository.kt @@ -103,4 +103,16 @@ class RemoteRepository constructor( ): ApiResponse { return safeApiCall { apiService.postUserNutritionData(param = param) } } + + /** + * 获取菜品信息 + * @param restId 从device服务获取的canteenId字段 + * @param foodName 菜品名称,多个使用逗号拼接 + */ + suspend fun getFoodInfo( + restId: String = GlobalData.restId, + foodName: String, + ): ApiResponse> { + return safeApiCall { apiService.getFoodInfo(restId = restId, foodName = foodName) } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/utils/BitmapCropper.kt b/app/src/main/java/com/sw/dualscreen/utils/BitmapCropper.kt new file mode 100644 index 0000000..ac6e7b6 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/utils/BitmapCropper.kt @@ -0,0 +1,38 @@ +package com.sw.dualscreen.utils +import android.graphics.Bitmap + +object BitmapCropper { + /** + * 裁剪Bitmap中心区域为指定尺寸 + * @param original 原始Bitmap + * @param targetWidth 目标宽度 + * @param targetHeight 目标高度 + * @return 裁剪后的Bitmap + */ + fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap { + val originalWidth = original.width + val originalHeight = original.height + + // 计算中心点坐标 + var startX = (originalWidth - targetWidth) / 2 + var startY = (originalHeight - targetHeight) / 2 + + // 边界检查 + startX = startX.coerceAtLeast(0) + startY = startY.coerceAtLeast(0) + val actualWidth = minOf(targetWidth, originalWidth - startX) + val actualHeight = minOf(targetHeight, originalHeight - startY) + + return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight) + } +} + +// 使用示例 +fun main() { + // 假设这是从资源加载的Bitmap + val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888) + + // 裁剪中心500x500区域 + val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500) + println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}") +} diff --git a/app/src/main/java/com/sw/dualscreen/utils/BitmapSaver.kt b/app/src/main/java/com/sw/dualscreen/utils/BitmapSaver.kt new file mode 100644 index 0000000..20ab354 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/utils/BitmapSaver.kt @@ -0,0 +1,53 @@ +package com.sw.dualscreen.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 + +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) + return saveBitmap(bitmap, File(dir, 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 + } + } +} diff --git a/app/src/main/java/com/sw/dualscreen/viewmodel/UserViewModel.kt b/app/src/main/java/com/sw/dualscreen/viewmodel/UserViewModel.kt index 212eebb..5bf659a 100644 --- a/app/src/main/java/com/sw/dualscreen/viewmodel/UserViewModel.kt +++ b/app/src/main/java/com/sw/dualscreen/viewmodel/UserViewModel.kt @@ -49,6 +49,8 @@ class UserViewModel : BaseViewModel() { */ private val _loadFaceResult = MutableStateFlow(false) val loadFaceResult : StateFlow = _loadFaceResult + private val _identifiedFoodInfoList2 = MutableStateFlow>(emptyList()) + val identifiedFoodInfoList2: StateFlow> = _identifiedFoodInfoList2 /** * 获取token @@ -205,4 +207,14 @@ class UserViewModel : BaseViewModel() { } } } + + fun getFoodInfo(foodName: String) { + Timber.d("getFoodInfo") + launchWithLoading { + val response = repository.getFoodInfo(foodName = foodName) + if (parseResponse(response)) { + _identifiedFoodInfoList2.value = response.result ?: emptyList() + } + } + } } \ No newline at end of file