UNPKG

bowling-analysis-system

Version:

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

385 lines (326 loc) 13.3 kB
/** * @module utils/DataValidator * @description Utilities for validating input data before processing */ /** * Validate keypoint data structure * @param {Object} data - The keypoint data to validate * @returns {Object} Validation result with isValid flag and error messages */ function validateKeypointData(data) { const result = { isValid: true, errors: [], warnings: [], validFrames: 0, totalFrames: 0 }; // Check if data exists if (!data) { result.isValid = false; result.errors.push('No data provided'); return result; } // Check if frames property exists if (!data.frames || !Array.isArray(data.frames)) { result.isValid = false; result.errors.push('Missing frames array'); return result; } result.totalFrames = data.frames.length; // Check if frames array is empty if (data.frames.length === 0) { result.isValid = false; result.errors.push('Frames array is empty'); return result; } // Check each frame for validity let validFrameCount = 0; data.frames.forEach((frame, index) => { if (frame && frame.pose_landmarks && Array.isArray(frame.pose_landmarks) && frame.pose_landmarks.length > 0) { validFrameCount++; } else if (!frame) { result.warnings.push(`Frame ${index} is null or undefined`); } else if (!frame.pose_landmarks) { result.warnings.push(`Frame ${index} has no pose_landmarks property`); } else if (frame.pose_landmarks.length === 0) { result.warnings.push(`Frame ${index} has empty pose_landmarks array`); } }); result.validFrames = validFrameCount; // Check if there are enough valid frames if (validFrameCount === 0) { result.isValid = false; result.errors.push('No valid frames found'); } else if (validFrameCount < 10) { // Require at least 10 valid frames for meaningful metrics result.warnings.push(`Only ${validFrameCount} valid frames found, metrics may be unreliable`); } // Check if data has expected properties if (!data.id && !data.video_id) { result.warnings.push('Missing id and video_id properties'); } return result; } /** * Generate valid frames array from keypoint data * @param {Object} data - The keypoint data * @returns {Array} Array of valid frames */ function generateValidFrames(data) { if (!data || !data.frames || !Array.isArray(data.frames)) { return []; } return data.frames.filter(frame => frame && frame.pose_landmarks && Array.isArray(frame.pose_landmarks) && frame.pose_landmarks.length > 0 ); } /** * Standardize pose landmarks from array format to object format * @param {Array} landmarks - Pose landmarks in array format * @returns {Array} Standardized pose landmarks in object format with x, y, z properties */ function standardizePoseLandmarks(landmarks) { if (!landmarks) { console.log('standardizePoseLandmarks: landmarks is null or undefined'); return []; } if (!Array.isArray(landmarks)) { // If landmarks is an object, try to convert it to an array of objects format if (typeof landmarks === 'object' && landmarks !== null) { const standardizedLandmarks = []; // Try different approaches to extract landmarks from the object // This is a generic approach trying to handle different potential object structures // Approach 1: Object may have numeric properties (0, 1, 2...) for each landmark const numericKeys = Object.keys(landmarks).filter(key => !isNaN(parseInt(key))); if (numericKeys.length > 0) { for (const key of numericKeys) { const landmark = landmarks[key]; if (landmark && typeof landmark === 'object') { // Extract x, y, z from the landmark object standardizedLandmarks.push({ x: landmark.x !== undefined ? landmark.x : 0, y: landmark.y !== undefined ? landmark.y : 0, z: landmark.z !== undefined ? landmark.z : 0, visibility: landmark.visibility !== undefined ? landmark.visibility : 1.0 }); } } console.log(`standardizePoseLandmarks: created ${standardizedLandmarks.length} landmarks from object with numeric keys`); return standardizedLandmarks; } // Approach 2: Object may have named properties for each landmark // Try common landmark names const commonNames = [ 'nose', 'leftEye', 'rightEye', 'leftEar', 'rightEar', 'leftShoulder', 'rightShoulder', 'leftElbow', 'rightElbow', 'leftWrist', 'rightWrist', 'leftHip', 'rightHip', 'leftKnee', 'rightKnee', 'leftAnkle', 'rightAnkle' ]; let namedKeysFound = 0; for (const name of commonNames) { if (landmarks[name] && typeof landmarks[name] === 'object') { const landmark = landmarks[name]; standardizedLandmarks.push({ name: name, x: landmark.x !== undefined ? landmark.x : 0, y: landmark.y !== undefined ? landmark.y : 0, z: landmark.z !== undefined ? landmark.z : 0, visibility: landmark.visibility !== undefined ? landmark.visibility : 1.0 }); namedKeysFound++; } } if (namedKeysFound > 0) { console.log(`standardizePoseLandmarks: created ${standardizedLandmarks.length} landmarks from object with named keys`); return standardizedLandmarks; } // If the object has x, y, z properties directly, it might be a single landmark if ('x' in landmarks && 'y' in landmarks) { console.log('standardizePoseLandmarks: found a single landmark object'); return [{ x: landmarks.x !== undefined ? landmarks.x : 0, y: landmarks.y !== undefined ? landmarks.y : 0, z: landmarks.z !== undefined ? landmarks.z : 0, visibility: landmarks.visibility !== undefined ? landmarks.visibility : 1.0 }]; } } // If all approaches fail, return empty array return []; } console.log(`standardizePoseLandmarks: landmarks length = ${landmarks.length}`); // Check if landmarks are already in object format if (landmarks.length > 0 && typeof landmarks[0] === 'object' && 'x' in landmarks[0]) { console.log('standardizePoseLandmarks: landmarks already in object format'); return landmarks; } // Special case: If landmarks is an array with a single element that is an array of landmarks if (landmarks.length === 1 && Array.isArray(landmarks[0])) { console.log(`standardizePoseLandmarks: landmarks[0] is an array with length ${landmarks[0].length}`); // This is the format from keypoint.json: [[landmark1], [landmark2], ...] const landmarksArray = landmarks[0]; // Check if each element is an array (which should be the case) if (landmarksArray.length > 0 && Array.isArray(landmarksArray[0])) { console.log(`standardizePoseLandmarks: found ${landmarksArray.length} landmarks in array format`); // Convert each landmark from array [x, y, z] to object {x, y, z} const standardizedLandmarks = landmarksArray.map(landmark => { if (Array.isArray(landmark) && landmark.length >= 3) { return { x: landmark[0], y: landmark[1], z: landmark[2] }; } else { return null; } }).filter(landmark => landmark !== null); console.log(`standardizePoseLandmarks: created ${standardizedLandmarks.length} standardized landmarks`); return standardizedLandmarks; } // Fallback to the old flattened array logic const flattenedArray = landmarks[0]; // Check if it's divisible by 3 (x, y, z coordinates) if (flattenedArray.length % 3 === 0) { const landmarkCount = flattenedArray.length / 3; console.log(`standardizePoseLandmarks: flattened array represents ${landmarkCount} landmarks`); const standardizedLandmarks = []; // Convert to object format for (let i = 0; i < landmarkCount; i++) { const offset = i * 3; standardizedLandmarks.push({ x: flattenedArray[offset], y: flattenedArray[offset + 1], z: flattenedArray[offset + 2] }); } console.log(`standardizePoseLandmarks: created ${standardizedLandmarks.length} standardized landmarks`); return standardizedLandmarks; } else { console.log(`standardizePoseLandmarks: flattened array length ${flattenedArray.length} is not divisible by 3`); } } // Check if landmarks are in array format (array of arrays) if (landmarks.length > 0 && Array.isArray(landmarks[0])) { console.log('standardizePoseLandmarks: landmarks are in array format (array of arrays)'); // Convert from array format to object format const standardizedLandmarks = landmarks.map(landmark => { if (Array.isArray(landmark) && landmark.length >= 3) { return { x: landmark[0], y: landmark[1], z: landmark[2] }; } else { // If landmark is not in expected format, return null return null; } }).filter(landmark => landmark !== null); console.log(`standardizePoseLandmarks: created ${standardizedLandmarks.length} standardized landmarks from array format`); return standardizedLandmarks; } console.log(`standardizePoseLandmarks: landmarks are in an unexpected format: ${typeof landmarks[0]}`); // If landmarks are in an unexpected format, return empty array return []; } /** * Validate events data structure * @param {Object} events - Events data to validate * @returns {Object} Validation result */ function validateEventsData(events) { const result = { isValid: true, errors: [], warnings: [], eventCount: 0, multiFrameEvents: {}, dynamicEvents: [] }; if (!events) { result.isValid = false; result.errors.push('No events data provided'); return result; } // Handle both direct events format and nested events format const eventsObj = events.events || events; if (typeof eventsObj !== 'object' || eventsObj === null) { result.isValid = false; result.errors.push('Missing or invalid events object in events data'); return result; } // Core events that should always be present const requiredEvents = [ 'frontFootLanding', 'backFootLanding', 'releasePoint' ]; // Remove alternative event names - strictly validate against required events only const foundEvents = { frontFootLanding: false, backFootLanding: false, releasePoint: false }; // Track the number of multi-frame events let multiFrameCount = 0; // Check all events for (const eventName in eventsObj) { const event = eventsObj[eventName]; // Check if this is a core event if (requiredEvents.includes(eventName)) { foundEvents[eventName] = true; result.eventCount++; // Validate event structure if (!event || typeof event !== 'object') { result.warnings.push(`Event ${eventName} has invalid structure`); continue; } // Check for frame property if (event.frame === undefined && event.frameIndex === undefined) { result.warnings.push(`Event ${eventName} is missing frame/frameIndex property`); } // Check for confidence property if (event.confidence === undefined) { result.warnings.push(`Event ${eventName} is missing confidence property`); } // Check if this is a multi-frame event if (Array.isArray(event.frames) || Array.isArray(event.frameIndices)) { multiFrameCount++; result.multiFrameEvents[eventName] = Array.isArray(event.frames) ? event.frames.length : event.frameIndices.length; } } else { // This is a dynamic event, not a core event result.dynamicEvents.push(eventName); } } // Check if all required events were found const missingEvents = requiredEvents.filter(event => !foundEvents[event]); if (missingEvents.length > 0) { // Add warning but don't invalidate - we may have alternative names result.warnings.push(`Missing core events: ${missingEvents.join(', ')}`); // Check if we can find alternative names for missing events for (const missingEvent of missingEvents) { const alternatives = alternativeEventNames[missingEvent] || []; const foundAlternative = alternatives.some(alt => eventsObj[alt] !== undefined); if (!foundAlternative) { // Only mark as invalid if we can't find any alternative names result.errors.push(`Missing required event: ${missingEvent} and no alternative found`); result.isValid = false; } } } // Add information about multi-frame events if (multiFrameCount > 0) { result.warnings.push(`Found ${multiFrameCount} multi-frame events: ${JSON.stringify(result.multiFrameEvents)}`); } return result; } module.exports = { validateKeypointData, generateValidFrames, validateEventsData, standardizePoseLandmarks };