bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
154 lines (131 loc) • 6.6 kB
JavaScript
/**
* @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;