UNPKG

bowling-analysis-system

Version:

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

215 lines (183 loc) 7.75 kB
/** * @module utils/validation * @description Validation utilities for the bowling analysis system */ /** * Validates keypoint data to ensure it meets requirements for metrics calculation * @param {Object|Array} keypointData - The keypoint data to validate, either an array of frames or object with frames property * @throws {Error} If validation fails */ function validateKeypointData(keypointData) { // Check if keypointData is null or undefined if (!keypointData) { throw new Error('Keypoint data is null or undefined'); } // Handle both array of frames or object with frames property const frames = Array.isArray(keypointData) ? keypointData : (keypointData.frames || null); // Check if frames is an array if (!frames || !Array.isArray(frames)) { throw new Error('Invalid keypoint data: frames must be an array'); } // Check if we have enough frames if (frames.length < 30) { throw new Error(`Insufficient frames: ${frames.length} (minimum 30 required)`); } // Get valid frames (those with non-empty landmarks) const validFrames = frames.filter(frame => { if (!frame) return false; // Handle different pose_landmarks formats if (frame.pose_landmarks) { // Handle format: frame.pose_landmarks is an array if (Array.isArray(frame.pose_landmarks)) { // Format: frame.pose_landmarks is [[landmark1, landmark2, ...]] if (frame.pose_landmarks.length > 0 && Array.isArray(frame.pose_landmarks[0])) { return frame.pose_landmarks[0].length > 0; } // Format: frame.pose_landmarks is [landmark1, landmark2, ...] return frame.pose_landmarks.length > 0; } return false; } // No pose_landmarks, but maybe the frame itself is the landmarks array return Array.isArray(frame) && frame.length > 0; }); // Check if we have enough valid frames if (validFrames.length < 30) { throw new Error(`Insufficient valid frames: ${validFrames.length} (minimum 30 required)`); } // If we got here, basic validation passed return true; } /** * Validates bias data to ensure it meets requirements * @param {Object} biasData - The bias data to validate * @throws {Error} If validation fails */ function validateBiasData(biasData) { // Skip validation if bias data is null or undefined if (!biasData) { return; } // Check if bias data is an object if (typeof biasData !== 'object') { throw new Error('Bias data must be an object'); } // Basic structure check - be lenient about required fields // As long as it has patterns, version, or some other recognizable field, consider it valid if (!biasData.patterns && !biasData.version && !biasData.correlations && !biasData.metrics) { console.warn('Bias data is missing common fields like patterns, version, correlations, or metrics'); } // If we have patterns, validate those if (biasData.patterns) { if (!Array.isArray(biasData.patterns)) { throw new Error('Bias patterns must be an array'); } // Check each pattern has the required fields for (const pattern of biasData.patterns) { if (!pattern.metric || !pattern.correlation) { console.warn('Some bias patterns are missing metric or correlation fields'); } } } // Success if we got here return true; } /** * Validate reference data structure * @param {Object} referenceData - Reference data to validate * @returns {boolean} True if valid * @throws {Error} If validation fails with specific reason */ function validateReferenceData(referenceData) { if (!referenceData) { throw new Error('No reference data provided'); } // Check for events object if (!referenceData.events || typeof referenceData.events !== 'object') { throw new Error('Missing or invalid events object in reference data'); } // Check for required events const requiredEvents = ['frontFootLanding', 'backFootLanding', 'releasePoint']; const missingEvents = requiredEvents.filter(event => !referenceData.events[event]); if (missingEvents.length > 0) { throw new Error(`Missing required events in reference data: ${missingEvents.join(', ')}`); } // Validate event frame indices for (const [eventName, eventData] of Object.entries(referenceData.events)) { if (typeof eventData === 'number') { if (!Number.isInteger(eventData) || eventData < 0) { throw new Error(`Invalid frame index for event ${eventName}: ${eventData}`); } } else if (Array.isArray(eventData)) { if (eventData.length === 0) { throw new Error(`Empty frame array for event ${eventName}`); } if (!eventData.every(frame => Number.isInteger(frame) && frame >= 0)) { throw new Error(`Invalid frame indices for event ${eventName}`); } } else if (typeof eventData === 'object') { if (!Number.isInteger(eventData.frameIndex) || eventData.frameIndex < 0) { throw new Error(`Invalid frameIndex for event ${eventName}`); } if (eventData.allFrames) { if (!Array.isArray(eventData.allFrames) || eventData.allFrames.length === 0) { throw new Error(`Invalid allFrames array for event ${eventName}`); } if (!eventData.allFrames.every(frame => Number.isInteger(frame) && frame >= 0)) { throw new Error(`Invalid frame indices in allFrames for event ${eventName}`); } } } else { throw new Error(`Invalid event data format for ${eventName}`); } } // Check for metadata if (!referenceData.metadata || typeof referenceData.metadata !== 'object') { throw new Error('Missing or invalid metadata in reference data'); } return true; } /** * Validates metrics data structure * @param {Object} metrics - The metrics data to validate * @returns {boolean} Whether the metrics are valid * @throws {Error} If validation fails with specific reason */ function validateMetrics(metrics) { if (!metrics || typeof metrics !== 'object') { throw new Error('Invalid metrics data structure'); } // Check for required sections if (!metrics.metadata || typeof metrics.metadata !== 'object') { throw new Error('Missing or invalid metadata section'); } if (!metrics.summary || typeof metrics.summary !== 'object') { throw new Error('Missing or invalid summary section'); } // Check for required metadata fields const requiredMetadata = ['processedAt', 'totalFrames', 'validFrames']; const missingMetadata = requiredMetadata.filter(field => !metrics.metadata[field]); if (missingMetadata.length > 0) { throw new Error(`Missing required metadata fields: ${missingMetadata.join(', ')}`); } // Check for required metric categories const requiredCategories = ['timing', 'angles', 'velocities', 'positions']; const missingCategories = requiredCategories.filter(category => !metrics.summary[category]); if (missingCategories.length > 0) { throw new Error(`Missing required metric categories: ${missingCategories.join(', ')}`); } // Validate each category has metrics for (const category of requiredCategories) { const metrics = metrics.summary[category]; if (!metrics || typeof metrics !== 'object' || Object.keys(metrics).length === 0) { throw new Error(`Empty or invalid metrics in category: ${category}`); } } return true; } module.exports = { validateKeypointData, validateBiasData, validateReferenceData, validateMetrics };