diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt index 6a1b97f656..a5aa9aa1c1 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt @@ -279,19 +279,40 @@ internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_ex } private fun startOcr() { - imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> + imageAnalysis.setAnalyzer(cameraExecutor) { videoFrame: ImageProxy -> if (viewModel.isRunningOcrOnFrame) { - imageProxy.close() + videoFrame.close() return@setAnalyzer } - viewModel.ocrOnFrameStarted() + // Running OCR as often as we can while camera feedback is displayed to the user - captureFullResImageForOcr() - imageProxy.close() + viewModel.ocrOnFrameStarted() + if (viewModel.ocrConfig.useHighRes) { + captureHighResImageForOcr() + videoFrame.close() + } else { + captureFrameFromVideoStreamForOcr(videoFrame) + } + } + } + + private fun captureFrameFromVideoStreamForOcr(imageProxy: ImageProxy) { + lifecycleScope.launch(bgDispatcher) { + try { + val (bitmap, imageInfo) = imageProxy.toBitmap() to imageProxy.imageInfo + val cropConfig: OcrCropConfig = buildOcrCropConfigUseCase( + rotationDegrees = imageInfo.rotationDegrees, + cameraPreview = binding.preview, + documentScannerArea = binding.documentScannerArea, + ) + viewModel.runOcrOnFrame(frame = bitmap, cropConfig) + } finally { + imageProxy.close() + } } } - private fun captureFullResImageForOcr() { + private fun captureHighResImageForOcr() { imageCapture.takePicture( cameraExecutor, object : ImageCapture.OnImageCapturedCallback() { diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt index eca7949b36..b557c4895e 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt @@ -13,6 +13,7 @@ import com.simprints.core.livedata.send import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp import com.simprints.feature.externalcredential.screens.scanocr.model.DetectedOcrBlock +import com.simprints.feature.externalcredential.screens.scanocr.model.OcrConfig import com.simprints.feature.externalcredential.screens.scanocr.model.OcrCropConfig import com.simprints.feature.externalcredential.screens.scanocr.model.OcrDocumentType import com.simprints.feature.externalcredential.screens.scanocr.model.asExternalCredentialType @@ -23,7 +24,9 @@ import com.simprints.feature.externalcredential.screens.scanocr.usecase.Normaliz import com.simprints.feature.externalcredential.screens.scanocr.usecase.ZoomOntoCredentialUseCase import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.config.store.models.experimental import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.credential.store.CredentialImageRepository @@ -73,6 +76,19 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor( private val _finishOcrEvent = MutableLiveData>() private lateinit var startTime: Timestamp + lateinit var ocrConfig: OcrConfig + private set + + init { + viewModelScope.launch { + ocrConfig = configManager.getProjectConfiguration().experimental().let { config -> + OcrConfig( + useHighRes = config.ocrUseHighRes, + capturesRequired = config.ocrCaptures.coerceIn(OCR_CAPTURE_MIN, OCR_CAPTURE_MAX), + ) + } + } + } private fun updateState(state: (ScanOcrState) -> ScanOcrState) { this.state = state(this.state) @@ -89,7 +105,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor( ScanOcrState.ScanningInProgress( ocrDocumentType = ocrDocumentType, successfulCaptures = 0, - scansRequired = SUCCESSFUL_SCANS_REQUIRED, + scansRequired = ocrConfig.capturesRequired, ) } } @@ -110,7 +126,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor( ScanOcrState.ScanningInProgress( ocrDocumentType = ocrDocumentType, successfulCaptures = detectedBlocks.size, - scansRequired = SUCCESSFUL_SCANS_REQUIRED, + scansRequired = ocrConfig.capturesRequired, ) } } finally { @@ -168,6 +184,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor( } companion object { - private const val SUCCESSFUL_SCANS_REQUIRED = 3 + private const val OCR_CAPTURE_MIN = 1 + private const val OCR_CAPTURE_MAX = 10 } } diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrConfig.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrConfig.kt new file mode 100644 index 0000000000..314b03df9a --- /dev/null +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/model/OcrConfig.kt @@ -0,0 +1,9 @@ +package com.simprints.feature.externalcredential.screens.scanocr.model + +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports + +@ExcludedFromGeneratedTestCoverageReports("Data struct") +internal data class OcrConfig( + val useHighRes: Boolean, + val capturesRequired: Int, +) diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt index e6ced18048..9722d8380b 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt @@ -63,6 +63,18 @@ data class ExperimentalProjectConfiguration( ?.toLong() ?: FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT + val ocrUseHighRes: Boolean + get() = customConfig + ?.get(OCR_USE_HIGH_RES) + ?.let { it as? Boolean } + ?: OCR_USE_HIGH_RES_DEFAULT + + val ocrCaptures: Int + get() = customConfig + ?.get(OCR_CAPTURES) + ?.let { it as? Int } + ?: OCR_CAPTURES_DEFAULT + companion object { internal const val ENABLE_ID_POOL_VALIDATION = "validateIdentificationPool" internal const val SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED = "singleQualityFallbackRequired" @@ -82,5 +94,10 @@ data class ExperimentalProjectConfiguration( internal const val FALLBACK_TO_COMMCARE_THRESHOLD_DAYS = "fallbackToCommCareThresholdDays" internal const val FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT = 5L + + internal const val OCR_USE_HIGH_RES = "ocrHighRes" + internal const val OCR_USE_HIGH_RES_DEFAULT = true + internal const val OCR_CAPTURES = "ocrCaptures" + internal const val OCR_CAPTURES_DEFAULT = 3 } } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt index 87afb9cccd..4d9a1c968d 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt @@ -163,4 +163,28 @@ internal class ExperimentalProjectConfigurationTest { assertThat(ExperimentalProjectConfiguration(config).fallbackToCommCareThresholdDays).isEqualTo(result) } } + + @Test + fun `check ocr use high res flag correctly`() { + mapOf( + emptyMap() to true, + mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to 1) to true, + mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to false) to false, + mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to true) to true, + ).forEach { (config, result) -> + assertThat(ExperimentalProjectConfiguration(config).ocrUseHighRes).isEqualTo(result) + } + } + + @Test + fun `check ocr captures value correctly`() { + val expectedOcrCaptures = 10 + mapOf( + emptyMap() to ExperimentalProjectConfiguration.OCR_CAPTURES_DEFAULT, + mapOf(ExperimentalProjectConfiguration.OCR_CAPTURES to true) to ExperimentalProjectConfiguration.OCR_CAPTURES_DEFAULT, + mapOf(ExperimentalProjectConfiguration.OCR_CAPTURES to expectedOcrCaptures) to expectedOcrCaptures, + ).forEach { (config, result) -> + assertThat(ExperimentalProjectConfiguration(config).ocrCaptures).isEqualTo(result) + } + } }