Move SSD contour cell helper to typed Kotlin

ober

aa24fcefdc8e48ce495e7524844d0c2fdc3b4f91

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 230f0d5..332e833 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -705,7 +705,7 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/VisionArrays.kt"
       (typed-library (com sfb ssdreview)
-        (export grayAt edgeAt sideCoverage darkSideCoverage)
+        (export grayAt edgeAt sideCoverage darkSideCoverage contourCell)
         (type Int32)
         (type IntArray)
         (type BooleanArray)
@@ -715,6 +715,16 @@
           (if (> a b) a b))
         (def (array-min-int32 (a : Int32) (b : Int32)) : Int32
           (if (< a b) a b))
+        (def (coverage-count-at-least (coverage : FloatArray) (threshold : Float32)) : Int32
+          (for/fold ((count (int32 0)))
+                    ((index (in-range (int32 0) (float-array-length coverage))))
+            (if (>= (float-array-ref coverage index) threshold)
+              (+ count (int32 1))
+              count)))
+        (def (coverage-sum (coverage : FloatArray)) : Float32
+          (for/fold ((total (float32 0.0)))
+                    ((index (in-range (int32 0) (float-array-length coverage))))
+            (+ total (float-array-ref coverage index))))
         (def (grayAt (gray : IntArray)
                      (width : Int32)
                      (height : Int32)
@@ -894,7 +904,54 @@
                 (/ (float32 top) (float32 (array-max-int32 (int32 1) w)))
                 (/ (float32 bottom) (float32 (array-max-int32 (int32 1) w)))
                 (/ (float32 left) (float32 (array-max-int32 (int32 1) h)))
-                (/ (float32 right) (float32 (array-max-int32 (int32 1) h)))))))))
+                (/ (float32 right) (float32 (array-max-int32 (int32 1) h)))))))
+        (def (contourCell (component : Component)
+                          (edge : BooleanArray)
+                          (width : Int32)
+                          (height : Int32)) : (Nullable SsdCell)
+          (let ((w (componentW component))
+                (h (componentH component)))
+            (let ((bboxArea (* w h)))
+              (if (or (or (< bboxArea (int32 50))
+                          (> bboxArea (int32 2400)))
+                      (or (or (< w (int32 7))
+                              (> w (int32 90)))
+                          (or (< h (int32 7))
+                              (> h (int32 90)))))
+                (nullable-none SsdCell)
+                (let ((aspect (/ (float32 w)
+                                 (float32 (array-max-int32 (int32 1) h)))))
+                  (if (or (< aspect (float32 0.55))
+                          (> aspect (float32 1.8)))
+                    (nullable-none SsdCell)
+                    (let ((perimeter (array-max-int32 (int32 1) (* (int32 2) (+ w h))))
+                          (safeArea (array-max-int32 (int32 1) bboxArea)))
+                      (let ((edgeDensity (/ (float32 (Component-area component))
+                                            (float32 safeArea)))
+                            (edgePerimeterRatio (/ (float32 (Component-area component))
+                                                   (float32 perimeter))))
+                        (if (or (< edgeDensity (float32 0.035))
+                                (or (< edgePerimeterRatio (float32 0.18))
+                                    (> edgePerimeterRatio (float32 5.0))))
+                          (nullable-none SsdCell)
+                          (let ((coverage
+                                  (sideCoverage edge width height
+                                                (Component-x1 component)
+                                                (Component-y1 component)
+                                                w
+                                                h)))
+                            (if (or (< (coverage-count-at-least coverage (float32 0.22))
+                                       (int32 3))
+                                    (< (coverage-sum coverage) (float32 1.10)))
+                              (nullable-none SsdCell)
+                              (nullable-some
+                                (make-SsdCell
+                                  ""
+                                  (float32 (Component-x1 component))
+                                  (float32 (Component-y1 component))
+                                  (float32 w)
+                                  (float32 h)
+                                  "android-contour")))))))))))))))
 
     (typed-kotlin-file "com/sfb/ssdreview/UiTextHelpers.kt"
       (typed-library (com sfb ssdreview)
@@ -3338,22 +3395,6 @@
        "            .mapNotNull { contourCell(it, edge, width, height) }"
        "    }"
        ""
-       "    private fun contourCell(component: Component, edge: BooleanArray, width: Int, height: Int): SsdCell? {"
-       "        val w = componentW(component)"
-       "        val h = componentH(component)"
-       "        val bboxArea = w * h"
-       "        if (bboxArea !in 50..2400 || w !in 7..90 || h !in 7..90) return null"
-       "        val aspect = w.toFloat() / max(1, h).toFloat()"
-       "        if (aspect !in 0.55f..1.8f) return null"
-       "        val perimeter = max(1, 2 * (w + h))"
-       "        val edgeDensity = component.area.toFloat() / max(1, bboxArea).toFloat()"
-       "        val edgePerimeterRatio = component.area.toFloat() / perimeter.toFloat()"
-       "        if (edgeDensity < 0.035f || edgePerimeterRatio !in 0.18f..5.0f) return null"
-       "        val coverage = sideCoverage(edge, width, height, component.x1, component.y1, w, h)"
-       "        if (coverage.count { it >= 0.22f } < 3 || coverage.sum() < 1.10f) return null"
-       "        return SsdCell(\"\", component.x1.toFloat(), component.y1.toFloat(), w.toFloat(), h.toFloat(), \"android-contour\")"
-       "    }"
-       ""
        "    private fun mergeDuplicateCells(cells: List<SsdCell>): MutableList<SsdCell> {"
        "        val merged = mutableListOf<SsdCell>()"
        "        for (cell in cells.sortedWith(compareBy<SsdCell> { it.y }.thenBy { it.x })) {"