UNPKG

bowling-analysis-system

Version:

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

571 lines (473 loc) 20.6 kB
/** * @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 };