// convolution.ts - Convolution operations for 1D and 2D data export interface ConvolutionOptions { mode?: 'full' | 'same' | 'valid'; boundary?: 'zero' | 'reflect' | 'symmetric'; } export interface ConvolutionResult1D { values: number[]; originalLength: number; kernelLength: number; mode: string; } export interface ConvolutionResult2D { matrix: number[][]; originalDimensions: [number, number]; kernelDimensions: [number, number]; mode: string; } /** * Validates input array for convolution operations */ function validateArray(arr: number[], name: string): void { if (!Array.isArray(arr) || arr.length === 0) { throw new Error(`${name} must be a non-empty array`); } if (arr.some(val => typeof val !== 'number' || !isFinite(val))) { throw new Error(`${name} must contain only finite numbers`); } } /** * Validates 2D matrix for convolution operations */ function validateMatrix(matrix: number[][], name: string): void { if (!Array.isArray(matrix) || matrix.length === 0) { throw new Error(`${name} must be a non-empty 2D array`); } const rowLength = matrix[0].length; if (rowLength === 0) { throw new Error(`${name} rows must be non-empty`); } for (let i = 0; i < matrix.length; i++) { if (!Array.isArray(matrix[i]) || matrix[i].length !== rowLength) { throw new Error(`${name} must be a rectangular matrix`); } if (matrix[i].some(val => typeof val !== 'number' || !isFinite(val))) { throw new Error(`${name} must contain only finite numbers`); } } } /** * Applies boundary conditions to extend an array */ function applyBoundary1D(signal: number[], padding: number, boundary: string): number[] { if (padding <= 0) return signal; let result = [...signal]; switch (boundary) { case 'zero': result = new Array(padding).fill(0).concat(result).concat(new Array(padding).fill(0)); break; case 'reflect': const leftPad = []; const rightPad = []; for (let i = 0; i < padding; i++) { leftPad.unshift(signal[Math.min(i + 1, signal.length - 1)]); rightPad.push(signal[Math.max(signal.length - 2 - i, 0)]); } result = leftPad.concat(result).concat(rightPad); break; case 'symmetric': const leftSymPad = []; const rightSymPad = []; for (let i = 0; i < padding; i++) { leftSymPad.unshift(signal[Math.min(i, signal.length - 1)]); rightSymPad.push(signal[Math.max(signal.length - 1 - i, 0)]); } result = leftSymPad.concat(result).concat(rightSymPad); break; default: throw new Error(`Unsupported boundary condition: ${boundary}`); } return result; } /** * Performs 1D convolution between signal and kernel * * @param signal - Input signal array * @param kernel - Convolution kernel array * @param options - Convolution options (mode, boundary) * @returns Convolution result with metadata */ /** * [CORRECTED] Performs 1D convolution between signal and kernel */ export function convolve1D( signal: number[], kernel: number[], options: ConvolutionOptions = {} ): ConvolutionResult1D { validateArray(signal, 'Signal'); validateArray(kernel, 'Kernel'); const { mode = 'full', boundary = 'zero' } = options; const flippedKernel = [...kernel].reverse(); const signalLen = signal.length; const kernelLen = flippedKernel.length; const outputLength = mode === 'full' ? signalLen + kernelLen - 1 : mode === 'same' ? signalLen : signalLen - kernelLen + 1; const result: number[] = new Array(outputLength); const halfKernelLen = Math.floor(kernelLen / 2); for (let i = 0; i < outputLength; i++) { let sum = 0; for (let j = 0; j < kernelLen; j++) { let signalIdx: number; switch (mode) { case 'full': signalIdx = i - j; break; case 'same': signalIdx = i - halfKernelLen + j; break; case 'valid': signalIdx = i + j; break; } // Handle boundary conditions if (signalIdx >= 0 && signalIdx < signalLen) { sum += signal[signalIdx] * flippedKernel[j]; } else if (boundary !== 'zero' && (mode === 'full' || mode === 'same')) { // This is a simplified boundary handler for the logic. Your more complex handler can be used here. let boundaryIdx = signalIdx; if (signalIdx < 0) { boundaryIdx = boundary === 'reflect' ? -signalIdx -1 : -signalIdx; } else if (signalIdx >= signalLen) { boundaryIdx = boundary === 'reflect' ? 2 * signalLen - signalIdx - 1 : 2 * signalLen - signalIdx - 2; } boundaryIdx = Math.max(0, Math.min(signalLen - 1, boundaryIdx)); sum += signal[boundaryIdx] * flippedKernel[j]; } // If boundary is 'zero', we add nothing, which is correct. } result[i] = sum; } return { values: result, originalLength: signalLen, kernelLength: kernelLen, mode }; } /** * Performs 2D convolution between matrix and kernel * * @param matrix - Input 2D matrix * @param kernel - 2D convolution kernel * @param options - Convolution options (mode, boundary) * @returns 2D convolution result with metadata */ export function convolve2D( matrix: number[][], kernel: number[][], options: ConvolutionOptions = {} ): ConvolutionResult2D { validateMatrix(matrix, 'Matrix'); validateMatrix(kernel, 'Kernel'); const { mode = 'same', boundary = 'reflect' } = options; // Flip kernel for convolution const flippedKernel = kernel.map(row => [...row].reverse()).reverse(); const matrixRows = matrix.length; const matrixCols = matrix[0].length; const kernelRows = flippedKernel.length; const kernelCols = flippedKernel[0].length; // Calculate output dimensions let outputRows: number, outputCols: number; let padTop: number, padLeft: number; switch (mode) { case 'full': outputRows = matrixRows + kernelRows - 1; outputCols = matrixCols + kernelCols - 1; padTop = kernelRows - 1; padLeft = kernelCols - 1; break; case 'same': outputRows = matrixRows; outputCols = matrixCols; padTop = Math.floor(kernelRows / 2); padLeft = Math.floor(kernelCols / 2); break; case 'valid': outputRows = Math.max(0, matrixRows - kernelRows + 1); outputCols = Math.max(0, matrixCols - kernelCols + 1); padTop = 0; padLeft = 0; break; default: throw new Error(`Unsupported convolution mode: ${mode}`); } // Create padded matrix based on boundary conditions const totalPadRows = mode === 'valid' ? 0 : kernelRows - 1; const totalPadCols = mode === 'valid' ? 0 : kernelCols - 1; const paddedMatrix: number[][] = []; // Initialize padded matrix with boundary conditions for (let i = -padTop; i < matrixRows + totalPadRows - padTop; i++) { const row: number[] = []; for (let j = -padLeft; j < matrixCols + totalPadCols - padLeft; j++) { let value = 0; if (i >= 0 && i < matrixRows && j >= 0 && j < matrixCols) { value = matrix[i][j]; } else if (boundary !== 'zero') { // Apply boundary conditions let boundaryI = i; let boundaryJ = j; if (boundary === 'reflect') { boundaryI = i < 0 ? -i - 1 : i >= matrixRows ? 2 * matrixRows - i - 1 : i; boundaryJ = j < 0 ? -j - 1 : j >= matrixCols ? 2 * matrixCols - j - 1 : j; } else if (boundary === 'symmetric') { boundaryI = i < 0 ? -i : i >= matrixRows ? 2 * matrixRows - i - 2 : i; boundaryJ = j < 0 ? -j : j >= matrixCols ? 2 * matrixCols - j - 2 : j; } boundaryI = Math.max(0, Math.min(boundaryI, matrixRows - 1)); boundaryJ = Math.max(0, Math.min(boundaryJ, matrixCols - 1)); value = matrix[boundaryI][boundaryJ]; } row.push(value); } paddedMatrix.push(row); } // Perform 2D convolution const result: number[][] = []; for (let i = 0; i < outputRows; i++) { const row: number[] = []; for (let j = 0; j < outputCols; j++) { let sum = 0; for (let ki = 0; ki < kernelRows; ki++) { for (let kj = 0; kj < kernelCols; kj++) { const matrixI = i + ki; const matrixJ = j + kj; if (matrixI >= 0 && matrixI < paddedMatrix.length && matrixJ >= 0 && matrixJ < paddedMatrix[0].length) { sum += paddedMatrix[matrixI][matrixJ] * flippedKernel[ki][kj]; } } } row.push(sum); } result.push(row); } return { matrix: result, originalDimensions: [matrixRows, matrixCols], kernelDimensions: [kernelRows, kernelCols], mode }; } /** * Creates common convolution kernels */ export class ConvolutionKernels { /** * Creates a Gaussian blur kernel */ static gaussian(size: number, sigma: number = 1.0): number[][] { if (size % 2 === 0) { throw new Error('Kernel size must be odd'); } const kernel: number[][] = []; const center = Math.floor(size / 2); let sum = 0; for (let i = 0; i < size; i++) { const row: number[] = []; for (let j = 0; j < size; j++) { const x = i - center; const y = j - center; const value = Math.exp(-(x * x + y * y) / (2 * sigma * sigma)); row.push(value); sum += value; } kernel.push(row); } // Normalize kernel return kernel.map(row => row.map(val => val / sum)); } /** * Creates a Sobel edge detection kernel */ static sobel(direction: 'x' | 'y' = 'x'): number[][] { if (direction === 'x') { return [ [-1, 0, 1], [-2, 0, 2], [-1, 0, 1] ]; } else { return [ [-1, -2, -1], [ 0, 0, 0], [ 1, 2, 1] ]; } } /** * Creates a Laplacian edge detection kernel */ static laplacian(): number[][] { return [ [ 0, -1, 0], [-1, 4, -1], [ 0, -1, 0] ]; } /** * Creates a box/average blur kernel */ static box(size: number): number[][] { if (size % 2 === 0) { throw new Error('Kernel size must be odd'); } const value = 1 / (size * size); const kernel: number[][] = []; for (let i = 0; i < size; i++) { kernel.push(new Array(size).fill(value)); } return kernel; } /** * Creates a 1D Gaussian kernel */ static gaussian1D(size: number, sigma: number = 1.0): number[] { if (size % 2 === 0) { throw new Error('Kernel size must be odd'); } const kernel: number[] = []; const center = Math.floor(size / 2); let sum = 0; for (let i = 0; i < size; i++) { const x = i - center; const value = Math.exp(-(x * x) / (2 * sigma * sigma)); kernel.push(value); sum += value; } // Normalize kernel return kernel.map(val => val / sum); } /** * Creates a 1D difference kernel for edge detection */ static difference1D(): number[] { return [-1, 0, 1]; } /** * Creates a 1D moving average kernel */ static average1D(size: number): number[] { if (size <= 0) { throw new Error('Kernel size must be positive'); } const value = 1 / size; return new Array(size).fill(value); } }