ImagingVisualizer.kt
src/jvmMain/kotlin/afield/ui/visualization/ImagingVisualizer.kt · 1169 lines
package and imports — 60 lines, from line 1
@file:afield.runtime.completion.CompletionCategory("Visualization")
package afield.ui.visualization
import afield.app.diagnostics.named
import afield.app.protocol.payload
import afield.data.table.div
import afield.data.table.get
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.document.graph.box
import afield.runtime.completion.CompletionCategory
import afield.runtime.execution.plus
import afield.runtime.lifecycle.end
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.ui.notation.center
import afield.ui.notation.long
import afield.ui.notation.toFLine
import afield.ui.notation.with
import afield.ui.output.column
import afield.ui.output.histogram
import afield.ui.output.tree
import afield.ai.conversation.images
import afield.audio.synth.*
import afield.graphics.color.Color4
import afield.graphics.color.Colormaps
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.lifecycle.map
import afield.tools.docs.format
import afield.ui.output.stats
import afield.ui.output.templates.filter
import afield.media.image.ComponentLabelling
import afield.media.image.Corner
import afield.media.image.FlowField
import afield.media.image.FlowVector
import afield.media.image.GaussianPyramid
import afield.media.image.Gradient
import afield.media.image.HoughCircle
import afield.media.image.HoughLine
import afield.media.image.Img
import afield.media.image.LaplacianPyramid
import afield.media.image.resize
import afield.media.image.toPngBytes
import java.util.Base64
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.ceil
import kotlin.math.cos
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.math.sqrt
Inline visualizers for the CPU imaging and computer-vision family (afield.stdlib/imaging*.kt):
what a gradient, a pyramid, a flow field or a list of corners looks like when a box evaluates one.
The imaging layer is around 250 public symbols of raster and feature code, and Img is the only
one of its types that carries a picture of its own. Everything else falls through to a reflective
object tree unless something here claims it, and there a list of forty Corners is a wall of
{x=…, y=…, response=…} and a GaussianPyramid is GaussianPyramid@3f1a2b. Those are exactly
the values you want to look at.
Where this hooks in
Three one-line hooks, small enough that this file can grow without touching ValueVisualizer.kt:
Img.toHTMLdelegates to img.Imgimplements ToHTML, whichValueVisualizerdispatches before its built-in table, so improving theImgpicture has to happen there.ValueVisualizer.visualizeBuiltIn's fallback branch tries format first, which covers the non-data classes (Gradient, GaussianPyramid, LaplacianPyramid,FlowField, ComponentLabelling).ValueVisualizer.formatListtries formatList first, which covers the homogeneous detector outputs (List<Corner>,List<HoughLine>,List<HoughCircle>,List<FlowVector>,List<Img>).
Turning a float image into pixels
The family is float-channel and unbounded: a Sobel dx, a Laplacian and a
difference-of-Gaussians are legitimately signed, an unnormalised response legitimately exceeds 1,
and a division somewhere upstream legitimately produces a NaN. A visualizer that clamps all of
those to 0..1 shows a flat white or a flat black rectangle and tells you nothing, so the raster
renderer picks one of three modes:
- direct — every finite sample already sits in
0..1. Shown as-is; nothing is rescaled. - signed ±m — some finite sample is negative. The range is made symmetric about zero
(
m = max(|min|, |max|)) so that zero is a fixed, recognisable colour. A 1-channel image is additionally run through thecoolwarmdiverging ramp from Colormaps (blue negative, pale zero, red positive), because grey cannot carry the sign of a gradient. Multi-channel signed data is offset and scaled per sample without a ramp. - auto-ranged — everything is non-negative but the maximum exceeds 1. Linearly rescaled, with the data's true range printed beside it, so a bright pixel is never mistaken for 1.0.
The caption names the mode whenever the mode rescaled something; the statistics table inside the disclosure names it in every case, including the fourth one, where no sample is finite at all.
Non-finite samples (NaN, ±∞) are counted, excluded from every statistic, and painted magenta
so they are impossible to miss. Excluding them is what keeps one NaN out of the minimum and the
maximum, where it would take the whole display range with it and quantise to a uniformly black
thumbnail captioned range NaN…NaN.
A 4-channel image is drawn over a checkerboard, so transparent and black are distinguishable.
The disclosure
Each raster visualizer carries a closed <details> with the per-channel strip, a per-channel
histogram computed over the full-resolution samples, and the numeric stats. It is a plain
<details> element rather than a lazy-value-fragment expander link, so it needs no running
server and works identically in a live box, in a docs capture and in a saved HTML file.
object ImagingVisualizer
The dispatch surface: format for a single imaging value, formatList for a homogeneous list of
them, and img for the Img thumbnail that Img.toHTML calls directly.
declaration — 57 lines, from line 124
object ImagingVisualizer {
private object K {
/** Long edge of the main inline raster thumbnail, in CSS px. */
const val THUMB_MAX_SIDE = 240
/** Long edge of one per-channel thumbnail inside the disclosure. */
const val CHANNEL_SIDE = 76
/** Long edge of one pyramid rung; the base rung gets this and the rest scale from it. */
const val PYRAMID_BASE_SIDE = 120
/** Long edge of a filmstrip frame for a `List<Img>`. */
const val FILMSTRIP_SIDE = 92
/** Width of an SVG geometry panel (corners / lines / circles / flow). */
const val PANEL_W = 236
/** Height bounds for an SVG geometry panel; the aspect of the data decides within these. */
const val PANEL_H_MIN = 72
const val PANEL_H_MAX = 236
/** Width of the small `(θ, ρ)` Hough accumulator panel. */
const val HOUGH_PARAM_W = 116
const val HOUGH_PARAM_H = 96
/** Histogram geometry, in CSS px. */
const val HIST_W = 236
const val HIST_H = 52
const val HIST_BINS = 64
/** Beyond this many list elements the geometry panels sub-sample rather than draw everything. */
const val MAX_GLYPHS = 900
/** Numeric readouts of individual detections stop after this many. */
const val MAX_LISTED = 6
/** Marker radii in CSS px. */
const val DOT_R_MIN = 1.6
const val DOT_R_MAX = 5.0
/** A flow field whose median displacement is under this fraction of the frame gets exaggerated. */
const val FLOW_VISIBLE_FRACTION = 0.035
// ---- palette (kept close to ValueVisualizer's: #9cf headers, #888 captions) --------------
const val C_HEAD = "#9cf"
const val C_DIM = "#8a90a0"
const val C_FAINT = "#666"
const val C_ACCENT = "#fc8"
const val C_OK = "#6cf"
const val C_BAD = "#f6a"
const val BORDER = "#3a3f4b"
const val PANEL_BG = "#111"
/** Painted over any non-finite sample. */
val NON_FINITE = Color4(1.0, 0.0, 0.85)
}
Dispatch
fun format(value: Any): String?
Render one of the imaging family's non-list types, or null if value is not one of them
(in which case ValueVisualizer carries on with its reflective/toString fallback).
fun format(value: Any): String? = when (value) {
is Img -> img(value)
is Gradient -> gradient(value)
is GaussianPyramid -> gaussianPyramid(value)
is LaplacianPyramid -> laplacianPyramid(value)
is FlowField -> flowField(value)
is ComponentLabelling -> componentLabelling(value)
else -> null
}
fun formatList(list: List<*>): String?
Render a homogeneous list of imaging values as one picture, or null when the list is not
homogeneously one of the supported element types (empty lists and mixed lists fall through to
ValueVisualizer's ordinary list rendering).
fun formatList(list: List<*>): String? {
if (list.isEmpty()) return null
return when {
list.all { it is Corner } -> corners(list.filterIsInstance<Corner>())
list.all { it is HoughLine } -> houghLines(list.filterIsInstance<HoughLine>())
list.all { it is HoughCircle } -> houghCircles(list.filterIsInstance<HoughCircle>())
list.all { it is FlowVector } -> flowField(FlowField(list.filterIsInstance<FlowVector>()))
list.all { it is Img } -> filmstrip(list.filterIsInstance<Img>())
else -> null
}
}
Img
fun img(image: Img): String
The inline picture for an Img: an aspect-correct thumbnail of the actual pixels under the
range policy this file's overview sets out, a one-line readout, and a disclosure holding
the per-channel strip, histogram and statistics.
Called from Img.toHTML(), so this is what evaluating an image in a box shows.
fun img(image: Img): String {
val stats = Stats.of(image)
val raster = renderRaster(image, stats, K.THUMB_MAX_SIDE)
return card {
append(rasterFigure(raster, image.channels == 4))
append(
caption(
typeToken("Img"),
"${image.width}×${image.height}",
channelWord(image.channels),
stats.rangeText(),
raster.modeNote
)
)
append(imgDisclosure(image, stats, raster))
}
}
Per-channel strip + histogram + numeric statistics, behind a closed <details>.
private fun imgDisclosure(image: Img, stats: Stats, raster: Raster): String = disclosure(
summary = if (image.channels == 1) "grey · viridis · histogram · stats"
else "channels · histogram · stats"
) {
append(channelStrip(image, stats))
append(histogramSvg(image, stats))
append(statsTable(image, stats, raster))
}
One small thumbnail per channel. A 1-channel image has no channels to split, so it instead
shows two different readings of itself: plain grey (forced, even when the main thumbnail is
the coolwarm diverging map — otherwise the strip would just repeat it) and viridis, whose
perceptual uniformity pulls structure out of a response image that grey flattens.
private fun channelStrip(image: Img, stats: Stats): String {
val small = fitLongEdge(image, K.CHANNEL_SIDE)
val panels: List<Pair<String, Raster>> = if (image.channels == 1) {
listOf(
"grey" to renderRaster(small, stats, K.CHANNEL_SIDE, ramp = null, forceRamp = true),
"viridis" to renderRaster(small, stats, K.CHANNEL_SIDE, ramp = "viridis", forceRamp = true)
)
} else {
(0 until image.channels).map { c ->
val ch = extractChannel(small, c)
// Alpha is always 0..1 by construction, so it gets its own direct-mode statistics
// rather than inheriting the colour channels' range.
val chStats = if (c == 3) Stats.of(ch) else stats
channelName(image.channels, c) to
renderRaster(ch, chStats, K.CHANNEL_SIDE, ramp = null, forceRamp = true)
}
}
return buildString {
append("<div style='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px'>")
for ((label, r) in panels) {
append("<div>")
append(rasterFigure(r, checkerboard = false))
append("<div style='color:${K.C_FAINT};font-size:10px;text-align:center;margin-top:2px'>")
append(label)
append("</div></div>")
}
append("</div>")
}
}
Per-channel histogram over the full-resolution samples (not the thumbnail), across the same display range the picture uses, so a bar's position corresponds to a colour above.
source — 73 lines, from line 296
private fun histogramSvg(image: Img, stats: Stats): String {
val lo = stats.displayLo
val hi = stats.displayHi
if (!(hi > lo)) {
return "<div style='color:${K.C_FAINT};font-size:10px;margin-bottom:6px'>histogram: constant image</div>"
}
val bins = K.HIST_BINS
val k = bins / (hi - lo)
val counts = Array(image.channels) { IntArray(bins) }
val n = image.pixelCount
for (p in 0 until n) {
val base = p * image.channels
for (c in 0 until image.channels) {
val v = image.data[base + c]
if (!v.isFinite()) continue
counts[c][(((v - lo) * k).toInt()).coerceIn(0, bins - 1)]++
}
}
val peak = counts.maxOf { arr -> arr.max() }.coerceAtLeast(1)
val bw = K.HIST_W.toDouble() / bins
return buildString {
append("<svg xmlns='http://www.w3.org/2000/svg' width='${K.HIST_W}' height='${K.HIST_H + 12}' ")
append("viewBox='0 0 ${K.HIST_W} ${K.HIST_H + 12}' style='display:block;margin-bottom:4px'>")
append("<rect x='0' y='0' width='${K.HIST_W}' height='${K.HIST_H}' fill='${K.PANEL_BG}' stroke='${K.BORDER}'/>")
// A zero line, when zero is inside the displayed range — the reference a signed image needs.
if (lo < 0f && hi > 0f) {
val zx = fmt(((0f - lo) / (hi - lo) * K.HIST_W).toDouble())
append("<line x1='$zx' y1='0' x2='$zx' y2='${K.HIST_H}' stroke='#555' stroke-dasharray='2 2'/>")
}
for (c in 0 until image.channels) {
val col = channelColor(image.channels, c)
val opacity = if (image.channels == 1) "0.9" else "0.55"
append("<g fill='$col' fill-opacity='$opacity'>")
for (b in 0 until bins) {
val v = counts[c][b]
if (v == 0) continue
// sqrt on the count: a linear axis hides everything next to a background spike.
val hgt = sqrt(v.toDouble() / peak) * (K.HIST_H - 2)
append("<rect x='${fmt(b * bw)}' y='${fmt(K.HIST_H - hgt)}' ")
append("width='${fmt(max(0.8, bw - 0.3))}' height='${fmt(hgt)}'/>")
}
append("</g>")
}
append("<text x='0' y='${K.HIST_H + 10}' fill='${K.C_FAINT}' font-size='9' font-family='monospace'>")
append(num(lo.toDouble()))
append("</text>")
append("<text x='${K.HIST_W}' y='${K.HIST_H + 10}' text-anchor='end' fill='${K.C_FAINT}' ")
append("font-size='9' font-family='monospace'>")
append(num(hi.toDouble()))
append("</text>")
append("<text x='${K.HIST_W / 2}' y='${K.HIST_H + 10}' text-anchor='middle' fill='${K.C_FAINT}' ")
append("font-size='9' font-family='monospace'>")
append("√count, peak ${counts.maxOf { it.max() }}")
append("</text>")
append("</svg>")
}
}
private fun statsTable(image: Img, stats: Stats, raster: Raster): String = buildString {
// flex-wrap, not a run of : `px=43200 samples=129600` is otherwise one unbreakable
// line that pushes the card past its max-width and gets clipped.
append("<div style='color:${K.C_DIM};font-size:10px;line-height:1.5;display:flex;")
append("flex-wrap:wrap;column-gap:10px;max-width:${K.HIST_W}px'>")
append(kv("min", num(stats.min)))
append(kv("max", num(stats.max)))
append(kv("mean", num(stats.mean)))
append(kv("σ", num(stats.sd)))
append(kv("px", "${image.pixelCount}"))
append(kv("samples", "${image.data.size}"))
append(kv("mode", raster.mode))
if (stats.nonFinite > 0) append(kv("non-finite", "${stats.nonFinite}", K.C_BAD))
append("</div>")
}
Gradient
A Gradient is two signed scalar images plus a direction; showing all three is what makes it
readable. dx/dy are drawn with the diverging policy (so a bright-to-dark edge and a
dark-to-bright edge are visibly opposite), alongside the magnitude and the hue-by-direction
image the type already knows how to make.
private fun gradient(g: Gradient): String {
val mag = g.magnitude()
val panels = listOf(
"dx (signed)" to g.dx,
"dy (signed)" to g.dy,
"magnitude" to mag,
"hue = direction" to g.toColorImg()
)
return card {
append(imageRow(panels, K.CHANNEL_SIDE + 20))
append(
caption(
typeToken("Gradient"),
"${g.width}×${g.height}",
"|∇| max ${num(mag.max().toDouble())}"
)
)
}
}
Pyramids
A GaussianPyramid as a ladder of rungs at true relative scale — the whole point of it.
private fun gaussianPyramid(p: GaussianPyramid): String = card {
append(pyramidLadder(p.levels))
append(
caption(
typeToken("GaussianPyramid"),
plural(p.size, "level"),
p.levels.joinToString(" → ") { "${it.width}×${it.height}" }
)
)
}
A LaplacianPyramid as its band-pass rungs (signed, so diverging) plus the low-pass residual. The residual is tagged, because it is the one rung that is not a band.
private fun laplacianPyramid(p: LaplacianPyramid): String = card {
append(pyramidLadder(p.bands + p.residual, residualIndex = p.bands.size))
append(
caption(
typeToken("LaplacianPyramid"),
plural(p.size, "band"),
"residual ${p.residual.width}×${p.residual.height}",
"reconstructs to ${p.bands.firstOrNull()?.let { "${it.width}×${it.height}" } ?: "residual"}"
)
)
}
Lay levels out left to right, each rung sized by its real fraction of the base resolution, bottom-aligned so the halving is the visible fact.
private fun pyramidLadder(levels: List<Img>, residualIndex: Int = -1): String {
val baseW = levels.firstOrNull()?.width?.toDouble() ?: 1.0
return buildString {
append("<div style='display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap;margin-bottom:4px'>")
levels.forEachIndexed { i, level ->
val side = max(8.0, K.PYRAMID_BASE_SIDE * level.width / baseW).roundToInt()
val stats = Stats.of(level)
val r = renderRaster(level, stats, side)
append("<div>")
append(rasterFigure(r, checkerboard = level.channels == 4))
append("<div style='color:${K.C_FAINT};font-size:9px;text-align:center;margin-top:2px'>")
append(if (i == residualIndex) "residual" else "L$i")
append("<br/>${level.width}×${level.height}")
append("</div></div>")
}
append("</div>")
}
}
A plain List<Img> as a filmstrip — the shape a frame sequence or a channel split has.
private fun filmstrip(images: List<Img>): String {
val shown = images.take(10)
return card {
append(imageRow(shown.mapIndexed { i, im -> "[$i]" to im }, K.FILMSTRIP_SIDE))
append(
caption(
typeToken("List<Img>"),
plural(images.size, "image"),
images.map { "${it.width}×${it.height}" }.distinct().take(3).joinToString(", "),
if (images.size > shown.size) "showing first ${shown.size}" else null
)
)
}
}
A labelled row of raster thumbnails, each independently ranged.
private fun imageRow(panels: List<Pair<String, Img>>, side: Int): String = buildString {
append("<div style='display:flex;gap:6px;align-items:flex-start;flex-wrap:wrap;margin-bottom:4px'>")
for ((label, im) in panels) {
val stats = Stats.of(im)
val r = renderRaster(im, stats, side)
append("<div>")
append(rasterFigure(r, checkerboard = im.channels == 4))
append("<div style='color:${K.C_FAINT};font-size:9px;text-align:center;margin-top:2px;max-width:${side + 8}px'>")
append(label)
append("</div></div>")
}
append("</div>")
}
ComponentLabelling
The label map with a hashed per-label palette (adjacent labels get unrelated colours, which is what makes touching blobs distinguishable), plus the area/centroid readout for the largest few.
source — 47 lines, from line 494
private fun componentLabelling(cl: ComponentLabelling): String {
val labels = cl.labels
val side = K.THUMB_MAX_SIDE
val scale = min(1.0, side.toDouble() / max(labels.width, labels.height))
val tw = max(1, (labels.width * scale).roundToInt())
val th = max(1, (labels.height * scale).roundToInt())
// Nearest-neighbour: averaging label ids would invent labels that do not exist.
val px = IntArray(tw * th)
for (y in 0 until th) {
val sy = ((y + 0.5) / th * labels.height).toInt().coerceIn(0, labels.height - 1)
for (x in 0 until tw) {
val sx = ((x + 0.5) / tw * labels.width).toInt().coerceIn(0, labels.width - 1)
val v = labels.data[labels.index(sx, sy, 0)]
px[y * tw + x] = if (!v.isFinite()) rgbInt(K.NON_FINITE) else {
val id = v.roundToInt()
if (id <= 0) 0xFF101216.toInt() else rgbInt(labelColor(id))
}
}
}
val b64 = Base64.getEncoder().encodeToString(afield.media.image.util.PngEncoder.encode(px, tw, th))
val biggest = cl.components.sortedByDescending { it.area }.take(K.MAX_LISTED)
return card {
append(imgTag(b64, tw, th, checkerboard = false))
append(
caption(
typeToken("ComponentLabelling"),
plural(cl.count, "component"),
"${labels.width}×${labels.height}",
cl.largest()?.let { "largest ${it.area}px" }
)
)
if (biggest.isNotEmpty()) {
append(disclosure("component areas & centroids") {
append("<div style='color:${K.C_DIM};font-size:10px;line-height:1.5'>")
for (c in biggest) {
append("<div>#${c.label} area=${c.area} bbox=${c.boundingWidth}×${c.boundingHeight}")
append(" centroid=(${num(c.centroidX)}, ${num(c.centroidY)})")
append(" fill=${num(c.fillRatio)}</div>")
}
if (cl.count > biggest.size) {
append("<div style='color:${K.C_FAINT}'>… +${cl.count - biggest.size} more</div>")
}
append("</div>")
})
}
}
}
Corners
A corner set as a scatter, with detector strength encoded as both radius and viridis colour.
A List<Corner> carries no image extent, so the canvas is the points' own bounding box
unioned with the origin — image coordinates start at (0, 0), so including it keeps the
points where they actually sit in the frame instead of floating them to fill the panel. The
caption states the inferred extent.
private fun corners(corners: List<Corner>): String {
val shown = subsample(corners.sortedByDescending { it.response }, K.MAX_GLYPHS)
val maxX = max(1.0, corners.maxOf { it.x })
val maxY = max(1.0, corners.maxOf { it.y })
val fit = Fit(0.0, 0.0, maxX * 1.02 + 1, maxY * 1.02 + 1)
val rLo = corners.minOf { it.response }
val rHi = corners.maxOf { it.response }
return card {
append(fit.svg {
for (c in shown) {
val t = norm(c.response, rLo, rHi)
val r = K.DOT_R_MIN + (K.DOT_R_MAX - K.DOT_R_MIN) * t
append("<circle cx='${fmt(fit.tx(c.x))}' cy='${fmt(fit.ty(c.y))}' r='${fmt(r)}' ")
append("fill='${hex(Colormaps.sample("viridis", t))}' fill-opacity='0.85' ")
append("stroke='#000' stroke-opacity='0.4' stroke-width='0.5'/>")
}
})
append(
caption(
typeToken("List<Corner>"),
plural(corners.size, "corner"),
"response ${num(rLo)}…${num(rHi)}",
"inferred extent ${fmt(fit.vbW)}×${fmt(fit.vbH)}",
if (shown.size < corners.size) "strongest ${shown.size} drawn" else null
)
)
append(disclosure("strongest corners") {
append("<div style='color:${K.C_DIM};font-size:10px;line-height:1.5'>")
for (c in corners.sortedByDescending { it.response }.take(K.MAX_LISTED)) {
append("<div>(${num(c.x)}, ${num(c.y)}) response=${num(c.response)}</div>")
}
append("</div>")
})
}
}
Hough
Hough lines, twice: clipped into an inferred square canvas, and as the (θ, ρ) accumulator
peaks they actually are.
A HoughLine is (rho, theta) — an infinite line with no extent of its own, which is
why afield.media.image.toFLine insists on being handed the image dimensions. With no image in
reach, the left panel draws each line clipped to a square of side S ≈ 1.41 · max|ρ|, the
smallest square that every peak can still cross; the caption says so, and says how many lines
missed it. Pass the real dimensions to linesToFLines(lines, w, h) when you want them in
place. The right panel is extent-free and therefore exact: ρ against θ, sized by votes.
source — 68 lines, from line 605
private fun houghLines(lines: List<HoughLine>): String {
val maxRho = max(1.0, lines.maxOf { abs(it.rho) })
val side = ceil(maxRho / 0.7071).coerceAtLeast(2.0)
// Two panels side by side must both fit inside the card, so the geometry half is narrowed.
val fit = Fit(0.0, 0.0, side, side, panelW = K.PANEL_W - K.HOUGH_PARAM_W + 8)
val vLo = lines.minOf { it.votes }.toDouble()
val vHi = lines.maxOf { it.votes }.toDouble()
var missed = 0
val shown = subsample(lines.sortedByDescending { it.votes }, K.MAX_GLYPHS)
val geometry = fit.svg {
for (l in shown) {
val seg = l.clipTo(side.toInt(), side.toInt())
if (seg == null) { missed++; continue }
val (a, b) = seg
val t = norm(l.votes.toDouble(), vLo, vHi)
append("<line x1='${fmt(fit.tx(a.x.toDouble()))}' y1='${fmt(fit.ty(a.y.toDouble()))}' ")
append("x2='${fmt(fit.tx(b.x.toDouble()))}' y2='${fmt(fit.ty(b.y.toDouble()))}' ")
append("stroke='${hex(Colormaps.sample("plasma", 0.15 + 0.8 * t))}' ")
append("stroke-width='${fmt(0.7 + 1.6 * t)}' stroke-opacity='0.9'/>")
}
}
val param = buildString {
append("<svg xmlns='http://www.w3.org/2000/svg' width='${K.HOUGH_PARAM_W}' height='${K.HOUGH_PARAM_H}' ")
append("viewBox='0 0 ${K.HOUGH_PARAM_W} ${K.HOUGH_PARAM_H}' ")
append("style='display:block;background:${K.PANEL_BG};border:1px solid ${K.BORDER};border-radius:3px'>")
val rhoLo = lines.minOf { it.rho }
val rhoHi = lines.maxOf { it.rho }
for (l in shown) {
val x = l.theta / Math.PI * (K.HOUGH_PARAM_W - 10) + 5
val y = K.HOUGH_PARAM_H - 5 - norm(l.rho, rhoLo, rhoHi) * (K.HOUGH_PARAM_H - 10)
val t = norm(l.votes.toDouble(), vLo, vHi)
append("<circle cx='${fmt(x)}' cy='${fmt(y)}' r='${fmt(1.4 + 2.6 * t)}' ")
append("fill='${hex(Colormaps.sample("plasma", 0.15 + 0.8 * t))}' fill-opacity='0.9'/>")
}
append("<text x='3' y='${K.HOUGH_PARAM_H - 1}' fill='${K.C_FAINT}' font-size='8' font-family='monospace'>θ=0</text>")
append("<text x='${K.HOUGH_PARAM_W - 3}' y='${K.HOUGH_PARAM_H - 1}' text-anchor='end' fill='${K.C_FAINT}' font-size='8' font-family='monospace'>π</text>")
append("<text x='3' y='9' fill='${K.C_FAINT}' font-size='8' font-family='monospace'>ρ</text>")
append("</svg>")
}
return card {
append("<div style='display:flex;gap:6px;align-items:flex-start;margin-bottom:4px'>")
append("<div>$geometry<div style='color:${K.C_FAINT};font-size:9px;text-align:center;margin-top:2px'>")
append("clipped to an inferred ${fmt(side)}×${fmt(side)} square</div></div>")
append("<div>$param<div style='color:${K.C_FAINT};font-size:9px;text-align:center;margin-top:2px'>")
append("(θ, ρ) peaks</div></div>")
append("</div>")
append(
caption(
typeToken("List<HoughLine>"),
plural(lines.size, "line"),
"votes ${vLo.toInt()}…${vHi.toInt()}",
"no image extent known",
if (missed > 0) "$missed missed the inferred square" else null
)
)
append(disclosure("strongest lines") {
append("<div style='color:${K.C_DIM};font-size:10px;line-height:1.5'>")
for (l in lines.sortedByDescending { it.votes }.take(K.MAX_LISTED)) {
append("<div>ρ=${num(l.rho)} θ=${num(l.theta)} rad ")
append("(${num(Math.toDegrees(l.theta))}°) votes=${l.votes}</div>")
}
append("</div>")
append("<div style='color:${K.C_FAINT};font-size:10px;margin-top:3px'>")
append("draw them in place with <code>linesToFLines(lines, img.width, img.height)</code>")
append("</div>")
})
}
}
Hough circles as actual circles. Unlike a line, a circle has an extent, so the canvas is the union of the circle bounding boxes with the origin — no assumption needed.
private fun houghCircles(circles: List<HoughCircle>): String {
val maxX = max(1.0, circles.maxOf { it.x + it.radius })
val maxY = max(1.0, circles.maxOf { it.y + it.radius })
val fit = Fit(0.0, 0.0, maxX * 1.02 + 1, maxY * 1.02 + 1)
val vLo = circles.minOf { it.votes }.toDouble()
val vHi = circles.maxOf { it.votes }.toDouble()
val shown = subsample(circles.sortedByDescending { it.votes }, K.MAX_GLYPHS)
return card {
append(fit.svg {
for (c in shown) {
val t = norm(c.votes.toDouble(), vLo, vHi)
val col = hex(Colormaps.sample("turbo", 0.1 + 0.85 * t))
append("<circle cx='${fmt(fit.tx(c.x))}' cy='${fmt(fit.ty(c.y))}' ")
append("r='${fmt(max(0.8, c.radius * fit.scale))}' fill='none' ")
append("stroke='$col' stroke-width='${fmt(0.8 + 1.4 * t)}' stroke-opacity='0.95'/>")
append("<circle cx='${fmt(fit.tx(c.x))}' cy='${fmt(fit.ty(c.y))}' r='1.4' fill='$col'/>")
}
})
append(
caption(
typeToken("List<HoughCircle>"),
plural(circles.size, "circle"),
"r ${num(circles.minOf { it.radius })}…${num(circles.maxOf { it.radius })}",
"votes ${vLo.toInt()}…${vHi.toInt()}",
"extent ${fmt(fit.vbW)}×${fmt(fit.vbH)}"
)
)
append(disclosure("strongest circles") {
append("<div style='color:${K.C_DIM};font-size:10px;line-height:1.5'>")
for (c in circles.sortedByDescending { it.votes }.take(K.MAX_LISTED)) {
append("<div>centre (${num(c.x)}, ${num(c.y)}) r=${num(c.radius)} votes=${c.votes}</div>")
}
append("</div>")
})
}
}
FlowField
A sparse optical-flow field as a quiver: one arrow per converged track, hue by direction (the same angle→hue convention Gradient.toColorImg uses), and failed tracks drawn as grey crosses rather than dropped — a tracker that never reports failure is lying, and so is a picture that hides the failures.
Displacements are drawn at true pixel scale unless the median motion is under
FLOW_VISIBLE_FRACTION of the frame diagonal, in which case an integer exaggeration factor is
applied and named in the caption.
source — 57 lines, from line 729
private fun flowField(flow: FlowField): String {
val all = flow.vectors
if (all.isEmpty()) {
return card {
append(caption(typeToken("FlowField"), "no tracks"))
}
}
val tracked = flow.tracked()
val maxX = max(1.0, all.maxOf { max(it.x0, it.x1) })
val maxY = max(1.0, all.maxOf { max(it.y0, it.y1) })
val fit = Fit(0.0, 0.0, maxX * 1.02 + 1, maxY * 1.02 + 1)
val diag = hypot(fit.vbW, fit.vbH)
val median = flow.medianMagnitude()
val exaggeration = if (median > 1e-9 && median < K.FLOW_VISIBLE_FRACTION * diag) {
max(1, (K.FLOW_VISIBLE_FRACTION * diag / median).roundToInt())
} else 1
val (mdx, mdy) = flow.medianDisplacement()
return card {
append(fit.svg {
// Failures first, so arrows draw over them.
for (v in all) {
if (v.ok) continue
val x = fit.tx(v.x0)
val y = fit.ty(v.y0)
append("<path d='M${fmt(x - 2.2)} ${fmt(y - 2.2)}L${fmt(x + 2.2)} ${fmt(y + 2.2)}")
append("M${fmt(x - 2.2)} ${fmt(y + 2.2)}L${fmt(x + 2.2)} ${fmt(y - 2.2)}' ")
append("stroke='#5b606c' stroke-width='0.9'/>")
}
for (v in subsample(tracked, K.MAX_GLYPHS)) {
val dx = v.dx * exaggeration
val dy = v.dy * exaggeration
val x0 = fit.tx(v.x0)
val y0 = fit.ty(v.y0)
val x1 = x0 + dx * fit.scale
val y1 = y0 + dy * fit.scale
val ang = atan2(y1 - y0, x1 - x0)
val head = 4.0
val col = hueColor((atan2(v.dy, v.dx) / (2 * Math.PI) + 1.0) % 1.0)
append("<path d='M${fmt(x0)} ${fmt(y0)}L${fmt(x1)} ${fmt(y1)}")
append("M${fmt(x1)} ${fmt(y1)}L${fmt(x1 - head * cos(ang - 0.4))} ${fmt(y1 - head * sin(ang - 0.4))}")
append("M${fmt(x1)} ${fmt(y1)}L${fmt(x1 - head * cos(ang + 0.4))} ${fmt(y1 - head * sin(ang + 0.4))}' ")
append("stroke='$col' stroke-width='1.2' stroke-linecap='round' fill='none'/>")
append("<circle cx='${fmt(x0)}' cy='${fmt(y0)}' r='1.1' fill='$col' fill-opacity='0.7'/>")
}
})
append(
caption(
typeToken("FlowField"),
"${tracked.size}/${all.size} tracked",
"median |d| ${num(median)}px",
"median d (${num(mdx)}, ${num(mdy)})",
if (exaggeration > 1) "arrows ×$exaggeration" else "arrows at true scale",
if (tracked.size < all.size) "× = failed track" else null
)
)
}
}
Raster rendering — the range policy lives here
Finite-only statistics of an Img, plus the display range the policy derives from them.
source — 63 lines, from line 792
private class Stats(
val min: Double,
val max: Double,
val mean: Double,
val sd: Double,
val nonFinite: Int,
val total: Int
) {
val allFinite: Boolean get() = nonFinite == 0
val anyFinite: Boolean get() = nonFinite < total
/** `true` when some finite sample is negative — the trigger for diverging display. */
val signed: Boolean get() = anyFinite && min < -1e-6
/** `true` when the finite range already fits the displayable `0..1` box. */
val inUnitBox: Boolean get() = anyFinite && min >= -1e-4 && max <= 1f + 1e-4
val displayLo: Float
get() = when {
!anyFinite -> 0f
signed -> -symmetric
inUnitBox -> 0f
else -> min.toFloat()
}
val displayHi: Float
get() = when {
!anyFinite -> 1f
signed -> symmetric
inUnitBox -> 1f
else -> max.toFloat()
}
private val symmetric: Float get() = max(abs(min), abs(max)).toFloat().coerceAtLeast(1e-9f)
fun rangeText(): String = when {
!anyFinite -> "no finite samples"
else -> "range ${num(min)}…${num(max)}"
}
companion object {
fun of(image: Img): Stats {
var mn = Double.POSITIVE_INFINITY
var mx = Double.NEGATIVE_INFINITY
var sum = 0.0
var sumSq = 0.0
var bad = 0
for (v in image.data) {
if (!v.isFinite()) { bad++; continue }
val d = v.toDouble()
if (d < mn) mn = d
if (d > mx) mx = d
sum += d
sumSq += d * d
}
val n = image.data.size - bad
if (n == 0) return Stats(0.0, 0.0, 0.0, 0.0, bad, image.data.size)
val mean = sum / n
val variance = (sumSq / n - mean * mean).coerceAtLeast(0.0)
return Stats(mn, mx, mean, sqrt(variance), bad, image.data.size)
}
}
}
A rendered thumbnail: the PNG payload plus what the policy did to get there.
private class Raster(
val base64: String,
val w: Int,
val h: Int,
val mode: String,
val modeNote: String?
)
Turn an Img into a display-ready PNG under the documented range policy.
@param ramp the Colormaps ramp to use; null means grey.
@param forceRamp take ramp literally. When false (the default) a null ramp is upgraded to
coolwarm for signed 1-channel data, which is the whole point of the diverging mode; the
per-channel strip sets this to true so it can show a genuinely plain grey reading.
source — 65 lines, from line 873
private fun renderRaster(
image: Img,
stats: Stats,
maxSide: Int,
ramp: String? = null,
forceRamp: Boolean = false
): Raster {
val small = fitLongEdgeSanitized(image, maxSide, stats)
val tw = small.width
val th = small.height
val lo = stats.displayLo
val hi = stats.displayHi
val span = (hi - lo).let { if (abs(it) < 1e-12f) 1f else it }
val chosenRamp = if (forceRamp) ramp
else ramp ?: if (image.channels == 1 && stats.signed) "coolwarm" else null
val lut = chosenRamp?.let { name -> Array(256) { rgbInt(Colormaps.sample(name, it / 255.0)) } }
val badRgb = rgbInt(K.NON_FINITE)
val px = IntArray(tw * th)
val ch = small.channels
for (p in 0 until tw * th) {
val i = p * ch
var bad = false
for (c in 0 until ch) if (!small.data[i + c].isFinite()) bad = true
if (bad) { px[p] = badRgb; continue }
px[p] = when {
ch == 1 -> {
val t = ((small.data[i] - lo) / span).coerceIn(0f, 1f)
if (lut != null) lut[(t * 255f).toInt().coerceIn(0, 255)]
else {
val v = q8(t)
(255 shl 24) or (v shl 16) or (v shl 8) or v
}
}
else -> {
val r = q8(((small.data[i] - lo) / span))
val g = q8(((small.data[i + 1] - lo) / span))
val b = q8(((small.data[i + 2] - lo) / span))
// Alpha is a coverage, never a signal: it is clamped, never rescaled.
val a = if (ch == 4) q8(small.data[i + 3]) else 255
(a shl 24) or (r shl 16) or (g shl 8) or b
}
}
}
val mode = when {
!stats.anyFinite -> "no finite data"
stats.signed -> "signed ±${num(max(abs(stats.min), abs(stats.max)))}"
stats.inUnitBox -> "direct"
else -> "auto-ranged"
}
val note = buildList {
when {
!stats.anyFinite -> add("every sample is non-finite")
stats.signed -> add("signed → ±${num(max(abs(stats.min), abs(stats.max)))}" +
(if (chosenRamp != null) " <i>$chosenRamp</i>" else ""))
stats.inUnitBox -> {}
else -> add("auto-ranged for display")
}
if (chosenRamp != null && !stats.signed) add("<i>$chosenRamp</i>")
if (stats.nonFinite > 0) add("${stats.nonFinite} non-finite (magenta)")
}.joinToString(" · ").ifBlank { null }
return Raster(
base64 = Base64.getEncoder().encodeToString(afield.media.image.util.PngEncoder.encode(px, tw, th)),
w = tw, h = th, mode = mode, modeNote = note
)
}
Downsample to fit maxSide on the long edge. Non-finite samples are replaced by the finite
mean before resampling — the area-average filter would otherwise smear one NaN across a
whole neighbourhood — and the original non-finite pixels are re-stamped afterwards by
nearest-neighbour, so the magenta lands where the bad data actually is.
private fun fitLongEdgeSanitized(image: Img, maxSide: Int, stats: Stats): Img {
if (stats.allFinite) return fitLongEdge(image, maxSide)
val fill = if (stats.anyFinite) stats.mean.toFloat() else 0f
val clean = Img(
image.width, image.height, image.channels,
FloatArray(image.data.size) { if (image.data[it].isFinite()) image.data[it] else fill }
)
val small = fitLongEdge(clean, maxSide)
if (small.width == image.width && small.height == image.height) {
// No resampling happened, so restore the exact non-finite pattern.
for (i in image.data.indices) if (!image.data[i].isFinite()) small.data[i] = image.data[i]
return small
}
for (y in 0 until small.height) {
val sy = ((y + 0.5) / small.height * image.height).toInt().coerceIn(0, image.height - 1)
for (x in 0 until small.width) {
val sx = ((x + 0.5) / small.width * image.width).toInt().coerceIn(0, image.width - 1)
for (c in 0 until image.channels) {
val v = image.data[image.index(sx, sy, c)]
if (!v.isFinite()) small.data[small.index(x, y, c)] = v
}
}
}
return small
}
Aspect-preserving downsample to at most maxSide on the long edge; never upsamples.
private fun fitLongEdge(image: Img, maxSide: Int): Img {
val scale = min(1.0, maxSide.toDouble() / max(image.width, image.height))
if (scale >= 1.0) return image
val tw = max(1, (image.width * scale).roundToInt())
val th = max(1, (image.height * scale).roundToInt())
if (tw == image.width && th == image.height) return image
return image.resize(tw, th)
}
One channel of image as a 1-channel image, without going through Img.channel's checks.
private fun extractChannel(image: Img, c: Int): Img {
val out = FloatArray(image.pixelCount)
for (p in 0 until image.pixelCount) out[p] = image.data[p * image.channels + c]
return Img(image.width, image.height, 1, out)
}
SVG geometry panels
A data-space → panel-space fit. Everything is emitted in panel pixels rather than through an
SVG viewBox, so a marker radius or a stroke width means the same thing at any data scale.
Y is not flipped: image coordinates and SVG coordinates both run downwards.
The members are spelled out (panelW, not w) for a reason. Inside svg's buildString the
innermost implicit receiver is a StringBuilder, and this package declares
val Any?.h: String — so a "height='$h'" template silently resolved to
ValueVisualizer.visualize(theStringBuilder) and emitted a rendered value tree in the middle
of the <svg> tag. It compiled, the unit tests still found their <circle> substrings, and
only looking at the capture showed the panel was gone.
private class Fit(
val vbX: Double,
val vbY: Double,
val vbW: Double,
val vbH: Double,
val panelW: Int = K.PANEL_W
) {
val panelH: Int = (panelW * vbH / vbW).roundToInt().coerceIn(K.PANEL_H_MIN, K.PANEL_H_MAX)
/** Uniform data→panel scale, with a small inset so glyphs on the border are not clipped. */
val scale: Double = min((panelW - 8.0) / vbW, (panelH - 8.0) / vbH)
private val ox = (panelW - vbW * scale) / 2.0
private val oy = (panelH - vbH * scale) / 2.0
fun tx(x: Double): Double = (x - vbX) * scale + ox
fun ty(y: Double): Double = (y - vbY) * scale + oy
inline fun svg(body: StringBuilder.() -> Unit): String = buildString {
append("<svg xmlns='http://www.w3.org/2000/svg' ")
append("width='${panelW}' height='${panelH}' viewBox='0 0 ${panelW} ${panelH}' ")
append("style='display:block;background:${K.PANEL_BG};border:1px solid ${K.BORDER};border-radius:3px'>")
// The data rectangle, so "where in the frame" is legible even for a sparse scatter.
append("<rect x='${fmt(ox)}' y='${fmt(oy)}' ")
append("width='${fmt(vbW * scale)}' height='${fmt(vbH * scale)}' ")
append("fill='none' stroke='#262a33'/>")
body()
append("</svg>")
}
}
Small HTML helpers
private inline fun card(body: StringBuilder.() -> Unit): String = buildString {
append("<div style='display:inline-block;max-width:340px;font-family:monospace;font-size:11px;line-height:1.4'>")
body()
append("</div>")
}
private fun typeToken(name: String): String = "<span style='color:${K.C_HEAD}'>$name</span>"
private fun caption(vararg parts: String?): String = buildString {
append("<div style='color:${K.C_DIM};margin-top:3px'>")
append(parts.filterNotNull().filter { it.isNotBlank() }.joinToString(" · "))
append("</div>")
}
private inline fun disclosure(summary: String, body: StringBuilder.() -> Unit): String = buildString {
append("<details style='margin-top:4px'>")
append("<summary style='cursor:pointer;color:${K.C_FAINT};font-size:10px;outline:none'>")
append(summary)
append("</summary>")
append("<div style='margin-top:4px'>")
body()
append("</div></details>")
}
private fun rasterFigure(r: Raster, checkerboard: Boolean): String = imgTag(r.base64, r.w, r.h, checkerboard)
The <img> itself. A 4-channel image sits on a CSS checkerboard so transparent black and
opaque black are not the same picture, and image-rendering:pixelated keeps a small thumbnail
exact about where its pixel edges are.
private fun imgTag(base64: String, w: Int, h: Int, checkerboard: Boolean): String = buildString {
val bg = if (checkerboard) {
"background-color:#3a3f4b;" +
"background-image:linear-gradient(45deg,#22252d 25%,transparent 25%,transparent 75%,#22252d 75%)," +
"linear-gradient(45deg,#22252d 25%,transparent 25%,transparent 75%,#22252d 75%);" +
"background-size:12px 12px;background-position:0 0,6px 6px;"
} else ""
append("<div style='display:inline-block;line-height:0;border:1px solid ${K.BORDER};border-radius:3px;$bg'>")
append("<img src='data:image/png;base64,$base64' width='$w' height='$h' ")
append("style='image-rendering:pixelated;display:block'/>")
append("</div>")
}
private fun kv(key: String, value: String, color: String = K.C_ACCENT): String =
"<span style='white-space:nowrap'><span style='color:${K.C_FAINT}'>$key</span>=" +
"<span style='color:$color'>$value</span></span>"
Numeric / colour helpers
private fun channelWord(channels: Int): String = when (channels) {
1 -> "gray"
3 -> "rgb"
else -> "rgba"
}
private fun channelName(channels: Int, c: Int): String =
if (channels == 1) "v" else listOf("R", "G", "B", "A")[c]
private fun channelColor(channels: Int, c: Int): String = when {
channels == 1 -> K.C_OK
else -> listOf("#f66", "#6f6", "#69f", "#ccc")[c]
}
A stable, well-separated colour per component label: golden-ratio hue rotation.
private fun labelColor(id: Int): Color4 = hsv((id * 0.61803398875) % 1.0, 0.72, 0.95)
private fun hueColor(hue: Double): String = hex(hsv(hue, 0.85, 1.0))
private fun hsv(h: Double, s: Double, v: Double): Color4 {
val hh = ((h % 1.0) + 1.0) % 1.0 * 6.0
val i = hh.toInt()
val f = hh - i
val p = v * (1 - s)
val q = v * (1 - s * f)
val t = v * (1 - s * (1 - f))
return when (i % 6) {
0 -> Color4(v, t, p)
1 -> Color4(q, v, p)
2 -> Color4(p, v, t)
3 -> Color4(p, q, v)
4 -> Color4(t, p, v)
else -> Color4(v, p, q)
}
}
private fun hex(c: Color4): String =
"#%02x%02x%02x".format(q8(c.r.toFloat()), q8(c.g.toFloat()), q8(c.b.toFloat()))
private fun rgbInt(c: Color4): Int =
(255 shl 24) or (q8(c.r.toFloat()) shl 16) or (q8(c.g.toFloat()) shl 8) or q8(c.b.toFloat())
private fun q8(v: Float): Int = (v.coerceIn(0f, 1f) * 255f + 0.5f).toInt().coerceIn(0, 255)
private fun norm(v: Double, lo: Double, hi: Double): Double =
if (hi - lo < 1e-12) 0.5 else ((v - lo) / (hi - lo)).coerceIn(0.0, 1.0)
Evenly thin a list down to at most limit entries, keeping the first (i.e. the strongest).
private fun <T> subsample(items: List<T>, limit: Int): List<T> {
if (items.size <= limit) return items
val step = items.size.toDouble() / limit
return (0 until limit).map { items[(it * step).toInt().coerceIn(0, items.size - 1)] }
}
Compact fixed-point for SVG coordinates.
private fun fmt(v: Double): String {
val r = (v * 100.0).roundToInt() / 100.0
return if (r == r.toLong().toDouble()) r.toLong().toString() else r.toString()
}
private fun fmt(v: Int): String = v.toString()
Human number: fixed-point in the middle of the range, scientific at the extremes.
private fun num(v: Double): String = when {
v.isNaN() -> "NaN"
v == Double.POSITIVE_INFINITY -> "∞"
v == Double.NEGATIVE_INFINITY -> "-∞"
v == 0.0 -> "0"
abs(v) >= 1e6 || abs(v) < 1e-4 -> "%.2e".format(v)
abs(v) >= 1000 -> "%.0f".format(v)
else -> "%.4f".format(v).trimEnd('0').trimEnd('.')
}
private fun num(v: Float): String = num(v.toDouble())
"1 circle", "4 circles" — a detector that finds exactly one thing should not stutter.
private fun plural(n: Int, singular: String): String =
if (n == 1) "1 $singular" else "$n ${singular}s"
}