flybrain/packages/brain/src/model/retina.ts
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

67 lines
2.6 KiB
TypeScript

/**
* Retina projection: an RGBA image of any size onto a population of input columns.
*
* Columns carry 2D coordinates in arbitrary dataset units. Each call re-derives the column
* bounding box (columns are immutable in practice, but the original kernel recomputed the bounds
* per frame and the arithmetic is reproduced here), normalizes every column into [0, 1], mirrors
* the X axis for hemisphere 0, and samples one pixel with nearest-neighbour rounding.
*/
export interface RetinaConfig {
/** Membrane drive per unit luminance. */
gain: number;
/** Default frame width in pixels. */
width: number;
/** Default frame height in pixels. */
height: number;
}
/** Original constants: a Game Boy sized frame at 0.20 drive per unit luminance. */
export const DEFAULT_RETINA_CONFIG: RetinaConfig = { gain: 0.20, width: 160, height: 144 };
/** Retina column geometry, normally a view onto a dataset's visual arrays. */
export interface RetinaColumns {
/** Interleaved x,y coordinates in dataset units, length >= 2 * count. */
xy: Float32Array;
/** 0 = left (mirrored on X), 1 = right. */
hemisphere: Uint8Array;
/** Number of columns to project. */
count: number;
}
/** Rec. 709 luminance weights, matching the original kernel. */
const RED = 0.2126;
const GREEN = 0.7152;
const BLUE = 0.0722;
/**
* Project one RGBA frame onto `out` (drive per column). `out` must have at least `columns.count`
* entries; entries beyond the column count are left untouched.
*/
export function projectFrame(
rgba: Uint8Array,
width: number,
height: number,
columns: { xy: Float32Array; hemisphere: Uint8Array; count: number },
gain: number,
out: Float32Array,
): void {
const { xy, hemisphere, count } = columns;
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (let i = 0; i < count; i++) {
minX = Math.min(minX, xy[i * 2]); maxX = Math.max(maxX, xy[i * 2]);
minY = Math.min(minY, xy[i * 2 + 1]); maxY = Math.max(maxY, xy[i * 2 + 1]);
}
const lastX = width - 1;
const lastY = height - 1;
for (let i = 0; i < count; i++) {
let normalizedX = (xy[i * 2] - minX) / (maxX - minX || 1);
if (hemisphere[i] === 0) normalizedX = 1 - normalizedX;
const normalizedY = (xy[i * 2 + 1] - minY) / (maxY - minY || 1);
const x = Math.max(0, Math.min(lastX, Math.round(normalizedX * lastX)));
const y = Math.max(0, Math.min(lastY, Math.round(normalizedY * lastY)));
const offset = (y * width + x) * 4;
const luminance = (rgba[offset] * RED + rgba[offset + 1] * GREEN + rgba[offset + 2] * BLUE) / 255;
out[i] = luminance * gain;
}
}