bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
657 lines (546 loc) • 22.2 kB
JavaScript
/**
* @module core/utils/CorrelationUtils
* @description Centralized utilities for correlation calculations and analysis
*/
const { findPeaks, findValleys } = require('./TimeSeriesUtils');
/**
* Calculate Pearson correlation coefficient between two signals
* @param {Array} signal1 - First signal array
* @param {Array} signal2 - Second signal array
* @returns {number} Correlation coefficient (-1 to 1) or 0 if calculation fails
*/
function calculateCorrelationCoefficient(signal1, signal2) {
// Filter out null values
const filteredPairs = [];
for (let i = 0; i < Math.min(signal1.length, signal2.length); i++) {
if (signal1[i] !== null && signal1[i] !== undefined &&
signal2[i] !== null && signal2[i] !== undefined) {
filteredPairs.push([signal1[i], signal2[i]]);
}
}
if (filteredPairs.length < 3) return 0; // Need at least a few points
// Calculate means
const mean1 = filteredPairs.reduce((sum, pair) => sum + pair[0], 0) / filteredPairs.length;
const mean2 = filteredPairs.reduce((sum, pair) => sum + pair[1], 0) / filteredPairs.length;
// Calculate covariance and variances
let covariance = 0;
let variance1 = 0;
let variance2 = 0;
for (const [val1, val2] of filteredPairs) {
const diff1 = val1 - mean1;
const diff2 = val2 - mean2;
covariance += diff1 * diff2;
variance1 += diff1 * diff1;
variance2 += diff2 * diff2;
}
// Avoid division by zero
if (variance1 === 0 || variance2 === 0) return 0;
// Calculate Pearson correlation
const correlation = covariance / (Math.sqrt(variance1) * Math.sqrt(variance2));
return correlation;
}
/**
* Calculate correlation score between a time series and event frames
* @param {Array} timeSeries - Time series data
* @param {Array} eventFrames - Array of event frame indices
* @returns {Object} Correlation information including score and details
*/
function calculateEventCorrelation(timeSeries, eventFrames) {
// Skip if insufficient data
if (!timeSeries || timeSeries.length === 0 || !eventFrames || eventFrames.length === 0) {
return { score: 0, details: { reason: 'Insufficient data' } };
}
// Create an array of zeros with spikes at event locations
const eventSignal = Array(timeSeries.length).fill(0);
eventFrames.forEach(frame => {
if (frame >= 0 && frame < eventSignal.length) {
eventSignal[frame] = 1;
}
});
// Calculate correlation between the time series and event signal
const score = calculateCorrelationCoefficient(timeSeries, eventSignal);
// Get additional details about the correlation
const details = {
peakCorrelation: false,
valleyCorrelation: false,
eventValues: eventFrames.map(frame => {
if (frame >= 0 && frame < timeSeries.length) {
return timeSeries[frame];
}
return null;
}).filter(v => v !== null)
};
// Check if events tend to occur at peaks or valleys
if (details.eventValues.length > 0) {
const nonEventValues = timeSeries.filter((v, i) => !eventFrames.includes(i) && v !== null);
if (nonEventValues.length > 0) {
const avgEventValue = details.eventValues.reduce((a, b) => a + b, 0) / details.eventValues.length;
const avgNonEventValue = nonEventValues.reduce((a, b) => a + b, 0) / nonEventValues.length;
details.peakCorrelation = avgEventValue > avgNonEventValue;
details.valleyCorrelation = avgEventValue < avgNonEventValue;
}
}
return { score, details };
}
/**
* Calculate correlations between metrics and reference moments
* @param {Object} metrics - Metrics data with timeSeries property
* @param {Object} referenceData - Reference data with moments property
* @returns {Object} Calculated correlations
*/
function calculateMetricEventCorrelations(metrics, referenceData) {
// Initialize correlations object
const correlations = {};
// Ensure we have valid data
if (!metrics || !metrics.timeSeries || !referenceData || !referenceData.moments) {
return { error: 'Missing required data for correlation calculation' };
}
// Process each metric category
Object.keys(metrics.timeSeries).forEach(category => {
correlations[category] = {};
// Process each metric in the category
Object.keys(metrics.timeSeries[category]).forEach(metricName => {
correlations[category][metricName] = {};
// Get time series data for this metric
const metricTimeSeries = metrics.timeSeries[category][metricName];
// Process each event type
Object.keys(referenceData.moments).forEach(eventType => {
// Get reference frames for this event
const eventFrames = referenceData.moments[eventType];
if (Array.isArray(eventFrames) && eventFrames.length > 0) {
// Calculate correlation between metric and event frames
correlations[category][metricName][eventType] =
calculateEventCorrelation(metricTimeSeries, eventFrames);
}
});
});
});
return correlations;
}
/**
* Calculate inter-metric correlations at a specific frame
* @param {Object} metrics - Metrics object with timeSeries property
* @param {number} frameIndex - Frame index to calculate correlations for
* @returns {Object} Inter-metric correlations
*/
function calculateInterMetricCorrelations(metrics, frameIndex) {
if (!metrics || !metrics.timeSeries) {
return { error: 'Missing metrics data' };
}
// Validate frame index
const maxFrames = getMaxFrameCount(metrics.timeSeries);
if (frameIndex < 0 || frameIndex >= maxFrames) {
return { error: 'Invalid frame index' };
}
const relationships = {
frame: frameIndex,
metrics: [],
correlations: {}
};
// Extract all available metrics at this frame
const allMetrics = [];
for (const category in metrics.timeSeries) {
for (const metricName in metrics.timeSeries[category]) {
const timeSeries = metrics.timeSeries[category][metricName];
if (Array.isArray(timeSeries) && frameIndex < timeSeries.length &&
timeSeries[frameIndex] !== null && timeSeries[frameIndex] !== undefined) {
const metric = {
id: `${category}.${metricName}`,
category,
name: metricName,
value: timeSeries[frameIndex]
};
allMetrics.push(metric);
relationships.metrics.push(metric);
}
}
}
// Calculate correlations between each pair of metrics
for (let i = 0; i < allMetrics.length; i++) {
const metric1 = allMetrics[i];
if (!relationships.correlations[metric1.id]) {
relationships.correlations[metric1.id] = {};
}
for (let j = i + 1; j < allMetrics.length; j++) {
const metric2 = allMetrics[j];
// Simple ratio correlation
const ratio = metric1.value !== 0 ? metric2.value / metric1.value : 0;
const difference = metric2.value - metric1.value;
relationships.correlations[metric1.id][metric2.id] = {
ratio,
difference
};
// Add the reverse correlation too
if (!relationships.correlations[metric2.id]) {
relationships.correlations[metric2.id] = {};
}
relationships.correlations[metric2.id][metric1.id] = {
ratio: metric2.value !== 0 ? metric1.value / metric2.value : 0,
difference: -difference
};
}
}
return relationships;
}
/**
* Get the maximum frame count from time series data
* @param {Object} timeSeriesData - Time series data object
* @returns {number} Maximum frame count
*/
function getMaxFrameCount(timeSeriesData) {
let maxFrames = 0;
for (const category in timeSeriesData) {
for (const metricName in timeSeriesData[category]) {
const timeSeries = timeSeriesData[category][metricName];
if (Array.isArray(timeSeries) && timeSeries.length > maxFrames) {
maxFrames = timeSeries.length;
}
}
}
return maxFrames;
}
/**
* Calculate timing correlations between metrics and reference data
* @param {Object} metrics - Metrics object with timing property
* @param {Object} reference - Reference data with timing property
* @returns {Object} Timing correlation factors
*/
function calculateTimingCorrelations(metrics, reference) {
if (!metrics || !metrics.timing || !reference || !reference.timing) {
return {};
}
const correlations = {
overallScore: 0,
factors: {}
};
// Process each timing metric
for (const timingMetric in metrics.timing) {
if (reference.timing[timingMetric] &&
metrics.timing[timingMetric] &&
typeof metrics.timing[timingMetric].value === 'number' &&
typeof reference.timing[timingMetric].value === 'number') {
const metricValue = metrics.timing[timingMetric].value;
const referenceValue = reference.timing[timingMetric].value;
// Calculate simple ratio and difference
const ratio = referenceValue !== 0 ? metricValue / referenceValue : 0;
const difference = metricValue - referenceValue;
const percentDiff = referenceValue !== 0 ? (difference / referenceValue) * 100 : 0;
correlations.factors[timingMetric] = {
metric: metricValue,
reference: referenceValue,
ratio,
difference,
percentDiff
};
}
}
// Calculate overall score based on average match
if (Object.keys(correlations.factors).length > 0) {
let totalMatch = 0;
let count = 0;
for (const metric in correlations.factors) {
// Calculate match score (1 = perfect match, 0 = complete mismatch)
const percentDiff = Math.abs(correlations.factors[metric].percentDiff);
const matchScore = Math.max(0, 1 - (percentDiff / 100));
correlations.factors[metric].matchScore = matchScore;
totalMatch += matchScore;
count++;
}
correlations.overallScore = count > 0 ? totalMatch / count : 0;
}
return correlations;
}
/**
* Predict a frame using correlation factors
* @param {Object} metrics - Phase one metrics data
* @param {Object} correlations - Correlation factors for the event
* @param {string} eventName - Name of the event
* @param {Function} [debugLogger] - Optional debug logging function
* @returns {number|null} Predicted frame index or null if prediction fails
*/
function predictFrameUsingCorrelations(metrics, correlations, eventName, debugLogger) {
const debug = debugLogger || (() => {});
debug(`Predicting ${eventName} using correlation factors`);
// Safety check for correlations
if (!correlations || Object.keys(correlations).length === 0) {
debug('No correlation data available for this event');
return null;
}
// Get time series data
const timeSeriesData = metrics.timeSeries || {};
if (!timeSeriesData || Object.keys(timeSeriesData).length === 0) {
debug('No time series data available for correlation-based prediction');
return null;
}
// Find frame count
let frameCount = 0;
for (const category in timeSeriesData) {
for (const metricName in timeSeriesData[category]) {
const data = timeSeriesData[category][metricName];
if (Array.isArray(data) && data.length > 0) {
frameCount = data.length;
break;
}
}
if (frameCount > 0) break;
}
if (frameCount === 0) {
debug('Cannot determine frame count for correlation-based prediction');
return null;
}
// Calculate scores for each frame
const scores = new Array(frameCount).fill(0);
let totalPredictiveScore = 0;
let metricsUsed = 0;
// For each metric with correlations
for (const category in correlations) {
// Skip if category doesn't exist in time series data
if (!timeSeriesData[category]) {
continue;
}
for (const metricName in correlations[category]) {
// Skip if metric doesn't exist in correlations or time series
if (!correlations[category][metricName] ||
!correlations[category][metricName][eventName] ||
!timeSeriesData[category][metricName]) {
continue;
}
const correlation = correlations[category][metricName][eventName];
// Skip if this metric doesn't have a predictive score
if (!correlation || !correlation.eventPredictiveScore || correlation.eventPredictiveScore <= 0) {
continue;
}
const metricData = timeSeriesData[category][metricName];
// Skip if metric data is not an array or is empty
if (!Array.isArray(metricData) || metricData.length === 0) {
continue;
}
// Weight of this metric in prediction
const weight = correlation.eventPredictiveScore;
totalPredictiveScore += weight;
metricsUsed++;
// Check pattern type
const pattern = correlation.window ? correlation.window.pattern : null;
// For each frame, calculate contribution to score
for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) {
if (frameIndex >= metricData.length || metricData[frameIndex] === null) continue;
let frameScore = 0;
// Check for pattern match
if (pattern && frameIndex > 1 && frameIndex < frameCount - 2 &&
frameIndex < metricData.length - 2) {
// Check for peak pattern
if (pattern === 'peak') {
if (frameIndex - 1 < metricData.length && frameIndex + 1 < metricData.length &&
metricData[frameIndex - 1] !== null && metricData[frameIndex + 1] !== null) {
const isPeak = metricData[frameIndex - 1] < metricData[frameIndex] &&
metricData[frameIndex] > metricData[frameIndex + 1];
if (isPeak) {
frameScore += 1.0;
}
}
}
// Check for valley pattern
else if (pattern === 'valley') {
if (frameIndex - 1 < metricData.length && frameIndex + 1 < metricData.length &&
metricData[frameIndex - 1] !== null && metricData[frameIndex + 1] !== null) {
const isValley = metricData[frameIndex - 1] > metricData[frameIndex] &&
metricData[frameIndex] < metricData[frameIndex + 1];
if (isValley) {
frameScore += 1.0;
}
}
}
// Check for inflection pattern
else if (pattern === 'inflection') {
if (frameIndex - 1 < metricData.length && frameIndex + 1 < metricData.length &&
metricData[frameIndex - 1] !== null && metricData[frameIndex + 1] !== null) {
const derivative1 = metricData[frameIndex] - metricData[frameIndex - 1];
const derivative2 = metricData[frameIndex + 1] - metricData[frameIndex];
// Sign change in derivative indicates inflection
if ((derivative1 * derivative2) <= 0) {
frameScore += 0.8;
}
}
}
}
// Check for value match
if (correlation.valueAtMainFrame !== null) {
// Calculate similarity to the expected value at frame
const valueDifference = Math.abs(metricData[frameIndex] - correlation.valueAtMainFrame);
// Make sure value range is valid
if (correlation.valueRange &&
correlation.valueRange.max !== null &&
correlation.valueRange.min !== null) {
const expectedRange = correlation.valueRange.max - correlation.valueRange.min;
if (expectedRange > 0) {
// Higher score for values closer to the expected value
const similarityScore = 1.0 - Math.min(1.0, valueDifference / expectedRange);
frameScore += similarityScore * 0.5;
}
}
}
// Check for derivative match
if (correlation.derivativeAtFrame !== null &&
frameIndex > 0 && frameIndex < frameCount - 1 &&
frameIndex - 1 < metricData.length && frameIndex + 1 < metricData.length &&
metricData[frameIndex - 1] !== null && metricData[frameIndex + 1] !== null) {
const derivative = (metricData[frameIndex + 1] - metricData[frameIndex - 1]) / 2;
const derivativeDifference = Math.abs(derivative - correlation.derivativeAtFrame);
// Higher score for derivatives closer to the expected derivative
const derivativeSimilarity = Math.exp(-derivativeDifference * 5);
frameScore += derivativeSimilarity * 0.5;
}
// Add weighted score to the frame
scores[frameIndex] += frameScore * weight;
}
}
}
// If no metrics were used, return null
if (metricsUsed === 0) {
debug(`No usable metrics found for ${eventName}, falling back to heuristic-based detection`);
return null;
}
// Apply a sliding window to favor clusters
if (totalPredictiveScore > 0) {
// Use raw scores without normalization
// Apply sliding window
const windowSize = 2;
const windowedScores = new Array(scores.length).fill(0);
for (let i = 0; i < scores.length; i++) {
let windowSum = scores[i];
let windowCount = 1;
// Look at frames before
for (let j = 1; j <= windowSize; j++) {
if (i - j >= 0) {
windowSum += scores[i - j] * (1 - j * 0.3);
windowCount++;
}
}
// Look at frames after
for (let j = 1; j <= windowSize; j++) {
if (i + j < scores.length) {
windowSum += scores[i + j] * (1 - j * 0.3);
windowCount++;
}
}
windowedScores[i] = windowSum / windowCount;
}
// Find the frame with highest score
let maxScore = -1;
let maxIndex = null;
for (let i = 0; i < windowedScores.length; i++) {
if (windowedScores[i] > maxScore) {
maxScore = windowedScores[i];
maxIndex = i;
}
}
if (maxIndex !== null && maxScore > 0.2) {
debug(`Correlation-based prediction for ${eventName}: frame ${maxIndex} (score: ${maxScore.toFixed(3)})`);
return maxIndex;
} else if (maxIndex !== null) {
debug(`Weak correlation-based prediction for ${eventName}: frame ${maxIndex} (score: ${maxScore.toFixed(3)})`);
debug(`Score too low, falling back to heuristic-based detection`);
}
} else {
debug(`No predictive metrics found for ${eventName}, falling back to heuristic-based detection`);
}
return null;
}
/**
* Calculate correlations between metrics and adjust them
* @param {Object} metrics - Metrics data
* @param {Object} reference - Reference data
* @returns {Object} Correlation adjustments that can be applied
*/
function calculateCorrelationAdjustments(metrics, reference) {
if (!metrics || !reference) {
return {};
}
const adjustments = {
scaling: {},
offsets: {},
relationships: {}
};
// Calculate scaling factors between metrics and reference
for (const category in metrics) {
if (typeof metrics[category] !== 'object' || !reference[category]) {
continue;
}
adjustments.scaling[category] = {};
adjustments.offsets[category] = {};
for (const metricName in metrics[category]) {
if (reference[category][metricName] &&
metrics[category][metricName] &&
typeof metrics[category][metricName].value === 'number' &&
typeof reference[category][metricName].value === 'number') {
const metricValue = metrics[category][metricName].value;
const referenceValue = reference[category][metricName].value;
// Calculate scaling factor (avoid division by zero)
if (metricValue !== 0) {
adjustments.scaling[category][metricName] = referenceValue / metricValue;
} else {
adjustments.scaling[category][metricName] = 1;
}
// Calculate offset
adjustments.offsets[category][metricName] = referenceValue - metricValue;
}
}
}
// Calculate inter-metric relationships
if (metrics.timeSeries) {
adjustments.relationships = calculateInterMetricRelationships(metrics.timeSeries);
}
return adjustments;
}
/**
* Calculate relationships between different metrics in time series data
* @param {Object} timeSeriesData - Time series data
* @returns {Object} Metric relationships
*/
function calculateInterMetricRelationships(timeSeriesData) {
const relationships = {};
// Get list of all metrics
const allMetrics = [];
for (const category in timeSeriesData) {
for (const metricName in timeSeriesData[category]) {
if (Array.isArray(timeSeriesData[category][metricName])) {
allMetrics.push({
id: `${category}.${metricName}`,
category,
name: metricName,
data: timeSeriesData[category][metricName]
});
}
}
}
// Calculate correlations between all pairs of metrics
for (let i = 0; i < allMetrics.length; i++) {
const metric1 = allMetrics[i];
relationships[metric1.id] = {};
for (let j = i + 1; j < allMetrics.length; j++) {
const metric2 = allMetrics[j];
// Calculate correlation between time series
const correlation = calculateCorrelationCoefficient(metric1.data, metric2.data);
if (!isNaN(correlation)) {
relationships[metric1.id][metric2.id] = { correlation };
// Add reverse relationship too
if (!relationships[metric2.id]) {
relationships[metric2.id] = {};
}
relationships[metric2.id][metric1.id] = { correlation };
}
}
}
return relationships;
}
module.exports = {
calculateCorrelationCoefficient,
calculateEventCorrelation,
calculateMetricEventCorrelations,
calculateInterMetricCorrelations,
getMaxFrameCount,
calculateTimingCorrelations,
predictFrameUsingCorrelations,
calculateCorrelationAdjustments,
calculateInterMetricRelationships
};