π€AI-generated documentationβ curatedAI Generated
About content generation types
(e.g., docs generated from codebase analysis)
(e.g., livestream β blog post, meeting notes β docs)
(e.g., hand-written tutorial)
Mocap & Skeleton Processing
Mocap processing takes 2D pose detections from multiple camera views, triangulates them into 3D, filters the resulting skeleton, and exports the data. It runs in two contexts: realtime (live, camera-bound) and posthoc (offline, video-bound).
Pose Detectionβ
RTMPose (Primary Detector)β
RTMPose is a top-down pose estimation model from MMPose, running via ONNX Runtime. It's the default skeleton detector for both realtime and posthoc pipelines.
Configuration (RTMPoseDetectorConfig):
model_name: RTMPose model variant (e.g."rtmw-x-l_256x192") β trades off inference time vs detection qualityconfidence_threshold: minimum confidence for a keypoint to be kept (a float, e.g.0.004)
Two deployment modes:
| Mode | Where it runs | Tradeoff |
|---|---|---|
| Centralized GPU (realtime default) | RealtimeSkeletonInferenceNode batches all cameras | One CUDA context, one OnnxSession, batched process_batch, lower latency |
| Inline (per-camera fallback / posthoc) | Each CameraNode/VideoNode runs its own Tracker | Simpler, no inter-camera dependency, higher total GPU memory |
In centralized GPU mode (the realtime default):
- CameraNodes skip skeleton detection entirely (they still run ChArUco on CPU)
RealtimeSkeletonInferenceNodereads raw frames from all camera ring buffers- Calls
tracker.process_batch(images_dict)β one batched SkellyTracker call (one ONNXsession.runper model) for all cameras simultaneously - TensorRT engine compilation on first run (1β3 minutes, cached)
- GPU OOM recovery: catches
MemoryError, rebuilds session (up to 3 retries)
CharUco Detectionβ
Runs alongside skeleton detection to find ChArUco board markers. Used for calibration (validating camera models) and can optionally provide additional 3D reference points during mocap.
Detector Config Flexibilityβ
Posthoc pipelines build a SkellyTracker Tracker from a TrackerConfig / DetectionStageConfig, so they can work with ChArUco (for calibration), MediaPipe, or RTMPose β MediaPipe and RTMPose are both first-class skeleton detectors. The realtime pipeline defaults to RTMPose but can also run MediaPipe (in per-camera mode).
Skeleton Filtering Pipeline (Realtime)β
The realtime filtering + fitting pipeline runs in RealtimeAggregatorNode after triangulation. Processing order:
Raw 3D keypoints (from triangulation; RTMPose names, mm)
β
βΌ
RealtimeKeypointFilter (One Euro smoothing + velocity-decay gap fill)
β Smooths jitter while preserving fast motion; briefly extrapolates
β keypoints that drop out for a few frames
β
βΌ
RealtimePointGate (velocity check)
β Rejects points that "teleport" (velocity exceeds max), so a single
β bad triangulation can't corrupt the skeleton
β
βΌ
RealtimeSkeletonRigidifier (rigid-body correction)
β Maps tracker keypoints β canonical landmarks, estimates each bone's
β length online (best-K-by-reprojection-error median, seeded from
β anthropometry), then enforces it with one closed-form forward pass
β for body + both hands. Output: a rigid, bone-length-consistent skeleton.
β
βΌ
Center of mass + XCoM (per-frame, on the rigidified skeleton; Winter 2009 / Hof 2008)
OneEuroFilterβ
A low-pass filter with adaptive cutoff frequency. It smooths jitter in slow movements while preserving fast motion. Parameters:
min_cutoff: minimum cutoff frequency (Hz)beta: speed coefficient (higher = more responsive to fast motion)d_cutoff: cutoff for velocity estimation
RealtimeKeypointFilter also gap-fills: a keypoint missing for a few frames is extrapolated from its last velocity (decaying to a stop) so the stream doesn't blink during brief dropouts.
RealtimePointGateβ
Detects "teleportation" artifacts β points that jump impossibly far in a single frame. If a point's velocity exceeds the max, it's rejected, preventing single-frame detection errors from corrupting downstream fitting.
RealtimeSkeletonRigidifier (rigid-body correction)β
The rigidifier is the single realtime skeleton constraint β a streaming version of the posthoc rigid-bones step (skellyforge enforce_rigid_bones). Per frame it:
- Maps RTMPose keypoints onto the canonical anatomical model (
canonical_body.yaml/canonical_hand.yamlin skellyforge) via the skellytracker trackerβcanonical mapping. - Updates each bone's online length estimate: a per-bone buffer keeps the best-K measurements ranked by reprojection error (with an age decay, so it never sticks on stale data) and reports their median. Each buffer is seeded from an anthropometric prior (bone-length ratio Γ subject height) until real observations accumulate.
- Runs a single closed-form forward pass from the root (
hips_centerfor the body,wristfor each hand): for each bone it keeps the observed direction but sets the length to the current estimate, placing the child off the already-corrected parent. No iteration, no convergence loop β O(bones) per frame, so it can't get stuck.
Because the enforced length is the estimate (not the noisy per-frame observation), segment lengths are rigid over time and converge to the same medians the posthoc pipeline produces. The center of mass is computed on this rigidified skeleton, matching the posthoc rigid_xyz β CoM flow.
The canonical model is the single source of truth for body topology, bone-length seeds, and β with its segment/COM tables β the center-of-mass calculation.
Triangulationβ
The Triangulator (core/tasks/triangulation/triangulator.py, ~578 lines) is a pure DLT implementation with no anipose dependency:
- Precomputation: On initialization, precomputes extrinsics matrices, rodrigues vectors, camera matrices, and distortion coefficients from the
CalibrationResult - Undistort: Corrects 2D observations using camera intrinsics + distortion coefficients
- DLT: Direct Linear Transform triangulation from 2+ camera views
- Outlier rejection: Subset-ensemble method β tries combinations of camera subsets, rejects points with high reprojection error across all subsets
- Reprojection error gating: Projects 3D points back to each camera, rejects if error exceeds threshold
Performance: All operations are vectorized over P points simultaneously. _batch_outlier_rejection() runs O(combinations) SVD calls, not O(P Γ combinations).
Input formats: Accepts dict[camera_id, ndarray] (single frame), 3D numpy arrays (n_cameras, n_points, 2) for single-frame batches, or 4D numpy arrays (n_cameras, n_frames, n_points, 2) for multi-frame batches.
Posthoc Mocap Taskβ
Orchestrated by run_posthoc_mocap_aggregator_task() in posthoc_mocap_task.py:
- Receives
frame_observationsβ a per-frame list of{camera_id: Observation}collected from all VideoNodes (unified SkellyTrackerObservations) - Builds an
ObservationBufferper camera and accumulates each camera's observations - Resolves the calibration TOML (explicit path wins; otherwise the most-recent successful calibration) and copies it into the recording folder
- Calls
skeleton_from_mediapipe_observation_recorders(detector=β¦, observation_recorders=β¦, β¦)to triangulate the 3D skeleton intooutput_data/ - Writes
tracker_schema.jsonfrom the activeTrackerDefinition(e.g.RTMPOSE_WHOLEBODY_DEFINITION) - Optionally triggers Blender export
Output Artifactsβ
After mocap processing, results are written under the recording folder:
recording_folder/
βββ synchronized_videos/ Input camera videos
βββ annotated_videos/ Videos with detected 2D keypoints drawn on
βββ output_data/ Reconstructed + post-processed 3D data
β βββ *.npy 3D keypoint arrays (frames Γ keypoints Γ 3), CoM, etc.
β βββ *.csv
β βββ *.parquet Primary tabular mocap data store
β βββ per_camera_weights.npy
βββ tracker_schema.json Keypoint names + connections (from the TrackerDefinition)
βββ <calibration>.toml Calibration used for this recording (copied in)
βββ <name>.blend Blender export (if enabled)
For the end-user walkthrough that produces these, see the Post-Hoc Motion Capture guide.
Blender Exportβ
Triggered after mocap processing (or manually via REST API). The export flow:
- Checks required .npy files exist in
output/ - Detects Blender executable (auto-detect or user-specified path)
- Runs Blender in
--backgroundmode with--python run_blender_export.py - Injects
freemocap_blender_addonsite-packages into Blender'ssys.path(no addon installation into Blender's user preferences needed) - Waits for subprocess to complete
- Verifies the
.blendfile was created and is non-empty - Optionally opens the
.blendin Blender GUI viasubprocess.Popen
The Blender addon reads the .npy files and builds a complete 3D scene: skeleton mesh, camera frustums, ground plane, lighting.
Realtime vs Posthoc: Known Differences (V2 Alpha)β
Since the posthoc pipeline was ported from V1 while the realtime pipeline was built fresh for V2, there are implementation differences:
| Aspect | Realtime | Posthoc |
|---|---|---|
| Filtering | One Euro + velocity gate + rigid-body correction | V1 filtering pipeline (being updated) |
| Triangulation | DLT with subset-ensemble outlier rejection | DLT with configurable outlier rejection |
| Detector | RTMPose (default); MediaPipe in per-camera mode | RTMPose or MediaPipe (configurable) |
| Output format | WebSocket streaming | .npy + .parquet files |
These gaps are known and will be closed as the posthoc pipeline is updated to match the realtime pipeline's filtering and triangulation approach. See the Post-Hoc Motion Capture guide for the current user-facing limitations (e.g. the 3D Reconstruction filter settings β Point Gate, One Euro, FABRIK β are not yet passed into posthoc processing).