288 lines
11 KiB
Kotlin
288 lines
11 KiB
Kotlin
package com.sw.inbound.objbox
|
|
|
|
import android.content.Context
|
|
import android.graphics.Bitmap
|
|
import android.net.Uri
|
|
import android.renderscript.Element.DataType
|
|
import androidx.core.graphics.scale
|
|
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 com.sw.inbound.utils.ext.toJsonString
|
|
import io.objectbox.Box
|
|
import io.objectbox.kotlin.boxFor
|
|
import io.objectbox.query.IdWithScore
|
|
import io.objectbox.query.Query
|
|
import org.pytorch.IValue
|
|
import org.pytorch.Module
|
|
import org.pytorch.torchvision.TensorImageUtils
|
|
import timber.log.Timber
|
|
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
|
|
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 BAG_RATE = 0.05
|
|
|
|
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()) {
|
|
initDefFoodData(context)
|
|
}
|
|
}.start()
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
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()
|
|
} finally {
|
|
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
|
|
rgb565Bitmap.recycle()
|
|
}
|
|
//System.gc()
|
|
//System.runFinalization()
|
|
}
|
|
return null
|
|
}
|
|
|
|
// 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 floatArray = bitmap2FloatArray(bitmap)
|
|
// return queryFood(floatArray, queryCount)
|
|
// }
|
|
|
|
// /**
|
|
// * 返回识别物品IdNameScore对象列表
|
|
// */
|
|
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
|
|
// val floatArray = bitmap2FloatArray(bitmap)
|
|
// return queryFoodNameScore(floatArray, queryCount)
|
|
// }
|
|
|
|
fun queryFoodNameScore(floatArray: FloatArray?, queryCount: Int = 15): 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()
|
|
}
|
|
val nameScoreList = mutableListOf<IdNameScore>()
|
|
idScoreList.forEach {
|
|
nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score))
|
|
}
|
|
Timber.tag("FoodModule").d("queryFood数据:${nameScoreList.toJsonString()}")
|
|
return nameScoreList
|
|
}
|
|
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
|
|
val floatArray = bitmap2FloatArray(bitmap, false)
|
|
if (floatArray == null) return emptyList()
|
|
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)
|
|
})
|
|
Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
|
|
return sortedScoreList
|
|
}
|
|
fun queryFood(floatArray: FloatArray, queryCount: Int = 15): 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
|
|
)
|
|
|
|
fun initDefFoodData(context: Context, action:()-> Unit={}) {
|
|
val count = box.all.count { it.foodIdx == 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")
|
|
|
|
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()
|
|
ObjectBox.boxStore.runInTx {
|
|
box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
|
|
}
|
|
}
|
|
action()
|
|
}
|
|
|
|
|
|
/**
|
|
* ,此方法的主要目的是:从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
|
|
|
|
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()
|
|
Timber.tag("FoodModule").d("文件拷贝失败 fileName=$fileName, error=${e.message}")
|
|
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
|
|
if (cacheFile.exists()) {
|
|
cacheFile.delete()
|
|
}
|
|
} finally {
|
|
outputStream?.close()
|
|
inputStream?.close()
|
|
}
|
|
return null
|
|
}
|
|
|
|
} |