blob: f7da1aa83d0bb4c5b2159f65743e28bd34e12371 [file]
export interface ScreenPoint {
px: number;
py: number;
rawPy: number;
rawX: number;
}
/**
* Smooths an array of screen points using a bidirectional adaptive Bilateral EMA.
* Preserves edges (step changes) while aggressively rejecting transient outliers.
*/
export function smoothPoints(
points: ScreenPoint[],
smoothingRadius: number,
edgeDetectionFactor: number,
edgeLookahead: number
): { smoothed: number[]; std: number[] } {
const n = points.length;
if (n === 0) return { smoothed: [], std: [] };
if (n === 1) return { smoothed: [points[0].py], std: [0] };
const MIN_PIXEL_NOISE = 2;
const sf = new Float64Array(n);
const sb = new Float64Array(n);
const vf = new Float64Array(n);
const vb = new Float64Array(n);
// Forward Pass
let prevMean = points[0].rawPy;
let prevVar = MIN_PIXEL_NOISE * MIN_PIXEL_NOISE;
sf[0] = prevMean;
vf[0] = prevVar;
for (let i = 1; i < n; i++) {
const raw = points[i].rawPy;
const dx = Math.abs(points[i].px - points[i - 1].px);
const alphaX = 1 - Math.exp(-Math.max(0, dx) / smoothingRadius);
const diffCurr = raw - prevMean;
const stdDev = Math.max(MIN_PIXEL_NOISE, Math.sqrt(prevVar));
const devCurr = Math.abs(diffCurr) / stdDev;
let edgeScore = devCurr;
let allSameSign = true;
for (let step = 1; step <= edgeLookahead; step++) {
const targetIdx = Math.min(i + step, n - 1);
const nextRaw = points[targetIdx].rawPy;
const diffNext = nextRaw - prevMean;
const devNext = Math.abs(diffNext) / stdDev;
if (diffCurr * diffNext <= 0) {
allSameSign = false;
break;
}
edgeScore = Math.min(edgeScore, devNext);
}
if (!allSameSign) edgeScore = 0;
const edgeFactor = 1 - Math.exp(-Math.pow(edgeScore / 2.0, 2));
const transientScore = Math.max(0, devCurr - edgeScore);
const outlierFactor = 1 - Math.exp(-Math.pow(transientScore / 2.0, 2));
let adaptiveAlphaMean = alphaX * (1 - outlierFactor * 0.95);
let adaptiveAlphaVar = alphaX * (1 - outlierFactor);
adaptiveAlphaMean =
adaptiveAlphaMean * (1 - edgeFactor) +
Math.max(adaptiveAlphaMean, edgeDetectionFactor) * edgeFactor;
adaptiveAlphaVar =
adaptiveAlphaVar * (1 - edgeFactor) +
Math.max(adaptiveAlphaVar, edgeDetectionFactor) * edgeFactor;
prevMean = adaptiveAlphaMean * raw + (1 - adaptiveAlphaMean) * prevMean;
sf[i] = prevMean;
const newDiffCurr = raw - prevMean;
prevVar = adaptiveAlphaVar * (newDiffCurr * newDiffCurr) + (1 - adaptiveAlphaVar) * prevVar;
vf[i] = prevVar;
}
// Backward Pass
prevMean = points[n - 1].rawPy;
prevVar = MIN_PIXEL_NOISE * MIN_PIXEL_NOISE;
sb[n - 1] = prevMean;
vb[n - 1] = prevVar;
for (let i = n - 2; i >= 0; i--) {
const raw = points[i].rawPy;
const dx = Math.abs(points[i + 1].px - points[i].px);
const alphaX = 1 - Math.exp(-Math.max(0, dx) / smoothingRadius);
const diffCurr = raw - prevMean;
const stdDev = Math.max(MIN_PIXEL_NOISE, Math.sqrt(prevVar));
const devCurr = Math.abs(diffCurr) / stdDev;
let edgeScore = devCurr;
let allSameSign = true;
for (let step = 1; step <= edgeLookahead; step++) {
const targetIdx = Math.max(i - step, 0);
const nextRaw = points[targetIdx].rawPy;
const diffNext = nextRaw - prevMean;
const devNext = Math.abs(diffNext) / stdDev;
if (diffCurr * diffNext <= 0) {
allSameSign = false;
break;
}
edgeScore = Math.min(edgeScore, devNext);
}
if (!allSameSign) edgeScore = 0;
const edgeFactor = 1 - Math.exp(-Math.pow(edgeScore / 2.0, 2));
const transientScore = Math.max(0, devCurr - edgeScore);
const outlierFactor = 1 - Math.exp(-Math.pow(transientScore / 2.0, 2));
let adaptiveAlphaMean = alphaX * (1 - outlierFactor * 0.95);
let adaptiveAlphaVar = alphaX * (1 - outlierFactor);
adaptiveAlphaMean =
adaptiveAlphaMean * (1 - edgeFactor) +
Math.max(adaptiveAlphaMean, edgeDetectionFactor) * edgeFactor;
adaptiveAlphaVar =
adaptiveAlphaVar * (1 - edgeFactor) +
Math.max(adaptiveAlphaVar, edgeDetectionFactor) * edgeFactor;
prevMean = adaptiveAlphaMean * raw + (1 - adaptiveAlphaMean) * prevMean;
sb[i] = prevMean;
const newDiffCurr = raw - prevMean;
prevVar = adaptiveAlphaVar * (newDiffCurr * newDiffCurr) + (1 - adaptiveAlphaVar) * prevVar;
vb[i] = prevVar;
}
const smoothed: number[] = [];
const std: number[] = [];
for (let i = 0; i < n; i++) {
smoothed.push((sf[i] + sb[i]) / 2);
// Take the max variance between forward and backward passes to be conservative, then sqrt for std
std.push(Math.sqrt(Math.max(vf[i], vb[i])));
}
return { smoothed, std };
}