UNPKG

bowling-analysis-system

Version:

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

638 lines (540 loc) 21.6 kB
/** * @module bowling_analysis/metrics/calculators/CompoundMetricsCalculator * @description Calculator for generating compound metrics by combining related measurements */ /** * Calculate compound metrics by combining related metrics * @param {Array} keypointData - Array of keypoint frames * @param {Array} validFrames - Array of valid keypoint frames * @param {Object} options - Calculator options * @returns {Promise<Object>} Compound metrics */ async function calculate(keypointData, validFrames, options = {}) { try { const { debug, includeTimeSeries, existingMetrics, existingTimeSeries } = options; // Initialize result const result = { compound: {}, timeSeries: {} }; // Need existing metrics to create compound metrics if (!existingMetrics) { console.warn('No metrics data provided to CompoundMetricsCalculator'); return { compound: {}, timeSeries: {} }; } // Create compound metrics that combine related measurements // 1. Delivery stability index (combines balance + position consistency) result.compound.deliveryStabilityIndex = calculateDeliveryStabilityIndex(existingMetrics, existingTimeSeries); // 2. Power efficiency ratio (combines power output with technique efficiency) result.compound.powerEfficiencyRatio = calculatePowerEfficiencyRatio(existingMetrics, existingTimeSeries); // 3. Technical synchronization (combines timing of related joints) result.compound.technicalSynchronization = calculateTechnicalSynchronization(existingMetrics, existingTimeSeries); // 4. Dynamic balance index (combines movement rate with balance maintenance) result.compound.dynamicBalanceIndex = calculateDynamicBalanceIndex(existingMetrics, existingTimeSeries); // 5. Biomechanical loading (combines acceleration and joint stress) result.compound.biomechanicalLoading = calculateBiomechanicalLoading(existingMetrics, existingTimeSeries); // Generate time series for compound metrics if enabled if (includeTimeSeries && existingTimeSeries) { // Generate time series for each compound metric const timeSeriesPromises = [ generateDeliveryStabilityTimeSeries(existingTimeSeries), generatePowerEfficiencyTimeSeries(existingTimeSeries), generateTechnicalSyncTimeSeries(existingTimeSeries), generateDynamicBalanceTimeSeries(existingTimeSeries), generateBiomechanicalLoadingTimeSeries(existingTimeSeries) ]; const timeSeriesResults = await Promise.all(timeSeriesPromises); // Merge time series results timeSeriesResults.forEach(tsSeries => { if (tsSeries) { Object.assign(result.timeSeries, tsSeries); } }); } return result; } catch (error) { console.error(`Error calculating compound metrics: ${error.message}`); return { compound: {}, timeSeries: {} }; } } /** * Calculate delivery stability index * @param {Object} metrics - Metrics data * @param {Object} timeSeries - Time series data * @returns {Object} Delivery stability index */ function calculateDeliveryStabilityIndex(metrics, timeSeries) { // Initialize with default values const result = { overall: null, approach: null, delivery: null, followThrough: null, components: {} }; try { // Extract balance metrics const balanceMetrics = metrics.balance || {}; // Extract position metrics const positionMetrics = metrics.position || {}; // Factor weights for components const weights = { posturalSway: 0.25, weightDistribution: 0.25, positionVariability: 0.2, trajectoryConsistency: 0.3 }; // Calculate component scores // 1. Postural sway (lower is better) let posturalSwayScore = null; if (balanceMetrics.posturalSways && balanceMetrics.posturalSways.left && balanceMetrics.posturalSways.right) { // Use raw postural sway values (no normalization) const averageSway = (balanceMetrics.posturalSways.left + balanceMetrics.posturalSways.right) / 2; posturalSwayScore = averageSway; } // 2. Weight distribution (more balanced is better) let weightDistributionScore = null; if (balanceMetrics.weightDistributions && balanceMetrics.weightDistributions.asymmetry !== undefined) { // Use raw asymmetry value weightDistributionScore = balanceMetrics.weightDistributions.asymmetry; } // 3. Position variability (lower is better) let positionVariabilityScore = null; if (positionMetrics.stability && positionMetrics.stability.stdDev !== undefined) { // Use raw standard deviation value positionVariabilityScore = positionMetrics.stability.stdDev; } // 4. Trajectory consistency (higher is better) let trajectoryConsistencyScore = null; if (positionMetrics.consistency !== undefined) { trajectoryConsistencyScore = positionMetrics.consistency; } // Store component scores result.components = { posturalSway: posturalSwayScore, weightDistribution: weightDistributionScore, positionVariability: positionVariabilityScore, trajectoryConsistency: trajectoryConsistencyScore }; // Calculate weighted overall score let totalScore = 0; let totalWeight = 0; for (const [component, score] of Object.entries(result.components)) { if (score !== null) { totalScore += score * weights[component]; totalWeight += weights[component]; } } // Calculate overall score if we have components if (totalWeight > 0) { result.overall = totalScore / totalWeight; // Derive phase-specific scores using the same weights // These would normally use phase-specific data result.approach = 0.9 * result.overall + 0.1 * Math.random(); // Placeholder result.delivery = 0.9 * result.overall + 0.1 * Math.random(); // Placeholder result.followThrough = 0.9 * result.overall + 0.1 * Math.random(); // Placeholder } return result; } catch (error) { console.error(`Error calculating delivery stability index: ${error.message}`); return result; } } /** * Calculate power efficiency ratio * @param {Object} metrics - Metrics data * @param {Object} timeSeries - Time series data * @returns {Object} Power efficiency ratio */ function calculatePowerEfficiencyRatio(metrics, timeSeries) { // Initialize with default values const result = { overall: null, components: {} }; try { // Extract power metrics const powerMetrics = metrics.power || {}; // Extract efficiency metrics const efficiencyMetrics = metrics.efficiency || {}; // Extract velocity metrics const velocityMetrics = metrics.velocity || {}; // Factor weights const weights = { powerOutput: 0.3, energyEfficiency: 0.3, velocityGeneration: 0.4 }; // 1. Power output score let powerOutputScore = null; if (powerMetrics.totalPower !== undefined) { // Use raw power value powerOutputScore = powerMetrics.totalPower; } // 2. Energy efficiency score let energyEfficiencyScore = null; if (efficiencyMetrics && efficiencyMetrics.overall !== undefined) { energyEfficiencyScore = efficiencyMetrics.overall; } // 3. Velocity generation score let velocityGenerationScore = null; if (velocityMetrics.ballVelocity !== undefined) { // Use raw velocity value velocityGenerationScore = velocityMetrics.ballVelocity; } // Store component scores result.components = { powerOutput: powerOutputScore, energyEfficiency: energyEfficiencyScore, velocityGeneration: velocityGenerationScore }; // Calculate weighted overall score let totalScore = 0; let totalWeight = 0; for (const [component, score] of Object.entries(result.components)) { if (score !== null) { totalScore += score * weights[component]; totalWeight += weights[component]; } } // Calculate overall score if we have components if (totalWeight > 0) { result.overall = totalScore / totalWeight; } return result; } catch (error) { console.error(`Error calculating power efficiency ratio: ${error.message}`); return result; } } /** * Calculate technical synchronization * @param {Object} metrics - Metrics data * @param {Object} timeSeries - Time series data * @returns {Object} Technical synchronization */ function calculateTechnicalSynchronization(metrics, timeSeries) { // Initialize with default values const result = { overall: null, components: {} }; try { // Extract angle metrics const angleMetrics = metrics.angles || {}; // Extract timing metrics const timingMetrics = metrics.timing || {}; // Factor weights const weights = { armLegSynchronization: 0.4, upperBodyAlignment: 0.3, timingPrecision: 0.3 }; // 1. Arm-leg synchronization let armLegSyncScore = null; // This would compare arm and leg movements to detect synchronization // For now using a placeholder if (angleMetrics.armAngles && angleMetrics.kneeFlexions) { // Placeholder calculation - in a real implementation, would // calculate correlation or phase difference between arm and leg movements armLegSyncScore = 0.7 + (Math.random() * 0.2); // Placeholder between 0.7-0.9 } // 2. Upper body alignment let upperBodyAlignmentScore = null; if (angleMetrics.shoulderRotations && angleMetrics.shoulderTilts) { // Placeholder calculation upperBodyAlignmentScore = 0.6 + (Math.random() * 0.3); // Placeholder between 0.6-0.9 } // 3. Timing precision let timingPrecisionScore = null; if (timingMetrics && timingMetrics.approach !== undefined && timingMetrics.release !== undefined) { // Placeholder calculation timingPrecisionScore = 0.7 + (Math.random() * 0.2); // Placeholder between 0.7-0.9 } // Store component scores result.components = { armLegSynchronization: armLegSyncScore, upperBodyAlignment: upperBodyAlignmentScore, timingPrecision: timingPrecisionScore }; // Calculate weighted overall score let totalScore = 0; let totalWeight = 0; for (const [component, score] of Object.entries(result.components)) { if (score !== null) { totalScore += score * weights[component]; totalWeight += weights[component]; } } // Calculate overall score if we have components if (totalWeight > 0) { result.overall = totalScore / totalWeight; } return result; } catch (error) { console.error(`Error calculating technical synchronization: ${error.message}`); return result; } } /** * Calculate dynamic balance index * @param {Object} metrics - Metrics data * @param {Object} timeSeries - Time series data * @returns {Object} Dynamic balance index */ function calculateDynamicBalanceIndex(metrics, timeSeries) { // Initialize with default values const result = { overall: null, phases: {}, components: {} }; try { // Extract balance metrics const balanceMetrics = metrics.balance || {}; // Extract velocity metrics const velocityMetrics = metrics.velocity || {}; // Factor weights const weights = { balanceMaintenance: 0.4, movementSpeed: 0.3, stabilityControl: 0.3 }; // 1. Balance maintenance let balanceMaintenanceScore = null; if (balanceMetrics.centerOfPressures && balanceMetrics.centerOfPressures.stdDev !== undefined) { // Use raw center of pressure stdDev value balanceMaintenanceScore = balanceMetrics.centerOfPressures.stdDev; } // 2. Movement speed (use raw velocity) let movementSpeedScore = null; if (velocityMetrics.approachVelocity !== undefined) { // Use raw approach velocity movementSpeedScore = velocityMetrics.approachVelocity; } // 3. Stability control let stabilityControlScore = null; if (balanceMetrics.stabilitiesIndices && balanceMetrics.stabilitiesIndices.overall !== undefined) { stabilityControlScore = balanceMetrics.stabilitiesIndices.overall; } // Store component scores result.components = { balanceMaintenance: balanceMaintenanceScore, movementSpeed: movementSpeedScore, stabilityControl: stabilityControlScore }; // Calculate weighted overall score let totalScore = 0; let totalWeight = 0; for (const [component, score] of Object.entries(result.components)) { if (score !== null) { totalScore += score * weights[component]; totalWeight += weights[component]; } } // Calculate overall score if we have components if (totalWeight > 0) { result.overall = totalScore / totalWeight; // Calculate phase-specific scores result.phases = { approach: 0.9 * result.overall + 0.1 * Math.random(), // Placeholder delivery: 0.9 * result.overall + 0.1 * Math.random(), // Placeholder followThrough: 0.9 * result.overall + 0.1 * Math.random() // Placeholder }; } return result; } catch (error) { console.error(`Error calculating dynamic balance index: ${error.message}`); return result; } } /** * Calculate biomechanical loading * @param {Object} metrics - Metrics data * @param {Object} timeSeries - Time series data * @returns {Object} Biomechanical loading */ function calculateBiomechanicalLoading(metrics, timeSeries) { // Initialize with default values const result = { overall: null, joints: {}, components: {} }; try { // We would need acceleration data and angle metrics const accelerationMetrics = metrics.acceleration || {}; const angleMetrics = metrics.angles || {}; // Factor weights const weights = { jointAcceleration: 0.4, angleVariation: 0.3, loadingRate: 0.3 }; // 1. Joint acceleration (using whatever acceleration data is available) let jointAccelerationScore = null; if (accelerationMetrics && Object.keys(accelerationMetrics).length > 0) { // Placeholder calculation jointAccelerationScore = 0.6 + (Math.random() * 0.3); // Placeholder between 0.6-0.9 } // 2. Angle variation (rapid changes in joint angles) let angleVariationScore = null; if (angleMetrics && Object.keys(angleMetrics).length > 0) { // Placeholder calculation angleVariationScore = 0.5 + (Math.random() * 0.4); // Placeholder between 0.5-0.9 } // 3. Loading rate let loadingRateScore = null; // Would typically combine acceleration and force metrics // Using placeholder for now loadingRateScore = 0.7 + (Math.random() * 0.2); // Placeholder between 0.7-0.9 // Store component scores result.components = { jointAcceleration: jointAccelerationScore, angleVariation: angleVariationScore, loadingRate: loadingRateScore }; // Calculate weighted overall score let totalScore = 0; let totalWeight = 0; for (const [component, score] of Object.entries(result.components)) { if (score !== null) { totalScore += score * weights[component]; totalWeight += weights[component]; } } // Calculate overall score if we have components if (totalWeight > 0) { result.overall = totalScore / totalWeight; // Calculate joint-specific loading result.joints = { shoulder: 0.9 * result.overall + 0.1 * Math.random(), // Placeholder elbow: 0.8 * result.overall + 0.2 * Math.random(), // Placeholder wrist: 0.85 * result.overall + 0.15 * Math.random(), // Placeholder knee: 0.95 * result.overall + 0.05 * Math.random(), // Placeholder ankle: 0.9 * result.overall + 0.1 * Math.random() // Placeholder }; } return result; } catch (error) { console.error(`Error calculating biomechanical loading: ${error.message}`); return result; } } /** * Generate delivery stability time series * @param {Object} timeSeries - Time series data * @returns {Object} Time series for delivery stability */ async function generateDeliveryStabilityTimeSeries(timeSeries) { if (!timeSeries) return null; try { const result = {}; // We need balance and position time series const balanceTimeSeries = timeSeries.balance || {}; const positionTimeSeries = timeSeries.position || {}; // Check if we have the necessary data const hasSwayData = balanceTimeSeries['posturalSways.left'] && balanceTimeSeries['posturalSways.right']; const hasPositionData = positionTimeSeries['centerOfMass'] || positionTimeSeries['bodyPosition']; if (!hasSwayData && !hasPositionData) { return null; } // Get frame count from any available time series let frameCount = 0; if (hasSwayData) { frameCount = balanceTimeSeries['posturalSways.left'].length; } else if (hasPositionData) { frameCount = positionTimeSeries['centerOfMass'] ? positionTimeSeries['centerOfMass'].length : positionTimeSeries['bodyPosition'].length; } if (frameCount === 0) return null; // Create stabilityIndex time series const stabilityTimeSeries = new Array(frameCount).fill(null); // For each frame, calculate a stability score for (let i = 0; i < frameCount; i++) { // Skip if we don't have data for this frame if ((hasSwayData && (balanceTimeSeries['posturalSways.left'][i] === null || balanceTimeSeries['posturalSways.right'][i] === null)) || (hasPositionData && positionTimeSeries['centerOfMass'] && positionTimeSeries['centerOfMass'][i] === null)) { continue; } // Calculate stability score for this frame (placeholder calculation) let stabilityScore = 0.5; // Default score let scoreComponents = 0; // Add sway component if available if (hasSwayData && balanceTimeSeries['posturalSways.left'][i] !== null && balanceTimeSeries['posturalSways.right'][i] !== null) { const avgSway = (balanceTimeSeries['posturalSways.left'][i] + balanceTimeSeries['posturalSways.right'][i]) / 2; // Use raw sway value (no normalization) stabilityScore += avgSway; scoreComponents++; } // Add position component if available if (hasPositionData && positionTimeSeries['centerOfMass'] && positionTimeSeries['centerOfMass'][i] !== null) { // Placeholder calculation - would normally be based on position stability const positionScore = 0.7; // Placeholder stabilityScore += positionScore; scoreComponents++; } // Calculate average if we have components if (scoreComponents > 0) { stabilityTimeSeries[i] = stabilityScore / scoreComponents; } } // Add to result result['compound.deliveryStability'] = stabilityTimeSeries; return result; } catch (error) { console.error(`Error generating delivery stability time series: ${error.message}`); return null; } } /** * Generate power efficiency time series * @param {Object} timeSeries - Time series data * @returns {Object} Time series for power efficiency */ async function generatePowerEfficiencyTimeSeries(timeSeries) { // Placeholder implementation - would combine power and velocity metrics return null; } /** * Generate technical synchronization time series * @param {Object} timeSeries - Time series data * @returns {Object} Time series for technical synchronization */ async function generateTechnicalSyncTimeSeries(timeSeries) { // Placeholder implementation - would analyze arm and leg synchronization return null; } /** * Generate dynamic balance time series * @param {Object} timeSeries - Time series data * @returns {Object} Time series for dynamic balance */ async function generateDynamicBalanceTimeSeries(timeSeries) { // Placeholder implementation - would combine balance and movement data return null; } /** * Generate biomechanical loading time series * @param {Object} timeSeries - Time series data * @returns {Object} Time series for biomechanical loading */ async function generateBiomechanicalLoadingTimeSeries(timeSeries) { // Placeholder implementation - would calculate joint loading over time return null; } module.exports = { calculate };