UNPKG

bowling-analysis-system

Version:

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

255 lines (211 loc) 7.63 kB
/** * @module metrics/calculations/TimeSeriesUtilities * @description Utility functions for time series analysis and metrics processing */ /** * Get a metric value from a specific frame in time series data * @param {Object} metrics - Metrics data object * @param {string} metricPath - Dot-notation path to the metric (e.g., "angles.elbowFlexions.left") * @param {number} frameIndex - Frame index * @returns {number|null} Metric value or null if not available */ function getMetricValueAtFrame(metrics, metricPath, frameIndex) { if (!metrics || !metricPath || typeof metricPath !== 'string') { return null; } // Parse the metric path const parts = metricPath.split('.'); if (parts.length < 2) { return null; } const category = parts[0]; const remainingPath = parts.slice(1).join('.'); // Ensure we have timeSeries data if (!metrics.timeSeries || !metrics.timeSeries[category]) { return null; } // Handle nested paths (e.g., "angles.elbowFlexions.left") let data = metrics.timeSeries[category]; const pathParts = remainingPath.split('.'); for (let i = 0; i < pathParts.length; i++) { if (!data[pathParts[i]]) { return null; } data = data[pathParts[i]]; } // If we've reached an array, get the value at the specified frame index if (Array.isArray(data)) { return frameIndex >= 0 && frameIndex < data.length ? data[frameIndex] : null; } // If we've reached an object with 'values' property (old time series format) if (data.values && Array.isArray(data.values)) { return frameIndex >= 0 && frameIndex < data.values.length ? data.values[frameIndex] : null; } return null; } /** * Calculate the average value of a metric over a range of frames * @param {Object} metrics - Metrics data object * @param {string} metricPath - Dot-notation path to the metric * @param {number} startFrame - Start frame index (inclusive) * @param {number} endFrame - End frame index (inclusive) * @returns {number|null} Average metric value or null if not available */ function getMetricAverageOverFrames(metrics, metricPath, startFrame, endFrame) { if (!metrics || !metricPath || typeof metricPath !== 'string') { return null; } // Parse the metric path const parts = metricPath.split('.'); if (parts.length < 2) { return null; } const category = parts[0]; const remainingPath = parts.slice(1).join('.'); // Ensure we have timeSeries data if (!metrics.timeSeries || !metrics.timeSeries[category]) { return null; } // Get the data array let data = metrics.timeSeries[category]; const pathParts = remainingPath.split('.'); for (let i = 0; i < pathParts.length; i++) { if (!data[pathParts[i]]) { return null; } data = data[pathParts[i]]; } // Get the values array let values = null; if (Array.isArray(data)) { values = data; } else if (data.values && Array.isArray(data.values)) { values = data.values; } if (!values) { return null; } // Calculate average over frame range const start = Math.max(0, startFrame); const end = Math.min(values.length - 1, endFrame); if (start > end) { return null; } let sum = 0; let count = 0; for (let i = start; i <= end; i++) { if (values[i] !== null && values[i] !== undefined) { sum += values[i]; count++; } } return count > 0 ? sum / count : null; } /** * Calculate standard deviation of a metric over a range of frames * @param {Object} metrics - Metrics data object * @param {string} metricPath - Dot-notation path to the metric * @param {number} startFrame - Start frame index (inclusive) * @param {number} endFrame - End frame index (inclusive) * @returns {number|null} Standard deviation or null if not available */ function getMetricStdDevOverFrames(metrics, metricPath, startFrame, endFrame) { if (!metrics || !metricPath || typeof metricPath !== 'string') { return null; } // Parse the metric path const parts = metricPath.split('.'); if (parts.length < 2) { return null; } const category = parts[0]; const remainingPath = parts.slice(1).join('.'); // Ensure we have timeSeries data if (!metrics.timeSeries || !metrics.timeSeries[category]) { return null; } // Get the data array let data = metrics.timeSeries[category]; const pathParts = remainingPath.split('.'); for (let i = 0; i < pathParts.length; i++) { if (!data[pathParts[i]]) { return null; } data = data[pathParts[i]]; } // Get the values array let values = null; if (Array.isArray(data)) { values = data; } else if (data.values && Array.isArray(data.values)) { values = data.values; } if (!values) { return null; } // Calculate standard deviation over frame range const start = Math.max(0, startFrame); const end = Math.min(values.length - 1, endFrame); if (start > end) { return null; } // Calculate mean first let sum = 0; let count = 0; for (let i = start; i <= end; i++) { if (values[i] !== null && values[i] !== undefined && !isNaN(values[i])) { sum += values[i]; count++; } } if (count <= 1) { return null; } const mean = sum / count; // Calculate squared differences let sumSquaredDiff = 0; for (let i = start; i <= end; i++) { if (values[i] !== null && values[i] !== undefined && !isNaN(values[i])) { sumSquaredDiff += Math.pow(values[i] - mean, 2); } } return Math.sqrt(sumSquaredDiff / count); } /** * Validate event object * @param {Object} event - Event object to validate * @returns {boolean} True if valid, false otherwise */ function isValidEvent(event) { return event && typeof event === 'object' && typeof event.name === 'string' && typeof event.frameIndex === 'number' && event.frameIndex >= 0 && typeof event.confidence === 'number' && event.confidence >= 0 && event.confidence <= 1; } /** * Format frame index as time string based on frame rate * @param {number} frameIndex - Frame index * @param {number} fps - Frames per second * @returns {string} Formatted time string (mm:ss.ms) */ function formatFrameTime(frameIndex, fps = 30) { if (typeof frameIndex !== 'number' || isNaN(frameIndex) || frameIndex < 0) { return '00:00.000'; } const totalSeconds = frameIndex / fps; const minutes = Math.floor(totalSeconds / 60); const seconds = Math.floor(totalSeconds % 60); const milliseconds = Math.floor((totalSeconds % 1) * 1000); return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`; } module.exports = { getMetricValueAtFrame, getMetricAverageOverFrames, getMetricStdDevOverFrames, isValidEvent, formatFrameTime };