Skip to main content

The load-data pipeline

The Load FreeMoCap Data panel has three controls: Clear Scene, a recording path, and Load Data. After loading, a Scope Data Parent dropdown appears; multiple recordings can be loaded into one scene and later operators act on whichever data parent is scoped.

The path property self-populates: get_test_recording_path() returns ~/freemocap_data/recordings/freemocap_test_data or .../freemocap_sample_data if either exists. Otherwise point it at a processed recording folder containing an output_data/ directory.

Input files​

Read from output_data/: mediapipe_body_3d_xyz.npy, mediapipe_right_hand_3d_xyz.npy, mediapipe_left_hand_3d_xyz.npy (each hand file falls back to an older-style name, mediapipe_right_hand_right_hand.npy and mediapipe_left_hand_left_hand.npy respectively), mediapipe_face_3d_xyz.npy, and mediapipe_body_total_body_center_of_mass.npy. A *calibration.toml at the recording root is picked up by glob and is optional. All coordinates arrive in millimeters and are divided by 1000 to meters on load. If the calibration metadata carries a groundplane flag, the later gravity-alignment stage is skipped. Trajectory names come from MediapipeTrajectoryNames; constructing the data model with any other data_source raises NotImplementedError.

Stages​

MainController.load_data() runs the stages in a fixed order and times each one. The first group is pure Python (there is a TODO noting it probably belongs in SkellyForge):

StageEffect
load_freemocap_dataLoads the npy files preceding, marks the original_from_file processing stage, sets scene start/end frames, and sets scene framerate
calculate_virtual_trajectoriesAdds derived joint-center trajectories to the body component
put_data_in_inertial_reference_frameRuns put_skeleton_on_ground; skipped entirely when the calibration already recorded an applied groundplane
enforce_rigid_bonesEnforces rigid body constraints on the trajectory data
fix_hand_dataHand-specific cleanup pass
calculate_joint_anglesWrites output_data/joint_angles.csv using the shared joint_angles_definitions
save_data_to_diskWrites the saved_data/ tree (see below)

The Blender phase then builds the scene:

StageEffect
create_emptiesCreates a keyframed empty per body and hand trajectory (face trajectories get none) under an empties_parent
add_rigBuilds the armature (see below) and bakes its animation with nla.bake
save_bone_and_joint_data_from_rigWrites saved_data/<recording>_bone_and_joint_data.csv from the finished rig
attach_rigid_body_mesh_to_rigPer-bone rigid meshes under rigid_body_meshes_parent
attach_skelly_mesh_to_rigAttaches the Skelly mesh, fitted to body dimensions stored in handler metadata
create_center_of_mass_meshCenter of mass sphere driven by its empty (a companion trails helper exists alongside it but is not called by the pipeline)
add_videosLoads the recording's videos (annotated_videos/ preferred, falling back to synchronized_videos/) as image planes under videos_parent
add_capture_camerasRebuilds each calibrated camera (see below)
setup_sceneMaterial-preview shading, hides plumbing empties, deletes the default cube, adds the ground plane
export_3d_modelFBX and BVH into a 3d_models/ subfolder of the recording
save_blender_fileSaves <recording>.blend into the recording folder

Finally the new data parent empty is registered into the addon's UI collection (so it becomes scopeable), the .blend file is saved, and a per-stage timing summary is printed.

Deriving the framerate​

utilities/recording_framerate.py sets the scene rate instead of guessing. It looks for *_timestamps.csv files under synchronized_videos/timestamps/, takes the median of a frame-duration column across at least 10 samples, converts to fps, and rejects results outside a plausible 1 to 1000 fps band. When nothing usable is found it returns None (its docstring explains that silently substituting a plausible-looking number caused the original bug). When a rate is found, the caller sets the scene framerate and stores it in config.reduce_shakiness.recording_fps; when it is None, the configured 30 fps default stays in place and an explicit warning is printed that exported timing and velocity-based smoothing may be incorrect.

Rig construction​

create_rig() uses the BY_BONE method: bone geometry comes from handler metadata bone_data, bone constraints come from declarative definitions in data_models/bones/, and finger tracking constraints are added by default. The rest pose is a TPose, per the addon's own header comment, specifically to make retargeting easier. Symmetry preservation and limit-rotation constraints are config-gated and off by default.

Capture cameras​

add_capture_cameras parses the recording's calibration TOML, detecting camera entries structurally (they must carry matrix, size, and world transform fields) rather than by key-name prefixes. For each camera it places a Blender camera at the calibrated world position, applies orientation plus a 180 degree X flip, converts the pixel-space focal length to millimeters against a fixed 36 mm sensor width, and parents everything under capture_cameras_parent. Scene render resolution is set from the first camera. Each camera gets its matching synchronized_videos clip as a background movie clip, matched by sorted index. The stage exits early with a message when there is no calibration file or the groundplane was never applied.

Ground plane​

The ground plane is sized to double the x/y extent of the center of mass trajectory, with a 10 meter floor. It gets a dark navy checkerboard material. Note that the code constant is square_size_meters = 0.50 even though both the docstring and a print statement in the same function still claim 20 cm squares; trust the constant.