swimtracker-app/SwimTracker/data_processing/PeakDetection.js

186 lines
5.1 KiB
JavaScript

/**
* A simple peak detector
*
* Usage: Successively add values via add() and query the indices of the peaks with peaks
*
* A peak is detected if the current point is local maximum (no filtering!) and the current
* value is larger than (threshold + minimum_since_last_peak)
*/
class PeakDetectorSimple {
constructor(threshold) {
this._threshold = threshold;
this._queue = [];
this._last_min = 0;
this._counter = 0;
}
getThreshold() {
return this._threshold;
}
addVector(vec) {
let result = [];
for (let i = 0; i < vec.length; ++i) {
const res = this.add(vec[i]);
if(res !== null)
result.push(res);
}
return result;
}
add(value) {
let result = null;
this._queue.push(value);
if (this._queue.length > 3) {
this._queue.shift();
}
if (this._queue.length !== 3) {
return null;
}
const [last, current, next] = this._queue;
const is_maximum = current > next && current > last;
if (is_maximum && (current - this._last_min) > this._threshold) {
result = this._counter + 1;
this._last_min = current;
}
this._last_min = Math.min(this._last_min, current);
this._counter += 1;
return result;
}
}
/**
* Implementation of Z-Score peak detection according to
* https://stackoverflow.com/questions/22583391/peak-signal-detection-in-realtime-timeseries-data
*/
class PeakDetectorZScore {
constructor(lag, threshold, influence, handleNewPeaks) {
this.peaks = [];
this._filter = ZScoreFilter(lag, threshold, influence);
this._counter = 0;
this._previous_signal = 0;
this._max = null;
this._max_index = null;
this._handleNewPeaks = handleNewPeaks;
}
addVector(vec) {
const callbackBackup = this._handleNewPeaks;
const numPeaksBefore = this.peaks.length;
this._handleNewPeaks = null;
for (let i = 0; i < vec.length; ++i) {
this.add(vec[i]);
}
this._handleNewPeaks = callbackBackup;
if (numPeaksBefore != this.peaks.length) {
this._handleNewPeaks(this.peaks);
}
}
add(value) {
const signal = this._filter.add(value);
if (signal != null) {
const rising_flank = this._previous_signal !== 1 && signal === 1;
const falling_flank = this._previous_signal === 1 && signal !== 1;
if (rising_flank)
this._max = -1;
if (signal === 1 && this._max != null && value > this._max) {
this._max = value;
this._max_index = this._counter;
}
if (falling_flank) {
this.peaks.push(this._max_index);
if (this._handleNewPeaks) {
this._handleNewPeaks(this.peaks);
}
}
this._previous_signal = signal;
}
this._counter += 1;
}
}
export { PeakDetectorSimple, PeakDetectorZScore };
// --------------------------------------- Helper classes -------------------------------------------------------------
class StatisticsQueue {
constructor(size) {
this._size = size;
this._queue = [];
this._queue_sum = 0; // running sum over all elements currently in _queue
this._queue_sum_sq = 0; // sum of squared elements in _queue
}
add(value) {
this._queue.push(value);
this._queue_sum += value;
this._queue_sum_sq += value * value;
if (this._queue.length > this._size) {
const removed = this._queue[0];
this._queue.shift();
this._queue_sum -= removed;
this._queue_sum_sq -= removed * removed;
}
}
get avg() {
return this._queue_sum / self._queue.length;
}
get variance() {
const exp_sq = this._queue_sum_sq / self._queue.length;
const my_avg = self.avg;
return exp_sq - (my_avg * my_avg);
}
get std_deviation() {
return Math.sqrt(this.variance);
}
get filled() {
return this._queue.length === this._size;
}
}
class ZScoreFilter {
constructor(lag, threshold, influence) {
this._threshold = threshold;
this._influence = influence;
this._last_value = null;
this._stat_queue = StatisticsQueue(lag);
}
add(value) {
let sq = this._stat_queue;
if (!sq.filled) {
sq.add(value);
this._last_value = value;
return null;
} else {
const avg = sq.avg;
if (Math.abs(value - avg) > this._threshold * sq.std_deviation) {
const signal = value > avg ? 1 : -1;
const filtered = this._influence * value + (1 - this._influence) * this._last_value;
sq.add(filtered);
this._last_value = filtered;
return signal;
} else {
sq.add(value);
this._last_value = value;
}
}
}
}