feat: implement camera feature for plant disease detection
This commit is contained in:
parent
6d9aa910b6
commit
69b7cbac0f
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.camera.view.LifecycleCameraController
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
|
interface CameraRepository {
|
||||||
|
fun takePicture(
|
||||||
|
controller: LifecycleCameraController,
|
||||||
|
executor: Executor,
|
||||||
|
onPictureTaken: (Bitmap) -> Unit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.camera.core.ImageCapture.OnImageCapturedCallback
|
||||||
|
import androidx.camera.core.ImageCaptureException
|
||||||
|
import androidx.camera.core.ImageProxy
|
||||||
|
import androidx.camera.view.LifecycleCameraController
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
|
class CameraRepositoryImpl : CameraRepository {
|
||||||
|
|
||||||
|
override fun takePicture(
|
||||||
|
controller: LifecycleCameraController,
|
||||||
|
executor: Executor,
|
||||||
|
onPictureTaken: (Bitmap) -> Unit
|
||||||
|
) {
|
||||||
|
Log.d("CameraRepository", "Starting takePicture")
|
||||||
|
|
||||||
|
controller.takePicture(
|
||||||
|
executor,
|
||||||
|
object : OnImageCapturedCallback() {
|
||||||
|
override fun onCaptureSuccess(image: ImageProxy) {
|
||||||
|
Log.d("CameraRepository", "Capture success")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val rotationDegrees = image.imageInfo.rotationDegrees
|
||||||
|
Log.d("CameraRepository", "Rotation: $rotationDegrees")
|
||||||
|
|
||||||
|
val matrix = Matrix().apply {
|
||||||
|
postRotate(rotationDegrees.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
val originalBitmap = image.toBitmap()
|
||||||
|
Log.d("CameraRepository", "Original bitmap size: ${originalBitmap.width} x ${originalBitmap.height}")
|
||||||
|
|
||||||
|
val rotatedBitmap = Bitmap.createBitmap(
|
||||||
|
originalBitmap,
|
||||||
|
0, 0,
|
||||||
|
image.width,
|
||||||
|
image.height,
|
||||||
|
matrix,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.d("CameraRepository", "Rotated bitmap size: ${rotatedBitmap.width} x ${rotatedBitmap.height}")
|
||||||
|
|
||||||
|
onPictureTaken(rotatedBitmap)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("CameraRepository", "Error while processing image: ${e.message}", e)
|
||||||
|
} finally {
|
||||||
|
image.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(exception: ImageCaptureException) {
|
||||||
|
Log.e("CameraRepository", "Couldn't take photo: ${exception.message}", exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.di
|
||||||
|
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.CameraRepository
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.CameraRepositoryImpl
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.CameraViewModel
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val cameraModule = module {
|
||||||
|
single<CameraRepository> { CameraRepositoryImpl() }
|
||||||
|
viewModel { CameraViewModel(get(), get())}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera
|
||||||
|
|
||||||
|
import androidx.camera.view.CameraController
|
||||||
|
import androidx.camera.view.LifecycleCameraController
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.R
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera.component.CustomCameraShutter
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CameraScreen(
|
||||||
|
navController: NavHostController,
|
||||||
|
viewModel: CameraViewModel
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
// val bitmaps by viewModel.bitmaps.collectAsState()
|
||||||
|
val bitmap by viewModel.bitmap.collectAsState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
// val scaffoldState = rememberBottomSheetScaffoldState()
|
||||||
|
|
||||||
|
val controller = remember {
|
||||||
|
LifecycleCameraController(context).apply {
|
||||||
|
setEnabledUseCases(
|
||||||
|
CameraController.IMAGE_CAPTURE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Deteksi Penyakit Tanaman", style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(top = 80.dp)
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f),
|
||||||
|
factory = {
|
||||||
|
PreviewView(it).apply {
|
||||||
|
this.controller = controller
|
||||||
|
controller.bindToLifecycle(lifecycleOwner)
|
||||||
|
this.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
CustomCameraShutter(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 16.dp, start = 20.dp, end = 20.dp),
|
||||||
|
onShutterClick = {
|
||||||
|
if (!uiState.isLoading)
|
||||||
|
viewModel.takePicture(
|
||||||
|
controller,
|
||||||
|
ContextCompat.getMainExecutor(context)
|
||||||
|
) {
|
||||||
|
navController.navigate("detail_screen")
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(24.dp)
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
painter = painterResource(id = R.drawable.scan_frame),
|
||||||
|
contentDescription = "scan frame"
|
||||||
|
)
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera
|
||||||
|
|
||||||
|
data class CameraUIState(
|
||||||
|
val isLoading: Boolean = false
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.camera
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.camera.view.LifecycleCameraController
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.AppConstant
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.cropToSquare
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.CameraRepository
|
||||||
|
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail.PlantDiagnosisViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
|
private const val TAG = "CameraViewModel"
|
||||||
|
|
||||||
|
class CameraViewModel(
|
||||||
|
private val cameraRepository: CameraRepository,
|
||||||
|
private val diagnosisViewModel: PlantDiagnosisViewModel
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(CameraUIState())
|
||||||
|
val uiState: StateFlow<CameraUIState> = _uiState
|
||||||
|
|
||||||
|
private val _bitmap = MutableStateFlow<Bitmap?>(null)
|
||||||
|
val bitmap: StateFlow<Bitmap?> = _bitmap
|
||||||
|
|
||||||
|
val prompt = AppConstant().prompt
|
||||||
|
|
||||||
|
fun takePicture(
|
||||||
|
controller: LifecycleCameraController,
|
||||||
|
executor: Executor,
|
||||||
|
onFinish: () -> Unit
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "takePicture() called")
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||||
|
|
||||||
|
cameraRepository.takePicture(controller, executor) { bitmap ->
|
||||||
|
Log.d(TAG, "Picture taken, updating state")
|
||||||
|
val croppedImage = cropToSquare(bitmap)
|
||||||
|
_bitmap.value = croppedImage
|
||||||
|
// croppedImage.let {
|
||||||
|
// diagnosisViewModel.analyzePlant(bitmap = it, prompt = prompt)
|
||||||
|
// Log.d(TAG, "analyzePlant() called")
|
||||||
|
// }
|
||||||
|
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||||
|
|
||||||
|
onFinish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user