{ "cells": [ { "cell_type": "markdown", "id": "8a8e95e7", "metadata": {}, "source": [ "# **multimoda-rs**: Intravascular Module – Notebook Tutorial\n", "\n", "This notebook follows the [Intravascular Alignment Tutorial](../tutorial_intravascular.rst) step-by-step:\n", "\n", "1. General note on segmentation preparation\n", "2. Workflow from CSV files (AIVUS-CAA format)\n", "3. Workflow from numpy arrays — including parameter fine-tuning\n", "4. Alignment with a CCTA centerline\n", "5. Saving geometries as `.obj` files\n", "6. Utility functions: `to_array` and `numpy_to_geometry`\n", "7. Class-level methods for `PyContour`, `PyFrame`, and `PyGeometry`\n", "\n", "> **Performance note:** Alignment is compute-intensive. Running the notebook via the console is faster than interactive cell-by-cell execution." ] }, { "cell_type": "markdown", "id": "539ebfaf", "metadata": {}, "source": [ "## 1. General note on segmentation preparation\n", "\n", "The package works with segmented intravascular images from IVUS or OCT.\n", "Segmentation must follow specific conventions for optimal results:\n", "\n", "- Files follow AIVUS-CAA convention: `diastolic_contours.csv`, `systolic_contours.csv`,\n", " `diastolic_reference_points.csv`, `systolic_reference_points.csv`.\n", "- Each file has columns `(frame_index, x_mm, y_mm, z_mm)` — **no header row**.\n", "- A correct anatomical **reference point** is essential for downstream centerline co-registration.\n", "\n", "For CAD a bifurcation serves as reference; for AAOCA the reference is placed at the aortic side of\n", "the most proximal fully closed lumen contour (see tutorial for details).\n", "\n", "**Four processing modes**\n", "\n", "| Mode | Inputs | Typical use-case |\n", "|------|--------|-----------------|\n", "| `full` | 4 states (rest-dia, rest-sys, stress-dia, stress-sys) | AAOCA pulsatile lumen analysis |\n", "| `doublepair` | 2 independent pullbacks, each with dia+sys | Pre/post-stenting across both phases |\n", "| `singlepair` | 1 pullback with dia+sys | CAD: diastole vs. systole within one pullback |\n", "| `single` | 1 pullback (dia or sys only) | Standalone reconstruction (CAD, OCT) |" ] }, { "cell_type": "code", "execution_count": 1, "id": "f7b1f230", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:13:32.349348Z", "iopub.status.busy": "2026-05-04T15:13:32.349130Z", "iopub.status.idle": "2026-05-04T15:13:50.293709Z", "shell.execute_reply": "2026-05-04T15:13:50.292360Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Working directory: /mnt/c/WorkingData/Documents/2_Coding/Rust/multimodars/examples/data\n" ] } ], "source": [ "import os\n", "from pathlib import Path\n", "import numpy as np\n", "import multimodars as mm\n", "\n", "cwd = Path.cwd()\n", "for candidate in [cwd, cwd.parent, cwd.parent.parent]:\n", " if (candidate / \"examples\" / \"data\").exists():\n", " os.chdir(candidate / \"examples\" / \"data\")\n", " break\n", " elif (candidate / \"data\").exists():\n", " os.chdir(candidate / \"data\")\n", " break\n", "print(f\"Working directory: {os.getcwd()}\")" ] }, { "cell_type": "code", "execution_count": 2, "id": "996ea018", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:13:50.296342Z", "iopub.status.busy": "2026-05-04T15:13:50.295949Z", "iopub.status.idle": "2026-05-04T15:13:59.154174Z", "shell.execute_reply": "2026-05-04T15:13:59.153230Z" }, "tags": [ "remove-input" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ "%pip --disable-pip-version-check install trimesh plotly scipy nbformat | grep -v 'already satisfied'\n", "\n", "\n", "import trimesh\n", "import plotly.graph_objects as go\n", "import plotly.io as pio\n", "pio.renderers.default = \"notebook_connected\"\n", "from plotly.subplots import make_subplots\n", "\n", "\n", "def trimesh_to_mesh3d(mesh, color, name, opacity=0.6):\n", " verts = mesh.vertices\n", " faces = mesh.faces\n", " return go.Mesh3d(\n", " x=verts[:, 0], y=verts[:, 1], z=verts[:, 2],\n", " i=faces[:, 0], j=faces[:, 1], k=faces[:, 2],\n", " color=color, opacity=opacity, name=name, flatshading=True,\n", " )\n", "\n", "\n", "def plot_pair(before_paths, after_paths, colors, titles):\n", " \"\"\"Side-by-side 3D comparison of two mesh lists.\"\"\"\n", " before_meshes = [trimesh.load(p) for p in before_paths]\n", " after_meshes = [trimesh.load(p) for p in after_paths]\n", " fig = make_subplots(\n", " rows=1, cols=2,\n", " specs=[[{\"type\": \"scene\"}, {\"type\": \"scene\"}]],\n", " subplot_titles=titles,\n", " )\n", " labels = [\"diastole\", \"systole\"]\n", " for mesh, color, label in zip(before_meshes, colors, labels):\n", " fig.add_trace(trimesh_to_mesh3d(mesh, color, f\"before_{label}\"), row=1, col=1)\n", " for mesh, color, label in zip(after_meshes, colors, labels):\n", " fig.add_trace(trimesh_to_mesh3d(mesh, color, f\"after_{label}\"), row=1, col=2)\n", " camera = dict(eye=dict(x=1.5, y=1.5, z=1.0))\n", " fig.update_layout(\n", " width=900, height=450,\n", " scene_camera=camera, scene2_camera=camera,\n", " scene=dict(aspectmode=\"data\"), scene2=dict(aspectmode=\"data\"),\n", " margin=dict(l=0, r=0, t=30, b=0),\n", " )\n", " fig.show()" ] }, { "cell_type": "markdown", "id": "3418ff43", "metadata": {}, "source": [ "## 2. Workflow from CSV files\n", "\n", "This section uses AIVUS-CAA CSV files.\n", "`from_file_full` processes all four states simultaneously (rest-diastole, rest-systole,\n", "stress-diastole, stress-systole) and writes aligned meshes to four output directories.\n", "\n", "See the [tutorial](../tutorial_intravascular.rst) for a detailed description of the CSV format." ] }, { "cell_type": "code", "execution_count": 3, "id": "45003053", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:13:59.156581Z", "iopub.status.busy": "2026-05-04T15:13:59.156372Z", "iopub.status.idle": "2026-05-04T15:14:15.511118Z", "shell.execute_reply": "2026-05-04T15:14:15.510168Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "✅ Successfully built geometry from path\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: rest_dia\n", "Diastole phase: Yes\n", "\n", "\n", "✅ Successfully built geometry from path\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: rest_sys\n", "Diastole phase: No\n", "\n", "\n", "✅ Successfully built geometry from path\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: stress_dia\n", "Diastole phase: Yes\n", "\n", "\n", "✅ Successfully built geometry from path\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: stress_sys\n", "Diastole phase: No\n", "\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'rest_sys' (anomalous: true) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | -2.30 | -0.19 | -0.01 | (3.63,5.12) |\n", "| 2 | 1 | 12.20 | -0.54 | 0.15 | (3.63,5.12) |\n", "| 3 | 2 | -10.30 | -0.44 | 0.53 | (3.63,5.12) |\n", "| 4 | 3 | -4.10 | -0.72 | 0.94 | (3.63,5.12) |\n", "| 5 | 4 | 0.80 | -0.75 | 0.26 | (3.63,5.12) |\n", "| 6 | 5 | 5.10 | -0.79 | 0.65 | (3.63,5.12) |\n", "| 7 | 6 | -6.50 | -1.21 | 0.69 | (3.63,5.12) |\n", "| 8 | 7 | 14.70 | -1.08 | 0.81 | (3.63,5.12) |\n", "| 9 | 8 | -35.30 | -1.03 | 0.24 | (3.63,5.12) |\n", "| 10 | 9 | 25.50 | -1.46 | 0.28 | (3.63,5.12) |\n", "| 11 | 10 | 41.30 | -2.40 | 0.73 | (3.63,5.12) |\n", "| 12 | 11 | -21.20 | -1.95 | 0.68 | (3.63,5.12) |\n", "| 13 | 12 | 4.80 | -2.06 | 0.66 | (3.63,5.12) |\n", "| 14 | 13 | -6.90 | -2.14 | 0.52 | (3.63,5.12) |\n", "| 15 | 14 | 23.40 | -2.27 | 0.46 | (3.63,5.12) |\n", "| 16 | 15 | 7.30 | -2.28 | 0.53 | (3.63,5.12) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'stress_dia' (anomalous: true) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | 3.80 | -0.10 | 0.02 | (3.60,4.59) |\n", "| 2 | 1 | 6.20 | -0.38 | 0.40 | (3.60,4.59) |\n", "| 3 | 2 | 1.90 | -0.24 | 0.42 | (3.60,4.59) |\n", "| 4 | 3 | -1.60 | -0.11 | 0.49 | (3.60,4.59) |\n", "| 5 | 4 | -1.90 | -0.22 | 0.50 | (3.60,4.59) |\n", "| 6 | 5 | 0.70 | -0.03 | 0.10 | (3.60,4.59) |\n", "| 7 | 6 | 3.20 | -0.05 | 0.09 | (3.60,4.59) |\n", "| 8 | 7 | 0.80 | -0.11 | -0.09 | (3.60,4.59) |\n", "| 9 | 8 | 2.40 | -0.14 | -0.13 | (3.60,4.59) |\n", "| 10 | 9 | 14.60 | -0.75 | 0.01 | (3.60,4.59) |\n", "| 11 | 10 | 0.00 | -1.08 | 0.08 | (3.60,4.59) |\n", "| 12 | 11 | 8.80 | -1.21 | 0.08 | (3.60,4.59) |\n", "| 13 | 12 | 11.80 | -1.28 | 0.20 | (3.60,4.59) |\n", "| 14 | 13 | -14.20 | -1.46 | -0.02 | (3.60,4.59) |\n", "| 15 | 14 | 22.50 | -1.55 | 0.29 | (3.60,4.59) |\n", "| 16 | 15 | -13.20 | -1.72 | 0.16 | (3.60,4.59) |\n", "| 17 | 16 | -18.90 | -1.61 | -0.27 | (3.60,4.59) |\n", "| 18 | 17 | -2.70 | -1.41 | -0.44 | (3.60,4.59) |\n", "| 19 | 18 | 7.50 | -1.74 | -0.31 | (3.60,4.59) |\n", "| 20 | 19 | 10.70 | -1.77 | -0.55 | (3.60,4.59) |\n", "| 21 | 20 | 18.00 | -2.30 | -0.37 | (3.60,4.59) |\n", "| 22 | 21 | 0.00 | -2.04 | -0.25 | (3.60,4.59) |\n", "| 23 | 22 | 24.70 | -2.34 | 0.36 | (3.60,4.59) |\n", "| 24 | 23 | 4.10 | -2.29 | 0.78 | (3.60,4.59) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'rest_dia' (anomalous: true) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | 7.70 | 0.12 | -0.36 | (3.72,5.25) |\n", "| 2 | 1 | 0.70 | 0.04 | -0.16 | (3.72,5.25) |\n", "| 3 | 2 | 6.20 | -0.28 | 0.03 | (3.72,5.25) |\n", "| 4 | 3 | -13.10 | -0.16 | 0.45 | (3.72,5.25) |\n", "| 5 | 4 | -23.90 | -0.32 | 0.89 | (3.72,5.25) |\n", "| 6 | 5 | -5.10 | -0.48 | 1.10 | (3.72,5.25) |\n", "| 7 | 6 | -29.80 | -0.94 | 0.82 | (3.72,5.25) |\n", "| 8 | 7 | -18.00 | -1.29 | 0.87 | (3.72,5.25) |\n", "| 9 | 8 | 1.50 | -1.36 | 0.86 | (3.72,5.25) |\n", "| 10 | 9 | -1.20 | -1.68 | 1.27 | (3.72,5.25) |\n", "| 11 | 10 | 47.40 | -1.54 | 1.87 | (3.72,5.25) |\n", "| 12 | 11 | 3.30 | -1.45 | 1.95 | (3.72,5.25) |\n", "| 13 | 12 | 1.00 | -1.51 | 2.08 | (3.72,5.25) |\n", "| 14 | 13 | 30.60 | -0.96 | 2.12 | (3.72,5.25) |\n", "| 15 | 14 | -7.00 | -1.11 | 1.93 | (3.72,5.25) |\n", "| 16 | 15 | 19.40 | -0.68 | 2" ] }, { "name": "stderr", "output_type": "stream", "text": [ "sidebranch file not found, skipping: \"ivus_rest/branch_diastolic_contours.csv\"\n", "process_directory: unknown mapping name 'catheter', skipping\n", "eem file not found, skipping: \"ivus_rest/eem_diastolic_contours.csv\"\n", "calcification file not found, skipping: \"ivus_rest/calcium_diastolic_contours.csv\"\n", "process_directory: unknown mapping name 'catheter', skipping\n", "calcification file not found, skipping: \"ivus_rest/calcium_systolic_contours.csv\"\n", "eem file not found, skipping: \"ivus_rest/eem_systolic_contours.csv\"\n", "sidebranch file not found, skipping: \"ivus_rest/branch_systolic_contours.csv\"\n", "sidebranch file not found, skipping: \"ivus_stress/branch_diastolic_contours.csv\"\n", "calcification file not found, skipping: \"ivus_stress/calcium_diastolic_contours.csv\"\n", "eem file not found, skipping: \"ivus_stress/eem_diastolic_contours.csv\"\n", "process_directory: unknown mapping name 'catheter', skipping\n", "sidebranch file not found, skipping: \"ivus_stress/branch_systolic_contours.csv\"\n", "calcification file not found, skipping: \"ivus_stress/calcium_systolic_contours.csv\"\n", "process_directory: unknown mapping name 'catheter', skipping\n", "eem file not found, skipping: \"ivus_stress/eem_systolic_contours.csv\"\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ ".47 | (3.72,5.25) |\n", "| 17 | 16 | 3.40 | -0.50 | 2.35 | (3.72,5.25) |\n", "| 18 | 17 | -0.90 | -0.58 | 2.24 | (3.72,5.25) |\n", "| 19 | 18 | -11.00 | -0.95 | 2.37 | (3.72,5.25) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'stress_sys' (anomalous: true) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | 0.00 | -0.22 | 0.10 | (3.80,4.64) |\n", "| 2 | 1 | 1.00 | -0.19 | 0.14 | (3.80,4.64) |\n", "| 3 | 2 | 0.00 | -0.04 | 0.17 | (3.80,4.64) |\n", "| 4 | 3 | -1.20 | 0.08 | 0.15 | (3.80,4.64) |\n", "| 5 | 4 | -1.10 | 0.03 | 0.09 | (3.80,4.64) |\n", "| 6 | 5 | 0.20 | 0.06 | 0.06 | (3.80,4.64) |\n", "| 7 | 6 | 7.30 | 0.06 | 0.24 | (3.80,4.64) |\n", "| 8 | 7 | 2.60 | -0.30 | 0.00 | (3.80,4.64) |\n", "| 9 | 8 | 11.50 | -0.32 | -0.18 | (3.80,4.64) |\n", "| 10 | 9 | 9.50 | -0.57 | -0.32 | (3.80,4.64) |\n", "| 11 | 10 | -1.00 | -0.53 | -0.36 | (3.80,4.64) |\n", "| 12 | 11 | 3.40 | -0.49 | -0.39 | (3.80,4.64) |\n", "| 13 | 12 | -4.10 | -0.52 | -0.46 | (3.80,4.64) |\n", "| 14 | 13 | 8.40 | -0.63 | -0.37 | (3.80,4.64) |\n", "| 15 | 14 | -2.10 | -0.76 | -0.43 | (3.80,4.64) |\n", "| 16 | 15 | 8.50 | -0.79 | -0.41 | (3.80,4.64) |\n", "| 17 | 16 | -6.00 | -0.81 | -0.81 | (3.80,4.64) |\n", "| 18 | 17 | 0.00 | -0.83 | -1.10 | (3.80,4.64) |\n", "| 19 | 18 | 0.40 | -0.82 | -1.12 | (3.80,4.64) |\n", "| 20 | 19 | 3.20 | -0.89 | -1.13 | (3.80,4.64) |\n", "| 21 | 20 | 9.50 | -0.96 | -1.14 | (3.80,4.64) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "\n", "✅ Aligned geometry 'stress_sys' to 'stress_dia'\n", "-----------------------------------------\n", "Applied initial translation: (-0.20, -0.05, 0.00) mm\n", "Found best rotation of 2.70° with parameters: \n", "range: 90.00° \n", "step size: 0.1°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "✅ Aligned geometry 'rest_sys' to 'rest_dia'\n", "-----------------------------------------\n", "Applied initial translation: (0.09, 0.13, -3.94) mm\n", "Found best rotation of 3.00° with parameters: \n", "range: 90.00° \n", "step size: 0.1°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "✅ Aligned geometry 'stress_sys' to 'rest_sys'\n", "-----------------------------------------\n", "Applied initial translation: (0.12, 0.66, 0.00) mm\n", "Found best rotation of 1.80° with parameters: \n", "range: 90.00° \n", "step size: 0.1°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "✅ Aligned geometry 'stress_dia' to 'rest_dia'\n", "-----------------------------------------\n", "Applied initial translation: (0.12, 0.66, 0.00) mm\n", "Found best rotation of 17.80° with parameters: \n", "range: 90.00° \n", "step size: 0.1°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "Saving files for 'rest_dia - rest_sys' to 'output/rest'\n", "LUMEN .obj files: 30/30 written successfully\n", "CATHETER .obj files: 30/30 written successfully\n", "WALL .obj files: 30/30 written successfully\n", "\n", "Saving files for 'stress_dia - stress_sys' to 'output/stress'\n", "LUMEN .obj files: 30/30 written successfully\n", "CATHETER .obj files: 30/30 written successfully\n", "WALL .obj files: 30/30 written successfully\n", "\n", "Saving files for 'rest_dia - stress_dia' to 'output/diastole'\n", "LUMEN .obj files: 30/30 written successfully\n", "CATHETER .obj files: 30/30 written successfully\n", "WALL .obj files: 30/30 written successfully\n", "\n", "Saving files for 'rest_sys - stress_sys' to 'output/systole'\n", "LUMEN .obj files: 30/30 written successfully\n", "CATHETER .obj files: 30/30 written successfully\n", "WALL .obj files: 30/30 written successfully\n" ] } ], "source": [ "# Full 4-state workflow: rest vs. stress, diastole vs. systole\n", "rest, stress, dia, sys, _ = mm.from_file_full(\n", " input_path_ab=\"ivus_rest\",\n", " input_path_cd=\"ivus_stress\",\n", " labels=[\"rest_dia\", \"rest_sys\", \"stress_dia\", \"stress_sys\"],\n", " step_rotation_deg=0.1,\n", " range_rotation_deg=90,\n", " image_center=(4.5, 4.5),\n", " radius=0.5,\n", " n_points=20,\n", " write_obj=True,\n", " watertight=False,\n", " contour_types=[mm.PyContourType.Lumen, mm.PyContourType.Catheter, mm.PyContourType.Wall],\n", " output_path_ab=\"output/rest\",\n", " output_path_cd=\"output/stress\",\n", " output_path_ac=\"output/diastole\",\n", " output_path_bd=\"output/systole\",\n", " interpolation_steps=28,\n", ")" ] }, { "cell_type": "code", "execution_count": 4, "id": "6650d4ec", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:14:15.512728Z", "iopub.status.busy": "2026-05-04T15:14:15.512536Z", "iopub.status.idle": "2026-05-04T15:14:15.995755Z", "shell.execute_reply": "2026-05-04T15:14:15.994611Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Successfully wrote lumen to output/before/dia_lumen.obj\n", "Successfully wrote lumen to output/before/sys_lumen.obj\n", "Successfully wrote lumen to output/after/dia_lumen.obj\n", "Successfully wrote lumen to output/after/sys_lumen.obj\n" ] } ], "source": [ "# Build \"before\" meshes from raw CSV, then write explicit .obj files for comparison\n", "rest_dia_raw = np.genfromtxt(\"ivus_rest/diastolic_contours.csv\")\n", "rest_dia_ref = np.genfromtxt(\"ivus_rest/diastolic_reference_points.csv\")\n", "rest_sys_raw = np.genfromtxt(\"ivus_rest/systolic_contours.csv\")\n", "rest_sys_ref = np.genfromtxt(\"ivus_rest/systolic_reference_points.csv\")\n", "\n", "rest_dia_before = mm.numpy_to_geometry(\n", " lumen_arr=rest_dia_raw, eem_arr=np.array([]), catheter_arr=np.array([]),\n", " wall_arr=np.array([]), reference_arr=rest_dia_ref, label=\"dia_before\",\n", ")\n", "rest_sys_before = mm.numpy_to_geometry(\n", " lumen_arr=rest_sys_raw, eem_arr=np.array([]), catheter_arr=np.array([]),\n", " wall_arr=np.array([]), reference_arr=rest_sys_ref, label=\"sys_before\",\n", ")\n", "rest_dia_before.frames = np.array([f.sort_frame_points() for f in rest_dia_before.frames])\n", "rest_sys_before.frames = np.array([f.sort_frame_points() for f in rest_sys_before.frames])\n", "\n", "# Write before (unprocessed) and after (aligned) meshes with explicit names\n", "mm.to_obj(rest_dia_before, \"output/before\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"dia\")\n", "mm.to_obj(rest_sys_before, \"output/before\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"sys\")\n", "mm.to_obj(rest.geom_a, \"output/after\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"dia\")\n", "mm.to_obj(rest.geom_b, \"output/after\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"sys\")" ] }, { "cell_type": "code", "execution_count": 5, "id": "2f1d5bea", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:14:15.997624Z", "iopub.status.busy": "2026-05-04T15:14:15.997434Z", "iopub.status.idle": "2026-05-04T15:14:17.096798Z", "shell.execute_reply": "2026-05-04T15:14:17.096091Z" } }, "outputs": [ { "data": { "text/html": [ " \n", " \n", " " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "