GameMaster-Mocap / utils /postprocess.py
vivekchakraverty's picture
Upload 16 files
fc3ca1b verified
"""
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
"""
# Smooth independently along the time axis (axis 0) for each joint & coord
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)
# Interpolate each joint and coordinate independently
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 # keep vertical component
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] # H36M R-ankle, L-ankle
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