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
JavaScript
/**
* 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