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 @@ -309,6 +309,8 @@ object FuzzyAttributeParser {
annotation.length
}

if (current.valueStart > valueEnd) continue

var rawValue = annotation.substring(current.valueStart, valueEnd).trim()

if (rawValue.isNotEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.appdevforall.codeonthego.computervision.domain.model.DetectionResult
import org.appdevforall.codeonthego.computervision.domain.model.ScaledBox
import org.appdevforall.codeonthego.computervision.domain.xml.AndroidXmlGenerator
import org.appdevforall.codeonthego.computervision.utils.MetadataDetector
import org.appdevforall.codeonthego.computervision.utils.getSortedScaledPlaceholders
import org.appdevforall.codeonthego.computervision.utils.buildPlaceholderOverrides
import kotlin.comparisons.compareBy

class YoloToXmlConverter(
Expand All @@ -23,41 +23,31 @@ class YoloToXmlConverter(
targetDpHeight: Int,
wrapInScroll: Boolean = true
): Pair<String, String> {
val uiCandidates = detections
.filter { (it.isYolo || it.label == "text") && it.label != "widget_tag" }
.filterNot { MetadataDetector.isMetadataDetection(it.label, it.text) }
.distinctBy {
if (it.label.startsWith("switch")) {
"${((it.boundingBox.top + it.boundingBox.bottom) / 2f).toInt() / 50}"
} else {
"${it.label}:${it.boundingBox.left}:${it.boundingBox.top}:${it.boundingBox.right}:${it.boundingBox.bottom}"
}
}
// 1. Filter and prepare base UI candidates
val uiCandidates = extractUiCandidates(detections)

var scaledBoxes = uiCandidates.map { geometryProcessor.scaleDetection(it, sourceImageWidth, sourceImageHeight, targetDpWidth, targetDpHeight) }
// 2. Scale detections to target DP dimensions
val scaledBoxes = scaleDetections(uiCandidates, sourceImageWidth, sourceImageHeight, targetDpWidth, targetDpHeight)

val parents = scaledBoxes.filter { it.label != "text" && !annotationMatcher.isTag(it.text) }
var texts = scaledBoxes.filter { it.label == "text" && !annotationMatcher.isTag(it.text) }
// 3. Associate isolated text detections to their respective UI widgets
val associatedBoxes = associateTextToWidgets(scaledBoxes)

scaledBoxes = geometryProcessor.assignTextToParents(parents, texts, scaledBoxes)
texts = scaledBoxes.filter { it.label == "text" && !annotationMatcher.isTag(it.text) }
scaledBoxes = geometryProcessor.assignNearbyTextToWidgets(scaledBoxes, texts)
// 4. Clean up and finalize the UI elements list
val uiElements = finalizeUiElements(associatedBoxes)

val uiElements = scaledBoxes.filter {
!annotationMatcher.isTag(it.text) && !MetadataDetector.isMetadataDetection(it.label, it.text)
}
val widgetTags = detections.filter { it.label == "widget_tag" || (!it.isYolo && annotationMatcher.isTag(it.text)) }
val canvasTags = widgetTags.map { geometryProcessor.scaleDetection(it, sourceImageWidth, sourceImageHeight, targetDpWidth, targetDpHeight) }
// 5. Extract and scale reference tags (e.g., T-1, B-1) from the canvas
val canvasTags = extractCanvasTags(detections, sourceImageWidth, sourceImageHeight, targetDpWidth, targetDpHeight)

// 6. Match margin annotations with the extracted UI elements
val finalAnnotations = annotationMatcher.matchAnnotationsToElements(canvasTags, uiElements, annotations)

// 7. Sort boxes top-to-bottom, left-to-right for sequential XML rendering
val sortedBoxes = uiElements.sortedWith(compareBy({ it.y }, { it.x }))

val selectedImageOverrides = buildSelectedImageOverrides(
uiElements = uiElements,
selectedImagesByPlaceholderId = selectedImagesByPlaceholderId
)
// 8. Prepare local drawable resources overrides for image placeholders
val selectedImageOverrides = uiElements.buildPlaceholderOverrides(selectedImagesByPlaceholderId)

// 9. Generate final XML output
return xmlGenerator.buildXml(
boxes = sortedBoxes,
annotations = finalAnnotations,
Expand All @@ -67,17 +57,63 @@ class YoloToXmlConverter(
)
}

private fun buildSelectedImageOverrides(
uiElements: List<ScaledBox>,
selectedImagesByPlaceholderId: Map<String, String>
): Map<ScaledBox, String> {
val placeholders = uiElements.getSortedScaledPlaceholders()

return placeholders.mapIndexedNotNull { index, box ->
val drawableReference = selectedImagesByPlaceholderId["ph_$index"]
?: return@mapIndexedNotNull null
box to drawableReference
}.toMap()
private fun extractUiCandidates(detections: List<DetectionResult>): List<DetectionResult> {
return detections
.filter { (it.isYolo || it.label == "text") && it.label != "widget_tag" }
.filterNot { MetadataDetector.isMetadataDetection(it.label, it.text) }
.distinctBy {
if (it.label.startsWith("switch")) {
// Deduplicate switches by grouping them within a 50px vertical band
"${((it.boundingBox.top + it.boundingBox.bottom) / 2f).toInt() / 50}"
} else {
// Exact coordinate deduplication for other widgets
"${it.label}:${it.boundingBox.left}:${it.boundingBox.top}:${it.boundingBox.right}:${it.boundingBox.bottom}"
}
}
}

private fun scaleDetections(
candidates: List<DetectionResult>,
sourceWidth: Int,
sourceHeight: Int,
targetWidth: Int,
targetHeight: Int
): List<ScaledBox> {
return candidates.map {
geometryProcessor.scaleDetection(it, sourceWidth, sourceHeight, targetWidth, targetHeight)
}
}

private fun associateTextToWidgets(scaledBoxes: List<ScaledBox>): List<ScaledBox> {
val parents = scaledBoxes.filter { it.label != "text" }
val initialTexts = scaledBoxes.filter { it.label == "text" && !annotationMatcher.isTag(it.text) }
val textAssignedBoxes = geometryProcessor.assignTextToParents(parents, initialTexts, scaledBoxes)
val remainingTexts = textAssignedBoxes.filter { it.label == "text" && !annotationMatcher.isTag(it.text) }

return geometryProcessor.assignNearbyTextToWidgets(textAssignedBoxes, remainingTexts)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private fun finalizeUiElements(boxes: List<ScaledBox>): List<ScaledBox> {
return boxes.filter {
// Keep the widget if it's not pure text, or if it is text but not recognized as a tag.
(it.label != "text" || !annotationMatcher.isTag(it.text)) &&
!MetadataDetector.isMetadataDetection(it.label, it.text)
}
}

private fun extractCanvasTags(
detections: List<DetectionResult>,
sourceWidth: Int,
sourceHeight: Int,
targetWidth: Int,
targetHeight: Int
): List<ScaledBox> {
val widgetTags = detections.filter {
it.label == "widget_tag" || (!it.isYolo && annotationMatcher.isTag(it.text))
}
return widgetTags.map {
geometryProcessor.scaleDetection(it, sourceWidth, sourceHeight, targetWidth, targetHeight)
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ fun List<ScaledBox>.getSortedScaledPlaceholders(): List<ScaledBox> {
return this.filter { it.label == IMAGE_PLACEHOLDER_LABEL }
.sortedWith(compareBy({ it.y }, { it.x }))
}

/**
* Associates ordered image placeholders with their selected drawable references.
* Useful for mapping user-selected gallery images to the physical canvas bounding boxes.
*/
fun List<ScaledBox>.buildPlaceholderOverrides(selectedImagesByPlaceholderId: Map<String, String>): Map<ScaledBox, String> {
val placeholders = this.getSortedScaledPlaceholders()

return placeholders.mapIndexedNotNull { index, box ->
val drawableReference = selectedImagesByPlaceholderId["ph_$index"]
?: return@mapIndexedNotNull null
box to drawableReference
}.toMap()
}
Loading