-
Notifications
You must be signed in to change notification settings - Fork 2
[MS-1421] Creating OCR framework to work with the OCR readouts with text filter and spatial constraints #1643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alexandr-simprints
merged 10 commits into
main
from
MS-1421-create-ocr-framework-for-intuitive-fields-extraction
May 5, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
94cb114
[MS-1421] Creating OCR framework to work with the OCR readouts with t…
alexandr-simprints bfdaf0d
[MS-1421] Removing redundant method
alexandr-simprints c5e0363
[MS-1421] Adding test coverage to the OCR Readerˆ
alexandr-simprints ac66a52
[MS-1421] Separating OcrQuery from OcrReader
alexandr-simprints d448371
[MS-1421] Removing Ocrbuilder in favor of ReadTextFromImageUseCase. T…
alexandr-simprints ce6929a
[MS-1421] KDoc updates and test fixes
alexandr-simprints 53e64f7
[MS-1426] adding ::containsPattern method to OcrQuery to be specfic i…
alexandr-simprints c49df3a
[MS-1426] Fixing tests
alexandr-simprints 3c5ef91
[MS-1421] Decoupling OcrQuery execution from construction by moving r…
alexandr-simprints e9b6a53
[MS-1421] Clearing the redundant code, removing OcrBlock class, addin…
alexandr-simprints File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
...src/main/java/com/simprints/feature/externalcredential/screens/scanocr/reader/OcrModel.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.simprints.feature.externalcredential.screens.scanocr.reader | ||
|
|
||
| import com.simprints.core.ExcludedFromGeneratedTestCoverageReports | ||
| import com.simprints.feature.externalcredential.model.BoundingBox | ||
|
|
||
| /** | ||
| * Wrapper for all scanned text after the OCR | ||
| * | ||
| * @param allLines all lines from blocks sorted by bounding box top coordinate ascending | ||
| */ | ||
| @ExcludedFromGeneratedTestCoverageReports("Data class") | ||
| internal data class OcrText( | ||
| val allLines: List<OcrLine>, | ||
| ) | ||
|
|
||
| /** | ||
| * A single line of text detected by the OCR kit. | ||
| * | ||
| * @param id unique id of the line in a single OCR scan | ||
| * @param text normalized text (extra spaces removed) | ||
| * @param boundingBox coordinates of the line | ||
| * @param blockBoundingBox parent coordinates | ||
| * @param confidence overall confidence of the text value based on the average confidence for each character (aka element) in [text] | ||
| */ | ||
| @ExcludedFromGeneratedTestCoverageReports("Data class") | ||
| internal data class OcrLine( | ||
| val id: Int, | ||
| val text: String, | ||
| val boundingBox: BoundingBox, | ||
| val blockBoundingBox: BoundingBox, | ||
| val confidence: Float, | ||
| ) |
47 changes: 47 additions & 0 deletions
47
...src/main/java/com/simprints/feature/externalcredential/screens/scanocr/reader/OcrQuery.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package com.simprints.feature.externalcredential.screens.scanocr.reader | ||
|
|
||
| /** | ||
| * Defines the search criteria for locating a single line of text within a scanned document. | ||
| * Used as the receiver of the [OcrReader.find] block. | ||
| */ | ||
| internal class OcrQuery { | ||
| internal val filters = mutableListOf<(OcrLine) -> Boolean>() | ||
| internal var belowAnchor: OcrQuery? = null | ||
| internal var aboveAnchor: OcrQuery? = null | ||
|
|
||
| fun matchesPattern(regex: Regex) { | ||
| filters += { line -> regex.matches(line.text) } | ||
| } | ||
|
|
||
| fun containsPattern(regex: Regex) { | ||
| filters += { line -> regex.containsMatchIn(line.text) } | ||
| } | ||
|
|
||
| fun containsText(text: String) { | ||
| filters += { line -> line.text.contains(text, ignoreCase = true) } | ||
| } | ||
|
|
||
| fun hasExactText(text: String) { | ||
| filters += { line -> line.text.equals(text, ignoreCase = true) } | ||
|
alexandr-simprints marked this conversation as resolved.
|
||
| } | ||
|
|
||
| fun hasId(id: Int) { | ||
| filters += { line -> line.id == id } | ||
| } | ||
|
|
||
| fun isBelow(resolveAnchor: OcrQuery.() -> Unit) { | ||
| belowAnchor = OcrQuery().apply(resolveAnchor) | ||
| } | ||
|
|
||
| fun isBelow(anchor: OcrLine) { | ||
| belowAnchor = OcrQuery().apply { filters += { line -> line.id == anchor.id } } | ||
| } | ||
|
|
||
| fun isAbove(resolveAnchor: OcrQuery.() -> Unit) { | ||
| aboveAnchor = OcrQuery().apply(resolveAnchor) | ||
| } | ||
|
|
||
| fun isAbove(anchor: OcrLine) { | ||
| aboveAnchor = OcrQuery().apply { filters += { line -> line.id == anchor.id } } | ||
| } | ||
| } | ||
70 changes: 70 additions & 0 deletions
70
...rc/main/java/com/simprints/feature/externalcredential/screens/scanocr/reader/OcrReader.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package com.simprints.feature.externalcredential.screens.scanocr.reader | ||
|
|
||
| /** | ||
| * Entry point for querying an [OcrText]. | ||
| * | ||
| * Usage: | ||
| * ``` | ||
| * val reader = OcrReader(ocrText) | ||
| * | ||
| * val membershipNumber = reader.find { | ||
| * matchesPattern(Regex("\\d{8}")) | ||
|
alexandr-simprints marked this conversation as resolved.
|
||
| * isBelow { containsText("membership number") } | ||
| * isAbove { containsText("expiry date") } | ||
| * } | ||
| * ``` | ||
| */ | ||
| internal class OcrReader( | ||
| val ocrText: OcrText, | ||
| ) { | ||
| /** | ||
| * Executes the query defined in [block] and returns the first matching [OcrLine], or null. | ||
| * The [block] receives an [OcrQuery] as its receiver — call filter methods directly | ||
| * without any chaining or terminal call. | ||
| * | ||
| * Usage: | ||
| * ``` | ||
| * val reader = OcrReader(ocrText) | ||
| * | ||
| * val membershipNumber = reader.find { | ||
| * matchesPattern(Regex("\\d{8}")) | ||
| * isBelow { containsText("membership number") } | ||
| * isAbove { containsText("expiry date") } | ||
| * } | ||
| * ``` | ||
| * | ||
| * Only one level of nesting is supported. Spatial filters inside an anchor block are silently ignored. | ||
| * The following is NOT supported: | ||
| * ``` | ||
| * reader.find { | ||
| * isBelow { | ||
| * isBelow { containsText("some major title") } // ignored — has no effect | ||
| * containsText("some subtitle") | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| fun find(block: OcrQuery.() -> Unit): OcrLine? = runQuery(OcrQuery().apply(block)) | ||
|
|
||
| private fun runQuery(query: OcrQuery): OcrLine? { | ||
| val belowAnchor = query.belowAnchor?.let { runQuery(it) } | ||
| val aboveAnchor = query.aboveAnchor?.let { runQuery(it) } | ||
|
|
||
| if (query.belowAnchor != null && belowAnchor == null) return null | ||
| if (query.aboveAnchor != null && aboveAnchor == null) return null | ||
|
|
||
| return ocrText.allLines.firstOrNull { line -> | ||
| val isBelowAnchor = belowAnchor == null || ( | ||
| line.boundingBox.top > belowAnchor.boundingBox.top && | ||
| line.boundingBox.left >= belowAnchor.boundingBox.left && | ||
| line.boundingBox.left < belowAnchor.boundingBox.right | ||
| ) | ||
| val isAboveAnchor = aboveAnchor == null || ( | ||
| line.boundingBox.top < aboveAnchor.boundingBox.top && | ||
|
alexandr-simprints marked this conversation as resolved.
|
||
| line.boundingBox.left >= aboveAnchor.boundingBox.left && | ||
| line.boundingBox.left < aboveAnchor.boundingBox.right | ||
| ) | ||
| query.filters.all { it(line) } && isBelowAnchor && isAboveAnchor | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 3 additions & 1 deletion
4
...rints/feature/externalcredential/screens/scanocr/usecase/GhanaIdCardOcrSelectorUseCase.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 3 additions & 1 deletion
4
...nts/feature/externalcredential/screens/scanocr/usecase/GhanaNhisCardOcrSelectorUseCase.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
.../simprints/feature/externalcredential/screens/scanocr/usecase/ReadTextFromImageUseCase.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.simprints.feature.externalcredential.screens.scanocr.usecase | ||
|
|
||
| import android.graphics.Bitmap | ||
| import com.google.android.gms.tasks.Tasks | ||
| import com.google.mlkit.vision.common.InputImage | ||
| import com.google.mlkit.vision.text.Text | ||
| import com.google.mlkit.vision.text.TextRecognition | ||
| import com.google.mlkit.vision.text.latin.TextRecognizerOptions | ||
| import com.simprints.core.ExcludedFromGeneratedTestCoverageReports | ||
| import com.simprints.feature.externalcredential.model.toBoundingBox | ||
| import com.simprints.feature.externalcredential.screens.scanocr.reader.OcrLine | ||
| import com.simprints.feature.externalcredential.screens.scanocr.reader.OcrText | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| @ExcludedFromGeneratedTestCoverageReports("Unable to mock Google ML Kit") | ||
| internal class ReadTextFromImageUseCase @Inject constructor() { | ||
| private val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) | ||
|
|
||
| operator fun invoke(bitmap: Bitmap): OcrText? { | ||
| val image = InputImage.fromBitmap(bitmap, 0) | ||
| val result = Tasks.await(recognizer.process(image)) ?: return null | ||
| return build(result) | ||
| } | ||
|
|
||
| private fun build(mlKitText: Text): OcrText { | ||
| var nextLineId = 0 | ||
|
|
||
| val allLinesSorted = mlKitText.textBlocks | ||
| .flatMap { block -> | ||
| block.lines.map { line -> | ||
| OcrLine( | ||
| id = nextLineId++, | ||
| text = line.text.trim(), | ||
| boundingBox = line.boundingBox.toBoundingBox(), | ||
| blockBoundingBox = block.boundingBox.toBoundingBox(), | ||
| confidence = line.confidence, | ||
| ) | ||
| } | ||
| }.sortedBy { it.boundingBox.top } | ||
|
|
||
| return OcrText(allLines = allLinesSorted) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.