Tutorial 05

February 26, 2026 · View on GitHub

What you will learn

  • Creating SfMData from scratch with pyalicevision (views, intrinsics, poses)
  • The AliceVision camera convention (R_c2w, camera Z = look direction)
  • Building a look-at camera
  • Writing .abc and .json files via sfmDataIO
  • Loading and transforming an existing .abc file
  • Rigid body transformations (rotation, translation, scale)
  • Connecting two nodes via file outputs (node chaining)

Prerequisites

  • Completed Tutorial 01.
  • Meshroom binary release (pyalicevision is bundled, not pip-installable).

Two nodes, one pipeline

This tutorial covers two nodes that work together:

HelloAdvanced_1 ──(.abc)──► HelloTransform_1
(creates the rig)           (applies transformation)

HelloAdvanced generates a ring of synthetic cameras and exports them as .abc/.json files via pyalicevision.

HelloTransform reads the .abc file, applies a rigid body transformation (scale, rotation, translation), and writes the transformed cameras.

Why two nodes? Changing the transformation only recomputes HelloTransform — HelloAdvanced's cache is preserved. This demonstrates how Meshroom nodes communicate through files and how its cache invalidation works.

Part 1: HelloAdvanced — Creating the camera rig

AliceVision camera convention

AliceVision stores camera orientation as a cam-to-world rotation matrix (R_c2w). Its columns are the camera's local axes expressed in world coordinates:

ColumnCamera axisMeaning
0XRight in the image
1YDown in the image
2ZLook direction (towards the scene)

The system is right-handed: cam_x x cam_y = cam_z.

The center is the camera position in world coordinates. Together, (R_c2w, center) fully describe a camera's extrinsic pose.

SfMData structure

An SfMData object groups cameras into three collections:

CollectionWhat it stores
ViewsImage metadata (path, width, height, links to intrinsic and pose)
IntrinsicsCamera optical models (focal length, principal point, sensor size)
PosesCamera positions and orientations (Pose3 = rotation + center)

HelloAdvanced creates all three from scratch — no input file needed. All cameras share a single intrinsic (same focal length and sensor).

Creating an empty SfMData and adding an intrinsic

from pyalicevision import sfmData as sfmDataModule
from pyalicevision import camera

sfm = sfmDataModule.SfMData()

focal_px = focal_mm * image_width / sensor_width
pinhole = camera.Pinhole(image_width, image_height, focal_px, focal_px,
                         image_width / 2.0, image_height / 2.0)
sfm.getIntrinsics()[0] = pinhole

Adding a View

view = sfmDataModule.View(
    "",            # image path (empty for synthetic cameras)
    view_id,       # unique view identifier
    intrinsic_id,  # index into getIntrinsics()
    pose_id,       # index into getPoses()
    image_width,
    image_height,
)
sfm.getViews()[view_id] = view

Look-at construction and adding a CameraPose

To point a camera at the origin from position center, we build R_c2w — a 3x3 matrix whose columns are the camera's local axes in world coordinates:

cam_z = -center / np.linalg.norm(center)     # Z = look direction (towards origin)
cam_x = np.cross(cam_z, up)                  # X = right in image
cam_x = cam_x / np.linalg.norm(cam_x)
cam_y = np.cross(cam_z, cam_x)              # Y = completes right-handed frame

R_c2w = np.column_stack([cam_x, cam_y, cam_z])

Where up = [0, 1, 0] (world Y-up).

To pass this to pyalicevision's Pose3, we transpose to R_w2c. This is necessary because the SWIG Eigen typemap converts between NumPy's row-major layout and Eigen's column-major layout, effectively transposing the matrix:

R_w2c = R_c2w.T

pose3 = geometry.Pose3(
    np.ascontiguousarray(R_w2c, dtype=np.float64),
    np.ascontiguousarray(center, dtype=np.float64),
)
cam_pose = sfmDataModule.CameraPose(pose3)
sfm.getPoses()[pose_id] = cam_pose

Arrays must be float64 and C-contiguous (the SWIG typemap rejects other formats).

Saving cameras to .abc and .json

from pyalicevision import sfmDataIO

sfmDataIO.save(sfm, "sfm.abc", sfmDataIO.ALL)       # Alembic (binary, for 3D viewer)
sfmDataIO.save(sfm, "cameras.json", sfmDataIO.ALL)   # JSON (human-readable)

The format is determined by the file extension. Both contain the same data.

Part 2: HelloTransform — Transforming SfMData

Loading an .abc file

HelloTransform receives the .abc path from HelloAdvanced's output. Loading creates an SfMData object populated with the saved data:

sfm = sfmDataModule.SfMData()
sfmDataIO.load(sfm, input_path, sfmDataIO.ALL)

Extracting poses

Poses are accessed through the same API used for creation. Due to the SWIG row/column-major conversion, rotation() returns R_w2c (the transpose of the stored R_c2w):

poses = sfm.getPoses()
for pose_id in poses.keys():
    cam_pose = poses[pose_id]
    pose3 = cam_pose.getTransform()
    R_w2c = np.array(pose3.rotation(), dtype=np.float64).reshape(3, 3)
    center = np.array(pose3.center(), dtype=np.float64).flatten()

Building the rotation matrix

Three helper functions (_rot_x, _rot_y, _rot_z) build 3x3 rotation matrices for each axis. They are combined in standard Euler order:

R_rig = Rz @ Ry @ Rx

Applying the transformation

The transformation order is scale -> rotate -> translate.

Camera centers (3D points in world coordinates):

new_center = R_rig @ (scale * center) + translation

Camera rotations (R_w2c). The rig rotation changes the world frame, so R_w2c must compensate:

new_R_w2c = R_w2c @ R_rig.T

Saving

The transformed poses are written back into the SfMData and saved:

new_pose3 = geometry.Pose3(
    np.ascontiguousarray(new_R_w2c, dtype=np.float64),
    np.ascontiguousarray(new_center, dtype=np.float64),
)
cam_pose.setTransform(new_pose3)

Then sfmDataIO.save() writes .abc and .json.

How to test it

A pre-configured project is available: open meshroom/tuto05-hello-advanced.mg in Meshroom (make sure you have run scripts/setup_projects.sh first). Or set it up manually:

  1. Add a HelloAdvanced node to your graph.
  2. Add a HelloTransform node.
  3. Connect HelloAdvanced's SfMData output to HelloTransform's Input SfMData input.
  4. Right-click on HelloAdvanced and select Compute to run it alone.
  5. Double-click on HelloAdvanced to display the generated cameras in the 3D viewer. You should see camera frustums arranged in a circle on the XZ plane, all pointing towards the origin.

Camera rig before transformation

  1. Now right-click on HelloTransform and select Compute. You can change the transformation parameters first (e.g., rotateX = 45).
  2. Double-click on HelloTransform to see the transformed cameras in the 3D viewer. Only HelloTransform has recomputed — HelloAdvanced's cache stays valid.

Camera rig after transformation