UNPKG

bowling-analysis-system

Version:

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

386 lines (328 loc) 10.9 kB
/** * @module metrics/calculations/MetricsUtilities * @description Utility functions for biomechanical metrics calculations */ /** * Utility function to calculate the angle between two vectors * @param {Object} v1 - First vector with x,y,z components * @param {Object} v2 - Second vector with x,y,z components * @returns {number} Angle in degrees */ function calculateAngleBetweenVectors(v1, v2) { // Check for null values if (!v1 || !v2) { return 0; } // Standardize input to make sure we have valid vectors const vector1 = standardizePointFormat(v1); const vector2 = standardizePointFormat(v2); if (!vector1 || !vector2) { return 0; } // Calculate dot product const dotProduct = vector1.x * vector2.x + vector1.y * vector2.y + vector1.z * vector2.z; // Calculate magnitudes const mag1 = Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y + vector1.z * vector1.z); const mag2 = Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y + vector2.z * vector2.z); // Avoid division by zero if (mag1 < 0.0001 || mag2 < 0.0001) { return 0; } // Calculate angle in radians and convert to degrees // Use Math.max and Math.min to clamp value between -1 and 1 to avoid NaN from floating point errors const angleRad = Math.acos(Math.max(-1, Math.min(1, dotProduct / (mag1 * mag2)))); return angleRad * (180 / Math.PI); } /** * Get landmark position safely with error checking * @param {Array|Object} landmarks - Pose landmarks array or object * @param {number} index - Index of the landmark * @param {number} minConfidence - Minimum confidence threshold (0-1) * @returns {Array|null} Landmark coordinates [x,y,z] or null */ function getSafeLandmark(landmarks, index, minConfidence = 0) { if (!landmarks) { return null; } // Check if landmarks is an array if (!Array.isArray(landmarks)) { // Handle object format - landmarks may be an object with numeric keys or named keys if (typeof landmarks === 'object' && landmarks !== null) { // Try to access by index if (landmarks[index] !== undefined) { const landmark = landmarks[index]; // If landmark is object with x,y,z properties if (typeof landmark === 'object' && 'x' in landmark) { if ('visibility' in landmark && landmark.visibility < minConfidence) { return null; } return landmark; } // If landmark is array with [x,y,z,visibility] if (Array.isArray(landmark) && landmark.length >= 3) { if (landmark.length >= 4 && landmark[3] < minConfidence) { return null; } return { x: landmark[0] || 0, y: landmark[1] || 0, z: landmark[2] || 0, visibility: landmark.length >= 4 ? landmark[3] : 1.0 }; } } // Try to access by landmark name if index represents a named landmark const landmarkNames = [ 'nose', 'leftEye', 'rightEye', 'leftEar', 'rightEar', 'leftShoulder', 'rightShoulder', 'leftElbow', 'rightElbow', 'leftWrist', 'rightWrist', 'leftHip', 'rightHip', 'leftKnee', 'rightKnee', 'leftAnkle', 'rightAnkle' ]; if (index < landmarkNames.length && landmarks[landmarkNames[index]] !== undefined) { const landmark = landmarks[landmarkNames[index]]; if (typeof landmark === 'object' && 'x' in landmark) { if ('visibility' in landmark && landmark.visibility < minConfidence) { return null; } return landmark; } } } return null; } // Handle keypoint.json format (array of arrays with [x,y,z,visibility]) // Format: landmarks may be an array of coordinates where each coordinate is [x,y,z,visibility] if (Array.isArray(landmarks[index]) && landmarks[index].length >= 3) { const landmark = landmarks[index]; // Check confidence if available (4th element) if (landmark.length >= 4 && landmark[3] < minConfidence) { return null; } return { x: landmark[0] || 0, y: landmark[1] || 0, z: landmark[2] || 0, visibility: landmark.length >= 4 ? landmark[3] : 1.0 }; } // If landmarks is already in the right format (array of objects with x,y,z properties) if (typeof landmarks[index] === 'object' && 'x' in landmarks[index]) { const landmark = landmarks[index]; if ('visibility' in landmark && landmark.visibility < minConfidence) { return null; } return landmark; } return null; } /** * Calculate distance between two points * @param {Array|Object} point1 - First point [x,y,z] or {x,y,z} * @param {Array|Object} point2 - Second point [x,y,z] or {x,y,z} * @returns {number} Distance between points */ function calculateDistance(point1, point2) { // Check if points are valid if (!point1 || !point2) return 0; let x1, y1, z1, x2, y2, z2; // Handle array format [x,y,z] if (Array.isArray(point1) && Array.isArray(point2)) { if (point1.length < 3 || point2.length < 3) return 0; x1 = point1[0]; y1 = point1[1]; z1 = point1[2]; x2 = point2[0]; y2 = point2[1]; z2 = point2[2]; } // Handle object format {x,y,z} else if (typeof point1 === 'object' && typeof point2 === 'object') { if (!('x' in point1) || !('y' in point1) || !('z' in point1) || !('x' in point2) || !('y' in point2) || !('z' in point2)) { return 0; } x1 = point1.x; y1 = point1.y; z1 = point1.z; x2 = point2.x; y2 = point2.y; z2 = point2.z; } else { return 0; // Invalid input format } // Check for NaN values if (isNaN(x1) || isNaN(y1) || isNaN(z1) || isNaN(x2) || isNaN(y2) || isNaN(z2)) { return 0; } // Calculate Euclidean distance return Math.sqrt( Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2) + Math.pow(z2 - z1, 2) ); } /** * Calculate velocity between two positions with time delta * @param {Array|Object} currentPos - Current position coordinates * @param {Array|Object} prevPos - Previous position coordinates * @param {number} timeDelta - Time difference in seconds * @returns {number|null} Velocity magnitude or null if invalid */ function calculateVelocity(currentPos, prevPos, timeDelta) { // Validate inputs if (!currentPos || !prevPos) { return null; } if (timeDelta <= 0 || isNaN(timeDelta)) { return null; } // Handle different position formats let current = standardizePosition(currentPos); let prev = standardizePosition(prevPos); if (!current || !prev) { return null; } // Calculate displacement const dx = current.x - prev.x; const dy = current.y - prev.y; const dz = current.z - prev.z; // Check for NaN values if (isNaN(dx) || isNaN(dy) || isNaN(dz)) { return null; } // Calculate magnitude of displacement const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); // Check for NaN or Infinity if (!isFinite(distance)) { return null; } // Calculate velocity (distance / time) const velocity = distance / timeDelta; // Final validation return isFinite(velocity) ? velocity : null; } /** * Calculate acceleration between two velocities * @param {number} currentVel - Current velocity * @param {number} prevVel - Previous velocity * @param {number} timeDelta - Time difference in seconds * @returns {number} Acceleration magnitude */ function calculateAcceleration(currentVel, prevVel, timeDelta) { // Validate inputs if (typeof currentVel !== 'number' || typeof prevVel !== 'number' || isNaN(currentVel) || isNaN(prevVel)) { return 0; } if (timeDelta <= 0 || isNaN(timeDelta)) { return 0; } // Calculate acceleration (change in velocity / time) return (currentVel - prevVel) / timeDelta; } /** * Standardize position to object format * @param {Array|Object} position - Position as array [x,y,z] or object {x,y,z} * @returns {Object|null} Standardized position as {x,y,z} */ function standardizePosition(position) { if (!position) { return null; } // If position is already an object with x,y,z properties if (typeof position === 'object' && 'x' in position && 'y' in position) { return { x: position.x, y: position.y, z: position.z || 0 }; } // If position is an array [x,y,z] if (Array.isArray(position)) { if (position.length < 2) { return null; } return { x: position[0], y: position[1], z: position[2] !== undefined ? position[2] : 0 }; } return null; } /** * Create a vector from two points * @param {Array|Object} point1 - First point [x,y,z] or {x,y,z} * @param {Array|Object} point2 - Second point [x,y,z] or {x,y,z} * @returns {Object} Vector with x,y,z components */ function createVector(point1, point2) { if (!point1 || !point2) { console.log('Missing points for vector creation'); return { x: 0, y: 0, z: 0 }; } let p1 = standardizePointFormat(point1); let p2 = standardizePointFormat(point2); if (!p1 || !p2) { console.log('Could not standardize point format'); return { x: 0, y: 0, z: 0 }; } return { x: p2.x - p1.x, y: p2.y - p1.y, z: p2.z - p1.z }; } /** * Standardize a point to {x,y,z} format regardless of input format * @param {Array|Object} point - Point as array [x,y,z] or object {x,y,z} * @returns {Object} Standardized point as {x,y,z} */ function standardizePointFormat(point) { if (!point) return null; // If already in {x,y,z} format if (typeof point === 'object' && 'x' in point) { return { x: point.x || 0, y: point.y || 0, z: point.z || 0 }; } // If in [x,y,z] format if (Array.isArray(point)) { if (point.length < 2) return null; return { x: point[0] || 0, y: point[1] || 0, z: point.length > 2 ? point[2] || 0 : 0 }; } return null; } /** * Calculate joint velocity between two landmark positions * @param {Array|Object} currentPos - Current joint position * @param {Array|Object} prevPos - Previous joint position * @param {number} timeDelta - Time between frames in milliseconds * @returns {number} Joint velocity in units per second */ function calculateJointVelocity(currentPos, prevPos, timeDelta = 33.33) { // Convert from milliseconds to seconds if needed const timeSeconds = timeDelta / 1000; // Use the existing velocity calculation function return calculateVelocity(currentPos, prevPos, timeSeconds); } // Export all utility functions module.exports = { calculateAngleBetweenVectors, getSafeLandmark, calculateDistance, calculateVelocity, calculateJointVelocity, standardizePosition, calculateAcceleration, createVector, standardizePointFormat };