Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 54
ADFA-3108: Improve CV-to-XML accuracy with fuzzy search and OCR refinement #1047
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
9ae6fa601c995c9eb52e01ab72ac8ca621751d142a2d54cb11f7df0b6fa0773737893a2c6b19473ad7d44d9eddb7811dd1ad6808670f646bbf25e48a236df86255644File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| package org.appdevforall.codeonthego.computervision.di | ||
| import org.appdevforall.codeonthego.computervision.data.repository.ComputerVisionRepository | ||
| import org.appdevforall.codeonthego.computervision.data.repository.ComputerVisionRepositoryImpl | ||
| import org.appdevforall.codeonthego.computervision.data.source.OcrSource | ||
| import org.appdevforall.codeonthego.computervision.data.source.YoloModelSource | ||
| import org.appdevforall.codeonthego.computervision.domain.RegionOcrProcessor | ||
| import org.appdevforall.codeonthego.computervision.ui.viewmodel.ComputerVisionViewModel | ||
| import org.koin.android.ext.koin.androidContext | ||
| import org.koin.core.module.dsl.viewModel | ||
| @@ -14,11 +16,13 @@ val computerVisionModule = module { | ||
| single { OcrSource() } | ||
| single { RegionOcrProcessor(ocrSource = get()) } | ||
Daniel-ADFA marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| single<ComputerVisionRepository> { | ||
| ComputerVisionRepositoryImpl( | ||
| assetManager = androidContext().assets, | ||
| yoloModelSource = get(), | ||
| ocrSource = get() | ||
| regionOcrProcessor = get() | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4,54 +4,27 @@ import android.graphics.RectF | ||
| import com.google.mlkit.vision.text.Text | ||
| import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult | ||
| /** | ||
| * A dedicated, robust class to safely merge YOLO detections with ML Kit text results. | ||
| * This class uses a crash-proof, multi-pass filtering approach that is immune to | ||
| * ConcurrentModificationExceptions. | ||
| */ | ||
| class DetectionMerger( | ||
| private val yoloResults: List<DetectionResult>, | ||
| private val allTextBlocks: List<Text.TextBlock> | ||
| private val enrichedComponents: List<DetectionResult>, | ||
| private val remainingYoloDetections: List<DetectionResult>, | ||
| private val fullImageTextBlocks: List<Text.TextBlock> | ||
| ) { | ||
| // Define the roles for different labels for hierarchical processing | ||
| private val componentLabels = setOf("button", "checkbox_checked", "checkbox_unchecked", "switch_on", "switch_off", "chip", "text_entry_box") | ||
| private val containerLabels = setOf("card", "toolbar") | ||
| /** | ||
| * Executes the merging logic by running a series of ordered, non-destructive passes. | ||
| * @return The final, correctly merged list of all UI detections. | ||
| */ | ||
| fun merge(): List<DetectionResult> { | ||
| // Pass 1: Interactive components get exclusive first rights to claim text. | ||
| // This returns a map of which text block is now "reserved" by which component. | ||
| val componentTextClaims = claimTextForComponents() | ||
| // Pass 2: Build the final list, respecting the claims made in Pass 1. | ||
| val finalDetections = mutableListOf<DetectionResult>() | ||
| val usedTextBlocks = mutableSetOf<Text.TextBlock>() | ||
| // Add all YOLO results, enriching them with their claimed text. | ||
| for (yoloResult in yoloResults) { | ||
| val claimedText = componentTextClaims[yoloResult] | ||
| if (claimedText != null) { | ||
| yoloResult.text = claimedText.text.replace("\n", " ") | ||
| usedTextBlocks.add(claimedText) | ||
| } | ||
| finalDetections.add(yoloResult) | ||
| } | ||
| // Pass 3: Process containers and add their *unclaimed* internal content. | ||
| val yoloContainers = yoloResults.filter { it.label in containerLabels } | ||
| for (container in yoloContainers) { | ||
| val contentCandidates = allTextBlocks.filter { it !in usedTextBlocks } | ||
| finalDetections.addAll(enrichedComponents) | ||
| finalDetections.addAll(remainingYoloDetections) | ||
Daniel-ADFA marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| for (textBlock in contentCandidates) { | ||
| val containers = remainingYoloDetections.filter { it.label in containerLabels } | ||
| for (container in containers) { | ||
| val candidates = fullImageTextBlocks.filter { it !in usedTextBlocks } | ||
| for (textBlock in candidates) { | ||
| val textBox = textBlock.boundingBox?.let { RectF(it) } ?: continue | ||
| if (container.boundingBox.contains(textBox)) { | ||
| // This text is inside the container and was not claimed by a button, | ||
| // so it must be a standalone TextView within the card. | ||
| finalDetections.add( | ||
| DetectionResult( | ||
| boundingBox = textBox, | ||
| @@ -61,14 +34,12 @@ class DetectionMerger( | ||
| isYolo = false | ||
| ) | ||
| ) | ||
| // Mark it as used so it doesn't also become an orphan. | ||
| usedTextBlocks.add(textBlock) | ||
| } | ||
| } | ||
| } | ||
| // Pass 4: Any text that is still unused after all other passes is a true orphan. | ||
| val orphanText = allTextBlocks.filter { it !in usedTextBlocks } | ||
| val orphanText = fullImageTextBlocks.filter { it !in usedTextBlocks } | ||
| for (textBlock in orphanText) { | ||
| textBlock.boundingBox?.let { | ||
| finalDetections.add( | ||
| @@ -85,51 +56,4 @@ class DetectionMerger( | ||
| return finalDetections | ||
| } | ||
| /** | ||
| * Finds the single best text block for each interactive component based on IoU score. | ||
| * This method is non-destructive and only returns a "plan" of claims. | ||
| * @return A map where the key is the component and the value is the text block it has claimed. | ||
| */ | ||
| private fun claimTextForComponents(): Map<DetectionResult, Text.TextBlock> { | ||
| val claims = mutableMapOf<DetectionResult, Text.TextBlock>() | ||
| val yoloComponents = yoloResults.filter { it.label in componentLabels } | ||
| val availableText = allTextBlocks.toMutableSet() // Use a set for efficient removal | ||
| for (component in yoloComponents) { | ||
| val bestMatch = availableText | ||
| .mapNotNull { textBlock -> | ||
| textBlock.boundingBox?.let { box -> | ||
| val iou = calculateIoU(component.boundingBox, RectF(box)) | ||
| Triple(textBlock, iou, component) | ||
| } | ||
| } | ||
| .filter { it.second > 0.05 } // Must have at least some meaningful overlap | ||
| .maxByOrNull { it.second } // Get the best match based on IoU | ||
| // If a best match was found, reserve it and remove it from future consideration. | ||
| bestMatch?.let { (textBlock, _, matchedComponent) -> | ||
| claims[matchedComponent] = textBlock | ||
| availableText.remove(textBlock) | ||
| } | ||
| } | ||
| return claims | ||
| } | ||
| /** | ||
| * Utility function to calculate Intersection over Union (IoU). | ||
| */ | ||
| private fun calculateIoU(box1: RectF, box2: RectF): Float { | ||
| val xA = maxOf(box1.left, box2.left) | ||
| val yA = maxOf(box1.top, box2.top) | ||
| val xB = minOf(box1.right, box2.right) | ||
| val yB = minOf(box1.bottom, box2.bottom) | ||
| val intersectionArea = maxOf(0f, xB - xA) * maxOf(0f, yB - yA) | ||
| val box1Area = box1.width() * box1.height() | ||
| val box2Area = box2.width() * box2.height() | ||
| val unionArea = box1Area + box2Area - intersectionArea | ||
| return if (unionArea == 0f) 0f else intersectionArea / unionArea | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.