bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
571 lines (473 loc) • 20.6 kB
JavaScript
/**
* @module metrics/calculations/PowerCalculations
* @description Functions for calculating power-related metrics
*/
const { getSafeLandmark, calculateDistance, createVector } = require('./MetricsUtilities');
const velocityCalculations = require('./VelocityCalculations');
const metricsConfig = require('../../../config/metricsConfig');
/**
* Calculate power generation for a specific joint based on joint velocities
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @param {number} jointIndex - Index of the joint to calculate power for
* @returns {number|null} Power value for the joint, or null if calculation is not possible
*/
function calculateJointPower(currentLandmarks, previousLandmarks, jointIndex) {
if (!currentLandmarks || !previousLandmarks) {
return null;
}
const joint = getSafeLandmark(currentLandmarks, jointIndex);
const prevJoint = getSafeLandmark(previousLandmarks, jointIndex);
if (!joint || !prevJoint) {
return null;
}
// Calculate joint velocity
const velocity = Math.sqrt(
Math.pow(joint.x - prevJoint.x, 2) +
Math.pow(joint.y - prevJoint.y, 2) +
Math.pow(joint.z - prevJoint.z, 2)
);
// Estimate force based on velocity
// F = m * a (approximated with velocity)
const forceFactor = metricsConfig?.power?.physics?.forceFactor || 1.0;
const force = velocity * forceFactor;
// Power = Force * Velocity
return force * velocity;
}
/**
* Calculate wrist power
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {Object} Left and right wrist power metrics
*/
function calculateWristPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Get wrist landmarks (15 = right wrist, 16 = left wrist)
const leftWristPower = calculateJointPower(currentLandmarks, previousLandmarks, 16);
const rightWristPower = calculateJointPower(currentLandmarks, previousLandmarks, 15);
return {
left: leftWristPower,
right: rightWristPower
};
}
/**
* Calculate elbow power
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {Object} Left and right elbow power metrics
*/
function calculateElbowPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Get elbow landmarks (13 = right elbow, 14 = left elbow)
const leftElbowPower = calculateJointPower(currentLandmarks, previousLandmarks, 14);
const rightElbowPower = calculateJointPower(currentLandmarks, previousLandmarks, 13);
return {
left: leftElbowPower,
right: rightElbowPower
};
}
/**
* Calculate shoulder power
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {Object} Left and right shoulder power metrics
*/
function calculateShoulderPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Get shoulder landmarks (11 = right shoulder, 12 = left shoulder)
const leftShoulderPower = calculateJointPower(currentLandmarks, previousLandmarks, 12);
const rightShoulderPower = calculateJointPower(currentLandmarks, previousLandmarks, 11);
return {
left: leftShoulderPower,
right: rightShoulderPower
};
}
/**
* Calculate hip power
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {Object} Left and right hip power metrics
*/
function calculateHipPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Get hip landmarks (23 = right hip, 24 = left hip)
const leftHipPower = calculateJointPower(currentLandmarks, previousLandmarks, 24);
const rightHipPower = calculateJointPower(currentLandmarks, previousLandmarks, 23);
return {
left: leftHipPower,
right: rightHipPower
};
}
/**
* Calculate knee power
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {Object} Left and right knee power metrics
*/
function calculateKneePower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Get knee landmarks (25 = right knee, 26 = left knee)
const leftKneePower = calculateJointPower(currentLandmarks, previousLandmarks, 26);
const rightKneePower = calculateJointPower(currentLandmarks, previousLandmarks, 25);
return {
left: leftKneePower,
right: rightKneePower
};
}
/**
* Calculate ankle power
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {Object} Left and right ankle power metrics
*/
function calculateAnklePower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Get ankle landmarks (27 = right ankle, 28 = left ankle)
const leftAnklePower = calculateJointPower(currentLandmarks, previousLandmarks, 28);
const rightAnklePower = calculateJointPower(currentLandmarks, previousLandmarks, 27);
return {
left: leftAnklePower,
right: rightAnklePower
};
}
/**
* Calculate arm power (combined power from shoulder, elbow, and wrist)
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {Object} Left and right arm power metrics
*/
function calculateArmPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Calculate individual joint powers
const shoulders = calculateShoulderPower(currentLandmarks, previousLandmarks);
const elbows = calculateElbowPower(currentLandmarks, previousLandmarks);
const wrists = calculateWristPower(currentLandmarks, previousLandmarks);
if (!shoulders || !elbows || !wrists) return null;
// Weight factors for each joint's contribution (from config with fallbacks)
const shoulderWeight = metricsConfig?.power?.weights?.shoulderWeight ?? 0.3;
const elbowWeight = metricsConfig?.power?.weights?.elbowWeight ?? 0.3;
const wristWeight = metricsConfig?.power?.weights?.wristWeight ?? 0.4;
// Calculate weighted sum for left and right arms
const leftArmPower =
(shoulders.left * shoulderWeight) +
(elbows.left * elbowWeight) +
(wrists.left * wristWeight);
const rightArmPower =
(shoulders.right * shoulderWeight) +
(elbows.right * elbowWeight) +
(wrists.right * wristWeight);
return {
left: leftArmPower,
right: rightArmPower
};
}
/**
* Calculate leg power (combined power from hip, knee, and ankle)
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {Object} Left and right leg power metrics
*/
function calculateLegPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Calculate individual joint powers
const hips = calculateHipPower(currentLandmarks, previousLandmarks);
const knees = calculateKneePower(currentLandmarks, previousLandmarks);
const ankles = calculateAnklePower(currentLandmarks, previousLandmarks);
if (!hips || !knees || !ankles) return null;
// Weight factors for each joint's contribution (from config with fallbacks)
const hipWeight = metricsConfig?.power?.weights?.hipWeight ?? 0.4;
const kneeWeight = metricsConfig?.power?.weights?.kneeWeight ?? 0.3;
const ankleWeight = metricsConfig?.power?.weights?.ankleWeight ?? 0.3;
// Calculate weighted sum for left and right legs
const leftLegPower =
(hips.left * hipWeight) +
(knees.left * kneeWeight) +
(ankles.left * ankleWeight);
const rightLegPower =
(hips.right * hipWeight) +
(knees.right * kneeWeight) +
(ankles.right * ankleWeight);
return {
left: leftLegPower,
right: rightLegPower
};
}
/**
* Calculate total body power
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {number} Total body power value
*/
function calculateTotalBodyPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
const arms = calculateArmPower(currentLandmarks, previousLandmarks);
const legs = calculateLegPower(currentLandmarks, previousLandmarks);
if (!arms && !legs) return null;
// Calculate total power using combined metrics
const upperBodyPower = calculateUpperBodyPower(currentLandmarks, previousLandmarks);
const lowerBodyPower = calculateLowerBodyPower(currentLandmarks, previousLandmarks);
if (!upperBodyPower && !lowerBodyPower) return null;
// Weight factors from config
const upperBodyWeight = metricsConfig?.power?.weights?.upperBodyWeight ?? 0.5;
const lowerBodyWeight = metricsConfig?.power?.weights?.lowerBodyWeight ?? 0.5;
return (upperBodyPower * upperBodyWeight) + (lowerBodyPower * lowerBodyWeight);
}
/**
* Calculate power generation
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {number} Power generation value
*/
function calculatePowerGeneration(currentLandmarks, previousLandmarks) {
return calculateTotalBodyPower(currentLandmarks, previousLandmarks);
}
/**
* Calculate energy transfer ratio from legs to arms
* @param {Array} landmarks - Current frame landmarks
* @returns {number} Energy transfer ratio
*/
function calculateEnergyTransfer(landmarks) {
if (!landmarks) return null;
// Calculate power in arms and legs
const armPower = calculateArmPower(landmarks);
const legPower = calculateLegPower(landmarks);
// If either power is null or right/left values don't exist, return null
if (!armPower || !legPower ||
armPower.left === null || armPower.right === null ||
legPower.left === null || legPower.right === null) {
return null;
}
// Calculate total arm and leg power
const totalArmPower = (armPower.left + armPower.right) / 2;
const totalLegPower = (legPower.left + legPower.right) / 2;
// Avoid division by zero
if (totalLegPower === 0) return 0;
// Calculate transfer ratio (how much leg power transfers to arms)
// Clamped between 0 and 1
return Math.min(1, Math.max(0, totalArmPower / totalLegPower));
}
/**
* Calculate potential energy
* @param {Array} landmarks - Frame landmarks
* @returns {number} Potential energy
*/
function calculatePotentialEnergy(landmarks) {
if (!landmarks) return null;
// Use center of mass height for potential energy
const com = getSafeLandmark(landmarks, 0); // Using nose as a proxy for COM
if (!com) return null;
// Potential energy = mgh (mass * gravity * height)
// Return the raw height value as a proxy for potential energy
return Math.abs(com[1]);
}
/**
* Calculate kinetic energy
* @param {Array} landmarks - Current frame landmarks
* @returns {number} Kinetic energy
*/
function calculateKineticEnergy(landmarks) {
if (!landmarks) return null;
// Use total body power as a proxy for kinetic energy
const totalBodyPower = calculateTotalBodyPower(landmarks);
return totalBodyPower;
}
/**
* Calculate total energy
* @param {Array} landmarks - Current frame landmarks
* @returns {number} Total energy
*/
function calculateTotalEnergy(landmarks) {
if (!landmarks) return null;
// Calculate potential energy (future improvement: model this based on height)
const potentialEnergy = 0; // Simplified model
// Calculate kinetic energy
const kineticEnergy = calculateKineticEnergy(landmarks);
// If we don't have valid kinetic energy, return null
if (kineticEnergy === null) return null;
// Total energy is the sum of potential and kinetic energy
return potentialEnergy + kineticEnergy;
}
/**
* Calculate work done
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {number} Work done
*/
function calculateWorkDone(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
// Calculate current energy
const currentEnergy = calculateTotalEnergy(currentLandmarks);
// If we don't have valid energy values, we can't calculate work
if (currentEnergy === null) return null;
// We can't accurately calculate previous energy without previous-previous landmarks
// So instead, return the current energy as an approximation of work done
return currentEnergy;
}
/**
* Calculate power efficiency
* @param {Array} landmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {number} Power efficiency
*/
function calculatePowerEfficiency(landmarks, previousLandmarks) {
if (!landmarks || !previousLandmarks) return null;
// Calculate work done (useful energy output)
const workDone = calculateWorkDone(landmarks, previousLandmarks);
// Calculate total power input
const totalPower = calculateTotalBodyPower(landmarks);
// If either value is null, return null
if (workDone === null || totalPower === null || totalPower === 0) return null;
// Efficiency is useful work divided by total power
// Clamped between 0 and 1
return Math.min(1, Math.max(0, workDone / totalPower));
}
/**
* Calculate upper body power (combined power from arms and torso)
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {number} Upper body power value
*/
function calculateUpperBodyPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
const arms = calculateArmPower(currentLandmarks, previousLandmarks);
if (!arms) return null;
// Calculate the average of left and right arm power
const armsPower = arms.left !== null && arms.right !== null ?
(arms.left + arms.right) / 2 :
(arms.left !== null ? arms.left : (arms.right !== null ? arms.right : 0));
// Get torso power contribution
// Torso landmarks (11, 12, 23, 24 - shoulders and hips)
const torsoPower = (
calculateJointPower(currentLandmarks, previousLandmarks, 11) || 0 +
calculateJointPower(currentLandmarks, previousLandmarks, 12) || 0 +
calculateJointPower(currentLandmarks, previousLandmarks, 23) || 0 +
calculateJointPower(currentLandmarks, previousLandmarks, 24) || 0
) / 4;
// Weight factors from config
const armsWeight = metricsConfig?.power?.weights?.armsWeight ?? 0.7;
const torsoWeight = metricsConfig?.power?.weights?.torsoWeight ?? 0.3;
// Combine with weighted factors
return (armsPower * armsWeight) + (torsoPower * torsoWeight);
}
/**
* Calculate lower body power (combined power from legs and hips)
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @returns {number} Lower body power value
*/
function calculateLowerBodyPower(currentLandmarks, previousLandmarks) {
if (!currentLandmarks || !previousLandmarks) return null;
const legs = calculateLegPower(currentLandmarks, previousLandmarks);
if (!legs) return null;
// Calculate the average of left and right leg power
const legsPower = legs.left !== null && legs.right !== null ?
(legs.left + legs.right) / 2 :
(legs.left !== null ? legs.left : (legs.right !== null ? legs.right : 0));
// Get hip power contribution
const hips = calculateHipPower(currentLandmarks, previousLandmarks);
if (!hips) return legsPower; // Fall back to just leg power if hip power can't be calculated
const hipsPower = hips.left !== null && hips.right !== null ?
(hips.left + hips.right) / 2 :
(hips.left !== null ? hips.left : (hips.right !== null ? hips.right : 0));
// Weight factors from config
const legsWeight = metricsConfig?.power?.weights?.legsWeight ?? 0.7;
const hipsWeight = metricsConfig?.power?.weights?.hipsWeight ?? 0.3;
// Combine with weighted factors
return (legsPower * legsWeight) + (hipsPower * hipsWeight);
}
/**
* Calculate momentum for each body segment
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @param {number} timeDelta - Time between frames in ms
* @returns {Object} Object containing left and right momentum values
*/
function calculateMomentum(currentLandmarks, previousLandmarks, timeDelta = 33.33) {
if (!currentLandmarks || !previousLandmarks) return null;
// Calculate velocities
const armVelocities = velocityCalculations.calculateArmVelocities(currentLandmarks, previousLandmarks, timeDelta);
const legVelocities = velocityCalculations.calculateLegVelocities(currentLandmarks, previousLandmarks, timeDelta);
const torsoVelocity = velocityCalculations.calculateTorsoVelocity(currentLandmarks, previousLandmarks, timeDelta);
// Approximate mass ratios (normalized)
const armMassRatio = 0.05; // Each arm is ~5% of body mass
const legMassRatio = 0.15; // Each leg is ~15% of body mass
const torsoMassRatio = 0.60; // Torso ~60% of body mass
// Calculate momentum (p = m * v)
const leftArmMomentum = armVelocities?.left ? armVelocities.left * armMassRatio : 0;
const rightArmMomentum = armVelocities?.right ? armVelocities.right * armMassRatio : 0;
const leftLegMomentum = legVelocities?.left ? legVelocities.left * legMassRatio : 0;
const rightLegMomentum = legVelocities?.right ? legVelocities.right * legMassRatio : 0;
const torsoMomentum = torsoVelocity ? torsoVelocity * torsoMassRatio : 0;
// Total body momentum
const totalMomentum = leftArmMomentum + rightArmMomentum + leftLegMomentum + rightLegMomentum + torsoMomentum;
return {
total: totalMomentum,
arms: {
left: leftArmMomentum,
right: rightArmMomentum
},
legs: {
left: leftLegMomentum,
right: rightLegMomentum
},
torso: torsoMomentum
};
}
/**
* Calculate impulse (change in momentum over time)
* @param {Array} currentLandmarks - Current frame landmarks
* @param {Array} previousLandmarks - Previous frame landmarks
* @param {Array} prevPreviousLandmarks - Frame before previous frame landmarks
* @param {number} timeDelta - Time between frames in ms
* @returns {Object} Object containing impulse values for different body segments
*/
function calculateImpulse(currentLandmarks, previousLandmarks, prevPreviousLandmarks, timeDelta = 33.33) {
if (!currentLandmarks || !previousLandmarks || !prevPreviousLandmarks) return null;
// Calculate momentum at two consecutive points in time
const currentMomentum = calculateMomentum(currentLandmarks, previousLandmarks, timeDelta);
const previousMomentum = calculateMomentum(previousLandmarks, prevPreviousLandmarks, timeDelta);
if (!currentMomentum || !previousMomentum) return null;
// Impulse = change in momentum (ΔP)
const totalImpulse = currentMomentum.total - previousMomentum.total;
const leftArmImpulse = currentMomentum.arms.left - previousMomentum.arms.left;
const rightArmImpulse = currentMomentum.arms.right - previousMomentum.arms.right;
const leftLegImpulse = currentMomentum.legs.left - previousMomentum.legs.left;
const rightLegImpulse = currentMomentum.legs.right - previousMomentum.legs.right;
const torsoImpulse = currentMomentum.torso - previousMomentum.torso;
return {
total: totalImpulse,
arms: {
left: leftArmImpulse,
right: rightArmImpulse
},
legs: {
left: leftLegImpulse,
right: rightLegImpulse
},
torso: torsoImpulse
};
}
// Export the power calculation methods
module.exports = {
calculateJointPower,
calculateWristPower,
calculateElbowPower,
calculateShoulderPower,
calculateHipPower,
calculateKneePower,
calculateAnklePower,
calculateArmPower,
calculateLegPower,
calculateTotalBodyPower,
calculateUpperBodyPower,
calculateLowerBodyPower,
calculatePowerGeneration,
calculateEnergyTransfer,
calculatePotentialEnergy,
calculateKineticEnergy,
calculateTotalEnergy,
calculateWorkDone,
calculatePowerEfficiency,
calculateMomentum,
calculateImpulse
};