出入库功能改版

This commit is contained in:
2025-10-28 19:22:31 +08:00
parent f4e7cab2d0
commit b22f42d92a
121 changed files with 8271 additions and 3141 deletions
@@ -0,0 +1,18 @@
package com.sw.inbound.objbox
import android.graphics.Bitmap
import android.net.Uri
data class FoodClassInfo(
var class_names: List<String>,
var class_to_idx: Map<String, Int>,
var idx_to_class: Map<String, String>
)
data class FoodCollectionBean(
var imageUri: Uri? = null,
var bitmap: Bitmap? = null,
var imageVector: FloatArray? = null,
var isShowCamera: Boolean = false,
var isFinish:Boolean = false
)
@@ -0,0 +1,37 @@
package com.sw.inbound.objbox
import io.objectbox.annotation.Entity
import io.objectbox.annotation.HnswIndex
import io.objectbox.annotation.Id
import io.objectbox.annotation.VectorDistanceType
@Entity
data class Food(
@Id var id: Long = 0,
var name: String? = null,
var foodIdx: Int = 0,
@HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT)
var foodVector: FloatArray? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Food
if (id != other.id) return false
if (foodIdx != other.foodIdx) return false
if (name != other.name) return false
if (!foodVector.contentEquals(other.foodVector)) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + foodIdx
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (foodVector?.contentHashCode() ?: 0)
return result
}
}
@@ -0,0 +1,172 @@
package com.sw.inbound.objbox
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sw.inbound.MyApp
import com.sw.inbound.utils.AssetsTool
import com.sw.inbound.utils.ImageUtil
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
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 lateinit var box: Box<Food>
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)
fun init(context: Context) {
Thread{
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
box = ObjectBox.boxStore.boxFor(Food::class)
// if (box.all.isNotEmpty()) {
// box.removeAll()
// }
if (box.all.isEmpty()) {
initFoodData(context)
}
}.start()
}
fun uri2FloatArray(uri: Uri): FloatArray? {
return MyApp.instance?.let { context ->
ImageUtil.uriToBitmap(context, uri)?.let {
bitmap2FloatArray(it)
}
}
}
fun bitmap2FloatArray(bitmap: Bitmap): FloatArray {
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
)
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
return outputTensor.dataAsFloatArray
}
fun queryFood(uri: Uri, queryCount:Int = 15): List<String>? {
return uri2FloatArray(uri)?.let {
queryFood(it, queryCount)
}
}
fun queryFood(bitmap: Bitmap, queryCount:Int = 15): List<String> {
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
)
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
val floatArray = outputTensor.dataAsFloatArray
return queryFood(floatArray,queryCount)
}
fun queryFood(floatArray: FloatArray, queryCount:Int = 15): List<String> {
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}" }
val map = mutableMapOf<String, Int>()
query.findIdsWithScores().forEach {
val food = box.get(it.id)
//FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score)
food.name?.let { key ->
val count = map[key] ?: 0
map.put(key, count + 1)
}
}
val list = map.entries.sortedByDescending { it.value }.map { it.key }
return list
}
private fun initFoodData(context: Context) {
if (box.all.isNotEmpty()) {
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")
embeddingsList =
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
labelsList = Gson().fromJson(labelsJson, IntArray::class.java)
classInfo =
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
val foodMap = classInfo.idx_to_class
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
box.put(Food(name = foodName, foodVector = array, foodIdx = index))
}
}
/**
* ,此方法的主要目的是:从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
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2024 ObjectBox Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sw.inbound.objbox
import android.content.Context
import android.util.Log
import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder
import io.objectbox.android.Admin
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.sync.Sync
import java.io.File
import java.util.zip.GZIPOutputStream
import kotlin.io.copyTo
import kotlin.io.inputStream
import kotlin.io.outputStream
import kotlin.io.use
import kotlin.jvm.java
import kotlin.jvm.javaClass
/**
* Singleton to keep BoxStore reference and provide current list of Notes Objects.
* Inserts demo data if no Objects are stored.
*/
object ObjectBox {
// 确保所有设备/版本使用相同的objectbox-models/default.json
// 避免直接修改自动生成的MyObjectBox类
// 跨版本升级时使用boxStore.runInTx执行数据迁移
private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore
private set
/**
* If building the [boxStore] failed, contains the thrown error message.
*/
var dbExceptionMessage: String? = null
private set
fun init(context: Context) {
// On Android make sure to pass a Context when building the Store.
boxStore = try {
MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
} catch (e: DbException) {
if (e.javaClass.equals(DbException::class.java) || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message;
// checked in NoteListActivity to notify user.
dbExceptionMessage = e.toString()
return
} else {
// Failed to build BoxStore due to developer error.
throw e
}
}
// if (BuildConfig.DEBUG) {
var syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Log.d(TAG,"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
Admin(boxStore).start(context.applicationContext)
// }
}
/**
* If the database file is not in use, compresses (GZIP) and copies it to the given [target].
*/
fun copyAndGzipDatabaseFileTo(target: File, context: Context): Boolean {
if (BoxStore.isDatabaseOpen(context, null)) {
// Do not copy if database file is still in use.
// If it would be open, the copy will likely get corrupted
// as BoxStore may currently write data to the file.
Log.e(TAG, "Database file is still in use, can not copy.")
return false
}
// If a name was given when building BoxStore use that instead of the default below.
val dbName = BoxStoreBuilder.DEFAULT_NAME
File(context.filesDir, "objectbox/$dbName/data.mdb").inputStream().use { input ->
target.parentFile?.mkdirs()
GZIPOutputStream(target.outputStream()).use { output ->
input.copyTo(output, DEFAULT_BUFFER_SIZE)
}
}
return true
}
}