UNPKG

bowling-analysis-system

Version:

A comprehensive system for analyzing bowling techniques using video processing and metrics calculation

189 lines (153 loc) 7.32 kB
/** * @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 };