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/
|
.claude/
|
||||||
app/debug/
|
app/debug/
|
||||||
app/release/
|
app/release/
|
||||||
|
# 临时忽略 assets 下的 .pt 模型文件,正式文件就绪后取消此行
|
||||||
|
app/src/main/assets/*.pt
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.camera.extensions)
|
implementation(libs.androidx.camera.extensions)
|
||||||
|
|
||||||
//pytorch
|
//pytorch
|
||||||
// implementation (libs.pytorch.android)
|
implementation (libs.pytorch.android)
|
||||||
// implementation (libs.pytorch.android.torchvision)
|
implementation (libs.pytorch.android.torchvision)
|
||||||
|
|
||||||
// objectbox
|
// objectbox
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.content.SharedPreferences
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.shuwei.dish.match.R
|
import com.shuwei.dish.match.R
|
||||||
import com.shuwei.dish.match.db.DatabaseProvider
|
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.scale.ScaleServiceManager
|
||||||
import com.shuwei.dish.match.ui.InitActivity.Companion.TAG
|
import com.shuwei.dish.match.ui.InitActivity.Companion.TAG
|
||||||
import com.shuwei.dish.match.utils.AppUtil
|
import com.shuwei.dish.match.utils.AppUtil
|
||||||
@@ -44,7 +45,7 @@ class BaseApp : Application() {
|
|||||||
// GlobalData.deviceId = "39a7abdd06b3c7ab"
|
// GlobalData.deviceId = "39a7abdd06b3c7ab"
|
||||||
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
|
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
|
||||||
registerReceiver(BootReceiver(), filter)
|
registerReceiver(BootReceiver(), filter)
|
||||||
// ObjectBox.init(this)
|
ObjectBox.init(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
|
|||||||
@@ -1,308 +1,308 @@
|
|||||||
//package com.shuwei.dish.match.objbox
|
package com.shuwei.dish.match.objbox
|
||||||
//
|
|
||||||
//import android.content.Context
|
import android.content.Context
|
||||||
//import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
//import android.util.Log
|
import android.util.Log
|
||||||
//import androidx.core.graphics.scale
|
import androidx.core.graphics.scale
|
||||||
//import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
//import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
//import com.shuwei.dish.match.utils.AssetsTool
|
import com.shuwei.dish.match.utils.AssetsTool
|
||||||
//import com.shuwei.dish.match.utils.LogSaveUtil
|
import com.shuwei.dish.match.utils.LogSaveUtil
|
||||||
//import com.shuwei.dish.match.utils.ext.toJsonString
|
import com.shuwei.dish.match.utils.ext.toJsonString
|
||||||
//import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
//import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
//import org.pytorch.IValue
|
import org.pytorch.IValue
|
||||||
//import org.pytorch.Module
|
import org.pytorch.Module
|
||||||
//import org.pytorch.torchvision.TensorImageUtils
|
import org.pytorch.torchvision.TensorImageUtils
|
||||||
//import java.io.File
|
import java.io.File
|
||||||
//import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
//import java.io.IOException
|
import java.io.IOException
|
||||||
//import java.io.InputStream
|
import java.io.InputStream
|
||||||
//
|
|
||||||
//
|
|
||||||
//object FoodModule {
|
object FoodModule {
|
||||||
//
|
|
||||||
// private lateinit var module_mobile: Module
|
private lateinit var module_mobile: Module
|
||||||
//
|
|
||||||
// private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
|
private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
|
||||||
// private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
|
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
|
||||||
//
|
|
||||||
// // 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
|
// 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
|
||||||
// private const val MODEL_INPUT_WIDTH = 300
|
private const val MODEL_INPUT_WIDTH = 300
|
||||||
// private const val MODEL_INPUT_HEIGHT = 300
|
private const val MODEL_INPUT_HEIGHT = 300
|
||||||
// const val DEFAULT_FOOD_INDEX = -1
|
const val DEFAULT_FOOD_INDEX = -1
|
||||||
// const val DEFAULT_QUERY_COUNT = 50
|
const val DEFAULT_QUERY_COUNT = 50
|
||||||
//
|
|
||||||
// const val BAG_RATE = 0.05
|
const val BAG_RATE = 0.05
|
||||||
//
|
|
||||||
// suspend fun init(context: Context) {
|
suspend fun init(context: Context) {
|
||||||
// withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
|
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
|
||||||
// initDefFoodData(context)
|
initDefFoodData(context)
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
//// fun uri2FloatArray(uri: Uri): FloatArray? {
|
// fun uri2FloatArray(uri: Uri): FloatArray? {
|
||||||
//// return MyApp.instance?.let { context ->
|
// return MyApp.instance?.let { context ->
|
||||||
//// ImageUtil.uriToBitmap(context, uri)?.let {
|
// ImageUtil.uriToBitmap(context, uri)?.let {
|
||||||
//// bitmap2FloatArray(it)
|
// 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()
|
|
||||||
// }
|
// }
|
||||||
// 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? {
|
// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
|
||||||
//// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
|
// val floatArray = bitmap2FloatArray(bitmap)
|
||||||
//// val cacheDir = context.getCacheDir()
|
// return queryFood(floatArray, queryCount)
|
||||||
//// if (!cacheDir.exists()) {
|
// }
|
||||||
//// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
|
|
||||||
//// }
|
// /**
|
||||||
//// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
|
// * 返回识别物品IdNameScore对象列表
|
||||||
//// if (outPath.exists()) {
|
// */
|
||||||
//// outPath.delete() // TODO 如果该文件已经存在,就删掉
|
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
|
||||||
//// }
|
// val floatArray = bitmap2FloatArray(bitmap)
|
||||||
//// var `is`: InputStream? = null // 读取
|
// return queryFoodNameScore(floatArray, queryCount)
|
||||||
//// var fos: FileOutputStream? = null // 写入
|
// }
|
||||||
//// try {
|
|
||||||
//// // 创建文件,如果创建成功,就返回true
|
suspend fun queryFoodNameScore(
|
||||||
//// val res = outPath.createNewFile()
|
floatArray: FloatArray?,
|
||||||
//// if (res) {
|
queryCount: Int = DEFAULT_QUERY_COUNT
|
||||||
//// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
|
): List<IdNameScore> {
|
||||||
//// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
|
if (floatArray == null) return emptyList()
|
||||||
//// val buf = ByteArray(`is`.available()) // 缓存区
|
// val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
|
||||||
//// var byteCount: Int
|
// //查询比较分数
|
||||||
////
|
//// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
|
||||||
//// // 开始循环读取
|
// var idScoreList: List<IdWithScore>
|
||||||
//// 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
|
|
||||||
//
|
|
||||||
// try {
|
// try {
|
||||||
// inputStream = context.assets.open(fileName)
|
// idScoreList = query.findIdsWithScores();
|
||||||
// outputStream = FileOutputStream(cacheFile)
|
// } finally {
|
||||||
// var byteCount: Int
|
// // 先关闭Query,释放Cursor
|
||||||
// while (inputStream.read(buffer).also { byteCount = it } != -1) {
|
// query.close()
|
||||||
// outputStream.write(buffer, 0, byteCount)
|
// }
|
||||||
|
|
||||||
|
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) {
|
// } catch (e: IOException) {
|
||||||
// e.printStackTrace()
|
// e.printStackTrace()
|
||||||
// logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}")
|
|
||||||
// // 拷贝失败时删除残缺文件,避免下次读取到损坏文件
|
|
||||||
// if (cacheFile.exists()) {
|
|
||||||
// cacheFile.delete()
|
|
||||||
// }
|
|
||||||
// } finally {
|
// } finally {
|
||||||
// outputStream?.close()
|
// try {
|
||||||
// inputStream?.close()
|
// // TODO 一定要记得关闭资源,为了不去性能的磨损
|
||||||
|
// fos?.flush()
|
||||||
|
// `is`?.close()
|
||||||
|
// fos?.close()
|
||||||
|
// } catch (e: IOException) {
|
||||||
|
// e.printStackTrace()
|
||||||
|
// }
|
||||||
// }
|
// }
|
||||||
// return null
|
// return null
|
||||||
// }
|
// }
|
||||||
//
|
|
||||||
// private fun logInfo(msg:String) {
|
fun copyAssetToCache(context: Context, fileName: String): String? {
|
||||||
// Log.d("FoodModule", msg)
|
val cacheFile = File(context.cacheDir, fileName)
|
||||||
// LogSaveUtil.saveLogFile(msg)
|
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.base.GlobalData
|
||||||
import com.shuwei.dish.match.databinding.ActivityInitBinding
|
import com.shuwei.dish.match.databinding.ActivityInitBinding
|
||||||
import com.shuwei.dish.match.db.AppRepository
|
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.AppUtil
|
||||||
import com.shuwei.dish.match.utils.NetworkUtils
|
import com.shuwei.dish.match.utils.NetworkUtils
|
||||||
import com.shuwei.dish.match.utils.SpTool
|
import com.shuwei.dish.match.utils.SpTool
|
||||||
@@ -47,9 +48,9 @@ class InitActivity : BaseActivity() {
|
|||||||
// useBackground(false)
|
// useBackground(false)
|
||||||
binding = ActivityInitBinding.inflate(layoutInflater)
|
binding = ActivityInitBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
// lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// FoodModule.init(this@InitActivity)
|
FoodModule.init(this@InitActivity)
|
||||||
// }
|
}
|
||||||
// setHeaderBackground(isHomePage = true)
|
// setHeaderBackground(isHomePage = true)
|
||||||
setHeaderBgVisible(false)
|
setHeaderBgVisible(false)
|
||||||
BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString()
|
BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString()
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class MasterScaleActivity : BaseActivity() {
|
|||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
setHeaderBackground()
|
setHeaderBackground()
|
||||||
|
|
||||||
|
binding.ivMasterBack.setOnClickListener { finish() }
|
||||||
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
|
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
|
||||||
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
|
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
|
||||||
binding.rvScaleList.adapter = adapter
|
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.databinding.FragmentCollectBinding
|
||||||
import com.shuwei.dish.match.dialog.Loading
|
import com.shuwei.dish.match.dialog.Loading
|
||||||
import com.shuwei.dish.match.objbox.FoodCollectionBean
|
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.ui.SettingActivity
|
||||||
import com.shuwei.dish.match.utils.BitmapSaver
|
import com.shuwei.dish.match.utils.BitmapSaver
|
||||||
import com.shuwei.dish.match.utils.Debouncer
|
import com.shuwei.dish.match.utils.Debouncer
|
||||||
@@ -119,38 +120,38 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getImageVector(index: Int, bitmap: Bitmap) {
|
private fun getImageVector(index: Int, bitmap: Bitmap) {
|
||||||
//// val bitmap = BitmapCropper.cropCenter(
|
// val bitmap = BitmapCropper.cropCenter(
|
||||||
//// original = srcBmp,
|
// original = srcBmp,
|
||||||
//// targetWidth = 900, targetHeight = 900,
|
// targetWidth = 900, targetHeight = 900,
|
||||||
////// offsetX = 30, offsetY = 100
|
//// 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"
|
|
||||||
// )
|
// )
|
||||||
// log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
val imageVector = try {
|
||||||
//
|
FoodModule.bitmap2FloatArray(bitmap, false)
|
||||||
// activity?.runOnUiThread {
|
} catch (e: Exception) {
|
||||||
// foodCollectionList[index].let {
|
e.printStackTrace()
|
||||||
// it.imageVector = imageVector
|
toast("操作失败")
|
||||||
// it.bitmap = null
|
log("操作失败:${e.message}")
|
||||||
// it.isShowCamera = false
|
hideWaitingDialog()
|
||||||
// it.imageFile = file
|
return
|
||||||
// }
|
}
|
||||||
// collectionAdapter.notifyItemChanged(index)
|
val file = BitmapSaver.saveToAppFilesDir(
|
||||||
// }
|
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||||||
// if (bitmap.isRecycled.not()) {
|
)
|
||||||
// bitmap.recycle()
|
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
||||||
// }
|
|
||||||
// hideWaitingDialog()
|
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")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
tools:background="@color/bg_color">
|
tools:background="@color/bg_color">
|
||||||
|
|
||||||
<!-- 顶部标题 -->
|
<!-- 顶部标题 -->
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tvMasterTitle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
android:text="全设备秤数据监控"
|
android:text="全设备秤数据监控"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:textColor="@color/home_title"
|
android:textColor="@color/home_title"
|
||||||
android:textSize="36sp"
|
android:textSize="36sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
@@ -23,20 +27,36 @@
|
|||||||
android:id="@+id/tvConnectionStatus"
|
android:id="@+id/tvConnectionStatus"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="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:layout_marginBottom="16dp"
|
||||||
android:textColor="@color/home_sub_title"
|
android:textColor="@color/home_sub_title"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
tools:text="已发现设备:3 台" />
|
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
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rvScaleList"
|
android:id="@+id/rvScaleList"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
app:layout_constraintTop_toBottomOf="@id/tvConnectionStatus"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:overScrollMode="never"
|
android:overScrollMode="never"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
tools:listitem="@layout/list_item_scale_data"/>
|
tools:listitem="@layout/list_item_scale_data"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user