/** * Image utility functions for the recognition pipeline. * Provides resize, rotation, and pixel manipulation helpers. */ export interface Point { x: number; y: number; } export interface Size { width: number; height: number; } /** * Resize RGBA pixel data to target dimensions using bilinear interpolation. */ export function resizeImage( pixels: Uint8Array | Uint8ClampedArray, srcWidth: number, srcHeight: number, dstWidth: number, dstHeight: number ): Uint8Array { const result = new Uint8Array(dstWidth * dstHeight * 4); const xRatio = srcWidth / dstWidth; const yRatio = srcHeight / dstHeight; for (let y = 0; y < dstHeight; y++) { for (let x = 0; x < dstWidth; x++) { const srcX = x * xRatio; const srcY = y * yRatio; const x0 = Math.floor(srcX); const y0 = Math.floor(srcY); const x1 = Math.min(x0 + 1, srcWidth - 1); const y1 = Math.min(y0 + 1, srcHeight - 1); const xFrac = srcX - x0; const yFrac = srcY - y0; const dstIdx = (y * dstWidth + x) * 4; for (let c = 0; c < 4; c++) { const idx00 = (y0 * srcWidth + x0) * 4 + c; const idx10 = (y0 * srcWidth + x1) * 4 + c; const idx01 = (y1 * srcWidth + x0) * 4 + c; const idx11 = (y1 * srcWidth + x1) * 4 + c; const top = pixels[idx00] + (pixels[idx10] - pixels[idx00]) * xFrac; const bottom = pixels[idx01] + (pixels[idx11] - pixels[idx01]) * xFrac; result[dstIdx + c] = Math.round(top + (bottom - top) * yFrac); } } } return result; } /** * Rotate RGBA pixel data by 90, 180, or 270 degrees. */ export function rotateImage( pixels: Uint8Array | Uint8ClampedArray, width: number, height: number, degrees: 0 | 90 | 180 | 270 ): { pixels: Uint8Array; width: number; height: number } { if (degrees === 0) { return { pixels: new Uint8Array(pixels), width, height }; } const [newWidth, newHeight] = degrees === 90 || degrees === 270 ? [height, width] : [width, height]; const result = new Uint8Array(newWidth * newHeight * 4); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const srcIdx = (y * width + x) * 4; let dstX: number, dstY: number; switch (degrees) { case 90: dstX = height - 1 - y; dstY = x; break; case 180: dstX = width - 1 - x; dstY = height - 1 - y; break; case 270: dstX = y; dstY = width - 1 - x; break; default: dstX = x; dstY = y; } const dstIdx = (dstY * newWidth + dstX) * 4; result[dstIdx] = pixels[srcIdx]; result[dstIdx + 1] = pixels[srcIdx + 1]; result[dstIdx + 2] = pixels[srcIdx + 2]; result[dstIdx + 3] = pixels[srcIdx + 3]; } } return { pixels: result, width: newWidth, height: newHeight }; } /** * Convert RGBA to grayscale using luminance formula. */ export function toGrayscale(pixels: Uint8Array | Uint8ClampedArray): Uint8Array { const numPixels = pixels.length / 4; const gray = new Uint8Array(numPixels); for (let i = 0; i < numPixels; i++) { const idx = i * 4; gray[i] = Math.round( 0.299 * pixels[idx] + 0.587 * pixels[idx + 1] + 0.114 * pixels[idx + 2] ); } return gray; } /** * Convert grayscale back to RGBA. */ export function grayscaleToRgba( gray: Uint8Array, width: number, height: number ): Uint8Array { const rgba = new Uint8Array(width * height * 4); for (let i = 0; i < gray.length; i++) { const idx = i * 4; rgba[idx] = gray[i]; rgba[idx + 1] = gray[i]; rgba[idx + 2] = gray[i]; rgba[idx + 3] = 255; } return rgba; } /** * Apply Gaussian blur to grayscale image. * Uses separable 1D convolution for efficiency. */ export function gaussianBlur( gray: Uint8Array, width: number, height: number, radius: number = 5 ): Uint8Array { // Generate Gaussian kernel const sigma = radius / 2; const size = radius * 2 + 1; const kernel = new Float32Array(size); let sum = 0; for (let i = 0; i < size; i++) { const x = i - radius; kernel[i] = Math.exp(-(x * x) / (2 * sigma * sigma)); sum += kernel[i]; } // Normalize for (let i = 0; i < size; i++) { kernel[i] /= sum; } // Horizontal pass const temp = new Uint8Array(width * height); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let value = 0; for (let k = -radius; k <= radius; k++) { const sx = Math.max(0, Math.min(width - 1, x + k)); value += gray[y * width + sx] * kernel[k + radius]; } temp[y * width + x] = Math.round(value); } } // Vertical pass const result = new Uint8Array(width * height); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let value = 0; for (let k = -radius; k <= radius; k++) { const sy = Math.max(0, Math.min(height - 1, y + k)); value += temp[sy * width + x] * kernel[k + radius]; } result[y * width + x] = Math.round(value); } } return result; } /** * Calculate distance between two points. */ export function distance(a: Point, b: Point): number { const dx = b.x - a.x; const dy = b.y - a.y; return Math.sqrt(dx * dx + dy * dy); } /** * Calculate cross product of vectors (b-a) and (c-b). */ export function crossProduct(a: Point, b: Point, c: Point): number { return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); } /** * Calculate point-to-line distance. */ export function pointToLineDistance(point: Point, lineStart: Point, lineEnd: Point): number { const dx = lineEnd.x - lineStart.x; const dy = lineEnd.y - lineStart.y; const lengthSquared = dx * dx + dy * dy; if (lengthSquared === 0) { return distance(point, lineStart); } let t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / lengthSquared; t = Math.max(0, Math.min(1, t)); const projection = { x: lineStart.x + t * dx, y: lineStart.y + t * dy, }; return distance(point, projection); } /** * Sample a pixel from RGBA data using bilinear interpolation. */ export function sampleBilinear( pixels: Uint8Array | Uint8ClampedArray, width: number, height: number, x: number, y: number ): [number, number, number, number] { // Clamp to valid range x = Math.max(0, Math.min(width - 1, x)); y = Math.max(0, Math.min(height - 1, y)); const x0 = Math.floor(x); const y0 = Math.floor(y); const x1 = Math.min(x0 + 1, width - 1); const y1 = Math.min(y0 + 1, height - 1); const xFrac = x - x0; const yFrac = y - y0; const idx00 = (y0 * width + x0) * 4; const idx10 = (y0 * width + x1) * 4; const idx01 = (y1 * width + x0) * 4; const idx11 = (y1 * width + x1) * 4; const result: [number, number, number, number] = [0, 0, 0, 0]; for (let c = 0; c < 4; c++) { const c00 = pixels[idx00 + c]; const c10 = pixels[idx10 + c]; const c01 = pixels[idx01 + c]; const c11 = pixels[idx11 + c]; const top = c00 + (c10 - c00) * xFrac; const bottom = c01 + (c11 - c01) * xFrac; result[c] = Math.round(top + (bottom - top) * yFrac); } return result; }