This commit is contained in:
2026-01-14 10:38:09 +08:00
parent e86db688e1
commit 50d12de534
10 changed files with 254 additions and 189 deletions
@@ -0,0 +1,42 @@
package com.sw.dualscreen
class Test {
private var lastWeight = 0
// private var currentWeight = 0
data class WeightRecord(
var eatWeight: Int = 0,
var deviceWeight: Int = 0,
var lastWeight: Int = 0,
var state: Boolean = false
)
private val weightRecord by lazy { WeightRecord() }
fun main() {
readWeight { weight ->
if (weightRecord.state) {
//数据已记录
return@readWeight
}
if (weight == 0 || lastWeight == 0) {
return@readWeight
}
if (weight < lastWeight) {
weightRecord.let {
it.deviceWeight = weight
it.eatWeight = it.deviceWeight - it.lastWeight
it.state = true
}
}
lastWeight = weight
}
}
fun readWeight(block: (Int) -> Unit) {
}
}
@@ -884,7 +884,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
private fun uri2File(uri: Uri, block: (File?, FloatArray?) -> Unit) {
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
val imageVector = try {
FoodModule.bitmap2FloatArray(bitmap)
FoodModule.bitmap2FloatArray(bitmap, false)
} catch (e: Exception) {
e.printStackTrace()
return@let
@@ -148,7 +148,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
//// offsetX = 30, offsetY = 100
// )
val imageVector = try {
FoodModule.bitmap2FloatArray(bitmap)
FoodModule.bitmap2FloatArray(bitmap, false)
} catch (e: Exception) {
e.printStackTrace()
ToastUtils.showToast("操作失败")
@@ -1,34 +0,0 @@
//package com.sw.dualscreen.adapter
//
//import android.content.Context
//import android.view.LayoutInflater
//import android.view.View
//import android.view.ViewGroup
//import android.widget.ImageView
//import com.chad.library.adapter4.BaseQuickAdapter
//import com.chad.library.adapter4.viewholder.QuickViewHolder
//import com.sw.dualscreen.R
//import com.sw.dualscreen.databinding.ListItemCollectedDataBinding
//import com.sw.dualscreen.databinding.ListItemFoodCollectionBinding
//import com.sw.dualscreen.objbox.CollectedFoodInfo
//import com.sw.dualscreen.objbox.FoodCollectionBean
//
//class CollectedFoodAdapter (var list: MutableList<CollectedFoodInfo>) :
// BaseQuickAdapter<CollectedFoodInfo, CollectedFoodAdapter.VH>(list) {
//
// inner class VH(var binding: ListItemCollectedDataBinding) : QuickViewHolder(binding.root)
//
// override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
// val inflater = LayoutInflater.from(context)
// val binding = ListItemCollectedDataBinding.inflate(inflater, parent, false)
// return VH(binding)
// }
//
// override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodInfo?) {
// holder.binding.tvFoodName.text = item?.foodName
// holder.binding.divider.run {
// visibility = if (position == list.size - 1) View.GONE else View.VISIBLE
// }
// }
//
//}
@@ -6,6 +6,8 @@ import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.dualscreen.databinding.ListItemCollectedFoodBinding
import com.sw.dualscreen.ext.gone
import com.sw.dualscreen.ext.visible
import com.sw.dualscreen.objbox.CollectedFoodInfo
class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) :
@@ -22,6 +24,9 @@ class CollectedFoodNewAdapter(var list: MutableList<CollectedFoodInfo>) :
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodInfo?) {
holder.binding.tvFoodName.text = item?.foodName
holder.binding.tvCollectedNum.text = "已采集${item?.foodCount}"
holder.binding.divider.run {
if (position == list.size-1) gone() else visible()
}
}
}
@@ -4,9 +4,11 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.graphics.scale
import com.sw.dualscreen.MyApp
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import org.pytorch.IValue
@@ -23,25 +25,34 @@ object FoodModule {
private const val THRESHOLD = 0.8
// private const val THRESHOLD = 0.0
private lateinit var module: Module
private lateinit var box: Box<Food>
private var module: Module?=null
private var box: Box<Food>?=null
// private lateinit var embeddingsList: List<List<Float>>
// private lateinit var labelsList: IntArray
// private lateinit var classInfo: FoodClassInfo
val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
// val DEFAULT_FOOD_INDEX = -1
// val DEFAULT_FOOD_INDEX = -1
// 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
private const val MODEL_INPUT_WIDTH = 224
private const val MODEL_INPUT_HEIGHT = 224
@SuppressLint("SuspiciousIndentation")
fun init(context: Context, block: () -> Unit = {}) {
Thread {
module = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
val modelPath = copyAssetToCache(context, "best_embedding_model_mobile.pt")
// ToastUtils.showToast("modelPath==null:${modelPath == null}")
module = Module.load(modelPath)
// ToastUtils.showToast("module==null:${module == null}")
box = ObjectBox.boxStore.boxFor(Food::class)
//初始化默认重新拉取数据,先清空本地数据
// ObjectBox.boxStore.runInTx {
if (box.all.isNotEmpty()) {
box.removeAll()
box?.run {
if (all.isNotEmpty()) {
removeAll()
}
}
//if (box.all.isEmpty()) {
// initDefFoodData(context)
@@ -50,29 +61,46 @@ object FoodModule {
}.start()
}
fun uri2FloatArray(uri: Uri): FloatArray? {
return MyApp.instance?.let { context ->
ImageUtil.uriToBitmap(context, uri)?.let {
bitmap2FloatArray(it)
}
}
}
fun bitmap2FloatArray(bitmap: Bitmap): FloatArray {
var tensorStartTime = System.currentTimeMillis()
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
bitmap,
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
)
Timber.tag(TAG).d("bitmap2FloatArray-inputTensor耗时:${System.currentTimeMillis() - tensorStartTime}")
// if (module_mobile == null) {
// init(App.getContext())
// fun uri2FloatArray(uri: Uri): FloatArray? {
// return MyApp.instance?.let { context ->
// ImageUtil.uriToBitmap(context, uri)?.let {
// bitmap2FloatArray(it)
// }
// }
tensorStartTime = System.currentTimeMillis()
val outputTensor = module.forward(IValue.from(inputTensor)).toTensor()
Timber.tag(TAG).d("bitmap2FloatArray-toTensor耗时:${System.currentTimeMillis() - tensorStartTime}")
return outputTensor.dataAsFloatArray
// }
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
)
if (module == null) {
return null
}
val outputTensor = module?.forward(IValue.from(inputTensor))?.toTensor()
return outputTensor?.dataAsFloatArray
} catch (e: OutOfMemoryError) {
e.printStackTrace()
} finally {
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
rgb565Bitmap.recycle()
}
//System.gc()
//System.runFinalization()
}
return null
}
// fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
@@ -97,22 +125,24 @@ object FoodModule {
// return queryFoodNameScore(floatArray, queryCount)
// }
fun queryFoodNameScore(floatArray: FloatArray, queryCount: Int = 15): List<IdNameScore> {
fun queryFoodNameScore(floatArray: FloatArray?, queryCount: Int = 15): List<IdNameScore> {
if (floatArray == null) return emptyList()
val startTime = System.currentTimeMillis()
Timber.tag(TAG).d("queryFoodNameScore-已采集向量总数:${box.all.filter { it.isDel.not() }.size}")
box.store.runInTx { }
val query = box.query()
.equal(Food_.isDel, false)
.and()
.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
.build()
Timber.tag(TAG)
.d("queryFoodNameScore-已采集向量总数:${box?.all?.filter { it.isDel.not() }?.size}")
//box.store.runInTx { }
val query = box?.query()
?.equal(Food_.isDel, false)
?.and()
?.nearestNeighbors(Food_.foodVector, floatArray, queryCount)
?.build()
//查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
// val idScoreList = query.findIdsWithScores()
val objScoreList = query.findWithScores()
val objScoreList = query?.findWithScores()
Timber.tag(TAG).d("idScoreList:${GsonUtils.toJson(objScoreList)}")
val nameScoreList = mutableListOf<IdNameScore>()
objScoreList.forEach {
objScoreList?.forEach {
val food = it.get()
nameScoreList.add(
IdNameScore(
@@ -123,14 +153,15 @@ object FoodModule {
)
}
val nameScoreData = GsonUtils.toJson(nameScoreList)
Timber.tag(TAG).d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
query.close()
Timber.tag(TAG)
.d("queryFood,耗时:${System.currentTimeMillis() - startTime},数据:$nameScoreData")
query?.close()
return nameScoreList
}
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val startTime = System.currentTimeMillis()
val floatArray = bitmap2FloatArray(bitmap)
val floatArray = bitmap2FloatArray(bitmap, false)
Timber.tag(TAG).d("bitmap2FloatArray,耗时:${System.currentTimeMillis() - startTime}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
@@ -156,6 +187,9 @@ object FoodModule {
val sortedScoreList = maxScoreList.sortedWith(compareBy {
orderList.indexOf(it.name)
})
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
//Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
@@ -241,47 +275,75 @@ object FoodModule {
*
* 不可能反正SD
*/
fun copyAssetToCache(context: Context, fileName: String): String? {
// 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
val cacheDir = context.cacheDir
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.assets.open(fileName) // 拿到main/assets目录的输入流,用于读取字节
fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
val buf = ByteArray(`is`.available()) // 缓存区
var byteCount: Int
// fun copyAssetToCache(context: Context, fileName: String): String? {
// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
// val cacheDir = context.cacheDir
// 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.assets.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.absolutePath
// }
// } catch (e: IOException) {
// e.printStackTrace()
// } finally {
// try {
// // TODO 一定要记得关闭资源,为了不去性能的磨损
// fos!!.flush()
// `is`!!.close()
// fos.close()
// } catch (e: IOException) {
// e.printStackTrace()
// }
// }
// return null
// }
// 开始循环读取
while ((`is`.read(buf).also { byteCount = it }) != -1) {
fos.write(buf, 0, byteCount)
}
return outPath.absolutePath
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()
} finally {
try {
// TODO 一定要记得关闭资源,为了不去性能的磨损
fos!!.flush()
`is`!!.close()
fos.close()
} catch (e: IOException) {
e.printStackTrace()
Timber.tag("FoodModule").d("文件拷贝失败 fileName=$fileName, error=${e.message}")
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
if (cacheFile.exists()) {
cacheFile.delete()
}
} finally {
outputStream?.close()
inputStream?.close()
}
return null
}
}