2 * Conversions between browser image data (RGBA, row-major, 0-255 bytes) and
3 * numbl tensors (doubles, column-major, MATLAB image convention).
4 *
5 * Inside a numbl script an image is an `H x W x 3` array of doubles in the
6 * range 0-255, exactly like `imread` returns (but as doubles, not uint8).
7 *
8 * - Browser ImageData: Uint8ClampedArray, length W*H*4, RGBA, ROW-major.
9 * Pixel (x, y) red channel is at index (y * W + x) * 4.
10 * - numbl tensor: Float64Array, COLUMN-major. Element (i, j, c) of an
11 * [H, W, 3] array is at index i + j*H + c*H*W (i = row/y, j = col/x).
12 */
14import type { RuntimeValue, RuntimeTensor } from "numbl";
16export interface RawImage {
17 width: number;
18 height: number;
19 /** RGBA bytes, row-major (W*H*4). */
20 rgba: Uint8ClampedArray;
21}
23export interface TensorInput {
24 data: Float64Array;
25 shape: number[];
26}
28/** RGBA row-major bytes -> column-major [H, W, 3] doubles (0-255). */
29export function rgbaToTensorData(
30 rgba: Uint8ClampedArray | Uint8Array,
31 width: number,
32 height: number
33): TensorInput {
34 const W = width;
35 const H = height;
36 const plane = H * W;
37 const data = new Float64Array(plane * 3);
38 for (let y = 0; y < H; y++) {
39 for (let x = 0; x < W; x++) {
40 const src = (y * W + x) * 4;
41 const base = y + x * H; // column-major (row=y, col=x)
42 data[base] = rgba[src];
43 data[base + plane] = rgba[src + 1];
44 data[base + 2 * plane] = rgba[src + 2];
45 }
46 }
47 return { data, shape: [H, W, 3] };
48}
50function describeValue(v: RuntimeValue | undefined): string {
51 if (v === undefined) return "nothing";
52 if (typeof v === "number") return `a scalar (${v})`;
53 if (typeof v === "boolean") return "a logical scalar";
54 if (typeof v === "string") return "a string";
55 const kind = (v as { kind?: string }).kind;
56 return kind ? `a ${kind}` : "an unsupported value";
57}
59function isTensor(v: RuntimeValue | undefined): v is RuntimeTensor {
60 return (
61 typeof v === "object" &&
62 v !== null &&
63 (v as { kind?: string }).kind === "tensor"
64 );
65}
67/**
68 * A numbl output value -> RGBA row-major bytes.
69 *
70 * Accepts:
71 * - [H, W, 3] color image
72 * - [H, W] or [H, W, 1] grayscale (replicated to RGB)
73 *
74 * Values are clamped to 0-255 (Uint8ClampedArray rounds + clamps). The
75 * imaginary part of a complex result is ignored.
76 */
77export function tensorToRaw(v: RuntimeValue | undefined): RawImage {
78 if (!isTensor(v)) {
79 throw new Error(
80 `filterImage must return an H x W x 3 image array, but returned ${describeValue(v)}.`
81 );
82 }
83 const shape = v.shape;
84 if (shape.length < 2) {
85 throw new Error(
86 `filterImage returned a ${shape.length}-D array; expected a 2-D (grayscale) or 3-D (RGB) image.`
87 );
88 }
89 const H = shape[0];
90 const W = shape[1];
91 const C = shape.length >= 3 ? shape[2] : 1;
92 const data = v.data;
93 const plane = H * W;
94 const rgba = new Uint8ClampedArray(W * H * 4);
95 for (let y = 0; y < H; y++) {
96 for (let x = 0; x < W; x++) {
97 const dst = (y * W + x) * 4;
98 const base = y + x * H;
99 let r: number, g: number, b: number;
100 if (C >= 3) {
101 r = data[base];
102 g = data[base + plane];
103 b = data[base + 2 * plane];
104 } else {
105 r = g = b = data[base];
106 }
107 // Uint8ClampedArray assignment rounds to nearest and clamps to 0-255.
108 rgba[dst] = r;
109 rgba[dst + 1] = g;
110 rgba[dst + 2] = b;
111 rgba[dst + 3] = 255;
112 }
113 }
114 return { width: W, height: H, rgba };
115}
117/**
118 * Pull the name of the primary function out of a `.m` script, so we can call
119 * it (e.g. `out = filterImage(img);`). Returns null if the script has no
120 * function header (treated as a plain script using `img` / `out`).
121 */
122export function extractFunctionName(script: string): string | null {
123 // function out = name(...) | function [a,b] = name(...) | function name(...)
124 const m = script.match(
125 /function\s+(?:[\w\s,[\]]*?=\s*)?([A-Za-z]\w*)\s*\(/
126 );
127 return m ? m[1] : null;
128}