Move SSD vision pixel predicates to typed Kotlin

ober

fb5628d89e35ad694bd8c21128153c68c8c38c3b

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 687be49..c0d1cac 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -481,6 +481,63 @@
               (float-array-ref rect (int32 1))
               (float-array-ref rect (int32 3)))))))
 
+
+    (typed-kotlin-file "com/sfb/ssdreview/VisionPixels.kt"
+      (typed-library (com sfb ssdreview)
+        (export isColorMask looksLikeInk looksLikeBoxFill)
+        (type Int32)
+        (def (pixelRed (pixel : Int32)) : Int32
+          (bitwise-and
+            (bitwise-arithmetic-shift-right pixel (int32 16))
+            (int32 255)))
+        (def (pixelGreen (pixel : Int32)) : Int32
+          (bitwise-and
+            (bitwise-arithmetic-shift-right pixel (int32 8))
+            (int32 255)))
+        (def (pixelBlue (pixel : Int32)) : Int32
+          (bitwise-and pixel (int32 255)))
+        (def (maxInt32 (a : Int32) (b : Int32)) : Int32
+          (if (> a b) a b))
+        (def (minInt32 (a : Int32) (b : Int32)) : Int32
+          (if (< a b) a b))
+        (def (pixelMaxChannel (pixel : Int32)) : Int32
+          (let ((r (pixelRed pixel))
+                (g (pixelGreen pixel))
+                (b (pixelBlue pixel)))
+            (maxInt32 r (maxInt32 g b))))
+        (def (pixelMinChannel (pixel : Int32)) : Int32
+          (let ((r (pixelRed pixel))
+                (g (pixelGreen pixel))
+                (b (pixelBlue pixel)))
+            (minInt32 r (minInt32 g b))))
+        (def (pixelLuminance (pixel : Int32)) : Float
+          (+ (+ (* 0.2126 (exact->inexact (pixelRed pixel)))
+                (* 0.7152 (exact->inexact (pixelGreen pixel))))
+             (* 0.0722 (exact->inexact (pixelBlue pixel)))))
+        (def (isColorMask (pixel : Int32)) : Bool
+          (let ((maxC (pixelMaxChannel pixel))
+                (minC (pixelMinChannel pixel)))
+            (let ((saturation
+                    (if (equal? maxC (int32 0))
+                      (int32 0)
+                      (/ (* (- maxC minC) (int32 255)) maxC))))
+              (and (> saturation (int32 12))
+                   (and (> maxC (int32 120))
+                        (< maxC (int32 252)))))))
+        (def (looksLikeInk (pixel : Int32)) : Bool
+          (let ((maxC (pixelMaxChannel pixel))
+                (minC (pixelMinChannel pixel))
+                (lum (pixelLuminance pixel)))
+            (or (< lum 226.0)
+                (and (> (- maxC minC) (int32 28))
+                     (< lum 248.0)))))
+        (def (looksLikeBoxFill (pixel : Int32)) : Bool
+          (let ((maxC (pixelMaxChannel pixel))
+                (minC (pixelMinChannel pixel))
+                (lum (pixelLuminance pixel)))
+            (and (and (> lum 115.0)
+                      (< lum 252.0))
+                 (> (- maxC minC) (int32 8)))))))
     (typed-kotlin-file "com/sfb/ssdreview/UiTextHelpers.kt"
       (typed-library (com sfb ssdreview)
         (export normalizeFiringArc normalizeLabel isGenericGuessLabel isSsdPdfName
@@ -3001,36 +3058,6 @@
        "        return overlap / (a.w * a.h + b.w * b.h - overlap)"
        "    }"
        ""
-       "    private fun isColorMask(pixel: Int): Boolean {"
-       "        val r = (pixel shr 16) and 0xff"
-       "        val g = (pixel shr 8) and 0xff"
-       "        val b = pixel and 0xff"
-       "        val maxC = max(r, max(g, b))"
-       "        val minC = min(r, min(g, b))"
-       "        val saturation = if (maxC == 0) 0 else ((maxC - minC) * 255 / maxC)"
-       "        return saturation > 12 && maxC > 120 && maxC < 252"
-       "    }"
-       ""
-       "    private fun looksLikeInk(pixel: Int): Boolean {"
-       "        val r = (pixel shr 16) and 0xff"
-       "        val g = (pixel shr 8) and 0xff"
-       "        val b = pixel and 0xff"
-       "        val maxC = max(r, max(g, b))"
-       "        val minC = min(r, min(g, b))"
-       "        val lum = (0.2126 * r + 0.7152 * g + 0.0722 * b)"
-       "        return lum < 226.0 || (maxC - minC > 28 && lum < 248.0)"
-       "    }"
-       ""
-       "    private fun looksLikeBoxFill(pixel: Int): Boolean {"
-       "        val r = (pixel shr 16) and 0xff"
-       "        val g = (pixel shr 8) and 0xff"
-       "        val b = pixel and 0xff"
-       "        val maxC = max(r, max(g, b))"
-       "        val minC = min(r, min(g, b))"
-       "        val lum = (0.2126 * r + 0.7152 * g + 0.0722 * b)"
-       "        return lum > 115.0 && lum < 252.0 && maxC - minC > 8"
-       "    }"
-       ""
        "    private fun grayscale(pixels: IntArray): IntArray = IntArray(pixels.size) { index ->"
        "        val pixel = pixels[index]"
        "        val r = (pixel shr 16) and 0xff"