///| Feature extraction: turning a segmented glyph into a fixed-size vector so
///| that different glyphs can be compared numerically. The scheme downsamples
///| the glyph's bounding box onto a square grid, where each cell records
///| whether ink covers at least half of it. The flattened grid is the feature
///| vector.
///|
///| The same convention as elsewhere holds: pixel values below `128` are ink.
///| Normalize the pixels of `c` onto a `size`-by-`size` grid, returning
///| `size * size` cells in row-major order. Each cell is `255` when at least
///| half of the source pixels it covers are ink, otherwise `0`.
///|
///| The glyph is scaled to fit the grid while preserving its aspect ratio and
///| centered with paper padding, so a thin glyph stays thin instead of being
///|
/// stretched out of shape.
pub fn glyph_grid(img : Image, c : Component, size : Int) -> Array[Int] {
let grid = Array::make(size * size, 0)
let stride = img.width
let long_side = if c.w > c.h { c.w } else { c.h }
let gw = if c.w * size / long_side < 1 { 1 } else { c.w * size / long_side }
let gh = if c.h * size / long_side < 1 { 1 } else { c.h * size / long_side }
let off_x = (size - gw) / 2
let off_y = (size - gh) / 2
for oy = 0; oy < size; oy = oy + 1 {
let gy = oy - off_y
if gy < 0 || gy >= gh {
continue
}
let sy0 = c.y + gy * c.h / gh
let sy1_floor = c.y + (gy + 1) * c.h / gh
let sy1 = if sy1_floor > sy0 { sy1_floor } else { sy0 + 1 }
for ox = 0; ox < size; ox = ox + 1 {
let gx = ox - off_x
if gx < 0 || gx >= gw {
continue
}
let sx0 = c.x + gx * c.w / gw
let sx1_floor = c.x + (gx + 1) * c.w / gw
let sx1 = if sx1_floor > sx0 { sx1_floor } else { sx0 + 1 }
let mut ink = 0
let mut total = 0
for sy = sy0; sy < sy1; sy = sy + 1 {
for sx = sx0; sx < sx1; sx = sx + 1 {
total = total + 1
if img.pixels.at(sy * stride + sx).to_int() < 128 {
ink = ink + 1
}
}
}
if ink * 2 >= total {
grid[oy * size + ox] = 255
}
}
}
grid
}
///| The normalized feature grid of the built-in glyph for `c`, extracted by
///|
///| rendering it and running it through the same `glyph_grid` path used for
///| real input. Using this as a reference keeps templates in the exact feature
///| space the classifier sees, so a clean rendering matches itself at distance
///|
/// zero even when the raw font bitmap is not horizontally symmetric.
pub fn char_grid(c : Char) -> Array[Int] {
let img = render_rows(char_rows(c), 4)
let cs = merge_parts(connected_components(img))
if cs.length() == 0 {
Array::make(64, 0)
} else {
glyph_grid(img, cs[0], 8)
}
}