bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
196 lines (168 loc) • 7.27 kB
JavaScript
/**
* Timing calculation methods for bowling metrics
* Contains methods for calculating timing-related metrics from pose landmarks
*/
const { getSafeLandmark } = require('./MetricsUtilities');
const timingCalculations = {
/**
* Calculate phase progress
* @param {Array} landmarks - Pose landmarks array
* @returns {number|null} Phase progress (0-100) or null if calculation not possible
*/
calculatePhaseProgress(landmarks) {
try {
const wrist = getSafeLandmark(landmarks, 15) || getSafeLandmark(landmarks, 16); // Left or right wrist
const hip = getSafeLandmark(landmarks, 23) || getSafeLandmark(landmarks, 24); // Left or right hip
if (!wrist || !hip) {
return null;
}
// Calculate vertical position of wrist relative to hip
const relativeHeight = wrist.y - hip.y;
// Map to a 0-100 scale where negative values (wrist above hip) represent early phase
// and positive values (wrist below hip) represent later phase
let progress = 50 + (relativeHeight * 100);
progress = Math.max(0, Math.min(100, progress));
return progress;
} catch (error) {
console.warn('Error calculating phase progress:', error.message);
return null;
}
},
/**
* Calculate rhythm timing
* @param {Array} landmarks - Pose landmarks array
* @returns {number|null} Raw arm position relative to shoulder or null if calculation not possible
*/
calculateRhythmTiming(landmarks) {
try {
// This would ideally look at the timing between key events
// We'll approximate based on the position of the bowling arm
const wrist = getSafeLandmark(landmarks, 15) || getSafeLandmark(landmarks, 16); // Left or right wrist
const shoulder = getSafeLandmark(landmarks, 11) || getSafeLandmark(landmarks, 12); // Left or right shoulder
if (!wrist || !shoulder) {
return null;
}
// Return raw vertical position of wrist relative to shoulder
// Positive values: wrist below shoulder
// Negative values: wrist above shoulder
return wrist.y - shoulder.y;
} catch (error) {
console.warn('Error calculating rhythm timing:', error.message);
return null;
}
},
/**
* Calculate approach duration between two frames
* @param {Object} currentFrame - Current frame with pose_landmarks and timestamp
* @param {Object} startFrame - Start frame with pose_landmarks and timestamp
* @returns {number|null} Duration in seconds or null if calculation not possible
*/
calculateApproachDuration(currentFrame, startFrame) {
try {
if (!currentFrame || !startFrame || !currentFrame.timestamp || !startFrame.timestamp) {
return null;
}
const durationMs = currentFrame.timestamp - startFrame.timestamp;
// Convert to seconds
return durationMs / 1000;
} catch (error) {
console.warn('Error calculating approach duration:', error.message);
return null;
}
},
/**
* Calculate stride time using two frames
* @param {Object} footLandingFrame - Frame with foot landing pose_landmarks and timestamp
* @param {Object} previousFootLandingFrame - Previous foot landing frame with pose_landmarks and timestamp
* @returns {number|null} Stride time in seconds or null if calculation not possible
*/
calculateStrideTime(footLandingFrame, previousFootLandingFrame) {
try {
if (!footLandingFrame || !previousFootLandingFrame ||
!footLandingFrame.timestamp || !previousFootLandingFrame.timestamp) {
return null;
}
const durationMs = footLandingFrame.timestamp - previousFootLandingFrame.timestamp;
// Convert to seconds
return durationMs / 1000;
} catch (error) {
console.warn('Error calculating stride time:', error.message);
return null;
}
},
/**
* Calculate delivery time between front foot landing and release
* @param {Object} releaseFrame - Release frame with pose_landmarks and timestamp
* @param {Object} frontFootLandingFrame - Front foot landing frame with pose_landmarks and timestamp
* @returns {number|null} Delivery time in seconds or null if calculation not possible
*/
calculateDeliveryTime(releaseFrame, frontFootLandingFrame) {
try {
if (!releaseFrame || !frontFootLandingFrame ||
!releaseFrame.timestamp || !frontFootLandingFrame.timestamp) {
return null;
}
const durationMs = releaseFrame.timestamp - frontFootLandingFrame.timestamp;
// Convert to seconds
return durationMs / 1000;
} catch (error) {
console.warn('Error calculating delivery time:', error.message);
return null;
}
},
/**
* Calculate transition timing from backswing to follow through
* @param {Object} followThroughFrame - Follow through frame with pose_landmarks and timestamp
* @param {Object} backswingFrame - Backswing frame with pose_landmarks and timestamp
* @returns {number|null} Transition time in seconds or null if calculation not possible
*/
calculateTransitionTiming(followThroughFrame, backswingFrame) {
try {
if (!followThroughFrame || !backswingFrame ||
!followThroughFrame.timestamp || !backswingFrame.timestamp) {
return null;
}
const durationMs = followThroughFrame.timestamp - backswingFrame.timestamp;
// Convert to seconds
return durationMs / 1000;
} catch (error) {
console.warn('Error calculating transition timing:', error.message);
return null;
}
},
/**
* Calculate overall timing score based on the rhythm and fluidity of movement
* @param {Array} frames - Array of frames with pose_landmarks and timestamps
* @returns {number|null} Raw coefficient of variation (CV) or null if calculation not possible
*/
calculateTimingScore(frames) {
try {
if (!frames || !Array.isArray(frames) || frames.length < 10) {
return null;
}
// Extract timestamps if available
const timestamps = frames
.filter(frame => frame && frame.timestamp)
.map(frame => frame.timestamp);
if (timestamps.length < 10) {
return null; // Not enough timing data
}
// Calculate time intervals between frames
const intervals = [];
for (let i = 1; i < timestamps.length; i++) {
intervals.push(timestamps[i] - timestamps[i-1]);
}
// Calculate consistency of intervals (standard deviation)
const meanInterval = intervals.reduce((sum, val) => sum + val, 0) / intervals.length;
const squaredDifferences = intervals.map(interval => Math.pow(interval - meanInterval, 2));
const variance = squaredDifferences.reduce((sum, val) => sum + val, 0) / intervals.length;
const stdDev = Math.sqrt(variance);
// Calculate coefficient of variation (lower is better rhythm consistency)
return meanInterval > 0 ? (stdDev / meanInterval) : 1;
} catch (error) {
console.warn('Error calculating timing score:', error.message);
return null;
}
}
};
module.exports = timingCalculations;