UNPKG

bowling-analysis-system

Version:

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

404 lines (343 loc) 15 kB
/** * @module MetricBiasAnalyzer * @description Applies biases to metrics for correction */ const fs = require('fs'); const path = require('path'); // Use the modern BiasCalculator implementation const BiasCalculator = require('../processors/BiasCalculator'); /** * @class MetricBiasAnalyzer * @description Applies calculated biases to metrics for correction and adjustment */ class MetricBiasAnalyzer { /** * Apply biases to metrics * @param {Object} originalMetrics - The original metrics to adjust * @param {Object} biases - The calculated biases to apply * @returns {Object} The adjusted metrics * @static */ static applyBiases(originalMetrics, biases) { if (!originalMetrics || !biases) { console.warn('Missing data for bias application'); return null; } console.log('Applying biases to metrics'); // Create a deep copy of the original metrics const adjustedMetrics = JSON.parse(JSON.stringify(originalMetrics)); // Apply event biases this._applyEventBiases(adjustedMetrics, biases.events); // Apply value range adjustments this._applyValueRangeAdjustments(adjustedMetrics, biases.valueRanges); // Apply time series adjustments this._applyTimeSeriesAdjustments(adjustedMetrics, biases.timeSeries); // Add bias metadata adjustedMetrics.metadata = { ...adjustedMetrics.metadata, biasApplied: true, biasApplicationTimestamp: new Date().toISOString(), originalSource: originalMetrics.metadata }; console.log('Bias application complete'); return adjustedMetrics; } /** * Apply event biases to metrics * @param {Object} metrics - The metrics to adjust * @param {Object} eventBiases - The event biases to apply * @private * @static */ static _applyEventBiases(metrics, eventBiases) { if (!metrics.events || !eventBiases) { return; } console.log('Applying event biases'); for (const eventName in eventBiases) { if (metrics.events[eventName]) { const bias = eventBiases[eventName]; const originalFrame = metrics.events[eventName].frameIndex; const adjustedFrame = originalFrame + bias.frameIndexDiff; metrics.events[eventName].frameIndex = adjustedFrame; metrics.events[eventName].originalFrameIndex = originalFrame; console.log(`Adjusted event ${eventName}: ${originalFrame} -> ${adjustedFrame} (diff: ${bias.frameIndexDiff})`); } } } /** * Apply value range adjustments to metrics * @param {Object} metrics - The metrics to adjust * @param {Object} rangeAdjustments - The value range adjustments to apply * @private * @static */ static _applyValueRangeAdjustments(metrics, rangeAdjustments) { if (!rangeAdjustments) { return; } console.log('Applying value range adjustments'); // Apply to angles if (rangeAdjustments.angles && metrics.angles) { for (const angleName in rangeAdjustments.angles) { if (metrics.angles[angleName]) { const adjustment = rangeAdjustments.angles[angleName]; const originalValue = metrics.angles[angleName].value; // Apply scaling or direct shift with no random noise let adjustedValue; if (adjustment.scaleFactor && !isNaN(adjustment.scaleFactor) && isFinite(adjustment.scaleFactor)) { adjustedValue = originalValue * adjustment.scaleFactor; } else { adjustedValue = originalValue + adjustment.valueDiff; } // Store original value metrics.angles[angleName].originalValue = originalValue; metrics.angles[angleName].value = adjustedValue; console.log(`Adjusted angle ${angleName}: ${originalValue.toFixed(2)} -> ${adjustedValue.toFixed(2)}`); // Adjust series if present if (metrics.angles[angleName].series && adjustment.scaleFactor) { this._applySeriesValueAdjustment(metrics.angles[angleName].series, adjustment.scaleFactor); } } } } // Apply to velocity if (rangeAdjustments.velocity && metrics.velocity) { for (const velocityName in rangeAdjustments.velocity) { if (metrics.velocity[velocityName]) { const adjustment = rangeAdjustments.velocity[velocityName]; const originalValue = metrics.velocity[velocityName].value; // Apply scaling or direct shift with no random noise let adjustedValue; if (adjustment.scaleFactor && !isNaN(adjustment.scaleFactor) && isFinite(adjustment.scaleFactor)) { adjustedValue = originalValue * adjustment.scaleFactor; } else { adjustedValue = originalValue + adjustment.valueDiff; } // Store original value metrics.velocity[velocityName].originalValue = originalValue; metrics.velocity[velocityName].value = adjustedValue; console.log(`Adjusted velocity ${velocityName}: ${originalValue.toFixed(2)} -> ${adjustedValue.toFixed(2)}`); // Adjust series if present if (metrics.velocity[velocityName].series && adjustment.scaleFactor) { this._applySeriesValueAdjustment(metrics.velocity[velocityName].series, adjustment.scaleFactor); } } } } // Apply to bowling technique if (rangeAdjustments.bowlingTechnique && metrics.bowlingTechnique) { for (const techniqueName in rangeAdjustments.bowlingTechnique) { if (metrics.bowlingTechnique[techniqueName]) { const adjustment = rangeAdjustments.bowlingTechnique[techniqueName]; const originalValue = metrics.bowlingTechnique[techniqueName].value; // Apply scaling or direct shift with no random noise let adjustedValue; if (adjustment.scaleFactor && !isNaN(adjustment.scaleFactor) && isFinite(adjustment.scaleFactor)) { adjustedValue = originalValue * adjustment.scaleFactor; } else { adjustedValue = originalValue + adjustment.valueDiff; } // Store original value metrics.bowlingTechnique[techniqueName].originalValue = originalValue; metrics.bowlingTechnique[techniqueName].value = adjustedValue; console.log(`Adjusted technique ${techniqueName}: ${originalValue.toFixed(2)} -> ${adjustedValue.toFixed(2)}`); } } } } /** * Apply time series adjustments to metrics * @param {Object} metrics - The metrics to adjust * @param {Object} seriesAdjustments - The time series adjustments to apply * @private * @static */ static _applyTimeSeriesAdjustments(metrics, seriesAdjustments) { if (!seriesAdjustments) { console.log('No time series adjustments provided, using defaults'); // If biases are missing, apply a small deterministic scaling factor // instead of random noise if (metrics.timeSeries && Array.isArray(metrics.timeSeries)) { // Apply a small consistent scaling factor (1.02) instead of random noise const scaleFactor = 1.02; // 2% deterministic adjustment metrics.timeSeries.forEach(point => { if (point && typeof point.value === 'number') { // Apply deterministic scaling point.value *= scaleFactor; } }); console.log(`Applied default scaling factor of ${scaleFactor} to time series data`); } return; } console.log('Applying time series adjustments'); // Apply to angles if (seriesAdjustments.angles && metrics.angles) { for (const angleName in seriesAdjustments.angles) { // Check for pluralized metric names (combined metrics) const isPluralized = angleName.endsWith('s') && !angleName.endsWith('ss'); const singularName = isPluralized ? angleName.slice(0, -1) : null; const leftName = singularName ? `left${singularName.charAt(0).toUpperCase() + singularName.slice(1)}` : null; const rightName = singularName ? `right${singularName.charAt(0).toUpperCase() + singularName.slice(1)}` : null; // Handle combined metrics if (isPluralized && metrics.angles[angleName]) { const adjustment = seriesAdjustments.angles[angleName]; // Apply adjustments to the combined metric series if (metrics.angles[angleName].series) { // Apply time shift if (adjustment.timeShift) { this._applyTimeShift(metrics.angles[angleName].series, adjustment.timeShift); console.log(`Applied time shift to combined ${angleName}: ${adjustment.timeShift} frames`); } // Apply amplitude scaling if (adjustment.amplitudeScale) { this._applySeriesValueAdjustment(metrics.angles[angleName].series, adjustment.amplitudeScale); console.log(`Applied amplitude scaling to combined ${angleName}: ${adjustment.amplitudeScale.toFixed(2)}`); } } } // Handle regular metrics else if (metrics.angles[angleName] && metrics.angles[angleName].series) { const adjustment = seriesAdjustments.angles[angleName]; // Apply time shift if (adjustment.timeShift) { this._applyTimeShift(metrics.angles[angleName].series, adjustment.timeShift); console.log(`Applied time shift to ${angleName}: ${adjustment.timeShift} frames`); } // Apply amplitude scaling if (adjustment.amplitudeScale) { this._applySeriesValueAdjustment(metrics.angles[angleName].series, adjustment.amplitudeScale); console.log(`Applied amplitude scaling to ${angleName}: ${adjustment.amplitudeScale.toFixed(2)}`); } } } } // Apply to velocity if (seriesAdjustments.velocity && metrics.velocity) { for (const velocityName in seriesAdjustments.velocity) { // Check for pluralized metric names (combined metrics) const isPluralized = velocityName.endsWith('s') && !velocityName.endsWith('ss'); const singularName = isPluralized ? velocityName.slice(0, -1) : null; const leftName = singularName ? `left${singularName.charAt(0).toUpperCase() + singularName.slice(1)}` : null; const rightName = singularName ? `right${singularName.charAt(0).toUpperCase() + singularName.slice(1)}` : null; // Handle combined metrics if (isPluralized && metrics.velocity[velocityName]) { const adjustment = seriesAdjustments.velocity[velocityName]; // Apply adjustments to the combined metric series if (metrics.velocity[velocityName].series) { // Apply time shift if (adjustment.timeShift) { this._applyTimeShift(metrics.velocity[velocityName].series, adjustment.timeShift); console.log(`Applied time shift to combined ${velocityName}: ${adjustment.timeShift} frames`); } // Apply amplitude scaling if (adjustment.amplitudeScale) { this._applySeriesValueAdjustment(metrics.velocity[velocityName].series, adjustment.amplitudeScale); console.log(`Applied amplitude scaling to combined ${velocityName}: ${adjustment.amplitudeScale.toFixed(2)}`); } } } // Handle regular metrics else if (metrics.velocity[velocityName] && metrics.velocity[velocityName].series) { const adjustment = seriesAdjustments.velocity[velocityName]; // Apply time shift if (adjustment.timeShift) { this._applyTimeShift(metrics.velocity[velocityName].series, adjustment.timeShift); console.log(`Applied time shift to ${velocityName}: ${adjustment.timeShift} frames`); } // Apply amplitude scaling if (adjustment.amplitudeScale) { this._applySeriesValueAdjustment(metrics.velocity[velocityName].series, adjustment.amplitudeScale); console.log(`Applied amplitude scaling to ${velocityName}: ${adjustment.amplitudeScale.toFixed(2)}`); } } } } } /** * Apply a time shift to a time series * @param {Array} series - The time series to adjust * @param {number} shift - The time shift to apply * @private * @static */ static _applyTimeShift(series, shift) { if (!series || !Array.isArray(series) || shift === 0) { return; } const result = new Array(series.length).fill(null); for (let i = 0; i < series.length; i++) { const newIndex = i + shift; if (newIndex >= 0 && newIndex < series.length) { result[newIndex] = series[i]; } } // Fill in any nulls with closest values for (let i = 0; i < result.length; i++) { if (result[i] === null) { // Find closest non-null value let left = i - 1; let right = i + 1; while (left >= 0 && result[left] === null) { left--; } while (right < result.length && result[right] === null) { right++; } if (left >= 0 && right < result.length) { // Interpolate const leftVal = result[left]; const rightVal = result[right]; const ratio = (i - left) / (right - left); result[i] = leftVal + (rightVal - leftVal) * ratio; } else if (left >= 0) { result[i] = result[left]; } else if (right < result.length) { result[i] = result[right]; } } } // Copy result back to original series for (let i = 0; i < series.length; i++) { series[i] = result[i]; } } /** * Apply value adjustment to a time series * @param {Array} series - The time series to adjust * @param {number} scaleFactor - The scale factor to apply * @private * @static */ static _applySeriesValueAdjustment(series, scaleFactor) { if (!series || !Array.isArray(series) || !scaleFactor || isNaN(scaleFactor)) { return; } for (let i = 0; i < series.length; i++) { series[i] = series[i] * scaleFactor; } } /** * Save adjusted metrics to a file * @param {Object} adjustedMetrics - The adjusted metrics * @param {string} outputPath - Path to save the adjusted metrics * @returns {boolean} Success status * @static */ static saveAdjustedMetrics(adjustedMetrics, outputPath) { try { const dirPath = path.dirname(outputPath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } fs.writeFileSync(outputPath, JSON.stringify(adjustedMetrics, null, 2)); console.log(`Adjusted metrics saved to ${outputPath}`); return true; } catch (error) { console.error(`Error saving adjusted metrics: ${error.message}`); return false; } } } module.exports = MetricBiasAnalyzer;