Compare commits
3
Commits
0d0fcd24b7
...
428852a07e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
428852a07e | ||
|
|
96863e2078 | ||
|
|
41036cdbf9 |
@@ -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) {
|
||||
|
||||
@@ -41,6 +41,7 @@ class GoodsInfoSearchAdapter(private var list: MutableList<CookFoodGoodsEntity>)
|
||||
): VH {
|
||||
val binding =
|
||||
ListItemSearchGoodsInfoBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
|
||||
return VH(binding)
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.shuwei.dish.match.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.shuwei.dish.match.R
|
||||
import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter
|
||||
import com.shuwei.dish.match.base.BaseActivity
|
||||
import com.shuwei.dish.match.databinding.DialogFoodRecognizeBinding
|
||||
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
|
||||
|
||||
/**
|
||||
* 菜品识别结果弹窗,继承 BottomSheetDialog 确保只初始化一次
|
||||
* @param activity 宿主 Activity
|
||||
* @param onItemSelected 用户点击某一菜品时的回调
|
||||
*/
|
||||
class FoodRecognizeDialog(
|
||||
private val activity: BaseActivity,
|
||||
private val onItemSelected: (item: CookFoodGoodsEntity) -> Unit
|
||||
) : BottomSheetDialog(activity, R.style.BottomSheet) {
|
||||
|
||||
private val binding = DialogFoodRecognizeBinding.inflate(LayoutInflater.from(activity))
|
||||
private val list = mutableListOf<CookFoodGoodsEntity>()
|
||||
private val adapter = GoodsInfoSearchAdapter(list).apply {
|
||||
setOnItemClickListener { _, _, position ->
|
||||
list[position].isClicked = true
|
||||
notifyItemChanged(position)
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
onItemSelected(list[position])
|
||||
dismiss()
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
setContentView(binding.root)
|
||||
setCancelable(true)
|
||||
behavior.skipCollapsed = false
|
||||
setOnDismissListener { activity.hideStatusBar() }
|
||||
binding.recyclerView.run {
|
||||
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
|
||||
adapter = this@FoodRecognizeDialog.adapter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载识别结果数据并显示弹窗
|
||||
* @param items 识别到的菜品列表
|
||||
*/
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun loadData(items: List<CookFoodGoodsEntity>) {
|
||||
list.clear()
|
||||
list.addAll(items)
|
||||
adapter.notifyDataSetChanged()
|
||||
if (!isShowing) show()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.shuwei.dish.match.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
@@ -18,12 +17,15 @@ 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
|
||||
import com.shuwei.dish.match.utils.Weigher2
|
||||
import com.shuwei.dish.match.utils.WeightUtil
|
||||
import com.shuwei.dish.match.utils.ext.gone
|
||||
import com.shuwei.dish.match.utils.ext.startActivity
|
||||
import com.shuwei.dish.match.utils.ext.visible
|
||||
import com.shuwei.dish.match.viewmodel.AppViewModel
|
||||
import com.shuwei.dish.match.viewmodel.factory.AppFactory
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -47,9 +49,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()
|
||||
@@ -63,9 +65,6 @@ class InitActivity : BaseActivity() {
|
||||
WeightUtil.startContinuousRead()
|
||||
|
||||
initViews()
|
||||
val dpi = resources.displayMetrics.density
|
||||
val metrics = resources.displayMetrics.toString()
|
||||
Log.d(TAG, "dpi=$dpi, $metrics")
|
||||
}
|
||||
|
||||
private fun initViewModel() {
|
||||
@@ -74,9 +73,9 @@ class InitActivity : BaseActivity() {
|
||||
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
|
||||
}
|
||||
|
||||
private fun startNextPage() {
|
||||
private fun startNextPage(withDelay: Boolean = false) {
|
||||
lifecycleScope.launch {
|
||||
delay(2000)
|
||||
if (withDelay) delay(2000)
|
||||
// 子设备:直接进入小屏专属页面,不走原有大屏业务流程
|
||||
if (GlobalData.deviceRole == DeviceRole.SLAVE) {
|
||||
startActivity<SlaveActivity>()
|
||||
@@ -135,9 +134,6 @@ class InitActivity : BaseActivity() {
|
||||
binding.btnConnectNetwork.setOnClickListener {
|
||||
openNetworkSettings()
|
||||
}
|
||||
|
||||
// 启动加载动画 - 持续旋转
|
||||
startLoadingAnimation()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,9 +155,12 @@ class InitActivity : BaseActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (NetworkUtils.isNetworkConnected(this)) {
|
||||
startNextPage()
|
||||
// 有网络时隐藏网络检测区域,直接跳转
|
||||
binding.llNetwork.gone()
|
||||
startNextPage(withDelay = false)
|
||||
return
|
||||
}
|
||||
binding.llNetwork.visible()
|
||||
startCountdown()
|
||||
}
|
||||
|
||||
@@ -178,9 +177,11 @@ class InitActivity : BaseActivity() {
|
||||
startTime = SystemClock.elapsedRealtime()
|
||||
lastNetworkCheckTime = 0
|
||||
// binding.llNetwork.setBackgroundColor(Color.TRANSPARENT)
|
||||
binding.llLoading.visibility = android.view.View.VISIBLE
|
||||
binding.llNetworkButton.visibility = android.view.View.GONE
|
||||
binding.llLoading.visible()
|
||||
binding.llNetworkButton.gone()
|
||||
binding.tvCountdown.text = "60秒"
|
||||
// 无网络时才启动加载动画
|
||||
startLoadingAnimation()
|
||||
scheduleCountdown()
|
||||
}
|
||||
|
||||
@@ -233,8 +234,8 @@ class InitActivity : BaseActivity() {
|
||||
*/
|
||||
private fun showNetworkButton() {
|
||||
// binding.llNetwork.setBackgroundColor("#5C77F7".toColorInt())
|
||||
binding.llLoading.visibility = android.view.View.GONE
|
||||
binding.llNetworkButton.visibility = android.view.View.VISIBLE
|
||||
binding.llLoading.gone()
|
||||
binding.llNetworkButton.visible()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.shuwei.dish.match.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.viewModels
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.shuwei.dish.match.R
|
||||
@@ -12,23 +13,34 @@ import com.shuwei.dish.match.adapter.DishPartAdapter
|
||||
import com.shuwei.dish.match.base.BaseActivity
|
||||
import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding
|
||||
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
|
||||
import com.shuwei.dish.match.dialog.FoodSearchDialog
|
||||
import com.shuwei.dish.match.dialog.CommonDialog
|
||||
import com.shuwei.dish.match.dialog.FoodRecognizeDialog
|
||||
import com.shuwei.dish.match.dialog.FoodSearchDialog
|
||||
import com.shuwei.dish.match.entity.CookFoodEntity
|
||||
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
|
||||
import com.shuwei.dish.match.entity.FoodRecord
|
||||
import com.shuwei.dish.match.net.NetViewModel
|
||||
import android.view.ViewGroup
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
|
||||
import com.shuwei.dish.match.objbox.FoodModule
|
||||
import com.shuwei.dish.match.utils.AddressUtil
|
||||
import com.shuwei.dish.match.utils.CameraUtils
|
||||
import com.shuwei.dish.match.utils.ImageUtil
|
||||
import com.shuwei.dish.match.utils.SwipeCallback
|
||||
import com.shuwei.dish.match.utils.WeightUtil
|
||||
import com.shuwei.dish.match.utils.ext.clickWithDebounce
|
||||
import com.shuwei.dish.match.utils.ext.gone
|
||||
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
|
||||
import com.shuwei.dish.match.utils.ext.startActivity
|
||||
import com.shuwei.dish.match.utils.ext.toJsonString
|
||||
import com.shuwei.dish.match.utils.ext.toast
|
||||
import com.shuwei.dish.match.utils.ext.visible
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Serializable
|
||||
import kotlin.getValue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
class PrepareCookActivity : BaseActivity() {
|
||||
@@ -40,6 +52,8 @@ class PrepareCookActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityPrepareCookBinding
|
||||
|
||||
private val cameraUtils: CameraUtils by lazy { CameraUtils(this) }
|
||||
|
||||
private var food: FoodRecord? = null
|
||||
|
||||
private var goodsList: MutableList<CookFoodGoodsEntity>? = null
|
||||
@@ -69,6 +83,7 @@ class PrepareCookActivity : BaseActivity() {
|
||||
binding.tvDishName.text = food?.foodName ?: ""
|
||||
addViewClickListener()
|
||||
|
||||
initCamera(binding.flCameraContainer)
|
||||
initRecyclerView()
|
||||
|
||||
getDishDetail()
|
||||
@@ -81,6 +96,9 @@ class PrepareCookActivity : BaseActivity() {
|
||||
//Log.d(TAG, "addViewClickListener: address=$address,state=$state,weight=$weight")
|
||||
if (address == AddressUtil.ONE) {
|
||||
binding.tvDishPartWeight.text = "${weight}"
|
||||
if (state == WeightUtil.STATE_STABLE) {
|
||||
recognizeFood(weight)
|
||||
}
|
||||
}
|
||||
})
|
||||
binding.ivWeightClear.setOnClickListener {
|
||||
@@ -134,6 +152,25 @@ class PrepareCookActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val isTakingPhoto = AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
* 重量稳定时触发菜品识别
|
||||
* @param weight 当前稳定重量
|
||||
*/
|
||||
private fun recognizeFood(weight: Double) {
|
||||
if (weight < 10) {
|
||||
if (recognizeDialog.isShowing) recognizeDialog.dismiss()
|
||||
return
|
||||
}
|
||||
if (isTakingPhoto.get() || recognizeDialog.isShowing) return
|
||||
isTakingPhoto.set(true)
|
||||
cameraUtils.takePhoto(
|
||||
succCallback = cameraSuccessCallback,
|
||||
failCallback = cameraFailureCallback
|
||||
)
|
||||
}
|
||||
|
||||
private fun addFood() {
|
||||
FoodSearchDialog().show(this) { item ->
|
||||
val filterResult = list.firstOrNull { it.goodsId == item.goodsId }
|
||||
@@ -251,6 +288,16 @@ class PrepareCookActivity : BaseActivity() {
|
||||
|
||||
private var firstReqSize = 0
|
||||
private val list = mutableListOf<CookFoodGoodsEntity>()
|
||||
private val recognizeDialog by lazy {
|
||||
FoodRecognizeDialog(this) { item ->
|
||||
// TODO: 处理识别结果选中
|
||||
}
|
||||
.apply {
|
||||
setOnDismissListener {
|
||||
isTakingPhoto.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val dishPartAdapter by lazy {
|
||||
DishPartAdapter(list).apply {
|
||||
setOnItemClickListener { _, _, positon ->
|
||||
@@ -326,6 +373,26 @@ class PrepareCookActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
cameraUtils.bind()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
cameraUtils.unbind()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化相机并绑定预览容器
|
||||
* @param container 相机预览容器
|
||||
*/
|
||||
fun initCamera(container: ViewGroup) {
|
||||
cameraUtils.initCamera()
|
||||
val previewBinding = LayoutCameraPreviewBinding.inflate(layoutInflater, container)
|
||||
cameraUtils.setPreviewController(previewBinding.previewView)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("GestureBackNavigation")
|
||||
@Deprecated("Deprecated in Java")
|
||||
@@ -367,4 +434,49 @@ class PrepareCookActivity : BaseActivity() {
|
||||
}.show()
|
||||
}
|
||||
|
||||
private val cameraSuccessCallback: (Uri) -> Unit = { uri ->
|
||||
Log.d(TAG, "takePhoto success")
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val bitmap = ImageUtil.uriToBitmap(this@PrepareCookActivity, uri)
|
||||
if (bitmap == null) {
|
||||
Log.d(TAG, "takePhoto bitmap is null")
|
||||
isTakingPhoto.set(false)
|
||||
return@withContext
|
||||
}
|
||||
val foodScoreList = FoodModule.getFoodScoreList(bitmap)
|
||||
if (foodScoreList.isEmpty()) {
|
||||
Log.d(TAG, "takePhoto foodScoreList is empty")
|
||||
isTakingPhoto.set(false)
|
||||
return@withContext
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
queryFood(foodScoreList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val cameraFailureCallback: (String) -> Unit = { errMsg ->
|
||||
Log.d(TAG, "takePhoto failure")
|
||||
isTakingPhoto.set(false)
|
||||
}
|
||||
|
||||
private fun queryFood(list: List<FoodModule.IdNameScore>) {
|
||||
Log.d(TAG, "takePhoto queryFood, list=${list.toString()}")
|
||||
//调用接口成功,返回食材列表设置isTakingPhoto.set(false),暂时写死数据
|
||||
val foodList = mutableListOf<CookFoodGoodsEntity>()
|
||||
list.forEachIndexed { index, food ->
|
||||
val foodName = food.name.split("WP").first()
|
||||
val foodScore = ((1-food.score) * 100).roundedDecimalPlace(2)
|
||||
foodList.add(CookFoodGoodsEntity(
|
||||
goodsId = index.toString(),
|
||||
goodsName = "${foodName}-${foodScore}%",
|
||||
foodId = index.toString()
|
||||
))
|
||||
}
|
||||
recognizeDialog.loadData(foodList)
|
||||
isTakingPhoto.set(false)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -82,18 +82,19 @@ class SelectDishActivity : BaseActivity() {
|
||||
private var configFinished = false
|
||||
|
||||
fun judgeDeviceConfig(block: () -> Unit) {
|
||||
if (configFinished) {
|
||||
block()
|
||||
return
|
||||
}
|
||||
appViewModel.loadSeasoning {
|
||||
if (it.isEmpty()) {
|
||||
showDeviceConfigDialog()
|
||||
return@loadSeasoning
|
||||
}
|
||||
configFinished = true
|
||||
block()
|
||||
}
|
||||
// if (configFinished) {
|
||||
// block()
|
||||
// return
|
||||
// }
|
||||
// appViewModel.loadSeasoning {
|
||||
// if (it.isEmpty()) {
|
||||
// showDeviceConfigDialog()
|
||||
// return@loadSeasoning
|
||||
// }
|
||||
// configFinished = true
|
||||
// block()
|
||||
// }
|
||||
block()
|
||||
}
|
||||
|
||||
private fun showDeviceConfigDialog() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.shuwei.dish.match.utils;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Created by Administrator on 2018/11/9.
|
||||
*/
|
||||
public class GpioUtils {
|
||||
|
||||
private static final String TAG = "GpioUtils";
|
||||
|
||||
/*
|
||||
给export申请权限
|
||||
*/
|
||||
public static void upgradeRootPermissionForExport() {
|
||||
upgradeRootPermission("/sys/class/gpio/export");
|
||||
}
|
||||
|
||||
/*
|
||||
配置一个gpio路径
|
||||
*/
|
||||
public static boolean exportGpio(int gpio) {
|
||||
return writeNode("/sys/class/gpio/export", "" + gpio);
|
||||
}
|
||||
|
||||
/*
|
||||
给获取io口的状态的路径申请权限,该方法需要在exportGpio后调用
|
||||
*/
|
||||
public static void upgradeRootPermissionForGpio(int gpio) {
|
||||
upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/direction");
|
||||
upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/value");
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
设置io口为输入或输出
|
||||
*/
|
||||
public static boolean setGpioDirection(int gpio, int arg) {
|
||||
String gpioDirection = "";
|
||||
if (arg == 0) gpioDirection = "out";
|
||||
else if (arg == 1) gpioDirection = "in";
|
||||
else return false;
|
||||
return writeNode("/sys/class/gpio/gpio" + gpio + "/direction", gpioDirection);
|
||||
}
|
||||
|
||||
/*
|
||||
获取io口的状态为输出还是输入
|
||||
*/
|
||||
public static String getGpioDirection(int gpio) {
|
||||
String gpioDirection = "";
|
||||
gpioDirection = readNode("/sys/class/gpio/gpio" + gpio + "/direction");
|
||||
return gpioDirection;
|
||||
}
|
||||
|
||||
/*
|
||||
给输出io口写值,高电平或低电平
|
||||
*/
|
||||
public static boolean writeGpioValue(int gpio, String arg) {
|
||||
return writeNode("/sys/class/gpio/gpio" + gpio + "/value", arg);
|
||||
}
|
||||
|
||||
//获取当前gpio是高还是低
|
||||
public static String getGpioValue(int gpio) {
|
||||
return readNode("/sys/class/gpio/gpio" + gpio + "/value");
|
||||
}
|
||||
|
||||
|
||||
private static boolean upgradeRootPermission(String path) {
|
||||
Process process = null;
|
||||
DataOutputStream os = null;
|
||||
try {
|
||||
String cmd = "chmod 777 " + path;
|
||||
process = Runtime.getRuntime().exec("su"); //切换到root帐号
|
||||
os = new DataOutputStream(process.getOutputStream());
|
||||
os.writeBytes(cmd + "\n");
|
||||
os.writeBytes("exit\n");
|
||||
os.flush();
|
||||
process.waitFor();
|
||||
} catch (Exception e) {
|
||||
} finally {
|
||||
try {
|
||||
if (os != null) {
|
||||
os.close();
|
||||
}
|
||||
process.destroy();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
return process.waitFor() == 0;
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean writeNode(String path, String arg) {
|
||||
Log.d(TAG, "Gpio_test set node path: " + path + " arg: " + arg);
|
||||
if (path == null || arg == null) {
|
||||
Log.e(TAG, "set node error");
|
||||
return false;
|
||||
}
|
||||
FileWriter fileWriter = null;
|
||||
BufferedWriter bufferedWriter = null;
|
||||
try {
|
||||
fileWriter = new FileWriter(path);
|
||||
fileWriter.write(arg);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Gpio_test write node error!! path" + path + " arg: " + arg);
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
if (fileWriter != null) {
|
||||
fileWriter.close();
|
||||
}
|
||||
if (bufferedWriter != null) {
|
||||
bufferedWriter.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static long mTime = 0;
|
||||
private static int mFailTimes = 0;
|
||||
|
||||
private static String readNode(String path) {
|
||||
String result = "";
|
||||
|
||||
FileReader fread = null;
|
||||
BufferedReader buffer = null;
|
||||
try {
|
||||
fread = new FileReader(path);
|
||||
buffer = new BufferedReader(fread);
|
||||
String str = null;
|
||||
while ((str = buffer.readLine()) != null) {
|
||||
result = str;
|
||||
break;
|
||||
}
|
||||
mFailTimes = 0;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "IO Exception");
|
||||
e.printStackTrace();
|
||||
if (mTime == 0 || SystemClock.uptimeMillis() - mTime < 1000) {
|
||||
mFailTimes++;
|
||||
}
|
||||
if (mFailTimes >= 3) {
|
||||
Log.d(TAG, "read format node continuous failed three times, exist thread");
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (buffer != null) {
|
||||
buffer.close();
|
||||
}
|
||||
if (fread != null) {
|
||||
fread.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.shuwei.dish.match.utils;
|
||||
|
||||
/**
|
||||
* Log统一管理类
|
||||
*/
|
||||
public class L {
|
||||
|
||||
private L() {
|
||||
/* cannot be instantiated */
|
||||
throw new UnsupportedOperationException("cannot be instantiated");
|
||||
}
|
||||
|
||||
public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
|
||||
private static final String TAG = "mzf";
|
||||
|
||||
// 下面四个是默认tag的函数
|
||||
public static void i(String msg) {
|
||||
if (isDebug)
|
||||
android.util.Log.i(TAG, msg);
|
||||
}
|
||||
|
||||
public static void d(String msg) {
|
||||
if (isDebug)
|
||||
android.util.Log.d(TAG, msg);
|
||||
}
|
||||
|
||||
public static void e(String msg) {
|
||||
if (isDebug)
|
||||
android.util.Log.e(TAG, msg);
|
||||
}
|
||||
|
||||
public static void v(String msg) {
|
||||
if (isDebug)
|
||||
android.util.Log.v(TAG, msg);
|
||||
}
|
||||
|
||||
// 下面是传入自定义tag的函数
|
||||
public static void i(String tag, String msg) {
|
||||
if (isDebug)
|
||||
android.util.Log.i(tag, msg);
|
||||
}
|
||||
|
||||
public static void d(String tag, String msg) {
|
||||
if (isDebug)
|
||||
android.util.Log.d(tag, msg);
|
||||
}
|
||||
|
||||
public static void e(String tag, String msg) {
|
||||
if (isDebug)
|
||||
android.util.Log.e(tag, msg);
|
||||
}
|
||||
|
||||
public static void v(String tag, String msg) {
|
||||
if (isDebug)
|
||||
android.util.Log.v(tag, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.shuwei.dish.match.utils;
|
||||
|
||||
public class LightManager {
|
||||
private static final String TAG = "LightManager";
|
||||
|
||||
public static void openGreenLight() {
|
||||
L.e(TAG, "openGreenLight");
|
||||
GpioUtils.writeGpioValue(41, "1");
|
||||
}
|
||||
|
||||
public static void closeGreenLight() {
|
||||
L.e(TAG, "closeGreenLight");
|
||||
GpioUtils.writeGpioValue(41, "0");
|
||||
}
|
||||
|
||||
public static void openRedLight() {
|
||||
L.e(TAG, "openRedLight");
|
||||
GpioUtils.writeGpioValue(40, "1");
|
||||
}
|
||||
|
||||
public static void closeRedLight() {
|
||||
L.e(TAG, "closeRedLight");
|
||||
GpioUtils.writeGpioValue(40, "0");
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- Loading 动画容器 -->
|
||||
<LinearLayout
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -262,6 +262,11 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/flCameraContainer"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/btnCook"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
tools:background="@color/white">
|
||||
|
||||
<View
|
||||
android:id="@+id/viewLine"
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="10dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:background="@drawable/shape_gray_dc_5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSheetName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="50dp"
|
||||
android:text="菜品识别结果"
|
||||
android:textColor="@color/black333"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="45dp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginEnd="45dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:minHeight="500dp"
|
||||
android:nestedScrollingEnabled="true"
|
||||
android:overScrollMode="never"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="2"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/list_item_search_goods_info" />
|
||||
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user