bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
293 lines (239 loc) • 9.94 kB
JavaScript
/**
* @module bowling_analysis/metrics/EventDetectionUtils
* @description Utility functions for event detection and validation
*/
/**
* Derive velocity metrics from position data if they don't exist
* @param {Object} metrics - The metrics object containing time series data
* @returns {Object} The metrics object with derived velocity metrics
*/
function deriveVelocityFromPosition(metrics) {
if (!metrics || !metrics.timeSeries) {
return metrics;
}
// Check if velocity metrics already exist
const hasVelocityData = metrics.timeSeries.velocity &&
(metrics.timeSeries.velocity.leftAnkleVelocity ||
metrics.timeSeries.velocity.leftFootVelocity ||
metrics.timeSeries.velocity.rightAnkleVelocity ||
metrics.timeSeries.velocity.rightFootVelocity);
if (hasVelocityData) {
return metrics; // Velocity data already exists
}
console.log('Deriving velocity metrics from position data');
// Initialize velocity category if needed
if (!metrics.timeSeries.velocity) {
metrics.timeSeries.velocity = {};
}
// Extract position data
const leftAnkleHeight = metrics.timeSeries?.position?.leftAnkleHeight ||
metrics.timeSeries?.position?.leftFootHeight;
const rightAnkleHeight = metrics.timeSeries?.position?.rightAnkleHeight ||
metrics.timeSeries?.position?.rightFootHeight;
const leftAnkleX = metrics.timeSeries?.position?.leftAnkleX ||
metrics.timeSeries?.position?.leftFootX;
const rightAnkleX = metrics.timeSeries?.position?.rightAnkleX ||
metrics.timeSeries?.position?.rightFootX;
// Derive velocities if position data exists
if (leftAnkleHeight && leftAnkleHeight.length > 1) {
metrics.timeSeries.velocity.leftAnkleVelocity = calculateVelocityFromPosition(leftAnkleHeight, leftAnkleX);
console.log('Derived left ankle velocity from position data');
}
if (rightAnkleHeight && rightAnkleHeight.length > 1) {
metrics.timeSeries.velocity.rightAnkleVelocity = calculateVelocityFromPosition(rightAnkleHeight, rightAnkleX);
console.log('Derived right ankle velocity from position data');
}
// If we have ankle joint angles, also derive angular velocities
if (metrics.timeSeries.angles) {
const elbowAngle = metrics.timeSeries.angles.elbowFlexion;
const shoulderAngle = metrics.timeSeries.angles.shoulderRotation;
if (elbowAngle && elbowAngle.length > 1) {
metrics.timeSeries.velocity.elbowAngularVelocity = calculateAngularVelocity(elbowAngle);
console.log('Derived elbow angular velocity from angle data');
}
if (shoulderAngle && shoulderAngle.length > 1) {
metrics.timeSeries.velocity.shoulderAngularVelocity = calculateAngularVelocity(shoulderAngle);
console.log('Derived shoulder angular velocity from angle data');
}
}
return metrics;
}
/**
* Calculate angular velocity from angle data
* @param {Array<number>} angleData - Array of angle values
* @returns {Array<number>} Array of calculated angular velocity values
*/
function calculateAngularVelocity(angleData) {
if (!angleData || angleData.length < 2) {
return [];
}
const angularVelocity = [0]; // First frame has zero velocity
for (let i = 1; i < angleData.length; i++) {
if (angleData[i-1] === null || angleData[i] === null) {
angularVelocity.push(null);
continue;
}
// Calculate angular velocity as the rate of change of angle
const velocityValue = Math.abs(angleData[i] - angleData[i-1]);
angularVelocity.push(velocityValue);
}
return angularVelocity;
}
/**
* Calculate velocity from position data using both vertical and horizontal components
* @param {Array<number>} heightData - Array of height values
* @param {Array<number>} xData - Array of x position values
* @returns {Array<number>} Array of calculated velocity values
*/
function calculateVelocityFromPosition(heightData, xData) {
if (!heightData || heightData.length < 2) {
return [];
}
const velocities = [0]; // First frame has zero velocity
for (let i = 1; i < heightData.length; i++) {
if (heightData[i-1] === null || heightData[i] === null) {
velocities.push(null);
continue;
}
// Calculate vertical velocity component
const verticalDelta = Math.abs(heightData[i] - heightData[i-1]);
// Calculate horizontal velocity component if available
let horizontalDelta = 0;
if (xData && xData[i] !== null && xData[i-1] !== null) {
horizontalDelta = Math.abs(xData[i] - xData[i-1]);
}
// Combined velocity (Pythagorean theorem)
const velocity = Math.sqrt(verticalDelta * verticalDelta + horizontalDelta * horizontalDelta);
velocities.push(velocity);
}
return velocities;
}
/**
* Get a metric value from a nested path
* @param {Object} metrics - The metrics object
* @param {string} path - The dot-notation path to the metric
* @returns {any} The metric value or undefined if not found
*/
function getMetricByPath(metrics, path) {
if (!metrics || !path) {
return undefined;
}
const parts = path.split('.');
let current = metrics;
for (const part of parts) {
if (current === undefined || current === null) {
return undefined;
}
current = current[part];
}
return current;
}
/**
* Detect events from moments data, mapping from moments to standardized event names
* @param {Object} metrics - The metrics object that contains moments data
* @returns {Object} Object with detected events
*/
function detectEventsFromMoments(metrics) {
const events = {};
if (!metrics || !metrics.moments) {
return events;
}
// Map from moments keys to standardized event names
const eventMapping = {
'ball_release': 'releasePoint',
'left_foot_plant': 'frontFootLanding',
'right_foot_plant': 'backFootLanding'
};
// Process each moment type
Object.keys(eventMapping).forEach(momentKey => {
if (!metrics.moments[momentKey] || !metrics.moments[momentKey].length) {
return;
}
const standardName = eventMapping[momentKey];
const frames = metrics.moments[momentKey];
// For most events, we use the first frame of the moment
let frameToUse = frames[0];
// For foot plants, find contiguous sequences and use first frame
if (momentKey === 'left_foot_plant' || momentKey === 'right_foot_plant') {
// Sort frames
const sortedFrames = [...frames].sort((a, b) => a - b);
// Find sequences of contiguous frames
const sequences = [];
let currentSequence = [sortedFrames[0]];
for (let i = 1; i < sortedFrames.length; i++) {
if (sortedFrames[i] === sortedFrames[i-1] + 1) {
// Continue current sequence
currentSequence.push(sortedFrames[i]);
} else {
// Start a new sequence
sequences.push(currentSequence);
currentSequence = [sortedFrames[i]];
}
}
// Add final sequence
if (currentSequence.length > 0) {
sequences.push(currentSequence);
}
// Use the first frame of the longest sequence
if (sequences.length > 0) {
// Sort sequences by length (descending)
sequences.sort((a, b) => b.length - a.length);
frameToUse = sequences[0][0]; // First frame of longest sequence
}
}
// Create the event
events[standardName] = {
frameIndex: frameToUse,
confidence: 1.0, // High confidence for moments-based detection
method: 'moments_analysis'
};
// Add side information for foot landings
if (momentKey === 'left_foot_plant') {
events[standardName].side = 'left';
} else if (momentKey === 'right_foot_plant') {
events[standardName].side = 'right';
}
});
return events;
}
/**
* Validate that events follow the proper biomechanical sequence
* @param {Object} events - The detected events
* @returns {boolean} True if the sequence is valid, false otherwise
*/
function validateEventSequence(events) {
if (!events) {
return false;
}
const backFootFrame = events.backFootLanding?.frameIndex;
const frontFootFrame = events.frontFootLanding?.frameIndex;
const releaseFrame = events.releasePoint?.frameIndex;
// Log detected events
console.log(`Validating event sequence: backFoot=${backFootFrame}, frontFoot=${frontFootFrame}, release=${releaseFrame}`);
// Check if we have all required events
if (backFootFrame === null || backFootFrame === undefined ||
frontFootFrame === null || frontFootFrame === undefined ||
releaseFrame === null || releaseFrame === undefined) {
console.warn('Event sequence invalid: Missing one or more required events');
return false;
}
// Ensure proper sequence: backFootLanding < frontFootLanding < releasePoint
if (backFootFrame >= frontFootFrame) {
console.warn(`Event sequence invalid: Back foot landing (${backFootFrame}) should occur before front foot landing (${frontFootFrame})`);
return false;
}
if (frontFootFrame >= releaseFrame) {
console.warn(`Event sequence invalid: Front foot landing (${frontFootFrame}) should occur before release point (${releaseFrame})`);
return false;
}
console.log('Event sequence valid: Proper biomechanical sequence detected');
return true;
}
module.exports = {
deriveVelocityFromPosition,
calculateVelocityFromPosition,
calculateAngularVelocity,
getMetricByPath,
detectEventsFromMoments,
validateEventSequence
};