Move SSD edge mask to typed Kotlin

ober

573e97fea4a03cb8b80c00afa0df5e9c06a9e8b5

diff --git a/.build.yml b/.build.yml
index 93b4b7d..b222cf6 100644
--- a/.build.yml
+++ b/.build.yml
@@ -5,7 +5,7 @@ packages:
   - make=4.4.1-r4
 sources:
   # Build dependency: full immutable commit, mirrored in dependencies.lock.json.
-  - "https://git.sr.ht/~lisp/jerboa#9dd139f6d61883e002be82dcdf923d357d09c1b0"
+  - "https://git.sr.ht/~lisp/jerboa#b0c66c1bb8042f3c48fcb3eae95222bc50d0a410"
   # The second source is the build subject selected by the SourceHut submitter.
   - https://git.sr.ht/~lisp/jerboa-android
 tasks:
@@ -14,6 +14,6 @@ tasks:
       test "$(apk info -v chez-scheme)" = chez-scheme-10.3.0-r2
       test "$(apk info -v git)" = git-2.54.0-r0
       test "$(apk info -v make)" = make-4.4.1-r4
-      test "$(git -C ../jerboa rev-parse HEAD)" = 9dd139f6d61883e002be82dcdf923d357d09c1b0
-      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = d9c0b1bd6f2c6c87bee6610e275b3997d29e53ac
+      test "$(git -C ../jerboa rev-parse HEAD)" = b0c66c1bb8042f3c48fcb3eae95222bc50d0a410
+      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = cde344bb6e2cda51bf460b54492ee3f42d953850
       JERBOA="chez --libdirs .:../jerboa/lib --script" make test
diff --git a/dependencies.lock.json b/dependencies.lock.json
index 46f6236..6da1126 100644
--- a/dependencies.lock.json
+++ b/dependencies.lock.json
@@ -11,8 +11,8 @@
   "generator_runtime": {
     "name": "jerboa",
     "repository": "https://git.sr.ht/~lisp/jerboa",
-    "commit": "9dd139f6d61883e002be82dcdf923d357d09c1b0",
-    "tree": "d9c0b1bd6f2c6c87bee6610e275b3997d29e53ac"
+    "commit": "b0c66c1bb8042f3c48fcb3eae95222bc50d0a410",
+    "tree": "cde344bb6e2cda51bf460b54492ee3f42d953850"
   },
   "assurance_tools": {
     "osv_scanner": {
diff --git a/scripts/verify-supply-chain.sh b/scripts/verify-supply-chain.sh
index 63a2a35..03e4828 100755
--- a/scripts/verify-supply-chain.sh
+++ b/scripts/verify-supply-chain.sh
@@ -3,8 +3,8 @@ set -eu
 
 repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
 lock="$repo/dependencies.lock.json"
-jerboa_commit=9dd139f6d61883e002be82dcdf923d357d09c1b0 # gitsafe:ignore
-jerboa_tree=d9c0b1bd6f2c6c87bee6610e275b3997d29e53ac # gitsafe:ignore
+jerboa_commit=b0c66c1bb8042f3c48fcb3eae95222bc50d0a410 # gitsafe:ignore
+jerboa_tree=cde344bb6e2cda51bf460b54492ee3f42d953850 # gitsafe:ignore
 gradle_sha=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
 jdk_macos_sha=8fa1eff40bb637a33613b2ccb8b12c70dc3661cc22cf8e784943715769a05336
 jdk_linux_sha=d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331
diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 5c8db68..f25473e 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -789,7 +789,7 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/VisionArrays.kt"
       (typed-library (com sfb ssdreview)
-        (export grayAt edgeAt sideCoverage darkSideCoverage contourCell toneCell)
+        (export grayAt edgeAt edgeMask sideCoverage darkSideCoverage contourCell toneCell)
         (type Int32)
         (type IntArray)
         (type BooleanArray)
@@ -799,6 +799,10 @@
           (if (> a b) a b))
         (def (array-min-int32 (a : Int32) (b : Int32)) : Int32
           (if (< a b) a b))
+        (def (array-abs-int32 (value : Int32)) : Int32
+          (if (< value (int32 0))
+            (- (int32 0) value)
+            value))
         (def (coverage-count-at-least (coverage : FloatArray) (threshold : Float32)) : Int32
           (for/fold ((count (int32 0)))
                     ((index (in-range (int32 0) (float-array-length coverage))))
@@ -831,6 +835,122 @@
                           (>= y height))))
             #f
             (boolean-array-ref edge (+ (* y width) x))))
+        (def (sobelPixel (gray : IntArray)
+                         (width : Int32)
+                         (x : Int32)
+                         (y : Int32)
+                         (dx : Int32)
+                         (dy : Int32)) : Int32
+          (int-array-ref gray (+ (* (+ y dy) width) (+ x dx))))
+        (def (edgeGradientAt (gray : IntArray)
+                             (width : Int32)
+                             (height : Int32)
+                             (x : Int32)
+                             (y : Int32)) : Int32
+          (if (or (or (< x (int32 1))
+                      (< y (int32 1)))
+                  (or (>= x (- width (int32 1)))
+                      (>= y (- height (int32 1)))))
+            (int32 0)
+            (let ((nw (sobelPixel gray width x y (int32 -1) (int32 -1)))
+                  (n (sobelPixel gray width x y (int32 0) (int32 -1)))
+                  (ne (sobelPixel gray width x y (int32 1) (int32 -1)))
+                  (w (sobelPixel gray width x y (int32 -1) (int32 0)))
+                  (e (sobelPixel gray width x y (int32 1) (int32 0)))
+                  (sw (sobelPixel gray width x y (int32 -1) (int32 1)))
+                  (s (sobelPixel gray width x y (int32 0) (int32 1)))
+                  (se (sobelPixel gray width x y (int32 1) (int32 1))))
+              (let ((gx
+                      (+ (+ (+ (+ (+ (- (int32 0) nw)
+                                      ne)
+                                   (- (int32 0) (* (int32 2) w)))
+                                (* (int32 2) e))
+                             (- (int32 0) sw))
+                          se))
+                    (gy
+                      (+ (+ (+ (+ (+ (- (int32 0) nw)
+                                      (- (int32 0) (* (int32 2) n)))
+                                   (- (int32 0) ne))
+                                sw)
+                             (* (int32 2) s))
+                          se)))
+                (array-min-int32
+                  (int32 65535)
+                  (+ (array-abs-int32 gx) (array-abs-int32 gy)))))))
+        (def (edgeGradientAtIndex (gray : IntArray)
+                                  (width : Int32)
+                                  (height : Int32)
+                                  (index : Int32)) : Int32
+          (edgeGradientAt gray width height (mod index width) (/ index width)))
+        (def (edgeHistogram (gray : IntArray)
+                            (width : Int32)
+                            (height : Int32)) : IntArray
+          (let ((histogram
+                  (int-array-build
+                    (int32 221)
+                    (index (int32 0)))))
+            (begin
+              (for/fold ((count (int32 0)))
+                        ((index (in-range (int32 0) (* width height))))
+                (let ((mag (edgeGradientAtIndex gray width height index)))
+                  (if (> mag (int32 0))
+                    (let ((bin (array-min-int32 (int32 220) mag)))
+                      (begin
+                        (int-array-set!
+                          histogram
+                          bin
+                          (+ (int-array-ref histogram bin) (int32 1)))
+                        (+ count (int32 1))))
+                    count)))
+              histogram)))
+        (def (edgeHistogramSampleCount (histogram : IntArray)) : Int32
+          (for/fold ((count (int32 0)))
+                    ((bin (in-range (int32 1) (int-array-length histogram))))
+            (+ count (int-array-ref histogram bin))))
+        (def (edgeThresholdFromHistogram (histogram : IntArray)
+                                         (sampleCount : Int32)) : Int32
+          (if (<= sampleCount (int32 0))
+            (int32 120)
+            (let ((rank
+                    (array-min-int32
+                      (- sampleCount (int32 1))
+                      (/ (+ (* sampleCount (int32 88)) (int32 50))
+                         (int32 100)))))
+              (let ((state
+                      (for/fold ((state (int32 0)))
+                                ((bin (in-range (int32 1)
+                                                (int-array-length histogram))))
+                        (if (< state (int32 0))
+                          state
+                          (let ((next (+ state (int-array-ref histogram bin))))
+                            (if (> next rank)
+                              (- (int32 0) (+ bin (int32 1)))
+                              next))))))
+                (let ((adaptive
+                        (if (< state (int32 0))
+                          (- (int32 0) (+ state (int32 1)))
+                          (int32 220))))
+                  (array-min-int32
+                    (array-max-int32 (int32 90) adaptive)
+                    (int32 220)))))))
+        (def (edgeMask (gray : IntArray)
+                       (width : Int32)
+                       (height : Int32)) : BooleanArray
+          (if (or (< width (int32 3))
+                  (< height (int32 3)))
+            (boolean-array-build
+              (* width height)
+              (index #f))
+            (let ((histogram (edgeHistogram gray width height)))
+              (let ((threshold
+                      (edgeThresholdFromHistogram
+                        histogram
+                        (edgeHistogramSampleCount histogram))))
+                (boolean-array-build
+                  (* width height)
+                  (index
+                    (>= (edgeGradientAtIndex gray width height index)
+                        threshold)))))))
         (def (edgeBandTop? (edge : BooleanArray)
                            (width : Int32)
                            (height : Int32)
@@ -3250,10 +3370,8 @@
        ""
        "import android.graphics.Bitmap"
        "import java.util.ArrayDeque"
-       "import kotlin.math.abs"
        "import kotlin.math.max"
        "import kotlin.math.min"
-       "import kotlin.math.roundToInt"
        ""
        "object SsdDetector {"
        "    fun detectCells(bitmap: Bitmap): MutableList<SsdCell> {"
@@ -3446,26 +3564,6 @@
        "        return merged.mapIndexed { index, cell -> cell.copy(id = \"c%04d\".format(index + 1)) }.toMutableList()"
        "    }"
        ""
-       "    private fun edgeMask(gray: IntArray, width: Int, height: Int): BooleanArray {"
-       "        val gradients = IntArray(width * height)"
-       "        val samples = mutableListOf<Int>()"
-       "        if (width < 3 || height < 3) return BooleanArray(width * height)"
-       "        for (y in 1 until height - 1) {"
-       "            for (x in 1 until width - 1) {"
-       "                fun p(dx: Int, dy: Int): Int = gray[(y + dy) * width + x + dx]"
-       "                val gx = -p(-1, -1) + p(1, -1) - 2 * p(-1, 0) + 2 * p(1, 0) - p(-1, 1) + p(1, 1)"
-       "                val gy = -p(-1, -1) - 2 * p(0, -1) - p(1, -1) + p(-1, 1) + 2 * p(0, 1) + p(1, 1)"
-       "                val mag = min(65535, abs(gx) + abs(gy))"
-       "                gradients[y * width + x] = mag"
-       "                if (mag > 0) samples.add(mag)"
-       "            }"
-       "        }"
-       "        samples.sort()"
-       "        val adaptive = if (samples.isEmpty()) 120 else samples[(samples.size * 0.88f).roundToInt().coerceIn(0, samples.size - 1)].coerceAtLeast(90)"
-       "        val threshold = min(adaptive, 220)"
-       "        return BooleanArray(width * height) { gradients[it] >= threshold }"
-       "    }"
-       ""
        "}"
        ))
     (kotlin-file-lines "com/sfb/ssdreview/SsdVisionBridge.kt"