UNPKG

bowling-analysis-system

Version:

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

293 lines (239 loc) 9.94 kB
/** * @module bowling_analysis/metrics/EventDetectionUtils * @description Utility functions for event detection and validation */ /** * Derive velocity metrics from position data if they don't exist * @param {Object} metrics - The metrics object containing time series data * @returns {Object} The metrics object with derived velocity metrics */ function deriveVelocityFromPosition(metrics) { if (!metrics || !metrics.timeSeries) { return metrics; } // Check if velocity metrics already exist const hasVelocityData = metrics.timeSeries.velocity && (metrics.timeSeries.velocity.leftAnkleVelocity || metrics.timeSeries.velocity.leftFootVelocity || metrics.timeSeries.velocity.rightAnkleVelocity || metrics.timeSeries.velocity.rightFootVelocity); if (hasVelocityData) { return metrics; // Velocity data already exists } console.log('Deriving velocity metrics from position data'); // Initialize velocity category if needed if (!metrics.timeSeries.velocity) { metrics.timeSeries.velocity = {}; } // Extract position data const leftAnkleHeight = metrics.timeSeries?.position?.leftAnkleHeight || metrics.timeSeries?.position?.leftFootHeight; const rightAnkleHeight = metrics.timeSeries?.position?.rightAnkleHeight || metrics.timeSeries?.position?.rightFootHeight; const leftAnkleX = metrics.timeSeries?.position?.leftAnkleX || metrics.timeSeries?.position?.leftFootX; const rightAnkleX = metrics.timeSeries?.position?.rightAnkleX || metrics.timeSeries?.position?.rightFootX; // Derive velocities if position data exists if (leftAnkleHeight && leftAnkleHeight.length > 1) { metrics.timeSeries.velocity.leftAnkleVelocity = calculateVelocityFromPosition(leftAnkleHeight, leftAnkleX); console.log('Derived left ankle velocity from position data'); } if (rightAnkleHeight && rightAnkleHeight.length > 1) { metrics.timeSeries.velocity.rightAnkleVelocity = calculateVelocityFromPosition(rightAnkleHeight, rightAnkleX); console.log('Derived right ankle velocity from position data'); } // If we have ankle joint angles, also derive angular velocities if (metrics.timeSeries.angles) { const elbowAngle = metrics.timeSeries.angles.elbowFlexion; const shoulderAngle = metrics.timeSeries.angles.shoulderRotation; if (elbowAngle && elbowAngle.length > 1) { metrics.timeSeries.velocity.elbowAngularVelocity = calculateAngularVelocity(elbowAngle); console.log('Derived elbow angular velocity from angle data'); } if (shoulderAngle && shoulderAngle.length > 1) { metrics.timeSeries.velocity.shoulderAngularVelocity = calculateAngularVelocity(shoulderAngle); console.log('Derived shoulder angular velocity from angle data'); } } return metrics; } /** * Calculate angular velocity from angle data * @param {Array<number>} angleData - Array of angle values * @returns {Array<number>} Array of calculated angular velocity values */ function calculateAngularVelocity(angleData) { if (!angleData || angleData.length < 2) { return []; } const angularVelocity = [0]; // First frame has zero velocity for (let i = 1; i < angleData.length; i++) { if (angleData[i-1] === null || angleData[i] === null) { angularVelocity.push(null); continue; } // Calculate angular velocity as the rate of change of angle const velocityValue = Math.abs(angleData[i] - angleData[i-1]); angularVelocity.push(velocityValue); } return angularVelocity; } /** * Calculate velocity from position data using both vertical and horizontal components * @param {Array<number>} heightData - Array of height values * @param {Array<number>} xData - Array of x position values * @returns {Array<number>} Array of calculated velocity values */ function calculateVelocityFromPosition(heightData, xData) { if (!heightData || heightData.length < 2) { return []; } const velocities = [0]; // First frame has zero velocity for (let i = 1; i < heightData.length; i++) { if (heightData[i-1] === null || heightData[i] === null) { velocities.push(null); continue; } // Calculate vertical velocity component const verticalDelta = Math.abs(heightData[i] - heightData[i-1]); // Calculate horizontal velocity component if available let horizontalDelta = 0; if (xData && xData[i] !== null && xData[i-1] !== null) { horizontalDelta = Math.abs(xData[i] - xData[i-1]); } // Combined velocity (Pythagorean theorem) const velocity = Math.sqrt(verticalDelta * verticalDelta + horizontalDelta * horizontalDelta); velocities.push(velocity); } return velocities; } /** * Get a metric value from a nested path * @param {Object} metrics - The metrics object * @param {string} path - The dot-notation path to the metric * @returns {any} The metric value or undefined if not found */ function getMetricByPath(metrics, path) { if (!metrics || !path) { return undefined; } const parts = path.split('.'); let current = metrics; for (const part of parts) { if (current === undefined || current === null) { return undefined; } current = current[part]; } return current; } /** * Detect events from moments data, mapping from moments to standardized event names * @param {Object} metrics - The metrics object that contains moments data * @returns {Object} Object with detected events */ function detectEventsFromMoments(metrics) { const events = {}; if (!metrics || !metrics.moments) { return events; } // Map from moments keys to standardized event names const eventMapping = { 'ball_release': 'releasePoint', 'left_foot_plant': 'frontFootLanding', 'right_foot_plant': 'backFootLanding' }; // Process each moment type Object.keys(eventMapping).forEach(momentKey => { if (!metrics.moments[momentKey] || !metrics.moments[momentKey].length) { return; } const standardName = eventMapping[momentKey]; const frames = metrics.moments[momentKey]; // For most events, we use the first frame of the moment let frameToUse = frames[0]; // For foot plants, find contiguous sequences and use first frame if (momentKey === 'left_foot_plant' || momentKey === 'right_foot_plant') { // Sort frames const sortedFrames = [...frames].sort((a, b) => a - b); // Find sequences of contiguous frames const sequences = []; let currentSequence = [sortedFrames[0]]; for (let i = 1; i < sortedFrames.length; i++) { if (sortedFrames[i] === sortedFrames[i-1] + 1) { // Continue current sequence currentSequence.push(sortedFrames[i]); } else { // Start a new sequence sequences.push(currentSequence); currentSequence = [sortedFrames[i]]; } } // Add final sequence if (currentSequence.length > 0) { sequences.push(currentSequence); } // Use the first frame of the longest sequence if (sequences.length > 0) { // Sort sequences by length (descending) sequences.sort((a, b) => b.length - a.length); frameToUse = sequences[0][0]; // First frame of longest sequence } } // Create the event events[standardName] = { frameIndex: frameToUse, confidence: 1.0, // High confidence for moments-based detection method: 'moments_analysis' }; // Add side information for foot landings if (momentKey === 'left_foot_plant') { events[standardName].side = 'left'; } else if (momentKey === 'right_foot_plant') { events[standardName].side = 'right'; } }); return events; } /** * Validate that events follow the proper biomechanical sequence * @param {Object} events - The detected events * @returns {boolean} True if the sequence is valid, false otherwise */ function validateEventSequence(events) { if (!events) { return false; } const backFootFrame = events.backFootLanding?.frameIndex; const frontFootFrame = events.frontFootLanding?.frameIndex; const releaseFrame = events.releasePoint?.frameIndex; // Log detected events console.log(`Validating event sequence: backFoot=${backFootFrame}, frontFoot=${frontFootFrame}, release=${releaseFrame}`); // Check if we have all required events if (backFootFrame === null || backFootFrame === undefined || frontFootFrame === null || frontFootFrame === undefined || releaseFrame === null || releaseFrame === undefined) { console.warn('Event sequence invalid: Missing one or more required events'); return false; } // Ensure proper sequence: backFootLanding < frontFootLanding < releasePoint if (backFootFrame >= frontFootFrame) { console.warn(`Event sequence invalid: Back foot landing (${backFootFrame}) should occur before front foot landing (${frontFootFrame})`); return false; } if (frontFootFrame >= releaseFrame) { console.warn(`Event sequence invalid: Front foot landing (${frontFootFrame}) should occur before release point (${releaseFrame})`); return false; } console.log('Event sequence valid: Proper biomechanical sequence detected'); return true; } module.exports = { deriveVelocityFromPosition, calculateVelocityFromPosition, calculateAngularVelocity, getMetricByPath, detectEventsFromMoments, validateEventSequence };