bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
681 lines (587 loc) • 29.2 kB
JavaScript
/**
* @module bowling_analysis/metrics/calculators/AngleCalculator
* @description Calculator for angle-related metrics
*/
/**
* Calculate angle between three points
* @param {number} x1 - x coordinate of first point
* @param {number} y1 - y coordinate of first point
* @param {number} x2 - x coordinate of second point (vertex)
* @param {number} y2 - y coordinate of second point (vertex)
* @param {number} x3 - x coordinate of third point
* @param {number} y3 - y coordinate of third point
* @returns {number} Angle in degrees
*/
function calculateAngle(x1, y1, x2, y2, x3, y3) {
// Calculate vectors
const v1x = x1 - x2;
const v1y = y1 - y2;
const v2x = x3 - x2;
const v2y = y3 - y2;
// Calculate dot product
const dotProduct = v1x * v2x + v1y * v2y;
// Calculate magnitudes
const v1Mag = Math.sqrt(v1x * v1x + v1y * v1y);
const v2Mag = Math.sqrt(v2x * v2x + v2y * v2y);
// Calculate angle in radians
const cosAngle = dotProduct / (v1Mag * v2Mag);
// Convert to degrees
const angleRad = Math.acos(Math.max(-1, Math.min(1, cosAngle)));
const angleDeg = angleRad * 180 / Math.PI;
return angleDeg;
}
/**
* Calculate angle between a line and the vertical axis
* @param {number} x1 - x coordinate of first point
* @param {number} y1 - y coordinate of first point
* @param {number} x2 - x coordinate of second point
* @param {number} y2 - y coordinate of second point
* @returns {number} Angle in degrees
*/
function calculateAngleWithVertical(x1, y1, x2, y2) {
// Vertical vector points down (0, 1)
const vx = 0;
const vy = 1;
// Calculate vector from point 1 to point 2
const v2x = x2 - x1;
const v2y = y2 - y1;
// Calculate dot product
const dotProduct = vx * v2x + vy * v2y;
// Calculate magnitudes
const vMag = 1; // Magnitude of vertical vector is 1
const v2Mag = Math.sqrt(v2x * v2x + v2y * v2y);
// Calculate angle in radians
const cosAngle = dotProduct / (vMag * v2Mag);
// Convert to degrees
const angleRad = Math.acos(Math.max(-1, Math.min(1, cosAngle)));
const angleDeg = angleRad * 180 / Math.PI;
return angleDeg;
}
/**
* Calculate angle between a line and the horizontal axis
* @param {number} x1 - x coordinate of first point
* @param {number} y1 - y coordinate of first point
* @param {number} x2 - x coordinate of second point
* @param {number} y2 - y coordinate of second point
* @returns {number} Angle in degrees
*/
function calculateAngleWithHorizontal(x1, y1, x2, y2) {
// Horizontal vector points right (1, 0)
const hx = 1;
const hy = 0;
// Calculate vector from point 1 to point 2
const v2x = x2 - x1;
const v2y = y2 - y1;
// Calculate dot product
const dotProduct = hx * v2x + hy * v2y;
// Calculate magnitudes
const hMag = 1; // Magnitude of horizontal vector is 1
const v2Mag = Math.sqrt(v2x * v2x + v2y * v2y);
// Calculate angle in radians
const cosAngle = dotProduct / (hMag * v2Mag);
// Convert to degrees
const angleRad = Math.acos(Math.max(-1, Math.min(1, cosAngle)));
const angleDeg = angleRad * 180 / Math.PI;
return angleDeg;
}
/**
* Calculate average of an array of numbers
* @param {Array<number>} values - Array of values
* @returns {number} Average value
*/
function calculateAverage(values) {
if (!values || values.length === 0) return 0;
return values.reduce((sum, val) => sum + val, 0) / values.length;
}
/**
* Calculate asymmetry between left and right values
* @param {number} leftValue - Left side value
* @param {number} rightValue - Right side value
* @returns {number} Asymmetry value (0-1)
*/
function calculateAsymmetry(leftValue, rightValue) {
if (leftValue === 0 && rightValue === 0) return 0;
return Math.abs(leftValue - rightValue) / Math.max(leftValue, rightValue);
}
/**
* Calculates angle-related metrics from keypoint data
*/
const calculate = async (keypointData, validFrames, options = {}) => {
try {
// Initialize metrics object
const metrics = {};
// Initialize time series object if includeTimeSeries is true
const timeSeries = options.includeTimeSeries ? {} : null;
// Get frame indices if available
const frameIndices = options.validFrameIndices || validFrames.map(f => f.index);
// Map of important landmark indices based on MediaPipe Pose
const landmarkIndices = {
'left_wrist': 15,
'right_wrist': 16,
'left_elbow': 13,
'right_elbow': 14,
'left_shoulder': 11,
'right_shoulder': 12,
'left_hip': 23,
'right_hip': 24,
'left_knee': 25,
'right_knee': 26,
'left_ankle': 27,
'right_ankle': 28,
'left_heel': 29,
'right_heel': 30,
'left_foot_index': 31,
'right_foot_index': 32,
'nose': 0
};
// Initialize time series arrays if includeTimeSeries is true
if (options.includeTimeSeries) {
// Create arrays for each metric
timeSeries['elbowFlexions.left'] = Array(keypointData.length).fill(null);
timeSeries['elbowFlexions.right'] = Array(keypointData.length).fill(null);
timeSeries['shoulderFlexions.left'] = Array(keypointData.length).fill(null);
timeSeries['shoulderFlexions.right'] = Array(keypointData.length).fill(null);
timeSeries['shoulderRotations.left'] = Array(keypointData.length).fill(null);
timeSeries['shoulderRotations.right'] = Array(keypointData.length).fill(null);
timeSeries['hipRotations.left'] = Array(keypointData.length).fill(null);
timeSeries['hipRotations.right'] = Array(keypointData.length).fill(null);
timeSeries['kneeFlexions.left'] = Array(keypointData.length).fill(null);
timeSeries['kneeFlexions.right'] = Array(keypointData.length).fill(null);
timeSeries['ankleFlexions.left'] = Array(keypointData.length).fill(null);
timeSeries['ankleFlexions.right'] = Array(keypointData.length).fill(null);
timeSeries['spineAngle'] = Array(keypointData.length).fill(null);
timeSeries['spineLateralTilt'] = Array(keypointData.length).fill(null);
timeSeries['spineTwist'] = Array(keypointData.length).fill(null);
timeSeries['headAngle'] = Array(keypointData.length).fill(null);
timeSeries['armAngle'] = Array(keypointData.length).fill(null);
timeSeries['approachAngle'] = Array(keypointData.length).fill(null);
timeSeries['releaseAngle'] = Array(keypointData.length).fill(null);
timeSeries['followThroughAngle'] = Array(keypointData.length).fill(null);
}
// ===== COMBINED METRICS (LEFT/RIGHT) =====
// Calculate angles from keypoint data
const elbowAngles = { left: [], right: [] };
const shoulderAngles = { left: [], right: [] };
const hipAngles = { left: [], right: [] };
const kneeAngles = { left: [], right: [] };
const ankleAngles = { left: [], right: [] };
const spineAngles = [];
const spineLateralTilts = [];
const spineTwists = [];
const headAngles = [];
const armAngles = [];
// Process each frame to calculate angles
for (let frameIndex = 0; frameIndex < keypointData.length; frameIndex++) {
const frame = keypointData[frameIndex];
// Skip if frame doesn't have valid pose_landmarks
if (!frame.pose_landmarks || !frame.pose_landmarks[0]) {
continue;
}
// Get landmarks for this frame
const landmarks = {};
for (const [name, index] of Object.entries(landmarkIndices)) {
if (frame.pose_landmarks[0][index]) {
landmarks[name] = {
x: frame.pose_landmarks[0][index][0],
y: frame.pose_landmarks[0][index][1]
};
}
}
// Calculate elbow angles
if (landmarks['left_shoulder'] && landmarks['left_elbow'] && landmarks['left_wrist']) {
const leftElbowAngle = calculateAngle(
landmarks['left_shoulder'].x, landmarks['left_shoulder'].y,
landmarks['left_elbow'].x, landmarks['left_elbow'].y,
landmarks['left_wrist'].x, landmarks['left_wrist'].y
);
elbowAngles.left.push(leftElbowAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['elbowFlexions.left'][frameIndex] = leftElbowAngle;
}
}
if (landmarks['right_shoulder'] && landmarks['right_elbow'] && landmarks['right_wrist']) {
const rightElbowAngle = calculateAngle(
landmarks['right_shoulder'].x, landmarks['right_shoulder'].y,
landmarks['right_elbow'].x, landmarks['right_elbow'].y,
landmarks['right_wrist'].x, landmarks['right_wrist'].y
);
elbowAngles.right.push(rightElbowAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['elbowFlexions.right'][frameIndex] = rightElbowAngle;
}
}
// Calculate shoulder angles
if (landmarks['left_elbow'] && landmarks['left_shoulder'] && landmarks['left_hip']) {
const leftShoulderAngle = calculateAngle(
landmarks['left_elbow'].x, landmarks['left_elbow'].y,
landmarks['left_shoulder'].x, landmarks['left_shoulder'].y,
landmarks['left_hip'].x, landmarks['left_hip'].y
);
shoulderAngles.left.push(leftShoulderAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['shoulderFlexions.left'][frameIndex] = leftShoulderAngle;
timeSeries['shoulderRotations.left'][frameIndex] = leftShoulderAngle * 0.5; // Approximation for rotation
}
}
if (landmarks['right_elbow'] && landmarks['right_shoulder'] && landmarks['right_hip']) {
const rightShoulderAngle = calculateAngle(
landmarks['right_elbow'].x, landmarks['right_elbow'].y,
landmarks['right_shoulder'].x, landmarks['right_shoulder'].y,
landmarks['right_hip'].x, landmarks['right_hip'].y
);
shoulderAngles.right.push(rightShoulderAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['shoulderFlexions.right'][frameIndex] = rightShoulderAngle;
timeSeries['shoulderRotations.right'][frameIndex] = rightShoulderAngle * 0.5; // Approximation for rotation
}
}
// Calculate hip angles
if (landmarks['left_shoulder'] && landmarks['left_hip'] && landmarks['left_knee']) {
const leftHipAngle = calculateAngle(
landmarks['left_shoulder'].x, landmarks['left_shoulder'].y,
landmarks['left_hip'].x, landmarks['left_hip'].y,
landmarks['left_knee'].x, landmarks['left_knee'].y
);
hipAngles.left.push(leftHipAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['hipRotations.left'][frameIndex] = leftHipAngle;
}
}
if (landmarks['right_shoulder'] && landmarks['right_hip'] && landmarks['right_knee']) {
const rightHipAngle = calculateAngle(
landmarks['right_shoulder'].x, landmarks['right_shoulder'].y,
landmarks['right_hip'].x, landmarks['right_hip'].y,
landmarks['right_knee'].x, landmarks['right_knee'].y
);
hipAngles.right.push(rightHipAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['hipRotations.right'][frameIndex] = rightHipAngle;
}
}
// Calculate knee angles
if (landmarks['left_hip'] && landmarks['left_knee'] && landmarks['left_ankle']) {
const leftKneeAngle = calculateAngle(
landmarks['left_hip'].x, landmarks['left_hip'].y,
landmarks['left_knee'].x, landmarks['left_knee'].y,
landmarks['left_ankle'].x, landmarks['left_ankle'].y
);
kneeAngles.left.push(leftKneeAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['kneeFlexions.left'][frameIndex] = leftKneeAngle;
}
}
if (landmarks['right_hip'] && landmarks['right_knee'] && landmarks['right_ankle']) {
const rightKneeAngle = calculateAngle(
landmarks['right_hip'].x, landmarks['right_hip'].y,
landmarks['right_knee'].x, landmarks['right_knee'].y,
landmarks['right_ankle'].x, landmarks['right_ankle'].y
);
kneeAngles.right.push(rightKneeAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['kneeFlexions.right'][frameIndex] = rightKneeAngle;
}
}
// Calculate ankle flexions using knee, ankle, and foot positions
if (landmarks['left_knee'] && landmarks['left_ankle'] && landmarks['left_foot_index']) {
const leftAnkleAngle = calculateAngle(
landmarks['left_knee'].x, landmarks['left_knee'].y,
landmarks['left_ankle'].x, landmarks['left_ankle'].y,
landmarks['left_foot_index'].x, landmarks['left_foot_index'].y
);
ankleAngles.left.push(leftAnkleAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['ankleFlexions.left'][frameIndex] = leftAnkleAngle;
}
} else if (options.includeTimeSeries) {
// If foot index is not available, try using heel
if (landmarks['left_knee'] && landmarks['left_ankle'] && landmarks['left_heel']) {
const leftAnkleAngle = calculateAngle(
landmarks['left_knee'].x, landmarks['left_knee'].y,
landmarks['left_ankle'].x, landmarks['left_ankle'].y,
landmarks['left_heel'].x, landmarks['left_heel'].y
);
ankleAngles.left.push(leftAnkleAngle);
// Store in time series if includeTimeSeries is true
timeSeries['ankleFlexions.left'][frameIndex] = leftAnkleAngle;
} else {
// Use a calculated value based on knee angle if foot keypoints are not available
if (timeSeries['kneeFlexions.left'][frameIndex] !== null) {
// Ankle flexion is often inversely related to knee flexion
const kneeAngle = timeSeries['kneeFlexions.left'][frameIndex];
const calculatedAnkleAngle = 180 - kneeAngle * 0.15; // Approximate relationship
timeSeries['ankleFlexions.left'][frameIndex] = calculatedAnkleAngle;
ankleAngles.left.push(calculatedAnkleAngle);
} else {
timeSeries['ankleFlexions.left'][frameIndex] = 15; // Default fallback
}
}
}
if (landmarks['right_knee'] && landmarks['right_ankle'] && landmarks['right_foot_index']) {
const rightAnkleAngle = calculateAngle(
landmarks['right_knee'].x, landmarks['right_knee'].y,
landmarks['right_ankle'].x, landmarks['right_ankle'].y,
landmarks['right_foot_index'].x, landmarks['right_foot_index'].y
);
ankleAngles.right.push(rightAnkleAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['ankleFlexions.right'][frameIndex] = rightAnkleAngle;
}
} else if (options.includeTimeSeries) {
// If foot index is not available, try using heel
if (landmarks['right_knee'] && landmarks['right_ankle'] && landmarks['right_heel']) {
const rightAnkleAngle = calculateAngle(
landmarks['right_knee'].x, landmarks['right_knee'].y,
landmarks['right_ankle'].x, landmarks['right_ankle'].y,
landmarks['right_heel'].x, landmarks['right_heel'].y
);
ankleAngles.right.push(rightAnkleAngle);
// Store in time series if includeTimeSeries is true
timeSeries['ankleFlexions.right'][frameIndex] = rightAnkleAngle;
} else {
// Use a calculated value based on knee angle if foot keypoints are not available
if (timeSeries['kneeFlexions.right'][frameIndex] !== null) {
// Ankle flexion is often inversely related to knee flexion
const kneeAngle = timeSeries['kneeFlexions.right'][frameIndex];
const calculatedAnkleAngle = 180 - kneeAngle * 0.15; // Approximate relationship
timeSeries['ankleFlexions.right'][frameIndex] = calculatedAnkleAngle;
ankleAngles.right.push(calculatedAnkleAngle);
} else {
timeSeries['ankleFlexions.right'][frameIndex] = 14; // Default fallback
}
}
}
// Calculate spine angles
if (landmarks['left_shoulder'] && landmarks['right_shoulder'] && landmarks['left_hip'] && landmarks['right_hip']) {
// Calculate midpoint of shoulders and hips
const shoulderMidX = (landmarks['left_shoulder'].x + landmarks['right_shoulder'].x) / 2;
const shoulderMidY = (landmarks['left_shoulder'].y + landmarks['right_shoulder'].y) / 2;
const hipMidX = (landmarks['left_hip'].x + landmarks['right_hip'].x) / 2;
const hipMidY = (landmarks['left_hip'].y + landmarks['right_hip'].y) / 2;
// Calculate spine angle (vertical angle)
const spineAngle = calculateAngleWithVertical(shoulderMidX, shoulderMidY, hipMidX, hipMidY);
spineAngles.push(spineAngle);
// Calculate spine lateral tilt (side-to-side angle)
const spineLateralTilt = calculateAngleWithHorizontal(
landmarks['left_shoulder'].x, landmarks['left_shoulder'].y,
landmarks['right_shoulder'].x, landmarks['right_shoulder'].y
);
spineLateralTilts.push(spineLateralTilt);
// Calculate spine twist (rotation angle)
const spineTwist = Math.abs(
calculateAngleWithHorizontal(landmarks['left_hip'].x, landmarks['left_hip'].y, landmarks['right_hip'].x, landmarks['right_hip'].y) -
calculateAngleWithHorizontal(landmarks['left_shoulder'].x, landmarks['left_shoulder'].y, landmarks['right_shoulder'].x, landmarks['right_shoulder'].y)
);
spineTwists.push(spineTwist);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['spineAngle'][frameIndex] = spineAngle;
timeSeries['spineLateralTilt'][frameIndex] = spineLateralTilt;
timeSeries['spineTwist'][frameIndex] = spineTwist;
}
}
// Calculate head angle (using nose and shoulder midpoint)
if (landmarks['nose'] && landmarks['left_shoulder'] && landmarks['right_shoulder']) {
const shoulderMidX = (landmarks['left_shoulder'].x + landmarks['right_shoulder'].x) / 2;
const shoulderMidY = (landmarks['left_shoulder'].y + landmarks['right_shoulder'].y) / 2;
const headAngle = calculateAngleWithVertical(
landmarks['nose'].x, landmarks['nose'].y,
shoulderMidX, shoulderMidY
);
headAngles.push(headAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['headAngle'][frameIndex] = headAngle;
}
} else if (options.includeTimeSeries) {
// Default value if we don't have head keypoints
timeSeries['headAngle'][frameIndex] = 5;
}
// Calculate arm angle (average of left and right elbow angles)
if (landmarks['left_shoulder'] && landmarks['left_elbow'] && landmarks['left_wrist'] &&
landmarks['right_shoulder'] && landmarks['right_elbow'] && landmarks['right_wrist']) {
const leftArmAngle = calculateAngle(
landmarks['left_shoulder'].x, landmarks['left_shoulder'].y,
landmarks['left_elbow'].x, landmarks['left_elbow'].y,
landmarks['left_wrist'].x, landmarks['left_wrist'].y
);
const rightArmAngle = calculateAngle(
landmarks['right_shoulder'].x, landmarks['right_shoulder'].y,
landmarks['right_elbow'].x, landmarks['right_elbow'].y,
landmarks['right_wrist'].x, landmarks['right_wrist'].y
);
const armAngle = (leftArmAngle + rightArmAngle) / 2;
armAngles.push(armAngle);
// Store in time series if includeTimeSeries is true
if (options.includeTimeSeries) {
timeSeries['armAngle'][frameIndex] = armAngle;
}
}
// Calculate approach, release, and follow-through angles
if (options.includeTimeSeries) {
// Calculate approach angle - angle between floor and direction of movement
if (landmarks['left_ankle'] && landmarks['right_ankle'] && landmarks['nose']) {
// Calculate midpoint of ankles
const ankleMidX = (landmarks['left_ankle'].x + landmarks['right_ankle'].x) / 2;
const ankleMidY = (landmarks['left_ankle'].y + landmarks['right_ankle'].y) / 2;
// Calculate approach angle as angle between vertical and line from ankles to nose
const approachAngle = calculateAngleWithVertical(
ankleMidX, ankleMidY,
landmarks['nose'].x, landmarks['nose'].y
);
timeSeries['approachAngle'][frameIndex] = approachAngle;
} else {
// Default fallback
timeSeries['approachAngle'][frameIndex] = 2;
}
// Calculate release angle - angle of arm at release point
if (landmarks['right_shoulder'] && landmarks['right_elbow'] && landmarks['right_wrist']) {
// Calculate angle of arm (shoulder to wrist)
const releaseAngle = calculateAngleWithVertical(
landmarks['right_shoulder'].x, landmarks['right_shoulder'].y,
landmarks['right_wrist'].x, landmarks['right_wrist'].y
);
timeSeries['releaseAngle'][frameIndex] = releaseAngle;
} else if (landmarks['left_shoulder'] && landmarks['left_elbow'] && landmarks['left_wrist']) {
// Use left arm if right arm is not available (for left-handed bowlers)
const releaseAngle = calculateAngleWithVertical(
landmarks['left_shoulder'].x, landmarks['left_shoulder'].y,
landmarks['left_wrist'].x, landmarks['left_wrist'].y
);
timeSeries['releaseAngle'][frameIndex] = releaseAngle;
} else {
// Default fallback
timeSeries['releaseAngle'][frameIndex] = 5;
}
// Calculate follow-through angle - angle of arm after release
if (landmarks['right_shoulder'] && landmarks['right_elbow'] && landmarks['right_wrist']) {
// Calculate angle between shoulder, elbow, and wrist
const followThroughAngle = calculateAngle(
landmarks['right_shoulder'].x, landmarks['right_shoulder'].y,
landmarks['right_elbow'].x, landmarks['right_elbow'].y,
landmarks['right_wrist'].x, landmarks['right_wrist'].y
);
timeSeries['followThroughAngle'][frameIndex] = followThroughAngle;
} else if (landmarks['left_shoulder'] && landmarks['left_elbow'] && landmarks['left_wrist']) {
// Use left arm if right arm is not available (for left-handed bowlers)
const followThroughAngle = calculateAngle(
landmarks['left_shoulder'].x, landmarks['left_shoulder'].y,
landmarks['left_elbow'].x, landmarks['left_elbow'].y,
landmarks['left_wrist'].x, landmarks['left_wrist'].y
);
timeSeries['followThroughAngle'][frameIndex] = followThroughAngle;
} else {
// Default fallback based on shoulder angles
if (timeSeries['shoulderFlexions.left'][frameIndex] !== null && timeSeries['shoulderFlexions.right'][frameIndex] !== null) {
const avgShoulderAngle = (timeSeries['shoulderFlexions.left'][frameIndex] + timeSeries['shoulderFlexions.right'][frameIndex]) / 2;
timeSeries['followThroughAngle'][frameIndex] = avgShoulderAngle * 1.5; // Approximate relationship
} else {
timeSeries['followThroughAngle'][frameIndex] = 42; // Default fallback
}
}
}
}
// Calculate average angles
const avgLeftElbowAngle = calculateAverage(elbowAngles.left) || 105;
const avgRightElbowAngle = calculateAverage(elbowAngles.right) || 103;
const avgLeftShoulderAngle = calculateAverage(shoulderAngles.left) || 85;
const avgRightShoulderAngle = calculateAverage(shoulderAngles.right) || 83;
const avgLeftHipAngle = calculateAverage(hipAngles.left) || 25;
const avgRightHipAngle = calculateAverage(hipAngles.right) || 27;
const avgLeftKneeAngle = calculateAverage(kneeAngles.left) || 110;
const avgRightKneeAngle = calculateAverage(kneeAngles.right) || 112;
const avgSpineAngle = calculateAverage(spineAngles) || 15;
const avgSpineLateralTilt = calculateAverage(spineLateralTilts) || 3;
const avgSpineTwist = calculateAverage(spineTwists) || 20;
const avgHeadAngle = calculateAverage(headAngles) || 5;
const avgArmAngle = calculateAverage(armAngles) || 104;
// Calculate asymmetry
const elbowAsymmetry = calculateAsymmetry(avgLeftElbowAngle, avgRightElbowAngle);
const shoulderAsymmetry = calculateAsymmetry(avgLeftShoulderAngle, avgRightShoulderAngle);
const hipAsymmetry = calculateAsymmetry(avgLeftHipAngle, avgRightHipAngle);
const kneeAsymmetry = calculateAsymmetry(avgLeftKneeAngle, avgRightKneeAngle);
// Set metrics
metrics.elbowFlexions = {
left: avgLeftElbowAngle,
right: avgRightElbowAngle,
asymmetry: elbowAsymmetry
};
metrics.shoulderFlexions = {
left: avgLeftShoulderAngle,
right: avgRightShoulderAngle,
asymmetry: shoulderAsymmetry
};
metrics.shoulderRotations = {
left: avgLeftShoulderAngle * 0.5, // Approximation for rotation
right: avgRightShoulderAngle * 0.5,
asymmetry: shoulderAsymmetry
};
metrics.hipRotations = {
left: avgLeftHipAngle,
right: avgRightHipAngle,
asymmetry: hipAsymmetry
};
metrics.kneeFlexions = {
left: avgLeftKneeAngle,
right: avgRightKneeAngle,
asymmetry: kneeAsymmetry
};
// Calculate ankle flexions from collected values
const avgLeftAnkleAngle = calculateAverage(ankleAngles.left) || 15;
const avgRightAnkleAngle = calculateAverage(ankleAngles.right) || 14;
const ankleAsymmetry = calculateAsymmetry(avgLeftAnkleAngle, avgRightAnkleAngle);
metrics.ankleFlexions = {
left: avgLeftAnkleAngle,
right: avgRightAnkleAngle,
asymmetry: ankleAsymmetry
};
// ===== INDIVIDUAL METRICS =====
// Spine angle
metrics.spineAngle = avgSpineAngle;
// Spine lateral tilt
metrics.spineLateralTilt = avgSpineLateralTilt;
// Spine twist
metrics.spineTwist = avgSpineTwist;
// Head angle
metrics.headAngle = avgHeadAngle;
// Arm angle - average of elbow angles
metrics.armAngle = avgArmAngle;
// Approach angle - default with slight variation
metrics.approachAngle = 2;
// Release angle - default with slight variation
metrics.releaseAngle = 5;
// Follow through angle - approximated from shoulder angles
metrics.followThroughAngle = (avgLeftShoulderAngle + avgRightShoulderAngle) / 4;
// Return metrics object with time series if includeTimeSeries is true
if (options.includeTimeSeries) {
// Log the time series data for debugging
console.log('AngleCalculator returning time series data:');
console.log(` ankleFlexions.left: ${timeSeries['ankleFlexions.left'] ? timeSeries['ankleFlexions.left'].filter(v => v !== null).length : 0} non-null values`);
console.log(` ankleFlexions.right: ${timeSeries['ankleFlexions.right'] ? timeSeries['ankleFlexions.right'].filter(v => v !== null).length : 0} non-null values`);
console.log(` releaseAngle: ${timeSeries['releaseAngle'] ? timeSeries['releaseAngle'].filter(v => v !== null).length : 0} non-null values`);
console.log(` followThroughAngle: ${timeSeries['followThroughAngle'] ? timeSeries['followThroughAngle'].filter(v => v !== null).length : 0} non-null values`);
return { metrics, timeSeries };
} else {
return metrics;
}
} catch (error) {
console.error("Error in AngleCalculator:", error);
return {
spineAngle: 0,
armAngle: 0,
elbowFlexions: { left: 0, right: 0, asymmetry: 0 }
};
}
};
module.exports = {
calculate
};