feat(layout): 更新主秤监控界面布局并启用PyTorch模型功能
- 将主秤监控界面从LinearLayout改为ConstraintLayout布局 - 添加返回按钮并实现点击关闭页面功能 - 在BaseApp中初始化ObjectBox数据库 - 启用PyTorch模型依赖库并移除注释代码 - 在CollectFragment中激活食物图像向量化功能 - 初始化FoodModule中的PyTorch模型 - 临时忽略assets下的.pt模型文件到.gitignore
This commit is contained in:
@@ -11,3 +11,5 @@ local.properties
|
||||
.claude/
|
||||
app/debug/
|
||||
app/release/
|
||||
# 临时忽略 assets 下的 .pt 模型文件,正式文件就绪后取消此行
|
||||
app/src/main/assets/*.pt
|
||||
|
||||
@@ -137,8 +137,8 @@ dependencies {
|
||||
implementation(libs.androidx.camera.extensions)
|
||||
|
||||
//pytorch
|
||||
// implementation (libs.pytorch.android)
|
||||
// implementation (libs.pytorch.android.torchvision)
|
||||
implementation (libs.pytorch.android)
|
||||
implementation (libs.pytorch.android.torchvision)
|
||||
|
||||
// objectbox
|
||||
if (isDebug) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import com.shuwei.dish.match.R
|
||||
import com.shuwei.dish.match.db.DatabaseProvider
|
||||
import com.shuwei.dish.match.objbox.ObjectBox
|
||||
import com.shuwei.dish.match.scale.ScaleServiceManager
|
||||
import com.shuwei.dish.match.ui.InitActivity.Companion.TAG
|
||||
import com.shuwei.dish.match.utils.AppUtil
|
||||
@@ -44,7 +45,7 @@ class BaseApp : Application() {
|
||||
// GlobalData.deviceId = "39a7abdd06b3c7ab"
|
||||
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
|
||||
registerReceiver(BootReceiver(), filter)
|
||||
// ObjectBox.init(this)
|
||||
ObjectBox.init(this)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
|
||||
@@ -1,308 +1,308 @@
|
||||
//package com.shuwei.dish.match.objbox
|
||||
//
|
||||
//import android.content.Context
|
||||
//import android.graphics.Bitmap
|
||||
//import android.util.Log
|
||||
//import androidx.core.graphics.scale
|
||||
//import com.google.gson.Gson
|
||||
//import com.google.gson.reflect.TypeToken
|
||||
//import com.shuwei.dish.match.utils.AssetsTool
|
||||
//import com.shuwei.dish.match.utils.LogSaveUtil
|
||||
//import com.shuwei.dish.match.utils.ext.toJsonString
|
||||
//import kotlinx.coroutines.Dispatchers
|
||||
//import kotlinx.coroutines.withContext
|
||||
//import org.pytorch.IValue
|
||||
//import org.pytorch.Module
|
||||
//import org.pytorch.torchvision.TensorImageUtils
|
||||
//import java.io.File
|
||||
//import java.io.FileOutputStream
|
||||
//import java.io.IOException
|
||||
//import java.io.InputStream
|
||||
//
|
||||
//
|
||||
//object FoodModule {
|
||||
//
|
||||
// private lateinit var module_mobile: Module
|
||||
//
|
||||
// private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
|
||||
// private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
|
||||
//
|
||||
// // 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
|
||||
// private const val MODEL_INPUT_WIDTH = 300
|
||||
// private const val MODEL_INPUT_HEIGHT = 300
|
||||
// const val DEFAULT_FOOD_INDEX = -1
|
||||
// const val DEFAULT_QUERY_COUNT = 50
|
||||
//
|
||||
// const val BAG_RATE = 0.05
|
||||
//
|
||||
// suspend fun init(context: Context) {
|
||||
// withContext(Dispatchers.IO) {
|
||||
// module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
|
||||
// initDefFoodData(context)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//// fun uri2FloatArray(uri: Uri): FloatArray? {
|
||||
//// return MyApp.instance?.let { context ->
|
||||
//// ImageUtil.uriToBitmap(context, uri)?.let {
|
||||
//// bitmap2FloatArray(it)
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
// fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
|
||||
// var rgb565Bitmap: Bitmap? = null
|
||||
// try {
|
||||
// val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
|
||||
// if (isRecycle) {
|
||||
// originBitmap.recycle()
|
||||
package com.shuwei.dish.match.objbox
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import androidx.core.graphics.scale
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.shuwei.dish.match.utils.AssetsTool
|
||||
import com.shuwei.dish.match.utils.LogSaveUtil
|
||||
import com.shuwei.dish.match.utils.ext.toJsonString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.pytorch.IValue
|
||||
import org.pytorch.Module
|
||||
import org.pytorch.torchvision.TensorImageUtils
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
object FoodModule {
|
||||
|
||||
private lateinit var module_mobile: Module
|
||||
|
||||
private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
|
||||
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
|
||||
|
||||
// 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
|
||||
private const val MODEL_INPUT_WIDTH = 300
|
||||
private const val MODEL_INPUT_HEIGHT = 300
|
||||
const val DEFAULT_FOOD_INDEX = -1
|
||||
const val DEFAULT_QUERY_COUNT = 50
|
||||
|
||||
const val BAG_RATE = 0.05
|
||||
|
||||
suspend fun init(context: Context) {
|
||||
withContext(Dispatchers.IO) {
|
||||
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
|
||||
initDefFoodData(context)
|
||||
}
|
||||
}
|
||||
|
||||
// fun uri2FloatArray(uri: Uri): FloatArray? {
|
||||
// return MyApp.instance?.let { context ->
|
||||
// ImageUtil.uriToBitmap(context, uri)?.let {
|
||||
// bitmap2FloatArray(it)
|
||||
// }
|
||||
// rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
|
||||
// scaledBitmap.recycle()
|
||||
// val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
|
||||
// rgb565Bitmap,
|
||||
// NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
|
||||
// NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
|
||||
// )
|
||||
// val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
|
||||
// return outputTensor.dataAsFloatArray
|
||||
// } catch (e: OutOfMemoryError) {
|
||||
// e.printStackTrace()
|
||||
// logInfo("bitmap2FloatArray异常:${e.message}")
|
||||
// } finally {
|
||||
// if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
|
||||
// rgb565Bitmap.recycle()
|
||||
// }
|
||||
// //System.gc()
|
||||
// //System.runFinalization()
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
//// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List<String>? {
|
||||
//// return uri2FloatArray(uri)?.let {
|
||||
//// queryFood(it, queryCount)
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
//// /**
|
||||
//// * 返回识别物品名称列表
|
||||
//// */
|
||||
//// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
|
||||
//// val floatArray = bitmap2FloatArray(bitmap)
|
||||
//// return queryFood(floatArray, queryCount)
|
||||
//// }
|
||||
//
|
||||
//// /**
|
||||
//// * 返回识别物品IdNameScore对象列表
|
||||
//// */
|
||||
//// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
|
||||
//// val floatArray = bitmap2FloatArray(bitmap)
|
||||
//// return queryFoodNameScore(floatArray, queryCount)
|
||||
//// }
|
||||
//
|
||||
// suspend fun queryFoodNameScore(
|
||||
// floatArray: FloatArray?,
|
||||
// queryCount: Int = DEFAULT_QUERY_COUNT
|
||||
// ): List<IdNameScore> {
|
||||
// if (floatArray == null) return emptyList()
|
||||
//// 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}" }
|
||||
//// var idScoreList: List<IdWithScore>
|
||||
//// try {
|
||||
//// idScoreList = query.findIdsWithScores();
|
||||
//// } finally {
|
||||
//// // 先关闭Query,释放Cursor
|
||||
//// query.close()
|
||||
//// }
|
||||
//
|
||||
// logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
|
||||
// val idScoreList = ObjectBox.query(floatArray, queryCount)
|
||||
// // 批量加载所有实体,一次DB操作替代循环单条查询
|
||||
// val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id }
|
||||
// val nameScoreList = mutableListOf<IdNameScore>()
|
||||
// idScoreList.forEach {
|
||||
// val name = foodMap[it.id]?.name ?: ""
|
||||
// nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
|
||||
// }
|
||||
// logInfo("queryFood数据:${nameScoreList.toJsonString()}")
|
||||
// return nameScoreList
|
||||
// }
|
||||
//
|
||||
// suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
|
||||
// val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
|
||||
// logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
|
||||
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||
// if (nameScoreList.isEmpty()) {
|
||||
// return emptyList()
|
||||
// }
|
||||
// //暂时不过滤
|
||||
// val maxScoreList = nameScoreList
|
||||
// //.filter { it.score < 0.15 }
|
||||
// .groupBy { it.name }
|
||||
// .map { (_, value) -> value.minByOrNull { it.score }!! }
|
||||
// .toMutableList()
|
||||
//// val map = mutableMapOf<String, Int>()
|
||||
//// nameScoreList.forEach {
|
||||
//// val key = it.name
|
||||
//// val count = map[key] ?: 0
|
||||
//// map[key] = count + 1
|
||||
//// }
|
||||
//// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
|
||||
//// val firstFood = nameScoreList[0].name
|
||||
//// orderList.remove(firstFood)
|
||||
//// orderList.add(0, firstFood)
|
||||
////
|
||||
//// val sortedScoreList = maxScoreList.sortedWith(compareBy {
|
||||
//// orderList.indexOf(it.name)
|
||||
//// })
|
||||
// val sortedScoreList = maxScoreList.sortedBy { it.score }
|
||||
// logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
|
||||
// return sortedScoreList
|
||||
// }
|
||||
//
|
||||
//// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
|
||||
//// val map = mutableMapOf<String, Int>()
|
||||
//// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||
//// nameScoreList.filter { it.score < 0.05 }.forEach {
|
||||
//// val count = map[it.name] ?: 0
|
||||
//// map[it.name] = count + 1
|
||||
//// }
|
||||
//// val list = map.entries.sortedByDescending { it.value }.map { it.key }
|
||||
//// return list
|
||||
//// }
|
||||
//
|
||||
// data class IdNameScore(
|
||||
// val id: Long,
|
||||
// var name: String,
|
||||
// val score: Double
|
||||
// )
|
||||
//
|
||||
// suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
|
||||
// try {
|
||||
// val count = ObjectBox.countIndexField(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")
|
||||
//
|
||||
// val embeddingsList: List<List<Float>> =
|
||||
// Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
|
||||
// val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
|
||||
// val classInfo: FoodClassInfo =
|
||||
// Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
|
||||
//
|
||||
// val foodMap = classInfo.idx_to_class
|
||||
// val list = mutableListOf<Food>()
|
||||
// val size = embeddingsList.size
|
||||
// val size2 = labelsList.size
|
||||
// val size3 = foodMap.size
|
||||
// logInfo("initDefFoodData: $size,$size2,$size3")
|
||||
// embeddingsList.forEachIndexed { index, floatList ->
|
||||
// val classIdx = labelsList[index]
|
||||
// val foodName = foodMap["$classIdx"]
|
||||
// val array = floatList.toFloatArray()
|
||||
// // ObjectBox.boxStore.runInTx {
|
||||
// // box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
|
||||
// // }
|
||||
// val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)
|
||||
// list.add(food)
|
||||
// }
|
||||
//// val tempList = list.chunked(10)
|
||||
// ObjectBox.putAll(list)
|
||||
// action()
|
||||
// } catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
// logInfo("initDefFoodData: ---${e.message}--")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
|
||||
fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
|
||||
var rgb565Bitmap: Bitmap? = null
|
||||
try {
|
||||
val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
|
||||
if (isRecycle) {
|
||||
originBitmap.recycle()
|
||||
}
|
||||
rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
|
||||
scaledBitmap.recycle()
|
||||
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
|
||||
rgb565Bitmap,
|
||||
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
|
||||
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
|
||||
)
|
||||
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
|
||||
return outputTensor.dataAsFloatArray
|
||||
} catch (e: OutOfMemoryError) {
|
||||
e.printStackTrace()
|
||||
logInfo("bitmap2FloatArray异常:${e.message}")
|
||||
} finally {
|
||||
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
|
||||
rgb565Bitmap.recycle()
|
||||
}
|
||||
//System.gc()
|
||||
//System.runFinalization()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List<String>? {
|
||||
// return uri2FloatArray(uri)?.let {
|
||||
// queryFood(it, queryCount)
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * ,此方法的主要目的是:从assets 拷贝到 app的cache目录
|
||||
// * @param context
|
||||
// * @param fileName
|
||||
// * @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
|
||||
// *
|
||||
// * 不可能反正SD
|
||||
// * 返回识别物品名称列表
|
||||
// */
|
||||
//// fun copyAssetToCache(context: Context, fileName: String): String? {
|
||||
//// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
|
||||
//// val cacheDir = context.getCacheDir()
|
||||
//// if (!cacheDir.exists()) {
|
||||
//// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
|
||||
//// }
|
||||
//// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
|
||||
//// if (outPath.exists()) {
|
||||
//// outPath.delete() // TODO 如果该文件已经存在,就删掉
|
||||
//// }
|
||||
//// var `is`: InputStream? = null // 读取
|
||||
//// var fos: FileOutputStream? = null // 写入
|
||||
//// try {
|
||||
//// // 创建文件,如果创建成功,就返回true
|
||||
//// val res = outPath.createNewFile()
|
||||
//// if (res) {
|
||||
//// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
|
||||
//// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
|
||||
//// val buf = ByteArray(`is`.available()) // 缓存区
|
||||
//// var byteCount: Int
|
||||
////
|
||||
//// // 开始循环读取
|
||||
//// while ((`is`.read(buf).also { byteCount = it }) != -1) {
|
||||
//// fos.write(buf, 0, byteCount)
|
||||
//// }
|
||||
//// return outPath.getAbsolutePath()
|
||||
//// }
|
||||
//// } catch (e: IOException) {
|
||||
//// e.printStackTrace()
|
||||
//// } finally {
|
||||
//// try {
|
||||
//// // TODO 一定要记得关闭资源,为了不去性能的磨损
|
||||
//// fos?.flush()
|
||||
//// `is`?.close()
|
||||
//// fos?.close()
|
||||
//// } catch (e: IOException) {
|
||||
//// e.printStackTrace()
|
||||
//// }
|
||||
//// }
|
||||
//// return null
|
||||
//// }
|
||||
//
|
||||
// fun copyAssetToCache(context: Context, fileName: String): String? {
|
||||
// val cacheFile = File(context.cacheDir, fileName)
|
||||
// val buffer = ByteArray(8 * 1024)
|
||||
// var inputStream: InputStream? = null
|
||||
// var outputStream: FileOutputStream? = null
|
||||
//
|
||||
// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
|
||||
// val floatArray = bitmap2FloatArray(bitmap)
|
||||
// return queryFood(floatArray, queryCount)
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 返回识别物品IdNameScore对象列表
|
||||
// */
|
||||
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
|
||||
// val floatArray = bitmap2FloatArray(bitmap)
|
||||
// return queryFoodNameScore(floatArray, queryCount)
|
||||
// }
|
||||
|
||||
suspend fun queryFoodNameScore(
|
||||
floatArray: FloatArray?,
|
||||
queryCount: Int = DEFAULT_QUERY_COUNT
|
||||
): List<IdNameScore> {
|
||||
if (floatArray == null) return emptyList()
|
||||
// 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}" }
|
||||
// var idScoreList: List<IdWithScore>
|
||||
// try {
|
||||
// inputStream = context.assets.open(fileName)
|
||||
// outputStream = FileOutputStream(cacheFile)
|
||||
// var byteCount: Int
|
||||
// while (inputStream.read(buffer).also { byteCount = it } != -1) {
|
||||
// outputStream.write(buffer, 0, byteCount)
|
||||
// idScoreList = query.findIdsWithScores();
|
||||
// } finally {
|
||||
// // 先关闭Query,释放Cursor
|
||||
// query.close()
|
||||
// }
|
||||
|
||||
logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
|
||||
val idScoreList = ObjectBox.query(floatArray, queryCount)
|
||||
// 批量加载所有实体,一次DB操作替代循环单条查询
|
||||
val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id }
|
||||
val nameScoreList = mutableListOf<IdNameScore>()
|
||||
idScoreList.forEach {
|
||||
val name = foodMap[it.id]?.name ?: ""
|
||||
nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
|
||||
}
|
||||
logInfo("queryFood数据:${nameScoreList.toJsonString()}")
|
||||
return nameScoreList
|
||||
}
|
||||
|
||||
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
|
||||
val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
|
||||
logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
|
||||
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||
if (nameScoreList.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
//暂时不过滤
|
||||
val maxScoreList = nameScoreList
|
||||
//.filter { it.score < 0.15 }
|
||||
.groupBy { it.name }
|
||||
.map { (_, value) -> value.minByOrNull { it.score }!! }
|
||||
.toMutableList()
|
||||
// val map = mutableMapOf<String, Int>()
|
||||
// nameScoreList.forEach {
|
||||
// val key = it.name
|
||||
// val count = map[key] ?: 0
|
||||
// map[key] = count + 1
|
||||
// }
|
||||
// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
|
||||
// val firstFood = nameScoreList[0].name
|
||||
// orderList.remove(firstFood)
|
||||
// orderList.add(0, firstFood)
|
||||
//
|
||||
// val sortedScoreList = maxScoreList.sortedWith(compareBy {
|
||||
// orderList.indexOf(it.name)
|
||||
// })
|
||||
val sortedScoreList = maxScoreList.sortedBy { it.score }
|
||||
logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
|
||||
return sortedScoreList
|
||||
}
|
||||
|
||||
// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
|
||||
// val map = mutableMapOf<String, Int>()
|
||||
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||
// nameScoreList.filter { it.score < 0.05 }.forEach {
|
||||
// val count = map[it.name] ?: 0
|
||||
// map[it.name] = count + 1
|
||||
// }
|
||||
// val list = map.entries.sortedByDescending { it.value }.map { it.key }
|
||||
// return list
|
||||
// }
|
||||
|
||||
data class IdNameScore(
|
||||
val id: Long,
|
||||
var name: String,
|
||||
val score: Double
|
||||
)
|
||||
|
||||
suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
|
||||
try {
|
||||
val count = ObjectBox.countIndexField(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")
|
||||
|
||||
val embeddingsList: List<List<Float>> =
|
||||
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
|
||||
val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
|
||||
val classInfo: FoodClassInfo =
|
||||
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
|
||||
|
||||
val foodMap = classInfo.idx_to_class
|
||||
val list = mutableListOf<Food>()
|
||||
val size = embeddingsList.size
|
||||
val size2 = labelsList.size
|
||||
val size3 = foodMap.size
|
||||
logInfo("initDefFoodData: $size,$size2,$size3")
|
||||
embeddingsList.forEachIndexed { index, floatList ->
|
||||
val classIdx = labelsList[index]
|
||||
val foodName = foodMap["$classIdx"]
|
||||
val array = floatList.toFloatArray()
|
||||
// ObjectBox.boxStore.runInTx {
|
||||
// box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
|
||||
// }
|
||||
val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)
|
||||
list.add(food)
|
||||
}
|
||||
// val tempList = list.chunked(10)
|
||||
ObjectBox.putAll(list)
|
||||
action()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logInfo("initDefFoodData: ---${e.message}--")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ,此方法的主要目的是:从assets 拷贝到 app的cache目录
|
||||
* @param context
|
||||
* @param fileName
|
||||
* @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
|
||||
*
|
||||
* 不可能反正SD
|
||||
*/
|
||||
// fun copyAssetToCache(context: Context, fileName: String): String? {
|
||||
// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
|
||||
// val cacheDir = context.getCacheDir()
|
||||
// if (!cacheDir.exists()) {
|
||||
// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
|
||||
// }
|
||||
// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
|
||||
// if (outPath.exists()) {
|
||||
// outPath.delete() // TODO 如果该文件已经存在,就删掉
|
||||
// }
|
||||
// var `is`: InputStream? = null // 读取
|
||||
// var fos: FileOutputStream? = null // 写入
|
||||
// try {
|
||||
// // 创建文件,如果创建成功,就返回true
|
||||
// val res = outPath.createNewFile()
|
||||
// if (res) {
|
||||
// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
|
||||
// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
|
||||
// val buf = ByteArray(`is`.available()) // 缓存区
|
||||
// var byteCount: Int
|
||||
//
|
||||
// // 开始循环读取
|
||||
// while ((`is`.read(buf).also { byteCount = it }) != -1) {
|
||||
// fos.write(buf, 0, byteCount)
|
||||
// }
|
||||
// return outPath.getAbsolutePath()
|
||||
// }
|
||||
// outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
|
||||
// return cacheFile.absolutePath
|
||||
// } catch (e: IOException) {
|
||||
// e.printStackTrace()
|
||||
// logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}")
|
||||
// // 拷贝失败时删除残缺文件,避免下次读取到损坏文件
|
||||
// if (cacheFile.exists()) {
|
||||
// cacheFile.delete()
|
||||
// }
|
||||
// } finally {
|
||||
// outputStream?.close()
|
||||
// inputStream?.close()
|
||||
// try {
|
||||
// // TODO 一定要记得关闭资源,为了不去性能的磨损
|
||||
// fos?.flush()
|
||||
// `is`?.close()
|
||||
// fos?.close()
|
||||
// } catch (e: IOException) {
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
// private fun logInfo(msg:String) {
|
||||
// Log.d("FoodModule", msg)
|
||||
// LogSaveUtil.saveLogFile(msg)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
fun copyAssetToCache(context: Context, fileName: String): String? {
|
||||
val cacheFile = File(context.cacheDir, fileName)
|
||||
val buffer = ByteArray(8 * 1024)
|
||||
var inputStream: InputStream? = null
|
||||
var outputStream: FileOutputStream? = null
|
||||
|
||||
try {
|
||||
inputStream = context.assets.open(fileName)
|
||||
outputStream = FileOutputStream(cacheFile)
|
||||
var byteCount: Int
|
||||
while (inputStream.read(buffer).also { byteCount = it } != -1) {
|
||||
outputStream.write(buffer, 0, byteCount)
|
||||
}
|
||||
outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
|
||||
return cacheFile.absolutePath
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}")
|
||||
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
|
||||
if (cacheFile.exists()) {
|
||||
cacheFile.delete()
|
||||
}
|
||||
} finally {
|
||||
outputStream?.close()
|
||||
inputStream?.close()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun logInfo(msg:String) {
|
||||
Log.d("FoodModule", msg)
|
||||
LogSaveUtil.saveLogFile(msg)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import com.shuwei.dish.match.base.DeviceRole
|
||||
import com.shuwei.dish.match.base.GlobalData
|
||||
import com.shuwei.dish.match.databinding.ActivityInitBinding
|
||||
import com.shuwei.dish.match.db.AppRepository
|
||||
import com.shuwei.dish.match.objbox.FoodModule
|
||||
import com.shuwei.dish.match.utils.AppUtil
|
||||
import com.shuwei.dish.match.utils.NetworkUtils
|
||||
import com.shuwei.dish.match.utils.SpTool
|
||||
@@ -47,9 +48,9 @@ class InitActivity : BaseActivity() {
|
||||
// useBackground(false)
|
||||
binding = ActivityInitBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
// lifecycleScope.launch {
|
||||
// FoodModule.init(this@InitActivity)
|
||||
// }
|
||||
lifecycleScope.launch {
|
||||
FoodModule.init(this@InitActivity)
|
||||
}
|
||||
// setHeaderBackground(isHomePage = true)
|
||||
setHeaderBgVisible(false)
|
||||
BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString()
|
||||
|
||||
@@ -52,6 +52,7 @@ class MasterScaleActivity : BaseActivity() {
|
||||
setContentView(binding.root)
|
||||
setHeaderBackground()
|
||||
|
||||
binding.ivMasterBack.setOnClickListener { finish() }
|
||||
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
|
||||
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
|
||||
binding.rvScaleList.adapter = adapter
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.shuwei.dish.match.base.BaseFragment
|
||||
import com.shuwei.dish.match.databinding.FragmentCollectBinding
|
||||
import com.shuwei.dish.match.dialog.Loading
|
||||
import com.shuwei.dish.match.objbox.FoodCollectionBean
|
||||
import com.shuwei.dish.match.objbox.FoodModule
|
||||
import com.shuwei.dish.match.ui.SettingActivity
|
||||
import com.shuwei.dish.match.utils.BitmapSaver
|
||||
import com.shuwei.dish.match.utils.Debouncer
|
||||
@@ -119,38 +120,38 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
||||
}
|
||||
|
||||
private fun getImageVector(index: Int, bitmap: Bitmap) {
|
||||
//// val bitmap = BitmapCropper.cropCenter(
|
||||
//// original = srcBmp,
|
||||
//// targetWidth = 900, targetHeight = 900,
|
||||
////// offsetX = 30, offsetY = 100
|
||||
//// )
|
||||
// val imageVector = try {
|
||||
// FoodModule.bitmap2FloatArray(bitmap, false)
|
||||
// } catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
// toast("操作失败")
|
||||
// log("操作失败:${e.message}")
|
||||
// hideWaitingDialog()
|
||||
// return
|
||||
// }
|
||||
// val file = BitmapSaver.saveToAppFilesDir(
|
||||
// bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||||
// val bitmap = BitmapCropper.cropCenter(
|
||||
// original = srcBmp,
|
||||
// targetWidth = 900, targetHeight = 900,
|
||||
//// offsetX = 30, offsetY = 100
|
||||
// )
|
||||
// log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
||||
//
|
||||
// activity?.runOnUiThread {
|
||||
// foodCollectionList[index].let {
|
||||
// it.imageVector = imageVector
|
||||
// it.bitmap = null
|
||||
// it.isShowCamera = false
|
||||
// it.imageFile = file
|
||||
// }
|
||||
// collectionAdapter.notifyItemChanged(index)
|
||||
// }
|
||||
// if (bitmap.isRecycled.not()) {
|
||||
// bitmap.recycle()
|
||||
// }
|
||||
// hideWaitingDialog()
|
||||
val imageVector = try {
|
||||
FoodModule.bitmap2FloatArray(bitmap, false)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast("操作失败")
|
||||
log("操作失败:${e.message}")
|
||||
hideWaitingDialog()
|
||||
return
|
||||
}
|
||||
val file = BitmapSaver.saveToAppFilesDir(
|
||||
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||||
)
|
||||
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
||||
|
||||
activity?.runOnUiThread {
|
||||
foodCollectionList[index].let {
|
||||
it.imageVector = imageVector
|
||||
it.bitmap = null
|
||||
it.isShowCamera = false
|
||||
it.imageFile = file
|
||||
}
|
||||
collectionAdapter.notifyItemChanged(index)
|
||||
}
|
||||
if (bitmap.isRecycled.not()) {
|
||||
bitmap.recycle()
|
||||
}
|
||||
hideWaitingDialog()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:background="@color/bg_color">
|
||||
|
||||
<!-- 顶部标题 -->
|
||||
<TextView
|
||||
android:id="@+id/tvMasterTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:text="全设备秤数据监控"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:textColor="@color/home_title"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold" />
|
||||
@@ -23,20 +27,36 @@
|
||||
android:id="@+id/tvConnectionStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvMasterTitle"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textColor="@color/home_sub_title"
|
||||
android:textSize="24sp"
|
||||
tools:text="已发现设备:3 台" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivMasterBack"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="start"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="10dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tvMasterTitle"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tvMasterTitle"
|
||||
android:src="@drawable/ic_back"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<!-- 秤数据列表 -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvScaleList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvConnectionStatus"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:overScrollMode="never"
|
||||
android:padding="16dp"
|
||||
tools:listitem="@layout/list_item_scale_data"/>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
Reference in New Issue
Block a user