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