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