multimoda-rs: Intravascular Module – Notebook Tutorial

This notebook follows the Intravascular Alignment Tutorial step-by-step:

  1. General note on segmentation preparation

  2. Workflow from CSV files (AIVUS-CAA format)

  3. Workflow from numpy arrays — including parameter fine-tuning

  4. Alignment with a CCTA centerline

  5. Saving geometries as .obj files

  6. Utility functions: to_array and numpy_to_geometry

  7. Class-level methods for PyContour, PyFrame, and PyGeometry

Performance note: Alignment is compute-intensive. Running the notebook via the console is faster than interactive cell-by-cell execution.

1. General note on segmentation preparation

The package works with segmented intravascular images from IVUS or OCT. Segmentation must follow specific conventions for optimal results:

  • Files follow AIVUS-CAA convention: diastolic_contours.csv, systolic_contours.csv, diastolic_reference_points.csv, systolic_reference_points.csv.

  • Each file has columns (frame_index, x_mm, y_mm, z_mm)no header row.

  • A correct anatomical reference point is essential for downstream centerline co-registration.

For CAD a bifurcation serves as reference; for AAOCA the reference is placed at the aortic side of the most proximal fully closed lumen contour (see tutorial for details).

Four processing modes

Mode

Inputs

Typical use-case

full

4 states (rest-dia, rest-sys, stress-dia, stress-sys)

AAOCA pulsatile lumen analysis

doublepair

2 independent pullbacks, each with dia+sys

Pre/post-stenting across both phases

singlepair

1 pullback with dia+sys

CAD: diastole vs. systole within one pullback

single

1 pullback (dia or sys only)

Standalone reconstruction (CAD, OCT)

import os
from pathlib import Path
import numpy as np
import multimodars as mm

cwd = Path.cwd()
for candidate in [cwd, cwd.parent, cwd.parent.parent]:
    if (candidate / "examples" / "data").exists():
        os.chdir(candidate / "examples" / "data")
        break
    elif (candidate / "data").exists():
        os.chdir(candidate / "data")
        break
print(f"Working directory: {os.getcwd()}")
Working directory: /mnt/c/WorkingData/Documents/2_Coding/Rust/multimodars/examples/data
Note: you may need to restart the kernel to use updated packages.

2. Workflow from CSV files

This section uses AIVUS-CAA CSV files. from_file_full processes all four states simultaneously (rest-diastole, rest-systole, stress-diastole, stress-systole) and writes aligned meshes to four output directories.

See the tutorial for a detailed description of the CSV format.

# Full 4-state workflow: rest vs. stress, diastole vs. systole
rest, stress, dia, sys, _ = mm.from_file_full(
    input_path_ab="ivus_rest",
    input_path_cd="ivus_stress",
    labels=["rest_dia", "rest_sys", "stress_dia", "stress_sys"],
    step_rotation_deg=0.1,
    range_rotation_deg=90,
    image_center=(4.5, 4.5),
    radius=0.5,
    n_points=20,
    write_obj=True,
    watertight=False,
    contour_types=[mm.PyContourType.Lumen, mm.PyContourType.Catheter, mm.PyContourType.Wall],
    output_path_ab="output/rest",
    output_path_cd="output/stress",
    output_path_ac="output/diastole",
    output_path_bd="output/systole",
    interpolation_steps=28,
)
✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: rest_dia
Diastole phase: Yes


✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: rest_sys
Diastole phase: No


✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: stress_dia
Diastole phase: Yes


✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: stress_sys
Diastole phase: No


+--------------------------------------------------------------------+
|          ✅ Finished aligning 'rest_sys' (anomalous: true)          |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | -2.30         | -0.19 | -0.01 | (3.63,5.12) |
| 2       | 1          | 12.20         | -0.54 | 0.15  | (3.63,5.12) |
| 3       | 2          | -10.30        | -0.44 | 0.53  | (3.63,5.12) |
| 4       | 3          | -4.10         | -0.72 | 0.94  | (3.63,5.12) |
| 5       | 4          | 0.80          | -0.75 | 0.26  | (3.63,5.12) |
| 6       | 5          | 5.10          | -0.79 | 0.65  | (3.63,5.12) |
| 7       | 6          | -6.50         | -1.21 | 0.69  | (3.63,5.12) |
| 8       | 7          | 14.70         | -1.08 | 0.81  | (3.63,5.12) |
| 9       | 8          | -35.30        | -1.03 | 0.24  | (3.63,5.12) |
| 10      | 9          | 25.50         | -1.46 | 0.28  | (3.63,5.12) |
| 11      | 10         | 41.30         | -2.40 | 0.73  | (3.63,5.12) |
| 12      | 11         | -21.20        | -1.95 | 0.68  | (3.63,5.12) |
| 13      | 12         | 4.80          | -2.06 | 0.66  | (3.63,5.12) |
| 14      | 13         | -6.90         | -2.14 | 0.52  | (3.63,5.12) |
| 15      | 14         | 23.40         | -2.27 | 0.46  | (3.63,5.12) |
| 16      | 15         | 7.30          | -2.28 | 0.53  | (3.63,5.12) |
+---------+------------+---------------+-------+-------+-------------+

+--------------------------------------------------------------------+
|         ✅ Finished aligning 'stress_dia' (anomalous: true)         |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | 3.80          | -0.10 | 0.02  | (3.60,4.59) |
| 2       | 1          | 6.20          | -0.38 | 0.40  | (3.60,4.59) |
| 3       | 2          | 1.90          | -0.24 | 0.42  | (3.60,4.59) |
| 4       | 3          | -1.60         | -0.11 | 0.49  | (3.60,4.59) |
| 5       | 4          | -1.90         | -0.22 | 0.50  | (3.60,4.59) |
| 6       | 5          | 0.70          | -0.03 | 0.10  | (3.60,4.59) |
| 7       | 6          | 3.20          | -0.05 | 0.09  | (3.60,4.59) |
| 8       | 7          | 0.80          | -0.11 | -0.09 | (3.60,4.59) |
| 9       | 8          | 2.40          | -0.14 | -0.13 | (3.60,4.59) |
| 10      | 9          | 14.60         | -0.75 | 0.01  | (3.60,4.59) |
| 11      | 10         | 0.00          | -1.08 | 0.08  | (3.60,4.59) |
| 12      | 11         | 8.80          | -1.21 | 0.08  | (3.60,4.59) |
| 13      | 12         | 11.80         | -1.28 | 0.20  | (3.60,4.59) |
| 14      | 13         | -14.20        | -1.46 | -0.02 | (3.60,4.59) |
| 15      | 14         | 22.50         | -1.55 | 0.29  | (3.60,4.59) |
| 16      | 15         | -13.20        | -1.72 | 0.16  | (3.60,4.59) |
| 17      | 16         | -18.90        | -1.61 | -0.27 | (3.60,4.59) |
| 18      | 17         | -2.70         | -1.41 | -0.44 | (3.60,4.59) |
| 19      | 18         | 7.50          | -1.74 | -0.31 | (3.60,4.59) |
| 20      | 19         | 10.70         | -1.77 | -0.55 | (3.60,4.59) |
| 21      | 20         | 18.00         | -2.30 | -0.37 | (3.60,4.59) |
| 22      | 21         | 0.00          | -2.04 | -0.25 | (3.60,4.59) |
| 23      | 22         | 24.70         | -2.34 | 0.36  | (3.60,4.59) |
| 24      | 23         | 4.10          | -2.29 | 0.78  | (3.60,4.59) |
+---------+------------+---------------+-------+-------+-------------+

+--------------------------------------------------------------------+
|          ✅ Finished aligning 'rest_dia' (anomalous: true)          |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | 7.70          | 0.12  | -0.36 | (3.72,5.25) |
| 2       | 1          | 0.70          | 0.04  | -0.16 | (3.72,5.25) |
| 3       | 2          | 6.20          | -0.28 | 0.03  | (3.72,5.25) |
| 4       | 3          | -13.10        | -0.16 | 0.45  | (3.72,5.25) |
| 5       | 4          | -23.90        | -0.32 | 0.89  | (3.72,5.25) |
| 6       | 5          | -5.10         | -0.48 | 1.10  | (3.72,5.25) |
| 7       | 6          | -29.80        | -0.94 | 0.82  | (3.72,5.25) |
| 8       | 7          | -18.00        | -1.29 | 0.87  | (3.72,5.25) |
| 9       | 8          | 1.50          | -1.36 | 0.86  | (3.72,5.25) |
| 10      | 9          | -1.20         | -1.68 | 1.27  | (3.72,5.25) |
| 11      | 10         | 47.40         | -1.54 | 1.87  | (3.72,5.25) |
| 12      | 11         | 3.30          | -1.45 | 1.95  | (3.72,5.25) |
| 13      | 12         | 1.00          | -1.51 | 2.08  | (3.72,5.25) |
| 14      | 13         | 30.60         | -0.96 | 2.12  | (3.72,5.25) |
| 15      | 14         | -7.00         | -1.11 | 1.93  | (3.72,5.25) |
| 16      | 15         | 19.40         | -0.68 | 2
sidebranch file not found, skipping: "ivus_rest/branch_diastolic_contours.csv"
process_directory: unknown mapping name 'catheter', skipping
eem file not found, skipping: "ivus_rest/eem_diastolic_contours.csv"
calcification file not found, skipping: "ivus_rest/calcium_diastolic_contours.csv"
process_directory: unknown mapping name 'catheter', skipping
calcification file not found, skipping: "ivus_rest/calcium_systolic_contours.csv"
eem file not found, skipping: "ivus_rest/eem_systolic_contours.csv"
sidebranch file not found, skipping: "ivus_rest/branch_systolic_contours.csv"
sidebranch file not found, skipping: "ivus_stress/branch_diastolic_contours.csv"
calcification file not found, skipping: "ivus_stress/calcium_diastolic_contours.csv"
eem file not found, skipping: "ivus_stress/eem_diastolic_contours.csv"
process_directory: unknown mapping name 'catheter', skipping
sidebranch file not found, skipping: "ivus_stress/branch_systolic_contours.csv"
calcification file not found, skipping: "ivus_stress/calcium_systolic_contours.csv"
process_directory: unknown mapping name 'catheter', skipping
eem file not found, skipping: "ivus_stress/eem_systolic_contours.csv"
.47  | (3.72,5.25) |
| 17      | 16         | 3.40          | -0.50 | 2.35  | (3.72,5.25) |
| 18      | 17         | -0.90         | -0.58 | 2.24  | (3.72,5.25) |
| 19      | 18         | -11.00        | -0.95 | 2.37  | (3.72,5.25) |
+---------+------------+---------------+-------+-------+-------------+

+--------------------------------------------------------------------+
|         ✅ Finished aligning 'stress_sys' (anomalous: true)         |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | 0.00          | -0.22 | 0.10  | (3.80,4.64) |
| 2       | 1          | 1.00          | -0.19 | 0.14  | (3.80,4.64) |
| 3       | 2          | 0.00          | -0.04 | 0.17  | (3.80,4.64) |
| 4       | 3          | -1.20         | 0.08  | 0.15  | (3.80,4.64) |
| 5       | 4          | -1.10         | 0.03  | 0.09  | (3.80,4.64) |
| 6       | 5          | 0.20          | 0.06  | 0.06  | (3.80,4.64) |
| 7       | 6          | 7.30          | 0.06  | 0.24  | (3.80,4.64) |
| 8       | 7          | 2.60          | -0.30 | 0.00  | (3.80,4.64) |
| 9       | 8          | 11.50         | -0.32 | -0.18 | (3.80,4.64) |
| 10      | 9          | 9.50          | -0.57 | -0.32 | (3.80,4.64) |
| 11      | 10         | -1.00         | -0.53 | -0.36 | (3.80,4.64) |
| 12      | 11         | 3.40          | -0.49 | -0.39 | (3.80,4.64) |
| 13      | 12         | -4.10         | -0.52 | -0.46 | (3.80,4.64) |
| 14      | 13         | 8.40          | -0.63 | -0.37 | (3.80,4.64) |
| 15      | 14         | -2.10         | -0.76 | -0.43 | (3.80,4.64) |
| 16      | 15         | 8.50          | -0.79 | -0.41 | (3.80,4.64) |
| 17      | 16         | -6.00         | -0.81 | -0.81 | (3.80,4.64) |
| 18      | 17         | 0.00          | -0.83 | -1.10 | (3.80,4.64) |
| 19      | 18         | 0.40          | -0.82 | -1.12 | (3.80,4.64) |
| 20      | 19         | 3.20          | -0.89 | -1.13 | (3.80,4.64) |
| 21      | 20         | 9.50          | -0.96 | -1.14 | (3.80,4.64) |
+---------+------------+---------------+-------+-------+-------------+

✅ Aligned geometry 'stress_sys' to 'stress_dia'
-----------------------------------------
Applied initial translation: (-0.20, -0.05, 0.00) mm
Found best rotation of 2.70° with parameters: 
range: 90.00° 
step size: 0.1°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

✅ Aligned geometry 'rest_sys' to 'rest_dia'
-----------------------------------------
Applied initial translation: (0.09, 0.13, -3.94) mm
Found best rotation of 3.00° with parameters: 
range: 90.00° 
step size: 0.1°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

✅ Aligned geometry 'stress_sys' to 'rest_sys'
-----------------------------------------
Applied initial translation: (0.12, 0.66, 0.00) mm
Found best rotation of 1.80° with parameters: 
range: 90.00° 
step size: 0.1°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

✅ Aligned geometry 'stress_dia' to 'rest_dia'
-----------------------------------------
Applied initial translation: (0.12, 0.66, 0.00) mm
Found best rotation of 17.80° with parameters: 
range: 90.00° 
step size: 0.1°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

Saving files for 'rest_dia - rest_sys' to 'output/rest'
LUMEN .obj files: 30/30 written successfully
CATHETER .obj files: 30/30 written successfully
WALL .obj files: 30/30 written successfully

Saving files for 'stress_dia - stress_sys' to 'output/stress'
LUMEN .obj files: 30/30 written successfully
CATHETER .obj files: 30/30 written successfully
WALL .obj files: 30/30 written successfully

Saving files for 'rest_dia - stress_dia' to 'output/diastole'
LUMEN .obj files: 30/30 written successfully
CATHETER .obj files: 30/30 written successfully
WALL .obj files: 30/30 written successfully

Saving files for 'rest_sys - stress_sys' to 'output/systole'
LUMEN .obj files: 30/30 written successfully
CATHETER .obj files: 30/30 written successfully
WALL .obj files: 30/30 written successfully
# Build "before" meshes from raw CSV, then write explicit .obj files for comparison
rest_dia_raw = np.genfromtxt("ivus_rest/diastolic_contours.csv")
rest_dia_ref = np.genfromtxt("ivus_rest/diastolic_reference_points.csv")
rest_sys_raw = np.genfromtxt("ivus_rest/systolic_contours.csv")
rest_sys_ref = np.genfromtxt("ivus_rest/systolic_reference_points.csv")

rest_dia_before = mm.numpy_to_geometry(
    lumen_arr=rest_dia_raw, eem_arr=np.array([]), catheter_arr=np.array([]),
    wall_arr=np.array([]), reference_arr=rest_dia_ref, label="dia_before",
)
rest_sys_before = mm.numpy_to_geometry(
    lumen_arr=rest_sys_raw, eem_arr=np.array([]), catheter_arr=np.array([]),
    wall_arr=np.array([]), reference_arr=rest_sys_ref, label="sys_before",
)
rest_dia_before.frames = np.array([f.sort_frame_points() for f in rest_dia_before.frames])
rest_sys_before.frames = np.array([f.sort_frame_points() for f in rest_sys_before.frames])

# Write before (unprocessed) and after (aligned) meshes with explicit names
mm.to_obj(rest_dia_before, "output/before", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="dia")
mm.to_obj(rest_sys_before, "output/before", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="sys")
mm.to_obj(rest.geom_a, "output/after", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="dia")
mm.to_obj(rest.geom_b, "output/after", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="sys")
Successfully wrote lumen to output/before/dia_lumen.obj
Successfully wrote lumen to output/before/sys_lumen.obj
Successfully wrote lumen to output/after/dia_lumen.obj
Successfully wrote lumen to output/after/sys_lumen.obj
plot_pair(
    before_paths=["output/before/dia_lumen.obj", "output/before/sys_lumen.obj"],
    after_paths=["output/after/dia_lumen.obj", "output/after/sys_lumen.obj"],
    colors=["royalblue", "firebrick"],
    titles=["Before Processing", "After Processing"],
)

The data is now neatly ordered in pairs (diastolic and systolic geometry). Every geometry contains contours for lumen, wall, and a synthetic catheter. The reference point is used for centerline co-registration. All points belonging to a contour are stored in a PyContour struct.

print(f"PyGeometryPair:\n{rest}")
print(f"PyGeometry:\n{rest.geom_a}")
print(f"PyFrame:\n{rest.geom_a.frames[0]}")
print(f"PyContour:\n{rest.geom_a.frames[0].lumen}")
print(f"Extras:\n{rest.geom_a.frames[0].extras}")
print(f"PyContourPoint:\n{rest.geom_a.frames[0].lumen.points[0]}")
PyGeometryPair:
GeometryPair rest_dia - rest_sys (diastolic: 14 frames, systolic: 14 frames)
PyGeometry:
Geometry(14 frames, label='rest_dia')
PyFrame:
Frame(id=0, centroid=(3.72, 5.25, 0.00), lumen=Contour(id=0, frame=385, points=501, centroid=(3.72, 5.25, 0.00), kind=Lumen), extras=2)
PyContour:
Contour(id=0, frame=385, points=501, centroid=(3.72, 5.25, 0.00), kind=Lumen)
Extras:
{'Catheter': Contour(id=0, frame=385, points=20, centroid=(3.84, 6.33, 0.00), kind=Catheter), 'Wall': Contour(id=0, frame=385, points=501, centroid=(3.72, 5.25, 0.00), kind=Wall)}
PyContourPoint:
Point(frame_id=19, pt_id=0, x=3.80, y=7.90, z=0.00, aortic=false)

Different stenosis measurements can be obtained directly from the objects:

# Summary over PyGeometryPair
summary_pair, deform_table = rest.get_summary()
print(f"PyGeometryPair summary (dia_geom: (mla [mm²], max. stenosis, stenosis length [mm])):\n{summary_pair}")
print(f"Deformation table:\n{np.array(deform_table)}")

# Summary over PyGeometry
print(f"\nPyGeometry summary:\n{rest.geom_a.get_summary()[0]}")

# Per-contour measurements
print(f"\nContour area [mm²]: {rest.geom_a.frames[0].lumen.get_area():.4f}")
print(f"Elliptic ratio:      {rest.geom_a.frames[-1].lumen.get_elliptic_ratio():.4f}")
+----+----------+-----------+----------+-----------+-------+
| id | area_dia | ellip_dia | area_sys | ellip_sys |   z   |
+----+----------+-----------+----------+-----------+-------+
PyGeometryPair summary (dia_geom: (mla [mm²], max. stenosis, stenosis length [mm])):
((5.559206007496853, 0.6780775439844883, 10.364606842105268), (6.111481670202526, 0.663038559502465, 7.7734551315789515))
Deformation table:
[[ 0.          5.63662776  4.58651358  6.11148167  4.35376487  0.        ]
 [ 1.          5.79662471  4.75220339  6.25936689  3.38956113  1.29557586]
 [ 2.          5.70440311  3.69759593  6.49106359  2.85326248  2.59115171]
 [ 3.          5.55920601  2.56932308  6.84671006  2.17905332  3.88672757]
 [ 4.          5.5883072   1.63583386  7.59416089  1.90836576  5.18230342]
 [ 5.          6.41743783  1.50724622  7.57484592  1.67549107  6.47787928]
 [ 6.          7.25510456  1.36697234  7.3074273   1.63078837  7.77345513]
 [ 7.          7.74398201  1.24613112 10.23430197  1.32956583  9.06903099]
 [ 8.          7.77347285  1.43038361 11.90101937  1.19922679 10.36460684]
 [ 9.          9.05387612  1.32700846 14.15414345  1.08910895 11.6601827 ]
 [10.         11.91520903  1.18319293 11.7040278   1.17176063 12.95575855]
 [11.         15.27166328  1.08294811 13.56639023  1.14517785 14.25133441]
 [12.         17.26877359  1.05840795 15.42409292  1.11280753 15.54691026]
 [13.         16.34175131  1.0407399  18.13703568  1.06629346 16.84248612]]
| 0  | 5.64     | 4.59      | 6.11     | 4.35      | 0.00  |
| 1  | 5.80     | 4.75      | 6.26     | 3.39      | 1.30  |
| 2  | 5.70     | 3.70      | 6.49     | 2.85      | 2.59  |
| 3  | 5.56     | 2.57      | 6.85     | 2.18      | 3.89  |
| 4  | 5.59     | 1.64      | 7.59     | 1.91      | 5.18  |
| 5  | 6.42     | 1.51      | 7.57     | 1.68      | 6.48  |
| 6  | 7.26     | 1.37      | 7.31     | 1.63      | 7.77  |
| 7  | 7.74     | 1.25      | 10.23    | 1.33      | 9.07  |
| 8  | 7.77     | 1.43      | 11.90    | 1.20      | 10.36 |
| 9  | 9.05     | 1.33      | 14.15    | 1.09      | 11.66 |
| 10 | 11.92    | 1.18      | 11.70    | 1.17      | 12.96 |
| 11 | 15.27    | 1.08      | 13.57    | 1.15      | 14.25 |
| 12 | 17.27    | 1.06      | 15.42    | 1.11      | 15.55 |
| 13 | 16.34    | 1.04      | 18.14    | 1.07      | 16.84 |
+----+----------+-----------+----------+-----------+-------+

PyGeometry summary:
5.559206007496853

Contour area [mm²]: 5.6366
Elliptic ratio:      1.0407

The four pairs represent all four possible comparisons in gated images — particularly relevant for coronary artery anomalies (AAOCA): rest pulsatile, stress pulsatile, stress-induced diastolic, and stress-induced systolic lumen deformation.

The interpolation_steps parameter generates intermediate meshes between paired states, useful for deformation animations in Blender. Set interpolation_steps=0 to skip interpolation.

Coronary Artery Disease

from_file_single reconstructs a single pullback with all contour layers (lumen, EEM, wall, catheter):

cad, _ = mm.from_file_single(
    input_path="ivus_full",
    diastole=True,
    step_rotation_deg=0.1,
    range_rotation_deg=90,
    image_center=(4.5, 4.5),
    radius=0.5,
    n_points=20,
    write_obj=True,
    watertight=False,
    contour_types=[
        mm.PyContourType.Lumen, mm.PyContourType.Eem,
        mm.PyContourType.Catheter, mm.PyContourType.Wall,
    ],
    output_path="output/cad",
)
# Centre coordinate system on EEM and write both versions
cad_aligned = cad.center_to_contour(mm.PyContourType.Eem)
mm.to_obj(cad_aligned, "output/cad", watertight=False,
          contour_types=[mm.PyContourType.Lumen, mm.PyContourType.Eem, mm.PyContourType.Wall],
          filename_prefix="aligned")

# from_file_single writes files as {contour_type}_{input_path_name}.obj
lumen = trimesh.load("output/cad/lumen_ivus_full.obj")
eem   = trimesh.load("output/cad/eem_ivus_full.obj")
wall  = trimesh.load("output/cad/wall_ivus_full.obj")
lumen_a = trimesh.load("output/cad/aligned_lumen.obj")
eem_a   = trimesh.load("output/cad/aligned_eem.obj")
wall_a  = trimesh.load("output/cad/aligned_wall.obj")

fig = make_subplots(
    rows=1, cols=2,
    specs=[[{"type": "scene"}, {"type": "scene"}]],
    subplot_titles=("CAD (lumen-centred)", "CAD (EEM-centred)"),
)
camera = dict(eye=dict(x=1.5, y=1.5, z=1.0))
for t in [trimesh_to_mesh3d(lumen, "firebrick", "Lumen", 1.0),
          trimesh_to_mesh3d(eem,   "royalblue", "EEM",   0.6),
          trimesh_to_mesh3d(wall,  "white",     "Wall",  0.5)]:
    fig.add_trace(t, row=1, col=1)
for t in [trimesh_to_mesh3d(lumen_a, "firebrick", "Lumen", 1.0),
          trimesh_to_mesh3d(eem_a,   "royalblue", "EEM",   0.6),
          trimesh_to_mesh3d(wall_a,  "white",     "Wall",  0.5)]:
    fig.add_trace(t, row=1, col=2)
fig.update_layout(
    width=900, height=450,
    scene=dict(camera=camera, aspectmode="data"),
    scene2=dict(camera=camera, aspectmode="data"),
    margin=dict(l=0, r=0, t=40, b=0),
)
fig.show()
✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
✅ Eem
✅ Calcification
✅ Sidebranch
✅ Catheter
-----------------------------------------
Label: ivus_full
Diastole phase: Yes


+--------------------------------------------------------------------+
|         ✅ Finished aligning 'ivus_full' (anomalous: false)         |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | 0.00          | 0.04  | -0.07 | (4.38,4.06) |
| 2       | 1          | -0.60         | 0.11  | -0.15 | (4.38,4.06) |
| 3       | 2          | 19.20         | 0.04  | -0.24 | (4.38,4.06) |
| 4       | 3          | 90.00         | -0.12 | -0.46 | (4.38,4.06) |
| 5       | 4          | 5.30          | -0.24 | -0.36 | (4.38,4.06) |
| 6       | 5          | -36.00        | -0.16 | -0.44 | (4.38,4.06) |
| 7       | 6          | 18.00         | -0.21 | -0.50 | (4.38,4.06) |
| 8       | 7          | 2.70          | -0.12 | -0.52 | (4.38,4.06) |
| 9       | 8          | -18.00        | -0.08 | -0.46 | (4.38,4.06) |
| 10      | 9          | 0.00          | 0.02  | -0.39 | (4.38,4.06) |
| 11      | 10         | 0.00          | -0.06 | -0.42 | (4.38,4.06) |
| 12      | 11         | -90.00        | -0.14 | -0.34 | (4.38,4.06) |
| 13      | 12         | 69.40         | 0.05  | -0.52 | (4.38,4.06) |
| 14      | 13         | 0.00          | 0.29  | -0.53 | (4.38,4.06) |
| 15      | 14         | -5.90         | 0.32  | -0.51 | (4.38,4.06) |
| 16      | 15         | -12.30        | 0.36  | -0.50 | (4.38,4.06) |
| 17      | 16         | -6.90         | 0.35  | -0.36 | (4.38,4.06) |
| 18      | 17         | 0.00          | 0.38  | -0.40 | (4.38,4.06) |
| 19      | 18         | -8.60         | 0.35  | -0.35 | (4.38,4.06) |
| 20      | 19         | -0.80         | 0.43  | -0.35 | (4.38,4.06) |
| 21      | 20         | -19.60        | 0.35  | -0.23 | (4.38,4.06) |
| 22      | 21         | -10.50        | 0.39  | -0.05 | (4.38,4.06) |
| 23      | 22         | -3.70         | 0.31  | 0.03  | (4.38,4.06) |
| 24     
process_directory: unknown mapping name 'catheter', skipping
 | 23         | -2.20         | 0.23  | 0.04  | (4.38,4.06) |
| 25      | 24         | 0.00          | 0.14  | -0.00 | (4.38,4.06) |
| 26      | 25         | 0.00          | 0.03  | -0.09 | (4.38,4.06) |
| 27      | 26         | -18.00        | -0.06 | -0.16 | (4.38,4.06) |
| 28      | 27         | 17.00         | -0.05 | -0.14 | (4.38,4.06) |
| 29      | 28         | 0.00          | -0.01 | -0.06 | (4.38,4.06) |
| 30      | 29         | 18.00         | 0.08  | -0.02 | (4.38,4.06) |
| 31      | 30         | 89.40         | 0.20  | -0.59 | (4.38,4.06) |
+---------+------------+---------------+-------+-------+-------------+
Successfully wrote OBJ files for geometry ivus_full to output/cad
Successfully wrote lumen to output/cad/aligned_lumen.obj
Successfully wrote eem to output/cad/aligned_eem.obj
Successfully wrote wall to output/cad/aligned_wall.obj

Pre- vs. Post-stenting

For pre/post-stenting comparison across both cardiac phases, from_file_full is used with the pre-stent pullback as input_path_ab and the post-stent pullback as input_path_cd.

prestent, poststent, dia_comp, sys_comp, _ = mm.from_file_full(
    input_path_ab="ivus_prestent",
    input_path_cd="ivus_poststent",
    step_rotation_deg=0.1,
    range_rotation_deg=45,
    watertight=False,
    output_path_ab="output/stent_rest",
    output_path_cd="output/stent_stress",
    output_path_ac="output/stent_diastole",
    output_path_bd="output/stent_systole",
    interpolation_steps=0,
)
# Write comparison geometries with explicit names
mm.to_obj(dia_comp.geom_a, "output/stent_vis", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="prestent")
mm.to_obj(dia_comp.geom_b, "output/stent_vis", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="poststent")

mesh_pre  = trimesh.load("output/stent_vis/prestent_lumen.obj")
mesh_post = trimesh.load("output/stent_vis/poststent_lumen.obj")

fig = go.Figure(data=[
    trimesh_to_mesh3d(mesh_pre,  "royalblue", "Pre-stent (diastole)"),
    trimesh_to_mesh3d(mesh_post, "firebrick", "Post-stent (diastole)"),
])
fig.update_layout(
    title="Pre- vs. Post-stenting: diastolic lumen comparison",
    scene=dict(aspectmode="data"),
    margin=dict(l=0, r=0, t=40, b=0),
)
fig.show()
eem file not found, skipping: "ivus_prestent/eem_diastolic_contours.csv"
sidebranch file not found, skipping: "ivus_prestent/branch_diastolic_contours.csv"
calcification file not found, skipping: "ivus_prestent/calcium_diastolic_contours.csv"
process_directory: unknown mapping name 'catheter', skipping
sidebranch file not found, skipping: "ivus_prestent/branch_systolic_contours.csv"
process_directory: unknown mapping name 'catheter', skipping
calcification file not found, skipping: "ivus_prestent/calcium_systolic_contours.csv"
eem file not found, skipping: "ivus_prestent/eem_systolic_contours.csv"
sidebranch file not found, skipping: "ivus_poststent/branch_diastolic_contours.csv"
eem file not found, skipping: "ivus_poststent/eem_diastolic_contours.csv"
calcification file not found, skipping: "ivus_poststent/calcium_diastolic_contours.csv"
process_directory: unknown mapping name 'catheter', skipping
eem file not found, skipping: "ivus_poststent/eem_systolic_contours.csv"
sidebranch file not found, skipping: "ivus_poststent/branch_systolic_contours.csv"
calcification file not found, skipping: "ivus_poststent/calcium_systolic_contours.csv"
process_directory: unknown mapping name 'catheter', skipping
✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: ivus_prestent
Diastole phase: Yes


✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: ivus_prestent
Diastole phase: No


✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: ivus_poststent
Diastole phase: Yes


✅ Successfully built geometry from path
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: ivus_poststent
Diastole phase: No


+--------------------------------------------------------------------+
|       ✅ Finished aligning 'ivus_prestent' (anomalous: true)        |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | -11.70        | 0.08  | 0.17  | (4.32,5.28) |
| 2       | 1          | -2.80         | 0.23  | 0.35  | (4.32,5.28) |
| 3       | 2          | -8.00         | 0.14  | 0.69  | (4.32,5.28) |
| 4       | 3          | 1.40          | 0.09  | 0.86  | (4.32,5.28) |
| 5       | 4          | 39.20         | -0.72 | -0.77 | (4.32,5.28) |
| 6       | 5          | -19.80        | -0.23 | -0.57 | (4.32,5.28) |
| 7       | 6          | 13.90         | -0.38 | -0.86 | (4.32,5.28) |
| 8       | 7          | 3.20          | -0.30 | -0.49 | (4.32,5.28) |
| 9       | 8          | -4.00         | -0.10 | -0.61 | (4.32,5.28) |
| 10      | 9          | 5.30          | -0.20 | -0.51 | (4.32,5.28) |
| 11      | 10         | -3.40         | -0.21 | -0.08 | (4.32,5.28) |
| 12      | 11         | -45.00        | 0.07  | 0.90  | (4.32,5.28) |
| 13      | 12         | 0.00          | -0.11 | 0.78  | (4.32,5.28) |
| 14      | 13         | 36.00         | -0.10 | 0.99  | (4.32,5.28) |
| 15      | 14         | -17.70        | -0.10 | 1.01  | (4.32,5.28) |
| 16      | 15         | -45.00        | -0.29 | 0.49  | (4.32,5.28) |
| 17      | 16         | 8.00          | -0.30 | 0.48  | (4.32,5.28) |
| 18      | 17         | 0.00          | -0.35 | 0.23  | (4.32,5.28) |
| 19      | 18         | -6.80         | -0.36 | 0.22  | (4.32,5.28) |
| 20      | 19         | -9.80         | -0.12 | 0.24  | (4.32,5.28) |
| 21      | 20         | -14.70        | 0.04  | 0.26  | (4.32,5.28) |
| 22      | 21         | -2.80         | 0.07  | 0.37  | (4.32,5.28) |
| 23      | 22         | -45.00        | 0.22  | 1.06  | (4.32,5.28) |
| 24      | 23         | -18.00        | 0.11  | 1.11  | (4.32,5.28) |
| 25      | 24         | 45.00         | 0.09  | 0.78  | (4.32,5.28) |
| 26      | 25         | 45.00         | -0.24 | 0.67  | (4.32,5.28) |
| 27      | 26         | 39.70         | -0.25 | 0.70  | (4.32,5.28) |
| 28      | 27         | 36.00         | -0.09 | 0.97  | (4.32,5.28) |
| 29      | 28         | -36.10        | -0.38 | 1.48  | (4.32,5.28) |
+---------+------------+---------------+-------+-------+-------------+
⚠️	Hole detected! Attempting to fix using Geometry::insert_frame(...) (baseline spacing = 0.559)
✅ Fixed one-frame hole between Frame 24 and Frame 25 (dz = 0.909, ratio = 1.627)
✅ Fixed one-frame hole between Frame 31 and Frame 32 (dz = 0.876, ratio = 1.568)
✅ Fixed one-frame hole between Frame 33 and Frame 34 (dz = 0.842, ratio = 1.508)
⚠️	Hole detected! Attempting to fix using Geometry::insert_frame(...) (baseline spacing = 0.533)
✅ Fixed one-frame hole between Frame 19 and Frame 20 (dz = 1.099, ratio = 2.061)

+-------------------------------------------------------------------+
|      ✅ Finished aligning 'ivus_poststent' (anomalous: true)       |
+---------+------------+---------------+------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx  |  Ty   |  Centroid   |
+---------+------------+---------------+------+-------+-------------+
| 1       | 0          | 12.40         | 0.18 | -0.01 | (4.69,3.78) |
| 2       | 1          | 7.50          | 0.29 | 0.00  | (4.69,3.78) |
| 3       | 2          | -6.70         | 0.20 | 0.31  | (4.69,3.78) |
| 4       | 3          | 3.30          | 0.33 | 0.39  | (4.69,3.78) |
| 5       | 4          | 0.00          | 0.25 | 0.06  | (4.69,3.78) |
| 6       | 5          | 2.20          | 0.34 | 0.32  | (4.69,3.78) |
| 7       | 6          | 8.70          | 0.50 | 0.31  | (4.69,3.78) |
| 8       | 7          | -7.90         | 0.46 | 0.32  | (4.69,3.78) |
| 9       | 8          | 7.30          | 0.54 | 0.22  | (4.69,3.78) |
| 10      | 9          | 7.90          | 0.65 | 0.09  | (4.69,3.78) |
| 11      | 10         | 0.00          | 0.62 | 0.00  | (4.69,3.78) |
| 12      | 11         | 5.30          | 0.66 | -0.11 | (4.69,3.78) |
| 13      | 12         | -11.40        | 0.60 | -0.18 | (4.69,3.78) |
| 14      | 13         | -2.90         | 0.56 | -0.21 | (4.69,3.78) |
| 15      | 14         | 0.00          | 0.53 | -0.35 | (4.69,3.78) |
| 16      | 15         | 18.00         | 0.55 | -0.46 | (4.69,3.78) |
| 17      | 16         | 12.40         | 0.59 | -0.58 | (4.69,3.78) |
| 18      | 17         | 5.80          | 0.61 | -0.61 | (4.69,3.78) |
| 19      | 18         | 13.20         | 0.63 | -0.72 | (4.69,3.78) |
| 20      | 19         | 0.70          | 0.61 | -0.73 | (4.69,3.78) |
| 21      | 20         | 0.00          | 0.57 | -0.75 | (4.69,3.78) |
| 22      | 21         | -18.00        | 0.89 | -0.63 | (4.69,3.78) |
| 23      | 22         | -0.70         | 0.85 | -0.69 | (4.69,3.78) |
| 24      | 23         | -15.90        | 0.83 | -0.50 | (4.69,3.78) |
| 25      | 24         | 3.60          | 0.83 | -0.49 | (4.69,3.78) |
| 26      | 25         | 15.70         | 0.90 | -0.63 | (4.69,3.78) |
| 27      | 26         | 16.20         | 0.85 | -0.78 | (4.69,3.78) |
| 28      | 27         | -5.40         | 0.91 | -0.70 | (4.69,3.78) |
| 29      | 28         | 6.40          | 0.89 | -0.73 | (4.69,3.78) |
| 30      | 29         | 24.20         | 0.78 | -0.99 | (4.69,3.78) |
| 31      | 30         | -1.30         | 0.72 | -0.91 | (4.69,3.78) |
| 32      | 31         | 19.40         | 0.65 | -0.94 | (4.69,3.78) |
| 33      | 32         | 0.00          | 0.86 | -1.01 | (4.69,3.78) |
| 34      | 33         | 9.90          | 0.74 | -1.06 | (4.69,3.78) |
+---------+------------+---------------+------+-------+-------------+

+--------------------------------------------------------------------+
|       ✅ Finished aligning 'ivus_prestent' (anomalous: true)        |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | 0.10          | 0.01  | 0.12  | (4.20,4.59) |
| 2       | 1          | -8.10         | -0.03 | 0.07  | (4.20,4.59) |
| 3       | 2          | 9.20          | -0.05 | -0.14 | (4.20,4.59) |
| 4       | 3          | -10.90        | 0.27  | -0.75 | (4.20,4.59) |
| 5       | 4          | 9.90          | 0.11  | -1.14 | (4.20,4.59) |
| 6       | 5          | -1.10         | 0.03  | -1.17 | (4.20,4.59) |
| 7       | 6          | 8.00          | -0.13 | -1.28 | (4.20,4.59) |
| 8       | 7          | -4.10         | -0.08 | -1.25 | (4.20,4.59) |
| 9       | 8          | -0.80         | -0.06 | -1.36 | (4.20,4.59) |
| 10      | 9          | -0.70         | 0.02  | -1.22 | (4.20,4.59) |
| 11      | 10         | 6.00          | -0.26 | -1.22 | (4.20,4.59) |
| 12      | 11         | -0.30         | -0.35 | -1.21 | (4.20,4.59) |
| 13      | 12         | -4.70         | -0.13 | -0.88 | (4.20,4.59) |
| 14      | 13         | -4.30         | -0.08 | -0.87 | (4.20,4.59) |
| 15      | 14         | 4.20          | -0.14 | -0.77 | (4.20,4.59) |
| 16      | 15         | -3.60         | -0.13 | -0.74 | (4.20,4.59) |
| 17      | 16         | 0.00          | -0.17 | -0.52 | (4.20,4.59) |
| 18      | 17         | -45.00        | 0.06  | 0.32  | (4.20,4.59) |
| 19      | 18         | -3.50         | 0.04  | 0.28  | (4.20,4.59) |
| 20      | 19         | 45.00         | -0.42 | -0.17 | (4.20,4.59) |
| 21      | 20         | -11.60        | -0.29 | -0.06 | (4.20,4.59) |
| 22      | 21         | 45.00         | -0.72 | -0.15 | (4.20,4.59) |
+---------+------------+---------------+-------+-------+-------------+
⚠️	Hole detected! Attempting to fix using Geometry::insert_frame(...) (baseline spacing = 0.569)
✅ Fixed one-frame hole between Frame 30 and Frame 31 (dz = 0.876, ratio = 1.539)

+-------------------------------------------------------------------+
|      ✅ Finished aligning 'ivus_poststent' (anomalous: true)       |
+---------+------------+---------------+------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx  |  Ty   |  Centroid   |
+---------+------------+---------------+------+-------+-------------+
| 1       | 0          | 3.90          | 0.16 | 0.16  | (4.84,3.84) |
| 2       | 1          | -10.70        | 0.08 | 0.12  | (4.84,3.84) |
| 3       | 2          | 17.50         | 0.40 | 0.34  | (4.84,3.84) |
| 4       | 3          | -12.70        | 0.19 | 0.38  | (4.84,3.84) |
| 5       | 4          | 3.90          | 0.34 | 0.38  | (4.84,3.84) |
| 6       | 5          | 6.00          | 0.47 | 0.39  | (4.84,3.84) |
| 7       | 6          | 13.90         | 0.65 | 0.30  | (4.84,3.84) |
| 8       | 7          | -18.00        | 0.37 | 0.43  | (4.84,3.84) |
| 9       | 8          | 15.80         | 0.57 | 0.20  | (4.84,3.84) |
| 10      | 9          | 3.00          | 0.57 | 0.18  | (4.84,3.84) |
| 11      | 10         | 0.30          | 0.68 | 0.18  | (4.84,3.84) |
| 12      | 11         | 8.80          | 0.72 | 0.02  | (4.84,3.84) |
| 13      | 12         | 7.10          | 0.81 | -0.06 | (4.84,3.84) |
| 14      | 13         | 0.00          | 0.76 | -0.14 | (4.84,3.84) |
| 15      | 14         | 5.40          | 0.78 | -0.23 | (4.84,3.84) |
| 16      | 15         | -0.80         | 0.85 | -0.25 | (4.84,3.84) |
| 17      | 16         | 10.50         | 0.88 | -0.35 | (4.84,3.84) |
| 18      | 17         | 13.70         | 0.94 | -0.43 | (4.84,3.84) |
| 19      | 18         | 0.20          | 0.92 | -0.50 | (4.84,3.84) |
| 20      | 19         | 23.30         | 0.92 | -0.72 | (4.84,3.84) |
| 21      | 20         | -12.40        | 0.86 | -0.68 | (4.84,3.84) |
| 22      | 21         | 32.40         | 0.82 | -0.85 | (4.84,3.84) |
| 23      | 22         | 18.10         | 0.78 | -0.89 | (4.84,3.84) |
| 24      | 23         | -18.00        | 0.74 | -0.75 | (4.84,3.84) |
| 25      | 24         | 18.00         | 0.79 | -0.94 | (4.84,3.84) |
| 26      | 25         | 11.30         | 0.70 | -0.95 | (4.84,3.84) |
| 27      | 26         | 0.00          | 0.77 | -0.91 | (4.84,3.84) |
| 28      | 27         | 0.00          | 0.70 | -0.88 | (4.84,3.84) |
| 29      | 28         | 12.70         | 0.74 | -0.64 | (4.84,3.84) |
| 30      | 29         | 0.00          | 1.10 | -0.63 | (4.84,3.84) |
| 31      | 30         | 11.40         | 0.82 | -0.72 | (4.84,3.84) |
| 32      | 31         | -9.60         | 1.10 | -0.64 | (4.84,3.84) |
| 33      | 32         | 4.70          | 1.09 | -0.64 | (4.84,3.84) |
+---------+------------+---------------+------+-------+-------------+

✅ Aligned geometry 'ivus_poststent' to 'ivus_poststent'
-----------------------------------------
Applied initial translation: (-0.16, -0.05, 0.00) mm
Found best rotation of -34.90° with parameters: 
range: 45.00° 
step size: 0.1°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

✅ Aligned geometry 'ivus_prestent' to 'ivus_prestent'
-----------------------------------------
Applied initial translation: (0.12, 0.70, 0.00) mm
Found best rotation of -0.50° with parameters: 
range: 45.00° 
step size: 0.1°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

✅ Aligned geometry 'ivus_poststent' to 'ivus_prestent'
-----------------------------------------
Applied initial translation: (-0.37, 1.50, 0.00) mm
Found best rotation of 30.20° with parameters: 
range: 45.00° 
step size: 0.1°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

✅ Aligned geometry 'ivus_poststent' to 'ivus_prestent'
-----------------------------------------
Applied initial translation: (-0.37, 1.50, 0.00) mm
Found best rotation of -27.40° with parameters: 
range: 45.00° 
step size: 0.1°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

Saving files for 'ivus_prestent - ivus_prestent' to 'output/stent_rest'
LUMEN .obj files: 2/2 written successfully
CATHETER .obj files: 2/2 written successfully
WALL .obj files: 2/2 written successfully

Saving files for 'ivus_poststent - ivus_poststent' to 'output/stent_stress'
LUMEN .obj files: 2/2 written successfully
CATHETER .obj files: 2/2 written successfully
WALL .obj files: 2/2 written successfully

Saving files for 'ivus_prestent - ivus_poststent' to 'output/stent_diastole'
LUMEN .obj files: 2/2 written successfully
CATHETER .obj files: 2/2 written successfully
WALL .obj files: 2/2 written successfully

Saving files for 'ivus_prestent - ivus_poststent' to 'output/stent_systole'
LUMEN .obj files: 2/2 written successfully
CATHETER .obj files: 2/2 written successfully
WALL .obj files: 2/2 written successfully
Successfully wrote lumen to output/stent_vis/prestent_lumen.obj
Successfully wrote lumen to output/stent_vis/poststent_lumen.obj

3. Workflow from numpy arrays

The numpy workflow accepts data from any source: you build PyInputData objects manually and then call the same alignment functions. Use this workflow when your segmentation software uses different file formats, when you need pre-processing steps, or when embedding multimodars into a larger pipeline.

For stent comparison, both pre- and post-stent geometries are packed into PyInputData objects:

before_arr = np.genfromtxt("ivus_prestent/diastolic_contours.csv", delimiter='\t')
before_ref = np.genfromtxt("ivus_prestent/diastolic_reference_points.csv", delimiter='\t')
after_arr  = np.genfromtxt("ivus_poststent/diastolic_contours.csv", delimiter='\t')
after_ref  = np.genfromtxt("ivus_poststent/diastolic_reference_points.csv", delimiter='\t')

before_input = mm.numpy_to_inputdata(
    lumen_arr=before_arr, ref_point=before_ref,
    record=None, diastole=True, label="prestent",
)
after_input = mm.numpy_to_inputdata(
    lumen_arr=after_arr, ref_point=after_ref,
    record=None, diastole=True, label="poststent",
)

pair, _ = mm.from_array_singlepair(
    input_data_a=before_input,
    input_data_b=after_input,
    step_rotation_deg=0.01,
    range_rotation_deg=30,
    output_path="output/stent_comparison",
)
✅ Successfully built geometry from input data
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: prestent
Diastole phase: Yes


✅ Successfully built geometry from input data
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: poststent
Diastole phase: Yes


+--------------------------------------------------------------------+
|          ✅ Finished aligning 'prestent' (anomalous: true)          |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | -19.43        | 0.23  | 0.35  | (4.32,5.28) |
| 2       | 1          | 2.77          | 0.08  | 0.17  | (4.32,5.28) |
| 3       | 2          | -8.61         | 0.14  | 0.69  | (4.32,5.28) |
| 4       | 3          | 1.44          | 0.09  | 0.86  | (4.32,5.28) |
| 5       | 4          | -10.88        | -0.10 | -0.61 | (4.32,5.28) |
| 6       | 5          | 5.32          | -0.20 | -0.51 | (4.32,5.28) |
| 7       | 6          | -0.54         | -0.30 | -0.49 | (4.32,5.28) |
| 8       | 7          | -3.14         | -0.38 | -0.86 | (4.32,5.28) |
| 9       | 8          | 7.40          | -0.72 | -0.77 | (4.32,5.28) |
| 10      | 9          | -19.75        | -0.23 | -0.57 | (4.32,5.28) |
| 11      | 10         | 0.00          | -0.21 | -0.08 | (4.32,5.28) |
| 12      | 11         | 16.53         | -0.35 | 0.23  | (4.32,5.28) |
| 13      | 12         | -6.85         | -0.36 | 0.22  | (4.32,5.28) |
| 14      | 13         | 0.00          | -0.30 | 0.48  | (4.32,5.28) |
| 15      | 14         | -8.01         | -0.29 | 0.49  | (4.32,5.28) |
| 16      | 15         | -17.73        | -0.12 | 0.24  | (4.32,5.28) |
| 17      | 16         | -14.66        | 0.04  | 0.26  | (4.32,5.28) |
| 18      | 17         | -30.00        | 0.07  | 0.90  | (4.32,5.28) |
| 19      | 18         | 16.52         | -0.09 | 0.97  | (4.32,5.28) |
| 20      | 19         | -14.64        | -0.10 | 1.01  | (4.32,5.28) |
| 21      | 20         | 17.73         | -0.10 | 0.99  | (4.32,5.28) |
| 22      | 21         | -26.19        | -0.11 | 0.78  | (4.32,5.28) |
| 23      | 22         | 30.00         | 0.07  | 0.37  | (4.32,5.28) |
| 24      | 23         | -6.49         | 0.09  | 0.78  | (4.32,5.28) |
| 25      | 24         | 29.51         | -0.25 | 0.70  | (4.32,5.28) |
| 26      | 25         | -29.88        | -0.24 | 0.67  | (4.32,5.28) |
| 27      | 26         | 30.00         | -0.38 | 1.48  | (4.32,5.28) |
| 28      | 27         | 30.00         | 0.11  | 1.11  | (4.32,5.28) |
| 29      | 28         | 18.00         | 0.22  | 1.06  | (4.32,5.28) |
+---------+------------+---------------+-------+-------+-------------+
⚠️	Hole detected! Attempting to fix using Geometry::insert_frame(...) (baseline spacing = 0.559)
✅ Fixed one-frame hole between Frame 24 and Frame 25 (dz = 0.909, ratio = 1.627)
✅ Fixed one-frame hole between Frame 31 and Frame 32 (dz = 0.876, ratio = 1.568)
✅ Fixed one-frame hole between Frame 33 and Frame 34 (dz = 0.842, ratio = 1.508)

+-------------------------------------------------------------------+
|        ✅ Finished aligning 'poststent' (anomalous: false)         |
+---------+------------+---------------+------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx  |  Ty   |  Centroid   |
+---------+------------+---------------+------+-------+-------------+
| 1       | 0          | 12.38         | 0.18 | -0.01 | (4.69,3.78) |
| 2       | 1          | 7.45          | 0.29 | 0.00  | (4.69,3.78) |
| 3       | 2          | -12.68        | 0.25 | 0.06  | (4.69,3.78) |
| 4       | 3          | -1.86         | 0.20 | 0.31  | (4.69,3.78) |
| 5       | 4          | 3.30          | 0.33 | 0.39  | (4.69,3.78) |
| 6       | 5          | 6.94          | 0.34 | 0.32  | (4.69,3.78) |
| 7       | 6          | 8.74          | 0.50 | 0.31  | (4.69,3.78) |
| 8       | 7          | -7.95         | 0.46 | 0.32  | (4.69,3.78) |
| 9       | 8          | 7.24          | 0.54 | 0.22  | (4.69,3.78) |
| 10      | 9          | 7.93          | 0.65 | 0.09  | (4.69,3.78) |
| 11      | 10         | 0.00          | 0.62 | 0.00  | (4.69,3.78) |
| 12      | 11         | 5.25          | 0.66 | -0.11 | (4.69,3.78) |
| 13      | 12         | -11.45        | 0.60 | -0.18 | (4.69,3.78) |
| 14      | 13         | -2.92         | 0.56 | -0.21 | (4.69,3.78) |
| 15      | 14         | 0.00          | 0.53 | -0.35 | (4.69,3.78) |
| 16      | 15         | 18.00         | 0.55 | -0.46 | (4.69,3.78) |
| 17      | 16         | 12.37         | 0.59 | -0.58 | (4.69,3.78) |
| 18      | 17         | 5.81          | 0.61 | -0.61 | (4.69,3.78) |
| 19      | 18         | 13.24         | 0.63 | -0.72 | (4.69,3.78) |
| 20      | 19         | 0.67          | 0.61 | -0.73 | (4.69,3.78) |
| 21      | 20         | -18.00        | 0.83 | -0.50 | (4.69,3.78) |
| 22      | 21         | 18.00         | 0.57 | -0.75 | (4.69,3.78) |
| 23      | 22         | -23.65        | 0.83 | -0.49 | (4.69,3.78) |
| 24      | 23         | 15.70         | 0.90 | -0.63 | (4.69,3.78) |
| 25      | 24         | -3.16         | 0.89 | -0.63 | (4.69,3.78) |
| 26      | 25         | -0.74         | 0.85 | -0.69 | (4.69,3.78) |
| 27      | 26         | 0.00          | 0.89 | -0.73 | (4.69,3.78) |
| 28      | 27         | -6.41         | 0.91 | -0.70 | (4.69,3.78) |
| 29      | 28         | 5.45          | 0.85 | -0.78 | (4.69,3.78) |
| 30      | 29         | -0.54         | 0.72 | -0.91 | (4.69,3.78) |
| 31      | 30         | 19.47         | 0.65 | -0.94 | (4.69,3.78) |
| 32      | 31         | 6.12          | 0.74 | -1.06 | (4.69,3.78) |
| 33      | 32         | -9.93         | 0.86 | -1.01 | (4.69,3.78) |
| 34      | 33         | -1.94         | 0.78 | -0.99 | (4.69,3.78) |
+---------+------------+---------------+------+-------+-------------+

✅ Aligned geometry 'poststent' to 'prestent'
-----------------------------------------
Applied initial translation: (-0.37, 1.50, 0.00) mm
Found best rotation of 18.55° with parameters: 
range: 30.00° 
step size: 0.01°
Applied final translation: ( 0, 0.00, 0.00) mm
-----------------------------------------

Saving files for 'prestent - poststent' to 'output/stent_comparison'
LUMEN .obj files: 2/2 written successfully
CATHETER .obj files: 2/2 written successfully
WALL .obj files: 2/2 written successfully

A single 3D geometry can also be reconstructed from OCT. from_array_single returns a PyGeometry from a single-state contour array. The replace_frame method demonstrates in-place editing before writing:

oct_raw = np.genfromtxt("oct_single/oct_contours_raw.csv", delimiter=',')
oct_ref = np.genfromtxt("oct_single/oct_ref.csv", delimiter=',')

oct_input = mm.numpy_to_inputdata(
    lumen_arr=oct_raw, ref_point=oct_ref,
    record=None, diastole=True, label="oct",
)

oct_recon, _ = mm.from_array_single(
    input_data=oct_input,
    step_rotation_deg=0.01,
    range_rotation_deg=6,
    image_center=(5.0, 5.0),
    radius=0.5,
    n_points=40,
    write_obj=False,
    watertight=False,
    output_path="output/oct",
    smooth=False,
)

# Replace an outlier frame and write both versions for comparison
frame = oct_recon.get_frame_at_z(34.8)
oct_replaced = oct_recon.replace_frame(frame.id + 1, frame)

mm.to_obj(oct_recon,    "output/oct", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="oct")
mm.to_obj(oct_replaced, "output/oct", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="oct_replaced")

oct_mesh       = trimesh.load("output/oct/oct_lumen.obj")
oct_mesh_fixed = trimesh.load("output/oct/oct_replaced_lumen.obj")

fig = make_subplots(
    rows=1, cols=2,
    specs=[[{"type": "scene"}, {"type": "scene"}]],
    subplot_titles=["OCT original", "OCT with replaced frame"],
)
scene_cfg = dict(aspectmode="data", camera=dict(eye=dict(x=1.5, y=1.5, z=1.0)))
for t in [trimesh_to_mesh3d(oct_mesh, "royalblue", "OCT Lumen")]:
    fig.add_trace(t, row=1, col=1)
for t in [trimesh_to_mesh3d(oct_mesh_fixed, "tomato", "OCT Replaced")]:
    fig.add_trace(t, row=1, col=2)
fig.update_layout(
    width=1200, height=600,
    scene=scene_cfg, scene2=scene_cfg,
    margin=dict(l=0, r=0, t=40, b=0),
)
fig.show()
✅ Successfully built geometry from input data
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: oct
Diastole phase: Yes


+--------------------------------------------------------------------+
|            ✅ Finished aligning 'oct' (anomalous: false)            |
+---------+------------+---------------+-------+-------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty   |  Centroid   |
+---------+------------+---------------+-------+-------+-------------+
| 1       | 0          | -1.75         | 0.00  | 0.05  | (6.30,5.97) |
| 2       | 1          | 1.40          | 0.02  | 0.12  | (6.30,5.97) |
| 3       | 2          | 0.47          | -0.03 | 0.17  | (6.30,5.97) |
| 4       | 3          | 3.10          | -0.04 | 0.25  | (6.30,5.97) |
| 5       | 4          | -1.86         | -0.06 | 0.29  | (6.30,5.97) |
| 6       | 5          | 4.01          | -0.10 | 0.42  | (6.30,5.97) |
| 7       | 6          | 5.88          | -0.13 | 0.59  | (6.30,5.97) |
| 8       | 7          | 5.99          | -0.14 | 0.78  | (6.30,5.97) |
| 9       | 8          | 0.71          | -0.13 | 0.90  | (6.30,5.97) |
| 10      | 9          | 2.19          | -0.10 | 1.04  | (6.30,5.97) |
| 11      | 10         | 2.24          | -0.09 | 1.14  | (6.30,5.97) |
| 12      | 11         | 2.62          | -0.05 | 1.27  | (6.30,5.97) |
| 13      | 12         | 0.92          | 0.00  | 1.37  | (6.30,5.97) |
| 14      | 13         | 5.28          | 0.05  | 1.50  | (6.30,5.97) |
| 15      | 14         | 1.82          | 0.09  | 1.59  | (6.30,5.97) |
| 16      | 15         | 2.20          | 0.15  | 1.68  | (6.30,5.97) |
| 17      | 16         | 0.01          | 0.22  | 1.78  | (6.30,5.97) |
| 18      | 17         | -0.59         | 0.29  | 1.83  | (6.30,5.97) |
| 19      | 18         | 4.33          | 0.36  | 1.91  | (6.30,5.97) |
| 20      | 19         | 2.80          | 0.42  | 1.95  | (6.30,5.97) |
| 21      | 20         | -0.80         | 0.51  | 1.97  | (6.30,5.97) |
| 22      | 21         | -6.00         | 0.67  | 1.95  | (6.30,5.97) |
| 23      | 22         | 2.37          | 0.76  | 1.90  | (6.30,5.97) |
| 24      | 23         | 0.83          | 0.82  | 1.88  | (6.30,5.97) |
| 25      | 24         | -0.62         | 0.78  | 1.86  | (6.30,5.97) |
| 26      | 25         | -0.87         | 0.82  | 1.84  | (6.30,5.97) |
| 27      | 26         | 3.52          | 0.82  | 1.84  | (6.30,5.97) |
| 28      | 27         | 4.63          | 0.86  | 1.87  | (6.30,5.97) |
| 29      | 28         | 5.46          | 0.88  | 1.92  | (6.30,5.97) |
| 30      | 29         | 1.04          | 0.90  | 1.95  | (6.30,5.97) |
| 31      | 30         | 1.64          | 0.91  | 2.00  | (6.30,5.97) |
| 32      | 31         | 0.20          | 0.90  | 2.06  | (6.30,5.97) |
| 33      | 32         | 0.88          | 0.93  | 2.13  | (6.30,5.97) |
| 34      | 33         | 1.35          | 0.96  | 2.22  | (6.30,5.97) |
| 35      | 34         | 3.43          | 0.99  | 2.28  | (6.30,5.97) |
| 36      | 35         | 3.22          | 1.04  | 2.32  | (6.30,5.97) |
| 37      | 36         | 5.27          | 1.14  | 2.39  | (6.30,5.97) |
| 38      | 37         | 5.99          | 1.25  | 2.48  | (6.30,5.97) |
| 39      | 38         | 5.99          | 1.43  | 2.53  | (6.30,5.97) |
| 40      | 39         | 4.19          | 1.56  | 2.58  | (6.30,5.97) |
| 41      | 40         | 4.06          | 1.69  | 2.59  | (6.30,5.97) |
| 42      | 41         | 3.93          | 1.79  | 2.60  | (6.30,5.97) |
| 43      | 42         | 0.38          | 1.84  | 2.61  | (6.30,5.97) |
| 44      | 43         | 3.33          | 1.92  | 2.61  | (6.30,5.97) |
| 45      | 44         | 2.85          | 1.97  | 2.61  | (6.30,5.97) |
| 46      | 45         | -0.62         | 2.00  | 2.59  | (6.30,5.97) |
| 47      | 46         | 0.62          | 2.02  | 2.57  | (6.30,5.97) |
| 48      | 47         | 2.22          | 2.05  | 2.52  | (6.30,5.97) |
| 49      | 48         | 0.99          | 2.04  | 2.53  | (6.30,5.97) |
| 50      | 49         | 5.96          | 2.09  | 2.51  | (6.30,5.97) |
| 51      | 50         | -4.71         | 2.08  | 2.54  | (6.30,5.97) |
| 52      | 51         | 1.17          | 2.09  | 2.52  | (6.30,5.97) |
| 53      | 52         | 0.70          | 2.14  | 2.51  | (6.30,5.97) |
| 54      | 53         | -0.97         | 2.19  | 2.45  | (6.30,5.97) |
| 55      | 54         | 2.69          | 2.23  | 2.40  | (6.30,5.97) |
| 56      | 55         | -4.29         | 2.19  | 2.38  | (6.30,5.97) |
| 57      | 56         | 2.00          | 2.22  | 2.29  | (6.30,5.97) |
| 58      | 57         | 1.90          | 2.25  | 2.22  | (6.30,5.97) |
| 59      | 58         | -1.62         | 2.28  | 2.14  | (6.30,5.97) |
| 60      | 59         | 1.61          | 2.26  | 2.08  | (6.30,5.97) |
| 61      | 60         | 1.23          | 2.23  | 2.03  | (6.30,5.97) |
| 62      | 61         | 1.38          | 2.18  | 1.99  | (6.30,5.97) |
| 63      | 62         | 2.23          | 2.16  | 1.96  | (6.30,5.97) |
| 64      | 63         | 1.43          | 2.16  | 1.91  | (6.30,5.97) |
| 65      | 64         | -0.70         | 2.14  | 1.88  | (6.30,5.97) |
| 66      | 65         | 2.59          | 2.14  | 1.85  | (6.30,5.97) |
| 67      | 66         | 0.93          | 2.13  | 1.83  | (6.30,5.97) |
| 68      | 67         | 1.79          | 2.13  | 1.81  | (6.30,5.97) |
| 69      | 68         | 1.58          | 2.12  | 1.80  | (6.30,5.97) |
| 70      | 69         | 0.97          | 2.10  | 1.82  | (6.30,5.97) |
| 71      | 70         | 1.95          | 2.07  | 1.81  | (6.30,5.97) |
| 72      | 71         | 0.56          | 2.04  | 1.80  | (6.30,5.97) |
| 73      | 72         | 1.54          | 2.02  | 1.78  | (6.30,5.97) |
| 74      | 73         | -0.07         | 2.00  | 1.77  | (6.30,5.97) |
| 75      | 74         | 1.18          | 1.97  | 1.78  | (6.30,5.97) |
| 76      | 75         | 0.83          | 1.96  | 1.75  | (6.30,5.97) |
| 77      | 76         | 1.11          | 1.94  | 1.71  | (6.30,5.97) |
| 78      | 77         | 1.73          | 1.95  | 1.69  | (6.30,5.97) |
| 79      | 78         | 0.16          | 1.94  | 1.66  | (6.30,5.97) |
| 80      | 79         | 2.62          | 1.94  | 1.64  | (6.30,5.97) |
| 81      | 80         | 2.81          | 1.95  | 1.63  | (6.30,5.97) |
| 82      | 81         | -1.60         | 1.97  | 1.62  | (6.30,5.97) |
| 83      | 82         | 4.01          | 1.95  | 1.63  | (6.30,5.97) |
| 84      | 83         | 5.99          | 1.96  | 1.69  | (6.30,5.97) |
| 85      | 84         | 5.99          | 2.03  | 1.60  | (6.30,5.97) |
| 86      | 85         | 4.49          | 2.06  | 1.53  | (6.30,5.97) |
| 87      | 86         | -0.62         | 2.09  | 1.45  | (6.30,5.97) |
| 88      | 87         | 1.30          | 2.12  | 1.41  | (6.30,5.97) |
| 89      | 88         | -0.70         | 2.15  | 1.37  | (6.30,5.97) |
| 90      | 89         | 2.85          | 2.19  | 1.33  | (6.30,5.97) |
| 91      | 90         | 0.63          | 2.23  | 1.28  | (6.30,5.97) |
| 92      | 91         | -0.24         | 2.27  | 1.25  | (6.30,5.97) |
| 93      | 92         | -0.26         | 2.31  | 1.23  | (6.30,5.97) |
| 94      | 93         | 3.01          | 2.35  | 1.15  | (6.30,5.97) |
| 95      | 94         | 2.36          | 2.37  | 1.08  | (6.30,5.97) |
| 96      | 95         | 3.39          | 2.36  | 1.05  | (6.30,5.97) |
| 97      | 96         | 3.71          | 2.37  | 1.04  | (6.30,5.97) |
| 98      | 97         | 5.99          | 2.38  | 1.00  | (6.30,5.97) |
| 99      | 98         | 5.99          | 2.43  | 0.99  | (6.30,5.97) |
| 100     | 99         | 5.69          | 2.44  | 0.99  | (6.30,5.97) |
| 101     | 100        | 0.35          | 2.46  | 0.90  | (6.30,5.97) |
| 102     | 101        | 0.00          | 2.46  | 0.79  | (6.30,5.97) |
| 103     | 102        | 2.10          | 2.45  | 0.66  | (6.30,5.97) |
| 104     | 103        | 3.25          | 2.43  | 0.57  | (6.30,5.97) |
| 105     | 104        | 3.92          | 2.42  | 0.47  | (6.30,5.97) |
| 106     | 105        | 0.01          | 2.41  | 0.41  | (6.30,5.97) |
| 107     | 106        | 3.23          | 2.42  | 0.33  | (6.30,5.97) |
| 108     | 107        | 0.98          | 2.41  | 0.28  | (6.30,5.97) |
| 109     | 108        | 1.37          | 2.40  | 0.25  | (6.30,5.97) |
| 110     | 109        | -0.05         | 2.41  | 0.22  | (6.30,5.97) |
| 111     | 110        | 1.56          | 2.38  | 0.20  | (6.30,5.97) |
| 112     | 111        | 2.63          | 2.35  | 0.16  | (6.30,5.97) |
| 113     | 112        | 0.12          | 2.32  | 0.15  | (6.30,5.97) |
| 114     | 113        | -1.41         | 2.32  | 0.14  | (6.30,5.97) |
| 115     | 114        | 1.38          | 2.32  | 0.14  | (6.30,5.97) |
| 116     | 115        | 0.00          | 2.29  | 0.15  | (6.30,5.97) |
| 117     | 116        | 3.14          | 2.27  | 0.13  | (6.30,5.97) |
| 118     | 117        | -0.86         | 2.25  | 0.12  | (6.30,5.97) |
| 119     | 118        | 4.18          | 2.20  | 0.14  | (6.30,5.97) |
| 120     | 119        | -1.10         | 2.19  | 0.15  | (6.30,5.97) |
| 121     | 120        | -0.71         | 2.17  | 0.20  | (6.30,5.97) |
| 122     | 121        | 0.67          | 2.13  | 0.24  | (6.30,5.97) |
| 123     | 122        | 1.49          | 2.12  | 0.27  | (6.30,5.97) |
| 124     | 123        | 1.96          | 2.09  | 0.32  | (6.30,5.97) |
| 125     | 124        | -0.32         | 2.08  | 0.34  | (6.30,5.97) |
| 126     | 125        | 2.68          | 2.04  | 0.36  | (6.30,5.97) |
| 127     | 126        | 0.64          | 2.02  | 0.33  | (6.30,5.97) |
| 128     | 127        | -1.77         | 1.99  | 0.33  | (6.30,5.97) |
| 129     | 128        | -1.93         | 1.98  | 0.31  | (6.30,5.97) |
| 130     | 129        | -1.08         | 1.99  | 0.23  | (6.30,5.97) |
| 131     | 130        | 5.99          | 1.94  | 0.28  | (6.30,5.97) |
| 132     | 131        | -4.71         | 1.95  | 0.28  | (6.30,5.97) |
| 133     | 132        | -4.51         | 1.92  | 0.35  | (6.30,5.97) |
| 134     | 133        | 1.89          | 1.86  | 0.42  | (6.30,5.97) |
| 135     | 134        | -2.08         | 1.82  | 0.46  | (6.30,5.97) |
| 136     | 135        | -0.74         | 1.76  | 0.50  | (6.30,5.97) |
| 137     | 136        | -3.69         | 1.70  | 0.57  | (6.30,5.97) |
| 138     | 137        | 2.54          | 1.58  | 0.67  | (6.30,5.97) |
| 139     | 138        | 5.99          | 1.54  | 0.66  | (6.30,5.97) |
| 140     | 139        | 5.99          | 1.41  | 0.72  | (6.30,5.97) |
| 141     | 140        | 0.62          | 1.35  | 0.73  | (6.30,5.97) |
| 142     | 141        | 3.40          | 1.35  | 0.76  | (6.30,5.97) |
| 143     | 142        | 1.15          | 1.35  | 0.80  | (6.30,5.97) |
| 144     | 143        | 0.27          | 1.31  | 0.84  | (6.30,5.97) |
| 145     | 144        | -1.53         | 1.31  | 0.82  | (6.30,5.97) |
| 146     | 145        | -6.00         | 1.32  | 0.82  | (6.30,5.97) |
| 147     | 146        | 0.00          | 1.33  | 0.90  | (6.30,5.97) |
| 148     | 147        | 0.00          | 1.33  | 1.02  | (6.30,5.97) |
| 149     | 148        | 5.99          | 1.38  | 1.05  | (6.30,5.97) |
| 150     | 149        | -5.73         | 1.42  | 0.98  | (6.30,5.97) |
| 151     | 150        | 1.77          | 1.50  | 0.93  | (6.30,5.97) |
| 152     | 151        | -6.00         | 1.64  | 0.81  | (6.30,5.97) |
| 153     | 152        | -5.97         | 1.72  | 0.76  | (6.30,5.97) |
| 154     | 153        | 2.13          | 1.81  | 0.72  | (6.30,5.97) |
| 155     | 154        | 0.97          | 1.88  | 0.70  | (6.30,5.97) |
| 156     | 155        | -3.46         | 1.94  | 0.69  | (6.30,5.97) |
| 157     | 156        | -2.27         | 1.98  | 0.69  | (6.30,5.97) |
| 158     | 157        | 3.29          | 2.00  | 0.68  | (6.30,5.97) |
| 159     | 158        | 0.53          | 2.02  | 0.70  | (6.30,5.97) |
| 160     | 159        | -3.84         | 2.04  | 0.72  | (6.30,5.97) |
| 161     | 160        | 0.88          | 2.05  | 0.75  | (6.30,5.97) |
| 162     | 161        | 1.50          | 2.06  | 0.78  | (6.30,5.97) |
| 163     | 162        | 0.86          | 2.07  | 0.80  | (6.30,5.97) |
| 164     | 163        | -1.45         | 2.09  | 0.83  | (6.30,5.97) |
| 165     | 164        | -5.89         | 2.12  | 0.87  | (6.30,5.97) |
| 166     | 165        | 1.19          | 2.15  | 0.90  | (6.30,5.97) |
| 167     | 166        | 1.00          | 2.15  | 0.90  | (6.30,5.97) |
| 168     | 167        | 1.81          | 2.14  | 0.90  | (6.30,5.97) |
| 169     | 168        | -0.80         | 2.14  | 0.90  | (6.30,5.97) |
| 170     | 169        | 0.67          | 2.15  | 0.89  | (6.30,5.97) |
| 171     | 170        | 0.61          | 2.16  | 0.89  | (6.30,5.97) |
| 172     | 171        | 1.12          | 2.15  | 0.89  | (6.30,5.97) |
| 173     | 172        | -1.80         | 2.13  | 0.91  | (6.30,5.97) |
| 174     | 173        | -2.57         | 2.13  | 0.95  | (6.30,5.97) |
| 175     | 174        | -6.00         | 0.76  | -0.29 | (6.30,5.97) |
| 176     | 175        | 5.74          | 2.09  | 1.06  | (6.30,5.97) |
| 177     | 176        | -1.61         | 2.11  | 1.08  | (6.30,5.97) |
| 178     | 177        | -0.99         | 2.11  | 1.05  | (6.30,5.97) |
| 179     | 178        | 1.95          | 2.12  | 1.02  | (6.30,5.97) |
| 180     | 179        | 1.87          | 2.11  | 0.99  | (6.30,5.97) |
| 181     | 180        | -0.88         | 2.12  | 0.99  | (6.30,5.97) |
| 182     | 181        | 0.48          | 2.13  | 1.01  | (6.30,5.97) |
| 183     | 182        | -1.57         | 2.14  | 1.04  | (6.30,5.97) |
| 184     | 183        | -0.01         | 2.17  | 1.08  | (6.30,5.97) |
| 185     | 184        | -0.61         | 2.19  | 1.11  | (6.30,5.97) |
| 186     | 185        | -1.26         | 2.22  | 1.16  | (6.30,5.97) |
| 187     | 186        | -0.16         | 2.25  | 1.19  | (6.30,5.97) |
| 188     | 187        | -0.22         | 2.28  | 1.23  | (6.30,5.97) |
| 189     | 188        | 0.00          | 2.32  | 1.25  | (6.30,5.97) |
| 190     | 189        | -0.38         | 2.38  | 1.32  | (6.30,5.97) |
| 191     | 190        | 0.00          | 2.44  | 1.35  | (6.30,5.97) |
| 192     | 191        | 0.00          | 2.50  | 1.38  | (6.30,5.97) |
| 193     | 192        | 0.00          | 2.54  | 1.41  | (6.30,5.97) |
| 194     | 193        | -0.44         | 2.59  | 1.47  | (6.30,5.97) |
| 195     | 194        | -0.67         | 2.63  | 1.53  | (6.30,5.97) |
| 196     | 195        | 0.69          | 2.67  | 1.60  | (6.30,5.97) |
| 197     | 196        | -0.31         | 2.72  | 1.66  | (6.30,5.97) |
| 198     | 197        | -0.27         | 2.76  | 1.72  | (6.30,5.97) |
| 199     | 198        | -0.46         | 2.79  | 1.77  | (6.30,5.97) |
| 200     | 199        | -1.27         | 2.82  | 1.84  | (6.30,5.97) |
| 201     | 200        | 0.03          | 2.84  | 1.89  | (6.30,5.97) |
| 202     | 201        | 0.24          | 2.86  | 1.92  | (6.30,5.97) |
| 203     | 202        | 0.47          | 2.87  | 1.94  | (6.30,5.97) |
| 204     | 203        | 0.76          | 2.88  | 1.95  | (6.30,5.97) |
| 205     | 204        | -2.53         | 2.85  | 1.96  | (6.30,5.97) |
| 206     | 205        | -3.82         | 2.78  | 1.92  | (6.30,5.97) |
| 207     | 206        | -2.64         | 2.66  | 1.87  | (6.30,5.97) |
| 208     | 207        | 1.72          | 2.54  | 1.79  | (6.30,5.97) |
| 209     | 208        | 5.02          | 2.37  | 1.71  | (6.30,5.97) |
| 210     | 209        | -0.41         | 2.27  | 1.68  | (6.30,5.97) |
| 211     | 210        | 1.44          | 2.15  | 1.67  | (6.30,5.97) |
| 212     | 211        | -5.68         | 1.99  | 1.62  | (6.30,5.97) |
| 213     | 212        | 0.00          | 1.91  | 1.59  | (6.30,5.97) |
| 214     | 213        | -2.03         | 1.84  | 1.60  | (6.30,5.97) |
| 215     | 214        | -5.80         | 1.72  | 1.61  | (6.30,5.97) |
| 216     | 215        | -2.52         | 1.64  | 1.61  | (6.30,5.97) |
| 217     | 216        | -3.02         | 1.56  | 1.60  | (6.30,5.97) |
| 218     | 217        | -0.79         | 1.52  | 1.61  | (6.30,5.97) |
| 219     | 218        | -0.27         | 1.49  | 1.60  | (6.30,5.97) |
| 220     | 219        | -2.81         | 1.47  | 1.60  | (6.30,5.97) |
| 221     | 220        | -1.28         | 1.44  | 1.61  | (6.30,5.97) |
| 222     | 221        | -2.63         | 1.41  | 1.63  | (6.30,5.97) |
| 223     | 222        | -1.33         | 1.39  | 1.64  | (6.30,5.97) |
| 224     | 223        | -1.16         | 1.37  | 1.64  | (6.30,5.97) |
| 225     | 224        | -2.10         | 1.34  | 1.65  | (6.30,5.97) |
| 226     | 225        | -2.43         | 1.31  | 1.65  | (6.30,5.97) |
| 227     | 226        | -1.13         | 1.28  | 1.66  | (6.30,5.97) |
| 228     | 227        | -1.61         | 1.26  | 1.67  | (6.30,5.97) |
| 229     | 228        | 0.23          | 1.24  | 1.67  | (6.30,5.97) |
| 230     | 229        | -1.46         | 1.21  | 1.68  | (6.30,5.97) |
| 231     | 230        | -1.54         | 1.19  | 1.69  | (6.30,5.97) |
| 232     | 231        | 0.00          | 1.17  | 1.71  | (6.30,5.97) |
| 233     | 232        | -1.35         | 1.15  | 1.72  | (6.30,5.97) |
| 234     | 233        | 1.73          | 1.15  | 1.73  | (6.30,5.97) |
| 235     | 234        | -0.12         | 1.14  | 1.73  | (6.30,5.97) |
| 236     | 235        | -1.14         | 1.12  | 1.73  | (6.30,5.97) |
| 237     | 236        | -0.49         | 1.09  | 1.75  | (6.30,5.97) |
| 238     | 237        | -2.95         | 1.07  | 1.76  | (6.30,5.97) |
| 239     | 238        | -0.12         | 1.06  | 1.76  | (6.30,5.97) |
| 240     | 239        | -1.87         | 1.03  | 1.74  | (6.30,5.97) |
| 241     | 240        | -0.03         | 1.00  | 1.73  | (6.30,5.97) |
| 242     | 241        | -1.49         | 0.99  | 1.73  | (6.30,5.97) |
| 243     | 242        | 1.90          | 1.00  | 1.70  | (6.30,5.97) |
| 244     | 243        | 0.24          | 0.96  | 1.70  | (6.30,5.97) |
| 245     | 244        | 0.51          | 0.95  | 1.69  | (6.30,5.97) |
| 246     | 245        | -0.80         | 0.92  | 1.68  | (6.30,5.97) |
| 247     | 246        | -0.32         | 0.91  | 1.67  | (6.30,5.97) |
| 248     | 247        | -0.20         | 0.90  | 1.66  | (6.30,5.97) |
| 249     | 248        | -2.02         | 0.89  | 1.66  | (6.30,5.97) |
| 250     | 249        | -1.93         | 0.90  | 1.66  | (6.30,5.97) |
| 251     | 250        | 1.96          | 0.89  | 1.66  | (6.30,5.97) |
| 252     | 251        | 0.94          | 0.89  | 1.66  | (6.30,5.97) |
| 253     | 252        | -0.72         | 0.89  | 1.65  | (6.30,5.97) |
| 254     | 253        | -0.04         | 0.89  | 1.66  | (6.30,5.97) |
| 255     | 254        | -0.53         | 0.90  | 1.67  | (6.30,5.97) |
| 256     | 255        | 0.46          | 0.88  | 1.65  | (6.30,5.97) |
| 257     | 256        | -1.66         | 0.86  | 1.63  | (6.30,5.97) |
| 258     | 257        | -1.58         | 0.85  | 1.63  | (6.30,5.97) |
| 259     | 258        | 1.73          | 0.86  | 1.62  | (6.30,5.97) |
| 260     | 259        | -1.50         | 0.87  | 1.62  | (6.30,5.97) |
| 261     | 260        | -0.70         | 0.84  | 1.59  | (6.30,5.97) |
| 262     | 261        | -1.45         | 0.84  | 1.59  | (6.30,5.97) |
| 263     | 262        | 1.30          | 0.83  | 1.58  | (6.30,5.97) |
| 264     | 263        | -1.63         | 0.84  | 1.59  | (6.30,5.97) |
| 265     | 264        | 1.35          | 0.83  | 1.55  | (6.30,5.97) |
| 266     | 265        | -1.02         | 0.83  | 1.56  | (6.30,5.97) |
| 267     | 266        | 0.18          | 0.83  | 1.54  | (6.30,5.97) |
| 268     | 267        | 0.24          | 0.84  | 1.52  | (6.30,5.97) |
| 269     | 268        | -0.84         | 0.84  | 1.50  | (6.30,5.97) |
| 270     | 269        | -0.83         | 0.85  | 1.49  | (6.30,5.97) |
| 271     | 270        | 0.00          | 0.85  | 1.47  | (6.30,5.97) |
| 272     | 271        | 0.84          | 0.84  | 1.45  | (6.30,5.97) |
| 273     | 272        | 0.00          | 0.85  | 1.41  | (6.30,5.97) |
| 274     | 273        | 0.00          | 0.85  | 1.38  | (6.30,5.97) |
| 275     | 274        | -1.67         | 0.85  | 1.36  | (6.30,5.97) |
| 276     | 275        | -0.12         | 0.85  | 1.33  | (6.30,5.97) |
| 277     | 276        | -0.86         | 0.86  | 1.30  | (6.30,5.97) |
| 278     | 277        | -0.06         | 0.87  | 1.27  | (6.30,5.97) |
| 279     | 278        | -2.59         | 0.87  | 1.24  | (6.30,5.97) |
| 280     | 279        | -0.78         | 0.87  | 1.24  | (6.30,5.97) |
+---------+------------+---------------+-------+-------+-------------+
Successfully wrote lumen to output/oct/oct_lumen.obj
Successfully wrote lumen to output/oct/oct_replaced_lumen.obj

Alignment algorithm — parameter fine-tuning

from_array_singlepair (and all from_array_* variants) expose every alignment parameter. The example below uses from_array_singlepair with a record file (AIVUS-CAA combined CSV):

Key parameters:

  • step_rotation_deg / range_rotation_deg — angular search space; ±range at step resolution. Reducing range_rotation_deg when the orientation is roughly known cuts runtime substantially.

  • sample_size — contours are downsampled to at most this many points before Hausdorff distance computation. Default 500 is appropriate for most IVUS datasets.

  • image_center, radius, n_points — define the synthetic catheter used as a rotational anchor. Larger n_points weights the catheter more heavily; n_points=0 disables the catheter.

  • bruteforce — sweeps the full angular range without hierarchical refinement. Avoid for routine use.

  • smooth — 3-point moving average over each contour after alignment. Recommended.

  • postprocessing — equalises axial frame spacing across pullbacks. Recommended when heart rate differs between conditions (e.g. rest vs. stress).

record   = np.genfromtxt("ivus_rest/combined_sorted_manual.csv", delimiter=',', skip_header=1)
dia_cont = np.genfromtxt("ivus_rest/diastolic_contours.csv",       delimiter='\t')
dia_ref  = np.genfromtxt("ivus_rest/diastolic_reference_points.csv", delimiter='\t')
sys_cont = np.genfromtxt("ivus_rest/systolic_contours.csv",          delimiter='\t')
sys_ref  = np.genfromtxt("ivus_rest/systolic_reference_points.csv",  delimiter='\t')

dia_input = mm.numpy_to_inputdata(
    lumen_arr=dia_cont, ref_point=dia_ref,
    record=record, diastole=True, label="diastole_rest",
)
sys_input = mm.numpy_to_inputdata(
    lumen_arr=sys_cont, ref_point=sys_ref,
    record=record, diastole=False, label="systole_rest",
)

rest_array, (dia_logs, sys_logs) = mm.from_array_singlepair(
    input_data_a=dia_input,
    input_data_b=sys_input,
    step_rotation_deg=0.01,
    range_rotation_deg=60,
    output_path="output/rest_array",
    interpolation_steps=0,
    smooth=True,
    postprocessing=True,
)
print("Logs (dia):", dia_logs[:3], "...")
✅ Successfully built geometry from input data
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: diastole_rest
Diastole phase: Yes

Logs (dia): [(1, 0, -7.709999999999979, -0.12124275208313184, 0.3603055547533245, 3.5949458260233564, 5.610358740916264), (2, 1, -43.34, -1.0674125981869906, 2.7305771329569084, 3.5949458260233564, 5.610358740916264), (3, 2, 11.049999999999999, -0.6964904200670023, 2.598353951083425, 3.5949458260233564, 5.610358740916264)] ...

✅ Successfully built geometry from input data
-----------------------------------------
✅ Lumen
❌ Eem
❌ Calcification
❌ Sidebranch
✅ Catheter
-----------------------------------------
Label: systole_rest
Diastole phase: No


+-------------------------------------------------------------------+
|       ✅ Finished aligning 'systole_rest' (anomalous: true)        |
+---------+------------+---------------+-------+------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty  |  Centroid   |
+---------+------------+---------------+-------+------+-------------+
| 1       | 0          | 2.23          | 0.19  | 0.01 | (3.82,5.13) |
| 2       | 1          | 8.17          | -0.35 | 0.16 | (3.82,5.13) |
| 3       | 2          | -10.33        | -0.24 | 0.54 | (3.82,5.13) |
| 4       | 3          | -4.10         | -0.53 | 0.95 | (3.82,5.13) |
| 5       | 4          | 0.82          | -0.56 | 0.27 | (3.82,5.13) |
| 6       | 5          | 5.14          | -0.60 | 0.66 | (3.82,5.13) |
| 7       | 6          | -6.48         | -1.01 | 0.70 | (3.82,5.13) |
| 8       | 7          | 14.71         | -0.89 | 0.82 | (3.82,5.13) |
| 9       | 8          | -35.31        | -0.84 | 0.25 | (3.82,5.13) |
| 10      | 9          | 25.53         | -1.27 | 0.29 | (3.82,5.13) |
| 11      | 10         | 41.32         | -2.21 | 0.74 | (3.82,5.13) |
| 12      | 11         | -21.19        | -1.75 | 0.69 | (3.82,5.13) |
| 13      | 12         | 4.84          | -1.87 | 0.67 | (3.82,5.13) |
| 14      | 13         | -6.91         | -1.95 | 0.53 | (3.82,5.13) |
| 15      | 14         | 23.44         | -2.08 | 0.47 | (3.82,5.13) |
| 16      | 15         | 7.25          | -2.09 | 0.54 | (3.82,5.13) |
+---------+------------+---------------+-------+------+-------------+

+-------------------------------------------------------------------+
|       ✅ Finished aligning 'diastole_rest' (anomalous: true)       |
+---------+------------+---------------+-------+------+-------------+
| Contour | Matched To | Rotation (°) |  Tx   |  Ty  |  Centroid   |
+---------+------------+---------------+-------+------+-------------+
| 1       | 0          | -7.71         | -0.12 | 0.36 | (3.59,5.61) |
| 2       | 1          | -43.34        | -1.07 | 2.73 | (3.59,5.61) |
| 3       | 2          | 11.05         | -0.70 | 2.60 | (3.59,5.61) |
| 4       | 3          | 0.98          | -0.62 | 2.71 | (3.59,5.61) |
| 5       | 4          | -3.39         | -0.80 | 2.83 | (3.59,5.61) |
| 6       | 5          | -60.00        | -1.80 | 1.63 | (3.59,5.61) |
| 7       | 6          | 47.39         | -1.66 | 2.23 | (3.59,5.61) |
| 8       | 7          | 3.29          | -1.57 | 2.31 | (3.59,5.61) |
| 9       | 8          | 1.02          | -1.63 | 2.44 | (3.59,5.61) |
| 10      | 9          | -60.00        | -1.49 | 1.22 | (3.59,5.61) |
| 11      | 10         | -1.47         | -1.41 | 1.23 | (3.59,5.61) |
| 12      | 11         | 47.99         | -1.23 | 2.29 | (3.59,5.61) |
| 13      | 12         | 6.95          | -1.08 | 2.48 | (3.59,5.61) |
| 14      | 13         | -54.00        | -1.06 | 1.18 | (3.59,5.61) |
| 15      | 14         | 19.24         | -0.40 | 0.39 | (3.59,5.61) |
| 16      | 15         | -13.07        | -0.29 | 0.81 | (3.59,5.61) |
| 17      | 16         | -36.24        | -0.60 | 1.46 | (3.59,5.61) |
| 18      | 17         | 5.08          | -0.44 | 1.25 | (3.59,5.61) |
| 19      | 18         | -2.25         | -0.08 | 0.20 | (3.59,5.61) |
+---------+------------+---------------+-------+------+-------------+

✅ Aligned geometry 'systole_rest' to 'diastole_rest'
-----------------------------------------
Applied initial translation: (-0.22, 0.48, -2.67) mm
Found best rotation of 11.56° with parameters: 
range: 60.00° 
step size: 0.01°
Applied final translation: ( 0, 0.00, -0.00) mm
-----------------------------------------

Saving files for 'diastole_rest - systole_rest' to 'output/rest_array'
LUMEN .obj files: 2/2 written successfully
CATHETER .obj files: 2/2 written successfully
WALL .obj files: 2/2 written successfully

4. Alignment with a centerline

A PyCenterline is built from a raw (N×3) coordinate array — no index column required:

cl_raw    = np.genfromtxt("centerline_raw.csv", delimiter=',')
centerline = mm.numpy_to_centerline(cl_raw)

align_three_point co-registers a geometry pair onto the centerline using three anatomical landmarks: an aortic reference point, an upper vessel point, and a lower vessel point. The preferred method is align_combined, which additionally optimizes against a CCTA point cloud (see the CCTA tutorial for how to prepare results["rca_points"]).

cl_raw = np.genfromtxt("centerline_raw.csv", delimiter=',')
cl     = mm.numpy_to_centerline(cl_raw)

aligned_geometry, resampled_cl = mm.align_three_point(
    centerline=cl,
    geometry=rest,   # PyGeometryPair from from_file_full above
    main_ref_pt=(12.2605, -201.3643, 1751.0554),
    counterclockwise_ref_pt=(11.7567, -202.1920, 1754.7975),
    clockwise_ref_pt=(15.6605, -202.1920, 1749.9655),
    write=True,
    watertight=False,
    interpolation_steps=0,
)
print("Resampled centerline (first 3 points):", mm.to_array(resampled_cl)[:3])
---------------------Centerline alignment: Finding optimal rotation---------------------
Resampled centerline (first 3 points): [[ 0.00000000e+00  1.30847000e+01 -2.00350800e+02  1.75186020e+03]
 [ 1.00000000e+00  1.35807358e+01 -2.01462354e+02  1.75228890e+03]
 [ 2.00000000e+00  1.38271910e+01 -2.02714193e+02  1.75244535e+03]]
✅ Best angle found: 75.00°

Saving files for 'None' to 'output/aligned'
LUMEN .obj files: 2/2 written successfully
CATHETER .obj files: 2/2 written successfully
WALL .obj files: 2/2 written successfully
resample_centerline_by_contours: centroid_count=14, centroid_mean_spacing=Some(1.2955758552631584), centerline_length=161.5421151319061, spacing=1.295576
resample_centerline_by_contours: produced 125 points
# Write before/after alignment meshes with explicit names and visualise
mm.to_obj(aligned_geometry.geom_a, "output/aligned", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="dia")
mm.to_obj(aligned_geometry.geom_b, "output/aligned", watertight=False,
          contour_types=[mm.PyContourType.Lumen], filename_prefix="sys")

plot_pair(
    before_paths=["output/after/dia_lumen.obj", "output/after/sys_lumen.obj"],
    after_paths=["output/aligned/dia_lumen.obj", "output/aligned/sys_lumen.obj"],
    colors=["royalblue", "firebrick"],
    titles=["Before Alignment", "After Alignment"],
)
Successfully wrote lumen to output/aligned/dia_lumen.obj
Successfully wrote lumen to output/aligned/sys_lumen.obj

5. Saving geometries as .obj files

to_obj can be called on any PyGeometryPair or PyGeometry to export all or selected contour layers. The filename_prefix determines the output file stem, and contour_types selects which layers to export:

# Export lumen and catheter from the aligned rest geometry
mm.to_obj(
    rest.geom_a,
    "output/rest_export",
    watertight=False,
    contour_types=[mm.PyContourType.Lumen, mm.PyContourType.Catheter],
    filename_prefix="aligned",
)
# Creates: output/rest_export/aligned_lumen.obj  and  aligned_catheter.obj
import os
print(os.listdir("output/rest_export"))
Successfully wrote lumen to output/rest_export/aligned_lumen.obj
Successfully wrote catheter to output/rest_export/aligned_catheter.obj
['aligned_catheter.mtl', 'aligned_catheter.obj', 'aligned_lumen.mtl', 'aligned_lumen.obj']

6. Utility functions: to_array and numpy_to_geometry

to_array

to_array is a single-dispatch converter from any Py* object to numpy arrays. The return type depends on the input:

Input type

Return

PyContour or PyCenterline

ndarray shape (N, 4): frame_index, x, y, z

PyFrame or PyGeometry

dict[str, ndarray] with keys lumen, eem, … reference

PyGeometryPair

tuple[dict, dict] — one dict per geometry

PyInputData

dict with layer keys plus diastole and label

# PyGeometryPair → (dict, dict)
rest_dia_arr, rest_sys_arr = mm.to_array(rest)
print("Lumen array shape (geom_a):", rest_dia_arr["lumen"].shape)

# PyGeometry → dict
geom_arr = mm.to_array(rest.geom_a)
print("Keys:", list(geom_arr.keys()))

# PyCenterline → ndarray
cl_arr = mm.to_array(resampled_cl)
print("Centerline array shape:", cl_arr.shape)

# PyContour → ndarray
contour_arr = mm.to_array(rest.geom_a.frames[-1].lumen)
print("Contour array shape:", contour_arr.shape)

# PyInputData → dict
input_arr = mm.to_array(dia_input)
print("InputData keys:", list(input_arr.keys()))
Lumen array shape (geom_a): (7014, 4)
Keys: ['lumen', 'eem', 'calcification', 'sidebranch', 'catheter', 'wall', 'reference']
Centerline array shape: (125, 4)
Contour array shape: (501, 4)
InputData keys: ['lumen', 'eem', 'calcification', 'sidebranch', 'reference', 'diastole', 'label', 'records']

numpy_to_geometry

When you already have aligned contour data as numpy arrays and want to construct a PyGeometry directly — for example after custom post-processing — use numpy_to_geometry:

# Reconstruct a PyGeometry from numpy arrays
lumen_np = rest_dia_arr["lumen"]
ref_np   = rest_dia_arr["reference"]

reconstructed = mm.numpy_to_geometry(
    lumen_arr=lumen_np,
    eem_arr=np.array([]),
    catheter_arr=np.array([]),
    wall_arr=np.array([]),
    reference_arr=ref_np,
    label="reconstructed_rest_dia",
)
print(f"Frames: {len(reconstructed.frames)}, "
      f"first frame lumen points: {len(reconstructed.frames[0].lumen.points)}")
Frames: 14, first frame lumen points: 501

7. Class-level methods

PyContour

All transformation methods return a new object and leave the original unchanged:

contour = rest.geom_a.frames[0].lumen

# Geometry queries
area          = contour.get_area()
elliptic_ratio = contour.get_elliptic_ratio()
pts_list      = contour.points_as_tuples()
(p1, p2), dist_close  = contour.find_closest_opposite()
(p1, p2), dist_far    = contour.find_farthest_points()

print(f"Area: {area:.4f} mm²  |  Elliptic ratio: {elliptic_ratio:.4f}")
print(f"Min diameter: {dist_close:.4f} mm  |  Max diameter: {dist_far:.4f} mm")

# Transformations (all return new PyContour)
contour_rot  = contour.rotate(20.0)
contour_trsl = contour_rot.translate(0.0, 1.0, 2.0)
contour_sort = contour.sort_contour_points()

# Write a modified contour back into the geometry
old_frame = rest.geom_a.frames[2]
new_lumen = old_frame.lumen.rotate(20.0)
new_frame = mm.PyFrame(
    id=old_frame.id,
    centroid=old_frame.centroid,
    lumen=new_lumen,
    extras=old_frame.extras,
    reference_point=old_frame.reference_point,
)
modified_geom = rest.geom_a.replace_frame(2, new_frame)
print("Frame 2 replaced:", modified_geom.frames[2].lumen is new_lumen)
Area: 5.6366 mm²  |  Elliptic ratio: 4.5865
Min diameter: 1.1578 mm  |  Max diameter: 5.3083 mm
Frame 2 replaced: False

PyFrame and PyGeometry

PyFrame mirrors the contour transformations and applies them to all layers:

frame = rest.geom_a.frames[0]
frame_rot    = frame.rotate(20.0)
frame_trsl   = frame.translate(0.0, 1.0, 2.0)
frame_sorted = frame.sort_frame_points()

# PyGeometry geometry-level operations
geom_smooth  = rest.geom_a.smooth_frames()
geom_ds      = rest.geom_a.downsample(100)
geom_sorted  = rest.geom_a.sort_frame_points()
geom_centred = rest.geom_a.center_to_contour(mm.PyContourType.Catheter)
geom_rot     = rest.geom_a.rotate(20.0)
geom_trsl    = geom_rot.translate(0.0, 1.0, 2.0)

print(f"Original contour points: {len(rest.geom_a.frames[0].lumen.points)}")
print(f"Downsampled:             {len(geom_ds.frames[0].lumen.points)}")

# Frame access
frame_by_idx = rest.geom_a.get_frame_at_index(5)
frame_by_z   = rest.geom_a.get_frame_at_z(12.5)
lumen_list   = rest.geom_a.get_lumen_contours()
wall_list    = rest.geom_a.get_contours_by_type("Wall")

print(f"Frame at index 5 z: {frame_by_idx.lumen.points[0].z:.3f}")
print(f"Total lumen contours: {len(lumen_list)}")

# PyGeometryPair summary
(summary_a, summary_b), deform = rest.get_summary()
print(f"\nPair summary A: {summary_a}")
print(f"Pair summary B: {summary_b}")
print(f"Deformation table shape: {np.array(deform).shape}")
Original contour points: 501
Downsampled:             100
Frame at index 5 z: 6.478
Total lumen contours: 14

Pair summary A: (5.559206007496853, 0.6780775439844883, 10.364606842105268)+----+----------+-----------+----------+-----------+-------+
| id | area_dia | ellip_dia | area_sys | ellip_sys |   z   |
+----+----------+-----------+----------+-----------+-------+
| 0  | 5.64     | 4.59      | 6.11     | 4.35      | 0.00  |
| 1  | 5.80     | 4.75      | 6.26     | 3.39      | 1.30  |

Pair summary B: (6.111481670202526, 0.663038559502465, 7.7734551315789515)
Deformation table shape: (14, 6)
| 2  | 5.70     | 3.70      | 6.49     | 2.85      | 2.59  |
| 3  | 5.56     | 2.57      | 6.85     | 2.18      | 3.89  |
| 4  | 5.59     | 1.64      | 7.59     | 1.91      | 5.18  |
| 5  | 6.42     | 1.51      | 7.57     | 1.68      | 6.48  |
| 6  | 7.26     | 1.37      | 7.31     | 1.63      | 7.77  |
| 7  | 7.74     | 1.25      | 10.23    | 1.33      | 9.07  |
| 8  | 7.77     | 1.43      | 11.90    | 1.20      | 10.36 |
| 9  | 9.05     | 1.33      | 14.15    | 1.09      | 11.66 |
| 10 | 11.92    | 1.18      | 11.70    | 1.17      | 12.96 |
| 11 | 15.27    | 1.08      | 13.57    | 1.15      | 14.25 |
| 12 | 17.27    | 1.06      | 15.42    | 1.11      | 15.55 |
| 13 | 16.34    | 1.04      | 18.14    | 1.07      | 16.84 |
+----+----------+-----------+----------+-----------+-------+