bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
189 lines (153 loc) • 7.32 kB
JavaScript
/**
* @module bowling_analysis/metrics/calculators/DerivativeMetricsCalculator
* @description Calculator for generating derivative metrics from existing time series data
*/
// Reuse existing derivative calculation functions from the metrics/index.js
const { calculateDerivative, calculatePositionDerivative, smoothTimeSeries } = require('../index.js');
/**
* Calculate derivative metrics (rate of change, acceleration profiles)
* @param {Array} keypointData - Array of keypoint frames
* @param {Array} validFrames - Array of valid keypoint frames
* @param {Object} options - Calculator options
* @returns {Promise<Object>} Derivative metrics
*/
async function calculate(keypointData, validFrames, options = {}) {
try {
const { debug, includeTimeSeries, existingTimeSeries } = options;
// Initialize result
const result = {};
// Initialize time series
const timeSeries = {};
// Need time series data to calculate derivatives
if (!existingTimeSeries) {
console.warn('No time series data provided to DerivativeMetricsCalculator');
return { timeSeries: {} };
}
// Categories to process for derivatives
const categories = [
'angles',
'velocity',
'position',
'balance',
'power',
'rotation'
];
// Smoothing window size for rate of change calculation
const SMOOTHING_WINDOW = 5;
// Process each category
for (const category of categories) {
if (!existingTimeSeries[category]) continue;
// Store derivatives at the category level
const categoryDerivatives = {};
const categorySecondDerivatives = {};
// Process each metric in this category
for (const metricKey of Object.keys(existingTimeSeries[category])) {
const timeSeries = existingTimeSeries[category][metricKey];
// Skip if not an array
if (!Array.isArray(timeSeries)) continue;
// Calculate first derivative (rate of change)
const smoothedSeries = smoothTimeSeries(timeSeries, SMOOTHING_WINDOW);
const firstDerivative = calculateDerivative(smoothedSeries);
// Calculate second derivative (acceleration profile)
const smoothedFirstDerivative = smoothTimeSeries(firstDerivative, SMOOTHING_WINDOW);
const secondDerivative = calculateDerivative(smoothedFirstDerivative);
// Store time series data
result.timeSeries = result.timeSeries || {};
result.timeSeries[`${category}.${metricKey}.rateOfChange`] = firstDerivative;
result.timeSeries[`${category}.${metricKey}.accelerationProfile`] = secondDerivative;
// Calculate statistics for the derivatives
const rateOfChangeStats = calculateTimeSeriesStats(firstDerivative);
const accelerationStats = calculateTimeSeriesStats(secondDerivative);
// Store metrics
categoryDerivatives[metricKey] = rateOfChangeStats;
categorySecondDerivatives[metricKey] = accelerationStats;
}
// Add to result if we have derivatives
if (Object.keys(categoryDerivatives).length > 0) {
result[`${category}RateOfChange`] = categoryDerivatives;
}
if (Object.keys(categorySecondDerivatives).length > 0) {
result[`${category}AccelerationProfile`] = categorySecondDerivatives;
}
}
// Add special handling for position derivatives (uses 2D/3D vectors)
if (existingTimeSeries.position) {
// Process position metrics to create velocity and acceleration vectors
for (const posKey of Object.keys(existingTimeSeries.position)) {
// Skip if not position data (we need x,y properties)
if (!existingTimeSeries.position[posKey] ||
!Array.isArray(existingTimeSeries.position[posKey]) ||
!existingTimeSeries.position[posKey][0] ||
typeof existingTimeSeries.position[posKey][0] !== 'object') {
continue;
}
// Calculate position derivatives
const velocityVectors = calculatePositionDerivative(existingTimeSeries.position[posKey]);
const accelerationVectors = calculatePositionDerivative(velocityVectors);
// Store time series
result.timeSeries = result.timeSeries || {};
result.timeSeries[`position.${posKey}.velocity`] = velocityVectors;
result.timeSeries[`position.${posKey}.acceleration`] = accelerationVectors;
// Calculate vector magnitude statistics
const velocityStats = calculateVectorStats(velocityVectors);
const accelerationStats = calculateVectorStats(accelerationVectors);
// Store metrics
result.vectorVelocity = result.vectorVelocity || {};
result.vectorAcceleration = result.vectorAcceleration || {};
result.vectorVelocity[posKey] = velocityStats;
result.vectorAcceleration[posKey] = accelerationStats;
}
}
return result;
} catch (error) {
console.error(`Error calculating derivative metrics: ${error.message}`);
return {};
}
}
/**
* Calculate statistics for a time series
* @param {Array} timeSeries - Array of values
* @returns {Object} Statistics (mean, max, min, stdDev)
*/
function calculateTimeSeriesStats(timeSeries) {
const validValues = timeSeries.filter(val => val !== null && val !== undefined && !isNaN(val));
if (validValues.length === 0) {
return { mean: null, max: null, min: null, stdDev: null };
}
const sum = validValues.reduce((acc, val) => acc + val, 0);
const mean = sum / validValues.length;
const max = Math.max(...validValues);
const min = Math.min(...validValues);
const squaredDifferences = validValues.map(val => Math.pow(val - mean, 2));
const variance = squaredDifferences.reduce((acc, val) => acc + val, 0) / validValues.length;
const stdDev = Math.sqrt(variance);
return { mean, max, min, stdDev };
}
/**
* Calculate statistics for a series of vectors
* @param {Array} vectors - Array of vector objects with x, y properties
* @returns {Object} Statistics for magnitude, x, and y
*/
function calculateVectorStats(vectors) {
// Filter out null/undefined vectors
const validVectors = vectors.filter(vec => vec !== null && vec !== undefined);
if (validVectors.length === 0) {
return {
magnitude: { mean: null, max: null, min: null, stdDev: null },
x: { mean: null, max: null, min: null, stdDev: null },
y: { mean: null, max: null, min: null, stdDev: null }
};
}
// Extract components and magnitudes
const xValues = validVectors.map(vec => vec.x).filter(val => val !== null && !isNaN(val));
const yValues = validVectors.map(vec => vec.y).filter(val => val !== null && !isNaN(val));
const magnitudes = validVectors.map(vec => vec.magnitude).filter(val => val !== null && !isNaN(val));
return {
magnitude: calculateTimeSeriesStats(magnitudes),
x: calculateTimeSeriesStats(xValues),
y: calculateTimeSeriesStats(yValues)
};
}
module.exports = {
calculate
};