UNPKG

bowling-analysis-system

Version:

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

154 lines (131 loc) 6.6 kB
/** * @module bowling_analysis/metrics/calculators/TimingCalculator * @description Calculator for timing-related metrics */ const timingCalculations = require('../calculations/TimingCalculations'); /** * @class TimingCalculator * @description Calculates timing-related metrics based on keypoint data and events */ class TimingCalculator { /** * Calculate timing metrics * @param {Array|Object} data - Array of keypoint frames or metrics object from Phase 1 * @param {Array|Object} validFramesOrTimeSeries - Valid frames or time series data * @param {Object} options - Calculation options or events object (Phase 3) * @param {Object} [events] - Detected events (only used in Phase 3) * @returns {Object} Calculated timing metrics */ static async calculate(data, validFramesOrTimeSeries, options = {}, events = {}) { try { // Determine if we're running in Phase 1 or Phase 3 mode const isPhaseOne = Array.isArray(data) && Array.isArray(validFramesOrTimeSeries); // Initialize result and timeSeries structures const result = {}; const timeSeries = {}; if (isPhaseOne) { // Phase 1 mode: Calculate basic timing metrics that don't depend on events const keypointData = data; const validFrames = validFramesOrTimeSeries; // Initialize Phase 1 specific metrics result.phaseProgress = null; result.rhythmTiming = null; // Calculate time series for phase progress and rhythm timing if (options.includeTimeSeries) { timeSeries.phaseProgress = Array(keypointData.length).fill(null); timeSeries.rhythmTiming = Array(keypointData.length).fill(null); // Calculate phase progress and rhythm timing for each valid frame validFrames.forEach((frame) => { const index = frame.index !== undefined ? frame.index : 0; if (index >= 0 && index < keypointData.length) { timeSeries.phaseProgress[index] = timingCalculations.calculatePhaseProgress( frame.keypoints ); timeSeries.rhythmTiming[index] = timingCalculations.calculateRhythmTiming( frame.keypoints ); } }); // Calculate averages const validPhaseProgress = timeSeries.phaseProgress.filter(v => v !== null); const validRhythmTiming = timeSeries.rhythmTiming.filter(v => v !== null); if (validPhaseProgress.length > 0) { result.phaseProgress = validPhaseProgress.reduce((a, b) => a + b, 0) / validPhaseProgress.length; } if (validRhythmTiming.length > 0) { result.rhythmTiming = validRhythmTiming.reduce((a, b) => a + b, 0) / validRhythmTiming.length; } } } else { // Phase 3 mode: Calculate event-dependent timing metrics const phaseData = data; const timeSeries = validFramesOrTimeSeries; const evts = events || options; // Support both parameter positions // Initialize Phase 3 specific metrics result.timingScores = {}; result.phaseDurations = {}; result.deliveryTimes = {}; result.transitionTimings = {}; // Only calculate timing metrics if we have events if (evts && typeof evts === 'object') { // Extract event frames considering different formats const releaseFrame = evts.releaseFrame || evts.releasePoint?.frame; const frontFootFrame = evts.frontFootFrame || evts.frontFootLanding?.frame; const backFootFrame = evts.backFootFrame || evts.backFootLanding?.frame; // Get the keypointData length const keypointLength = timeSeries?.frameIndex?.length || phaseData?.timeSeries?.frameIndex?.length || 132; // Extract frames from phase data const allFrames = Array.from({ length: keypointLength }, (_, i) => ({ index: i, // Try to get keypoints from original data in different formats keypoints: phaseData.keypointData?.[i]?.keypoints || phaseData.frames?.[i]?.keypoints || {} })); // Calculate timing score using the entire sequence if (Array.isArray(allFrames) && allFrames.length > 0) { result.timingScores.overall = timingCalculations.calculateTimingScore(allFrames); } // Calculate approach duration if we have events if (backFootFrame !== undefined && frontFootFrame !== undefined) { const backFootObj = { timestamp: backFootFrame, frame: backFootFrame }; const frontFootObj = { timestamp: frontFootFrame, frame: frontFootFrame }; result.phaseDurations.approach = timingCalculations.calculateStrideTime( frontFootObj, backFootObj ); } // Calculate delivery time if (frontFootFrame !== undefined && releaseFrame !== undefined) { const frontFootObj = { timestamp: frontFootFrame, frame: frontFootFrame }; const releaseObj = { timestamp: releaseFrame, frame: releaseFrame }; result.deliveryTimes.release = timingCalculations.calculateDeliveryTime( releaseObj, frontFootObj ); } // Calculate transition timing if we have a release frame if (releaseFrame !== undefined) { // Estimate follow-through frame as 15 frames after release const followThroughFrame = releaseFrame + 15; const releaseObj = { timestamp: releaseFrame, frame: releaseFrame }; const followThroughObj = { timestamp: followThroughFrame, frame: followThroughFrame }; result.transitionTimings.followThrough = timingCalculations.calculateTransitionTiming( followThroughObj, releaseObj ); } } } return { metrics: result, timeSeries: timeSeries }; } catch (error) { console.error('Error calculating timing metrics:', error); return { metrics: {}, timeSeries: {} }; } } } module.exports = TimingCalculator;