UNPKG

bowling-analysis-system

Version:

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

378 lines (319 loc) 12.3 kB
/** * Bias Calculator Processor * * Calculates biases from keypoint data * @module processors/BiasCalculatorProcessor */ const { BaseProcessor } = require('../core/BaseProcessor'); const metricsConfig = require('../config/metricsConfig'); /** * @class BiasCalculatorProcessor * @description Processor for calculating biases from keypoint data * @extends BaseProcessor */ class BiasCalculatorProcessor extends BaseProcessor { /** * Create a new bias calculator processor * @param {Object} config - Processor configuration */ constructor(config = {}) { if (!config.logger) { throw new Error('Logger is required for BiasCalculatorProcessor'); } super('biasCalculator', config); this.biasCalculators = new Map(); // Configuration for bias calculation this.config = { ...config, useDynamicBiasCalculation: config.useDynamicBiasCalculation !== false, calibrationFrameCount: config.calibrationFrameCount || 30 }; // Store calibration data this.calibrationData = null; this.logger = config.logger; } /** * Register a bias calculator * @param {string} name - Calculator name * @param {Function} calculator - Calculator function * @returns {BiasCalculatorProcessor} Processor instance for chaining */ registerCalculator(name, calculator) { if (typeof calculator !== 'function') { throw new Error(`Calculator for ${name} must be a function`); } this.biasCalculators.set(name, calculator); return this; } /** * Set calibration data for dynamic bias calculation * @param {Object} calibrationData - Calibration data containing reference measurements * @returns {BiasCalculatorProcessor} Processor instance for chaining */ setCalibrationData(calibrationData) { if (!calibrationData || typeof calibrationData !== 'object') { throw new Error('Invalid calibration data'); } this.calibrationData = calibrationData; return this; } /** * Dynamically calculate bias values based on calibration data * @param {Object} inputData - Input data to process * @returns {Object} Calculated bias values * @private */ _calculateDynamicBiases(inputData) { if (!this.calibrationData) { throw new Error('Calibration data is required for dynamic bias calculation'); } if (!inputData) { throw new Error('Input data is required for bias calculation'); } const dynamicBiases = { angle: {}, position: {}, velocity: {} }; // Calculate angle biases if (!this.calibrationData.angles || !inputData.angles) { throw new Error('Angle data missing from calibration or input data'); } // For each joint angle in calibration data for (const [joint, calibrationValue] of Object.entries(this.calibrationData.angles)) { // Get corresponding value from input data const inputValue = inputData.angles[joint]; if (inputValue !== undefined && calibrationValue !== undefined) { // Calculate bias as the difference between calibration and input, // scaled by a biomechanically-derived factor const difference = Math.abs(calibrationValue - inputValue); // Apply a scaling factor based on joint-specific reliability const jointReliability = 0.8; const scaledBias = difference * jointReliability; // Bias values have biomechanical limits dynamicBiases.angle[joint] = Math.max(0.01, Math.min(0.10, scaledBias)); } else { throw new Error(`Missing angle data for joint: ${joint}`); } } // Calculate position biases if (!this.calibrationData.positions || !inputData.positions) { throw new Error('Position data missing from calibration or input data'); } for (const axis of ['x', 'y', 'z']) { if (this.calibrationData.positions[axis] !== undefined && inputData.positions[axis] !== undefined) { // Calculate position bias with axis-specific scaling const difference = Math.abs(this.calibrationData.positions[axis] - inputData.positions[axis]); const axisReliability = 0.8; const scaledBias = difference * axisReliability; dynamicBiases.position[axis] = Math.max(0.01, Math.min(0.10, scaledBias)); } else { throw new Error(`Missing position data for axis: ${axis}`); } } // Calculate velocity biases if (!this.calibrationData.velocities || !inputData.velocities) { throw new Error('Velocity data missing from calibration or input data'); } for (const [phase, calibrationValue] of Object.entries(this.calibrationData.velocities)) { const inputValue = inputData.velocities[phase]; if (inputValue !== undefined && calibrationValue !== undefined) { // Calculate velocity bias with phase-specific scaling const difference = Math.abs(calibrationValue - inputValue); const phaseReliability = 0.8; const scaledBias = difference * phaseReliability; dynamicBiases.velocity[phase] = Math.max(0.01, Math.min(0.10, scaledBias)); } else { throw new Error(`Missing velocity data for phase: ${phase}`); } } return dynamicBiases; } /** * Process input data * @param {Object} input - Input data * @param {Object} context - Processing context * @returns {Promise<Object>} Processed data with calculated biases * @private */ async _process(input, context) { if (!input) { throw new Error('No input data provided'); } // Determine whether to use dynamic or default biases let biases; if (this.config.useDynamicBiasCalculation && input.keypoints) { // Extract the data needed for bias calculation from input const biasInputData = this._extractBiasInputData(input.keypoints); // Calculate dynamic biases biases = this._calculateDynamicBiases(biasInputData); this.logger.info('Calculated dynamic bias values'); } else if (this.biasCalculators.size === 0) { throw new Error('No bias calculators registered and no dynamic calculation enabled'); } // Process biases with registered calculators const processedBiases = {}; // Process each bias type with its registered calculator for (const [type, calculator] of this.biasCalculators.entries()) { try { processedBiases[type] = await calculator(input, biases ? biases[type] : undefined); if (!processedBiases[type] || Object.keys(processedBiases[type]).length === 0) { throw new Error(`Calculator for ${type} produced empty bias data`); } } catch (error) { this.logger.error(`Error in bias calculator for ${type}: ${error.message}`); throw error; } } // Verify all required bias types are processed const requiredBiasTypes = ['angle', 'position', 'velocity']; for (const type of requiredBiasTypes) { if (!processedBiases[type]) { throw new Error(`Missing required bias type: ${type}`); } } return { ...input, biases: processedBiases }; } /** * Extract data needed for bias calculation from input keypoints * @param {Array} keypoints - Input keypoints data * @returns {Object} Extracted data for bias calculation * @private */ _extractBiasInputData(keypoints) { if (!keypoints || !Array.isArray(keypoints) || keypoints.length === 0) { throw new Error('Invalid keypoints data'); } // Use a subset of frames for calibration const framesToUse = Math.min(keypoints.length, this.config.calibrationFrameCount); if (framesToUse < 10) { throw new Error('Insufficient frames for bias calibration'); } const calibrationFrames = keypoints.slice(0, framesToUse); // Extract angle data const angles = this._extractJointAngles(calibrationFrames); // Extract position data const positions = this._extractPositionData(calibrationFrames); // Extract velocity data const velocities = this._extractVelocityData(calibrationFrames); return { angles, positions, velocities }; } /** * Extract joint angle data from frames * @param {Array} frames - Calibration frames * @returns {Object} Joint angle averages * @private */ _extractJointAngles(frames) { // Implementation would extract joint angles from keypoints // This is a simplified placeholder const jointAngles = {}; // Calculate averages of joint angles across frames for (const frame of frames) { // Process each joint in the frame for (const joint of ['elbow', 'shoulder', 'knee', 'hip', 'ankle', 'wrist']) { if (frame.joints && frame.joints[joint] && frame.joints[joint].angle !== undefined) { if (!jointAngles[joint]) { jointAngles[joint] = []; } jointAngles[joint].push(frame.joints[joint].angle); } } } // Convert arrays to averages const result = {}; for (const [joint, angles] of Object.entries(jointAngles)) { if (angles.length > 0) { result[joint] = angles.reduce((sum, angle) => sum + angle, 0) / angles.length; } } return result; } /** * Extract position data from frames * @param {Array} frames - Calibration frames * @returns {Object} Position averages by axis * @private */ _extractPositionData(frames) { // Implementation would extract position data from keypoints // This is a simplified placeholder const positions = { x: [], y: [], z: [] }; // Calculate average positions across frames for (const frame of frames) { if (frame.centerOfMass) { if (frame.centerOfMass.x !== undefined) positions.x.push(frame.centerOfMass.x); if (frame.centerOfMass.y !== undefined) positions.y.push(frame.centerOfMass.y); if (frame.centerOfMass.z !== undefined) positions.z.push(frame.centerOfMass.z); } } // Convert arrays to averages const result = {}; for (const [axis, values] of Object.entries(positions)) { if (values.length > 0) { result[axis] = values.reduce((sum, value) => sum + value, 0) / values.length; } } return result; } /** * Extract velocity data from frames * @param {Array} frames - Calibration frames * @returns {Object} Velocity averages by phase * @private */ _extractVelocityData(frames) { // Implementation would extract velocity data from keypoints // This is a simplified placeholder const velocities = { approach: [], release: [], followThrough: [] }; // Calculate phase-specific velocities // Assuming frames are in sequential order const frameCount = frames.length; const approachFrames = frames.slice(0, Math.floor(frameCount * 0.3)); const releaseFrames = frames.slice(Math.floor(frameCount * 0.3), Math.floor(frameCount * 0.6)); const followThroughFrames = frames.slice(Math.floor(frameCount * 0.6)); // Process approach phase for (const frame of approachFrames) { if (frame.velocity && frame.velocity.magnitude !== undefined) { velocities.approach.push(frame.velocity.magnitude); } } // Process release phase for (const frame of releaseFrames) { if (frame.velocity && frame.velocity.magnitude !== undefined) { velocities.release.push(frame.velocity.magnitude); } } // Process follow-through phase for (const frame of followThroughFrames) { if (frame.velocity && frame.velocity.magnitude !== undefined) { velocities.followThrough.push(frame.velocity.magnitude); } } // Convert arrays to averages const result = {}; for (const [phase, values] of Object.entries(velocities)) { if (values.length > 0) { result[phase] = values.reduce((sum, value) => sum + value, 0) / values.length; } } return result; } } module.exports = BiasCalculatorProcessor;