bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
638 lines (540 loc) • 21.6 kB
JavaScript
/**
* @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
};