diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4f149a3..c9cfa0b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,14 +57,7 @@ android { } dependencies { -// implementation( -// fileTree( -// mapOf( -// "dir" to "libs", -// "include" to listOf("*.aar", "*.jar") -// ) -// ) -// ) + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar")))) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) diff --git a/app/libs/BaseRecyclerViewAdapterHelper4-4.1.2.aar b/app/libs/BaseRecyclerViewAdapterHelper4-4.1.2.aar new file mode 100644 index 0000000..e493c3c Binary files /dev/null and b/app/libs/BaseRecyclerViewAdapterHelper4-4.1.2.aar differ diff --git a/app/src/main/java/com/sw/dualscreen/activity/BaseActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/BaseActivity.kt index 951b75e..a9773ad 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/BaseActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/BaseActivity.kt @@ -8,14 +8,14 @@ import android.view.View import android.view.WindowManager import android.widget.TextView import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding import com.sw.dualscreen.R +import com.sw.dualscreen.model.IActivityResult import com.sw.dualscreen.view.CustomDialog import com.sw.dualscreen.viewmodel.BaseViewModel -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import timber.log.Timber @@ -81,4 +81,20 @@ abstract class BaseActivity : AppCompatActivity() { mDialogWaiting?.dismiss() 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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/activity/FoodCollectionActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/FoodCollectionActivity.kt new file mode 100644 index 0000000..b48ec71 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/activity/FoodCollectionActivity.kt @@ -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() { + + private val TAG = "FoodCollectionActivity" + private var imageCapture: ImageCapture? = null + private var isAnalyzing = true // 控制是否进行图像分析 + private var imageAnalysis: ImageAnalysis? = null + private var cameraProviderFuture: ListenableFuture? = null + private var presentation: SecondaryScreenPresentation? = null + private val executor = Executors.newSingleThreadExecutor() + + private val viewModel by viewModels() + 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? = null + private val list: MutableList = 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) + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/adapter/FoodCollectionAdapter.kt b/app/src/main/java/com/sw/dualscreen/adapter/FoodCollectionAdapter.kt new file mode 100644 index 0000000..affe7c5 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/adapter/FoodCollectionAdapter.kt @@ -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) : + BaseQuickAdapter(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) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/model/IActivityResult.java b/app/src/main/java/com/sw/dualscreen/model/IActivityResult.java new file mode 100644 index 0000000..7ec4480 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/model/IActivityResult.java @@ -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 result); + } +} diff --git a/app/src/main/java/com/sw/dualscreen/objbox/DataBean.kt b/app/src/main/java/com/sw/dualscreen/objbox/DataBean.kt index 70b91d1..f363d08 100644 --- a/app/src/main/java/com/sw/dualscreen/objbox/DataBean.kt +++ b/app/src/main/java/com/sw/dualscreen/objbox/DataBean.kt @@ -1,5 +1,8 @@ package com.sw.dualscreen.objbox +import android.graphics.Bitmap +import android.net.Uri + data class FoodClassInfo( var class_names: List, var class_to_idx: Map, @@ -12,3 +15,11 @@ data class FoodClassInfo( // var foodIdx: Int, // 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 +) diff --git a/app/src/main/java/com/sw/dualscreen/utils/StorageUtils.kt b/app/src/main/java/com/sw/dualscreen/utils/StorageUtils.kt new file mode 100644 index 0000000..484247e --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/utils/StorageUtils.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/ic_back.png b/app/src/main/res/drawable-xxhdpi/ic_back.png new file mode 100644 index 0000000..d7692eb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_back.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_camera128.png b/app/src/main/res/drawable-xxhdpi/ic_camera128.png new file mode 100644 index 0000000..7df367e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_camera128.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_camera256.png b/app/src/main/res/drawable-xxhdpi/ic_camera256.png new file mode 100644 index 0000000..cc72a98 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_camera256.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_camera64.png b/app/src/main/res/drawable-xxhdpi/ic_camera64.png new file mode 100644 index 0000000..68097a5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_camera64.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete.png b/app/src/main/res/drawable-xxhdpi/ic_delete.png new file mode 100644 index 0000000..18207b8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delete.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_red.png b/app/src/main/res/drawable-xxhdpi/ic_delete_red.png new file mode 100644 index 0000000..ff00640 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delete_red.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_finish.png b/app/src/main/res/drawable-xxhdpi/ic_finish.png new file mode 100644 index 0000000..7f433bf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_finish.png differ diff --git a/app/src/main/res/drawable/bg_gray.xml b/app/src/main/res/drawable/bg_gray.xml new file mode 100644 index 0000000..cdabf71 --- /dev/null +++ b/app/src/main/res/drawable/bg_gray.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_food_collection.xml b/app/src/main/res/layout/activity_food_collection.xml new file mode 100644 index 0000000..eadbb47 --- /dev/null +++ b/app/src/main/res/layout/activity_food_collection.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + +