Skip to content

Commit 6ccf708

Browse files
committed
Display a message when no document is detected in captured image
1 parent 2dd5f8c commit 6ccf708

File tree

2 files changed

+88
-41
lines changed

2 files changed

+88
-41
lines changed

app/src/main/java/org/mydomain/myscan/MainViewModel.kt

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class MainViewModel(
5656
private val _pageIds = MutableStateFlow<List<String>>(imageRepository.imageIds())
5757
val pageIds: StateFlow<List<String>> = _pageIds
5858

59-
private val _captureState = MutableStateFlow<CaptureState>(CaptureState())
59+
private val _captureState = MutableStateFlow<CaptureState>(CaptureState.Idle)
6060
val captureState: StateFlow<CaptureState> = _captureState
6161

6262
init {
@@ -78,28 +78,36 @@ class MainViewModel(
7878
}
7979
}
8080

81-
data class CaptureState(val frozenImage: Bitmap? = null, val processedImage: Bitmap? = null) {
82-
fun isIdle(): Boolean { return frozenImage == null }
83-
fun isProcessed(): Boolean { return processedImage != null }
84-
fun withProcessed(processedImage: Bitmap? = null): CaptureState {
85-
return if (processedImage == null) {
86-
CaptureState()
87-
} else {
88-
CaptureState(frozenImage, processedImage)
89-
}
90-
}
81+
sealed class CaptureState {
82+
open val frozenImage: Bitmap? = null
83+
84+
object Idle : CaptureState()
85+
data class Capturing(override val frozenImage: Bitmap) : CaptureState()
86+
data class CaptureError(override val frozenImage: Bitmap) : CaptureState()
87+
data class CapturePreview(
88+
override val frozenImage: Bitmap,
89+
val processed: Bitmap
90+
) : CaptureState()
9191
}
9292

93-
fun onCapturePressed(frozenImage: Bitmap?) {
94-
_captureState.value = CaptureState(frozenImage)
93+
94+
fun onCapturePressed(frozenImage: Bitmap) {
95+
_captureState.value = CaptureState.Capturing(frozenImage)
9596
}
9697

97-
fun onCaptureProcessed(captured: Bitmap?) {
98-
_captureState.value = _captureState.value.withProcessed(captured)
98+
private fun onCaptureProcessed(captured: Bitmap?) {
99+
val current = _captureState.value
100+
_captureState.value = when {
101+
current is CaptureState.Capturing && captured != null ->
102+
CaptureState.CapturePreview(current.frozenImage, captured)
103+
current is CaptureState.Capturing ->
104+
CaptureState.CaptureError(current.frozenImage)
105+
else -> CaptureState.Idle
106+
}
99107
}
100108

101109
fun liveAnalysis(imageProxy: ImageProxy) {
102-
if (!_captureState.value.isIdle()) {
110+
if (_captureState.value !is CaptureState.Idle) {
103111
imageProxy.close()
104112
return
105113
}
@@ -131,7 +139,7 @@ class MainViewModel(
131139

132140
private suspend fun processCapturedImage(imageProxy: ImageProxy): Bitmap? = withContext(Dispatchers.IO) {
133141
var corrected: Bitmap? = null
134-
var bitmap = imageProxy.toBitmap()
142+
val bitmap = imageProxy.toBitmap()
135143
val segmentation = imageSegmentationService.runSegmentationAndReturn(bitmap, 0)
136144
if (segmentation != null) {
137145
val mask = segmentation.segmentation.toBinaryMask()
@@ -145,16 +153,19 @@ class MainViewModel(
145153
}
146154

147155
fun addProcessedImage(quality: Int = 75) {
148-
val bitmap = _captureState.value.processedImage
149-
_captureState.value = CaptureState()
150-
if (bitmap == null) {
151-
return
156+
val current = _captureState.value
157+
if (current is CaptureState.CapturePreview) {
158+
val outputStream = ByteArrayOutputStream()
159+
current.processed.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
160+
val jpegBytes = outputStream.toByteArray()
161+
imageRepository.add(jpegBytes)
162+
_pageIds.value = imageRepository.imageIds()
152163
}
153-
val outputStream = ByteArrayOutputStream()
154-
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
155-
val jpegBytes = outputStream.toByteArray()
156-
imageRepository.add(jpegBytes)
157-
_pageIds.value = imageRepository.imageIds()
164+
_captureState.value = CaptureState.Idle
165+
}
166+
167+
fun afterCaptureError() {
168+
_captureState.value = CaptureState.Idle
158169
}
159170

160171
fun deletePage(id: String) {

app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import androidx.compose.foundation.layout.width
4242
import androidx.compose.foundation.lazy.LazyListState
4343
import androidx.compose.foundation.lazy.rememberLazyListState
4444
import androidx.compose.foundation.shape.CircleShape
45+
import androidx.compose.foundation.shape.RoundedCornerShape
4546
import androidx.compose.material3.BottomAppBar
4647
import androidx.compose.material3.Button
4748
import androidx.compose.material3.MaterialTheme
@@ -71,6 +72,7 @@ import androidx.compose.ui.platform.LocalContext
7172
import androidx.compose.ui.platform.LocalDensity
7273
import androidx.compose.ui.tooling.preview.Preview
7374
import androidx.compose.ui.unit.dp
75+
import androidx.compose.ui.unit.sp
7476
import androidx.lifecycle.compose.collectAsStateWithLifecycle
7577
import kotlinx.coroutines.delay
7678
import org.mydomain.myscan.LiveAnalysisState
@@ -105,13 +107,23 @@ fun CameraScreen(
105107
}
106108

107109
val captureState by viewModel.captureState.collectAsStateWithLifecycle()
108-
if (captureState.isProcessed()) {
110+
if (captureState is CaptureState.CapturePreview) {
109111
LaunchedEffect(captureState) {
110112
delay(CAPTURED_IMAGE_DISPLAY_DURATION)
111113
viewModel.addProcessedImage()
112114
}
113115
}
114116

117+
val showDetectionError = remember { mutableStateOf(false) }
118+
LaunchedEffect(captureState) {
119+
if (captureState is CaptureState.CaptureError) {
120+
showDetectionError.value = true
121+
delay(1000)
122+
showDetectionError.value = false
123+
viewModel.afterCaptureError()
124+
}
125+
}
126+
115127
val listState = rememberLazyListState()
116128
LaunchedEffect(pageIds.size) {
117129
if (pageIds.isNotEmpty()) {
@@ -138,14 +150,17 @@ fun CameraScreen(
138150
},
139151
cameraUiState = CameraUiState(pageIds.size, liveAnalysisState, captureState),
140152
onCapture = {
141-
Log.i("MyScan", "Pressed <Capture>")
142-
viewModel.onCapturePressed(previewView?.bitmap)
143-
captureController.takePicture(
144-
onImageCaptured = { imageProxy -> viewModel.onImageCaptured(imageProxy) }
145-
)
153+
previewView?.bitmap?.let {
154+
Log.i("MyScan", "Pressed <Capture>")
155+
viewModel.onCapturePressed(it)
156+
captureController.takePicture(
157+
onImageCaptured = { imageProxy -> viewModel.onImageCaptured(imageProxy) }
158+
)
159+
}
146160
},
147161
onFinalizePressed = onFinalizePressed,
148162
thumbnailCoords = thumbnailCoords,
163+
showDetectionError = showDetectionError.value
149164
)
150165
}
151166

@@ -157,6 +172,7 @@ private fun CameraScreenScaffold(
157172
onCapture: () -> Unit,
158173
onFinalizePressed: () -> Unit,
159174
thumbnailCoords: MutableState<Offset>,
175+
showDetectionError: Boolean,
160176
) {
161177
Box {
162178
Scaffold(
@@ -173,7 +189,7 @@ private fun CameraScreenScaffold(
173189
.padding(bottom = innerPadding.calculateBottomPadding())
174190
.fillMaxSize()
175191
) {
176-
CameraPreviewWithOverlay(cameraPreview, cameraUiState)
192+
CameraPreviewWithOverlay(cameraPreview, cameraUiState, showDetectionError)
177193
MessageBox(cameraUiState.liveAnalysisState.inferenceTime)
178194
CaptureButton(
179195
onClick = onCapture,
@@ -183,8 +199,8 @@ private fun CameraScreenScaffold(
183199
)
184200
}
185201
}
186-
cameraUiState.captureState.processedImage?.let {
187-
image -> CapturedImage(image.asImageBitmap(), thumbnailCoords)
202+
if (cameraUiState.captureState is CaptureState.CapturePreview) {
203+
CapturedImage(cameraUiState.captureState.processed.asImageBitmap(), thumbnailCoords)
188204
}
189205
}
190206
}
@@ -273,14 +289,16 @@ fun CaptureButton(onClick: () -> Unit, modifier: Modifier) {
273289
@Composable
274290
private fun CameraPreviewWithOverlay(
275291
cameraPreview: @Composable () -> Unit,
276-
cameraUiState: CameraUiState
292+
cameraUiState: CameraUiState,
293+
showDetectionError: Boolean
277294
) {
295+
val captureState = cameraUiState.captureState
278296
val width = LocalConfiguration.current.screenWidthDp
279297
val height = width / 3 * 4
280298

281299
var showShutter by remember { mutableStateOf(false) }
282-
LaunchedEffect(cameraUiState.captureState.frozenImage) {
283-
if (cameraUiState.captureState.frozenImage != null) {
300+
LaunchedEffect(captureState.frozenImage) {
301+
if (captureState.frozenImage != null) {
284302
showShutter = true
285303
delay(200)
286304
showShutter = false
@@ -294,7 +312,7 @@ private fun CameraPreviewWithOverlay(
294312
) {
295313
cameraPreview()
296314
AnalysisOverlay(cameraUiState.liveAnalysisState)
297-
cameraUiState.captureState.frozenImage?.let {
315+
captureState.frozenImage?.let {
298316
Image(
299317
bitmap = it.asImageBitmap(),
300318
contentDescription = null,
@@ -308,6 +326,21 @@ private fun CameraPreviewWithOverlay(
308326
.background(Color.Black.copy(alpha = 0.6f))
309327
)
310328
}
329+
if (showDetectionError) {
330+
Box(
331+
modifier = Modifier
332+
.align(Alignment.Center)
333+
.background(Color.Black.copy(alpha = 0.7f), shape = RoundedCornerShape(8.dp))
334+
.padding(16.dp)
335+
) {
336+
Text(
337+
text = "No document detected",
338+
color = Color.White,
339+
fontSize = 16.sp
340+
)
341+
}
342+
}
343+
311344
}
312345
}
313346

@@ -359,13 +392,15 @@ fun CameraScreenFooter(
359392
@Preview(showBackground = true)
360393
@Composable
361394
fun CameraScreenPreview() {
362-
ScreenPreview(CaptureState())
395+
ScreenPreview(CaptureState.Idle)
363396
}
364397

365398
@Preview(showBackground = true, showSystemUi = true)
366399
@Composable
367400
fun CameraScreenPreviewWithProcessedImage() {
368-
ScreenPreview(CaptureState(processedImage = debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")))
401+
ScreenPreview(CaptureState.CapturePreview(
402+
debugImage("uncropped/img01.jpg"),
403+
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")))
369404
}
370405

371406
@Composable
@@ -403,6 +438,7 @@ private fun ScreenPreview(captureState: CaptureState) {
403438
onCapture = {},
404439
onFinalizePressed = {},
405440
thumbnailCoords = thumbnailCoords,
441+
showDetectionError = false,
406442
)
407443
}
408444
}

0 commit comments

Comments
 (0)