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
}
}
@@ -0,0 +1,7 @@
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:attr/colorControlHighlight">
<item android:id="@android:id/mask">
<color android:color="@android:color/white" />
</item>
</ripple>
+38 -72
View File
@@ -1,14 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFE7EFF8"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".activity.MainActivity">
tools:context=".activity.MainActivity"
tools:theme="@style/Theme.AppCompat.Light.DarkActionBar">
<FrameLayout
@@ -36,22 +37,6 @@
android:src="@drawable/ic_setting"
tools:ignore="ContentDescription" />
<!-- <LinearLayout-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="end"-->
<!-- android:background="@drawable/bg_detection_selector">-->
<!-- <Spinner-->
<!-- android:id="@+id/spinner"-->
<!-- style="@style/customSpinnerStyle"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="end"-->
<!-- android:padding="0dp"-->
<!-- android:popupBackground="@drawable/shape_for_custom_spinner" />-->
<!-- </LinearLayout>-->
</FrameLayout>
<TextView
@@ -59,10 +44,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:textStyle="bold"
android:textSize="52sp"
android:text="羊肉臊子荞面饸饹"
android:textColor="#FF0A1428"
android:text="羊肉臊子荞面饸饹" />
android:textSize="52sp"
android:textStyle="bold" />
<FrameLayout
android:id="@+id/foodDisplayLayout"
@@ -83,76 +68,57 @@
android:layout_height="match_parent"
android:contentDescription="食物图预览"
android:scaleType="centerCrop"
app:shapeAppearance="@style/round10dpCornerStyle"
android:visibility="gone" />
android:visibility="gone"
app:shapeAppearance="@style/round10dpCornerStyle" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="27dp"
android:background="@drawable/ic_camera_rect" />
<!-- preview_border-->
<!-- preview_border-->
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="0dp"
app:spanCount="2"
tools:itemCount="8"
android:overScrollMode="never"
tools:listitem="@layout/list_item_search_food"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
android:layout_marginHorizontal="14dp"
android:layout_marginTop="42dp"
android:layout_weight="1" />
<!-- <LinearLayout-->
<!-- android:id="@+id/layoutRescan"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginBottom="140dp"-->
<!-- android:gravity="center"-->
<!-- android:orientation="vertical"-->
<!-- android:visibility="gone">-->
<!-- <TextView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="未识别出菜品,请重试或手动搜索"-->
<!-- android:textColor="#FFFF3232"-->
<!-- android:textSize="24sp" />-->
<!-- <CheckedTextView-->
<!-- android:id="@+id/tv_rescan"-->
<!-- android:layout_width="360dp"-->
<!-- android:layout_height="90dp"-->
<!-- android:layout_marginTop="20dp"-->
<!-- android:background="@drawable/bg_search_food"-->
<!-- android:gravity="center"-->
<!-- android:padding="10dp"-->
<!-- android:text="重试"-->
<!-- android:paddingHorizontal="15dp"-->
<!-- android:textAlignment="center"-->
<!-- android:textColor="@color/color_search_food"-->
<!-- android:textStyle="bold"-->
<!-- android:checked="true"-->
<!-- android:textSize="32sp" />-->
<!-- </LinearLayout>-->
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" />
<TextView
android:id="@+id/tvToSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="38dp"
android:layout_marginBottom="54dp"
android:layout_marginBottom="0dp"
android:padding="10dp"
android:text="以上都不是,手动搜索"
android:textStyle="bold"
android:textColor="#FFFF3232"
android:textSize="31sp"
android:visibility="gone"/>
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="54dp"
android:gravity="center">
<TextView
android:id="@+id/tvWeightRealInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="12sp"
tools:text="以上都不是,手动搜索" />
</LinearLayout>
<FrameLayout
android:id="@+id/flPay"
android:layout_width="match_parent"
@@ -163,13 +129,13 @@
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnPay"
android:layout_width="match_parent"
android:layout_margin="32dp"
android:layout_height="100dp"
android:textStyle="bold"
android:text="去结算"
android:textSize="40sp"
android:layout_margin="32dp"
android:background="@drawable/bg_btn_save"
android:textColor="@color/white"/>
android:text="去结算"
android:textColor="@color/white"
android:textSize="40sp"
android:textStyle="bold" />
</FrameLayout>
+5 -2
View File
@@ -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"/>
<!-- android:background="?android:attr/selectableItemBackgroundBorderless"-->
<ImageView
android:id="@+id/btnTakePhoto"
@@ -106,7 +108,8 @@
android:src="@drawable/ic_take_photo"
app:layout_constraintBottom_toBottomOf="@id/flPreview"
app:layout_constraintEnd_toEndOf="@id/btnCollectedFood"
tools:ignore="ContentDescription" />
tools:ignore="ContentDescription"
android:foreground="@drawable/ripple_effect"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodList"
@@ -1,5 +1,5 @@
<?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@@ -8,11 +8,13 @@
android:orientation="horizontal">
<LinearLayout
android:id="@+id/layoutFood"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="25dp"
android:layout_marginTop="25dp"
android:layout_marginStart="80dp"
android:layout_weight="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:orientation="vertical">
<TextView
@@ -43,8 +45,20 @@
android:layout_height="80dp"
android:layout_marginEnd="75dp"
android:paddingHorizontal="18dp"
app:layout_constraintTop_toTopOf="@id/layoutFood"
app:layout_constraintBottom_toBottomOf="@id/layoutFood"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_close2"
tools:ignore="ContentDescription"
android:visibility="visible"/>
</LinearLayout>
<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider"
android:layout_width="match_parent"
app:layout_constraintTop_toBottomOf="@id/layoutFood"
android:layout_height="1dp"
android:layout_marginHorizontal="80dp"
android:layout_marginTop="25dp"
app:dividerColor="#FFC4CFDA"/>
</androidx.constraintlayout.widget.ConstraintLayout>