识别图片处理、查询菜品数据
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user