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