Source code for multimodars._processing

from __future__ import annotations

from .multimodars import (
    PyContour,
    PyContourType,
    PyGeometry,
    PyGeometryPair,
    PyInputData,
    PyCenterline,
    PyDiscretizedVesselTree,  # noqa: F401 — re-exported for type annotations
    from_file_full as _from_file_full,
    from_file_doublepair as _from_file_doublepair,
    from_file_singlepair as _from_file_singlepair,
    from_file_single as _from_file_single,
    from_array_full as _from_array_full,
    from_array_doublepair as _from_array_doublepair,
    from_array_singlepair as _from_array_singlepair,
    from_array_single as _from_array_single,
    align_three_point as _align_three_point,
    align_manual as _align_manual,
    align_combined as _align_combined,
    to_obj as _to_obj,
    find_centerline_bounded_points_simple as _find_centerline_bounded_points_simple,
    find_proximal_distal_scaling as _find_proximal_distal_scaling,
    build_adjacency_map as _build_adjacency_map,
    discretize_vessel as _discretize_vessel,
)

_AlignLog = list[tuple[int, int, float, float, float, float, float]]


def _default_contour_types() -> list[PyContourType]:
    return [PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]


# ---------------------------------------------------------------------------
# Processing functions — from CSV files
# ---------------------------------------------------------------------------


[docs] def from_file_full( input_path_ab: str, input_path_cd: str, labels: list[str] | None = None, step_rotation_deg: float = 0.5, range_rotation_deg: float = 90.0, sample_size: int = 500, image_center: tuple[float, float] = (4.5, 4.5), radius: float = 0.5, n_points: int = 20, write_obj: bool = True, watertight: bool = True, contour_types: list[PyContourType] | None = None, output_path_ab: str = "output/rest", output_path_cd: str = "output/stress", output_path_ac: str = "output/diastole", output_path_bd: str = "output/systole", interpolation_steps: int = 0, bruteforce: bool = False, smooth: bool = True, postprocessing: bool = True, ) -> tuple[ PyGeometryPair, PyGeometryPair, PyGeometryPair, PyGeometryPair, tuple[_AlignLog, _AlignLog, _AlignLog, _AlignLog], ]: """Process four intravascular imaging geometries in parallel from CSV folders. Reads REST and STRESS acquisitions from two input folders, aligns frames within and between each cardiac phase in parallel, and writes interpolated OBJ meshes. .. parsed-literal:: ``output_path_ac`` (Diastole: a vs. c) ┌──────────────────────────────────────────┐ ▼ ▼ **a** REST diastole **c** STRESS diastole │ ``output_path_ab`` (Rest: a+b) │ ``output_path_cd`` (Stress: c+d) ▼ ▼ **b** REST systole **d** STRESS systole └──────────────────────────────────────────┘ ``output_path_bd`` (Systole: b vs. d) .. warning:: The CSV must have **no header**. Each row is ``(frame index, x-coord (mm), y-coord (mm), z-coord (mm))``: .. code-block:: text 185, 5.32, 2.37, 0.0 ... Parameters ---------- input_path_ab : str Path to the REST input folder (contains diastolic ``a`` and systolic ``b`` CSVs). input_path_cd : str Path to the STRESS input folder (contains diastolic ``c`` and systolic ``d`` CSVs). labels : list of str, optional Labels for the four geometries ``[rest_dia, rest_sys, stress_dia, stress_sys]``. Must be exactly 4 strings; if a different number is supplied the last component of each input path is used instead. Default is ``[]``. step_rotation_deg : float, optional Rotation step in degrees. Default is ``0.5``. range_rotation_deg : float, optional Rotation search range (±) in degrees; a range of 90° gives 180° total. Default is ``90.0``. sample_size : int, optional Number of points to downsample to during alignment. Default is ``500``. image_center : tuple of float, optional Image center ``(x, y)`` in mm. Default is ``(4.5, 4.5)``. radius : float, optional Catheter radius in mm. Default is ``0.5``. n_points : int, optional Number of catheter points; more points increases the influence of the image center. Default is ``20``. write_obj : bool, optional Whether to write OBJ files to disk. Default is ``True``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. output_path_ab : str, optional Output directory for REST results (pair a+b). Default is ``"output/rest"``. output_path_cd : str, optional Output directory for STRESS results (pair c+d). Default is ``"output/stress"``. output_path_ac : str, optional Output directory for DIASTOLE results (pair a+c). Default is ``"output/diastole"``. output_path_bd : str, optional Output directory for SYSTOLE results (pair b+d). Default is ``"output/systole"``. interpolation_steps : int, optional Number of interpolated meshes between phases. Default is ``28``. bruteforce : bool, optional Whether to use brute-force alignment (one comparison per step). Default is ``False``. smooth : bool, optional Whether to smooth frames after alignment using a 3-point moving average. Default is ``True``. postprocessing : bool, optional Whether to adjust spacing within/between geometries to equal offsets. Default is ``True``. Returns ------- rest : PyGeometryPair Aligned geometry pair for the REST condition. stress : PyGeometryPair Aligned geometry pair for the STRESS condition. diastole : PyGeometryPair Aligned geometry pair for the diastolic phase. systole : PyGeometryPair Aligned geometry pair for the systolic phase. logs : tuple of list 4-tuple of alignment logs ``(logs_a, logs_b, logs_c, logs_d)``; each entry is a list of ``(id, matched_to, rel_rot_deg, total_rot_deg, tx, ty, centroid_x, centroid_y)``. Examples -------- >>> import multimodars as mm >>> rest, stress, dia, sys, _ = mm.from_file_full( ... "data/ivus_rest", "data/ivus_stress" ... ) """ if contour_types is None: contour_types = _default_contour_types() return _from_file_full( input_path_ab, input_path_cd, labels or [], step_rotation_deg, range_rotation_deg, sample_size, image_center, radius, n_points, write_obj, watertight, contour_types, output_path_ab, output_path_cd, output_path_ac, output_path_bd, interpolation_steps, bruteforce, smooth, postprocessing, )
[docs] def from_file_doublepair( input_path_ab: str, input_path_cd: str, labels: list[str] | None = None, step_rotation_deg: float = 0.5, range_rotation_deg: float = 90.0, sample_size: int = 500, image_center: tuple[float, float] = (4.5, 4.5), radius: float = 0.5, n_points: int = 20, write_obj: bool = True, watertight: bool = True, contour_types: list[PyContourType] | None = None, output_path_ab: str = "output/rest", output_path_cd: str = "output/stress", interpolation_steps: int = 0, bruteforce: bool = False, smooth: bool = True, postprocessing: bool = True, ) -> tuple[ PyGeometryPair, PyGeometryPair, tuple[_AlignLog, _AlignLog, _AlignLog, _AlignLog], ]: """Process two diastole/systole pairs in parallel from CSV folders. Reads REST and STRESS acquisitions independently, aligns frames within each pair, and writes interpolated OBJ meshes. .. parsed-literal:: **a** REST diastole **c** STRESS diastole │ ``output_path_ab`` (Rest: a+b) │ ``output_path_cd`` (Stress: c+d) ▼ ▼ **b** REST systole **d** STRESS systole .. warning:: The CSV must have **no header**. Each row is ``(frame index, x-coord (mm), y-coord (mm), z-coord (mm))``: .. code-block:: text 185, 5.32, 2.37, 0.0 ... Parameters ---------- input_path_ab : str Path to the REST input folder (contains diastolic ``a`` and systolic ``b`` CSVs). input_path_cd : str Path to the STRESS input folder (contains diastolic ``c`` and systolic ``d`` CSVs). labels : list of str, optional Labels for the four geometries ``[rest_dia, rest_sys, stress_dia, stress_sys]``. Must be exactly 4 strings; if a different number is supplied the last component of each input path is used instead. Default is ``[]``. step_rotation_deg : float, optional Rotation step in degrees. Default is ``0.5``. range_rotation_deg : float, optional Rotation search range (±) in degrees. Default is ``90.0``. sample_size : int, optional Number of points to downsample to. Default is ``500``. image_center : tuple of float, optional Image center ``(x, y)`` in mm. Default is ``(4.5, 4.5)``. radius : float, optional Catheter radius in mm. Default is ``0.5``. n_points : int, optional Number of catheter points. Default is ``20``. write_obj : bool, optional Whether to write OBJ files. Default is ``True``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. output_path_ab : str, optional Output directory for REST results (pair a+b). Default is ``"output/rest"``. output_path_cd : str, optional Output directory for STRESS results (pair c+d). Default is ``"output/stress"``. interpolation_steps : int, optional Number of interpolated meshes between phases. Default is ``28``. bruteforce : bool, optional Whether to use brute-force alignment. Default is ``False``. smooth : bool, optional Whether to smooth frames after alignment. Default is ``True``. postprocessing : bool, optional Whether to equalise spacing within/between geometries. Default is ``True``. Returns ------- rest : PyGeometryPair Aligned geometry pair for the REST condition. stress : PyGeometryPair Aligned geometry pair for the STRESS condition. logs : tuple of list 4-tuple of alignment logs ``(logs_a, logs_b, logs_c, logs_d)``; each entry is a list of ``(id, matched_to, rel_rot_deg, total_rot_deg, tx, ty, centroid_x, centroid_y)``. Examples -------- >>> import multimodars as mm >>> rest, stress, _ = mm.from_file_doublepair( ... "data/ivus_rest", "data/ivus_stress" ... ) """ if contour_types is None: contour_types = _default_contour_types() return _from_file_doublepair( input_path_ab, input_path_cd, labels or [], step_rotation_deg, range_rotation_deg, sample_size, image_center, radius, n_points, write_obj, watertight, contour_types, output_path_ab, output_path_cd, interpolation_steps, bruteforce, smooth, postprocessing, )
[docs] def from_file_singlepair( input_path: str, labels: list[str] | None = None, step_rotation_deg: float = 0.5, range_rotation_deg: float = 90.0, sample_size: int = 500, image_center: tuple[float, float] = (4.5, 4.5), radius: float = 0.5, n_points: int = 20, write_obj: bool = True, watertight: bool = True, contour_types: list[PyContourType] | None = None, output_path: str = "output/singlepair", interpolation_steps: int = 0, bruteforce: bool = False, smooth: bool = True, postprocessing: bool = True, ) -> tuple[PyGeometryPair, tuple[_AlignLog, _AlignLog]]: """Process a single diastole/systole pair from an input CSV folder. Reads one acquisition folder, aligns the diastolic and systolic frames, and returns a single ``PyGeometryPair``. .. code-block:: text Pipeline: diastole systole .. warning:: The CSV must have **no header**. Each row is ``(frame index, x-coord (mm), y-coord (mm), z-coord (mm))``: .. code-block:: text 185, 5.32, 2.37, 0.0 ... Parameters ---------- input_path : str Path to the input CSV folder. labels : list of str, optional Labels for the two geometries ``[diastole, systole]``. Must be exactly 2 strings; if a different number is supplied the last component of the input path is used instead. Default is ``[]``. step_rotation_deg : float, optional Rotation step in degrees. Default is ``0.5``. range_rotation_deg : float, optional Rotation search range (±) in degrees. Default is ``90.0``. sample_size : int, optional Number of points to downsample to. Default is ``500``. image_center : tuple of float, optional Image center ``(x, y)`` in mm. Default is ``(4.5, 4.5)``. radius : float, optional Catheter radius in mm. Default is ``0.5``. n_points : int, optional Number of catheter points. Default is ``20``. write_obj : bool, optional Whether to write OBJ files. Default is ``True``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. output_path : str, optional Directory path to write the processed geometry. Default is ``"output/singlepair"``. interpolation_steps : int, optional Number of interpolated meshes. Default is ``28``. bruteforce : bool, optional Whether to use brute-force alignment. Default is ``False``. smooth : bool, optional Whether to smooth frames after alignment. Default is ``True``. postprocessing : bool, optional Whether to equalise spacing within/between geometries. Default is ``True``. Returns ------- pair : PyGeometryPair Aligned diastole/systole geometry pair. logs : tuple of list 2-tuple of alignment logs ``(logs_a, logs_b)``; each entry is a list of ``(id, matched_to, rel_rot_deg, total_rot_deg, tx, ty, centroid_x, centroid_y)``. Examples -------- >>> import multimodars as mm >>> pair, _ = mm.from_file_singlepair("data/ivus_rest") """ if contour_types is None: contour_types = _default_contour_types() return _from_file_singlepair( input_path, labels or [], step_rotation_deg, range_rotation_deg, sample_size, image_center, radius, n_points, write_obj, watertight, contour_types, output_path, interpolation_steps, bruteforce, smooth, postprocessing, )
[docs] def from_file_single( input_path: str, labels: list[str] | None = None, diastole: bool = True, step_rotation_deg: float = 0.5, range_rotation_deg: float = 90.0, sample_size: int = 500, image_center: tuple[float, float] = (4.5, 4.5), radius: float = 0.5, n_points: int = 20, write_obj: bool = True, watertight: bool = True, contour_types: list[PyContourType] | None = None, output_path: str = "output/single", bruteforce: bool = False, smooth: bool = True, ) -> tuple[PyGeometry, _AlignLog]: """Process a single intravascular imaging geometry from a CSV file. Reads one phase (diastole or systole) from an IVUS CSV file, aligns frames within the geometry, and optionally writes OBJ output. .. warning:: The CSV must have **no header**. Each row is ``(frame index, x-coord (mm), y-coord (mm), z-coord (mm))``. Parameters ---------- input_path : str Path to the input CSV folder. labels : list of str, optional Label for the geometry (1 string). If a different number is supplied the last component of the input path is used instead. Default is ``[]``. diastole : bool, optional When ``True`` process the diastolic phase; otherwise systole. Default is ``True``. step_rotation_deg : float, optional Rotation step in degrees. Default is ``0.5``. range_rotation_deg : float, optional Rotation search range (±) in degrees. Default is ``90.0``. sample_size : int, optional Number of points to downsample to. Default is ``500``. image_center : tuple of float, optional Image center ``(x, y)`` in mm. Default is ``(4.5, 4.5)``. radius : float, optional Catheter radius in mm. Default is ``0.5``. n_points : int, optional Number of catheter points. Default is ``20``. write_obj : bool, optional Whether to write OBJ files. Default is ``True``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. output_path : str, optional Directory path to write the processed geometry. Default is ``"output/single"``. bruteforce : bool, optional Whether to use brute-force alignment. Default is ``False``. smooth : bool, optional Whether to smooth frames after alignment. Default is ``True``. Returns ------- geom : PyGeometry Processed geometry for the chosen phase. logs : list Alignment log entries; each entry is ``(id, matched_to, rel_rot_deg, total_rot_deg, tx, ty, centroid_x, centroid_y)``. Examples -------- >>> import multimodars as mm >>> geom, _ = mm.from_file_single("data/ivus.csv", diastole=False) """ if contour_types is None: contour_types = _default_contour_types() return _from_file_single( input_path, labels or [], diastole, step_rotation_deg, range_rotation_deg, sample_size, image_center, radius, n_points, write_obj, watertight, contour_types, output_path, bruteforce, smooth, )
# --------------------------------------------------------------------------- # Processing functions — from PyInputData arrays # ---------------------------------------------------------------------------
[docs] def from_array_full( input_data_a: PyInputData, input_data_b: PyInputData, input_data_c: PyInputData, input_data_d: PyInputData, step_rotation_deg: float = 0.5, range_rotation_deg: float = 90.0, sample_size: int = 500, image_center: tuple[float, float] = (4.5, 4.5), radius: float = 0.5, n_points: int = 20, write_obj: bool = True, watertight: bool = True, contour_types: list[PyContourType] | None = None, output_path_ab: str = "output/rest", output_path_cd: str = "output/stress", output_path_ac: str = "output/diastole", output_path_bd: str = "output/systole", interpolation_steps: int = 0, bruteforce: bool = False, smooth: bool = True, postprocessing: bool = True, ) -> tuple[ PyGeometryPair, PyGeometryPair, PyGeometryPair, PyGeometryPair, tuple[_AlignLog, _AlignLog, _AlignLog, _AlignLog], ]: """Process four ``PyInputData`` objects in parallel, aligning and interpolating between phases. Accepts pre-loaded input data for REST diastole, REST systole, STRESS diastole, and STRESS systole, then aligns frames within and between each cardiac phase. .. parsed-literal:: ``output_path_ac`` (Diastole: a vs. c) ┌──────────────────────────────────────────┐ ▼ ▼ **a** REST diastole **c** STRESS diastole │ ``output_path_ab`` (Rest: a+b) │ ``output_path_cd`` (Stress: c+d) ▼ ▼ **b** REST systole **d** STRESS systole └──────────────────────────────────────────┘ ``output_path_bd`` (Systole: b vs. d) Parameters ---------- input_data_a : PyInputData Diastolic REST input data. input_data_b : PyInputData Systolic REST input data. input_data_c : PyInputData Diastolic STRESS input data. input_data_d : PyInputData Systolic STRESS input data. step_rotation_deg : float, optional Rotation step in degrees. Default is ``0.5``. range_rotation_deg : float, optional Rotation search range (±) in degrees. Default is ``90.0``. sample_size : int, optional Number of points to downsample to. Default is ``500``. image_center : tuple of float, optional Image center ``(x, y)`` in mm. Default is ``(4.5, 4.5)``. radius : float, optional Catheter radius in mm. Default is ``0.5``. n_points : int, optional Number of catheter points. Default is ``20``. write_obj : bool, optional Whether to write OBJ files. Default is ``True``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. output_path_ab : str, optional Output directory for REST results (pair a+b). Default is ``"output/rest"``. output_path_cd : str, optional Output directory for STRESS results (pair c+d). Default is ``"output/stress"``. output_path_ac : str, optional Output directory for DIASTOLE results (pair a+c). Default is ``"output/diastole"``. output_path_bd : str, optional Output directory for SYSTOLE results (pair b+d). Default is ``"output/systole"``. interpolation_steps : int, optional Number of interpolation steps between phases. Default is ``28``. bruteforce : bool, optional Whether to use brute-force alignment. Default is ``False``. smooth : bool, optional Whether to smooth frames after alignment. Default is ``True``. postprocessing : bool, optional Whether to equalise spacing within/between geometries. Default is ``True``. Returns ------- rest : PyGeometryPair Aligned geometry pair for the REST condition. stress : PyGeometryPair Aligned geometry pair for the STRESS condition. diastole : PyGeometryPair Aligned geometry pair for the diastolic phase. systole : PyGeometryPair Aligned geometry pair for the systolic phase. logs : tuple of list 4-tuple of alignment logs ``(logs_a, logs_b, logs_c, logs_d)``; each entry is a list of ``(id, matched_to, rel_rot_deg, total_rot_deg, tx, ty, centroid_x, centroid_y)``. Examples -------- >>> import multimodars as mm >>> rest, stress, dia, sys, _ = mm.from_array_full( ... rest_dia, rest_sys, stress_dia, stress_sys ... ) """ if contour_types is None: contour_types = _default_contour_types() return _from_array_full( input_data_a, input_data_b, input_data_c, input_data_d, step_rotation_deg, range_rotation_deg, sample_size, image_center, radius, n_points, write_obj, watertight, contour_types, output_path_ab, output_path_cd, output_path_ac, output_path_bd, interpolation_steps, bruteforce, smooth, postprocessing, )
[docs] def from_array_doublepair( input_data_a: PyInputData, input_data_b: PyInputData, input_data_c: PyInputData, input_data_d: PyInputData, step_rotation_deg: float = 0.5, range_rotation_deg: float = 90.0, sample_size: int = 500, image_center: tuple[float, float] = (4.5, 4.5), radius: float = 0.5, n_points: int = 20, write_obj: bool = True, watertight: bool = True, contour_types: list[PyContourType] | None = None, output_path_ab: str = "output/rest", output_path_cd: str = "output/stress", interpolation_steps: int = 0, bruteforce: bool = False, smooth: bool = True, postprocessing: bool = True, ) -> tuple[ PyGeometryPair, PyGeometryPair, tuple[_AlignLog, _AlignLog, _AlignLog, _AlignLog], ]: """Process two ``PyInputData`` pairs in parallel, aligning frames within each pair independently. Accepts pre-loaded data for REST (diastole + systole) and STRESS (diastole + systole), aligns each pair independently, and writes interpolated OBJ meshes. .. parsed-literal:: **a** REST diastole **c** STRESS diastole │ ``output_path_ab`` (Rest: a+b) │ ``output_path_cd`` (Stress: c+d) ▼ ▼ **b** REST systole **d** STRESS systole Parameters ---------- input_data_a : PyInputData Diastolic REST input data. input_data_b : PyInputData Systolic REST input data. input_data_c : PyInputData Diastolic STRESS input data. input_data_d : PyInputData Systolic STRESS input data. step_rotation_deg : float, optional Rotation step in degrees. Default is ``0.5``. range_rotation_deg : float, optional Rotation search range (±) in degrees. Default is ``90.0``. sample_size : int, optional Number of points to downsample to. Default is ``500``. image_center : tuple of float, optional Image center ``(x, y)`` in mm. Default is ``(4.5, 4.5)``. radius : float, optional Catheter radius in mm. Default is ``0.5``. n_points : int, optional Number of catheter points. Default is ``20``. write_obj : bool, optional Whether to write OBJ files. Default is ``True``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. output_path_ab : str, optional Output directory for REST results (pair a+b). Default is ``"output/rest"``. output_path_cd : str, optional Output directory for STRESS results (pair c+d). Default is ``"output/stress"``. interpolation_steps : int, optional Number of interpolation steps between phases. Default is ``28``. bruteforce : bool, optional Whether to use brute-force alignment. Default is ``False``. smooth : bool, optional Whether to smooth frames after alignment. Default is ``True``. postprocessing : bool, optional Whether to equalise spacing within/between geometries. Default is ``True``. Returns ------- rest : PyGeometryPair Aligned geometry pair for the REST condition. stress : PyGeometryPair Aligned geometry pair for the STRESS condition. logs : tuple of list 4-tuple of alignment logs ``(logs_a, logs_b, logs_c, logs_d)``; each entry is a list of ``(id, matched_to, rel_rot_deg, total_rot_deg, tx, ty, centroid_x, centroid_y)``. Examples -------- >>> import multimodars as mm >>> rest, stress, _ = mm.from_array_doublepair( ... rest_dia, rest_sys, stress_dia, stress_sys ... ) """ if contour_types is None: contour_types = _default_contour_types() return _from_array_doublepair( input_data_a, input_data_b, input_data_c, input_data_d, step_rotation_deg, range_rotation_deg, sample_size, image_center, radius, n_points, write_obj, watertight, contour_types, output_path_ab, output_path_cd, interpolation_steps, bruteforce, smooth, postprocessing, )
[docs] def from_array_singlepair( input_data_a: PyInputData, input_data_b: PyInputData, step_rotation_deg: float = 0.5, range_rotation_deg: float = 90.0, sample_size: int = 500, image_center: tuple[float, float] = (4.5, 4.5), radius: float = 0.5, n_points: int = 20, write_obj: bool = True, watertight: bool = True, contour_types: list[PyContourType] | None = None, output_path: str = "output/singlepair", interpolation_steps: int = 0, bruteforce: bool = False, smooth: bool = True, postprocessing: bool = True, ) -> tuple[PyGeometryPair, tuple[_AlignLog, _AlignLog]]: """Align and interpolate between two ``PyInputData`` objects (diastole and systole). Accepts pre-loaded diastolic and systolic input data, aligns frames between the two phases, and returns a single ``PyGeometryPair``. .. code-block:: text diastole ──▶ systole Parameters ---------- input_data_a : PyInputData Diastolic input data. input_data_b : PyInputData Systolic input data. step_rotation_deg : float, optional Rotation step in degrees. Default is ``0.5``. range_rotation_deg : float, optional Rotation search range (±) in degrees. Default is ``90.0``. sample_size : int, optional Number of points to downsample to. Default is ``500``. image_center : tuple of float, optional Image center ``(x, y)`` in mm. Default is ``(4.5, 4.5)``. radius : float, optional Catheter radius in mm. Default is ``0.5``. n_points : int, optional Number of catheter points. Default is ``20``. write_obj : bool, optional Whether to write OBJ files. Default is ``True``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. output_path : str, optional Directory path to write interpolated output files. Default is ``"output/singlepair"``. interpolation_steps : int, optional Number of interpolation steps between phases. Default is ``28``. bruteforce : bool, optional Whether to use brute-force alignment. Default is ``False``. smooth : bool, optional Whether to smooth frames after alignment. Default is ``True``. postprocessing : bool, optional Whether to equalise spacing within/between geometries. Default is ``True``. Returns ------- pair : PyGeometryPair Aligned diastole/systole geometry pair. logs : tuple of list 2-tuple of alignment logs ``(logs_a, logs_b)``; each entry is a list of ``(id, matched_to, rel_rot_deg, total_rot_deg, tx, ty, centroid_x, centroid_y)``. Examples -------- >>> import multimodars as mm >>> pair, _ = mm.from_array_singlepair(rest_dia, rest_sys) """ if contour_types is None: contour_types = _default_contour_types() return _from_array_singlepair( input_data_a, input_data_b, step_rotation_deg, range_rotation_deg, sample_size, image_center, radius, n_points, write_obj, watertight, contour_types, output_path, interpolation_steps, bruteforce, smooth, postprocessing, )
[docs] def from_array_single( input_data: PyInputData, step_rotation_deg: float = 0.5, range_rotation_deg: float = 90.0, sample_size: int = 500, image_center: tuple[float, float] = (4.5, 4.5), radius: float = 0.5, n_points: int = 20, write_obj: bool = False, watertight: bool = True, contour_types: list[PyContourType] | None = None, output_path: str = "output/single", bruteforce: bool = False, smooth: bool = True, ) -> tuple[PyGeometry, _AlignLog]: """Process a single geometry phase from a ``PyInputData`` object. Accepts pre-loaded input data for one cardiac phase, aligns frames within the geometry, and optionally writes OBJ output. Parameters ---------- input_data : PyInputData Input data for a single cardiac phase (e.g. diastolic REST). step_rotation_deg : float, optional Rotation step in degrees. Default is ``0.5``. range_rotation_deg : float, optional Rotation search range (±) in degrees. Default is ``90.0``. sample_size : int, optional Number of points to downsample to. Default is ``500``. image_center : tuple of float, optional Image center ``(x, y)`` in mm. Default is ``(4.5, 4.5)``. radius : float, optional Catheter radius in mm. Default is ``0.5``. n_points : int, optional Number of catheter points. Default is ``20``. write_obj : bool, optional Whether to write OBJ files. Default is ``False``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. output_path : str, optional Directory path to write the processed geometry. Default is ``"output/single"``. bruteforce : bool, optional Whether to use brute-force alignment. Default is ``False``. smooth : bool, optional Whether to smooth frames after alignment. Default is ``True``. Returns ------- geom : PyGeometry Processed geometry for the chosen phase. logs : list Alignment log entries; each entry is ``(id, matched_to, rel_rot_deg, total_rot_deg, tx, ty, centroid_x, centroid_y)``. Examples -------- >>> import multimodars as mm >>> geom, _ = mm.from_array_single(input_data) """ if contour_types is None: contour_types = _default_contour_types() return _from_array_single( input_data, step_rotation_deg, range_rotation_deg, sample_size, image_center, radius, n_points, write_obj, watertight, contour_types, output_path, bruteforce, smooth, )
# --------------------------------------------------------------------------- # Alignment functions # ---------------------------------------------------------------------------
[docs] def align_three_point( centerline: PyCenterline, geometry: PyGeometryPair | PyGeometry, main_ref_pt: tuple[float, float, float], counterclockwise_ref_pt: tuple[float, float, float], clockwise_ref_pt: tuple[float, float, float], angle_step_deg: float = 1.0, write: bool = False, watertight: bool = True, interpolation_steps: int = 0, output_dir: str = "output/aligned", contour_types: list[PyContourType] | None = None, case_name: str = "None", align_wall_anomalous: bool = False, ) -> tuple[PyGeometryPair | PyGeometry, PyCenterline]: """Align a geometry (or geometry pair) to the centerline using three reference points. Creates centerline-aligned meshes based on three anatomical reference points (main ostium, counterclockwise side, clockwise side). Only works for elliptic vessels such as coronary artery anomalies. Parameters ---------- centerline : PyCenterline Centerline of the vessel. geometry : PyGeometryPair or PyGeometry Single geometry or diastolic/systolic geometry pair to align. main_ref_pt : tuple of float ``(x, y, z)`` reference point at the aortic ostium. counterclockwise_ref_pt : tuple of float ``(x, y, z)`` counterclockwise reference point (viewed proximal → distal). clockwise_ref_pt : tuple of float ``(x, y, z)`` clockwise reference point (viewed proximal → distal). angle_step_deg : float, optional Step size in degrees for the rotation search. Default is ``1.0``. write : bool, optional Whether to write the aligned meshes to OBJ files. Default is ``False``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. interpolation_steps : int, optional Number of interpolation steps between phases. Only used when *geometry* is a ``PyGeometryPair``. Default is ``0``. output_dir : str, optional Output directory for aligned meshes. Default is ``"output/aligned"``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. case_name : str, optional Case name used as a filename prefix. Default is ``"None"``. align_wall_anomalous : bool, optional When ``True``, rotate the Wall contour in every frame (from frame 2 onward) so its aortic straight portion aligns to the plane defined by frames 0 and 1. Only meaningful for anomalous vessels. Default is ``False``. Returns ------- geometry : PyGeometryPair or PyGeometry Aligned geometry, matching the type of the input. centerline : PyCenterline Resampled centerline. Examples -------- >>> import multimodars as mm >>> result, cl = mm.align_three_point( ... centerline, ... geometry_pair, ... (12.2605, -201.3643, 1751.0554), ... (11.7567, -202.1920, 1754.7975), ... (15.6605, -202.1920, 1749.9655), ... ) """ if contour_types is None: contour_types = _default_contour_types() return _align_three_point( centerline, geometry, main_ref_pt, counterclockwise_ref_pt, clockwise_ref_pt, angle_step_deg, write, watertight, interpolation_steps, output_dir, contour_types, case_name, align_wall_anomalous, )
[docs] def align_manual( centerline: PyCenterline, geometry: PyGeometryPair | PyGeometry, rotation_angle: float, ref_point: tuple[float, float, float], write: bool = False, watertight: bool = True, interpolation_steps: int = 0, output_dir: str = "output/aligned", contour_types: list[PyContourType] | None = None, case_name: str = "None", align_wall_anomalous: bool = False, ) -> tuple[PyGeometryPair | PyGeometry, PyCenterline]: """Align a geometry (or geometry pair) to the centerline using a manual rotation angle. Creates centerline-aligned meshes using an explicit rotation angle and a single reference point on the centerline. Only works for elliptic vessels such as coronary artery anomalies. Parameters ---------- centerline : PyCenterline Centerline of the vessel. geometry : PyGeometryPair or PyGeometry Single geometry or diastolic/systolic geometry pair to align. rotation_angle : float Rotation angle in radians to apply. ref_point : tuple of float ``(x, y, z)`` reference point on the centerline. write : bool, optional Whether to write the aligned meshes to OBJ files. Default is ``False``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. interpolation_steps : int, optional Number of interpolation steps between phases. Only used when *geometry* is a ``PyGeometryPair``. Default is ``0``. output_dir : str, optional Output directory for aligned meshes. Default is ``"output/aligned"``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. case_name : str, optional Case name used as a filename prefix. Default is ``"None"``. align_wall_anomalous : bool, optional When ``True``, rotate the Wall contour in every frame (from frame 2 onward) so its aortic straight portion aligns to the plane defined by frames 0 and 1. Only meaningful for anomalous vessels. Default is ``False``. Returns ------- geometry : PyGeometryPair or PyGeometry Aligned geometry, matching the type of the input. centerline : PyCenterline Resampled centerline. Examples -------- >>> import multimodars as mm >>> result, cl = mm.align_manual( ... centerline, geometry_pair, rotation_angle=1.57, ref_point=(1.0, 2.0, 3.0) ... ) """ if contour_types is None: contour_types = _default_contour_types() return _align_manual( centerline, geometry, rotation_angle, ref_point, write, watertight, interpolation_steps, output_dir, contour_types, case_name, align_wall_anomalous, )
[docs] def align_combined( centerline: PyCenterline, geometry: PyGeometryPair | PyGeometry, main_ref_pt: tuple[float, float, float], counterclockwise_ref_pt: tuple[float, float, float], clockwise_ref_pt: tuple[float, float, float], points: list[tuple[float, float, float]], angle_step_deg: float = 1.0, angle_range_deg: float = 15.0, index_range: int = 2, write: bool = False, watertight: bool = True, interpolation_steps: int = 0, output_dir: str = "output/aligned", contour_types: list[PyContourType] | None = None, case_name: str = "None", align_wall_anomalous: bool = False, ) -> tuple[PyGeometryPair | PyGeometry, PyCenterline]: """Align a geometry (or geometry pair) using three reference points and Hausdorff refinement. Creates centerline-aligned meshes using three anatomical reference points for an initial orientation and a set of additional points for Hausdorff distance-based fine-tuning of the rotation. Parameters ---------- centerline : PyCenterline Centerline of the vessel. geometry : PyGeometryPair or PyGeometry Single geometry or diastolic/systolic geometry pair to align. main_ref_pt : tuple of float ``(x, y, z)`` reference point at the aortic ostium. counterclockwise_ref_pt : tuple of float ``(x, y, z)`` counterclockwise reference point (viewed proximal → distal). clockwise_ref_pt : tuple of float ``(x, y, z)`` clockwise reference point (viewed proximal → distal). points : list of tuple of float Point cloud used for Hausdorff distance calculation during rotation refinement. angle_step_deg : float, optional Step size in degrees for the rotation search. Default is ``1.0``. angle_range_deg : float, optional Total rotation search range in degrees. Default is ``15.0``. index_range : int, optional Number of centerline indices considered around the reference. Default is ``2``. write : bool, optional Whether to write the aligned meshes to OBJ files. Default is ``False``. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. interpolation_steps : int, optional Number of interpolation steps between phases. Only used when *geometry* is a ``PyGeometryPair``. Default is ``0``. output_dir : str, optional Output directory for aligned meshes. Default is ``"output/aligned"``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. case_name : str, optional Case name used as a filename prefix. Default is ``"None"``. align_wall_anomalous : bool, optional When ``True``, rotate the Wall contour in every frame (from frame 2 onward) so its aortic straight portion aligns to the plane defined by frames 0 and 1. Only meaningful for anomalous vessels. Default is ``False``. Returns ------- geometry : PyGeometryPair or PyGeometry Aligned geometry, matching the type of the input. centerline : PyCenterline Resampled centerline. Examples -------- >>> import multimodars as mm >>> result, cl = mm.align_combined( ... centerline, ... geometry_pair, ... (12.2605, -201.3643, 1751.0554), ... (11.7567, -202.1920, 1754.7975), ... (15.6605, -202.1920, 1749.9655), ... point_cloud, ... ) """ if contour_types is None: contour_types = _default_contour_types() return _align_combined( centerline, geometry, main_ref_pt, counterclockwise_ref_pt, clockwise_ref_pt, points, angle_step_deg, angle_range_deg, index_range, write, watertight, interpolation_steps, output_dir, contour_types, case_name, align_wall_anomalous, )
# --------------------------------------------------------------------------- # OBJ export # ---------------------------------------------------------------------------
[docs] def to_obj( geometry: PyGeometry, output_path: str, watertight: bool = True, contour_types: list[PyContourType] | None = None, filename_prefix: str = "", ) -> None: """Convert a ``PyGeometry`` object into OBJ files and write them to disk. Writes the specified contour types as OBJ meshes without UV coordinates. Each contour type is written to its own file together with a corresponding MTL material file. Parameters ---------- geometry : PyGeometry Input geometry instance containing the mesh data. output_path : str Directory path where the OBJ and MTL files will be written. watertight : bool, optional Whether to write a watertight or shell mesh. Default is ``True``. contour_types : list of PyContourType, optional Contour types to export. Default is ``[PyContourType.Lumen, PyContourType.Catheter, PyContourType.Wall]``. filename_prefix : str, optional Optional prefix prepended to all output filenames. Default is ``""``. Raises ------ RuntimeError If any of the underlying file writes fail. Examples -------- >>> import multimodars as mm >>> mm.to_obj(geometry, "output/meshes", watertight=True) """ if contour_types is None: contour_types = _default_contour_types() return _to_obj(geometry, output_path, watertight, contour_types, filename_prefix)
# --------------------------------------------------------------------------- # IVUS/CCTA Fusion # ---------------------------------------------------------------------------
[docs] def find_centerline_bounded_points_simple( centerline: PyCenterline, points: list[tuple[float, float, float]], radius: float, ) -> list[tuple[float, float, float]]: """Find points bounded by spheres along a coronary vessel centerline. This version accepts and returns simple Python lists of tuples. Parameters ---------- centerline : PyCenterline Centerline of the vessel. points : list of tuple of float List of ``(x, y, z)`` point coordinates. radius : float Radius of the bounding spheres around each centerline point. Returns ------- bounded_points : list of tuple of float Filtered points that are inside the bounding spheres. Examples -------- >>> import multimodars as mm >>> >>> # Load centerline and point cloud >>> centerline = mm.load_centerline("path/to/centerline.json") >>> points = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), ...] # or mesh.vertices.tolist() >>> >>> # Find points bounded by centerline spheres >>> bounded_points = mm.find_centerline_bounded_points(centerline, points, 2.0) >>> print(f"Found {len(bounded_points)} points inside vessel bounds")""" return _find_centerline_bounded_points_simple(centerline, points, radius)
[docs] def find_proximal_distal_scaling( anomalous_points: list[tuple[float, float, float]], n_proximal: int, n_distal: int, centerline: PyCenterline, proximal_reference: list[tuple[float, float, float]], distal_reference: list[tuple[float, float, float]], ): """Find the optimal diameter scaling for the proximal and distal regions. Parameters ---------- anomalous_points : list of tuple of float ``(x, y, z)`` coordinates of the anomalous vessel region. n_proximal : int Number of proximal points used for comparison. n_distal : int Number of distal points used for comparison. centerline : PyCenterline Centerline of the vessel region. proximal_reference : list of tuple of float Reference ``(x, y, z)`` points from the CCTA mesh for the proximal region. distal_reference : list of tuple of float Reference ``(x, y, z)`` points for the distal region. Returns ------- proximal_scaling : float Optimal scaling distance for the proximal region. distal_scaling : float Optimal scaling distance for the distal region. Examples -------- >>> import multimodars as mm""" return _find_proximal_distal_scaling( anomalous_points, n_proximal, n_distal, centerline, proximal_reference, distal_reference, )
[docs] def build_adjacency_map( faces: list[list[int]], ) -> dict[int, set[int]]: """Build a vertex adjacency map from a triangle mesh face list. For each triangle face, all three undirected edges are recorded so that every vertex maps to the set of vertices it shares an edge with. Parameters ---------- faces : list of list of int Triangle faces, each represented as a three-element array of vertex indices ``[v0, v1, v2]``. Returns ------- adjacency_map : dict of int to set of int Mapping from each vertex index to the set of its directly connected neighbour vertex indices. Examples -------- >>> import multimodars as mm >>> >>> faces = [[0, 1, 2], [1, 2, 3]] >>> adj = mm.build_adjacency_map(faces) >>> print(adj[1]) # {0, 2, 3}""" return _build_adjacency_map( faces, )
[docs] def discretize_vessel( centerline: "PyCenterline", points: list[tuple[float, float, float]], branch_id: int = 0, step_size: float = 0.5, n_points: int = 200, ) -> list[PyContour]: """Discretize a vessel surface mesh along a centerline branch into uniform cross-sections. Walks the specified centerline branch at uniform arc-length intervals of ``step_size``, projects the supplied mesh points onto each perpendicular cross-sectional plane, discards incomplete slices (empty or not covering all four angular quadrants), and resamples the remaining contours to exactly ``n_points`` evenly-spaced points via a closed Catmull-Rom spline. Parameters ---------- centerline : PyCenterline Centerline object containing one or more branches. points : list of tuple of (float, float, float) 3-D surface mesh points ``(x, y, z)`` to project onto each cross-section. branch_id : int, optional Index of the centerline branch to walk. Default is ``0``. step_size : float, optional Arc-length distance between consecutive cross-sections in the same units as ``centerline`` and ``points``. Default is ``0.5``. n_points : int, optional Number of evenly-spaced points per output contour. Default is ``200``. Returns ------- contours : list of PyContour One contour per surviving cross-section, each containing exactly ``n_points`` uniformly distributed points lying on a Catmull-Rom spline fit to the projected surface points. Examples -------- >>> import multimodars as mm >>> >>> contours = mm.discretize_vessel(centerline, mesh_points, branch_id=0, step_size=0.5) >>> print(len(contours))""" return _discretize_vessel( centerline, points, branch_id, step_size, n_points, )