Skip to content

Motiondescriptors

mg_motiondescriptors

mg_motiondescriptors(self, window='hann', entropy_bins=50, fmin=0.2, fmax=10.0, save_data=True, save_plot=True, data_format='csv', target_name=None, overwrite=True)

Scalar movement descriptors derived from the quantity-of-motion (QoM) signal.

Computes a compact set of higher-level descriptors that summarise how something moves, complementing the per-frame motion data from :func:motion:

  • motion_energy — mean squared QoM; the overall amount of movement.
  • motion_smoothness — SPARC (spectral arc length) of the QoM profile; a dimensionless, validated smoothness metric (less negative = smoother, more negative = jerkier).
  • motion_entropy — normalised (0–1) Shannon entropy of the QoM magnitude distribution; the complexity/variedness of the motion.
  • spectral descriptors of the QoM signal (Hann-windowed by default): the dominant frequency (Hz, the main movement-rhythm rate) and the spectral centroid (Hz, the "centre of mass" of the movement spectrum).

Parameters:

Name Type Description Default
window str

FFT window for the spectral descriptors — 'hann' (default, recommended to reduce leakage) or 'none' for a rectangular window.

'hann'
entropy_bins int

Number of histogram bins for the entropy estimate. Defaults to 50.

50
fmin float

Lowest frequency (Hz) considered for the dominant frequency and spectral centroid, excluding slow amplitude drift near DC. Defaults to 0.2.

0.2
fmax float

Highest frequency (Hz) considered for those spectral descriptors. Defaults to 10.0.

10.0
save_data bool

Save the descriptors to a data file. Defaults to True.

True
save_plot bool

Save the figure (QoM time series + power spectrum). Defaults to True.

True
data_format str

Data file format: 'csv', 'tsv' or 'txt'. Defaults to 'csv'.

'csv'
target_name str

Output image name. Defaults to None (<name>_motiondescriptors.png).

None
overwrite bool

Overwrite or auto-increment the filename. Defaults to True.

True

Returns:

Name Type Description
MgFigure 'MgFigure'

figure whose .data holds the scalar descriptors and the spectrum arrays

'MgFigure'

(frequencies, power), or None if there are too few frames.

Source code in musicalgestures/_motiondescriptors.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def mg_motiondescriptors(self, window: str = 'hann', entropy_bins: int = 50,
                         fmin: float = 0.2, fmax: float = 10.0,
                         save_data: bool = True, save_plot: bool = True,
                         data_format: str = 'csv', target_name: str | None = None,
                         overwrite: bool = True) -> "MgFigure":
    """Scalar movement descriptors derived from the quantity-of-motion (QoM) signal.

    Computes a compact set of higher-level descriptors that summarise *how* something moves,
    complementing the per-frame motion data from :func:`motion`:

    - **motion_energy** — mean squared QoM; the overall amount of movement.
    - **motion_smoothness** — SPARC (spectral arc length) of the QoM profile; a dimensionless,
      validated smoothness metric (less negative = smoother, more negative = jerkier).
    - **motion_entropy** — normalised (0–1) Shannon entropy of the QoM magnitude distribution;
      the complexity/variedness of the motion.
    - **spectral descriptors** of the QoM signal (Hann-windowed by default): the **dominant
      frequency** (Hz, the main movement-rhythm rate) and the **spectral centroid** (Hz, the
      "centre of mass" of the movement spectrum).

    Args:
        window (str, optional): FFT window for the spectral descriptors — 'hann' (default,
            recommended to reduce leakage) or 'none' for a rectangular window.
        entropy_bins (int, optional): Number of histogram bins for the entropy estimate. Defaults to 50.
        fmin (float, optional): Lowest frequency (Hz) considered for the dominant frequency and
            spectral centroid, excluding slow amplitude drift near DC. Defaults to 0.2.
        fmax (float, optional): Highest frequency (Hz) considered for those spectral descriptors.
            Defaults to 10.0.
        save_data (bool, optional): Save the descriptors to a data file. Defaults to True.
        save_plot (bool, optional): Save the figure (QoM time series + power spectrum). Defaults to True.
        data_format (str, optional): Data file format: 'csv', 'tsv' or 'txt'. Defaults to 'csv'.
        target_name (str, optional): Output image name. Defaults to None (``<name>_motiondescriptors.png``).
        overwrite (bool, optional): Overwrite or auto-increment the filename. Defaults to True.

    Returns:
        MgFigure: figure whose ``.data`` holds the scalar descriptors and the spectrum arrays
        (``frequencies``, ``power``), or None if there are too few frames.
    """
    qom, fps = _movement_qom(self)
    if qom.size < 4:
        print('Not enough frames to compute motion descriptors.')
        return None

    motion_energy = float(np.mean(qom ** 2))
    motion_smoothness = _sparc(qom, fps)
    motion_entropy = _motion_entropy(qom, bins=entropy_bins)
    freqs, power = _qom_spectrum(qom, fps, window=window)

    # Restrict the spectral descriptors to a movement band so slow amplitude drift near DC
    # doesn't masquerade as the dominant movement rhythm.
    band = (freqs >= fmin) & (freqs <= fmax)
    if band.any() and power[band].any():
        f_band, p_band = freqs[band], power[band]
        dominant_freq = float(f_band[int(np.argmax(p_band))])
        spectral_centroid = float(np.sum(f_band * p_band) / np.sum(p_band))
    else:
        dominant_freq = 0.0
        spectral_centroid = 0.0

    d = {
        'of': self.of,
        'fps': fps,
        'n_frames': int(qom.size),
        'motion_energy': motion_energy,
        'motion_smoothness': motion_smoothness,
        'motion_entropy': motion_entropy,
        'dominant_freq': dominant_freq,
        'spectral_centroid': spectral_centroid,
        'window': window,
        'fmin': fmin,
        'fmax': fmax,
        'qom': qom,
        'frequencies': freqs,
        'power': power,
    }

    if save_data:
        _save_descriptors(self.of, d, data_format, overwrite)

    target_name = resolve_filename(self.of, '_motiondescriptors.png', target_name, overwrite)

    times = np.arange(qom.size) / fps
    fig, (ax_q, ax_s) = plt.subplots(2, 1, figsize=(12, 7), dpi=300)
    fig.patch.set_facecolor('white')
    fig.patch.set_alpha(1)

    ax_q.plot(times, qom, color='#1f77b4', lw=1.0)
    ax_q.fill_between(times, qom, color='#1f77b4', alpha=0.15)
    ax_q.set(xlabel='Time (s)', ylabel='Quantity of motion', title='Quantity of motion over time')
    ax_q.margins(x=0)

    # Power spectrum up to a sensible movement band (10 Hz), dominant frequency marked.
    band = freqs <= 10.0 if freqs.size else slice(None)
    ax_s.plot(freqs[band], power[band], color='#d62728', lw=1.0)
    ax_s.fill_between(freqs[band], power[band], color='#d62728', alpha=0.15)
    if dominant_freq > 0:
        ax_s.axvline(dominant_freq, color='#333333', ls='--', lw=1.0,
                     label=f'dominant {dominant_freq:.2f} Hz')
        ax_s.legend()
    ax_s.set(xlabel='Frequency (Hz)', ylabel='Power',
             title=f'QoM power spectrum ({window} window)')
    ax_s.margins(x=0)

    summary = (f'energy = {motion_energy:.3g}    smoothness (SPARC) = {motion_smoothness:.3f}    '
               f'entropy = {motion_entropy:.3f}    spectral centroid = {spectral_centroid:.2f} Hz')
    fig.suptitle(summary, fontsize=11, fontweight='bold')

    plt.tight_layout(rect=[0, 0, 1, 0.95])
    if save_plot:
        plt.savefig(target_name, format='png', transparent=False)
    plt.close(fig)

    mgf = MgFigure(figure=fig, figure_type='video.motiondescriptors', data=d, layers=None,
                   image=target_name if save_plot else None)
    self.motiondescriptors_figure = mgf
    return mgf