From 50d12de534ef3dc4f1574232eef1f0228dc074ec Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Wed, 14 Jan 2026 10:38:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/sw/dualscreen/Test.kt | 42 ++++ .../sw/dualscreen/activity/MainActivity.kt | 2 +- .../activity/fragment/CollectFragment.kt | 2 +- .../adapter/CollectedFoodAdapter.kt | 34 --- .../adapter/CollectedFoodNewAdapter.kt | 5 + .../com/sw/dualscreen/objbox/FoodModule.kt | 212 +++++++++++------- app/src/main/res/drawable/ripple_effect.xml | 7 + app/src/main/res/layout/activity_main.xml | 110 ++++----- app/src/main/res/layout/fragment_collect.xml | 7 +- .../res/layout/list_item_collected_food.xml | 22 +- 10 files changed, 254 insertions(+), 189 deletions(-) create mode 100644 app/src/main/java/com/sw/dualscreen/Test.kt delete mode 100644 app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodAdapter.kt create mode 100644 app/src/main/res/drawable/ripple_effect.xml diff --git a/app/src/main/java/com/sw/dualscreen/Test.kt b/app/src/main/java/com/sw/dualscreen/Test.kt new file mode 100644 index 0000000..f4aa6e5 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/Test.kt @@ -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) { + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt index a0f1f7a..d9abf82 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt @@ -884,7 +884,7 @@ class MainActivity : BaseActivity() { 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 diff --git a/app/src/main/java/com/sw/dualscreen/activity/fragment/CollectFragment.kt b/app/src/main/java/com/sw/dualscreen/activity/fragment/CollectFragment.kt index e5e1a59..84d2ef3 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/fragment/CollectFragment.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/fragment/CollectFragment.kt @@ -148,7 +148,7 @@ class CollectFragment : BaseFragment() { //// offsetX = 30, offsetY = 100 // ) val imageVector = try { - FoodModule.bitmap2FloatArray(bitmap) + FoodModule.bitmap2FloatArray(bitmap, false) } catch (e: Exception) { e.printStackTrace() ToastUtils.showToast("操作失败") diff --git a/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodAdapter.kt b/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodAdapter.kt deleted file mode 100644 index ab6dc01..0000000 --- a/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodAdapter.kt +++ /dev/null @@ -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) : -// BaseQuickAdapter(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 -// } -// } -// -//} \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodNewAdapter.kt b/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodNewAdapter.kt index a39a4a9..15c388b 100644 --- a/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodNewAdapter.kt +++ b/app/src/main/java/com/sw/dualscreen/adapter/CollectedFoodNewAdapter.kt @@ -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) : @@ -22,6 +24,9 @@ class CollectedFoodNewAdapter(var list: MutableList) : 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() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt b/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt index a01da07..9a0f986 100644 --- a/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt +++ b/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt @@ -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 + private var module: Module?=null + private var box: Box?=null // private lateinit var embeddingsList: List> // 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? { @@ -97,22 +125,24 @@ object FoodModule { // return queryFoodNameScore(floatArray, queryCount) // } - fun queryFoodNameScore(floatArray: FloatArray, queryCount: Int = 15): List { + fun queryFoodNameScore(floatArray: FloatArray?, queryCount: Int = 15): List { + 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() - 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 { 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 } - } \ No newline at end of file diff --git a/app/src/main/res/drawable/ripple_effect.xml b/app/src/main/res/drawable/ripple_effect.xml new file mode 100644 index 0000000..f176362 --- /dev/null +++ b/app/src/main/res/drawable/ripple_effect.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2d8bf3d..1aac77a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,14 +1,15 @@ + tools:context=".activity.MainActivity" + tools:theme="@style/Theme.AppCompat.Light.DarkActionBar"> - - - - - - - - - - - - - - - - + android:textSize="52sp" + android:textStyle="bold" /> + android:visibility="gone" + app:shapeAppearance="@style/round10dpCornerStyle" /> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_weight="1" + android:overScrollMode="never" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" + app:spanCount="2" + tools:itemCount="8" + tools:listitem="@layout/list_item_search_food" /> - + android:textStyle="bold" + android:visibility="gone" /> + + + + + + + android:text="去结算" + android:textColor="@color/white" + android:textSize="40sp" + android:textStyle="bold" /> diff --git a/app/src/main/res/layout/fragment_collect.xml b/app/src/main/res/layout/fragment_collect.xml index d5e9d0a..29bef97 100644 --- a/app/src/main/res/layout/fragment_collect.xml +++ b/app/src/main/res/layout/fragment_collect.xml @@ -97,7 +97,9 @@ android:src="@drawable/ic_retake_photo" app:layout_constraintEnd_toEndOf="@id/btnCollectedFood" app:layout_constraintTop_toTopOf="@id/flPreview" - tools:ignore="ContentDescription" /> + tools:ignore="ContentDescription" + android:foreground="@drawable/ripple_effect"/> + + tools:ignore="ContentDescription" + android:foreground="@drawable/ripple_effect"/> - - \ No newline at end of file + + + \ No newline at end of file