Move SSD learned feature JSON to typed Kotlin

ober

dcf24991955881a0dbad86a42fa563edaca1ce40

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 3420add..b43783a 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1061,6 +1061,72 @@
               "height"
               (int32 1))))))
 
+    (typed-kotlin-file "com/sfb/ssdreview/LearnedFeatures.kt"
+      (kotlin-imports (org json JSONArray) (org json JSONObject))
+      (typed-library (com sfb ssdreview)
+        (export groupFeaturesFromTruthGroup groupFeaturesFromGroup)
+        (type JSONArray)
+        (type JSONObject)
+        (type Float32)
+        (type FloatArray)
+        (type Int32)
+        (def (featureJsonArrayOrEmpty (raw : (Nullable JSONArray))) : JSONArray
+          (if (nullable-null? raw)
+              (json-array-empty)
+              (nullable-get raw)))
+        (def (featurePositiveFloat (value : Float32)) : Float32
+          (if (> value (float32 1.0))
+              value
+              (float32 1.0)))
+        (def (featureSafeImage (value : Int32)) : Float32
+          (featurePositiveFloat (float32 value)))
+        (def (featureObject (count : Int32)
+                            (x1 : Float32)
+                            (y1 : Float32)
+                            (x2 : Float32)
+                            (y2 : Float32)
+                            (imageWidth : Int32)
+                            (imageHeight : Int32)) : JSONObject
+          (let ((w (featurePositiveFloat (- x2 x1)))
+                (h (featurePositiveFloat (- y2 y1)))
+                (safeWidth (featureSafeImage imageWidth))
+                (safeHeight (featureSafeImage imageHeight)))
+            (let ((out (json-object-empty)))
+              (begin
+                (json-object-put-int32! out "count" count)
+                (json-object-put-float32! out "cx"
+                  (/ (/ (+ x1 x2) (float32 2.0)) safeWidth))
+                (json-object-put-float32! out "cy"
+                  (/ (/ (+ y1 y2) (float32 2.0)) safeHeight))
+                (json-object-put-float32! out "w" (/ w safeWidth))
+                (json-object-put-float32! out "h" (/ h safeHeight))
+                (json-object-put-float32! out "aspect" (/ w h))
+                out))))
+        (def (groupFeaturesFromTruthGroup (group : JSONObject)
+                                          (imageWidth : Int32)
+                                          (imageHeight : Int32)) : JSONObject
+          (let ((bbox (featureJsonArrayOrEmpty (json-object-opt-json-array group "bbox"))))
+            (featureObject
+              (json-object-opt-int32-default group "count" (int32 0))
+              (json-array-opt-float32 bbox (int32 0) (float32 0.0))
+              (json-array-opt-float32 bbox (int32 1) (float32 0.0))
+              (json-array-opt-float32 bbox (int32 2) (float32 0.0))
+              (json-array-opt-float32 bbox (int32 3) (float32 0.0))
+              imageWidth
+              imageHeight)))
+        (def (groupFeaturesFromGroup (group : SsdGroup)
+                                     (imageWidth : Int32)
+                                     (imageHeight : Int32)) : JSONObject
+          (let ((bbox (SsdGroup-bbox group)))
+            (featureObject
+              (SsdGroup-count group)
+              (float-array-ref bbox (int32 0))
+              (float-array-ref bbox (int32 1))
+              (float-array-ref bbox (int32 2))
+              (float-array-ref bbox (int32 3))
+              imageWidth
+              imageHeight)))))
+
     (typed-kotlin-file "com/sfb/ssdreview/Guess.kt"
       (typed-library (com sfb ssdreview)
         (export make-Guess Guess? Guess-label Guess-boxType Guess-reason)
@@ -5341,7 +5407,7 @@
        "        var applied = 0"
        "        session.groups.forEach { group ->"
        "            if (group.label.isNotBlank() && !group.label.endsWith(\"?\")) return@forEach"
-       "            val features = groupFeatures(group, session.imageWidth, session.imageHeight)"
+       "            val features = groupFeaturesFromGroup(group, session.imageWidth, session.imageHeight)"
        "            var best: JSONObject? = null"
        "            var bestScore = Float.POSITIVE_INFINITY"
        "            for (i in 0 until examples.length()) {"
@@ -5595,7 +5661,7 @@
        "                    .put(\"firing_arc\", group.optString(\"firing_arc\", group.optString(\"arc\")))"
        "                    .put(\"count\", group.optInt(\"count\"))"
        "                    .put(\"bbox\", group.optJSONArray(\"bbox\") ?: JSONArray())"
-       "                    .put(\"features\", groupFeatures(group, imageWidth(truth), imageHeight(truth))))"
+       "                    .put(\"features\", groupFeaturesFromTruthGroup(group, imageWidth(truth), imageHeight(truth))))"
        "            }"
        "        }"
        "        val learned = JSONObject()"
@@ -5819,39 +5885,6 @@
        "        return file.inputStream().use { readBoundedBytes(it, MAX_ZIP_ENTRY_BYTES).decodeToString() }"
        "    }"
        ""
-       "    private fun groupFeatures(group: JSONObject, imageWidth: Int, imageHeight: Int): JSONObject {"
-       "        val bbox = group.optJSONArray(\"bbox\") ?: JSONArray()"
-       "        val x1 = bbox.optDouble(0, 0.0)"
-       "        val y1 = bbox.optDouble(1, 0.0)"
-       "        val x2 = bbox.optDouble(2, 0.0)"
-       "        val y2 = bbox.optDouble(3, 0.0)"
-       "        val w = (x2 - x1).coerceAtLeast(1.0)"
-       "        val h = (y2 - y1).coerceAtLeast(1.0)"
-       "        return JSONObject()"
-       "            .put(\"count\", group.optInt(\"count\", 0))"
-       "            .put(\"cx\", ((x1 + x2) / 2.0) / imageWidth.coerceAtLeast(1))"
-       "            .put(\"cy\", ((y1 + y2) / 2.0) / imageHeight.coerceAtLeast(1))"
-       "            .put(\"w\", w / imageWidth.coerceAtLeast(1))"
-       "            .put(\"h\", h / imageHeight.coerceAtLeast(1))"
-       "            .put(\"aspect\", w / h)"
-       "    }"
-       ""
-       "    private fun groupFeatures(group: SsdGroup, imageWidth: Int, imageHeight: Int): JSONObject {"
-       "        val x1 = group.bbox[0].toDouble()"
-       "        val y1 = group.bbox[1].toDouble()"
-       "        val x2 = group.bbox[2].toDouble()"
-       "        val y2 = group.bbox[3].toDouble()"
-       "        val w = (x2 - x1).coerceAtLeast(1.0)"
-       "        val h = (y2 - y1).coerceAtLeast(1.0)"
-       "        return JSONObject()"
-       "            .put(\"count\", group.count)"
-       "            .put(\"cx\", ((x1 + x2) / 2.0) / imageWidth.coerceAtLeast(1))"
-       "            .put(\"cy\", ((y1 + y2) / 2.0) / imageHeight.coerceAtLeast(1))"
-       "            .put(\"w\", w / imageWidth.coerceAtLeast(1))"
-       "            .put(\"h\", h / imageHeight.coerceAtLeast(1))"
-       "            .put(\"aspect\", w / h)"
-       "    }"
-       ""
        "    private fun featureDistance(a: JSONObject, b: JSONObject): Float {"
        "        val countA = a.optDouble(\"count\", 0.0)"
        "        val countB = b.optDouble(\"count\", 0.0)"