From f54c827b05e3751b24dcc7d776da87223e3bd80c Mon Sep 17 00:00:00 2001 From: Alexander Refsum Jensenius Date: Sun, 23 Aug 2026 15:50:30 +0200 Subject: [PATCH] Four more pose methods that said they always return a figure #350's tail: mypy 52 to 50, and the number understates it. Annotating the six `render_*` helpers in `_pose_visualize.py`, which carried no return annotations at all, first pushed the count UP to 54 --- because four public methods in `_pose.py` declared `-> MgFigure` while receiving `MgFigure | None` from them. So `pose_waterfall()`, `pose_segments()`, `pose_center()` and `pose_distance()` have always been able to return None, on too few frames or an empty render, and have always said otherwise. The same shape as the nine audio methods corrected in 1.13.0: the code was right and the description was wrong. Signatures and docstrings now agree with it. Two inference fixes in the same file. `points.append(None)` is deliberate --- a keypoint below the confidence threshold is recorded as absent --- but the list was inferred `list[tuple[int, int]]` from its first append, so the intended None was the error. And `_trail` is a deque or None depending on whether marker history was asked for, unannotated at three sites. 682 tests pass, 4 skipped. Co-Authored-By: Claude Opus 5 --- musicalgestures/_pose.py | 26 ++++++++++++++------------ musicalgestures/_pose_visualize.py | 14 +++++++------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/musicalgestures/_pose.py b/musicalgestures/_pose.py index a7c9d8b8..d5c0bd4e 100644 --- a/musicalgestures/_pose.py +++ b/musicalgestures/_pose.py @@ -486,7 +486,7 @@ def pose( avg_acc = np.zeros((self.height, self.width, 3), dtype=np.float64) if save_average_pose else None avg_n = 0 from collections import deque - _trail = deque(maxlen=int(marker_history)) if marker_history and marker_history > 0 else None + _trail: deque | None = deque(maxlen=int(marker_history)) if marker_history and marker_history > 0 else None while True: # Read frame-by-frame @@ -509,7 +509,7 @@ def pose( H = output.shape[2] W = output.shape[3] - points = [] + points: list[tuple[int, int] | None] = [] for i in range(nPoints): @@ -824,7 +824,7 @@ def _rerender_pose_from_cache(self: "musicalgestures.MgVideo", style='both', ove video_out = None frame_bytes = width * height * 3 from collections import deque - _trail = deque(maxlen=int(marker_history)) if marker_history and marker_history > 0 else None + _trail: deque | None = deque(maxlen=int(marker_history)) if marker_history and marker_history > 0 else None pb = MgProgressbar(total=len(data), prefix='Re-rendering pose (cached):') for ii, row in enumerate(data): @@ -920,7 +920,7 @@ def _ensure_pose_keypoints(self: "musicalgestures.MgVideo", **pose_kwargs): def mg_pose_waterfall(self: "musicalgestures.MgVideo", style: str = 'trajectories', n_samples: int = 40, markers: list | None = None, color_by: str | None = None, cmap: str = 'hsv', dpi: int = 200, elev: float = 20, azim: float = -60, lw: float = 1.0, axes: bool = True, crop: bool = False, - target_name: str | None = None, overwrite: bool = True, **pose_kwargs) -> "MgFigure": + target_name: str | None = None, overwrite: bool = True, **pose_kwargs) -> "MgFigure | None": """ Render a 3D spatio-temporal waterfall of the pose, cascading along the time (depth) axis — a pose-based counterpart to ``silhouette_waterfall()``. Uses cached pose keypoints from a @@ -952,7 +952,7 @@ def mg_pose_waterfall(self: "musicalgestures.MgVideo", style: str = 'trajectorie **pose_kwargs: Forwarded to ``pose()`` if keypoints have to be computed. Returns: - MgFigure: the 3D waterfall figure, or None if there are too few frames. + MgFigure | None: the 3D waterfall figure, or None if there are too few frames. """ from musicalgestures._pose_visualize import render_pose_waterfall @@ -974,7 +974,7 @@ def mg_pose_waterfall(self: "musicalgestures.MgVideo", style: str = 'trajectorie def mg_pose_segments(self: "musicalgestures.MgVideo", segments: list | None = None, n_bins: int = 36, cmap: str = 'viridis', dpi: int = 200, ncols: int = 6, - target_name: str | None = None, overwrite: bool = True, **pose_kwargs) -> "MgFigure": + target_name: str | None = None, overwrite: bool = True, **pose_kwargs) -> "MgFigure | None": """ Circular (polar) motion plots and statistics for each body segment. @@ -998,7 +998,7 @@ def mg_pose_segments(self: "musicalgestures.MgVideo", segments: list | None = No **pose_kwargs: Forwarded to ``pose()`` if keypoints have to be computed. Returns: - MgFigure: the grid of circular plots (per-segment stats in ``.data['stats']``), or None. + MgFigure | None: the grid of circular plots (per-segment stats in ``.data['stats']``), or None. """ from musicalgestures._pose_visualize import render_segment_circular @@ -1018,7 +1018,7 @@ def mg_pose_segments(self: "musicalgestures.MgVideo", segments: list | None = No return mgf -def mg_pose_center(self: "musicalgestures.MgVideo", save_data: bool = True, dpi: int = 200, target_name: str | None = None, overwrite: bool = True, **pose_kwargs) -> "MgFigure": +def mg_pose_center(self: "musicalgestures.MgVideo", save_data: bool = True, dpi: int = 200, target_name: str | None = None, overwrite: bool = True, **pose_kwargs) -> "MgFigure | None": """ Centre the pose data on its global centroid — a 2D port of the MoCap Toolbox ``mccenter``. @@ -1037,7 +1037,8 @@ def mg_pose_center(self: "musicalgestures.MgVideo", save_data: bool = True, dpi: **pose_kwargs: Forwarded to ``pose()`` if keypoints have to be computed. Returns: - MgFigure: the centred-trajectories figure; ``.data['coords']`` holds the (T, n, 2) centred + MgFigure | None: the centred-trajectories figure; ``.data['coords']`` holds the (T, n, 2) centred + None when the figure could not be built, for instance with too few frames. coordinates and ``.data['offset']`` the removed centroid. None if there are too few frames. """ from musicalgestures._pose_visualize import render_pose_center @@ -1068,7 +1069,7 @@ def mg_pose_center(self: "musicalgestures.MgVideo", save_data: bool = True, dpi: return mgf -def mg_pose_distance(self: "musicalgestures.MgVideo", dpi: int = 200, target_name: str | None = None, overwrite: bool = True, **pose_kwargs) -> "MgFigure": +def mg_pose_distance(self: "musicalgestures.MgVideo", dpi: int = 200, target_name: str | None = None, overwrite: bool = True, **pose_kwargs) -> "MgFigure | None": """ Per-marker distance travelled and the average across markers — a 2D port of the MoCap Toolbox ``mccumdist``. @@ -1086,7 +1087,8 @@ def mg_pose_distance(self: "musicalgestures.MgVideo", dpi: int = 200, target_nam **pose_kwargs: Forwarded to ``pose()`` if keypoints have to be computed. Returns: - MgFigure: ``.data['total']`` (per-marker totals), ``.data['average']`` (mean total), and + MgFigure | None: ``.data['total']`` (per-marker totals), ``.data['average']`` (mean total), and + None when the figure could not be built, for instance with too few frames. ``.data['cumulative']`` (per-marker cumulative curves). None if there are too few frames. """ from musicalgestures._pose_visualize import render_pose_distance @@ -1184,7 +1186,7 @@ def _pose_mediapipe( avg_acc = np.zeros((self.height, self.width, 3), dtype=np.float64) if save_average_pose else None avg_n = 0 from collections import deque - _trail = deque(maxlen=int(marker_history)) if marker_history and marker_history > 0 else None + _trail: deque | None = deque(maxlen=int(marker_history)) if marker_history and marker_history > 0 else None estimator = MediaPipePoseEstimator(device=device.lower()) diff --git a/musicalgestures/_pose_visualize.py b/musicalgestures/_pose_visualize.py index 9dd2919d..6163d225 100644 --- a/musicalgestures/_pose_visualize.py +++ b/musicalgestures/_pose_visualize.py @@ -3,7 +3,7 @@ import cv2 import matplotlib import matplotlib.pyplot as plt -from musicalgestures._utils import MgImage, generate_outfilename +from musicalgestures._utils import MgFigure, MgImage, generate_outfilename def _layout_labels(anchors, box_w, box_h, width, height, iterations=400, gap=None): @@ -90,7 +90,7 @@ def _per_marker_stats(coords, fps, fmin=0.2, fmax=8.0): def render_average_pose(data, names, connections, width, height, fps, avg_frame, - target_name, overwrite=True, fmin=0.2, fmax=8.0, style='both'): + target_name, overwrite=True, fmin=0.2, fmax=8.0, style='both') -> "MgImage | None": """ Render the average pose of the whole video, with per-marker quantity of motion (colour + label) and dominant frequency (label) annotated. @@ -191,7 +191,7 @@ def render_average_pose(data, names, connections, width, height, fps, avg_frame, def render_trajectories(data, names, width, height, fps, target_name, overwrite=True, - background='black', labels=False): + background='black', labels=False) -> "MgImage | None": """ Render every marker's spatial trajectory across the whole video. @@ -258,7 +258,7 @@ def render_trajectories(data, names, width, height, fps, target_name, overwrite= def render_pose_waterfall(data, names, width, height, fps, target_name, overwrite=True, style='trajectories', connections=None, n_samples=40, markers=None, color_by=None, cmap='hsv', dpi=200, - elev=20, azim=-60, lw=1.0, axes=True, crop=False): + elev=20, azim=-60, lw=1.0, axes=True, crop=False) -> "MgFigure | None": """ Render a 3D spatio-temporal waterfall of the pose, cascading along the time (depth) axis — a pose-based counterpart to ``silhouette_waterfall()``. @@ -450,7 +450,7 @@ def _segment_angles(coords, a, b): def render_segment_circular(data, names, connections, width, height, fps, target_name, overwrite=True, segments=None, n_bins=36, cmap='viridis', - dpi=200, ncols=6): + dpi=200, ncols=6) -> "MgFigure | None": """ Circular (polar) motion plots and statistics for every body segment. @@ -591,7 +591,7 @@ def pose_center(data, names): def render_pose_center(data, names, width, height, target_name, overwrite=True, - cmap='hsv', dpi=200): + cmap='hsv', dpi=200) -> "MgFigure | None": """ Centre the pose data (see :func:`pose_center`) and plot the centred marker trajectories. @@ -667,7 +667,7 @@ def pose_distance(data, names, width, height): def render_pose_distance(data, names, width, height, fps, target_name, overwrite=True, - cmap='hsv', dpi=200): + cmap='hsv', dpi=200) -> "MgFigure | None": """ Plot per-marker cumulative distance travelled over time plus a ranked total per marker.