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