Display a message when no document is detected in captured image
This commit is contained in:
@@ -56,7 +56,7 @@ class MainViewModel(
|
||||
private val _pageIds = MutableStateFlow<List<String>>(imageRepository.imageIds())
|
||||
val pageIds: StateFlow<List<String>> = _pageIds
|
||||
|
||||
private val _captureState = MutableStateFlow<CaptureState>(CaptureState())
|
||||
private val _captureState = MutableStateFlow<CaptureState>(CaptureState.Idle)
|
||||
val captureState: StateFlow<CaptureState> = _captureState
|
||||
|
||||
init {
|
||||
@@ -78,28 +78,36 @@ class MainViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
data class CaptureState(val frozenImage: Bitmap? = null, val processedImage: Bitmap? = null) {
|
||||
fun isIdle(): Boolean { return frozenImage == null }
|
||||
fun isProcessed(): Boolean { return processedImage != null }
|
||||
fun withProcessed(processedImage: Bitmap? = null): CaptureState {
|
||||
return if (processedImage == null) {
|
||||
CaptureState()
|
||||
} else {
|
||||
CaptureState(frozenImage, processedImage)
|
||||
}
|
||||
sealed class CaptureState {
|
||||
open val frozenImage: Bitmap? = null
|
||||
|
||||
object Idle : CaptureState()
|
||||
data class Capturing(override val frozenImage: Bitmap) : CaptureState()
|
||||
data class CaptureError(override val frozenImage: Bitmap) : CaptureState()
|
||||
data class CapturePreview(
|
||||
override val frozenImage: Bitmap,
|
||||
val processed: Bitmap
|
||||
) : CaptureState()
|
||||
}
|
||||
|
||||
|
||||
fun onCapturePressed(frozenImage: Bitmap) {
|
||||
_captureState.value = CaptureState.Capturing(frozenImage)
|
||||
}
|
||||
|
||||
private fun onCaptureProcessed(captured: Bitmap?) {
|
||||
val current = _captureState.value
|
||||
_captureState.value = when {
|
||||
current is CaptureState.Capturing && captured != null ->
|
||||
CaptureState.CapturePreview(current.frozenImage, captured)
|
||||
current is CaptureState.Capturing ->
|
||||
CaptureState.CaptureError(current.frozenImage)
|
||||
else -> CaptureState.Idle
|
||||
}
|
||||
}
|
||||
|
||||
fun onCapturePressed(frozenImage: Bitmap?) {
|
||||
_captureState.value = CaptureState(frozenImage)
|
||||
}
|
||||
|
||||
fun onCaptureProcessed(captured: Bitmap?) {
|
||||
_captureState.value = _captureState.value.withProcessed(captured)
|
||||
}
|
||||
|
||||
fun liveAnalysis(imageProxy: ImageProxy) {
|
||||
if (!_captureState.value.isIdle()) {
|
||||
if (_captureState.value !is CaptureState.Idle) {
|
||||
imageProxy.close()
|
||||
return
|
||||
}
|
||||
@@ -131,7 +139,7 @@ class MainViewModel(
|
||||
|
||||
private suspend fun processCapturedImage(imageProxy: ImageProxy): Bitmap? = withContext(Dispatchers.IO) {
|
||||
var corrected: Bitmap? = null
|
||||
var bitmap = imageProxy.toBitmap()
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
val segmentation = imageSegmentationService.runSegmentationAndReturn(bitmap, 0)
|
||||
if (segmentation != null) {
|
||||
val mask = segmentation.segmentation.toBinaryMask()
|
||||
@@ -145,16 +153,19 @@ class MainViewModel(
|
||||
}
|
||||
|
||||
fun addProcessedImage(quality: Int = 75) {
|
||||
val bitmap = _captureState.value.processedImage
|
||||
_captureState.value = CaptureState()
|
||||
if (bitmap == null) {
|
||||
return
|
||||
val current = _captureState.value
|
||||
if (current is CaptureState.CapturePreview) {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
current.processed.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||
val jpegBytes = outputStream.toByteArray()
|
||||
imageRepository.add(jpegBytes)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
}
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||
val jpegBytes = outputStream.toByteArray()
|
||||
imageRepository.add(jpegBytes)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
_captureState.value = CaptureState.Idle
|
||||
}
|
||||
|
||||
fun afterCaptureError() {
|
||||
_captureState.value = CaptureState.Idle
|
||||
}
|
||||
|
||||
fun deletePage(id: String) {
|
||||
|
||||
@@ -42,6 +42,7 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -71,6 +72,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.delay
|
||||
import org.mydomain.myscan.LiveAnalysisState
|
||||
@@ -105,13 +107,23 @@ fun CameraScreen(
|
||||
}
|
||||
|
||||
val captureState by viewModel.captureState.collectAsStateWithLifecycle()
|
||||
if (captureState.isProcessed()) {
|
||||
if (captureState is CaptureState.CapturePreview) {
|
||||
LaunchedEffect(captureState) {
|
||||
delay(CAPTURED_IMAGE_DISPLAY_DURATION)
|
||||
viewModel.addProcessedImage()
|
||||
}
|
||||
}
|
||||
|
||||
val showDetectionError = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(captureState) {
|
||||
if (captureState is CaptureState.CaptureError) {
|
||||
showDetectionError.value = true
|
||||
delay(1000)
|
||||
showDetectionError.value = false
|
||||
viewModel.afterCaptureError()
|
||||
}
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
LaunchedEffect(pageIds.size) {
|
||||
if (pageIds.isNotEmpty()) {
|
||||
@@ -138,14 +150,17 @@ fun CameraScreen(
|
||||
},
|
||||
cameraUiState = CameraUiState(pageIds.size, liveAnalysisState, captureState),
|
||||
onCapture = {
|
||||
Log.i("MyScan", "Pressed <Capture>")
|
||||
viewModel.onCapturePressed(previewView?.bitmap)
|
||||
captureController.takePicture(
|
||||
onImageCaptured = { imageProxy -> viewModel.onImageCaptured(imageProxy) }
|
||||
)
|
||||
previewView?.bitmap?.let {
|
||||
Log.i("MyScan", "Pressed <Capture>")
|
||||
viewModel.onCapturePressed(it)
|
||||
captureController.takePicture(
|
||||
onImageCaptured = { imageProxy -> viewModel.onImageCaptured(imageProxy) }
|
||||
)
|
||||
}
|
||||
},
|
||||
onFinalizePressed = onFinalizePressed,
|
||||
thumbnailCoords = thumbnailCoords,
|
||||
showDetectionError = showDetectionError.value
|
||||
)
|
||||
}
|
||||
|
||||
@@ -157,6 +172,7 @@ private fun CameraScreenScaffold(
|
||||
onCapture: () -> Unit,
|
||||
onFinalizePressed: () -> Unit,
|
||||
thumbnailCoords: MutableState<Offset>,
|
||||
showDetectionError: Boolean,
|
||||
) {
|
||||
Box {
|
||||
Scaffold(
|
||||
@@ -173,7 +189,7 @@ private fun CameraScreenScaffold(
|
||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||
.fillMaxSize()
|
||||
) {
|
||||
CameraPreviewWithOverlay(cameraPreview, cameraUiState)
|
||||
CameraPreviewWithOverlay(cameraPreview, cameraUiState, showDetectionError)
|
||||
MessageBox(cameraUiState.liveAnalysisState.inferenceTime)
|
||||
CaptureButton(
|
||||
onClick = onCapture,
|
||||
@@ -183,8 +199,8 @@ private fun CameraScreenScaffold(
|
||||
)
|
||||
}
|
||||
}
|
||||
cameraUiState.captureState.processedImage?.let {
|
||||
image -> CapturedImage(image.asImageBitmap(), thumbnailCoords)
|
||||
if (cameraUiState.captureState is CaptureState.CapturePreview) {
|
||||
CapturedImage(cameraUiState.captureState.processed.asImageBitmap(), thumbnailCoords)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,14 +289,16 @@ fun CaptureButton(onClick: () -> Unit, modifier: Modifier) {
|
||||
@Composable
|
||||
private fun CameraPreviewWithOverlay(
|
||||
cameraPreview: @Composable () -> Unit,
|
||||
cameraUiState: CameraUiState
|
||||
cameraUiState: CameraUiState,
|
||||
showDetectionError: Boolean
|
||||
) {
|
||||
val captureState = cameraUiState.captureState
|
||||
val width = LocalConfiguration.current.screenWidthDp
|
||||
val height = width / 3 * 4
|
||||
|
||||
var showShutter by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(cameraUiState.captureState.frozenImage) {
|
||||
if (cameraUiState.captureState.frozenImage != null) {
|
||||
LaunchedEffect(captureState.frozenImage) {
|
||||
if (captureState.frozenImage != null) {
|
||||
showShutter = true
|
||||
delay(200)
|
||||
showShutter = false
|
||||
@@ -294,7 +312,7 @@ private fun CameraPreviewWithOverlay(
|
||||
) {
|
||||
cameraPreview()
|
||||
AnalysisOverlay(cameraUiState.liveAnalysisState)
|
||||
cameraUiState.captureState.frozenImage?.let {
|
||||
captureState.frozenImage?.let {
|
||||
Image(
|
||||
bitmap = it.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
@@ -308,6 +326,21 @@ private fun CameraPreviewWithOverlay(
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
)
|
||||
}
|
||||
if (showDetectionError) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.background(Color.Black.copy(alpha = 0.7f), shape = RoundedCornerShape(8.dp))
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "No document detected",
|
||||
color = Color.White,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,13 +392,15 @@ fun CameraScreenFooter(
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun CameraScreenPreview() {
|
||||
ScreenPreview(CaptureState())
|
||||
ScreenPreview(CaptureState.Idle)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, showSystemUi = true)
|
||||
@Composable
|
||||
fun CameraScreenPreviewWithProcessedImage() {
|
||||
ScreenPreview(CaptureState(processedImage = debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")))
|
||||
ScreenPreview(CaptureState.CapturePreview(
|
||||
debugImage("uncropped/img01.jpg"),
|
||||
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -403,6 +438,7 @@ private fun ScreenPreview(captureState: CaptureState) {
|
||||
onCapture = {},
|
||||
onFinalizePressed = {},
|
||||
thumbnailCoords = thumbnailCoords,
|
||||
showDetectionError = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user