Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -73,6 +76,19 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor(
private val _finishOcrEvent = MutableLiveData<LiveDataEventWithContent<ScannedCredential>>()

private lateinit var startTime: Timestamp
lateinit var ocrConfig: OcrConfig
private set

init {
viewModelScope.launch {
ocrConfig = configManager.getProjectConfiguration().experimental().let { config ->
OcrConfig(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not return the class directly from the experimental config? MIN/MAX could be defined in the experimental config the same way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The settings are feature-related, and the idea was for the feature to provide the configuration data classes within its scope (as done with OcrCropConfig).

Currently, the values are coming from the experimental part of the project configuration, and I'd like to keep them atomic, and not feature-specific.

The idea is to test what project settings work best, and later move the configuration to Vulcan 'MF-ID' tab. That means that this part will be refactored in the following releases, and the only thing that we will need to change is where the setting values are coming from

useHighRes = config.ocrUseHighRes,
capturesRequired = config.ocrCaptures.coerceIn(OCR_CAPTURE_MIN, OCR_CAPTURE_MAX),
)
}
}
}

private fun updateState(state: (ScanOcrState) -> ScanOcrState) {
this.state = state(this.state)
Expand All @@ -89,7 +105,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor(
ScanOcrState.ScanningInProgress(
ocrDocumentType = ocrDocumentType,
successfulCaptures = 0,
scansRequired = SUCCESSFUL_SCANS_REQUIRED,
scansRequired = ocrConfig.capturesRequired,
)
}
}
Expand All @@ -110,7 +126,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor(
ScanOcrState.ScanningInProgress(
ocrDocumentType = ocrDocumentType,
successfulCaptures = detectedBlocks.size,
scansRequired = SUCCESSFUL_SCANS_REQUIRED,
scansRequired = ocrConfig.capturesRequired,
)
}
} finally {
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,28 @@ internal class ExperimentalProjectConfigurationTest {
assertThat(ExperimentalProjectConfiguration(config).fallbackToCommCareThresholdDays).isEqualTo(result)
}
}

@Test
fun `check ocr use high res flag correctly`() {
mapOf(
emptyMap<String, Any>() 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<String, Any>() 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)
}
}
}