UNPKG

bowling-analysis-system

Version:

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

196 lines (168 loc) 7.27 kB
/** * 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;