{ "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": [ "
\n", "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plot_pair(\n", " before_paths=[\"output/before/dia_lumen.obj\", \"output/before/sys_lumen.obj\"],\n", " after_paths=[\"output/after/dia_lumen.obj\", \"output/after/sys_lumen.obj\"],\n", " colors=[\"royalblue\", \"firebrick\"],\n", " titles=[\"Before Processing\", \"After Processing\"],\n", ")" ] }, { "cell_type": "markdown", "id": "90585578", "metadata": {}, "source": [ "The data is now neatly ordered in pairs (diastolic and systolic geometry). Every geometry contains\n", "contours for lumen, wall, and a synthetic catheter. The reference point is used for centerline\n", "co-registration. All points belonging to a contour are stored in a `PyContour` struct." ] }, { "cell_type": "code", "execution_count": 6, "id": "852594dc", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:14:17.106324Z", "iopub.status.busy": "2026-05-04T15:14:17.106108Z", "iopub.status.idle": "2026-05-04T15:14:17.115466Z", "shell.execute_reply": "2026-05-04T15:14:17.114889Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "PyGeometryPair:\n", "GeometryPair rest_dia - rest_sys (diastolic: 14 frames, systolic: 14 frames)\n", "PyGeometry:\n", "Geometry(14 frames, label='rest_dia')\n", "PyFrame:\n", "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)\n", "PyContour:\n", "Contour(id=0, frame=385, points=501, centroid=(3.72, 5.25, 0.00), kind=Lumen)\n", "Extras:\n", "{'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)}\n", "PyContourPoint:\n", "Point(frame_id=19, pt_id=0, x=3.80, y=7.90, z=0.00, aortic=false)\n" ] } ], "source": [ "print(f\"PyGeometryPair:\\n{rest}\")\n", "print(f\"PyGeometry:\\n{rest.geom_a}\")\n", "print(f\"PyFrame:\\n{rest.geom_a.frames[0]}\")\n", "print(f\"PyContour:\\n{rest.geom_a.frames[0].lumen}\")\n", "print(f\"Extras:\\n{rest.geom_a.frames[0].extras}\")\n", "print(f\"PyContourPoint:\\n{rest.geom_a.frames[0].lumen.points[0]}\")" ] }, { "cell_type": "markdown", "id": "4213f69a", "metadata": {}, "source": [ "Different stenosis measurements can be obtained directly from the objects:" ] }, { "cell_type": "code", "execution_count": 7, "id": "4a93dd04", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:14:17.117396Z", "iopub.status.busy": "2026-05-04T15:14:17.117210Z", "iopub.status.idle": "2026-05-04T15:14:17.535043Z", "shell.execute_reply": "2026-05-04T15:14:17.534390Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+----+----------+-----------+----------+-----------+-------+\n", "| id | area_dia | ellip_dia | area_sys | ellip_sys | z |\n", "+----+----------+-----------+----------+-----------+-------+\n", "PyGeometryPair summary (dia_geom: (mla [mm²], max. stenosis, stenosis length [mm])):\n", "((5.559206007496853, 0.6780775439844883, 10.364606842105268), (6.111481670202526, 0.663038559502465, 7.7734551315789515))\n", "Deformation table:\n", "[[ 0. 5.63662776 4.58651358 6.11148167 4.35376487 0. ]\n", " [ 1. 5.79662471 4.75220339 6.25936689 3.38956113 1.29557586]\n", " [ 2. 5.70440311 3.69759593 6.49106359 2.85326248 2.59115171]\n", " [ 3. 5.55920601 2.56932308 6.84671006 2.17905332 3.88672757]\n", " [ 4. 5.5883072 1.63583386 7.59416089 1.90836576 5.18230342]\n", " [ 5. 6.41743783 1.50724622 7.57484592 1.67549107 6.47787928]\n", " [ 6. 7.25510456 1.36697234 7.3074273 1.63078837 7.77345513]\n", " [ 7. 7.74398201 1.24613112 10.23430197 1.32956583 9.06903099]\n", " [ 8. 7.77347285 1.43038361 11.90101937 1.19922679 10.36460684]\n", " [ 9. 9.05387612 1.32700846 14.15414345 1.08910895 11.6601827 ]\n", " [10. 11.91520903 1.18319293 11.7040278 1.17176063 12.95575855]\n", " [11. 15.27166328 1.08294811 13.56639023 1.14517785 14.25133441]\n", " [12. 17.26877359 1.05840795 15.42409292 1.11280753 15.54691026]\n", " [13. 16.34175131 1.0407399 18.13703568 1.06629346 16.84248612]]\n", "| 0 | 5.64 | 4.59 | 6.11 | 4.35 | 0.00 |\n", "| 1 | 5.80 | 4.75 | 6.26 | 3.39 | 1.30 |\n", "| 2 | 5.70 | 3.70 | 6.49 | 2.85 | 2.59 |\n", "| 3 | 5.56 | 2.57 | 6.85 | 2.18 | 3.89 |\n", "| 4 | 5.59 | 1.64 | 7.59 | 1.91 | 5.18 |\n", "| 5 | 6.42 | 1.51 | 7.57 | 1.68 | 6.48 |\n", "| 6 | 7.26 | 1.37 | 7.31 | 1.63 | 7.77 |\n", "| 7 | 7.74 | 1.25 | 10.23 | 1.33 | 9.07 |\n", "| 8 | 7.77 | 1.43 | 11.90 | 1.20 | 10.36 |\n", "| 9 | 9.05 | 1.33 | 14.15 | 1.09 | 11.66 |\n", "| 10 | 11.92 | 1.18 | 11.70 | 1.17 | 12.96 |\n", "| 11 | 15.27 | 1.08 | 13.57 | 1.15 | 14.25 |\n", "| 12 | 17.27 | 1.06 | 15.42 | 1.11 | 15.55 |\n", "| 13 | 16.34 | 1.04 | 18.14 | 1.07 | 16.84 |\n", "+----+----------+-----------+----------+-----------+-------+\n", "\n", "PyGeometry summary:\n", "5.559206007496853\n", "\n", "Contour area [mm²]: 5.6366\n", "Elliptic ratio: 1.0407\n" ] } ], "source": [ "# Summary over PyGeometryPair\n", "summary_pair, deform_table = rest.get_summary()\n", "print(f\"PyGeometryPair summary (dia_geom: (mla [mm²], max. stenosis, stenosis length [mm])):\\n{summary_pair}\")\n", "print(f\"Deformation table:\\n{np.array(deform_table)}\")\n", "\n", "# Summary over PyGeometry\n", "print(f\"\\nPyGeometry summary:\\n{rest.geom_a.get_summary()[0]}\")\n", "\n", "# Per-contour measurements\n", "print(f\"\\nContour area [mm²]: {rest.geom_a.frames[0].lumen.get_area():.4f}\")\n", "print(f\"Elliptic ratio: {rest.geom_a.frames[-1].lumen.get_elliptic_ratio():.4f}\")" ] }, { "cell_type": "markdown", "id": "31b03e3a", "metadata": {}, "source": [ "The four pairs represent all four possible comparisons in gated images — particularly relevant for\n", "coronary artery anomalies (AAOCA): rest pulsatile, stress pulsatile, stress-induced diastolic, and\n", "stress-induced systolic lumen deformation.\n", "\n", "The `interpolation_steps` parameter generates intermediate meshes between paired states, useful for\n", "deformation animations in Blender. Set `interpolation_steps=0` to skip interpolation.\n", "\n", "### Coronary Artery Disease\n", "\n", "`from_file_single` reconstructs a single pullback with all contour layers (lumen, EEM, wall, catheter):" ] }, { "cell_type": "code", "execution_count": 8, "id": "9e30d724", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:14:17.537081Z", "iopub.status.busy": "2026-05-04T15:14:17.536890Z", "iopub.status.idle": "2026-05-04T15:14:25.541213Z", "shell.execute_reply": "2026-05-04T15:14:25.540402Z" } }, "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: ivus_full\n", "Diastole phase: Yes\n", "\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'ivus_full' (anomalous: false) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | 0.00 | 0.04 | -0.07 | (4.38,4.06) |\n", "| 2 | 1 | -0.60 | 0.11 | -0.15 | (4.38,4.06) |\n", "| 3 | 2 | 19.20 | 0.04 | -0.24 | (4.38,4.06) |\n", "| 4 | 3 | 90.00 | -0.12 | -0.46 | (4.38,4.06) |\n", "| 5 | 4 | 5.30 | -0.24 | -0.36 | (4.38,4.06) |\n", "| 6 | 5 | -36.00 | -0.16 | -0.44 | (4.38,4.06) |\n", "| 7 | 6 | 18.00 | -0.21 | -0.50 | (4.38,4.06) |\n", "| 8 | 7 | 2.70 | -0.12 | -0.52 | (4.38,4.06) |\n", "| 9 | 8 | -18.00 | -0.08 | -0.46 | (4.38,4.06) |\n", "| 10 | 9 | 0.00 | 0.02 | -0.39 | (4.38,4.06) |\n", "| 11 | 10 | 0.00 | -0.06 | -0.42 | (4.38,4.06) |\n", "| 12 | 11 | -90.00 | -0.14 | -0.34 | (4.38,4.06) |\n", "| 13 | 12 | 69.40 | 0.05 | -0.52 | (4.38,4.06) |\n", "| 14 | 13 | 0.00 | 0.29 | -0.53 | (4.38,4.06) |\n", "| 15 | 14 | -5.90 | 0.32 | -0.51 | (4.38,4.06) |\n", "| 16 | 15 | -12.30 | 0.36 | -0.50 | (4.38,4.06) |\n", "| 17 | 16 | -6.90 | 0.35 | -0.36 | (4.38,4.06) |\n", "| 18 | 17 | 0.00 | 0.38 | -0.40 | (4.38,4.06) |\n", "| 19 | 18 | -8.60 | 0.35 | -0.35 | (4.38,4.06) |\n", "| 20 | 19 | -0.80 | 0.43 | -0.35 | (4.38,4.06) |\n", "| 21 | 20 | -19.60 | 0.35 | -0.23 | (4.38,4.06) |\n", "| 22 | 21 | -10.50 | 0.39 | -0.05 | (4.38,4.06) |\n", "| 23 | 22 | -3.70 | 0.31 | 0.03 | (4.38,4.06) |\n", "| 24 " ] }, { "name": "stderr", "output_type": "stream", "text": [ "process_directory: unknown mapping name 'catheter', skipping\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " | 23 | -2.20 | 0.23 | 0.04 | (4.38,4.06) |\n", "| 25 | 24 | 0.00 | 0.14 | -0.00 | (4.38,4.06) |\n", "| 26 | 25 | 0.00 | 0.03 | -0.09 | (4.38,4.06) |\n", "| 27 | 26 | -18.00 | -0.06 | -0.16 | (4.38,4.06) |\n", "| 28 | 27 | 17.00 | -0.05 | -0.14 | (4.38,4.06) |\n", "| 29 | 28 | 0.00 | -0.01 | -0.06 | (4.38,4.06) |\n", "| 30 | 29 | 18.00 | 0.08 | -0.02 | (4.38,4.06) |\n", "| 31 | 30 | 89.40 | 0.20 | -0.59 | (4.38,4.06) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "Successfully wrote OBJ files for geometry ivus_full to output/cad\n", "Successfully wrote lumen to output/cad/aligned_lumen.obj\n", "Successfully wrote eem to output/cad/aligned_eem.obj\n", "Successfully wrote wall to output/cad/aligned_wall.obj\n" ] }, { "data": { "text/html": [ "
\n", "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "cad, _ = mm.from_file_single(\n", " input_path=\"ivus_full\",\n", " diastole=True,\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=[\n", " mm.PyContourType.Lumen, mm.PyContourType.Eem,\n", " mm.PyContourType.Catheter, mm.PyContourType.Wall,\n", " ],\n", " output_path=\"output/cad\",\n", ")\n", "# Centre coordinate system on EEM and write both versions\n", "cad_aligned = cad.center_to_contour(mm.PyContourType.Eem)\n", "mm.to_obj(cad_aligned, \"output/cad\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen, mm.PyContourType.Eem, mm.PyContourType.Wall],\n", " filename_prefix=\"aligned\")\n", "\n", "# from_file_single writes files as {contour_type}_{input_path_name}.obj\n", "lumen = trimesh.load(\"output/cad/lumen_ivus_full.obj\")\n", "eem = trimesh.load(\"output/cad/eem_ivus_full.obj\")\n", "wall = trimesh.load(\"output/cad/wall_ivus_full.obj\")\n", "lumen_a = trimesh.load(\"output/cad/aligned_lumen.obj\")\n", "eem_a = trimesh.load(\"output/cad/aligned_eem.obj\")\n", "wall_a = trimesh.load(\"output/cad/aligned_wall.obj\")\n", "\n", "fig = make_subplots(\n", " rows=1, cols=2,\n", " specs=[[{\"type\": \"scene\"}, {\"type\": \"scene\"}]],\n", " subplot_titles=(\"CAD (lumen-centred)\", \"CAD (EEM-centred)\"),\n", ")\n", "camera = dict(eye=dict(x=1.5, y=1.5, z=1.0))\n", "for t in [trimesh_to_mesh3d(lumen, \"firebrick\", \"Lumen\", 1.0),\n", " trimesh_to_mesh3d(eem, \"royalblue\", \"EEM\", 0.6),\n", " trimesh_to_mesh3d(wall, \"white\", \"Wall\", 0.5)]:\n", " fig.add_trace(t, row=1, col=1)\n", "for t in [trimesh_to_mesh3d(lumen_a, \"firebrick\", \"Lumen\", 1.0),\n", " trimesh_to_mesh3d(eem_a, \"royalblue\", \"EEM\", 0.6),\n", " trimesh_to_mesh3d(wall_a, \"white\", \"Wall\", 0.5)]:\n", " fig.add_trace(t, row=1, col=2)\n", "fig.update_layout(\n", " width=900, height=450,\n", " scene=dict(camera=camera, aspectmode=\"data\"),\n", " scene2=dict(camera=camera, aspectmode=\"data\"),\n", " margin=dict(l=0, r=0, t=40, b=0),\n", ")\n", "fig.show()" ] }, { "cell_type": "markdown", "id": "37a663e0", "metadata": {}, "source": [ "### Pre- vs. Post-stenting\n", "\n", "For pre/post-stenting comparison across both cardiac phases, `from_file_full` is used with the\n", "pre-stent pullback as `input_path_ab` and the post-stent pullback as `input_path_cd`." ] }, { "cell_type": "code", "execution_count": 9, "id": "1ef782ec", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:14:25.567303Z", "iopub.status.busy": "2026-05-04T15:14:25.567065Z", "iopub.status.idle": "2026-05-04T15:14:37.029921Z", "shell.execute_reply": "2026-05-04T15:14:37.029216Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "eem file not found, skipping: \"ivus_prestent/eem_diastolic_contours.csv\"\n", "sidebranch file not found, skipping: \"ivus_prestent/branch_diastolic_contours.csv\"\n", "calcification file not found, skipping: \"ivus_prestent/calcium_diastolic_contours.csv\"\n", "process_directory: unknown mapping name 'catheter', skipping\n", "sidebranch file not found, skipping: \"ivus_prestent/branch_systolic_contours.csv\"\n", "process_directory: unknown mapping name 'catheter', skipping\n", "calcification file not found, skipping: \"ivus_prestent/calcium_systolic_contours.csv\"\n", "eem file not found, skipping: \"ivus_prestent/eem_systolic_contours.csv\"\n", "sidebranch file not found, skipping: \"ivus_poststent/branch_diastolic_contours.csv\"\n", "eem file not found, skipping: \"ivus_poststent/eem_diastolic_contours.csv\"\n", "calcification file not found, skipping: \"ivus_poststent/calcium_diastolic_contours.csv\"\n", "process_directory: unknown mapping name 'catheter', skipping\n", "eem file not found, skipping: \"ivus_poststent/eem_systolic_contours.csv\"\n", "sidebranch file not found, skipping: \"ivus_poststent/branch_systolic_contours.csv\"\n", "calcification file not found, skipping: \"ivus_poststent/calcium_systolic_contours.csv\"\n", "process_directory: unknown mapping name 'catheter', skipping\n" ] }, { "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: ivus_prestent\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: ivus_prestent\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: ivus_poststent\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: ivus_poststent\n", "Diastole phase: No\n", "\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'ivus_prestent' (anomalous: true) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | -11.70 | 0.08 | 0.17 | (4.32,5.28) |\n", "| 2 | 1 | -2.80 | 0.23 | 0.35 | (4.32,5.28) |\n", "| 3 | 2 | -8.00 | 0.14 | 0.69 | (4.32,5.28) |\n", "| 4 | 3 | 1.40 | 0.09 | 0.86 | (4.32,5.28) |\n", "| 5 | 4 | 39.20 | -0.72 | -0.77 | (4.32,5.28) |\n", "| 6 | 5 | -19.80 | -0.23 | -0.57 | (4.32,5.28) |\n", "| 7 | 6 | 13.90 | -0.38 | -0.86 | (4.32,5.28) |\n", "| 8 | 7 | 3.20 | -0.30 | -0.49 | (4.32,5.28) |\n", "| 9 | 8 | -4.00 | -0.10 | -0.61 | (4.32,5.28) |\n", "| 10 | 9 | 5.30 | -0.20 | -0.51 | (4.32,5.28) |\n", "| 11 | 10 | -3.40 | -0.21 | -0.08 | (4.32,5.28) |\n", "| 12 | 11 | -45.00 | 0.07 | 0.90 | (4.32,5.28) |\n", "| 13 | 12 | 0.00 | -0.11 | 0.78 | (4.32,5.28) |\n", "| 14 | 13 | 36.00 | -0.10 | 0.99 | (4.32,5.28) |\n", "| 15 | 14 | -17.70 | -0.10 | 1.01 | (4.32,5.28) |\n", "| 16 | 15 | -45.00 | -0.29 | 0.49 | (4.32,5.28) |\n", "| 17 | 16 | 8.00 | -0.30 | 0.48 | (4.32,5.28) |\n", "| 18 | 17 | 0.00 | -0.35 | 0.23 | (4.32,5.28) |\n", "| 19 | 18 | -6.80 | -0.36 | 0.22 | (4.32,5.28) |\n", "| 20 | 19 | -9.80 | -0.12 | 0.24 | (4.32,5.28) |\n", "| 21 | 20 | -14.70 | 0.04 | 0.26 | (4.32,5.28) |\n", "| 22 | 21 | -2.80 | 0.07 | 0.37 | (4.32,5.28) |\n", "| 23 | 22 | -45.00 | 0.22 | 1.06 | (4.32,5.28) |\n", "| 24 | 23 | -18.00 | 0.11 | 1.11 | (4.32,5.28) |\n", "| 25 | 24 | 45.00 | 0.09 | 0.78 | (4.32,5.28) |\n", "| 26 | 25 | 45.00 | -0.24 | 0.67 | (4.32,5.28) |\n", "| 27 | 26 | 39.70 | -0.25 | 0.70 | (4.32,5.28) |\n", "| 28 | 27 | 36.00 | -0.09 | 0.97 | (4.32,5.28) |\n", "| 29 | 28 | -36.10 | -0.38 | 1.48 | (4.32,5.28) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "⚠️\tHole detected! Attempting to fix using Geometry::insert_frame(...) (baseline spacing = 0.559)\n", "✅ Fixed one-frame hole between Frame 24 and Frame 25 (dz = 0.909, ratio = 1.627)\n", "✅ Fixed one-frame hole between Frame 31 and Frame 32 (dz = 0.876, ratio = 1.568)\n", "✅ Fixed one-frame hole between Frame 33 and Frame 34 (dz = 0.842, ratio = 1.508)\n", "⚠️\tHole detected! Attempting to fix using Geometry::insert_frame(...) (baseline spacing = 0.533)\n", "✅ Fixed one-frame hole between Frame 19 and Frame 20 (dz = 1.099, ratio = 2.061)\n", "\n", "+-------------------------------------------------------------------+\n", "| ✅ Finished aligning 'ivus_poststent' (anomalous: true) |\n", "+---------+------------+---------------+------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+------+-------+-------------+\n", "| 1 | 0 | 12.40 | 0.18 | -0.01 | (4.69,3.78) |\n", "| 2 | 1 | 7.50 | 0.29 | 0.00 | (4.69,3.78) |\n", "| 3 | 2 | -6.70 | 0.20 | 0.31 | (4.69,3.78) |\n", "| 4 | 3 | 3.30 | 0.33 | 0.39 | (4.69,3.78) |\n", "| 5 | 4 | 0.00 | 0.25 | 0.06 | (4.69,3.78) |\n", "| 6 | 5 | 2.20 | 0.34 | 0.32 | (4.69,3.78) |\n", "| 7 | 6 | 8.70 | 0.50 | 0.31 | (4.69,3.78) |\n", "| 8 | 7 | -7.90 | 0.46 | 0.32 | (4.69,3.78) |\n", "| 9 | 8 | 7.30 | 0.54 | 0.22 | (4.69,3.78) |\n", "| 10 | 9 | 7.90 | 0.65 | 0.09 | (4.69,3.78) |\n", "| 11 | 10 | 0.00 | 0.62 | 0.00 | (4.69,3.78) |\n", "| 12 | 11 | 5.30 | 0.66 | -0.11 | (4.69,3.78) |\n", "| 13 | 12 | -11.40 | 0.60 | -0.18 | (4.69,3.78) |\n", "| 14 | 13 | -2.90 | 0.56 | -0.21 | (4.69,3.78) |\n", "| 15 | 14 | 0.00 | 0.53 | -0.35 | (4.69,3.78) |\n", "| 16 | 15 | 18.00 | 0.55 | -0.46 | (4.69,3.78) |\n", "| 17 | 16 | 12.40 | 0.59 | -0.58 | (4.69,3.78) |\n", "| 18 | 17 | 5.80 | 0.61 | -0.61 | (4.69,3.78) |\n", "| 19 | 18 | 13.20 | 0.63 | -0.72 | (4.69,3.78) |\n", "| 20 | 19 | 0.70 | 0.61 | -0.73 | (4.69,3.78) |\n", "| 21 | 20 | 0.00 | 0.57 | -0.75 | (4.69,3.78) |\n", "| 22 | 21 | -18.00 | 0.89 | -0.63 | (4.69,3.78) |\n", "| 23 | 22 | -0.70 | 0.85 | -0.69 | (4.69,3.78) |\n", "| 24 | 23 | -15.90 | 0.83 | -0.50 | (4.69,3.78) |\n", "| 25 | 24 | 3.60 | 0.83 | -0.49 | (4.69,3.78) |\n", "| 26 | 25 | 15.70 | 0.90 | -0.63 | (4.69,3.78) |\n", "| 27 | 26 | 16.20 | 0.85 | -0.78 | (4.69,3.78) |\n", "| 28 | 27 | -5.40 | 0.91 | -0.70 | (4.69,3.78) |\n", "| 29 | 28 | 6.40 | 0.89 | -0.73 | (4.69,3.78) |\n", "| 30 | 29 | 24.20 | 0.78 | -0.99 | (4.69,3.78) |\n", "| 31 | 30 | -1.30 | 0.72 | -0.91 | (4.69,3.78) |\n", "| 32 | 31 | 19.40 | 0.65 | -0.94 | (4.69,3.78) |\n", "| 33 | 32 | 0.00 | 0.86 | -1.01 | (4.69,3.78) |\n", "| 34 | 33 | 9.90 | 0.74 | -1.06 | (4.69,3.78) |\n", "+---------+------------+---------------+------+-------+-------------+\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'ivus_prestent' (anomalous: true) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | 0.10 | 0.01 | 0.12 | (4.20,4.59) |\n", "| 2 | 1 | -8.10 | -0.03 | 0.07 | (4.20,4.59) |\n", "| 3 | 2 | 9.20 | -0.05 | -0.14 | (4.20,4.59) |\n", "| 4 | 3 | -10.90 | 0.27 | -0.75 | (4.20,4.59) |\n", "| 5 | 4 | 9.90 | 0.11 | -1.14 | (4.20,4.59) |\n", "| 6 | 5 | -1.10 | 0.03 | -1.17 | (4.20,4.59) |\n", "| 7 | 6 | 8.00 | -0.13 | -1.28 | (4.20,4.59) |\n", "| 8 | 7 | -4.10 | -0.08 | -1.25 | (4.20,4.59) |\n", "| 9 | 8 | -0.80 | -0.06 | -1.36 | (4.20,4.59) |\n", "| 10 | 9 | -0.70 | 0.02 | -1.22 | (4.20,4.59) |\n", "| 11 | 10 | 6.00 | -0.26 | -1.22 | (4.20,4.59) |\n", "| 12 | 11 | -0.30 | -0.35 | -1.21 | (4.20,4.59) |\n", "| 13 | 12 | -4.70 | -0.13 | -0.88 | (4.20,4.59) |\n", "| 14 | 13 | -4.30 | -0.08 | -0.87 | (4.20,4.59) |\n", "| 15 | 14 | 4.20 | -0.14 | -0.77 | (4.20,4.59) |\n", "| 16 | 15 | -3.60 | -0.13 | -0.74 | (4.20,4.59) |\n", "| 17 | 16 | 0.00 | -0.17 | -0.52 | (4.20,4.59) |\n", "| 18 | 17 | -45.00 | 0.06 | 0.32 | (4.20,4.59) |\n", "| 19 | 18 | -3.50 | 0.04 | 0.28 | (4.20,4.59) |\n", "| 20 | 19 | 45.00 | -0.42 | -0.17 | (4.20,4.59) |\n", "| 21 | 20 | -11.60 | -0.29 | -0.06 | (4.20,4.59) |\n", "| 22 | 21 | 45.00 | -0.72 | -0.15 | (4.20,4.59) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "⚠️\tHole detected! Attempting to fix using Geometry::insert_frame(...) (baseline spacing = 0.569)\n", "✅ Fixed one-frame hole between Frame 30 and Frame 31 (dz = 0.876, ratio = 1.539)\n", "\n", "+-------------------------------------------------------------------+\n", "| ✅ Finished aligning 'ivus_poststent' (anomalous: true) |\n", "+---------+------------+---------------+------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+------+-------+-------------+\n", "| 1 | 0 | 3.90 | 0.16 | 0.16 | (4.84,3.84) |\n", "| 2 | 1 | -10.70 | 0.08 | 0.12 | (4.84,3.84) |\n", "| 3 | 2 | 17.50 | 0.40 | 0.34 | (4.84,3.84) |\n", "| 4 | 3 | -12.70 | 0.19 | 0.38 | (4.84,3.84) |\n", "| 5 | 4 | 3.90 | 0.34 | 0.38 | (4.84,3.84) |\n", "| 6 | 5 | 6.00 | 0.47 | 0.39 | (4.84,3.84) |\n", "| 7 | 6 | 13.90 | 0.65 | 0.30 | (4.84,3.84) |\n", "| 8 | 7 | -18.00 | 0.37 | 0.43 | (4.84,3.84) |\n", "| 9 | 8 | 15.80 | 0.57 | 0.20 | (4.84,3.84) |\n", "| 10 | 9 | 3.00 | 0.57 | 0.18 | (4.84,3.84) |\n", "| 11 | 10 | 0.30 | 0.68 | 0.18 | (4.84,3.84) |\n", "| 12 | 11 | 8.80 | 0.72 | 0.02 | (4.84,3.84) |\n", "| 13 | 12 | 7.10 | 0.81 | -0.06 | (4.84,3.84) |\n", "| 14 | 13 | 0.00 | 0.76 | -0.14 | (4.84,3.84) |\n", "| 15 | 14 | 5.40 | 0.78 | -0.23 | (4.84,3.84) |\n", "| 16 | 15 | -0.80 | 0.85 | -0.25 | (4.84,3.84) |\n", "| 17 | 16 | 10.50 | 0.88 | -0.35 | (4.84,3.84) |\n", "| 18 | 17 | 13.70 | 0.94 | -0.43 | (4.84,3.84) |\n", "| 19 | 18 | 0.20 | 0.92 | -0.50 | (4.84,3.84) |\n", "| 20 | 19 | 23.30 | 0.92 | -0.72 | (4.84,3.84) |\n", "| 21 | 20 | -12.40 | 0.86 | -0.68 | (4.84,3.84) |\n", "| 22 | 21 | 32.40 | 0.82 | -0.85 | (4.84,3.84) |\n", "| 23 | 22 | 18.10 | 0.78 | -0.89 | (4.84,3.84) |\n", "| 24 | 23 | -18.00 | 0.74 | -0.75 | (4.84,3.84) |\n", "| 25 | 24 | 18.00 | 0.79 | -0.94 | (4.84,3.84) |\n", "| 26 | 25 | 11.30 | 0.70 | -0.95 | (4.84,3.84) |\n", "| 27 | 26 | 0.00 | 0.77 | -0.91 | (4.84,3.84) |\n", "| 28 | 27 | 0.00 | 0.70 | -0.88 | (4.84,3.84) |\n", "| 29 | 28 | 12.70 | 0.74 | -0.64 | (4.84,3.84) |\n", "| 30 | 29 | 0.00 | 1.10 | -0.63 | (4.84,3.84) |\n", "| 31 | 30 | 11.40 | 0.82 | -0.72 | (4.84,3.84) |\n", "| 32 | 31 | -9.60 | 1.10 | -0.64 | (4.84,3.84) |\n", "| 33 | 32 | 4.70 | 1.09 | -0.64 | (4.84,3.84) |\n", "+---------+------------+---------------+------+-------+-------------+\n", "\n", "✅ Aligned geometry 'ivus_poststent' to 'ivus_poststent'\n", "-----------------------------------------\n", "Applied initial translation: (-0.16, -0.05, 0.00) mm\n", "Found best rotation of -34.90° with parameters: \n", "range: 45.00° \n", "step size: 0.1°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "✅ Aligned geometry 'ivus_prestent' to 'ivus_prestent'\n", "-----------------------------------------\n", "Applied initial translation: (0.12, 0.70, 0.00) mm\n", "Found best rotation of -0.50° with parameters: \n", "range: 45.00° \n", "step size: 0.1°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "✅ Aligned geometry 'ivus_poststent' to 'ivus_prestent'\n", "-----------------------------------------\n", "Applied initial translation: (-0.37, 1.50, 0.00) mm\n", "Found best rotation of 30.20° with parameters: \n", "range: 45.00° \n", "step size: 0.1°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "✅ Aligned geometry 'ivus_poststent' to 'ivus_prestent'\n", "-----------------------------------------\n", "Applied initial translation: (-0.37, 1.50, 0.00) mm\n", "Found best rotation of -27.40° with parameters: \n", "range: 45.00° \n", "step size: 0.1°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "Saving files for 'ivus_prestent - ivus_prestent' to 'output/stent_rest'\n", "LUMEN .obj files: 2/2 written successfully\n", "CATHETER .obj files: 2/2 written successfully\n", "WALL .obj files: 2/2 written successfully\n", "\n", "Saving files for 'ivus_poststent - ivus_poststent' to 'output/stent_stress'\n", "LUMEN .obj files: 2/2 written successfully\n", "CATHETER .obj files: 2/2 written successfully\n", "WALL .obj files: 2/2 written successfully\n", "\n", "Saving files for 'ivus_prestent - ivus_poststent' to 'output/stent_diastole'\n", "LUMEN .obj files: 2/2 written successfully\n", "CATHETER .obj files: 2/2 written successfully\n", "WALL .obj files: 2/2 written successfully\n", "\n", "Saving files for 'ivus_prestent - ivus_poststent' to 'output/stent_systole'\n", "LUMEN .obj files: 2/2 written successfully\n", "CATHETER .obj files: 2/2 written successfully\n", "WALL .obj files: 2/2 written successfully\n", "Successfully wrote lumen to output/stent_vis/prestent_lumen.obj\n", "Successfully wrote lumen to output/stent_vis/poststent_lumen.obj\n" ] }, { "data": { "text/html": [ "
\n", "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "prestent, poststent, dia_comp, sys_comp, _ = mm.from_file_full(\n", " input_path_ab=\"ivus_prestent\",\n", " input_path_cd=\"ivus_poststent\",\n", " step_rotation_deg=0.1,\n", " range_rotation_deg=45,\n", " watertight=False,\n", " output_path_ab=\"output/stent_rest\",\n", " output_path_cd=\"output/stent_stress\",\n", " output_path_ac=\"output/stent_diastole\",\n", " output_path_bd=\"output/stent_systole\",\n", " interpolation_steps=0,\n", ")\n", "# Write comparison geometries with explicit names\n", "mm.to_obj(dia_comp.geom_a, \"output/stent_vis\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"prestent\")\n", "mm.to_obj(dia_comp.geom_b, \"output/stent_vis\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"poststent\")\n", "\n", "mesh_pre = trimesh.load(\"output/stent_vis/prestent_lumen.obj\")\n", "mesh_post = trimesh.load(\"output/stent_vis/poststent_lumen.obj\")\n", "\n", "fig = go.Figure(data=[\n", " trimesh_to_mesh3d(mesh_pre, \"royalblue\", \"Pre-stent (diastole)\"),\n", " trimesh_to_mesh3d(mesh_post, \"firebrick\", \"Post-stent (diastole)\"),\n", "])\n", "fig.update_layout(\n", " title=\"Pre- vs. Post-stenting: diastolic lumen comparison\",\n", " scene=dict(aspectmode=\"data\"),\n", " margin=dict(l=0, r=0, t=40, b=0),\n", ")\n", "fig.show()" ] }, { "cell_type": "markdown", "id": "e9aa87db", "metadata": {}, "source": [ "## 3. Workflow from numpy arrays\n", "\n", "The numpy workflow accepts data from **any source**: you build `PyInputData` objects manually and\n", "then call the same alignment functions. Use this workflow when your segmentation software uses\n", "different file formats, when you need pre-processing steps, or when embedding `multimodars` into a\n", "larger pipeline.\n", "\n", "For stent comparison, both pre- and post-stent geometries are packed into `PyInputData` objects:" ] }, { "cell_type": "code", "execution_count": 10, "id": "f3b436f1", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:14:37.037138Z", "iopub.status.busy": "2026-05-04T15:14:37.036942Z", "iopub.status.idle": "2026-05-04T15:14:42.965362Z", "shell.execute_reply": "2026-05-04T15:14:42.964277Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "✅ Successfully built geometry from input data\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: prestent\n", "Diastole phase: Yes\n", "\n", "\n", "✅ Successfully built geometry from input data\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: poststent\n", "Diastole phase: Yes\n", "\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'prestent' (anomalous: true) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | -19.43 | 0.23 | 0.35 | (4.32,5.28) |\n", "| 2 | 1 | 2.77 | 0.08 | 0.17 | (4.32,5.28) |\n", "| 3 | 2 | -8.61 | 0.14 | 0.69 | (4.32,5.28) |\n", "| 4 | 3 | 1.44 | 0.09 | 0.86 | (4.32,5.28) |\n", "| 5 | 4 | -10.88 | -0.10 | -0.61 | (4.32,5.28) |\n", "| 6 | 5 | 5.32 | -0.20 | -0.51 | (4.32,5.28) |\n", "| 7 | 6 | -0.54 | -0.30 | -0.49 | (4.32,5.28) |\n", "| 8 | 7 | -3.14 | -0.38 | -0.86 | (4.32,5.28) |\n", "| 9 | 8 | 7.40 | -0.72 | -0.77 | (4.32,5.28) |\n", "| 10 | 9 | -19.75 | -0.23 | -0.57 | (4.32,5.28) |\n", "| 11 | 10 | 0.00 | -0.21 | -0.08 | (4.32,5.28) |\n", "| 12 | 11 | 16.53 | -0.35 | 0.23 | (4.32,5.28) |\n", "| 13 | 12 | -6.85 | -0.36 | 0.22 | (4.32,5.28) |\n", "| 14 | 13 | 0.00 | -0.30 | 0.48 | (4.32,5.28) |\n", "| 15 | 14 | -8.01 | -0.29 | 0.49 | (4.32,5.28) |\n", "| 16 | 15 | -17.73 | -0.12 | 0.24 | (4.32,5.28) |\n", "| 17 | 16 | -14.66 | 0.04 | 0.26 | (4.32,5.28) |\n", "| 18 | 17 | -30.00 | 0.07 | 0.90 | (4.32,5.28) |\n", "| 19 | 18 | 16.52 | -0.09 | 0.97 | (4.32,5.28) |\n", "| 20 | 19 | -14.64 | -0.10 | 1.01 | (4.32,5.28) |\n", "| 21 | 20 | 17.73 | -0.10 | 0.99 | (4.32,5.28) |\n", "| 22 | 21 | -26.19 | -0.11 | 0.78 | (4.32,5.28) |\n", "| 23 | 22 | 30.00 | 0.07 | 0.37 | (4.32,5.28) |\n", "| 24 | 23 | -6.49 | 0.09 | 0.78 | (4.32,5.28) |\n", "| 25 | 24 | 29.51 | -0.25 | 0.70 | (4.32,5.28) |\n", "| 26 | 25 | -29.88 | -0.24 | 0.67 | (4.32,5.28) |\n", "| 27 | 26 | 30.00 | -0.38 | 1.48 | (4.32,5.28) |\n", "| 28 | 27 | 30.00 | 0.11 | 1.11 | (4.32,5.28) |\n", "| 29 | 28 | 18.00 | 0.22 | 1.06 | (4.32,5.28) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "⚠️\tHole detected! Attempting to fix using Geometry::insert_frame(...) (baseline spacing = 0.559)\n", "✅ Fixed one-frame hole between Frame 24 and Frame 25 (dz = 0.909, ratio = 1.627)\n", "✅ Fixed one-frame hole between Frame 31 and Frame 32 (dz = 0.876, ratio = 1.568)\n", "✅ Fixed one-frame hole between Frame 33 and Frame 34 (dz = 0.842, ratio = 1.508)\n", "\n", "+-------------------------------------------------------------------+\n", "| ✅ Finished aligning 'poststent' (anomalous: false) |\n", "+---------+------------+---------------+------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+------+-------+-------------+\n", "| 1 | 0 | 12.38 | 0.18 | -0.01 | (4.69,3.78) |\n", "| 2 | 1 | 7.45 | 0.29 | 0.00 | (4.69,3.78) |\n", "| 3 | 2 | -12.68 | 0.25 | 0.06 | (4.69,3.78) |\n", "| 4 | 3 | -1.86 | 0.20 | 0.31 | (4.69,3.78) |\n", "| 5 | 4 | 3.30 | 0.33 | 0.39 | (4.69,3.78) |\n", "| 6 | 5 | 6.94 | 0.34 | 0.32 | (4.69,3.78) |\n", "| 7 | 6 | 8.74 | 0.50 | 0.31 | (4.69,3.78) |\n", "| 8 | 7 | -7.95 | 0.46 | 0.32 | (4.69,3.78) |\n", "| 9 | 8 | 7.24 | 0.54 | 0.22 | (4.69,3.78) |\n", "| 10 | 9 | 7.93 | 0.65 | 0.09 | (4.69,3.78) |\n", "| 11 | 10 | 0.00 | 0.62 | 0.00 | (4.69,3.78) |\n", "| 12 | 11 | 5.25 | 0.66 | -0.11 | (4.69,3.78) |\n", "| 13 | 12 | -11.45 | 0.60 | -0.18 | (4.69,3.78) |\n", "| 14 | 13 | -2.92 | 0.56 | -0.21 | (4.69,3.78) |\n", "| 15 | 14 | 0.00 | 0.53 | -0.35 | (4.69,3.78) |\n", "| 16 | 15 | 18.00 | 0.55 | -0.46 | (4.69,3.78) |\n", "| 17 | 16 | 12.37 | 0.59 | -0.58 | (4.69,3.78) |\n", "| 18 | 17 | 5.81 | 0.61 | -0.61 | (4.69,3.78) |\n", "| 19 | 18 | 13.24 | 0.63 | -0.72 | (4.69,3.78) |\n", "| 20 | 19 | 0.67 | 0.61 | -0.73 | (4.69,3.78) |\n", "| 21 | 20 | -18.00 | 0.83 | -0.50 | (4.69,3.78) |\n", "| 22 | 21 | 18.00 | 0.57 | -0.75 | (4.69,3.78) |\n", "| 23 | 22 | -23.65 | 0.83 | -0.49 | (4.69,3.78) |\n", "| 24 | 23 | 15.70 | 0.90 | -0.63 | (4.69,3.78) |\n", "| 25 | 24 | -3.16 | 0.89 | -0.63 | (4.69,3.78) |\n", "| 26 | 25 | -0.74 | 0.85 | -0.69 | (4.69,3.78) |\n", "| 27 | 26 | 0.00 | 0.89 | -0.73 | (4.69,3.78) |\n", "| 28 | 27 | -6.41 | 0.91 | -0.70 | (4.69,3.78) |\n", "| 29 | 28 | 5.45 | 0.85 | -0.78 | (4.69,3.78) |\n", "| 30 | 29 | -0.54 | 0.72 | -0.91 | (4.69,3.78) |\n", "| 31 | 30 | 19.47 | 0.65 | -0.94 | (4.69,3.78) |\n", "| 32 | 31 | 6.12 | 0.74 | -1.06 | (4.69,3.78) |\n", "| 33 | 32 | -9.93 | 0.86 | -1.01 | (4.69,3.78) |\n", "| 34 | 33 | -1.94 | 0.78 | -0.99 | (4.69,3.78) |\n", "+---------+------------+---------------+------+-------+-------------+\n", "\n", "✅ Aligned geometry 'poststent' to 'prestent'\n", "-----------------------------------------\n", "Applied initial translation: (-0.37, 1.50, 0.00) mm\n", "Found best rotation of 18.55° with parameters: \n", "range: 30.00° \n", "step size: 0.01°\n", "Applied final translation: ( 0, 0.00, 0.00) mm\n", "-----------------------------------------\n", "\n", "Saving files for 'prestent - poststent' to 'output/stent_comparison'\n", "LUMEN .obj files: 2/2 written successfully\n", "CATHETER .obj files: 2/2 written successfully\n", "WALL .obj files: 2/2 written successfully\n" ] } ], "source": [ "before_arr = np.genfromtxt(\"ivus_prestent/diastolic_contours.csv\", delimiter='\\t')\n", "before_ref = np.genfromtxt(\"ivus_prestent/diastolic_reference_points.csv\", delimiter='\\t')\n", "after_arr = np.genfromtxt(\"ivus_poststent/diastolic_contours.csv\", delimiter='\\t')\n", "after_ref = np.genfromtxt(\"ivus_poststent/diastolic_reference_points.csv\", delimiter='\\t')\n", "\n", "before_input = mm.numpy_to_inputdata(\n", " lumen_arr=before_arr, ref_point=before_ref,\n", " record=None, diastole=True, label=\"prestent\",\n", ")\n", "after_input = mm.numpy_to_inputdata(\n", " lumen_arr=after_arr, ref_point=after_ref,\n", " record=None, diastole=True, label=\"poststent\",\n", ")\n", "\n", "pair, _ = mm.from_array_singlepair(\n", " input_data_a=before_input,\n", " input_data_b=after_input,\n", " step_rotation_deg=0.01,\n", " range_rotation_deg=30,\n", " output_path=\"output/stent_comparison\",\n", ")" ] }, { "cell_type": "markdown", "id": "ba9600ea", "metadata": {}, "source": [ "A single 3D geometry can also be reconstructed from OCT. `from_array_single` returns a\n", "`PyGeometry` from a single-state contour array. The `replace_frame` method demonstrates\n", "in-place editing before writing:" ] }, { "cell_type": "code", "execution_count": 11, "id": "3aa0c3f9", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:14:42.967777Z", "iopub.status.busy": "2026-05-04T15:14:42.967568Z", "iopub.status.idle": "2026-05-04T15:15:20.571263Z", "shell.execute_reply": "2026-05-04T15:15:20.570480Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "✅ Successfully built geometry from input data\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: oct\n", "Diastole phase: Yes\n", "\n", "\n", "+--------------------------------------------------------------------+\n", "| ✅ Finished aligning 'oct' (anomalous: false) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "| 1 | 0 | -1.75 | 0.00 | 0.05 | (6.30,5.97) |\n", "| 2 | 1 | 1.40 | 0.02 | 0.12 | (6.30,5.97) |\n", "| 3 | 2 | 0.47 | -0.03 | 0.17 | (6.30,5.97) |\n", "| 4 | 3 | 3.10 | -0.04 | 0.25 | (6.30,5.97) |\n", "| 5 | 4 | -1.86 | -0.06 | 0.29 | (6.30,5.97) |\n", "| 6 | 5 | 4.01 | -0.10 | 0.42 | (6.30,5.97) |\n", "| 7 | 6 | 5.88 | -0.13 | 0.59 | (6.30,5.97) |\n", "| 8 | 7 | 5.99 | -0.14 | 0.78 | (6.30,5.97) |\n", "| 9 | 8 | 0.71 | -0.13 | 0.90 | (6.30,5.97) |\n", "| 10 | 9 | 2.19 | -0.10 | 1.04 | (6.30,5.97) |\n", "| 11 | 10 | 2.24 | -0.09 | 1.14 | (6.30,5.97) |\n", "| 12 | 11 | 2.62 | -0.05 | 1.27 | (6.30,5.97) |\n", "| 13 | 12 | 0.92 | 0.00 | 1.37 | (6.30,5.97) |\n", "| 14 | 13 | 5.28 | 0.05 | 1.50 | (6.30,5.97) |\n", "| 15 | 14 | 1.82 | 0.09 | 1.59 | (6.30,5.97) |\n", "| 16 | 15 | 2.20 | 0.15 | 1.68 | (6.30,5.97) |\n", "| 17 | 16 | 0.01 | 0.22 | 1.78 | (6.30,5.97) |\n", "| 18 | 17 | -0.59 | 0.29 | 1.83 | (6.30,5.97) |\n", "| 19 | 18 | 4.33 | 0.36 | 1.91 | (6.30,5.97) |\n", "| 20 | 19 | 2.80 | 0.42 | 1.95 | (6.30,5.97) |\n", "| 21 | 20 | -0.80 | 0.51 | 1.97 | (6.30,5.97) |\n", "| 22 | 21 | -6.00 | 0.67 | 1.95 | (6.30,5.97) |\n", "| 23 | 22 | 2.37 | 0.76 | 1.90 | (6.30,5.97) |\n", "| 24 | 23 | 0.83 | 0.82 | 1.88 | (6.30,5.97) |\n", "| 25 | 24 | -0.62 | 0.78 | 1.86 | (6.30,5.97) |\n", "| 26 | 25 | -0.87 | 0.82 | 1.84 | (6.30,5.97) |\n", "| 27 | 26 | 3.52 | 0.82 | 1.84 | (6.30,5.97) |\n", "| 28 | 27 | 4.63 | 0.86 | 1.87 | (6.30,5.97) |\n", "| 29 | 28 | 5.46 | 0.88 | 1.92 | (6.30,5.97) |\n", "| 30 | 29 | 1.04 | 0.90 | 1.95 | (6.30,5.97) |\n", "| 31 | 30 | 1.64 | 0.91 | 2.00 | (6.30,5.97) |\n", "| 32 | 31 | 0.20 | 0.90 | 2.06 | (6.30,5.97) |\n", "| 33 | 32 | 0.88 | 0.93 | 2.13 | (6.30,5.97) |\n", "| 34 | 33 | 1.35 | 0.96 | 2.22 | (6.30,5.97) |\n", "| 35 | 34 | 3.43 | 0.99 | 2.28 | (6.30,5.97) |\n", "| 36 | 35 | 3.22 | 1.04 | 2.32 | (6.30,5.97) |\n", "| 37 | 36 | 5.27 | 1.14 | 2.39 | (6.30,5.97) |\n", "| 38 | 37 | 5.99 | 1.25 | 2.48 | (6.30,5.97) |\n", "| 39 | 38 | 5.99 | 1.43 | 2.53 | (6.30,5.97) |\n", "| 40 | 39 | 4.19 | 1.56 | 2.58 | (6.30,5.97) |\n", "| 41 | 40 | 4.06 | 1.69 | 2.59 | (6.30,5.97) |\n", "| 42 | 41 | 3.93 | 1.79 | 2.60 | (6.30,5.97) |\n", "| 43 | 42 | 0.38 | 1.84 | 2.61 | (6.30,5.97) |\n", "| 44 | 43 | 3.33 | 1.92 | 2.61 | (6.30,5.97) |\n", "| 45 | 44 | 2.85 | 1.97 | 2.61 | (6.30,5.97) |\n", "| 46 | 45 | -0.62 | 2.00 | 2.59 | (6.30,5.97) |\n", "| 47 | 46 | 0.62 | 2.02 | 2.57 | (6.30,5.97) |\n", "| 48 | 47 | 2.22 | 2.05 | 2.52 | (6.30,5.97) |\n", "| 49 | 48 | 0.99 | 2.04 | 2.53 | (6.30,5.97) |\n", "| 50 | 49 | 5.96 | 2.09 | 2.51 | (6.30,5.97) |\n", "| 51 | 50 | -4.71 | 2.08 | 2.54 | (6.30,5.97) |\n", "| 52 | 51 | 1.17 | 2.09 | 2.52 | (6.30,5.97) |\n", "| 53 | 52 | 0.70 | 2.14 | 2.51 | (6.30,5.97) |\n", "| 54 | 53 | -0.97 | 2.19 | 2.45 | (6.30,5.97) |\n", "| 55 | 54 | 2.69 | 2.23 | 2.40 | (6.30,5.97) |\n", "| 56 | 55 | -4.29 | 2.19 | 2.38 | (6.30,5.97) |\n", "| 57 | 56 | 2.00 | 2.22 | 2.29 | (6.30,5.97) |\n", "| 58 | 57 | 1.90 | 2.25 | 2.22 | (6.30,5.97) |\n", "| 59 | 58 | -1.62 | 2.28 | 2.14 | (6.30,5.97) |\n", "| 60 | 59 | 1.61 | 2.26 | 2.08 | (6.30,5.97) |\n", "| 61 | 60 | 1.23 | 2.23 | 2.03 | (6.30,5.97) |\n", "| 62 | 61 | 1.38 | 2.18 | 1.99 | (6.30,5.97) |\n", "| 63 | 62 | 2.23 | 2.16 | 1.96 | (6.30,5.97) |\n", "| 64 | 63 | 1.43 | 2.16 | 1.91 | (6.30,5.97) |\n", "| 65 | 64 | -0.70 | 2.14 | 1.88 | (6.30,5.97) |\n", "| 66 | 65 | 2.59 | 2.14 | 1.85 | (6.30,5.97) |\n", "| 67 | 66 | 0.93 | 2.13 | 1.83 | (6.30,5.97) |\n", "| 68 | 67 | 1.79 | 2.13 | 1.81 | (6.30,5.97) |\n", "| 69 | 68 | 1.58 | 2.12 | 1.80 | (6.30,5.97) |\n", "| 70 | 69 | 0.97 | 2.10 | 1.82 | (6.30,5.97) |\n", "| 71 | 70 | 1.95 | 2.07 | 1.81 | (6.30,5.97) |\n", "| 72 | 71 | 0.56 | 2.04 | 1.80 | (6.30,5.97) |\n", "| 73 | 72 | 1.54 | 2.02 | 1.78 | (6.30,5.97) |\n", "| 74 | 73 | -0.07 | 2.00 | 1.77 | (6.30,5.97) |\n", "| 75 | 74 | 1.18 | 1.97 | 1.78 | (6.30,5.97) |\n", "| 76 | 75 | 0.83 | 1.96 | 1.75 | (6.30,5.97) |\n", "| 77 | 76 | 1.11 | 1.94 | 1.71 | (6.30,5.97) |\n", "| 78 | 77 | 1.73 | 1.95 | 1.69 | (6.30,5.97) |\n", "| 79 | 78 | 0.16 | 1.94 | 1.66 | (6.30,5.97) |\n", "| 80 | 79 | 2.62 | 1.94 | 1.64 | (6.30,5.97) |\n", "| 81 | 80 | 2.81 | 1.95 | 1.63 | (6.30,5.97) |\n", "| 82 | 81 | -1.60 | 1.97 | 1.62 | (6.30,5.97) |\n", "| 83 | 82 | 4.01 | 1.95 | 1.63 | (6.30,5.97) |\n", "| 84 | 83 | 5.99 | 1.96 | 1.69 | (6.30,5.97) |\n", "| 85 | 84 | 5.99 | 2.03 | 1.60 | (6.30,5.97) |\n", "| 86 | 85 | 4.49 | 2.06 | 1.53 | (6.30,5.97) |\n", "| 87 | 86 | -0.62 | 2.09 | 1.45 | (6.30,5.97) |\n", "| 88 | 87 | 1.30 | 2.12 | 1.41 | (6.30,5.97) |\n", "| 89 | 88 | -0.70 | 2.15 | 1.37 | (6.30,5.97) |\n", "| 90 | 89 | 2.85 | 2.19 | 1.33 | (6.30,5.97) |\n", "| 91 | 90 | 0.63 | 2.23 | 1.28 | (6.30,5.97) |\n", "| 92 | 91 | -0.24 | 2.27 | 1.25 | (6.30,5.97) |\n", "| 93 | 92 | -0.26 | 2.31 | 1.23 | (6.30,5.97) |\n", "| 94 | 93 | 3.01 | 2.35 | 1.15 | (6.30,5.97) |\n", "| 95 | 94 | 2.36 | 2.37 | 1.08 | (6.30,5.97) |\n", "| 96 | 95 | 3.39 | 2.36 | 1.05 | (6.30,5.97) |\n", "| 97 | 96 | 3.71 | 2.37 | 1.04 | (6.30,5.97) |\n", "| 98 | 97 | 5.99 | 2.38 | 1.00 | (6.30,5.97) |\n", "| 99 | 98 | 5.99 | 2.43 | 0.99 | (6.30,5.97) |\n", "| 100 | 99 | 5.69 | 2.44 | 0.99 | (6.30,5.97) |\n", "| 101 | 100 | 0.35 | 2.46 | 0.90 | (6.30,5.97) |\n", "| 102 | 101 | 0.00 | 2.46 | 0.79 | (6.30,5.97) |\n", "| 103 | 102 | 2.10 | 2.45 | 0.66 | (6.30,5.97) |\n", "| 104 | 103 | 3.25 | 2.43 | 0.57 | (6.30,5.97) |\n", "| 105 | 104 | 3.92 | 2.42 | 0.47 | (6.30,5.97) |\n", "| 106 | 105 | 0.01 | 2.41 | 0.41 | (6.30,5.97) |\n", "| 107 | 106 | 3.23 | 2.42 | 0.33 | (6.30,5.97) |\n", "| 108 | 107 | 0.98 | 2.41 | 0.28 | (6.30,5.97) |\n", "| 109 | 108 | 1.37 | 2.40 | 0.25 | (6.30,5.97) |\n", "| 110 | 109 | -0.05 | 2.41 | 0.22 | (6.30,5.97) |\n", "| 111 | 110 | 1.56 | 2.38 | 0.20 | (6.30,5.97) |\n", "| 112 | 111 | 2.63 | 2.35 | 0.16 | (6.30,5.97) |\n", "| 113 | 112 | 0.12 | 2.32 | 0.15 | (6.30,5.97) |\n", "| 114 | 113 | -1.41 | 2.32 | 0.14 | (6.30,5.97) |\n", "| 115 | 114 | 1.38 | 2.32 | 0.14 | (6.30,5.97) |\n", "| 116 | 115 | 0.00 | 2.29 | 0.15 | (6.30,5.97) |\n", "| 117 | 116 | 3.14 | 2.27 | 0.13 | (6.30,5.97) |\n", "| 118 | 117 | -0.86 | 2.25 | 0.12 | (6.30,5.97) |\n", "| 119 | 118 | 4.18 | 2.20 | 0.14 | (6.30,5.97) |\n", "| 120 | 119 | -1.10 | 2.19 | 0.15 | (6.30,5.97) |\n", "| 121 | 120 | -0.71 | 2.17 | 0.20 | (6.30,5.97) |\n", "| 122 | 121 | 0.67 | 2.13 | 0.24 | (6.30,5.97) |\n", "| 123 | 122 | 1.49 | 2.12 | 0.27 | (6.30,5.97) |\n", "| 124 | 123 | 1.96 | 2.09 | 0.32 | (6.30,5.97) |\n", "| 125 | 124 | -0.32 | 2.08 | 0.34 | (6.30,5.97) |\n", "| 126 | 125 | 2.68 | 2.04 | 0.36 | (6.30,5.97) |\n", "| 127 | 126 | 0.64 | 2.02 | 0.33 | (6.30,5.97) |\n", "| 128 | 127 | -1.77 | 1.99 | 0.33 | (6.30,5.97) |\n", "| 129 | 128 | -1.93 | 1.98 | 0.31 | (6.30,5.97) |\n", "| 130 | 129 | -1.08 | 1.99 | 0.23 | (6.30,5.97) |\n", "| 131 | 130 | 5.99 | 1.94 | 0.28 | (6.30,5.97) |\n", "| 132 | 131 | -4.71 | 1.95 | 0.28 | (6.30,5.97) |\n", "| 133 | 132 | -4.51 | 1.92 | 0.35 | (6.30,5.97) |\n", "| 134 | 133 | 1.89 | 1.86 | 0.42 | (6.30,5.97) |\n", "| 135 | 134 | -2.08 | 1.82 | 0.46 | (6.30,5.97) |\n", "| 136 | 135 | -0.74 | 1.76 | 0.50 | (6.30,5.97) |\n", "| 137 | 136 | -3.69 | 1.70 | 0.57 | (6.30,5.97) |\n", "| 138 | 137 | 2.54 | 1.58 | 0.67 | (6.30,5.97) |\n", "| 139 | 138 | 5.99 | 1.54 | 0.66 | (6.30,5.97) |\n", "| 140 | 139 | 5.99 | 1.41 | 0.72 | (6.30,5.97) |\n", "| 141 | 140 | 0.62 | 1.35 | 0.73 | (6.30,5.97) |\n", "| 142 | 141 | 3.40 | 1.35 | 0.76 | (6.30,5.97) |\n", "| 143 | 142 | 1.15 | 1.35 | 0.80 | (6.30,5.97) |\n", "| 144 | 143 | 0.27 | 1.31 | 0.84 | (6.30,5.97) |\n", "| 145 | 144 | -1.53 | 1.31 | 0.82 | (6.30,5.97) |\n", "| 146 | 145 | -6.00 | 1.32 | 0.82 | (6.30,5.97) |\n", "| 147 | 146 | 0.00 | 1.33 | 0.90 | (6.30,5.97) |\n", "| 148 | 147 | 0.00 | 1.33 | 1.02 | (6.30,5.97) |\n", "| 149 | 148 | 5.99 | 1.38 | 1.05 | (6.30,5.97) |\n", "| 150 | 149 | -5.73 | 1.42 | 0.98 | (6.30,5.97) |\n", "| 151 | 150 | 1.77 | 1.50 | 0.93 | (6.30,5.97) |\n", "| 152 | 151 | -6.00 | 1.64 | 0.81 | (6.30,5.97) |\n", "| 153 | 152 | -5.97 | 1.72 | 0.76 | (6.30,5.97) |\n", "| 154 | 153 | 2.13 | 1.81 | 0.72 | (6.30,5.97) |\n", "| 155 | 154 | 0.97 | 1.88 | 0.70 | (6.30,5.97) |\n", "| 156 | 155 | -3.46 | 1.94 | 0.69 | (6.30,5.97) |\n", "| 157 | 156 | -2.27 | 1.98 | 0.69 | (6.30,5.97) |\n", "| 158 | 157 | 3.29 | 2.00 | 0.68 | (6.30,5.97) |\n", "| 159 | 158 | 0.53 | 2.02 | 0.70 | (6.30,5.97) |\n", "| 160 | 159 | -3.84 | 2.04 | 0.72 | (6.30,5.97) |\n", "| 161 | 160 | 0.88 | 2.05 | 0.75 | (6.30,5.97) |\n", "| 162 | 161 | 1.50 | 2.06 | 0.78 | (6.30,5.97) |\n", "| 163 | 162 | 0.86 | 2.07 | 0.80 | (6.30,5.97) |\n", "| 164 | 163 | -1.45 | 2.09 | 0.83 | (6.30,5.97) |\n", "| 165 | 164 | -5.89 | 2.12 | 0.87 | (6.30,5.97) |\n", "| 166 | 165 | 1.19 | 2.15 | 0.90 | (6.30,5.97) |\n", "| 167 | 166 | 1.00 | 2.15 | 0.90 | (6.30,5.97) |\n", "| 168 | 167 | 1.81 | 2.14 | 0.90 | (6.30,5.97) |\n", "| 169 | 168 | -0.80 | 2.14 | 0.90 | (6.30,5.97) |\n", "| 170 | 169 | 0.67 | 2.15 | 0.89 | (6.30,5.97) |\n", "| 171 | 170 | 0.61 | 2.16 | 0.89 | (6.30,5.97) |\n", "| 172 | 171 | 1.12 | 2.15 | 0.89 | (6.30,5.97) |\n", "| 173 | 172 | -1.80 | 2.13 | 0.91 | (6.30,5.97) |\n", "| 174 | 173 | -2.57 | 2.13 | 0.95 | (6.30,5.97) |\n", "| 175 | 174 | -6.00 | 0.76 | -0.29 | (6.30,5.97) |\n", "| 176 | 175 | 5.74 | 2.09 | 1.06 | (6.30,5.97) |\n", "| 177 | 176 | -1.61 | 2.11 | 1.08 | (6.30,5.97) |\n", "| 178 | 177 | -0.99 | 2.11 | 1.05 | (6.30,5.97) |\n", "| 179 | 178 | 1.95 | 2.12 | 1.02 | (6.30,5.97) |\n", "| 180 | 179 | 1.87 | 2.11 | 0.99 | (6.30,5.97) |\n", "| 181 | 180 | -0.88 | 2.12 | 0.99 | (6.30,5.97) |\n", "| 182 | 181 | 0.48 | 2.13 | 1.01 | (6.30,5.97) |\n", "| 183 | 182 | -1.57 | 2.14 | 1.04 | (6.30,5.97) |\n", "| 184 | 183 | -0.01 | 2.17 | 1.08 | (6.30,5.97) |\n", "| 185 | 184 | -0.61 | 2.19 | 1.11 | (6.30,5.97) |\n", "| 186 | 185 | -1.26 | 2.22 | 1.16 | (6.30,5.97) |\n", "| 187 | 186 | -0.16 | 2.25 | 1.19 | (6.30,5.97) |\n", "| 188 | 187 | -0.22 | 2.28 | 1.23 | (6.30,5.97) |\n", "| 189 | 188 | 0.00 | 2.32 | 1.25 | (6.30,5.97) |\n", "| 190 | 189 | -0.38 | 2.38 | 1.32 | (6.30,5.97) |\n", "| 191 | 190 | 0.00 | 2.44 | 1.35 | (6.30,5.97) |\n", "| 192 | 191 | 0.00 | 2.50 | 1.38 | (6.30,5.97) |\n", "| 193 | 192 | 0.00 | 2.54 | 1.41 | (6.30,5.97) |\n", "| 194 | 193 | -0.44 | 2.59 | 1.47 | (6.30,5.97) |\n", "| 195 | 194 | -0.67 | 2.63 | 1.53 | (6.30,5.97) |\n", "| 196 | 195 | 0.69 | 2.67 | 1.60 | (6.30,5.97) |\n", "| 197 | 196 | -0.31 | 2.72 | 1.66 | (6.30,5.97) |\n", "| 198 | 197 | -0.27 | 2.76 | 1.72 | (6.30,5.97) |\n", "| 199 | 198 | -0.46 | 2.79 | 1.77 | (6.30,5.97) |\n", "| 200 | 199 | -1.27 | 2.82 | 1.84 | (6.30,5.97) |\n", "| 201 | 200 | 0.03 | 2.84 | 1.89 | (6.30,5.97) |\n", "| 202 | 201 | 0.24 | 2.86 | 1.92 | (6.30,5.97) |\n", "| 203 | 202 | 0.47 | 2.87 | 1.94 | (6.30,5.97) |\n", "| 204 | 203 | 0.76 | 2.88 | 1.95 | (6.30,5.97) |\n", "| 205 | 204 | -2.53 | 2.85 | 1.96 | (6.30,5.97) |\n", "| 206 | 205 | -3.82 | 2.78 | 1.92 | (6.30,5.97) |\n", "| 207 | 206 | -2.64 | 2.66 | 1.87 | (6.30,5.97) |\n", "| 208 | 207 | 1.72 | 2.54 | 1.79 | (6.30,5.97) |\n", "| 209 | 208 | 5.02 | 2.37 | 1.71 | (6.30,5.97) |\n", "| 210 | 209 | -0.41 | 2.27 | 1.68 | (6.30,5.97) |\n", "| 211 | 210 | 1.44 | 2.15 | 1.67 | (6.30,5.97) |\n", "| 212 | 211 | -5.68 | 1.99 | 1.62 | (6.30,5.97) |\n", "| 213 | 212 | 0.00 | 1.91 | 1.59 | (6.30,5.97) |\n", "| 214 | 213 | -2.03 | 1.84 | 1.60 | (6.30,5.97) |\n", "| 215 | 214 | -5.80 | 1.72 | 1.61 | (6.30,5.97) |\n", "| 216 | 215 | -2.52 | 1.64 | 1.61 | (6.30,5.97) |\n", "| 217 | 216 | -3.02 | 1.56 | 1.60 | (6.30,5.97) |\n", "| 218 | 217 | -0.79 | 1.52 | 1.61 | (6.30,5.97) |\n", "| 219 | 218 | -0.27 | 1.49 | 1.60 | (6.30,5.97) |\n", "| 220 | 219 | -2.81 | 1.47 | 1.60 | (6.30,5.97) |\n", "| 221 | 220 | -1.28 | 1.44 | 1.61 | (6.30,5.97) |\n", "| 222 | 221 | -2.63 | 1.41 | 1.63 | (6.30,5.97) |\n", "| 223 | 222 | -1.33 | 1.39 | 1.64 | (6.30,5.97) |\n", "| 224 | 223 | -1.16 | 1.37 | 1.64 | (6.30,5.97) |\n", "| 225 | 224 | -2.10 | 1.34 | 1.65 | (6.30,5.97) |\n", "| 226 | 225 | -2.43 | 1.31 | 1.65 | (6.30,5.97) |\n", "| 227 | 226 | -1.13 | 1.28 | 1.66 | (6.30,5.97) |\n", "| 228 | 227 | -1.61 | 1.26 | 1.67 | (6.30,5.97) |\n", "| 229 | 228 | 0.23 | 1.24 | 1.67 | (6.30,5.97) |\n", "| 230 | 229 | -1.46 | 1.21 | 1.68 | (6.30,5.97) |\n", "| 231 | 230 | -1.54 | 1.19 | 1.69 | (6.30,5.97) |\n", "| 232 | 231 | 0.00 | 1.17 | 1.71 | (6.30,5.97) |\n", "| 233 | 232 | -1.35 | 1.15 | 1.72 | (6.30,5.97) |\n", "| 234 | 233 | 1.73 | 1.15 | 1.73 | (6.30,5.97) |\n", "| 235 | 234 | -0.12 | 1.14 | 1.73 | (6.30,5.97) |\n", "| 236 | 235 | -1.14 | 1.12 | 1.73 | (6.30,5.97) |\n", "| 237 | 236 | -0.49 | 1.09 | 1.75 | (6.30,5.97) |\n", "| 238 | 237 | -2.95 | 1.07 | 1.76 | (6.30,5.97) |\n", "| 239 | 238 | -0.12 | 1.06 | 1.76 | (6.30,5.97) |\n", "| 240 | 239 | -1.87 | 1.03 | 1.74 | (6.30,5.97) |\n", "| 241 | 240 | -0.03 | 1.00 | 1.73 | (6.30,5.97) |\n", "| 242 | 241 | -1.49 | 0.99 | 1.73 | (6.30,5.97) |\n", "| 243 | 242 | 1.90 | 1.00 | 1.70 | (6.30,5.97) |\n", "| 244 | 243 | 0.24 | 0.96 | 1.70 | (6.30,5.97) |\n", "| 245 | 244 | 0.51 | 0.95 | 1.69 | (6.30,5.97) |\n", "| 246 | 245 | -0.80 | 0.92 | 1.68 | (6.30,5.97) |\n", "| 247 | 246 | -0.32 | 0.91 | 1.67 | (6.30,5.97) |\n", "| 248 | 247 | -0.20 | 0.90 | 1.66 | (6.30,5.97) |\n", "| 249 | 248 | -2.02 | 0.89 | 1.66 | (6.30,5.97) |\n", "| 250 | 249 | -1.93 | 0.90 | 1.66 | (6.30,5.97) |\n", "| 251 | 250 | 1.96 | 0.89 | 1.66 | (6.30,5.97) |\n", "| 252 | 251 | 0.94 | 0.89 | 1.66 | (6.30,5.97) |\n", "| 253 | 252 | -0.72 | 0.89 | 1.65 | (6.30,5.97) |\n", "| 254 | 253 | -0.04 | 0.89 | 1.66 | (6.30,5.97) |\n", "| 255 | 254 | -0.53 | 0.90 | 1.67 | (6.30,5.97) |\n", "| 256 | 255 | 0.46 | 0.88 | 1.65 | (6.30,5.97) |\n", "| 257 | 256 | -1.66 | 0.86 | 1.63 | (6.30,5.97) |\n", "| 258 | 257 | -1.58 | 0.85 | 1.63 | (6.30,5.97) |\n", "| 259 | 258 | 1.73 | 0.86 | 1.62 | (6.30,5.97) |\n", "| 260 | 259 | -1.50 | 0.87 | 1.62 | (6.30,5.97) |\n", "| 261 | 260 | -0.70 | 0.84 | 1.59 | (6.30,5.97) |\n", "| 262 | 261 | -1.45 | 0.84 | 1.59 | (6.30,5.97) |\n", "| 263 | 262 | 1.30 | 0.83 | 1.58 | (6.30,5.97) |\n", "| 264 | 263 | -1.63 | 0.84 | 1.59 | (6.30,5.97) |\n", "| 265 | 264 | 1.35 | 0.83 | 1.55 | (6.30,5.97) |\n", "| 266 | 265 | -1.02 | 0.83 | 1.56 | (6.30,5.97) |\n", "| 267 | 266 | 0.18 | 0.83 | 1.54 | (6.30,5.97) |\n", "| 268 | 267 | 0.24 | 0.84 | 1.52 | (6.30,5.97) |\n", "| 269 | 268 | -0.84 | 0.84 | 1.50 | (6.30,5.97) |\n", "| 270 | 269 | -0.83 | 0.85 | 1.49 | (6.30,5.97) |\n", "| 271 | 270 | 0.00 | 0.85 | 1.47 | (6.30,5.97) |\n", "| 272 | 271 | 0.84 | 0.84 | 1.45 | (6.30,5.97) |\n", "| 273 | 272 | 0.00 | 0.85 | 1.41 | (6.30,5.97) |\n", "| 274 | 273 | 0.00 | 0.85 | 1.38 | (6.30,5.97) |\n", "| 275 | 274 | -1.67 | 0.85 | 1.36 | (6.30,5.97) |\n", "| 276 | 275 | -0.12 | 0.85 | 1.33 | (6.30,5.97) |\n", "| 277 | 276 | -0.86 | 0.86 | 1.30 | (6.30,5.97) |\n", "| 278 | 277 | -0.06 | 0.87 | 1.27 | (6.30,5.97) |\n", "| 279 | 278 | -2.59 | 0.87 | 1.24 | (6.30,5.97) |\n", "| 280 | 279 | -0.78 | 0.87 | 1.24 | (6.30,5.97) |\n", "+---------+------------+---------------+-------+-------+-------------+\n", "Successfully wrote lumen to output/oct/oct_lumen.obj\n", "Successfully wrote lumen to output/oct/oct_replaced_lumen.obj\n" ] }, { "data": { "text/html": [ "
\n", "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "oct_raw = np.genfromtxt(\"oct_single/oct_contours_raw.csv\", delimiter=',')\n", "oct_ref = np.genfromtxt(\"oct_single/oct_ref.csv\", delimiter=',')\n", "\n", "oct_input = mm.numpy_to_inputdata(\n", " lumen_arr=oct_raw, ref_point=oct_ref,\n", " record=None, diastole=True, label=\"oct\",\n", ")\n", "\n", "oct_recon, _ = mm.from_array_single(\n", " input_data=oct_input,\n", " step_rotation_deg=0.01,\n", " range_rotation_deg=6,\n", " image_center=(5.0, 5.0),\n", " radius=0.5,\n", " n_points=40,\n", " write_obj=False,\n", " watertight=False,\n", " output_path=\"output/oct\",\n", " smooth=False,\n", ")\n", "\n", "# Replace an outlier frame and write both versions for comparison\n", "frame = oct_recon.get_frame_at_z(34.8)\n", "oct_replaced = oct_recon.replace_frame(frame.id + 1, frame)\n", "\n", "mm.to_obj(oct_recon, \"output/oct\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"oct\")\n", "mm.to_obj(oct_replaced, \"output/oct\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"oct_replaced\")\n", "\n", "oct_mesh = trimesh.load(\"output/oct/oct_lumen.obj\")\n", "oct_mesh_fixed = trimesh.load(\"output/oct/oct_replaced_lumen.obj\")\n", "\n", "fig = make_subplots(\n", " rows=1, cols=2,\n", " specs=[[{\"type\": \"scene\"}, {\"type\": \"scene\"}]],\n", " subplot_titles=[\"OCT original\", \"OCT with replaced frame\"],\n", ")\n", "scene_cfg = dict(aspectmode=\"data\", camera=dict(eye=dict(x=1.5, y=1.5, z=1.0)))\n", "for t in [trimesh_to_mesh3d(oct_mesh, \"royalblue\", \"OCT Lumen\")]:\n", " fig.add_trace(t, row=1, col=1)\n", "for t in [trimesh_to_mesh3d(oct_mesh_fixed, \"tomato\", \"OCT Replaced\")]:\n", " fig.add_trace(t, row=1, col=2)\n", "fig.update_layout(\n", " width=1200, height=600,\n", " scene=scene_cfg, scene2=scene_cfg,\n", " margin=dict(l=0, r=0, t=40, b=0),\n", ")\n", "fig.show()" ] }, { "cell_type": "markdown", "id": "bbbac6fc", "metadata": {}, "source": [ "### Alignment algorithm — parameter fine-tuning\n", "\n", "`from_array_singlepair` (and all `from_array_*` variants) expose every alignment parameter.\n", "The example below uses `from_array_singlepair` with a `record` file (AIVUS-CAA combined CSV):\n", "\n", "**Key parameters:**\n", "\n", "- `step_rotation_deg` / `range_rotation_deg` — angular search space; ±range at step resolution.\n", " Reducing `range_rotation_deg` when the orientation is roughly known cuts runtime substantially.\n", "- `sample_size` — contours are downsampled to at most this many points before Hausdorff distance\n", " computation. Default 500 is appropriate for most IVUS datasets.\n", "- `image_center`, `radius`, `n_points` — define the synthetic catheter used as a rotational anchor.\n", " Larger `n_points` weights the catheter more heavily; `n_points=0` disables the catheter.\n", "- `bruteforce` — sweeps the full angular range without hierarchical refinement. Avoid for routine use.\n", "- `smooth` — 3-point moving average over each contour after alignment. Recommended.\n", "- `postprocessing` — equalises axial frame spacing across pullbacks. Recommended when heart rate\n", " differs between conditions (e.g. rest vs. stress)." ] }, { "cell_type": "code", "execution_count": 12, "id": "73e2ce7f", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:15:20.759729Z", "iopub.status.busy": "2026-05-04T15:15:20.759481Z", "iopub.status.idle": "2026-05-04T15:15:25.239986Z", "shell.execute_reply": "2026-05-04T15:15:25.238204Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "✅ Successfully built geometry from input data\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: diastole_rest\n", "Diastole phase: Yes\n", "\n", "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)] ...\n", "\n", "✅ Successfully built geometry from input data\n", "-----------------------------------------\n", "✅ Lumen\n", "❌ Eem\n", "❌ Calcification\n", "❌ Sidebranch\n", "✅ Catheter\n", "-----------------------------------------\n", "Label: systole_rest\n", "Diastole phase: No\n", "\n", "\n", "+-------------------------------------------------------------------+\n", "| ✅ Finished aligning 'systole_rest' (anomalous: true) |\n", "+---------+------------+---------------+-------+------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+------+-------------+\n", "| 1 | 0 | 2.23 | 0.19 | 0.01 | (3.82,5.13) |\n", "| 2 | 1 | 8.17 | -0.35 | 0.16 | (3.82,5.13) |\n", "| 3 | 2 | -10.33 | -0.24 | 0.54 | (3.82,5.13) |\n", "| 4 | 3 | -4.10 | -0.53 | 0.95 | (3.82,5.13) |\n", "| 5 | 4 | 0.82 | -0.56 | 0.27 | (3.82,5.13) |\n", "| 6 | 5 | 5.14 | -0.60 | 0.66 | (3.82,5.13) |\n", "| 7 | 6 | -6.48 | -1.01 | 0.70 | (3.82,5.13) |\n", "| 8 | 7 | 14.71 | -0.89 | 0.82 | (3.82,5.13) |\n", "| 9 | 8 | -35.31 | -0.84 | 0.25 | (3.82,5.13) |\n", "| 10 | 9 | 25.53 | -1.27 | 0.29 | (3.82,5.13) |\n", "| 11 | 10 | 41.32 | -2.21 | 0.74 | (3.82,5.13) |\n", "| 12 | 11 | -21.19 | -1.75 | 0.69 | (3.82,5.13) |\n", "| 13 | 12 | 4.84 | -1.87 | 0.67 | (3.82,5.13) |\n", "| 14 | 13 | -6.91 | -1.95 | 0.53 | (3.82,5.13) |\n", "| 15 | 14 | 23.44 | -2.08 | 0.47 | (3.82,5.13) |\n", "| 16 | 15 | 7.25 | -2.09 | 0.54 | (3.82,5.13) |\n", "+---------+------------+---------------+-------+------+-------------+\n", "\n", "+-------------------------------------------------------------------+\n", "| ✅ Finished aligning 'diastole_rest' (anomalous: true) |\n", "+---------+------------+---------------+-------+------+-------------+\n", "| Contour | Matched To | Rotation (°) | Tx | Ty | Centroid |\n", "+---------+------------+---------------+-------+------+-------------+\n", "| 1 | 0 | -7.71 | -0.12 | 0.36 | (3.59,5.61) |\n", "| 2 | 1 | -43.34 | -1.07 | 2.73 | (3.59,5.61) |\n", "| 3 | 2 | 11.05 | -0.70 | 2.60 | (3.59,5.61) |\n", "| 4 | 3 | 0.98 | -0.62 | 2.71 | (3.59,5.61) |\n", "| 5 | 4 | -3.39 | -0.80 | 2.83 | (3.59,5.61) |\n", "| 6 | 5 | -60.00 | -1.80 | 1.63 | (3.59,5.61) |\n", "| 7 | 6 | 47.39 | -1.66 | 2.23 | (3.59,5.61) |\n", "| 8 | 7 | 3.29 | -1.57 | 2.31 | (3.59,5.61) |\n", "| 9 | 8 | 1.02 | -1.63 | 2.44 | (3.59,5.61) |\n", "| 10 | 9 | -60.00 | -1.49 | 1.22 | (3.59,5.61) |\n", "| 11 | 10 | -1.47 | -1.41 | 1.23 | (3.59,5.61) |\n", "| 12 | 11 | 47.99 | -1.23 | 2.29 | (3.59,5.61) |\n", "| 13 | 12 | 6.95 | -1.08 | 2.48 | (3.59,5.61) |\n", "| 14 | 13 | -54.00 | -1.06 | 1.18 | (3.59,5.61) |\n", "| 15 | 14 | 19.24 | -0.40 | 0.39 | (3.59,5.61) |\n", "| 16 | 15 | -13.07 | -0.29 | 0.81 | (3.59,5.61) |\n", "| 17 | 16 | -36.24 | -0.60 | 1.46 | (3.59,5.61) |\n", "| 18 | 17 | 5.08 | -0.44 | 1.25 | (3.59,5.61) |\n", "| 19 | 18 | -2.25 | -0.08 | 0.20 | (3.59,5.61) |\n", "+---------+------------+---------------+-------+------+-------------+\n", "\n", "✅ Aligned geometry 'systole_rest' to 'diastole_rest'\n", "-----------------------------------------\n", "Applied initial translation: (-0.22, 0.48, -2.67) mm\n", "Found best rotation of 11.56° with parameters: \n", "range: 60.00° \n", "step size: 0.01°\n", "Applied final translation: ( 0, 0.00, -0.00) mm\n", "-----------------------------------------\n", "\n", "Saving files for 'diastole_rest - systole_rest' to 'output/rest_array'\n", "LUMEN .obj files: 2/2 written successfully\n", "CATHETER .obj files: 2/2 written successfully\n", "WALL .obj files: 2/2 written successfully\n" ] } ], "source": [ "record = np.genfromtxt(\"ivus_rest/combined_sorted_manual.csv\", delimiter=',', skip_header=1)\n", "dia_cont = np.genfromtxt(\"ivus_rest/diastolic_contours.csv\", delimiter='\\t')\n", "dia_ref = np.genfromtxt(\"ivus_rest/diastolic_reference_points.csv\", delimiter='\\t')\n", "sys_cont = np.genfromtxt(\"ivus_rest/systolic_contours.csv\", delimiter='\\t')\n", "sys_ref = np.genfromtxt(\"ivus_rest/systolic_reference_points.csv\", delimiter='\\t')\n", "\n", "dia_input = mm.numpy_to_inputdata(\n", " lumen_arr=dia_cont, ref_point=dia_ref,\n", " record=record, diastole=True, label=\"diastole_rest\",\n", ")\n", "sys_input = mm.numpy_to_inputdata(\n", " lumen_arr=sys_cont, ref_point=sys_ref,\n", " record=record, diastole=False, label=\"systole_rest\",\n", ")\n", "\n", "rest_array, (dia_logs, sys_logs) = mm.from_array_singlepair(\n", " input_data_a=dia_input,\n", " input_data_b=sys_input,\n", " step_rotation_deg=0.01,\n", " range_rotation_deg=60,\n", " output_path=\"output/rest_array\",\n", " interpolation_steps=0,\n", " smooth=True,\n", " postprocessing=True,\n", ")\n", "print(\"Logs (dia):\", dia_logs[:3], \"...\")" ] }, { "cell_type": "markdown", "id": "c933ae3c", "metadata": {}, "source": [ "## 4. Alignment with a centerline\n", "\n", "A `PyCenterline` is built from a raw (N×3) coordinate array — no index column required:\n", "\n", "```python\n", "cl_raw = np.genfromtxt(\"centerline_raw.csv\", delimiter=',')\n", "centerline = mm.numpy_to_centerline(cl_raw)\n", "```\n", "\n", "`align_three_point` co-registers a geometry pair onto the centerline using three anatomical\n", "landmarks: an aortic reference point, an upper vessel point, and a lower vessel point.\n", "The preferred method is `align_combined`, which additionally optimizes against a CCTA point cloud\n", "(see the [CCTA tutorial](../tutorial_ccta.rst) for how to prepare `results[\"rca_points\"]`)." ] }, { "cell_type": "code", "execution_count": 13, "id": "8c6dded4", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:15:25.241926Z", "iopub.status.busy": "2026-05-04T15:15:25.241740Z", "iopub.status.idle": "2026-05-04T15:15:26.017802Z", "shell.execute_reply": "2026-05-04T15:15:26.016732Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "---------------------Centerline alignment: Finding optimal rotation---------------------\n", "Resampled centerline (first 3 points): [[ 0.00000000e+00 1.30847000e+01 -2.00350800e+02 1.75186020e+03]\n", " [ 1.00000000e+00 1.35807358e+01 -2.01462354e+02 1.75228890e+03]\n", " [ 2.00000000e+00 1.38271910e+01 -2.02714193e+02 1.75244535e+03]]\n", "✅ Best angle found: 75.00°\n", "\n", "Saving files for 'None' to 'output/aligned'\n", "LUMEN .obj files: 2/2 written successfully\n", "CATHETER .obj files: 2/2 written successfully\n", "WALL .obj files: 2/2 written successfully\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "resample_centerline_by_contours: centroid_count=14, centroid_mean_spacing=Some(1.2955758552631584), centerline_length=161.5421151319061, spacing=1.295576\n", "resample_centerline_by_contours: produced 125 points\n" ] } ], "source": [ "cl_raw = np.genfromtxt(\"centerline_raw.csv\", delimiter=',')\n", "cl = mm.numpy_to_centerline(cl_raw)\n", "\n", "aligned_geometry, resampled_cl = mm.align_three_point(\n", " centerline=cl,\n", " geometry=rest, # PyGeometryPair from from_file_full above\n", " main_ref_pt=(12.2605, -201.3643, 1751.0554),\n", " counterclockwise_ref_pt=(11.7567, -202.1920, 1754.7975),\n", " clockwise_ref_pt=(15.6605, -202.1920, 1749.9655),\n", " write=True,\n", " watertight=False,\n", " interpolation_steps=0,\n", ")\n", "print(\"Resampled centerline (first 3 points):\", mm.to_array(resampled_cl)[:3])" ] }, { "cell_type": "code", "execution_count": 14, "id": "3f1d66c1", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:15:26.019838Z", "iopub.status.busy": "2026-05-04T15:15:26.019651Z", "iopub.status.idle": "2026-05-04T15:15:26.525697Z", "shell.execute_reply": "2026-05-04T15:15:26.525055Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Successfully wrote lumen to output/aligned/dia_lumen.obj\n", "Successfully wrote lumen to output/aligned/sys_lumen.obj\n" ] }, { "data": { "text/html": [ "
\n", "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Write before/after alignment meshes with explicit names and visualise\n", "mm.to_obj(aligned_geometry.geom_a, \"output/aligned\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"dia\")\n", "mm.to_obj(aligned_geometry.geom_b, \"output/aligned\", watertight=False,\n", " contour_types=[mm.PyContourType.Lumen], filename_prefix=\"sys\")\n", "\n", "plot_pair(\n", " before_paths=[\"output/after/dia_lumen.obj\", \"output/after/sys_lumen.obj\"],\n", " after_paths=[\"output/aligned/dia_lumen.obj\", \"output/aligned/sys_lumen.obj\"],\n", " colors=[\"royalblue\", \"firebrick\"],\n", " titles=[\"Before Alignment\", \"After Alignment\"],\n", ")" ] }, { "cell_type": "markdown", "id": "7b802e8a", "metadata": {}, "source": [ "## 5. Saving geometries as `.obj` files\n", "\n", "`to_obj` can be called on any `PyGeometryPair` or `PyGeometry` to export all or selected contour\n", "layers. The `filename_prefix` determines the output file stem, and `contour_types` selects which\n", "layers to export:" ] }, { "cell_type": "code", "execution_count": 15, "id": "c8a24a1e", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:15:26.533542Z", "iopub.status.busy": "2026-05-04T15:15:26.533331Z", "iopub.status.idle": "2026-05-04T15:15:26.627786Z", "shell.execute_reply": "2026-05-04T15:15:26.626769Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Successfully wrote lumen to output/rest_export/aligned_lumen.obj\n", "Successfully wrote catheter to output/rest_export/aligned_catheter.obj\n", "['aligned_catheter.mtl', 'aligned_catheter.obj', 'aligned_lumen.mtl', 'aligned_lumen.obj']\n" ] } ], "source": [ "# Export lumen and catheter from the aligned rest geometry\n", "mm.to_obj(\n", " rest.geom_a,\n", " \"output/rest_export\",\n", " watertight=False,\n", " contour_types=[mm.PyContourType.Lumen, mm.PyContourType.Catheter],\n", " filename_prefix=\"aligned\",\n", ")\n", "# Creates: output/rest_export/aligned_lumen.obj and aligned_catheter.obj\n", "import os\n", "print(os.listdir(\"output/rest_export\"))" ] }, { "cell_type": "markdown", "id": "77b65ea8", "metadata": {}, "source": [ "## 6. Utility functions: `to_array` and `numpy_to_geometry`\n", "\n", "### `to_array`\n", "\n", "`to_array` is a single-dispatch converter from any `Py*` object to numpy arrays.\n", "The return type depends on the input:\n", "\n", "| Input type | Return |\n", "|------------|--------|\n", "| `PyContour` or `PyCenterline` | `ndarray` shape `(N, 4)`: frame_index, x, y, z |\n", "| `PyFrame` or `PyGeometry` | `dict[str, ndarray]` with keys `lumen`, `eem`, … `reference` |\n", "| `PyGeometryPair` | `tuple[dict, dict]` — one dict per geometry |\n", "| `PyInputData` | `dict` with layer keys plus `diastole` and `label` |" ] }, { "cell_type": "code", "execution_count": 16, "id": "7e7b7da5", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:15:26.629757Z", "iopub.status.busy": "2026-05-04T15:15:26.629570Z", "iopub.status.idle": "2026-05-04T15:15:26.736148Z", "shell.execute_reply": "2026-05-04T15:15:26.735513Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Lumen array shape (geom_a): (7014, 4)\n", "Keys: ['lumen', 'eem', 'calcification', 'sidebranch', 'catheter', 'wall', 'reference']\n", "Centerline array shape: (125, 4)\n", "Contour array shape: (501, 4)\n", "InputData keys: ['lumen', 'eem', 'calcification', 'sidebranch', 'reference', 'diastole', 'label', 'records']\n" ] } ], "source": [ "# PyGeometryPair → (dict, dict)\n", "rest_dia_arr, rest_sys_arr = mm.to_array(rest)\n", "print(\"Lumen array shape (geom_a):\", rest_dia_arr[\"lumen\"].shape)\n", "\n", "# PyGeometry → dict\n", "geom_arr = mm.to_array(rest.geom_a)\n", "print(\"Keys:\", list(geom_arr.keys()))\n", "\n", "# PyCenterline → ndarray\n", "cl_arr = mm.to_array(resampled_cl)\n", "print(\"Centerline array shape:\", cl_arr.shape)\n", "\n", "# PyContour → ndarray\n", "contour_arr = mm.to_array(rest.geom_a.frames[-1].lumen)\n", "print(\"Contour array shape:\", contour_arr.shape)\n", "\n", "# PyInputData → dict\n", "input_arr = mm.to_array(dia_input)\n", "print(\"InputData keys:\", list(input_arr.keys()))" ] }, { "cell_type": "markdown", "id": "42add188", "metadata": {}, "source": [ "### `numpy_to_geometry`\n", "\n", "When you already have aligned contour data as numpy arrays and want to construct a `PyGeometry`\n", "directly — for example after custom post-processing — use `numpy_to_geometry`:" ] }, { "cell_type": "code", "execution_count": 17, "id": "51236c7d", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:15:26.738504Z", "iopub.status.busy": "2026-05-04T15:15:26.738314Z", "iopub.status.idle": "2026-05-04T15:15:26.777743Z", "shell.execute_reply": "2026-05-04T15:15:26.777025Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Frames: 14, first frame lumen points: 501\n" ] } ], "source": [ "# Reconstruct a PyGeometry from numpy arrays\n", "lumen_np = rest_dia_arr[\"lumen\"]\n", "ref_np = rest_dia_arr[\"reference\"]\n", "\n", "reconstructed = mm.numpy_to_geometry(\n", " lumen_arr=lumen_np,\n", " eem_arr=np.array([]),\n", " catheter_arr=np.array([]),\n", " wall_arr=np.array([]),\n", " reference_arr=ref_np,\n", " label=\"reconstructed_rest_dia\",\n", ")\n", "print(f\"Frames: {len(reconstructed.frames)}, \"\n", " f\"first frame lumen points: {len(reconstructed.frames[0].lumen.points)}\")" ] }, { "cell_type": "markdown", "id": "9c7d9844", "metadata": {}, "source": [ "## 7. Class-level methods\n", "\n", "### `PyContour`\n", "\n", "All transformation methods return a **new** object and leave the original unchanged:" ] }, { "cell_type": "code", "execution_count": 18, "id": "193e68cc", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:15:26.779641Z", "iopub.status.busy": "2026-05-04T15:15:26.779460Z", "iopub.status.idle": "2026-05-04T15:15:26.824792Z", "shell.execute_reply": "2026-05-04T15:15:26.824154Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Area: 5.6366 mm² | Elliptic ratio: 4.5865\n", "Min diameter: 1.1578 mm | Max diameter: 5.3083 mm\n", "Frame 2 replaced: False\n" ] } ], "source": [ "contour = rest.geom_a.frames[0].lumen\n", "\n", "# Geometry queries\n", "area = contour.get_area()\n", "elliptic_ratio = contour.get_elliptic_ratio()\n", "pts_list = contour.points_as_tuples()\n", "(p1, p2), dist_close = contour.find_closest_opposite()\n", "(p1, p2), dist_far = contour.find_farthest_points()\n", "\n", "print(f\"Area: {area:.4f} mm² | Elliptic ratio: {elliptic_ratio:.4f}\")\n", "print(f\"Min diameter: {dist_close:.4f} mm | Max diameter: {dist_far:.4f} mm\")\n", "\n", "# Transformations (all return new PyContour)\n", "contour_rot = contour.rotate(20.0)\n", "contour_trsl = contour_rot.translate(0.0, 1.0, 2.0)\n", "contour_sort = contour.sort_contour_points()\n", "\n", "# Write a modified contour back into the geometry\n", "old_frame = rest.geom_a.frames[2]\n", "new_lumen = old_frame.lumen.rotate(20.0)\n", "new_frame = mm.PyFrame(\n", " id=old_frame.id,\n", " centroid=old_frame.centroid,\n", " lumen=new_lumen,\n", " extras=old_frame.extras,\n", " reference_point=old_frame.reference_point,\n", ")\n", "modified_geom = rest.geom_a.replace_frame(2, new_frame)\n", "print(\"Frame 2 replaced:\", modified_geom.frames[2].lumen is new_lumen)" ] }, { "cell_type": "markdown", "id": "f5a77ecf", "metadata": {}, "source": [ "### `PyFrame` and `PyGeometry`\n", "\n", "`PyFrame` mirrors the contour transformations and applies them to all layers:" ] }, { "cell_type": "code", "execution_count": 19, "id": "6ea47acd", "metadata": { "execution": { "iopub.execute_input": "2026-05-04T15:15:26.826781Z", "iopub.status.busy": "2026-05-04T15:15:26.826603Z", "iopub.status.idle": "2026-05-04T15:15:27.238986Z", "shell.execute_reply": "2026-05-04T15:15:27.238107Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Original contour points: 501\n", "Downsampled: 100\n", "Frame at index 5 z: 6.478\n", "Total lumen contours: 14\n", "\n", "Pair summary A: (5.559206007496853, 0.6780775439844883, 10.364606842105268)+----+----------+-----------+----------+-----------+-------+\n", "| id | area_dia | ellip_dia | area_sys | ellip_sys | z |\n", "+----+----------+-----------+----------+-----------+-------+\n", "| 0 | 5.64 | 4.59 | 6.11 | 4.35 | 0.00 |\n", "| 1 | 5.80 | 4.75 | 6.26 | 3.39 | 1.30 |\n", "\n", "Pair summary B: (6.111481670202526, 0.663038559502465, 7.7734551315789515)\n", "Deformation table shape: (14, 6)\n", "| 2 | 5.70 | 3.70 | 6.49 | 2.85 | 2.59 |\n", "| 3 | 5.56 | 2.57 | 6.85 | 2.18 | 3.89 |\n", "| 4 | 5.59 | 1.64 | 7.59 | 1.91 | 5.18 |\n", "| 5 | 6.42 | 1.51 | 7.57 | 1.68 | 6.48 |\n", "| 6 | 7.26 | 1.37 | 7.31 | 1.63 | 7.77 |\n", "| 7 | 7.74 | 1.25 | 10.23 | 1.33 | 9.07 |\n", "| 8 | 7.77 | 1.43 | 11.90 | 1.20 | 10.36 |\n", "| 9 | 9.05 | 1.33 | 14.15 | 1.09 | 11.66 |\n", "| 10 | 11.92 | 1.18 | 11.70 | 1.17 | 12.96 |\n", "| 11 | 15.27 | 1.08 | 13.57 | 1.15 | 14.25 |\n", "| 12 | 17.27 | 1.06 | 15.42 | 1.11 | 15.55 |\n", "| 13 | 16.34 | 1.04 | 18.14 | 1.07 | 16.84 |\n", "+----+----------+-----------+----------+-----------+-------+\n" ] } ], "source": [ "frame = rest.geom_a.frames[0]\n", "frame_rot = frame.rotate(20.0)\n", "frame_trsl = frame.translate(0.0, 1.0, 2.0)\n", "frame_sorted = frame.sort_frame_points()\n", "\n", "# PyGeometry geometry-level operations\n", "geom_smooth = rest.geom_a.smooth_frames()\n", "geom_ds = rest.geom_a.downsample(100)\n", "geom_sorted = rest.geom_a.sort_frame_points()\n", "geom_centred = rest.geom_a.center_to_contour(mm.PyContourType.Catheter)\n", "geom_rot = rest.geom_a.rotate(20.0)\n", "geom_trsl = geom_rot.translate(0.0, 1.0, 2.0)\n", "\n", "print(f\"Original contour points: {len(rest.geom_a.frames[0].lumen.points)}\")\n", "print(f\"Downsampled: {len(geom_ds.frames[0].lumen.points)}\")\n", "\n", "# Frame access\n", "frame_by_idx = rest.geom_a.get_frame_at_index(5)\n", "frame_by_z = rest.geom_a.get_frame_at_z(12.5)\n", "lumen_list = rest.geom_a.get_lumen_contours()\n", "wall_list = rest.geom_a.get_contours_by_type(\"Wall\")\n", "\n", "print(f\"Frame at index 5 z: {frame_by_idx.lumen.points[0].z:.3f}\")\n", "print(f\"Total lumen contours: {len(lumen_list)}\")\n", "\n", "# PyGeometryPair summary\n", "(summary_a, summary_b), deform = rest.get_summary()\n", "print(f\"\\nPair summary A: {summary_a}\")\n", "print(f\"Pair summary B: {summary_b}\")\n", "print(f\"Deformation table shape: {np.array(deform).shape}\")" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.12" } }, "nbformat": 4, "nbformat_minor": 5 }