/** * Conversions between browser image data (RGBA, row-major, 0-255 bytes) and * numbl tensors (doubles, column-major, MATLAB image convention). * * Inside a numbl script an image is an `H x W x 3` array of doubles in the * range 0-255, exactly like `imread` returns (but as doubles, not uint8). * * - Browser ImageData: Uint8ClampedArray, length W*H*4, RGBA, ROW-major. * Pixel (x, y) red channel is at index (y * W + x) * 4. * - numbl tensor: Float64Array, COLUMN-major. Element (i, j, c) of an * [H, W, 3] array is at index i + j*H + c*H*W (i = row/y, j = col/x). */ import type { RuntimeValue, RuntimeTensor } from "numbl"; export interface RawImage { width: number; height: number; /** RGBA bytes, row-major (W*H*4). */ rgba: Uint8ClampedArray; } export interface TensorInput { data: Float64Array; shape: number[]; } /** RGBA row-major bytes -> column-major [H, W, 3] doubles (0-255). */ export function rgbaToTensorData( rgba: Uint8ClampedArray | Uint8Array, width: number, height: number ): TensorInput { const W = width; const H = height; const plane = H * W; const data = new Float64Array(plane * 3); for (let y = 0; y < H; y++) { for (let x = 0; x < W; x++) { const src = (y * W + x) * 4; const base = y + x * H; // column-major (row=y, col=x) data[base] = rgba[src]; data[base + plane] = rgba[src + 1]; data[base + 2 * plane] = rgba[src + 2]; } } return { data, shape: [H, W, 3] }; } function describeValue(v: RuntimeValue | undefined): string { if (v === undefined) return "nothing"; if (typeof v === "number") return `a scalar (${v})`; if (typeof v === "boolean") return "a logical scalar"; if (typeof v === "string") return "a string"; const kind = (v as { kind?: string }).kind; return kind ? `a ${kind}` : "an unsupported value"; } function isTensor(v: RuntimeValue | undefined): v is RuntimeTensor { return ( typeof v === "object" && v !== null && (v as { kind?: string }).kind === "tensor" ); } /** * A numbl output value -> RGBA row-major bytes. * * Accepts: * - [H, W, 3] color image * - [H, W] or [H, W, 1] grayscale (replicated to RGB) * * Values are clamped to 0-255 (Uint8ClampedArray rounds + clamps). The * imaginary part of a complex result is ignored. */ export function tensorToRaw(v: RuntimeValue | undefined): RawImage { if (!isTensor(v)) { throw new Error( `filterImage must return an H x W x 3 image array, but returned ${describeValue(v)}.` ); } const shape = v.shape; if (shape.length < 2) { throw new Error( `filterImage returned a ${shape.length}-D array; expected a 2-D (grayscale) or 3-D (RGB) image.` ); } const H = shape[0]; const W = shape[1]; const C = shape.length >= 3 ? shape[2] : 1; const data = v.data; const plane = H * W; const rgba = new Uint8ClampedArray(W * H * 4); for (let y = 0; y < H; y++) { for (let x = 0; x < W; x++) { const dst = (y * W + x) * 4; const base = y + x * H; let r: number, g: number, b: number; if (C >= 3) { r = data[base]; g = data[base + plane]; b = data[base + 2 * plane]; } else { r = g = b = data[base]; } // Uint8ClampedArray assignment rounds to nearest and clamps to 0-255. rgba[dst] = r; rgba[dst + 1] = g; rgba[dst + 2] = b; rgba[dst + 3] = 255; } } return { width: W, height: H, rgba }; } /** * Pull the name of the primary function out of a `.m` script, so we can call * it (e.g. `out = filterImage(img);`). Returns null if the script has no * function header (treated as a plain script using `img` / `out`). */ export function extractFunctionName(script: string): string | null { // function out = name(...) | function [a,b] = name(...) | function name(...) const m = script.match( /function\s+(?:[\w\s,[\]]*?=\s*)?([A-Za-z]\w*)\s*\(/ ); return m ? m[1] : null; }