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