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