新增菜品采集

This commit is contained in:
马增飞
2025-10-18 15:06:07 +08:00
parent a27cc5f7ee
commit ed6c5e83be
18 changed files with 439 additions and 10 deletions
+1 -8
View File
@@ -57,14 +57,7 @@ android {
} }
dependencies { dependencies {
// implementation( implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
// fileTree(
// mapOf(
// "dir" to "libs",
// "include" to listOf("*.aar", "*.jar")
// )
// )
// )
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material) implementation(libs.material)
Binary file not shown.
@@ -8,14 +8,14 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.sw.dualscreen.R import com.sw.dualscreen.R
import com.sw.dualscreen.model.IActivityResult
import com.sw.dualscreen.view.CustomDialog import com.sw.dualscreen.view.CustomDialog
import com.sw.dualscreen.viewmodel.BaseViewModel import com.sw.dualscreen.viewmodel.BaseViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@@ -81,4 +81,20 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
mDialogWaiting?.dismiss() mDialogWaiting?.dismiss()
mDialogWaiting = null mDialogWaiting = null
} }
private var launchPermissionCallback: IActivityResult.RequestPermissionCallback? = null
fun requestSinglePermissionResult(
permission: String,
permissionCallback: IActivityResult.RequestPermissionCallback?
) {
this.launchPermissionCallback = permissionCallback
permissionLauncher.launch(permission)
}
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { result: Boolean ->
launchPermissionCallback?.callback(result)
}
} }
@@ -0,0 +1,185 @@
package com.sw.dualscreen.activity
import android.annotation.SuppressLint
import android.net.Uri
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.recyclerview.widget.GridLayoutManager
import com.google.common.util.concurrent.ListenableFuture
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.FoodCollectionAdapter
import com.sw.dualscreen.databinding.ActivityFoodCollectionBinding
import com.sw.dualscreen.objbox.Food
import com.sw.dualscreen.objbox.FoodCollectionBean
import com.sw.dualscreen.objbox.FoodModule
import com.sw.dualscreen.objbox.ObjectBox
import com.sw.dualscreen.presentation.SecondaryScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.utils.StorageUtils
import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
import timber.log.Timber
import java.io.File
import java.util.concurrent.Executors
class FoodCollectionActivity : BaseActivity<ActivityFoodCollectionBinding>() {
private val TAG = "FoodCollectionActivity"
private var imageCapture: ImageCapture? = null
private var isAnalyzing = true // 控制是否进行图像分析
private var imageAnalysis: ImageAnalysis? = null
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
private var presentation: SecondaryScreenPresentation? = null
private val executor = Executors.newSingleThreadExecutor()
private val viewModel by viewModels<UserViewModel>()
private var selectedFoodName: String? = ""
private var bottomSheetDialog: CustomBottomSheetDialog? = null
private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
private var module: Module? = null
private var box: Box<Food>? = null
private val list: MutableList<FoodCollectionBean> = mutableListOf()
private val adapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(list).apply {
setOnItemClickListener { _, _, position ->
if (list[position].isShowCamera) {
takePhoto()
}
}
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
list.removeAt(position)
adapter.notifyItemRemoved(position)
adapter.notifyItemRangeChanged(position, list.size)
}
}
}
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityFoodCollectionBinding {
return ActivityFoodCollectionBinding.inflate(layoutInflater)
}
override fun initialize() {
binding.ivBack.setOnClickListener { finish() }
list.add(FoodCollectionBean(isShowCamera = true))
binding.rvFoodCollection.let {
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
it.adapter = adapter
}
binding.btnFoodSearch.setOnClickListener {
bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) {
selectedFoodName = it.foodName
binding.tvSelectedFood.text = selectedFoodName
}
bottomSheetDialog!!.show(supportFragmentManager, "CustomBottomSheetDialog")
}
binding.btnSave.setOnClickListener {
if (selectedFoodName.isNullOrBlank()) {
Toast.makeText(this, "请选择菜品名称", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (list.size <= 1) {
Toast.makeText(this, "请拍摄菜品照片", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
vectorThread()
}
}
private fun takePhoto() {
StorageUtils.requestCameraPermission(this) {
val outputFile = File(this.filesDir, "IMG_${System.currentTimeMillis()}.jpg")
outputFileUri =
FileProvider.getUriForFile(this, "${this.packageName}.fileprovider", outputFile)
takePictureLauncher.launch(outputFileUri!!)
}
}
private var outputFileUri: Uri? = null
@SuppressLint("NotifyDataSetChanged")
private val takePictureLauncher =
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
//Loading.show(this)
if (list.size < 6) {
list.add(list.size - 1, FoodCollectionBean(imageUri = outputFileUri))
} else {
list[list.size - 1] = FoodCollectionBean(imageUri = outputFileUri)
}
adapter.notifyDataSetChanged()
}
}
private fun vectorThread() {
//Loading.show(this)
showWaitingDialog("加载中……")
Thread {
list.forEachIndexed { index, it ->
image2VectorTask(it.imageUri!!, index)
}
runOnUiThread {
window.decorView.postDelayed({
//Loading.dismiss()
hideWaitingDialog()
}, 1000)
}
}.start()
}
private fun image2VectorTask(imageUri: Uri, position: Int) {
if (module == null) {
module =
Module.load(FoodModule.copyAssetToCache(this, "best_embedding_model_mobile.pt"))
}
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
ImageUtil.uriToBitmap(this, imageUri)?.let { bitmap ->
//val bitmap2 = BitmapCropper.cropCenter(bitmap!!, 500, 500)
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!!.forward(IValue.from(inputTensor)).toTensor()
val imageVector = outputTensor.dataAsFloatArray
box?.put(Food(name = selectedFoodName, foodIdx = 0, foodVector = imageVector))
list[position].let {
it.imageVector = imageVector
it.isFinish = true
}
runOnUiThread {
adapter.notifyItemChanged(position)
}
}
}
}
@@ -0,0 +1,42 @@
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.ListItemFoodCollectionBinding
import com.sw.dualscreen.objbox.FoodCollectionBean
class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
BaseQuickAdapter<FoodCollectionBean, FoodCollectionAdapter.VH>(list) {
inner class VH(var binding: ListItemFoodCollectionBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemFoodCollectionBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodCollectionBean?) {
val binding = holder.binding
item?.let {
binding.ivFinish.visibility = if (it.isFinish) View.VISIBLE else View.GONE
binding.ivDelete.visibility = if (it.isShowCamera) View.GONE else View.VISIBLE
binding.imageView.run {
if (it.isShowCamera) {
scaleType = ImageView.ScaleType.CENTER
setImageResource(R.drawable.ic_camera256)
} else {
scaleType = ImageView.ScaleType.FIT_CENTER
setImageURI(it.imageUri)
}
}
}
}
}
@@ -0,0 +1,24 @@
package com.sw.dualscreen.model;
import androidx.activity.result.ActivityResult;
import java.util.Map;
public interface IActivityResult {
interface LaunchActivityCallback {
void callback(ActivityResult result);
}
interface LaunchTackPictureCallback {
void callback(boolean result);
}
interface RequestPermissionCallback {
void callback(boolean result);
}
interface RequestMultiplePermissionsCallback {
void callback(Map<String,Boolean> result);
}
}
@@ -1,5 +1,8 @@
package com.sw.dualscreen.objbox package com.sw.dualscreen.objbox
import android.graphics.Bitmap
import android.net.Uri
data class FoodClassInfo( data class FoodClassInfo(
var class_names: List<String>, var class_names: List<String>,
var class_to_idx: Map<String, Int>, var class_to_idx: Map<String, Int>,
@@ -12,3 +15,11 @@ data class FoodClassInfo(
// var foodIdx: Int, // var foodIdx: Int,
// var score: Double // var score: Double
//) //)
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,34 @@
package com.sw.dualscreen.utils
import android.Manifest
import com.sw.dualscreen.activity.BaseActivity
object StorageUtils {
fun requestPermission(activity: BaseActivity<*>, callback: () -> Unit) {
activity.requestSinglePermissionResult(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
callback()
}
// activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
//// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//// if (Environment.isExternalStorageManager()) {
//// callback()
//// } else {
//// val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
////// activity.startActivityForResult(intent, MANAGE_REQUEST_CODE)
//// activity.launchActivityResult(intent) {
//// callback()
//// }
//// }
//// } else {
//
//// }
// }
}
fun requestCameraPermission(activity: BaseActivity<*>, callback: () -> Unit) {
activity.requestSinglePermissionResult(Manifest.permission.CAMERA) {
callback()
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ffffff"/>
<stroke android:color="#cdcdcd" android:width="1dp"/>
<corners android:radius="5dp"/>
</shape>
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<ImageView
android:id="@+id/ivBack"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="10dp"
android:src="@drawable/ic_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="菜品采集"
android:textColor="@color/black"
android:textSize="18sp" />
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFoodCollection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="3dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
tools:itemCount="6"
tools:listitem="@layout/list_item_food_collection" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tvSelectedFood"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="6dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:hint="选择菜品名称"
android:background="@drawable/bg_gray"
android:textColor="@color/black"
android:textSize="18sp" />
<Button
android:id="@+id/btnFoodSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="菜品检索"
android:textColor="#ffffff"
android:textSize="16sp" />
</LinearLayout>
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:id="@+id/btnSave"
android:layout_width="260dp"
android:layout_height="60dp"
android:text="保存"
android:layout_marginVertical="20dp"
android:layout_gravity="center"
android:textColor="#ffffff"
android:textSize="20sp" />
</LinearLayout>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="110dp"
android:background="@drawable/bg_gray"
android:layout_margin="3dp"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="center"
tools:src="@drawable/ic_camera256"/>
<ImageView
android:id="@+id/ivDelete"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end|top"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:src="@drawable/ic_delete_red"/>
<ImageView
android:id="@+id/ivFinish"
android:layout_width="30dp"
android:layout_height="30dp"
android:padding="5dp"
android:layout_gravity="end|bottom"
android:src="@drawable/ic_finish"/>
</FrameLayout>