UNPKG

bowling-analysis-system

Version:

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

1,429 lines (1,155 loc) 50.6 kB
/** * Metrics Data Connector * * Ensures all metrics data is using real implementations * @module core/processors/MetricsDataConnector */ const BaseProcessor = require('../BaseProcessor'); const fs = require('fs'); const path = require('path'); /** * Ensures all metrics in the system use real calculations * @class MetricsDataConnector * @extends BaseProcessor */ class MetricsDataConnector extends BaseProcessor { /** * Create a new MetricsDataConnector processor * @param {Object} options - Configuration options * @param {string} [options.metricsJsonPath] - Path to metrics.json file */ constructor(options = {}) { super('MetricsDataConnector', options); this.metricsJsonPath = options.metricsJsonPath || path.join(process.cwd(), 'data', 'output', 'metrics.json'); this.debug = options.debug || true; this.fixMissingFrames = options.fixMissingFrames !== false; this.firstValidFrameIndex = 0; } /** * Logs debug messages if debug mode is enabled * @param {...any} args - Arguments to log * @private */ _debug(...args) { if (this.debug) { console.log(`[MetricsDataConnector]`, ...args); } } /** * Process the metrics data and ensure all calculations use real implementations * @param {Object} input - Input data with metrics * @param {Object} [context] - Processing context * @returns {Object} - The updated input data */ async process(input, context) { this._debug('Starting processing'); if (!input || !input.analyzedFrames) { console.warn('No analyzed frames found in input'); return input; } try { // Detect the first valid frame with pose data this.firstValidFrameIndex = this._detectFirstValidFrame(input.analyzedFrames); this._debug(`First valid frame detected at index: ${this.firstValidFrameIndex}`); if (this.fixMissingFrames) { this._debug('Fixing missing frames in time series data'); input = this._fixTimeSeriesData(input); } // Read metrics.json let metricsJson; try { if (fs.existsSync(this.metricsJsonPath)) { const fileContent = fs.readFileSync(this.metricsJsonPath, 'utf8'); metricsJson = JSON.parse(fileContent); this._debug('Successfully read metrics.json'); } else { console.warn(`File does not exist: ${this.metricsJsonPath}`); return input; } } catch (error) { console.warn(`Could not read metrics.json: ${error.message}`); return input; } // Validate time series data - ensure only real calculated data this._validateAllMetrics(metricsJson); return input; } catch (error) { console.error(`Error: ${error.message}`); return input; } } /** * Detects the first valid frame in the analyzed frames array * @param {Array} analyzedFrames - Array of analyzed frames * @returns {number} - Index of the first valid frame * @private */ _detectFirstValidFrame(analyzedFrames) { if (!analyzedFrames || !Array.isArray(analyzedFrames)) { return 0; } for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (frame && frame.pose_landmarks && Array.isArray(frame.pose_landmarks) && frame.pose_landmarks.length > 0) { return i; } } return 0; // No valid frames found } /** * Fixes time series data by ensuring all metrics have the correct structure * @param {Object} input - Input data object * @returns {Object} - Updated input data * @private */ _fixTimeSeriesData(input) { if (!input.metrics || !input.metrics.timeSeries) { return input; } const frameCount = input.analyzedFrames.length; const firstValidFrame = this.firstValidFrameIndex; // Helper function to fix a single time series const fixSeries = (series) => { if (!series || !series.values || !Array.isArray(series.values)) { return series; } // Create a new array with null values for missing frames const fixedValues = Array(frameCount).fill(null); // Copy valid values starting from the first valid frame for (let i = firstValidFrame; i < frameCount && (i - firstValidFrame) < series.values.length; i++) { fixedValues[i] = series.values[i - firstValidFrame]; } return { ...series, values: fixedValues }; }; // Process each time series category const timeSeries = input.metrics.timeSeries; const fixedTimeSeries = {}; Object.keys(timeSeries).forEach(category => { fixedTimeSeries[category] = {}; Object.keys(timeSeries[category]).forEach(seriesName => { fixedTimeSeries[category][seriesName] = fixSeries(timeSeries[category][seriesName]); }); }); input.metrics.timeSeries = fixedTimeSeries; return input; } /** * Validates all metrics to ensure they use real calculations * @param {Object} metricsJson - Metrics data from metrics.json * @private */ _validateAllMetrics(metricsJson) { if (!metricsJson || !metricsJson.timeSeries) { return; } this._debug('Validating all metrics data for real implementations'); // Check all categories in timeSeries Object.keys(metricsJson.timeSeries).forEach(category => { if (metricsJson.timeSeries[category]) { // Check all series in this category Object.keys(metricsJson.timeSeries[category]).forEach(seriesName => { const series = metricsJson.timeSeries[category][seriesName]; // Check if series has data if (!series || !Array.isArray(series)) { this._debug(`Warning: ${category}.${seriesName} has invalid data structure`); } else { // Count valid values const totalValues = series.length; const nullValues = series.filter(v => v === null).length; const validValues = totalValues - nullValues; // Log metrics with no valid values if (validValues === 0 && totalValues > 0) { this._debug(`Warning: ${category}.${seriesName} has no valid values (${totalValues} total)`); } } }); } }); this._debug('Metric validation complete'); } /** * Generates real arm angle time series data * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of arm angle values * @private */ _generateArmAngleSeries(analyzedFrames) { if (!analyzedFrames || !Array.isArray(analyzedFrames)) { return []; } const armAngleSeries = []; for (let i = this.firstValidFrameIndex; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { armAngleSeries.push(null); continue; } // Find shoulder, elbow, and wrist landmarks for right arm (assuming right-handed bowler) // Pose landmark indices: 12=right shoulder, 14=right elbow, 16=right wrist const shoulder = frame.pose_landmarks.find(lm => lm.index === 12); const elbow = frame.pose_landmarks.find(lm => lm.index === 14); if (!shoulder || !elbow) { armAngleSeries.push(null); continue; } // Calculate arm vector const armVector = { x: elbow.x - shoulder.x, y: elbow.y - shoulder.y }; // Calculate angle with vertical (y-axis) const verticalVector = { x: 0, y: 1 }; // Calculate dot product const dotProduct = armVector.x * verticalVector.x + armVector.y * verticalVector.y; // Calculate magnitudes const armMag = Math.sqrt(armVector.x * armVector.x + armVector.y * armVector.y); const verticalMag = Math.sqrt(verticalVector.x * verticalVector.x + verticalVector.y * verticalVector.y); // Calculate angle in radians const cosTheta = dotProduct / (armMag * verticalMag); const theta = Math.acos(Math.max(-1, Math.min(1, cosTheta))); // Clamp to [-1, 1] // Convert to degrees const angleDegrees = theta * 180 / Math.PI; // Adjust angle based on direction const adjustedAngle = armVector.x < 0 ? angleDegrees : 360 - angleDegrees; armAngleSeries.push(adjustedAngle); } return armAngleSeries; } /** * Generates real elbow flexion time series data * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of elbow flexion values * @private */ _generateElbowFlexionSeries(analyzedFrames) { if (!analyzedFrames || !Array.isArray(analyzedFrames)) { return []; } const elbowFlexionSeries = []; for (let i = this.firstValidFrameIndex; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { elbowFlexionSeries.push(null); continue; } // Find shoulder, elbow and wrist landmarks for right arm (assuming right-handed bowler) // Pose landmark indices: 12=right shoulder, 14=right elbow, 16=right wrist const shoulder = frame.pose_landmarks.find(lm => lm.index === 12); const elbow = frame.pose_landmarks.find(lm => lm.index === 14); const wrist = frame.pose_landmarks.find(lm => lm.index === 16); if (!shoulder || !elbow || !wrist) { elbowFlexionSeries.push(null); continue; } // Calculate vectors const upperArm = { x: elbow.x - shoulder.x, y: elbow.y - shoulder.y }; const forearm = { x: wrist.x - elbow.x, y: wrist.y - elbow.y }; // Calculate dot product const dotProduct = upperArm.x * forearm.x + upperArm.y * forearm.y; // Calculate magnitudes const upperArmMag = Math.sqrt(upperArm.x * upperArm.x + upperArm.y * upperArm.y); const forearmMag = Math.sqrt(forearm.x * forearm.x + forearm.y * forearm.y); // Calculate angle in radians const cosTheta = dotProduct / (upperArmMag * forearmMag); const theta = Math.acos(Math.max(-1, Math.min(1, cosTheta))); // Clamp to [-1, 1] // Convert to degrees and get elbow flexion angle const angleDegrees = 180 - (theta * 180 / Math.PI); elbowFlexionSeries.push(angleDegrees); } return elbowFlexionSeries; } /** * Generate trunk lean series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of trunk lean values * @private */ _generateTrunkLeanSeries(analyzedFrames) { const trunkLeanSeries = []; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { continue; } try { // Pose landmark indices: 11=left shoulder, 12=right shoulder, 23=left hip, 24=right hip const leftShoulder = frame.pose_landmarks.find(lm => lm.index === 11); const rightShoulder = frame.pose_landmarks.find(lm => lm.index === 12); const leftHip = frame.pose_landmarks.find(lm => lm.index === 23); const rightHip = frame.pose_landmarks.find(lm => lm.index === 24); if (!leftShoulder || !rightShoulder || !leftHip || !rightHip) { trunkLeanSeries.push(null); continue; } // Calculate midpoints const shoulderMidpoint = { x: (leftShoulder.x + rightShoulder.x) / 2, y: (leftShoulder.y + rightShoulder.y) / 2 }; const hipMidpoint = { x: (leftHip.x + rightHip.x) / 2, y: (leftHip.y + rightHip.y) / 2 }; // Calculate trunk vector (from hip to shoulder) const trunkVector = { x: shoulderMidpoint.x - hipMidpoint.x, y: shoulderMidpoint.y - hipMidpoint.y }; // Calculate angle from vertical // Vertical vector points down (positive y) const verticalVector = { x: 0, y: 1 }; // Calculate dot product const dotProduct = trunkVector.x * verticalVector.x + trunkVector.y * verticalVector.y; // Calculate magnitudes const trunkMag = Math.sqrt(trunkVector.x * trunkVector.x + trunkVector.y * trunkVector.y); const verticalMag = Math.sqrt(verticalVector.x * verticalVector.x + verticalVector.y * verticalVector.y); if (trunkMag === 0) { trunkLeanSeries.push(null); continue; } // Calculate angle in radians const cosTheta = dotProduct / (trunkMag * verticalMag); // Clamp to valid range const theta = Math.acos(Math.max(-1, Math.min(1, cosTheta))); // Convert to degrees const angleDegrees = theta * 180 / Math.PI; // Determine direction of lean (left or right) const direction = trunkVector.x < 0 ? -1 : 1; trunkLeanSeries.push(direction * angleDegrees); } catch (error) { trunkLeanSeries.push(null); } } return trunkLeanSeries; } /** * Generates real knee flexion time series data * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of knee flexion values * @private */ _generateKneeFlexionSeries(analyzedFrames) { if (!analyzedFrames || !Array.isArray(analyzedFrames)) { return []; } const kneeFlexionSeries = []; for (let i = this.firstValidFrameIndex; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { kneeFlexionSeries.push(null); continue; } // Find hip, knee and ankle landmarks for right leg (assuming right-handed bowler) // Pose landmark indices: 24=right hip, 26=right knee, 28=right ankle const hip = frame.pose_landmarks.find(lm => lm.index === 24); const knee = frame.pose_landmarks.find(lm => lm.index === 26); const ankle = frame.pose_landmarks.find(lm => lm.index === 28); if (!hip || !knee || !ankle) { kneeFlexionSeries.push(null); continue; } // Calculate vectors const upperLeg = { x: knee.x - hip.x, y: knee.y - hip.y }; const lowerLeg = { x: ankle.x - knee.x, y: ankle.y - knee.y }; // Calculate dot product const dotProduct = upperLeg.x * lowerLeg.x + upperLeg.y * lowerLeg.y; // Calculate magnitudes const upperLegMag = Math.sqrt(upperLeg.x * upperLeg.x + upperLeg.y * upperLeg.y); const lowerLegMag = Math.sqrt(lowerLeg.x * lowerLeg.x + lowerLeg.y * lowerLeg.y); // Calculate angle in radians const cosTheta = dotProduct / (upperLegMag * lowerLegMag); const theta = Math.acos(Math.max(-1, Math.min(1, cosTheta))); // Clamp to [-1, 1] // Convert to degrees and get knee flexion angle const angleDegrees = 180 - (theta * 180 / Math.PI); kneeFlexionSeries.push(angleDegrees); } return kneeFlexionSeries; } /** * Generate shoulder rotation series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of shoulder rotation values * @private */ _generateShoulderRotationSeries(analyzedFrames) { const shoulderRotationSeries = []; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { continue; } try { // Pose landmark indices: 11=left shoulder, 12=right shoulder, 23=left hip, 24=right hip const leftShoulder = frame.pose_landmarks.find(lm => lm.index === 11); const rightShoulder = frame.pose_landmarks.find(lm => lm.index === 12); const leftHip = frame.pose_landmarks.find(lm => lm.index === 23); const rightHip = frame.pose_landmarks.find(lm => lm.index === 24); if (!leftShoulder || !rightShoulder || !leftHip || !rightHip) { shoulderRotationSeries.push(null); continue; } // Calculate shoulder and hip lines const shoulderVector = { x: rightShoulder.x - leftShoulder.x, y: rightShoulder.y - leftShoulder.y }; const hipVector = { x: rightHip.x - leftHip.x, y: rightHip.y - leftHip.y }; // Calculate dot product const dotProduct = shoulderVector.x * hipVector.x + shoulderVector.y * hipVector.y; // Calculate magnitudes const shoulderMag = Math.sqrt(shoulderVector.x * shoulderVector.x + shoulderVector.y * shoulderVector.y); const hipMag = Math.sqrt(hipVector.x * hipVector.x + hipVector.y * hipVector.y); // Check for zero magnitude if (shoulderMag === 0 || hipMag === 0) { shoulderRotationSeries.push(null); continue; } // Calculate angle in radians const cosTheta = dotProduct / (shoulderMag * hipMag); // Clamp to [-1, 1] to handle floating point errors const theta = Math.acos(Math.max(-1, Math.min(1, cosTheta))); // Convert to degrees const angleDegrees = theta * 180 / Math.PI; shoulderRotationSeries.push(angleDegrees); } catch (error) { shoulderRotationSeries.push(null); } } return shoulderRotationSeries; } /** * Generate shoulder tilt series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of shoulder tilt values * @private */ _generateShoulderTiltSeries(analyzedFrames) { const shoulderTiltSeries = []; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { continue; } try { // Pose landmark indices: 11=left shoulder, 12=right shoulder const leftShoulder = frame.pose_landmarks.find(lm => lm.index === 11); const rightShoulder = frame.pose_landmarks.find(lm => lm.index === 12); if (!leftShoulder || !rightShoulder) { shoulderTiltSeries.push(null); continue; } // Calculate tilt angle (angle from horizontal) const deltaY = rightShoulder.y - leftShoulder.y; const deltaX = rightShoulder.x - leftShoulder.x; // Calculate angle in radians const angleRadians = Math.atan2(deltaY, deltaX); // Convert to degrees const angleDegrees = angleRadians * 180 / Math.PI; shoulderTiltSeries.push(angleDegrees); } catch (error) { shoulderTiltSeries.push(null); } } return shoulderTiltSeries; } /** * Generate hip rotation series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of hip rotation values * @private */ _generateHipRotationSeries(analyzedFrames) { const hipRotationSeries = []; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { continue; } try { // Pose landmark indices: 23=left hip, 24=right hip const leftHip = frame.pose_landmarks.find(lm => lm.index === 23); const rightHip = frame.pose_landmarks.find(lm => lm.index === 24); if (!leftHip || !rightHip) { hipRotationSeries.push(null); continue; } // Calculate hip line angle from horizontal const deltaY = rightHip.y - leftHip.y; const deltaX = rightHip.x - leftHip.x; // Calculate angle in radians const angleRadians = Math.atan2(deltaY, deltaX); // Convert to degrees const angleDegrees = angleRadians * 180 / Math.PI; hipRotationSeries.push(angleDegrees); } catch (error) { hipRotationSeries.push(null); } } return hipRotationSeries; } /** * Generate spine twist series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of spine twist values * @private */ _generateSpineTwistSeries(analyzedFrames) { const spineTwistSeries = []; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { continue; } try { // This is similar to shoulder rotation but using different reference points // Pose landmark indices: 11=left shoulder, 12=right shoulder, 23=left hip, 24=right hip const leftShoulder = frame.pose_landmarks.find(lm => lm.index === 11); const rightShoulder = frame.pose_landmarks.find(lm => lm.index === 12); const leftHip = frame.pose_landmarks.find(lm => lm.index === 23); const rightHip = frame.pose_landmarks.find(lm => lm.index === 24); if (!leftShoulder || !rightShoulder || !leftHip || !rightHip) { spineTwistSeries.push(null); continue; } // Calculate shoulder and hip vectors const shoulderVector = { x: rightShoulder.x - leftShoulder.x, y: rightShoulder.y - leftShoulder.y }; const hipVector = { x: rightHip.x - leftHip.x, y: rightHip.y - leftHip.y }; // Calculate angle between vectors const dotProduct = shoulderVector.x * hipVector.x + shoulderVector.y * hipVector.y; const shoulderMag = Math.sqrt(shoulderVector.x * shoulderVector.x + shoulderVector.y * shoulderVector.y); const hipMag = Math.sqrt(hipVector.x * hipVector.x + hipVector.y * hipVector.y); if (shoulderMag === 0 || hipMag === 0) { spineTwistSeries.push(null); continue; } const cosAngle = dotProduct / (shoulderMag * hipMag); // Clamp to valid range const angle = Math.acos(Math.max(-1, Math.min(1, cosAngle))); spineTwistSeries.push(angle * 180 / Math.PI); } catch (error) { spineTwistSeries.push(null); } } return spineTwistSeries; } /** * Generate release angle series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of release angle values * @private */ _generateReleaseAngleSeries(analyzedFrames) { const releaseAngleSeries = []; // Find release frame index let releaseFrameIndex = -1; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (frame && frame.isReleaseFrame) { releaseFrameIndex = i; break; } } for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { releaseAngleSeries.push(null); continue; } // Only calculate for frames near the release point if (releaseFrameIndex === -1 || Math.abs(i - releaseFrameIndex) > 5) { releaseAngleSeries.push(null); continue; } try { // Pose landmark indices for wrist and elbow const wrist = frame.pose_landmarks.find(lm => lm.index === 16); const elbow = frame.pose_landmarks.find(lm => lm.index === 14); if (!wrist || !elbow) { releaseAngleSeries.push(null); continue; } // Calculate angle from horizontal const deltaY = wrist.y - elbow.y; const deltaX = wrist.x - elbow.x; // Calculate angle in radians const angleRadians = Math.atan2(deltaY, deltaX); // Convert to degrees const angleDegrees = angleRadians * 180 / Math.PI; releaseAngleSeries.push(angleDegrees); } catch (error) { releaseAngleSeries.push(null); } } return releaseAngleSeries; } /** * Generate shoulder velocity series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of shoulder velocity values * @private */ _generateShoulderVelocitySeries(analyzedFrames) { const shoulderVelocitySeries = []; for (let i = 0; i < analyzedFrames.length; i++) { // Skip first and last frames since we need previous and next frames if (i <= 0 || i >= analyzedFrames.length - 1) { shoulderVelocitySeries.push(null); continue; } const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { shoulderVelocitySeries.push(null); continue; } try { // Pose landmark index: 12=right shoulder const currentShoulder = frame.pose_landmarks.find(lm => lm.index === 12); // Get previous and next frames for velocity calculation const prevFrame = analyzedFrames[i - 1]; const nextFrame = analyzedFrames[i + 1]; if (!currentShoulder || !prevFrame || !nextFrame || !prevFrame.pose_landmarks || !nextFrame.pose_landmarks) { shoulderVelocitySeries.push(null); continue; } const prevShoulder = prevFrame.pose_landmarks.find(lm => lm.index === 12); const nextShoulder = nextFrame.pose_landmarks.find(lm => lm.index === 12); if (!prevShoulder || !nextShoulder) { shoulderVelocitySeries.push(null); continue; } // Calculate displacement const deltaX = nextShoulder.x - prevShoulder.x; const deltaY = nextShoulder.y - prevShoulder.y; // Calculate distance const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // Calculate time difference (assuming 30fps if no timestamps) const timePrev = prevFrame.timestamp || (i - 1) * 33.33; const timeNext = nextFrame.timestamp || (i + 1) * 33.33; const deltaTime = (timeNext - timePrev) / 1000; // Convert to seconds if (deltaTime === 0) { shoulderVelocitySeries.push(null); continue; } // Calculate velocity const velocity = distance / deltaTime; shoulderVelocitySeries.push(velocity); } catch (error) { shoulderVelocitySeries.push(null); } } return shoulderVelocitySeries; } /** * Generate hip velocity series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of hip velocity values * @private */ _generateHipVelocitySeries(analyzedFrames) { const hipVelocitySeries = []; for (let i = 0; i < analyzedFrames.length; i++) { // Skip first and last frames since we need previous and next frames if (i <= 0 || i >= analyzedFrames.length - 1) { hipVelocitySeries.push(null); continue; } const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { hipVelocitySeries.push(null); continue; } try { // Pose landmark index: 24=right hip const currentHip = frame.pose_landmarks.find(lm => lm.index === 24); // Get previous and next frames for velocity calculation const prevFrame = analyzedFrames[i - 1]; const nextFrame = analyzedFrames[i + 1]; if (!currentHip || !prevFrame || !nextFrame || !prevFrame.pose_landmarks || !nextFrame.pose_landmarks) { hipVelocitySeries.push(null); continue; } const prevHip = prevFrame.pose_landmarks.find(lm => lm.index === 24); const nextHip = nextFrame.pose_landmarks.find(lm => lm.index === 24); if (!prevHip || !nextHip) { hipVelocitySeries.push(null); continue; } // Calculate displacement const deltaX = nextHip.x - prevHip.x; const deltaY = nextHip.y - prevHip.y; // Calculate distance const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // Calculate time difference (assuming 30fps if no timestamps) const timePrev = prevFrame.timestamp || (i - 1) * 33.33; const timeNext = nextFrame.timestamp || (i + 1) * 33.33; const deltaTime = (timeNext - timePrev) / 1000; // Convert to seconds if (deltaTime === 0) { hipVelocitySeries.push(null); continue; } // Calculate velocity const velocity = distance / deltaTime; hipVelocitySeries.push(velocity); } catch (error) { hipVelocitySeries.push(null); } } return hipVelocitySeries; } /** * Generate approach velocity series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of approach velocity values * @private */ _generateApproachVelocitySeries(analyzedFrames) { const approachVelocitySeries = []; for (let i = 0; i < analyzedFrames.length; i++) { // Skip first and last frames since we need previous and next frames if (i <= 0 || i >= analyzedFrames.length - 1) { approachVelocitySeries.push(null); continue; } const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { approachVelocitySeries.push(null); continue; } try { // Use center of hips as reference point for approach const currentLeftHip = frame.pose_landmarks.find(lm => lm.index === 23); const currentRightHip = frame.pose_landmarks.find(lm => lm.index === 24); if (!currentLeftHip || !currentRightHip) { approachVelocitySeries.push(null); continue; } // Get previous and next frames for velocity calculation const prevFrame = analyzedFrames[i - 1]; const nextFrame = analyzedFrames[i + 1]; if (!prevFrame || !nextFrame || !prevFrame.pose_landmarks || !nextFrame.pose_landmarks) { approachVelocitySeries.push(null); continue; } const prevLeftHip = prevFrame.pose_landmarks.find(lm => lm.index === 23); const prevRightHip = prevFrame.pose_landmarks.find(lm => lm.index === 24); const nextLeftHip = nextFrame.pose_landmarks.find(lm => lm.index === 23); const nextRightHip = nextFrame.pose_landmarks.find(lm => lm.index === 24); if (!prevLeftHip || !prevRightHip || !nextLeftHip || !nextRightHip) { approachVelocitySeries.push(null); continue; } const prevHipCenter = { x: (prevLeftHip.x + prevRightHip.x) / 2, y: (prevLeftHip.y + prevRightHip.y) / 2 }; const nextHipCenter = { x: (nextLeftHip.x + nextRightHip.x) / 2, y: (nextLeftHip.y + nextRightHip.y) / 2 }; // Calculate displacement const deltaX = nextHipCenter.x - prevHipCenter.x; const deltaY = nextHipCenter.y - prevHipCenter.y; // Calculate distance const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // Calculate time difference (assuming 30fps if no timestamps) const timePrev = prevFrame.timestamp || (i - 1) * 33.33; const timeNext = nextFrame.timestamp || (i + 1) * 33.33; const deltaTime = (timeNext - timePrev) / 1000; // Convert to seconds if (deltaTime === 0) { approachVelocitySeries.push(null); continue; } // Calculate velocity const velocity = distance / deltaTime; approachVelocitySeries.push(velocity); } catch (error) { approachVelocitySeries.push(null); } } return approachVelocitySeries; } /** * Generate ball velocity series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of ball velocity values * @private */ _generateBallVelocitySeries(analyzedFrames) { const ballVelocitySeries = []; // Find release frame index let releaseFrameIndex = -1; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (frame && frame.isReleaseFrame) { releaseFrameIndex = i; break; } } for (let i = 0; i < analyzedFrames.length; i++) { // Skip first and last frames since we need previous and next frames if (i <= 0 || i >= analyzedFrames.length - 1) { ballVelocitySeries.push(null); continue; } const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { ballVelocitySeries.push(null); continue; } // Only calculate near release if (releaseFrameIndex === -1 || Math.abs(i - releaseFrameIndex) > 5) { ballVelocitySeries.push(null); continue; } try { // Use wrist as proxy for ball position const currentWrist = frame.pose_landmarks.find(lm => lm.index === 16); if (!currentWrist) { ballVelocitySeries.push(null); continue; } // Get previous and next frames for velocity calculation const prevFrame = analyzedFrames[i - 1]; const nextFrame = analyzedFrames[i + 1]; if (!prevFrame || !nextFrame || !prevFrame.pose_landmarks || !nextFrame.pose_landmarks) { ballVelocitySeries.push(null); continue; } const prevWrist = prevFrame.pose_landmarks.find(lm => lm.index === 16); const nextWrist = nextFrame.pose_landmarks.find(lm => lm.index === 16); if (!prevWrist || !nextWrist) { ballVelocitySeries.push(null); continue; } // Calculate displacement const deltaX = nextWrist.x - prevWrist.x; const deltaY = nextWrist.y - prevWrist.y; // Calculate distance const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // Calculate time difference (assuming 30fps if no timestamps) const timePrev = prevFrame.timestamp || (i - 1) * 33.33; const timeNext = nextFrame.timestamp || (i + 1) * 33.33; const deltaTime = (timeNext - timePrev) / 1000; // Convert to seconds if (deltaTime === 0) { ballVelocitySeries.push(null); continue; } // Calculate velocity const velocity = distance / deltaTime; ballVelocitySeries.push(velocity); } catch (error) { ballVelocitySeries.push(null); } } return ballVelocitySeries; } /** * Generate release position series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of release position values * @private */ _generateReleasePositionSeries(analyzedFrames) { const releasePositionSeries = []; // Find release frame index let releaseFrameIndex = -1; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (frame && frame.isReleaseFrame) { releaseFrameIndex = i; break; } } for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { releasePositionSeries.push(null); continue; } // Only calculate near release frame if (releaseFrameIndex === -1 || Math.abs(i - releaseFrameIndex) > 5) { releasePositionSeries.push(null); continue; } try { // Use wrist position at release const wrist = frame.pose_landmarks.find(lm => lm.index === 16); if (!wrist) { releasePositionSeries.push(null); continue; } // Return horizontal position as a relative value (0-1) releasePositionSeries.push(wrist.x); } catch (error) { releasePositionSeries.push(null); } } return releasePositionSeries; } /** * Generate foot position series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of foot position values * @private */ _generateFootPositionSeries(analyzedFrames) { const footPositionSeries = []; for (let i = 0; i < analyzedFrames.length; i++) { const frame = analyzedFrames[i]; if (!frame || !frame.pose_landmarks || !Array.isArray(frame.pose_landmarks) || frame.pose_landmarks.length === 0) { footPositionSeries.push(null); continue; } try { // Pose landmark indices: 31=right foot, 32=left foot const rightFoot = frame.pose_landmarks.find(lm => lm.index === 31); const leftFoot = frame.pose_landmarks.find(lm => lm.index === 32); if (!rightFoot || !leftFoot) { footPositionSeries.push(null); continue; } // Calculate foot position as distance between feet const deltaX = rightFoot.x - leftFoot.x; const deltaY = rightFoot.y - leftFoot.y; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); footPositionSeries.push(distance); } catch (error) { footPositionSeries.push(null); } } return footPositionSeries; } /** * Generate knee velocity series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of knee velocity values * @private */ _generateKneeVelocitySeries(analyzedFrames) { const kneeVelocitySeries = []; for (let i = 1; i < analyzedFrames.length - 1; i++) { const prevFrame = analyzedFrames[i - 1]; const currentFrame = analyzedFrames[i]; const nextFrame = analyzedFrames[i + 1]; if (!prevFrame || !currentFrame || !nextFrame || !prevFrame.pose_landmarks || !currentFrame.pose_landmarks || !nextFrame.pose_landmarks) { kneeVelocitySeries.push(null); continue; } try { // Use right knee (landmark index 26) for knee velocity const prevKnee = prevFrame.pose_landmarks.find(lm => lm.index === 26); const currentKnee = currentFrame.pose_landmarks.find(lm => lm.index === 26); const nextKnee = nextFrame.pose_landmarks.find(lm => lm.index === 26); if (!prevKnee || !currentKnee || !nextKnee) { kneeVelocitySeries.push(null); continue; } // Calculate displacement between previous and next frames const dx = nextKnee.x - prevKnee.x; const dy = nextKnee.y - prevKnee.y; const dz = (nextKnee.z || 0) - (prevKnee.z || 0); // Calculate displacement magnitude const displacement = Math.sqrt(dx * dx + dy * dy + dz * dz); // Calculate time delta (in seconds) - use timestamps if available, otherwise assume 30fps const timePrev = prevFrame.timestamp || (i - 1) * 33.33; const timeNext = nextFrame.timestamp || (i + 1) * 33.33; const deltaTime = (timeNext - timePrev) / 1000; // Convert to seconds if (deltaTime === 0) { kneeVelocitySeries.push(null); continue; } // Calculate velocity (distance per second) const velocity = displacement / deltaTime; kneeVelocitySeries.push(velocity); } catch (error) { kneeVelocitySeries.push(null); } } // Add null for first and last frames kneeVelocitySeries.unshift(null); kneeVelocitySeries.push(null); return kneeVelocitySeries; } /** * Generate leg velocity series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of leg velocity values * @private */ _generateLegVelocitySeries(analyzedFrames) { const legVelocitySeries = []; for (let i = 1; i < analyzedFrames.length - 1; i++) { const prevFrame = analyzedFrames[i - 1]; const currentFrame = analyzedFrames[i]; const nextFrame = analyzedFrames[i + 1]; if (!prevFrame || !currentFrame || !nextFrame || !prevFrame.pose_landmarks || !currentFrame.pose_landmarks || !nextFrame.pose_landmarks) { legVelocitySeries.push(null); continue; } try { // Use right ankle (landmark index 28) for leg velocity const prevAnkle = prevFrame.pose_landmarks.find(lm => lm.index === 28); const currentAnkle = currentFrame.pose_landmarks.find(lm => lm.index === 28); const nextAnkle = nextFrame.pose_landmarks.find(lm => lm.index === 28); if (!prevAnkle || !currentAnkle || !nextAnkle) { legVelocitySeries.push(null); continue; } // Calculate displacement between previous and next frames const dx = nextAnkle.x - prevAnkle.x; const dy = nextAnkle.y - prevAnkle.y; const dz = (nextAnkle.z || 0) - (prevAnkle.z || 0); // Calculate displacement magnitude const displacement = Math.sqrt(dx * dx + dy * dy + dz * dz); // Calculate time delta (in seconds) - use timestamps if available, otherwise assume 30fps const timePrev = prevFrame.timestamp || (i - 1) * 33.33; const timeNext = nextFrame.timestamp || (i + 1) * 33.33; const deltaTime = (timeNext - timePrev) / 1000; // Convert to seconds if (deltaTime === 0) { legVelocitySeries.push(null); continue; } // Calculate velocity (distance per second) const velocity = displacement / deltaTime; legVelocitySeries.push(velocity); } catch (error) { legVelocitySeries.push(null); } } // Add null for first and last frames legVelocitySeries.unshift(null); legVelocitySeries.push(null); return legVelocitySeries; } /** * Generate wrist velocity series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of wrist velocity values * @private */ _generateWristVelocitySeries(analyzedFrames) { const wristVelocitySeries = []; for (let i = 1; i < analyzedFrames.length - 1; i++) { const prevFrame = analyzedFrames[i - 1]; const currentFrame = analyzedFrames[i]; const nextFrame = analyzedFrames[i + 1]; if (!prevFrame || !currentFrame || !nextFrame || !prevFrame.pose_landmarks || !currentFrame.pose_landmarks || !nextFrame.pose_landmarks) { wristVelocitySeries.push(null); continue; } try { // Use right wrist (landmark index 16) for wrist velocity const prevWrist = prevFrame.pose_landmarks.find(lm => lm.index === 16); const currentWrist = currentFrame.pose_landmarks.find(lm => lm.index === 16); const nextWrist = nextFrame.pose_landmarks.find(lm => lm.index === 16); if (!prevWrist || !currentWrist || !nextWrist) { wristVelocitySeries.push(null); continue; } // Calculate displacement between previous and next frames const dx = nextWrist.x - prevWrist.x; const dy = nextWrist.y - prevWrist.y; const dz = (nextWrist.z || 0) - (prevWrist.z || 0); // Calculate displacement magnitude const displacement = Math.sqrt(dx * dx + dy * dy + dz * dz); // Calculate time delta (in seconds) - use timestamps if available, otherwise assume 30fps const timePrev = prevFrame.timestamp || (i - 1) * 33.33; const timeNext = nextFrame.timestamp || (i + 1) * 33.33; const deltaTime = (timeNext - timePrev) / 1000; // Convert to seconds if (deltaTime === 0) { wristVelocitySeries.push(null); continue; } // Calculate velocity (distance per second) const velocity = displacement / deltaTime; wristVelocitySeries.push(velocity); } catch (error) { wristVelocitySeries.push(null); } } // Add null for first and last frames wristVelocitySeries.unshift(null); wristVelocitySeries.push(null); return wristVelocitySeries; } /** * Generate arm velocity series from analyzed frames * @param {Array} analyzedFrames - Array of analyzed frames * @returns {Array} - Array of arm velocity values * @private */ _generateArmVelocitySeries(analyzedFrames) { const armVelocitySeries = []; for (let i = 1; i < analyzedFrames.length - 1; i++) { const prevFrame = analyzedFrames[i - 1]; const currentFrame = analyzedFrames[i]; const nextFrame = analyzedFrames[i + 1]; if (!prevFrame || !currentFrame || !nextFrame || !prevFrame.pose_landmarks || !currentFrame.pose_landmarks || !nextFrame.pose_landmarks) { armVelocitySeries.push(null); continue; } try { // Use right elbow (landmark index 14) for arm velocity const prevElbow = prevFrame.pose_landmarks.find(lm => lm.index === 14); const currentElbow = currentFrame.pose_landmarks.find(lm => lm.index === 14); const nextElbow = nextFrame.pose_landmarks.find(lm => lm.index === 14); if (!prevElbow || !currentElbow || !nextElbow) { armVelocitySeries.push(null); continue; } // Calculate displacement between previous and next frames const dx = nextElbow.x - prevElbow.x; const dy = nextElbow.y - prevElbow.y; const dz = (nextElbow.z || 0) - (prevElbow.z || 0); // Calculate displacement magnitude const displacement = Math.sqrt(dx * dx + dy * dy + dz * dz); // Calculate time delta (in seconds) - use timestamps if available, otherwise assume 30fps const timePrev = prevFrame.timestamp || (i - 1) * 33.33; const timeNext = nextFrame.timestamp || (i + 1) * 33.33; const deltaTime = (timeNext - timePrev) / 10