| """ |
| utils/postprocess.py |
| ──────────────────── |
| Optional post-processing utilities applied to raw MotionBERT output before |
| exporting. None of these are required — the pipeline works without them — |
| but they noticeably improve visual quality for real-world videos. |
| |
| Functions |
| ───────── |
| smooth_poses(poses, window) — Gaussian temporal smoothing (removes jitter) |
| resample_poses(poses, src_fps, dst_fps) — Resample to a target frame rate |
| centre_trajectory(poses) — Root always starts at world origin |
| apply_floor(poses) — Push lowest foot Y to Y=0 (no ground clipping) |
| """ |
|
|
| from __future__ import annotations |
| import numpy as np |
| from scipy.ndimage import gaussian_filter1d |
|
|
|
|
| def smooth_poses( |
| poses: np.ndarray, |
| sigma: float = 1.5, |
| ) -> np.ndarray: |
| """ |
| Apply Gaussian temporal smoothing to (T, N_joints, 3) pose data. |
| |
| Parameters |
| ---------- |
| poses : (T, J, 3) float32 |
| sigma : standard deviation of the Gaussian kernel (frames). |
| Larger = smoother but more lag. 1.0-2.5 is usually good. |
| |
| Returns |
| ------- |
| smoothed : (T, J, 3) float32 |
| """ |
| |
| return gaussian_filter1d(poses, sigma=sigma, axis=0).astype(np.float32) |
|
|
|
|
| def resample_poses( |
| poses: np.ndarray, |
| src_fps: float, |
| dst_fps: float, |
| ) -> tuple[np.ndarray, float]: |
| """ |
| Resample poses from src_fps to dst_fps using linear interpolation. |
| |
| Parameters |
| ---------- |
| poses : (T_src, J, 3) |
| src_fps : frames per second of the input |
| dst_fps : desired output frame rate |
| |
| Returns |
| ------- |
| resampled : (T_dst, J, 3) |
| dst_fps : actual output fps (same as input dst_fps) |
| """ |
| if abs(src_fps - dst_fps) < 0.01: |
| return poses, src_fps |
|
|
| T_src = poses.shape[0] |
| duration = (T_src - 1) / src_fps |
| T_dst = max(2, int(round(duration * dst_fps)) + 1) |
|
|
| src_times = np.linspace(0.0, duration, T_src) |
| dst_times = np.linspace(0.0, duration, T_dst) |
|
|
| |
| J = poses.shape[1] |
| out = np.zeros((T_dst, J, 3), dtype=np.float32) |
|
|
| for j in range(J): |
| for c in range(3): |
| out[:, j, c] = np.interp(dst_times, src_times, poses[:, j, c]) |
|
|
| return out, float(dst_fps) |
|
|
|
|
| def centre_trajectory(poses: np.ndarray) -> np.ndarray: |
| """ |
| Translate the entire animation so that the root joint (joint 0 = Hips) |
| starts at XZ origin. Y is left unchanged (height is meaningful). |
| |
| Parameters |
| ---------- |
| poses : (T, J, 3) |
| |
| Returns |
| ------- |
| centred : (T, J, 3) |
| """ |
| out = poses.copy() |
| offset = poses[0, 0, :].copy() |
| offset[1] = 0.0 |
| out -= offset[None, None, :] |
| return out |
|
|
|
|
| def apply_floor( |
| poses: np.ndarray, |
| foot_joints: list[int] | None = None, |
| ) -> np.ndarray: |
| """ |
| Shift the animation vertically so that the lowest foot position sits at Y=0. |
| |
| Parameters |
| ---------- |
| poses : (T, J, 3) in metres, Y-up |
| foot_joints : joint indices treated as feet. |
| Defaults to H36M joints 3, 6 (R-ankle, L-ankle). |
| |
| Returns |
| ------- |
| floored : (T, J, 3) |
| """ |
| if foot_joints is None: |
| foot_joints = [3, 6] |
|
|
| min_y = poses[:, foot_joints, 1].min() |
| if min_y < 0.0: |
| out = poses.copy() |
| out[:, :, 1] -= min_y |
| return out |
|
|
| return poses |
|
|
|
|
| def full_postprocess( |
| poses: np.ndarray, |
| fps: float, |
| *, |
| smooth_sigma: float = 1.5, |
| target_fps: float | None = None, |
| do_centre: bool = True, |
| do_floor: bool = True, |
| ) -> tuple[np.ndarray, float]: |
| """ |
| Apply the full post-processing chain in the recommended order. |
| |
| Parameters |
| ---------- |
| poses : (T, 17, 3) raw MotionBERT output in metres |
| fps : source frame rate |
| smooth_sigma : temporal smoothing strength (0 = off) |
| target_fps : if set, resample to this FPS after smoothing |
| do_centre : translate root start to XZ origin |
| do_floor : push lowest foot to Y=0 |
| |
| Returns |
| ------- |
| (processed_poses, effective_fps) |
| """ |
| p = poses.copy() |
|
|
| if smooth_sigma > 0.0: |
| p = smooth_poses(p, sigma=smooth_sigma) |
|
|
| if target_fps is not None and abs(target_fps - fps) > 0.01: |
| p, fps = resample_poses(p, fps, target_fps) |
|
|
| if do_centre: |
| p = centre_trajectory(p) |
|
|
| if do_floor: |
| p = apply_floor(p) |
|
|
| return p, fps |
|
|