UNPKG

bowling-analysis-system

Version:

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

681 lines (587 loc) 29.2 kB
/** * @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 };