server update (server_convolution.ts) add endpoints of peaks detection, valleys detection, vertices (including peaks and valleys) detection, outliers detection
671 lines
No EOL
22 KiB
TypeScript
671 lines
No EOL
22 KiB
TypeScript
// signal-processing.ts - Convolution-based signal processing functions
|
|
import { convolve1D, convolve2D, ConvolutionKernels, ConvolutionOptions } from './convolution';
|
|
|
|
export interface SmoothingOptions {
|
|
method?: 'gaussian' | 'moving_average';
|
|
windowSize?: number;
|
|
sigma?: number;
|
|
}
|
|
|
|
export interface EdgeDetectionOptions {
|
|
method?: 'sobel' | 'laplacian' | 'canny';
|
|
threshold?: number;
|
|
}
|
|
|
|
export interface FilterOptions {
|
|
type: 'lowpass' | 'highpass' | 'bandpass' | 'bandstop';
|
|
cutoffLow?: number;
|
|
cutoffHigh?: number;
|
|
order?: number;
|
|
}
|
|
|
|
export interface DerivativeOptions {
|
|
order?: 1 | 2;
|
|
method?: 'gradient' | 'laplacian';
|
|
}
|
|
|
|
/**
|
|
* Convolution-Based Signal Processing Library
|
|
* Functions that leverage convolution operations for signal processing
|
|
*/
|
|
export class SignalProcessor {
|
|
|
|
/**
|
|
* Smooth a 1D signal using convolution-based methods
|
|
*/
|
|
static smooth(signal: number[], options: SmoothingOptions = {}): number[] {
|
|
const { method = 'gaussian', windowSize = 5, sigma = 1.0 } = options;
|
|
|
|
if (signal.length === 0) {
|
|
throw new Error('Signal cannot be empty');
|
|
}
|
|
|
|
let kernel: number[];
|
|
|
|
switch (method) {
|
|
case 'gaussian':
|
|
kernel = ConvolutionKernels.gaussian1D(windowSize, sigma);
|
|
break;
|
|
|
|
case 'moving_average':
|
|
kernel = ConvolutionKernels.average1D(windowSize);
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Unsupported smoothing method: ${method}`);
|
|
}
|
|
|
|
return convolve1D(signal, kernel, { mode: 'same' }).values;
|
|
}
|
|
|
|
/**
|
|
* Detect edges in 2D image data using convolution-based methods
|
|
*/
|
|
static detectEdges2D(image: number[][], options: EdgeDetectionOptions = {}): number[][] {
|
|
const { method = 'sobel', threshold = 0.1 } = options;
|
|
|
|
let kernelX: number[][];
|
|
let kernelY: number[][];
|
|
|
|
switch (method) {
|
|
case 'sobel':
|
|
kernelX = ConvolutionKernels.sobel("x");
|
|
kernelY = ConvolutionKernels.sobel("y");
|
|
break;
|
|
|
|
case 'laplacian':
|
|
const laplacianKernel = ConvolutionKernels.laplacian();
|
|
return convolve2D(image, laplacianKernel, { mode: 'same' }).matrix.map(row =>
|
|
row.map(val => Math.abs(val) > threshold ? Math.abs(val) : 0)
|
|
);
|
|
|
|
default:
|
|
throw new Error(`Unsupported edge detection method: ${method}`);
|
|
}
|
|
|
|
// Apply both kernels and combine results
|
|
const edgesX = convolve2D(image, kernelX, { mode: 'same' }).matrix;
|
|
const edgesY = convolve2D(image, kernelY, { mode: 'same' }).matrix;
|
|
|
|
// Calculate gradient magnitude
|
|
const result: number[][] = [];
|
|
for (let i = 0; i < edgesX.length; i++) {
|
|
result[i] = [];
|
|
for (let j = 0; j < edgesX[i].length; j++) {
|
|
const magnitude = Math.sqrt(edgesX[i][j] ** 2 + edgesY[i][j] ** 2);
|
|
result[i][j] = magnitude > threshold ? magnitude : 0;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Apply digital filters using convolution
|
|
*/
|
|
static filter(signal: number[], options: FilterOptions): number[] {
|
|
const { type, cutoffLow = 0.1, cutoffHigh = 0.5, order = 4 } = options;
|
|
|
|
let kernel: number[];
|
|
|
|
switch (type) {
|
|
case 'lowpass':
|
|
// Low-pass filter using Gaussian kernel
|
|
kernel = ConvolutionKernels.gaussian1D(order * 4 + 1, order / 2);
|
|
return convolve1D(signal, kernel, { mode: 'same' }).values;
|
|
|
|
case 'highpass':
|
|
// High-pass filter using difference of Gaussians
|
|
const lpKernel = ConvolutionKernels.gaussian1D(order * 4 + 1, order / 2);
|
|
const smoothed = convolve1D(signal, lpKernel, { mode: 'same' }).values;
|
|
return signal.map((val, i) => val - smoothed[i]);
|
|
|
|
case 'bandpass':
|
|
// Band-pass as combination of high-pass and low-pass
|
|
const hp = this.filter(signal, { type: 'highpass', cutoffLow, order });
|
|
return this.filter(hp, { type: 'lowpass', cutoffLow: cutoffHigh, order });
|
|
|
|
case 'bandstop':
|
|
// Band-stop as original minus band-pass
|
|
const bp = this.filter(signal, { type: 'bandpass', cutoffLow, cutoffHigh, order });
|
|
return signal.map((val, i) => val - bp[i]);
|
|
|
|
default:
|
|
throw new Error(`Unsupported filter type: ${type}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate derivatives using convolution with derivative kernels
|
|
*/
|
|
static derivative(signal: number[], options: DerivativeOptions = {}): number[] {
|
|
const { order = 1, method = 'gradient' } = options;
|
|
|
|
let kernel: number[];
|
|
|
|
if (method === 'gradient') {
|
|
switch (order) {
|
|
case 1:
|
|
// First derivative using gradient kernel
|
|
kernel = [-0.5, 0, 0.5]; // Simple gradient
|
|
break;
|
|
case 2:
|
|
// Second derivative using Laplacian-like kernel
|
|
kernel = [1, -2, 1]; // Simple second derivative
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported derivative order: ${order}`);
|
|
}
|
|
} else if (method === 'laplacian' && order === 2) {
|
|
// 1D Laplacian
|
|
kernel = [1, -2, 1];
|
|
} else {
|
|
throw new Error(`Unsupported derivative method: ${method}`);
|
|
}
|
|
|
|
return convolve1D(signal, kernel, { mode: 'same' }).values;
|
|
}
|
|
|
|
/**
|
|
* Blur 2D image using Gaussian convolution
|
|
*/
|
|
static blur2D(image: number[][], sigma: number = 1.0, kernelSize?: number): number[][] {
|
|
const size = kernelSize || Math.ceil(sigma * 6) | 1; // Ensure odd size
|
|
const kernel = ConvolutionKernels.gaussian(size, sigma);
|
|
|
|
return convolve2D(image, kernel, { mode: 'same' }).matrix;
|
|
}
|
|
|
|
/**
|
|
* Sharpen 2D image using unsharp masking (convolution-based)
|
|
*/
|
|
static sharpen2D(image: number[][], strength: number = 1.0): number[][] {
|
|
const sharpenKernel = [
|
|
[0, -strength, 0],
|
|
[-strength, 1 + 4 * strength, -strength],
|
|
[0, -strength, 0]
|
|
];
|
|
|
|
return convolve2D(image, sharpenKernel, { mode: 'same' }).matrix;
|
|
}
|
|
|
|
/**
|
|
* Apply emboss effect using convolution
|
|
*/
|
|
static emboss2D(image: number[][], direction: 'ne' | 'nw' | 'se' | 'sw' = 'ne'): number[][] {
|
|
const embossKernels = {
|
|
ne: [[-2, -1, 0], [-1, 1, 1], [0, 1, 2]],
|
|
nw: [[0, -1, -2], [1, 1, -1], [2, 1, 0]],
|
|
se: [[0, 1, 2], [-1, 1, 1], [-2, -1, 0]],
|
|
sw: [[2, 1, 0], [1, 1, -1], [0, -1, -2]]
|
|
};
|
|
|
|
const kernel = embossKernels[direction];
|
|
return convolve2D(image, kernel, { mode: 'same' }).matrix;
|
|
}
|
|
|
|
/**
|
|
* Apply motion blur using directional convolution kernel
|
|
*/
|
|
static motionBlur(signal: number[], direction: number, length: number = 9): number[] {
|
|
// Create motion blur kernel
|
|
const kernel = new Array(length).fill(1 / length);
|
|
|
|
return convolve1D(signal, kernel, { mode: 'same' }).values;
|
|
}
|
|
|
|
/**
|
|
* Detect impulse response using convolution with known impulse
|
|
*/
|
|
static matchedFilter(signal: number[], template: number[]): number[] {
|
|
// Matched filter using cross-correlation (convolution with reversed template)
|
|
const reversedTemplate = [...template].reverse();
|
|
return convolve1D(signal, reversedTemplate, { mode: 'same' }).values;
|
|
}
|
|
|
|
/**
|
|
* Apply median filtering (note: not convolution-based, but commonly used with other filters)
|
|
*/
|
|
static medianFilter(signal: number[], windowSize: number = 3): number[] {
|
|
const result: number[] = [];
|
|
const halfWindow = Math.floor(windowSize / 2);
|
|
|
|
for (let i = 0; i < signal.length; i++) {
|
|
const window: number[] = [];
|
|
|
|
for (let j = Math.max(0, i - halfWindow); j <= Math.min(signal.length - 1, i + halfWindow); j++) {
|
|
window.push(signal[j]);
|
|
}
|
|
|
|
window.sort((a, b) => a - b);
|
|
const median = window[Math.floor(window.length / 2)];
|
|
result.push(median);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Cross-correlation using convolution
|
|
*/
|
|
static crossCorrelate(signal1: number[], signal2: number[]): number[] {
|
|
// Cross-correlation is convolution with one signal reversed
|
|
const reversedSignal2 = [...signal2].reverse();
|
|
return convolve1D(signal1, reversedSignal2, { mode: 'full' }).values;
|
|
}
|
|
|
|
/**
|
|
* Auto-correlation using convolution
|
|
*/
|
|
static autoCorrelate(signal: number[]): number[] {
|
|
return this.crossCorrelate(signal, signal);
|
|
}
|
|
|
|
/**
|
|
* Detect peaks using convolution-based edge detection
|
|
*/
|
|
/**
|
|
* [REWRITTEN] Detects peaks (local maxima) in a 1D signal.
|
|
* This is a more robust method that directly finds local maxima.
|
|
*/
|
|
static detectPeaksConvolution(signal: number[], options: {
|
|
smoothWindow?: number;
|
|
threshold?: number;
|
|
minDistance?: number;
|
|
} = {}): { index: number; value: number }[] {
|
|
const { smoothWindow = 0, threshold = -Infinity, minDistance = 1 } = options;
|
|
|
|
let processedSignal = signal;
|
|
// Optionally smooth the signal first to reduce noise
|
|
if (smoothWindow > 1) {
|
|
processedSignal = this.smooth(signal, { method: 'gaussian', windowSize: smoothWindow });
|
|
}
|
|
|
|
const peaks: { index: number; value: number }[] = [];
|
|
|
|
// Find all points that are higher than their immediate neighbors
|
|
for (let i = 1; i < processedSignal.length - 1; i++) {
|
|
const prev = processedSignal[i - 1];
|
|
const curr = processedSignal[i];
|
|
const next = processedSignal[i + 1];
|
|
|
|
if (curr > prev && curr > next && curr > threshold) {
|
|
peaks.push({ index: i, value: signal[i] }); // Store index and ORIGINAL value
|
|
}
|
|
}
|
|
|
|
// Check boundaries: Is the first or last point a peak?
|
|
if (processedSignal[0] > processedSignal[1] && processedSignal[0] > threshold) {
|
|
peaks.unshift({ index: 0, value: signal[0] });
|
|
}
|
|
const last = processedSignal.length - 1;
|
|
if (processedSignal[last] > processedSignal[last - 1] && processedSignal[last] > threshold) {
|
|
peaks.push({ index: last, value: signal[last] });
|
|
}
|
|
|
|
// [CORRECTED LOGIC] Enforce minimum distance between peaks
|
|
if (minDistance < 2 || peaks.length <= 1) {
|
|
return peaks;
|
|
}
|
|
|
|
// Sort peaks by value, highest first
|
|
peaks.sort((a, b) => b.value - a.value);
|
|
|
|
const finalPeaks: { index: number; value: number }[] = [];
|
|
const removed = new Array(peaks.length).fill(false);
|
|
|
|
for (let i = 0; i < peaks.length; i++) {
|
|
if (!removed[i]) {
|
|
finalPeaks.push(peaks[i]);
|
|
// Remove other peaks within the minimum distance
|
|
for (let j = i + 1; j < peaks.length; j++) {
|
|
if (!removed[j] && Math.abs(peaks[i].index - peaks[j].index) < minDistance) {
|
|
removed[j] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return finalPeaks.sort((a, b) => a.index - b.index);
|
|
}
|
|
|
|
|
|
/**
|
|
* [REWRITTEN] Detects valleys (local minima) in a 1D signal.
|
|
*/
|
|
static detectValleysConvolution(signal: number[], options: {
|
|
smoothWindow?: number;
|
|
threshold?: number;
|
|
minDistance?: number;
|
|
} = {}): { index: number; value: number }[] {
|
|
const invertedSignal = signal.map(x => -x);
|
|
const invertedThreshold = options.threshold !== undefined ? -options.threshold : undefined;
|
|
|
|
const invertedPeaks = this.detectPeaksConvolution(invertedSignal, { ...options, threshold: invertedThreshold });
|
|
|
|
return invertedPeaks.map(peak => ({
|
|
index: peak.index,
|
|
value: -peak.value,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Detect outliers using convolution-based methods
|
|
*/
|
|
/**
|
|
* [REWRITTEN] Detects outliers using more reliable and statistically sound methods.
|
|
*/
|
|
static detectOutliersConvolution(signal: number[], options: {
|
|
method?: 'local_deviation' | 'mean_diff';
|
|
windowSize?: number;
|
|
threshold?: number;
|
|
} = {}): { index: number; value: number; outlierScore: number }[] {
|
|
const { method = 'local_deviation', windowSize = 7, threshold = 3.0 } = options;
|
|
|
|
let outlierScores: number[];
|
|
|
|
switch (method) {
|
|
case 'mean_diff':
|
|
// Detects outliers by their difference from the local mean.
|
|
const meanKernel = ConvolutionKernels.average1D(windowSize);
|
|
const localMean = convolve1D(signal, meanKernel, { mode: 'same' }).values;
|
|
outlierScores = signal.map((val, i) => Math.abs(val - localMean[i]));
|
|
break;
|
|
|
|
case 'local_deviation':
|
|
// A robust method using Z-score: how many local standard deviations away a point is.
|
|
const avgKernel = ConvolutionKernels.average1D(windowSize);
|
|
const localMeanValues = convolve1D(signal, avgKernel, { mode: 'same' }).values;
|
|
const squaredDiffs = signal.map((val, i) => (val - localMeanValues[i]) ** 2);
|
|
const localVar = convolve1D(squaredDiffs, avgKernel, { mode: 'same' }).values;
|
|
outlierScores = signal.map((val, i) => {
|
|
const std = Math.sqrt(localVar[i]);
|
|
return std > 1e-6 ? Math.abs(val - localMeanValues[i]) / std : 0;
|
|
});
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Unsupported outlier detection method: ${method}`);
|
|
}
|
|
|
|
// Find points exceeding the threshold
|
|
const outliers: { index: number; value: number; outlierScore: number }[] = [];
|
|
outlierScores.forEach((score, i) => {
|
|
if (score > threshold) {
|
|
outliers.push({
|
|
index: i,
|
|
value: signal[i],
|
|
outlierScore: score
|
|
});
|
|
}
|
|
});
|
|
|
|
return outliers;
|
|
}
|
|
|
|
/**
|
|
* Detect trend vertices (turning points) using convolution
|
|
*/
|
|
/**
|
|
* [CORRECTED] Detects trend vertices (turning points) by finding all peaks and valleys.
|
|
* This version fixes a bug that prevented valleys from being detected.
|
|
*/
|
|
static detectTrendVertices(signal: number[], options: {
|
|
smoothingWindow?: number;
|
|
threshold?: number;
|
|
minDistance?: number;
|
|
} = {}): { index: number; value: number; type: 'peak' | 'valley' }[] {
|
|
const {
|
|
smoothingWindow = 5,
|
|
threshold = 0, // CORRECTED: Changed default from -Infinity to a sensible 0
|
|
minDistance = 3
|
|
} = options;
|
|
|
|
// Create the options object to pass down. The valley function will handle inverting the threshold itself.
|
|
const detectionOptions = { smoothingWindow, threshold, minDistance };
|
|
|
|
const peaks = this.detectPeaksConvolution(signal, detectionOptions).map(p => ({ ...p, type: 'peak' as const }));
|
|
const valleys = this.detectValleysConvolution(signal, detectionOptions).map(v => ({ ...v, type: 'valley' as const }));
|
|
|
|
// Combine peaks and valleys and sort them by their index to get the sequence of trend changes
|
|
const vertices = [...peaks, ...valleys];
|
|
vertices.sort((a, b) => a.index - b.index);
|
|
|
|
return vertices;
|
|
}
|
|
|
|
/**
|
|
* Detect vertices using curvature (second derivative)
|
|
*/
|
|
private static detectCurvatureVertices(
|
|
signal: number[],
|
|
threshold: number
|
|
): { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] {
|
|
// Use second derivative kernel for curvature
|
|
const curvatureKernel = [1, -2, 1]; // Discrete Laplacian
|
|
const curvature = convolve1D(signal, curvatureKernel, { mode: 'same' }).values;
|
|
|
|
const vertices: { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] = [];
|
|
|
|
// Find zero crossings in curvature with sufficient magnitude
|
|
for (let i = 1; i < curvature.length - 1; i++) {
|
|
const prev = curvature[i - 1];
|
|
const curr = curvature[i];
|
|
const next = curvature[i + 1];
|
|
|
|
// Zero crossing detection
|
|
if ((prev > 0 && next < 0) || (prev < 0 && next > 0)) {
|
|
const curvatureMagnitude = Math.abs(curr);
|
|
|
|
if (curvatureMagnitude > threshold) {
|
|
const type: 'peak' | 'valley' = prev > 0 ? 'peak' : 'valley';
|
|
|
|
vertices.push({
|
|
index: i,
|
|
value: signal[i],
|
|
type,
|
|
curvature: curr
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return vertices;
|
|
}
|
|
|
|
/**
|
|
* Detect vertices using gradient sign changes
|
|
*/
|
|
private static detectSignChangeVertices(
|
|
signal: number[],
|
|
threshold: number
|
|
): { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] {
|
|
// First derivative for gradient
|
|
const gradientKernel = [-0.5, 0, 0.5]; // Central difference
|
|
const gradient = convolve1D(signal, gradientKernel, { mode: 'same' }).values;
|
|
|
|
// Second derivative for curvature
|
|
const curvatureKernel = [1, -2, 1];
|
|
const curvature = convolve1D(signal, curvatureKernel, { mode: 'same' }).values;
|
|
|
|
const vertices: { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] = [];
|
|
|
|
// Find gradient sign changes
|
|
for (let i = 1; i < gradient.length - 1; i++) {
|
|
const prevGrad = gradient[i - 1];
|
|
const nextGrad = gradient[i + 1];
|
|
|
|
// Check for sign change with sufficient gradient magnitude
|
|
if (Math.abs(prevGrad) > threshold && Math.abs(nextGrad) > threshold) {
|
|
if ((prevGrad > 0 && nextGrad < 0) || (prevGrad < 0 && nextGrad > 0)) {
|
|
const type: 'peak' | 'valley' = prevGrad > 0 ? 'peak' : 'valley';
|
|
|
|
vertices.push({
|
|
index: i,
|
|
value: signal[i],
|
|
type,
|
|
curvature: curvature[i]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return vertices;
|
|
}
|
|
|
|
/**
|
|
* Detect vertices using momentum changes
|
|
*/
|
|
private static detectMomentumVertices(
|
|
signal: number[],
|
|
threshold: number
|
|
): { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] {
|
|
// Create momentum kernel (difference over larger window)
|
|
const momentumKernel = [-1, 0, 0, 0, 1]; // 4-point difference
|
|
const momentum = convolve1D(signal, momentumKernel, { mode: 'same' }).values;
|
|
|
|
// Detect momentum reversals
|
|
const momentumGradient = convolve1D(momentum, [-0.5, 0, 0.5], { mode: 'same' }).values;
|
|
const curvature = convolve1D(signal, [1, -2, 1], { mode: 'same' }).values;
|
|
|
|
const vertices: { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] = [];
|
|
|
|
for (let i = 2; i < momentum.length - 2; i++) {
|
|
const prevMomentum = momentum[i - 1];
|
|
const currMomentum = momentum[i];
|
|
const nextMomentum = momentum[i + 1];
|
|
|
|
// Check for momentum reversal
|
|
if (Math.abs(momentumGradient[i]) > threshold) {
|
|
if ((prevMomentum > 0 && nextMomentum < 0) || (prevMomentum < 0 && nextMomentum > 0)) {
|
|
const type: 'peak' | 'valley' = prevMomentum > 0 ? 'peak' : 'valley';
|
|
|
|
vertices.push({
|
|
index: i,
|
|
value: signal[i],
|
|
type,
|
|
curvature: curvature[i]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return vertices;
|
|
}
|
|
|
|
/**
|
|
* Detect trend direction changes using convolution
|
|
*/
|
|
static detectTrendChanges(signal: number[], options: {
|
|
windowSize?: number;
|
|
threshold?: number;
|
|
minTrendLength?: number;
|
|
} = {}): { index: number; fromTrend: 'up' | 'down' | 'flat'; toTrend: 'up' | 'down' | 'flat'; strength: number }[] {
|
|
const { windowSize = 10, threshold = 0.01, minTrendLength = 5 } = options;
|
|
|
|
// Calculate local trends using convolution with trend-detecting kernel
|
|
const trendKernel = new Array(windowSize).fill(0).map((_, i) => {
|
|
const center = windowSize / 2;
|
|
return (i - center) / (windowSize * windowSize / 12); // Linear trend kernel
|
|
});
|
|
|
|
const trends = convolve1D(signal, trendKernel, { mode: 'same' }).values;
|
|
|
|
// Classify trends
|
|
const trendDirection = trends.map(t => {
|
|
if (t > threshold) return 'up';
|
|
if (t < -threshold) return 'down';
|
|
return 'flat';
|
|
});
|
|
|
|
// Find trend changes
|
|
const changes: { index: number; fromTrend: 'up' | 'down' | 'flat'; toTrend: 'up' | 'down' | 'flat'; strength: number }[] = [];
|
|
|
|
let currentTrend: 'up' | 'down' | 'flat' = trendDirection[0];
|
|
let trendStart = 0;
|
|
|
|
for (let i = 1; i < trendDirection.length; i++) {
|
|
if (trendDirection[i] !== currentTrend) {
|
|
const trendLength = i - trendStart;
|
|
|
|
if (trendLength >= minTrendLength) {
|
|
changes.push({
|
|
index: i,
|
|
fromTrend: currentTrend,
|
|
toTrend: trendDirection[i] as 'up' | 'down' | 'flat',
|
|
strength: Math.abs(trends[i] - trends[trendStart])
|
|
});
|
|
}
|
|
|
|
currentTrend = trendDirection[i] as 'up' | 'down' | 'flat';
|
|
trendStart = i;
|
|
}
|
|
}
|
|
|
|
return changes;
|
|
}
|
|
|
|
/**
|
|
* Enforce minimum distance between vertices
|
|
*/
|
|
private static enforceMinDistanceVertices(
|
|
vertices: { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[],
|
|
minDistance: number
|
|
): { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] {
|
|
if (vertices.length <= 1) return vertices;
|
|
|
|
// Sort by curvature magnitude (stronger vertices first)
|
|
const sorted = [...vertices].sort((a, b) => Math.abs(b.curvature) - Math.abs(a.curvature));
|
|
const result: { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] = [];
|
|
|
|
for (const vertex of sorted) {
|
|
let tooClose = false;
|
|
|
|
for (const accepted of result) {
|
|
if (Math.abs(vertex.index - accepted.index) < minDistance) {
|
|
tooClose = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!tooClose) {
|
|
result.push(vertex);
|
|
}
|
|
}
|
|
|
|
// Sort result by index
|
|
return result.sort((a, b) => a.index - b.index);
|
|
}
|
|
|
|
/**
|
|
* Enforce minimum distance between detected features
|
|
*/
|
|
private static enforceMinDistanceConv(
|
|
features: { index: number; value: number; strength: number }[],
|
|
minDistance: number
|
|
): { index: number; value: number; strength: number }[] {
|
|
if (features.length <= 1) return features;
|
|
|
|
// Sort by strength (descending)
|
|
const sorted = [...features].sort((a, b) => b.strength - a.strength);
|
|
const result: { index: number; value: number; strength: number }[] = [];
|
|
|
|
for (const feature of sorted) {
|
|
let tooClose = false;
|
|
|
|
for (const accepted of result) {
|
|
if (Math.abs(feature.index - accepted.index) < minDistance) {
|
|
tooClose = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!tooClose) {
|
|
result.push(feature);
|
|
}
|
|
}
|
|
|
|
// Sort result by index
|
|
return result.sort((a, b) => a.index - b.index);
|
|
}
|
|
} |