server update (server_convolution.ts) add endpoints of peaks detection, valleys detection, vertices (including peaks and valleys) detection, outliers detection
415 lines
No EOL
11 KiB
TypeScript
415 lines
No EOL
11 KiB
TypeScript
// 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);
|
|
}
|
|
} |