识别图片处理、查询菜品数据

This commit is contained in:
2025-10-17 14:47:28 +08:00
parent dd51448aea
commit a27cc5f7ee
10 changed files with 221 additions and 31 deletions
+49
View File
@@ -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
}
@@ -34,8 +34,11 @@ import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.presentation.SecondaryScreenPresentation import com.sw.dualscreen.presentation.SecondaryScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.AssetsTool 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.Debouncer
import com.sw.dualscreen.utils.GlideUtils import com.sw.dualscreen.utils.GlideUtils
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.view.CustomBottomSheetDialog import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
@@ -170,6 +173,18 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun registerDataChange() { override fun registerDataChange() {
super.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 { lifecycleScope.launch {
viewModel.identifiedFoodInfoList.collect { list -> viewModel.identifiedFoodInfoList.collect { list ->
adapter.updateData(list) adapter.updateData(list)
@@ -192,19 +207,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
Timber.d("registerDataChange photoUri = ${photoUri.path}") Timber.d("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList() // 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 -> ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap ->
Timber.d("registerDataChange photoUri 拿到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 拿到识别数据") Timber.d("registerDataChange photoUri 拿到识别数据")
val name = resultList.joinToString(separator = ",") { it.name.toString() } val foodName = resultList.joinToString(separator = ",")
Timber.d("registerDataChange photoUri 识别数据名称:$name") Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
// Toast.makeText(this, "${res.name}|${res.foodIdx}|${res.score}", Toast.LENGTH_LONG).show() viewModel.getFoodInfo(foodName)
} }
} }
} }
@@ -99,4 +99,14 @@ interface ApiService {
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/userEatFood/doubleBalance/face/stall", @Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/userEatFood/doubleBalance/face/stall",
@Body param: List<UserNutritionParam> @Body param: List<UserNutritionParam>
): ApiResponse<String> ): ApiResponse<String>
/**
* 获取菜品信息
*/
@GET
suspend fun getFoodInfo(
@Url url: String = "${GlobalData.appBaseUrl}/zhstapi/zhst/getRestInfoFoodsByType/stall/v2",
@Query("restId") restId: String,
@Query("foodNames") foodName: String,
): ApiResponse<List<FoodInfo>>
} }
@@ -6,9 +6,9 @@ data class FoodClassInfo(
var idx_to_class: Map<String, String> var idx_to_class: Map<String, String>
) )
data class FoodQueryResult( //data class FoodQueryResult(
var id: Long, // var id: Long,
var name: String?, // var name: String?,
var foodIdx: Int, // var foodIdx: Int,
var score: Double // var score: Double
) //)
@@ -29,15 +29,15 @@ object FoodModule {
fun init(context: Context) { fun init(context: Context) {
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt")) module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
box = ObjectBox.boxStore.boxFor(Food::class) box = ObjectBox.boxStore.boxFor(Food::class)
if (box.all.isNotEmpty()) { // if (box.all.isNotEmpty()) {
box.removeAll() // box.removeAll()
} // }
if (box.all.isEmpty()) { if (box.all.isEmpty()) {
initFoodData(context) initFoodData(context)
} }
} }
fun queryFood(bitmap: Bitmap, queryCount:Int = 30): MutableList<FoodQueryResult> { fun queryFood(bitmap: Bitmap, queryCount:Int = 15): List<String> {
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor( val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
bitmap, bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
@@ -48,23 +48,21 @@ object FoodModule {
return queryFood(floatArray,queryCount) return queryFood(floatArray,queryCount)
} }
fun queryFood(floatArray: FloatArray, queryCount:Int = 30): MutableList<FoodQueryResult> { fun queryFood(floatArray: FloatArray, queryCount:Int = 15): List<String> {
val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build() val query: Query<Food> = 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 tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
val resultList = mutableListOf<FoodQueryResult>() val map = mutableMapOf<String, Int>()
query.findIdsWithScores().forEach { query.findIdsWithScores().forEach {
val food = box.get(it.id) val food = box.get(it.id)
resultList.add( //FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
FoodQueryResult( food.name?.let { key ->
id = it.id, val count = map[key] ?: 0
name = food.name, map.put(key, count + 1)
foodIdx = food.foodIdx,
score = it.score
)
)
} }
return resultList }
val list = map.entries.sortedByDescending { it.value }.map { it.key }
return list
} }
@@ -42,6 +42,11 @@ import kotlin.jvm.javaClass
*/ */
object ObjectBox { object ObjectBox {
// 确保所有设备/版本使用相同的objectbox-models/default.json
// 避免直接修改自动生成的MyObjectBox类
// 跨版本升级时使用boxStore.runInTx执行数据迁移
private const val TAG = "ObjectBox" private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore lateinit var boxStore: BoxStore
@@ -103,4 +103,16 @@ class RemoteRepository constructor(
): ApiResponse<String> { ): ApiResponse<String> {
return safeApiCall { apiService.postUserNutritionData(param = param) } return safeApiCall { apiService.postUserNutritionData(param = param) }
} }
/**
* 获取菜品信息
* @param restId 从device服务获取的canteenId字段
* @param foodName 菜品名称,多个使用逗号拼接
*/
suspend fun getFoodInfo(
restId: String = GlobalData.restId,
foodName: String,
): ApiResponse<List<FoodInfo>> {
return safeApiCall { apiService.getFoodInfo(restId = restId, foodName = foodName) }
}
} }
@@ -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}")
}
@@ -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
}
}
}
@@ -49,6 +49,8 @@ class UserViewModel : BaseViewModel() {
*/ */
private val _loadFaceResult = MutableStateFlow<Boolean>(false) private val _loadFaceResult = MutableStateFlow<Boolean>(false)
val loadFaceResult : StateFlow<Boolean> = _loadFaceResult val loadFaceResult : StateFlow<Boolean> = _loadFaceResult
private val _identifiedFoodInfoList2 = MutableStateFlow<List<FoodInfo>>(emptyList())
val identifiedFoodInfoList2: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList2
/** /**
* 获取token * 获取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()
}
}
}
} }