bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
255 lines (211 loc) • 7.63 kB
JavaScript
/**
* @module metrics/calculations/TimeSeriesUtilities
* @description Utility functions for time series analysis and metrics processing
*/
/**
* Get a metric value from a specific frame in time series data
* @param {Object} metrics - Metrics data object
* @param {string} metricPath - Dot-notation path to the metric (e.g., "angles.elbowFlexions.left")
* @param {number} frameIndex - Frame index
* @returns {number|null} Metric value or null if not available
*/
function getMetricValueAtFrame(metrics, metricPath, frameIndex) {
if (!metrics || !metricPath || typeof metricPath !== 'string') {
return null;
}
// Parse the metric path
const parts = metricPath.split('.');
if (parts.length < 2) {
return null;
}
const category = parts[0];
const remainingPath = parts.slice(1).join('.');
// Ensure we have timeSeries data
if (!metrics.timeSeries || !metrics.timeSeries[category]) {
return null;
}
// Handle nested paths (e.g., "angles.elbowFlexions.left")
let data = metrics.timeSeries[category];
const pathParts = remainingPath.split('.');
for (let i = 0; i < pathParts.length; i++) {
if (!data[pathParts[i]]) {
return null;
}
data = data[pathParts[i]];
}
// If we've reached an array, get the value at the specified frame index
if (Array.isArray(data)) {
return frameIndex >= 0 && frameIndex < data.length ? data[frameIndex] : null;
}
// If we've reached an object with 'values' property (old time series format)
if (data.values && Array.isArray(data.values)) {
return frameIndex >= 0 && frameIndex < data.values.length ? data.values[frameIndex] : null;
}
return null;
}
/**
* Calculate the average value of a metric over a range of frames
* @param {Object} metrics - Metrics data object
* @param {string} metricPath - Dot-notation path to the metric
* @param {number} startFrame - Start frame index (inclusive)
* @param {number} endFrame - End frame index (inclusive)
* @returns {number|null} Average metric value or null if not available
*/
function getMetricAverageOverFrames(metrics, metricPath, startFrame, endFrame) {
if (!metrics || !metricPath || typeof metricPath !== 'string') {
return null;
}
// Parse the metric path
const parts = metricPath.split('.');
if (parts.length < 2) {
return null;
}
const category = parts[0];
const remainingPath = parts.slice(1).join('.');
// Ensure we have timeSeries data
if (!metrics.timeSeries || !metrics.timeSeries[category]) {
return null;
}
// Get the data array
let data = metrics.timeSeries[category];
const pathParts = remainingPath.split('.');
for (let i = 0; i < pathParts.length; i++) {
if (!data[pathParts[i]]) {
return null;
}
data = data[pathParts[i]];
}
// Get the values array
let values = null;
if (Array.isArray(data)) {
values = data;
} else if (data.values && Array.isArray(data.values)) {
values = data.values;
}
if (!values) {
return null;
}
// Calculate average over frame range
const start = Math.max(0, startFrame);
const end = Math.min(values.length - 1, endFrame);
if (start > end) {
return null;
}
let sum = 0;
let count = 0;
for (let i = start; i <= end; i++) {
if (values[i] !== null && values[i] !== undefined) {
sum += values[i];
count++;
}
}
return count > 0 ? sum / count : null;
}
/**
* Calculate standard deviation of a metric over a range of frames
* @param {Object} metrics - Metrics data object
* @param {string} metricPath - Dot-notation path to the metric
* @param {number} startFrame - Start frame index (inclusive)
* @param {number} endFrame - End frame index (inclusive)
* @returns {number|null} Standard deviation or null if not available
*/
function getMetricStdDevOverFrames(metrics, metricPath, startFrame, endFrame) {
if (!metrics || !metricPath || typeof metricPath !== 'string') {
return null;
}
// Parse the metric path
const parts = metricPath.split('.');
if (parts.length < 2) {
return null;
}
const category = parts[0];
const remainingPath = parts.slice(1).join('.');
// Ensure we have timeSeries data
if (!metrics.timeSeries || !metrics.timeSeries[category]) {
return null;
}
// Get the data array
let data = metrics.timeSeries[category];
const pathParts = remainingPath.split('.');
for (let i = 0; i < pathParts.length; i++) {
if (!data[pathParts[i]]) {
return null;
}
data = data[pathParts[i]];
}
// Get the values array
let values = null;
if (Array.isArray(data)) {
values = data;
} else if (data.values && Array.isArray(data.values)) {
values = data.values;
}
if (!values) {
return null;
}
// Calculate standard deviation over frame range
const start = Math.max(0, startFrame);
const end = Math.min(values.length - 1, endFrame);
if (start > end) {
return null;
}
// Calculate mean first
let sum = 0;
let count = 0;
for (let i = start; i <= end; i++) {
if (values[i] !== null && values[i] !== undefined && !isNaN(values[i])) {
sum += values[i];
count++;
}
}
if (count <= 1) {
return null;
}
const mean = sum / count;
// Calculate squared differences
let sumSquaredDiff = 0;
for (let i = start; i <= end; i++) {
if (values[i] !== null && values[i] !== undefined && !isNaN(values[i])) {
sumSquaredDiff += Math.pow(values[i] - mean, 2);
}
}
return Math.sqrt(sumSquaredDiff / count);
}
/**
* Validate event object
* @param {Object} event - Event object to validate
* @returns {boolean} True if valid, false otherwise
*/
function isValidEvent(event) {
return event &&
typeof event === 'object' &&
typeof event.name === 'string' &&
typeof event.frameIndex === 'number' &&
event.frameIndex >= 0 &&
typeof event.confidence === 'number' &&
event.confidence >= 0 &&
event.confidence <= 1;
}
/**
* Format frame index as time string based on frame rate
* @param {number} frameIndex - Frame index
* @param {number} fps - Frames per second
* @returns {string} Formatted time string (mm:ss.ms)
*/
function formatFrameTime(frameIndex, fps = 30) {
if (typeof frameIndex !== 'number' || isNaN(frameIndex) || frameIndex < 0) {
return '00:00.000';
}
const totalSeconds = frameIndex / fps;
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.floor(totalSeconds % 60);
const milliseconds = Math.floor((totalSeconds % 1) * 1000);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
}
module.exports = {
getMetricValueAtFrame,
getMetricAverageOverFrames,
getMetricStdDevOverFrames,
isValidEvent,
formatFrameTime
};