UNPKG

bowling-analysis-system

Version:

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

374 lines (320 loc) 11.9 kB
/** * @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 };