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