Move SSD detector initial grouping to typed Kotlin

ober

15d309bf9f38311673bd10e85e844fb479d09b47

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 1740de2..086687d 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -448,7 +448,8 @@
                 ssdSessionFindCellById ssdSessionRecomputeGroup
                 ssdSessionSetSsdArea ssdSessionPruneGroupsOutsideSsdArea
                 createManualCells
-                ssdSessionNextCellId ssdSessionNextGroupId)
+                ssdSessionNextCellId ssdSessionNextGroupId
+                bboxForMutableCells)
         (type Int32)
         (type FloatArray)
         (type SsdCell)
@@ -702,6 +703,112 @@
                 (int32 4)
                 #\0))))))
 
+    (typed-kotlin-file "com/sfb/ssdreview/SsdDetectorGroups.kt"
+      (typed-library (com sfb ssdreview)
+        (export initialGroups)
+        (type Int32)
+        (type BooleanArray)
+        (def (detectorGroupId (index : Int32)) : String
+          (string-append "g"
+            (string-pad-start (int32->string (+ index (int32 1))) (int32 4) #\0)))
+        (def (markAdjacentCells! (cells : (MutableList SsdCell))
+                                 (seen : BooleanArray)
+                                 (component : BooleanArray)
+                                 (queue : (MutableList Int32))
+                                 (current : Int32)) : Unit
+          (begin
+            (for/fold ((ignored (int32 0)))
+                      ((j (in-range (int32 0) (list-size cells))))
+              (if (and (not (boolean-array-ref seen j))
+                       (adjacent (list-ref cells current) (list-ref cells j)))
+                  (begin
+                    (boolean-array-set! seen j #t)
+                    (boolean-array-set! component j #t)
+                    (mutable-list-add! queue j)
+                    ignored)
+                  ignored))
+            (begin)))
+        (def (growInitialGroupComponent (cells : (MutableList SsdCell))
+                                        (seen : BooleanArray)
+                                        (start : Int32)) : BooleanArray
+          (let ((component
+                  (boolean-array-build
+                    (list-size cells)
+                    (index #f)))
+                (queue (mutable-list-empty Int32)))
+            (begin
+              (boolean-array-set! seen start #t)
+              (boolean-array-set! component start #t)
+              (mutable-list-add! queue start)
+              (for/fold ((cursor (int32 0)))
+                        ((pass (in-range (int32 0) (list-size cells))))
+                (if (< cursor (list-size queue))
+                    (begin
+                      (markAdjacentCells!
+                        cells
+                        seen
+                        component
+                        queue
+                        (list-ref queue cursor))
+                      (+ cursor (int32 1)))
+                    cursor))
+              component)))
+        (def (componentCellIds (cells : (MutableList SsdCell))
+                               (component : BooleanArray)) : (MutableList String)
+          (for/fold ((ids (mutable-list-empty String)))
+                    ((i (in-range (int32 0) (list-size cells))))
+            (if (boolean-array-ref component i)
+                (begin
+                  (mutable-list-add! ids (SsdCell-id (list-ref cells i)))
+                  ids)
+                ids)))
+        (def (componentCells (cells : (MutableList SsdCell))
+                             (component : BooleanArray)) : (MutableList SsdCell)
+          (for/fold ((out (mutable-list-empty SsdCell)))
+                    ((i (in-range (int32 0) (list-size cells))))
+            (if (boolean-array-ref component i)
+                (begin
+                  (mutable-list-add! out (list-ref cells i))
+                  out)
+                out)))
+        (def (addInitialGroupFromComponent! (groups : (MutableList SsdGroup))
+                                            (cells : (MutableList SsdCell))
+                                            (component : BooleanArray)) : Unit
+          (let ((ids (componentCellIds cells component)))
+            (if (> (list-size ids) (int32 1))
+                (let ((groupCells (componentCells cells component)))
+                  (mutable-list-add!
+                    groups
+                    (make-SsdGroup
+                      (detectorGroupId (list-size groups))
+                      ""
+                      ""
+                      "candidate"
+                      (list-size groupCells)
+                      (bboxForMutableCells groupCells)
+                      ids
+                      ""
+                      "")))
+                (begin))))
+        (def (initialGroups (cells : (MutableList SsdCell))) : (MutableList SsdGroup)
+          (let ((groups (mutable-list-empty SsdGroup))
+                (seen
+                  (boolean-array-build
+                    (list-size cells)
+                    (index #f))))
+            (begin
+              (for/fold ((ignored (int32 0)))
+                        ((i (in-range (int32 0) (list-size cells))))
+                (if (boolean-array-ref seen i)
+                    ignored
+                    (begin
+                      (addInitialGroupFromComponent!
+                        groups
+                        cells
+                        (growInitialGroupComponent cells seen i))
+                      ignored)))
+              groups)))))
+
     (typed-kotlin-file "com/sfb/ssdreview/SourceKey.kt"
       (typed-library (com sfb ssdreview)
         (export sourceKey)
@@ -3788,7 +3895,7 @@
        "                        detectorSource = if (forceDetect && existingTruthGroupCount > 0) \"forced-android-plus-truth\" else \"android-fallback\""
        "                    }"
        "                    session.cells.addAll(detectedCells.filter { ssdSessionCellInsideSsdArea(session, it) })"
-       "                    session.groups.addAll(SsdDetector.initialGroups(session.cells).take(120))"
+       "                    session.groups.addAll(initialGroups(session.cells).take(120))"
        "                    if (existingTruth != null && existingTruthGroupCount > 0) {"
        "                        truthStore.applyTruth(session)"
        "                        ssdSessionPruneGroupsOutsideSsdArea(session)"
@@ -4794,61 +4901,6 @@
        "        return merged"
        "    }"
        ""
-       "    fun initialGroups(cells: List<SsdCell>): MutableList<SsdGroup> {"
-       "        val edges = List(cells.size) { mutableListOf<Int>() }"
-       "        for (i in cells.indices) {"
-       "            for (j in i + 1 until cells.size) {"
-       "                if (adjacent(cells[i], cells[j])) {"
-       "                    edges[i].add(j)"
-       "                    edges[j].add(i)"
-       "                }"
-       "            }"
-       "        }"
-       "        val seen = BooleanArray(cells.size)"
-       "        val components = mutableListOf<List<Int>>()"
-       "        for (i in cells.indices) {"
-       "            if (seen[i]) continue"
-       "            val stack = ArrayDeque<Int>()"
-       "            val component = mutableListOf<Int>()"
-       "            stack.add(i)"
-       "            seen[i] = true"
-       "            while (!stack.isEmpty()) {"
-       "                val index = stack.removeLast()"
-       "                component.add(index)"
-       "                for (next in edges[index]) {"
-       "                    if (!seen[next]) {"
-       "                        seen[next] = true"
-       "                        stack.add(next)"
-       "                    }"
-       "                }"
-       "            }"
-       "            if (component.size > 1) {"
-       "                components.add(component)"
-       "            }"
-       "        }"
-       "        return components"
-       "            .sortedWith(compareBy<List<Int>> { component -> component.minOf { cells[it].y } }"
-       "                .thenBy { component -> component.minOf { cells[it].x } })"
-       "            .mapIndexed { index, component ->"
-       "                val ids = component.sortedWith(compareBy<Int> { cells[it].y }.thenBy { cells[it].x })"
-       "                    .map { cells[it].id }"
-       "                    .toMutableList()"
-       "                val groupCells = ids.mapNotNull { id -> cells.firstOrNull { it.id == id } }"
-       "                SsdGroup("
-       "                    id = \"g%04d\".format(index + 1),"
-       "                    label = \"\","
-       "                    boxTypeId = \"\","
-       "                    status = \"candidate\","
-       "                    count = groupCells.size,"
-       "                    bbox = bboxForCells(groupCells),"
-       "                    cellIds = ids,"
-       "                    firingArc = \"\","
-       "                    notes = \"\""
-       "                )"
-       "            }"
-       "            .toMutableList()"
-       "    }"
-       ""
        "    fun shrinkRectToInk(bitmap: Bitmap, rect: FloatArray): FloatArray? {"
        "        return shrinkRectByMask(bitmap, rect) { pixel -> looksLikeInk(pixel) }"
        "    }"