bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
374 lines (320 loc) • 11.9 kB
JavaScript
/**
* @module config/dynamic-config
* @description Dynamic configuration system that adjusts based on input characteristics
*/
const path = require('path');
const fs = require('fs');
const { defaultLogger } = require('../utils/logger');
// Default base configuration
const DEFAULT_CONFIG = {
// Frame position percentages
framePositions: {
releasePoint: 0.8, // ~80% of total frames
frontFootLanding: 0.65, // ~65% of total frames
backFootLanding: 0.5, // ~50% of total frames
defaultPositions: {
releasePoint: 0.7, // For default event frame estimates
frontFootLanding: 0.5,
backFootLanding: 0.3
}
},
// Search windows
searchWindows: {
footPlantLookback: 0.08, // Look back 8% of total frames
minLookbackFrames: 5, // Minimum frames to look back
maxLookbackFrames: 20, // Maximum frames to look back
},
// Confidence values
confidence: {
releasePoint: 1.0,
frontFootLanding: 0.9,
backFootLanding: 0.375, // Will be 0.5 after validation
detectionBase: {
releasePoint: 0.95,
frontFootLanding: 0.9,
backFootLanding: 0.7,
},
defaultBase: 0.3, // Base confidence for defaults
sequenceCorrectionScale: 0.8, // Confidence reduction when correcting sequence
},
// Thresholds
thresholds: {
velocity: {
lowVelocityPercentile: 0.25, // 25th percentile of velocities
lowVelocityMultiplier: 1.2, // Add 20% margin to threshold
defaultLowVelocity: 0.5, // Default if calculation fails
},
confidence: {
releasePoint: 0.55,
footPlant: 0.45,
default: 0.45,
},
correlation: {
minimumCorrelations: 2,
correlationRatio: 0.15, // 15% of total correlations
},
consecutiveFrames: {
footPlant: 3,
adjustmentForFramerate: (fps) => Math.max(3, Math.floor(fps / 10)), // 3 frames at 30fps, 6 at 60fps
}
},
// Sequence validation
sequenceValidation: {
correctionOffsetRatio: 0.15, // 15% of total frames
minCorrectionOffset: 10, // Minimum frames to offset
},
// Visualization
visualization: {
colors: {
left: 'rgba(54, 162, 235, 0.7)',
right: 'rgba(255, 99, 132, 0.7)',
neutral: 'rgba(75, 192, 192, 0.2)',
},
chartOptions: {
lineTension: 0.4,
}
}
};
/**
* Dynamic configuration class
*/
class DynamicConfig {
constructor(options = {}) {
this.options = options;
this.logger = options.logger || defaultLogger.child('DynamicConfig');
this.config = { ...DEFAULT_CONFIG };
this.metadata = {
totalFrames: 0,
validFrames: 0,
frameRate: 30, // Default assumption
};
this.initialized = false;
}
/**
* Initialize with input data characteristics
* @param {Object} metadata - Input metadata
* @param {number} metadata.totalFrames - Total frame count
* @param {number} [metadata.validFrames] - Valid frame count
* @param {number} [metadata.frameRate] - Frame rate
* @returns {DynamicConfig} This instance for chaining
*/
initialize(metadata = {}) {
this.metadata = {
...this.metadata,
...metadata,
};
// Apply any environment variable overrides
this._applyEnvironmentOverrides();
// Adjust thresholds based on input characteristics
this._adjustThresholds();
this.initialized = true;
this.logger.debug('Dynamic configuration initialized', {
totalFrames: this.metadata.totalFrames,
validFrames: this.metadata.validFrames,
});
return this;
}
/**
* Apply environment variable overrides
* @private
*/
_applyEnvironmentOverrides() {
// Override confidence thresholds from environment
if (process.env.RELEASE_POINT_CONFIDENCE) {
this.config.confidence.releasePoint = parseFloat(process.env.RELEASE_POINT_CONFIDENCE);
}
if (process.env.FRONT_FOOT_CONFIDENCE) {
this.config.confidence.frontFootLanding = parseFloat(process.env.FRONT_FOOT_CONFIDENCE);
}
if (process.env.BACK_FOOT_CONFIDENCE) {
this.config.confidence.backFootLanding = parseFloat(process.env.BACK_FOOT_CONFIDENCE);
}
// Other environment overrides as needed
if (process.env.EVENT_CONFIDENCE_THRESHOLD) {
const threshold = parseFloat(process.env.EVENT_CONFIDENCE_THRESHOLD);
this.config.thresholds.confidence.default = threshold;
this.config.thresholds.confidence.footPlant = threshold;
}
}
/**
* Adjust thresholds based on input characteristics
* @private
*/
_adjustThresholds() {
const { totalFrames, validFrames, frameRate } = this.metadata;
// Skip adjustments if we don't have frame data
if (!totalFrames) return;
// Adjust consecutive frames based on frame rate
if (frameRate) {
this.config.thresholds.consecutiveFrames.footPlant =
this.config.thresholds.consecutiveFrames.adjustmentForFramerate(frameRate);
}
// Adjust confidence thresholds based on data quality
if (validFrames && totalFrames) {
const validRatio = validFrames / totalFrames;
// Lower threshold requirements for poor quality data
if (validRatio < 0.7) {
this.config.thresholds.confidence.releasePoint -= 0.05;
this.config.thresholds.confidence.footPlant -= 0.05;
this.config.thresholds.confidence.default -= 0.05;
}
}
}
/**
* Get a configuration value
* @param {string} key - Dot notation path to config value
* @param {any} defaultValue - Default value if not found
* @returns {any} Configuration value
*/
get(key, defaultValue) {
if (!this.initialized) {
this.logger.warn('Configuration accessed before initialization');
}
const path = key.split('.');
let current = this.config;
for (const segment of path) {
if (current === undefined || current === null) {
return defaultValue;
}
current = current[segment];
}
return current !== undefined ? current : defaultValue;
}
/**
* Calculate frame index based on percentage
* @param {string} eventType - Event type (releasePoint, frontFootLanding, backFootLanding)
* @param {string} [context="default"] - Context for the calculation
* @returns {number} Frame index
*/
getFrameIndex(eventType, context = 'default') {
if (!this.initialized || !this.metadata.totalFrames) {
this.logger.warn('Frame index calculation requested before initialization');
return 0;
}
const { totalFrames } = this.metadata;
let percentage;
if (context === 'default') {
percentage = this.get(`framePositions.defaultPositions.${eventType}`, 0.5);
} else {
percentage = this.get(`framePositions.${eventType}`, 0.5);
}
return Math.floor(totalFrames * percentage);
}
/**
* Calculate correction offset for sequence validation
* @returns {number} Correction offset in frames
*/
getSequenceCorrectionOffset() {
if (!this.initialized || !this.metadata.totalFrames) {
return this.get('sequenceValidation.minCorrectionOffset', 10);
}
const { totalFrames } = this.metadata;
const ratio = this.get('sequenceValidation.correctionOffsetRatio', 0.15);
const minOffset = this.get('sequenceValidation.minCorrectionOffset', 10);
return Math.max(minOffset, Math.floor(totalFrames * ratio));
}
/**
* Calculate low velocity threshold for foot plant detection
* @param {Array} velocities - Array of velocities to analyze
* @returns {number} Low velocity threshold
*/
getLowVelocityThreshold(velocities) {
if (!velocities || !velocities.length) {
return this.get('thresholds.velocity.defaultLowVelocity', 0.5);
}
try {
// Filter and sort velocities
const validVelocities = velocities.filter(v =>
v !== null && v !== undefined && !isNaN(v));
if (validVelocities.length < 5) {
return this.get('thresholds.velocity.defaultLowVelocity', 0.5);
}
validVelocities.sort((a, b) => a - b);
// Get percentile based on config
const percentile = this.get('thresholds.velocity.lowVelocityPercentile', 0.25);
const index = Math.floor(validVelocities.length * percentile);
const threshold = validVelocities[index];
// Apply multiplier for margin
const multiplier = this.get('thresholds.velocity.lowVelocityMultiplier', 1.2);
return threshold * multiplier;
} catch (error) {
this.logger.warn('Error calculating velocity threshold', { error: error.message });
return this.get('thresholds.velocity.defaultLowVelocity', 0.5);
}
}
/**
* Get configuration for enhanced event confidence
* @returns {Object} Event confidence configuration
*/
getEventConfidence() {
return {
releasePoint: this.get('confidence.releasePoint', 1.0),
frontFootLanding: this.get('confidence.frontFootLanding', 0.9),
backFootLanding: this.get('confidence.backFootLanding', 0.375),
};
}
/**
* Get lookback window size for foot plant validation
* @returns {number} Number of frames to look back
*/
getFootPlantLookbackWindow() {
if (!this.initialized || !this.metadata.totalFrames) {
return this.get('searchWindows.minLookbackFrames', 5);
}
const { totalFrames } = this.metadata;
const ratio = this.get('searchWindows.footPlantLookback', 0.08);
const minFrames = this.get('searchWindows.minLookbackFrames', 5);
const maxFrames = this.get('searchWindows.maxLookbackFrames', 20);
return Math.min(maxFrames, Math.max(minFrames, Math.floor(totalFrames * ratio)));
}
/**
* Calculate default confidence for events
* @param {Object} metrics - Metrics data
* @param {string} eventType - Event type
* @returns {number} Default confidence value
*/
calculateDefaultConfidence(metrics, eventType) {
// Base confidence value
let confidence = this.get('confidence.defaultBase', 0.3);
if (!metrics || !metrics.metadata) {
return confidence;
}
// Adjust based on metrics quality
const validFrames = metrics.metadata.validFrames || 0;
const totalFrames = metrics.metadata.totalFrames || 1;
const validFramesRatio = validFrames / totalFrames;
confidence *= Math.min(1.0, validFramesRatio * 1.5); // Scale by valid frame ratio
return confidence;
}
/**
* Get minimum number of valid correlations required
* @param {number} totalCorrelations - Total available correlations
* @returns {number} Minimum required correlations
*/
getMinValidCorrelations(totalCorrelations) {
if (!totalCorrelations) {
return this.get('thresholds.correlation.minimumCorrelations', 2);
}
const ratio = this.get('thresholds.correlation.correlationRatio', 0.15);
const min = this.get('thresholds.correlation.minimumCorrelations', 2);
return Math.max(min, Math.floor(totalCorrelations * ratio));
}
}
// Singleton instance
let instance = null;
/**
* Get or create configuration instance
* @param {Object} options - Configuration options
* @returns {DynamicConfig} Configuration instance
*/
function getConfig(options = {}) {
if (!instance) {
instance = new DynamicConfig(options);
}
return instance;
}
module.exports = {
DynamicConfig,
getConfig,
DEFAULT_CONFIG
};