|
| 1 | +"""Feature extraction module for velocity-based metrics in spiral drawing data.""" |
| 2 | + |
| 3 | +import numpy as np |
| 4 | +from scipy import stats |
| 5 | + |
| 6 | +from graphomotor.core import models |
| 7 | +from graphomotor.utils import center_spiral |
| 8 | + |
| 9 | + |
| 10 | +def _calculate_statistics(values: np.ndarray, name: str) -> dict[str, float]: |
| 11 | + """Helper function to calculate statistics for a given array. |
| 12 | +
|
| 13 | + Args: |
| 14 | + values: 1-D Numpy array of numerical values. |
| 15 | + name: Name prefix for the statistics (e.g., "linear_velocity"). |
| 16 | +
|
| 17 | + Returns: |
| 18 | + Dictionary containing calculated metrics (sum, median, variation, skewness, |
| 19 | + kurtosis) with keys prefixed by the provided name. |
| 20 | + """ |
| 21 | + return { |
| 22 | + f"{name}_sum": np.sum(np.abs(values)), |
| 23 | + f"{name}_median": np.median(np.abs(values)), |
| 24 | + f"{name}_variation": stats.variation(values), |
| 25 | + f"{name}_skewness": stats.skew(values), |
| 26 | + f"{name}_kurtosis": stats.kurtosis(values), |
| 27 | + } |
| 28 | + |
| 29 | + |
| 30 | +def calculate_velocity_metrics(spiral: models.Spiral) -> dict[str, float]: |
| 31 | + """Calculate velocity-based metrics from spiral drawing data. |
| 32 | +
|
| 33 | + This function computes three types of velocity metrics by calculating the difference |
| 34 | + between consecutive points in the spiral drawing data. The three types of velocity |
| 35 | + are: |
| 36 | + 1. Linear velocity: The magnitude of change of Euclidean distance in pixels |
| 37 | + per second. This is calculated as the square root of the sum of squares of |
| 38 | + the differences in x and y coordinates divided by the difference in time. |
| 39 | + 2. Radial velocity: The magnitude of change of distance from center (radius) in |
| 40 | + pixels per second. Radius is calculated as the square root of the sum of |
| 41 | + squares of x and y coordinates. |
| 42 | + 3. Angular velocity: The magnitude of change of angle in radians per second. |
| 43 | + Angle is calculated using the arctangent of y coordinates divided by x |
| 44 | + coordinates, and then unwrapped to maintain continuity across the -π to π |
| 45 | + boundary. |
| 46 | +
|
| 47 | + For each velocity type, the following metrics are calculated: |
| 48 | + - Sum: Sum of absolute velocity values |
| 49 | + - Median: Median of absolute velocity values |
| 50 | + - Variation: Coefficient of variation |
| 51 | + - Skewness: Asymmetry of the velocity distribution |
| 52 | + - Kurtosis: Tailedness of the velocity distribution |
| 53 | +
|
| 54 | + Args: |
| 55 | + spiral: Spiral object containing drawing data. |
| 56 | +
|
| 57 | + Returns: |
| 58 | + Dictionary containing calculated velocity metrics. |
| 59 | + """ |
| 60 | + spiral = center_spiral.center_spiral(spiral) |
| 61 | + x = spiral.data["x"].values |
| 62 | + y = spiral.data["y"].values |
| 63 | + time = spiral.data["seconds"].values |
| 64 | + radius = np.sqrt(x**2 + y**2) |
| 65 | + theta = np.unwrap(np.arctan2(y, x)) |
| 66 | + |
| 67 | + dx = np.diff(x) |
| 68 | + dy = np.diff(y) |
| 69 | + dt = np.diff(time) |
| 70 | + dr = np.diff(radius) |
| 71 | + dtheta = np.diff(theta) |
| 72 | + |
| 73 | + linear_velocity = np.sqrt(dx**2 + dy**2) / dt |
| 74 | + radial_velocity = dr / dt |
| 75 | + angular_velocity = dtheta / dt |
| 76 | + |
| 77 | + return { |
| 78 | + **_calculate_statistics(linear_velocity, "linear_velocity"), |
| 79 | + **_calculate_statistics(radial_velocity, "radial_velocity"), |
| 80 | + **_calculate_statistics(angular_velocity, "angular_velocity"), |
| 81 | + } |
0 commit comments