UNPKG

bowling-analysis-system

Version:

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

1,021 lines (840 loc) 34 kB
/** * @module metrics/calculations/BalanceCalculations * @description Functions for calculating balance metrics */ const { getSafeLandmark, calculateDistance, createVector } = require('./MetricsUtilities'); const positionCalculations = require('./PositionCalculations'); const metricsConfig = require('../../../config/metricsConfig'); /** * Calculate center of mass * @param {Array} landmarks - Array of pose landmarks * @returns {Array|null} Center of mass coordinates [x,y,z] or null */ function calculateCenterOfMass(landmarks) { // Use the implementation from positionCalculations instead of duplicating const com = positionCalculations.calculateCenterOfMass(landmarks); if (!com) return null; // Convert the object format to array format for compatibility return [com.x, com.y, com.z]; } /** * Calculate base of support * @param {Array} landmarks - Array of pose landmarks * @returns {Array|null} Base of support coordinates [x,y,z] or null */ function calculateBaseOfSupport(landmarks) { const rightFoot = getSafeLandmark(landmarks, 32); // Right foot index const leftFoot = getSafeLandmark(landmarks, 31); // Left foot index const rightHeel = getSafeLandmark(landmarks, 30); // Right heel const leftHeel = getSafeLandmark(landmarks, 29); // Left heel if (!rightFoot || !leftFoot || !rightHeel || !leftHeel) return null; return [ (rightFoot[0] + leftFoot[0] + rightHeel[0] + leftHeel[0]) / 4, (rightFoot[1] + leftFoot[1] + rightHeel[1] + leftHeel[1]) / 4, (rightFoot[2] + leftFoot[2] + rightHeel[2] + leftHeel[2]) / 4 ]; } /** * Calculate postural sway for both left and right sides * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Left and right postural sway values with their average */ function calculatePosturalSway(landmarks) { if (!landmarks) return null; // Calculate left and right postural sway const leftSway = calculateLeftPosturalSway(landmarks); const rightSway = calculateRightPosturalSway(landmarks); // If both are null, return null if (leftSway === null && rightSway === null) { return null; } // Calculate average postural sway const averageSway = (leftSway !== null && rightSway !== null) ? (leftSway + rightSway) / 2 : (leftSway !== null ? leftSway : rightSway); // Return the combined format with left, right, and average sway values return { left: leftSway, right: rightSway, average: averageSway }; } /** * Calculate stability index based on the relation between center of mass and base of support * A lower value indicates better stability * @param {Array} landmarks - Array of pose landmarks * @returns {number} Stability index value */ function calculateStabilityIndex(landmarks) { const com = calculateCenterOfMass(landmarks); const bos = calculateBaseOfSupport(landmarks); if (!com || !bos) return null; // Calculate 3D distance between CoM and BoS const distance = Math.sqrt( Math.pow(com[0] - bos[0], 2) + Math.pow(com[1] - bos[1], 2) + Math.pow(com[2] - bos[2], 2) ); // Normalize by height (use nose to feet distance as proxy for height) const nose = getSafeLandmark(landmarks, 0); const leftFoot = getSafeLandmark(landmarks, 31); if (!nose || !leftFoot) return null; const height = Math.abs(nose[1] - leftFoot[1]); if (height === 0) return null; return distance / height; } /** * Calculate dynamic stability by comparing stability indices between frames * @param {Array} currentLandmarks - Current frame landmarks * @param {Array} previousLandmarks - Previous frame landmarks * @returns {number} Dynamic stability value */ function calculateDynamicStability(currentLandmarks, previousLandmarks) { if (!previousLandmarks) return null; const currentStability = calculateStabilityIndex(currentLandmarks); const previousStability = calculateStabilityIndex(previousLandmarks); if (currentStability === null || previousStability === null) return null; // Calculate change in stability index (lower value is better) return Math.abs(currentStability - previousStability); } /** * Calculate lateral balance - how well centered the bowler is from left to right * @param {Array} landmarks - Array of pose landmarks * @returns {number} Lateral balance value (relative to foot width, 0 is perfectly centered) */ function calculateLateralBalance(landmarks) { const com = calculateCenterOfMass(landmarks); const leftFoot = getSafeLandmark(landmarks, 31); const rightFoot = getSafeLandmark(landmarks, 32); if (!com || !leftFoot || !rightFoot) return null; // Calculate the midpoint between feet (ideal balance point) const midpoint = [ (leftFoot[0] + rightFoot[0]) / 2, (leftFoot[1] + rightFoot[1]) / 2, (leftFoot[2] + rightFoot[2]) / 2 ]; // Calculate distance between feet for relative comparison const footWidth = Math.sqrt( Math.pow(leftFoot[0] - rightFoot[0], 2) + Math.pow(leftFoot[2] - rightFoot[2], 2) ); if (footWidth === 0) return null; // Calculate lateral deviation from midpoint relative to foot width // Positive values indicate weight shifted to right, negative to left return (com[0] - midpoint[0]) / (footWidth / 2); } /** * Calculate anterior balance - how well balanced the bowler is from front to back * @param {Array} landmarks - Array of pose landmarks * @returns {number} Anterior balance value (relative to foot length, 0 is perfectly centered) */ function calculateAnteriorBalance(landmarks) { const com = calculateCenterOfMass(landmarks); const leftToe = getSafeLandmark(landmarks, 31); const rightToe = getSafeLandmark(landmarks, 32); const leftHeel = getSafeLandmark(landmarks, 29); const rightHeel = getSafeLandmark(landmarks, 30); if (!com || !leftToe || !rightToe || !leftHeel || !rightHeel) return null; // Calculate the midpoint of toes and heels const toesMidpoint = [ (leftToe[0] + rightToe[0]) / 2, (leftToe[1] + rightToe[1]) / 2, (leftToe[2] + rightToe[2]) / 2 ]; const heelsMidpoint = [ (leftHeel[0] + rightHeel[0]) / 2, (leftHeel[1] + rightHeel[1]) / 2, (leftHeel[2] + rightHeel[2]) / 2 ]; // Calculate the ideal balance point (midpoint between toes and heels) const idealBalancePoint = [ (toesMidpoint[0] + heelsMidpoint[0]) / 2, (toesMidpoint[1] + heelsMidpoint[1]) / 2, (toesMidpoint[2] + heelsMidpoint[2]) / 2 ]; // Calculate foot length for relative comparison const footLength = Math.sqrt( Math.pow(toesMidpoint[0] - heelsMidpoint[0], 2) + Math.pow(toesMidpoint[2] - heelsMidpoint[2], 2) ); if (footLength === 0) return null; // Calculate anterior-posterior deviation relative to foot length // Positive values indicate weight shifted forward, negative backward return (com[2] - idealBalancePoint[2]) / (footLength / 2); } /** * Calculate release balance - balance at the moment of ball release * This is a special case of stability index focused on the release moment * @param {Array} landmarks - Array of pose landmarks * @returns {Object} Object with stability, lateral, and anterior balance at release */ function calculateReleaseBalance(landmarks) { // For now, just return the same balance metrics // In a full implementation, this would be calculated at the detected release frame return { stability: calculateStabilityIndex(landmarks), lateral: calculateLateralBalance(landmarks), anterior: calculateAnteriorBalance(landmarks) }; } /** * Calculate approach balance - balance during the approach phase * @param {Array} landmarks - Array of pose landmarks * @returns {Object} Object with stability, lateral, and anterior balance during approach */ function calculateApproachBalance(landmarks) { // For now, just return the same balance metrics // In a full implementation, this would be calculated for the approach phase return { stability: calculateStabilityIndex(landmarks), lateral: calculateLateralBalance(landmarks), anterior: calculateAnteriorBalance(landmarks) }; } /** * Calculate weight distribution * @param {Array} landmarks - Array of pose landmarks * @returns {Object} Weight distribution info with normalized value, confidence, and distribution details */ function calculateWeightDistribution(landmarks) { if (!landmarks) { return { distribution: { left: 0.5, right: 0.5 } }; } // Get ankle landmarks const leftAnkle = getSafeLandmark(landmarks, 27); const rightAnkle = getSafeLandmark(landmarks, 28); // Get hip landmarks const leftHip = getSafeLandmark(landmarks, 23); const rightHip = getSafeLandmark(landmarks, 24); // Get center of mass const com = calculateCenterOfMass(landmarks); if (!leftAnkle || !rightAnkle || !leftHip || !rightHip || !com) { return { distribution: { left: 0.5, right: 0.5 } }; } // Calculate distances from COM to each ankle const leftDistance = Math.sqrt( Math.pow(com[0] - leftAnkle.x, 2) + Math.pow(com[2] - leftAnkle.z, 2) // Using x-z plane for horizontal distribution ); const rightDistance = Math.sqrt( Math.pow(com[0] - rightAnkle.x, 2) + Math.pow(com[2] - rightAnkle.z, 2) ); const totalDistance = leftDistance + rightDistance; // Calculate weight distribution based on inverse distance // When COM is closer to one side, weight distribution is higher on that side let leftPercentage, rightPercentage; if (totalDistance > 0) { rightPercentage = leftDistance / totalDistance; leftPercentage = rightDistance / totalDistance; } else { // Default to 50/50 if distances are zero rightPercentage = 0.5; leftPercentage = 0.5; } // Calculate confidence based on quality of landmarks const landmarkConfidence = (leftAnkle.confidence || 0.5) * (rightAnkle.confidence || 0.5) * (leftHip.confidence || 0.5) * (rightHip.confidence || 0.5); const confidence = Math.min(0.95, Math.max(0.5, landmarkConfidence)); // Return the expected format with distribution only (no normalized value) return { confidence: confidence, distribution: { left: leftPercentage, right: rightPercentage }, // For backward compatibility leftPercentage, rightPercentage, rightFootPercentage: Math.round(rightPercentage * 100) }; } /** * Calculate stance width (distance between feet) * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Stance width or null if calculation is not possible */ function calculateStanceWidth(landmarks) { if (!landmarks || landmarks.length === 0) return null; const leftAnkle = getSafeLandmark(landmarks, 27); const rightAnkle = getSafeLandmark(landmarks, 28); if (!leftAnkle || !rightAnkle) { const leftFoot = getSafeLandmark(landmarks, 31); const rightFoot = getSafeLandmark(landmarks, 32); if (!leftFoot || !rightFoot) return null; // Calculate distance between feet return Math.sqrt( Math.pow(leftFoot[0] - rightFoot[0], 2) + Math.pow(leftFoot[2] - rightFoot[2], 2) ); } // Calculate distance between ankles in horizontal plane return Math.sqrt( Math.pow(leftAnkle[0] - rightAnkle[0], 2) + Math.pow(leftAnkle[2] - rightAnkle[2], 2) ); } /** * Calculate torso length (distance from mid-hip to mid-shoulder) * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Torso length or null if calculation is not possible */ function calculateTorsoLength(landmarks) { if (!landmarks || landmarks.length === 0) return null; // Get the landmarks with a higher confidence threshold to ensure quality const leftShoulder = getSafeLandmark(landmarks, 11, 0.1); const rightShoulder = getSafeLandmark(landmarks, 12, 0.1); const leftHip = getSafeLandmark(landmarks, 23, 0.1); const rightHip = getSafeLandmark(landmarks, 24, 0.1); if (!leftShoulder || !rightShoulder || !leftHip || !rightHip) return null; // Calculate midpoints const midShoulder = [ (leftShoulder[0] + rightShoulder[0]) / 2, (leftShoulder[1] + rightShoulder[1]) / 2, (leftShoulder[2] + rightShoulder[2]) / 2 ]; const midHip = [ (leftHip[0] + rightHip[0]) / 2, (leftHip[1] + rightHip[1]) / 2, (leftHip[2] + rightHip[2]) / 2 ]; // Calculate 3D distance between midpoints return Math.sqrt( Math.pow(midShoulder[0] - midHip[0], 2) + Math.pow(midShoulder[1] - midHip[1], 2) + Math.pow(midShoulder[2] - midHip[2], 2) ); } /** * Calculate center of pressures for both left and right feet * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Combined object with left and right center of pressure values and their average */ function calculateCenterOfPressures(landmarks) { if (!landmarks) return null; const leftCoP = calculateLeftCenterOfPressure(landmarks); const rightCoP = calculateRightCenterOfPressure(landmarks); // If both are null, return null if (!leftCoP && !rightCoP) return null; // Calculate average center of pressure (middle point between left and right) let averageCoP = null; if (leftCoP && rightCoP) { // Both values available, calculate true average averageCoP = { x: (leftCoP.x + rightCoP.x) / 2, y: (leftCoP.y + rightCoP.y) / 2, z: (leftCoP.z + rightCoP.z) / 2 }; } else if (leftCoP) { // Only left available averageCoP = { ...leftCoP }; } else if (rightCoP) { // Only right available averageCoP = { ...rightCoP }; } return { left: leftCoP, right: rightCoP, average: averageCoP }; } /** * Calculate postural sways for both left and right sides * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Combined object with left and right postural sway values */ function calculatePosturalSways(landmarks) { if (!landmarks) return null; // Calculate individual sways const leftSway = calculateLeftPosturalSway(landmarks); const rightSway = calculateRightPosturalSway(landmarks); // If both are null, return null if (leftSway === null && rightSway === null) { return null; } // Return combined object with left and right values // Plus calculate the average for combined value // No scaling or modification of the raw sway values return { left: leftSway, right: rightSway, average: (leftSway !== null && rightSway !== null) ? (leftSway + rightSway) / 2 : (leftSway !== null ? leftSway : rightSway) }; } /** * Calculate weight distributions for both left and right sides * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Combined object with left and right weight distribution values and their average */ function calculateWeightDistributions(landmarks) { const distribution = calculateWeightDistribution(landmarks); if (!distribution || !distribution.distribution) return null; const leftWeight = distribution.distribution.left; const rightWeight = distribution.distribution.right; return { left: leftWeight, right: rightWeight, // Add average property (should be 0.5 for perfect balance) average: (leftWeight + rightWeight) / 2, // Add asymmetry property (0 for perfect balance, 1 for complete asymmetry) asymmetry: Math.abs(leftWeight - rightWeight) }; } /** * Calculate left foot postural sway * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Left foot sway or null if calculation is not possible */ function calculateLeftPosturalSway(landmarks) { if (!landmarks) return null; // Get center of mass const centerOfMass = positionCalculations.calculateCenterOfMass(landmarks); if (!centerOfMass) return null; // Get left foot center of pressure const leftCenterOfPressure = calculateLeftCenterOfPressure(landmarks); if (!leftCenterOfPressure) return null; // Calculate horizontal distance (ignoring y/vertical component) const horizontalCoM = { x: centerOfMass.x, y: 0, z: centerOfMass.z }; const horizontalLeftCoP = { x: leftCenterOfPressure.x, y: 0, z: leftCenterOfPressure.z }; // Calculate horizontal distance between CoM and left foot CoP const distance = Math.sqrt( Math.pow(horizontalCoM.x - horizontalLeftCoP.x, 2) + Math.pow(horizontalCoM.z - horizontalLeftCoP.z, 2) ); // Get left foot for normalization const leftFoot = getSafeLandmark(landmarks, 31); const leftHeel = getSafeLandmark(landmarks, 29); if (!leftFoot || !leftHeel) return null; // Calculate foot length for normalization const footLength = Math.sqrt( Math.pow(leftFoot[0] - leftHeel[0], 2) + Math.pow(leftFoot[2] - leftHeel[2], 2) ); if (footLength === 0) return null; // Normalize by foot length return distance / footLength; } /** * Calculate right foot postural sway * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Right foot sway or null if calculation is not possible */ function calculateRightPosturalSway(landmarks) { if (!landmarks) return null; // Get center of mass const centerOfMass = positionCalculations.calculateCenterOfMass(landmarks); if (!centerOfMass) return null; // Get right foot center of pressure const rightCenterOfPressure = calculateRightCenterOfPressure(landmarks); if (!rightCenterOfPressure) return null; // Calculate horizontal distance (ignoring y/vertical component) const horizontalCoM = { x: centerOfMass.x, y: 0, z: centerOfMass.z }; const horizontalRightCoP = { x: rightCenterOfPressure.x, y: 0, z: rightCenterOfPressure.z }; // Calculate horizontal distance between CoM and right foot CoP const distance = Math.sqrt( Math.pow(horizontalCoM.x - horizontalRightCoP.x, 2) + Math.pow(horizontalCoM.z - horizontalRightCoP.z, 2) ); // Get right foot for normalization const rightFoot = getSafeLandmark(landmarks, 32); const rightHeel = getSafeLandmark(landmarks, 30); if (!rightFoot || !rightHeel) return null; // Calculate foot length for normalization const footLength = Math.sqrt( Math.pow(rightFoot[0] - rightHeel[0], 2) + Math.pow(rightFoot[2] - rightHeel[2], 2) ); if (footLength === 0) return null; // Normalize by foot length return distance / footLength; } /** * Calculate left foot center of pressure * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Left foot center of pressure or null if calculation is not possible */ function calculateLeftCenterOfPressure(landmarks) { if (!landmarks) return null; // Get left foot landmarks const leftFoot = getSafeLandmark(landmarks, 31); // Left foot index const leftHeel = getSafeLandmark(landmarks, 29); // Left heel const leftAnkle = getSafeLandmark(landmarks, 27); // Left ankle if (!leftFoot || !leftHeel || !leftAnkle) { return null; } // Get center of mass and anterior balance to determine pressure distribution const com = calculateCenterOfMass(landmarks); const anteriorBalance = calculateAnteriorBalance(landmarks); if (!com) return { x: 0, y: 0, z: 0 }; // Calculate foot length const footLength = Math.sqrt( Math.pow(leftFoot.x - leftHeel.x, 2) + Math.pow(leftFoot.z - leftHeel.z, 2) ); // Use a fixed ratio for deterministic calculation // This ensures no sine wave patterns are introduced const footRatio = 0.5; // Fixed midpoint between heel and toe // Calculate center of pressure position const leftCoP = { x: leftHeel.x + (leftFoot.x - leftHeel.x) * footRatio, y: Math.min(leftFoot.y, leftHeel.y), // Keep on ground level z: leftHeel.z + (leftFoot.z - leftHeel.z) * footRatio }; return leftCoP; } /** * Calculate right foot center of pressure * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Right foot center of pressure or null if calculation is not possible */ function calculateRightCenterOfPressure(landmarks) { if (!landmarks) return null; // Get right foot landmarks const rightFoot = getSafeLandmark(landmarks, 32); // Right foot index const rightHeel = getSafeLandmark(landmarks, 30); // Right heel const rightAnkle = getSafeLandmark(landmarks, 28); // Right ankle if (!rightFoot || !rightHeel || !rightAnkle) { return null; } // Get center of mass and anterior balance to determine pressure distribution const com = calculateCenterOfMass(landmarks); const anteriorBalance = calculateAnteriorBalance(landmarks); if (!com) return { x: 0, y: 0, z: 0 }; // Calculate foot length const footLength = Math.sqrt( Math.pow(rightFoot.x - rightHeel.x, 2) + Math.pow(rightFoot.z - rightHeel.z, 2) ); // Use a fixed ratio for deterministic calculation // This ensures no sine wave patterns are introduced const footRatio = 0.5; // Fixed midpoint between heel and toe // Calculate center of pressure position const rightCoP = { x: rightHeel.x + (rightFoot.x - rightHeel.x) * footRatio, y: Math.min(rightFoot.y, rightHeel.y), // Keep on ground level z: rightHeel.z + (rightFoot.z - rightHeel.z) * footRatio }; return rightCoP; } /** * Calculate balance asymmetry (0 = perfect balance, higher = more asymmetric) * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Calculated asymmetry value or null if calculation is not possible */ function calculateBalanceAsymmetry(landmarks) { if (!landmarks) return null; const weightDist = calculateWeightDistribution(landmarks); if (!weightDist || !weightDist.distribution) return null; // Calculate deviation from perfect balance (50%) // Subtract left percentage from 0.5 (perfect balance would be 0.5) return Math.abs(weightDist.distribution.left - 0.5) * 2; // Scale to 0-1 } /** * Calculate vertical stability (less vertical movement = more stable) * @param {Array} currentLandmarks - Current frame landmarks * @param {Array} previousLandmarks - Previous frame landmarks * @returns {number|null} Raw vertical displacement relative to body height */ function calculateVerticalStability(currentLandmarks, previousLandmarks) { if (!currentLandmarks || !previousLandmarks) return null; const currentCoM = positionCalculations.calculateCenterOfMass(currentLandmarks); const previousCoM = positionCalculations.calculateCenterOfMass(previousLandmarks); if (!currentCoM || !previousCoM) return null; // Calculate vertical displacement const verticalDisplacement = Math.abs(currentCoM.y - previousCoM.y); // Get body height as reference (e.g., distance from head to feet) const headLandmark = getSafeLandmark(currentLandmarks, 0); // Head const leftFootLandmark = getSafeLandmark(currentLandmarks, 31); // Left foot const rightFootLandmark = getSafeLandmark(currentLandmarks, 32); // Right foot if (!headLandmark || (!leftFootLandmark && !rightFootLandmark)) { return verticalDisplacement; // Return raw displacement if can't get reference } // Use the lowest foot point const footY = leftFootLandmark && rightFootLandmark ? Math.max(leftFootLandmark.y, rightFootLandmark.y) : (leftFootLandmark ? leftFootLandmark.y : rightFootLandmark.y); const bodyHeight = Math.abs(headLandmark.y - footY); if (bodyHeight === 0) return verticalDisplacement; // Return displacement relative to body height return verticalDisplacement / bodyHeight; } /** * Calculate follow-through balance * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Follow-through balance score or null if calculation is not possible */ function calculateFollowThroughBalance(landmarks) { if (!landmarks) return null; // Get key metrics const stabilityIndex = calculateStabilityIndex(landmarks); const weightDistribution = calculateWeightDistribution(landmarks); if (stabilityIndex === null || weightDistribution === null) return null; // Get balance configuration const balanceConfig = metricsConfig.balance.weightDistribution; // For right-handed bowlers, weight should be more on left foot during follow-through // This is a simplified approach - in a real system, handedness would be detected const weightBalanceFactor = weightDistribution > balanceConfig.idealThreshold ? Math.max(0, 1 - ((weightDistribution - balanceConfig.idealThreshold) / balanceConfig.deviationFactor)) : 1.0; // Calculate overall follow-through balance score using config weights return Math.max(0, Math.min(100, stabilityIndex * balanceConfig.stabilityWeight + weightBalanceFactor * balanceConfig.distributionWeight)); } /** * Calculate anterior-posterior (front-back) balance * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Raw anterior-posterior deviation relative to foot length */ function calculateAnteriorPosterior(landmarks) { if (!landmarks) return null; // Get center of mass const com = calculateCenterOfMass(landmarks); // Get foot landmarks for base of support reference const leftToe = getSafeLandmark(landmarks, 31); const rightToe = getSafeLandmark(landmarks, 32); const leftHeel = getSafeLandmark(landmarks, 29); const rightHeel = getSafeLandmark(landmarks, 30); if (!com || !leftToe || !rightToe || !leftHeel || !rightHeel) return null; // Calculate the midpoint of toes and heels to create a support rectangle const toesMidpoint = [ (leftToe[0] + rightToe[0]) / 2, (leftToe[1] + rightToe[1]) / 2, (leftToe[2] + rightToe[2]) / 2 ]; const heelsMidpoint = [ (leftHeel[0] + rightHeel[0]) / 2, (leftHeel[1] + rightHeel[1]) / 2, (leftHeel[2] + rightHeel[2]) / 2 ]; // Calculate the ideal balance point (midpoint between toes and heels) const idealBalancePoint = [ (toesMidpoint[0] + heelsMidpoint[0]) / 2, (toesMidpoint[1] + heelsMidpoint[1]) / 2, (toesMidpoint[2] + heelsMidpoint[2]) / 2 ]; // Calculate foot length for relative comparison const footLength = Math.sqrt( Math.pow(toesMidpoint[0] - heelsMidpoint[0], 2) + Math.pow(toesMidpoint[2] - heelsMidpoint[2], 2) ); if (footLength === 0) return null; // Calculate anterior-posterior deviation (z-axis direction) // Relative to foot length, 0 is perfectly balanced return (com[2] - idealBalancePoint[2]) / (footLength / 2); } /** * Calculate medial-lateral (side to side) balance * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Raw medial-lateral deviation relative to foot width */ function calculateMedialLateral(landmarks) { if (!landmarks) return null; // Get center of mass const com = calculateCenterOfMass(landmarks); // Get foot landmarks for base of support reference const leftFoot = getSafeLandmark(landmarks, 31); // Left foot index const rightFoot = getSafeLandmark(landmarks, 32); // Right foot index if (!com || !leftFoot || !rightFoot) return null; // Calculate the midpoint between feet (ideal balance point) const midpoint = [ (leftFoot[0] + rightFoot[0]) / 2, (leftFoot[1] + rightFoot[1]) / 2, (leftFoot[2] + rightFoot[2]) / 2 ]; // Calculate distance between feet for relative comparison const footWidth = Math.sqrt( Math.pow(leftFoot[0] - rightFoot[0], 2) + Math.pow(leftFoot[2] - rightFoot[2], 2) ); if (footWidth === 0) return null; // Calculate medial-lateral deviation (x-axis direction) // Relative to foot width, 0 is perfectly balanced return (com[0] - midpoint[0]) / (footWidth / 2); } /** * Calculate vertical balance (up-down stability) * @param {Array} currentLandmarks - Current frame landmarks * @param {Array} previousLandmarks - Previous frame landmarks * @returns {number|null} Vertical balance value or null if calculation is not possible */ function calculateVertical(currentLandmarks, previousLandmarks) { if (!currentLandmarks || !previousLandmarks) return null; // Get center of mass for both frames const currentCom = calculateCenterOfMass(currentLandmarks); const previousCom = calculateCenterOfMass(previousLandmarks); if (!currentCom || !previousCom) return null; // Calculate vertical movement (y-axis) const verticalMovement = Math.abs(currentCom[1] - previousCom[1]); // Get a reference length (torso length) for relative comparison const torsoLength = calculateTorsoLength(currentLandmarks); if (!torsoLength) return null; // Calculate movement as proportion of torso length const proportionalMovement = verticalMovement / torsoLength; // Convert to stability score (0-1 where 1 is perfectly stable) // Less movement means more stability return Math.max(0, 1 - (proportionalMovement * 5)); // Scale factor 5 makes it more sensitive } /** * Calculate all balance metrics for a frame * @param {Object} currentFrame - Current frame with pose landmarks * @param {Object} previousFrame - Previous frame with pose landmarks (optional) * @returns {Object} Balance metrics */ function calculateBalance(currentFrame, previousFrame = null) { // Early return if current frame is invalid if (!currentFrame || !currentFrame.keypoints) { return {}; } const landmarks = currentFrame.keypoints; const prevLandmarks = previousFrame?.keypoints; try { // Initialize results with all balance metrics const result = { centerOfMass: positionCalculations.calculateCenterOfMass(landmarks), centerOfPressures: calculateCenterOfPressures(landmarks), posturalSways: calculatePosturalSways(landmarks), stabilityIndex: calculateStabilityIndex(landmarks), weightDistributions: calculateWeightDistributions(landmarks), balanceAsymmetry: calculateBalanceAsymmetry(landmarks), lateralBalance: calculateLateralBalance(landmarks), anteriorBalance: calculateAnteriorBalance(landmarks) }; // Add dynamic stability if we have previous landmarks if (prevLandmarks) { result.dynamicStability = calculateDynamicStability(landmarks, prevLandmarks); result.verticalStability = calculateVerticalStability(landmarks, prevLandmarks); } // Add approach, release, and followThrough balance if needed // These depend on specific frames from events, so they will be calculated in phase 3 result.approachBalance = calculateApproachBalance(landmarks); result.releaseBalance = calculateReleaseBalance(landmarks); result.followThroughBalance = calculateFollowThroughBalance(landmarks); // Generate overallBalance if we have all required metrics if (!result.approachBalance || !result.releaseBalance || !result.followThroughBalance) { result.overallBalance = null; } else { result.overallBalance = ( result.approachBalance * 0.3 + result.releaseBalance * 0.4 + result.followThroughBalance * 0.3 ); } return result; } catch (error) { console.error(`Error calculating balance metrics: ${error.message}`); return {}; } } /** * Balance calculation methods - Calculate balance-related metrics from landmarks */ const balanceCalculations = { /** * Calculate center of pressures for both left and right feet * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Combined object with left and right center of pressure values and their average */ calculateCenterOfPressures, /** * Calculate postural sways for both left and right sides * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Combined object with left and right postural sway values */ calculatePosturalSways, /** * Calculate weight distributions for both left and right sides * @param {Array} landmarks - Array of pose landmarks * @returns {Object|null} Combined object with left and right weight distribution values and their average */ calculateWeightDistributions, /** * Calculate balance asymmetry (0 = perfect balance, higher = more asymmetric) * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Calculated asymmetry value or null if calculation is not possible */ calculateBalanceAsymmetry, /** * Calculate vertical stability (less vertical movement = more stable) * @param {Array} currentLandmarks - Current frame landmarks * @param {Array} previousLandmarks - Previous frame landmarks * @returns {number|null} Raw vertical displacement relative to body height */ calculateVerticalStability, /** * Calculate follow-through balance * @param {Array} landmarks - Array of pose landmarks * @returns {number|null} Follow-through balance score or null if calculation is not possible */ calculateFollowThroughBalance, /** * Calculate all balance metrics for a frame * @param {Object} currentFrame - Current frame with pose landmarks * @param {Object} previousFrame - Previous frame with pose landmarks (optional) * @returns {Object} Balance metrics */ calculateBalance, calculateStanceWidth, calculateTorsoLength }; module.exports = { // Combined metrics (with both left/right values) calculateCenterOfPressures, calculatePosturalSways, calculateWeightDistributions, // Single value balance metrics calculateCenterOfMass, calculateBaseOfSupport, calculatePosturalSway, calculateStabilityIndex, calculateWeightDistribution, calculateDynamicStability, calculateApproachBalance, calculateLateralBalance, calculateAnteriorBalance, calculateReleaseBalance, calculateBalanceAsymmetry, calculateVerticalStability, calculateFollowThroughBalance, calculateStanceWidth, calculateTorsoLength, // Individual left/right metrics (for internal use) calculateLeftCenterOfPressure, calculateRightCenterOfPressure, calculateLeftPosturalSway, calculateRightPosturalSway, // Full calculation method calculateBalance, // New balance metrics calculateAnteriorPosterior, calculateMedialLateral, calculateVertical };