From 6efd73e68daa9236979d1d4e512e2a150474ac33 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Mon, 24 Feb 2025 21:33:50 +0100 Subject: [PATCH 01/25] Add script for meshing --- .github/workflows/build_docs.yml | 4 +- .pre-commit-config.yaml | 1 + examples/synthseg.ipynb | 54 +++--- pyproject.toml | 11 +- src/mri2mesh/__init__.py | 12 +- src/mri2mesh/cli.py | 8 +- src/mri2mesh/mesh/__init__.py | 52 ++++++ src/mri2mesh/mesh/basic.py | 235 ++++++++++++++++++++++++ src/mri2mesh/mesh/idealized_brain.py | 224 ++++++++++++++++++++++ src/mri2mesh/morphology.py | 4 +- src/mri2mesh/reader.py | 4 +- src/mri2mesh/surface/__init__.py | 8 +- src/mri2mesh/surface/idealized_brain.py | 154 ++++++++++++++++ src/mri2mesh/viz/mpl_slice.py | 6 +- 14 files changed, 740 insertions(+), 37 deletions(-) create mode 100644 src/mri2mesh/mesh/__init__.py create mode 100644 src/mri2mesh/mesh/basic.py create mode 100644 src/mri2mesh/mesh/idealized_brain.py create mode 100644 src/mri2mesh/surface/idealized_brain.py diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 857afe5..be71d00 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -15,6 +15,7 @@ jobs: build: runs-on: ubuntu-22.04 + container: ghcr.io/finsberg/ftfetwild-meta:main env: PUBLISH_DIR: ./_build/html DISPLAY: ":99.0" @@ -27,9 +28,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install dependencies for pyvista - run: sudo apt-get update && sudo apt-get install -y libgl1-mesa-dev xvfb - - name: Setup python uses: actions/setup-python@v5 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e963a88..98a7b35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,7 @@ repos: - id: mypy files: ^src/|^tests/ args: ["--config-file", "pyproject.toml"] + additional_dependencies: ['types-toml', 'types-PyYAML'] - repo: https://github.com/kynan/nbstripout diff --git a/examples/synthseg.ipynb b/examples/synthseg.ipynb index c886d60..839c0c5 100644 --- a/examples/synthseg.ipynb +++ b/examples/synthseg.ipynb @@ -22,10 +22,7 @@ "from pathlib import Path\n", "import mri2mesh\n", "import numpy as np\n", - "import pyvista as pv\n", - "\n", - "pv.start_xvfb()\n", - "# pv.set_jupyter_backend('trame')" + "import pyvista as pv" ] }, { @@ -54,6 +51,8 @@ "outputs": [], "source": [ "# Path to the Nifty file\n", + "outdir = Path(\"results-synthseg\")\n", + "outdir.mkdir(exist_ok=True)\n", "path = Path(\"201_t13d_synthseg.nii.gz\")\n", "mri2mesh.viz.volume_clip.main(path)" ] @@ -298,6 +297,27 @@ "cell_type": "markdown", "id": "28", "metadata": {}, + "source": [ + "Let us also smooth the surface and save it as a `.ply` file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "par_surf = par_surf.smooth_taubin(n_iter=20, pass_band=0.05)\n", + "par_surf = par_surf.clip_closed_surface(normal=(0, 0, 1), origin=(0, 0, 1))\n", + "par_surf.compute_normals(inplace=True, flip_normals=False)\n", + "pv.save_meshio(outdir / \"par.ply\", par_surf)" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, "source": [ "## Extracting surfaces of the lateral ventricles \n", "\n", @@ -307,7 +327,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -317,7 +337,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "32", "metadata": {}, "source": [ "We can also plot the isosurface with pyvista" @@ -326,7 +346,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -339,7 +359,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "34", "metadata": {}, "source": [ "We can now generate a surface of this mask using `pyvista`" @@ -348,7 +368,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -357,7 +377,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "36", "metadata": {}, "source": [ "Let us plot the surface with `pyvista`" @@ -366,7 +386,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -378,7 +398,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "38", "metadata": {}, "source": [ "We see that the surface is not very smooth, but we can use the `smooth_taubin` method to smooth it\n" @@ -387,7 +407,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -396,14 +416,6 @@ "plotter.add_mesh(surf_lateral_ventricles)\n", "plotter.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "38", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/pyproject.toml b/pyproject.toml index 44d2dd3..f2b8ffb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,19 +5,19 @@ build-backend = "setuptools.build_meta" [project] name = "mri2mesh" version = "0.1.0" -dependencies = ["pyvista", "numpy", "matplotlib", "nibabel", "scikit-image", "scipy", ] +dependencies = ["pyvista", "numpy", "matplotlib", "nibabel", "scikit-image", "scipy", "meshio"] classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", ] description = "Tool for converting labeled MRI data to a mesh" authors = [ - {name = "Marius Causemann", email = "mariusca@simula.no"}, {name = "Henrik Finsberg", email = "henriknf@simula.no"}, + {name = "Marius Causemann", email = "mariusca@simula.no"}, ] -keywords = ["mri", "fem", "brain", "fluid", "mechanics", "meshing"] -urls = {Homepage = "https://github.com/kent-and/mri2mesh"} +keywords = ["mri", "fem", "brain", "meshing"] +urls = {Homepage = "https://github.com/scientificcomputing/mri2mesh"} [project.readme] @@ -30,6 +30,7 @@ mri2mesh = "mri2mesh.cli:main" [project.optional-dependencies] test = ["pytest", "pytest-cov"] +mesh = ["wildmeshing", "h5py"] docs = ["pyvista[jupyter]", "jupyter-book"] @@ -122,7 +123,7 @@ tag = true sign_tags = false tag_name = "v{new_version}" tag_message = "Bump version: {current_version} → {new_version}" -current_version = "1.1.3" +current_version = "0.1.0" [[tool.bumpversion.files]] diff --git a/src/mri2mesh/__init__.py b/src/mri2mesh/__init__.py index 641960b..49ae005 100644 --- a/src/mri2mesh/__init__.py +++ b/src/mri2mesh/__init__.py @@ -1,9 +1,19 @@ +from importlib.metadata import metadata + from . import viz from . import morphology from . import cli from . import vtk_utils from . import surface +from . import mesh from . import reader from .reader import read -__all__ = ["viz", "morphology", "cli", "vtk_utils", "surface", "reader", "read"] +__all__ = ["viz", "morphology", "cli", "vtk_utils", "surface", "reader", "read", "mesh"] + +meta = metadata("mri2mesh") +__version__ = meta["Version"] +__author__ = meta["Author-email"] +__license__ = meta["License"] +__email__ = meta["Author-email"] +__program_name__ = meta["Name"] diff --git a/src/mri2mesh/cli.py b/src/mri2mesh/cli.py index 1bed5b6..7a464bf 100644 --- a/src/mri2mesh/cli.py +++ b/src/mri2mesh/cli.py @@ -1,7 +1,7 @@ import logging import argparse -from . import viz, surface +from . import viz, surface, mesh def setup_parser(): @@ -27,6 +27,10 @@ def setup_parser(): # Surface generation parser surface_parser = subparsers.add_parser("surface", help="Generate surfaces") surface.add_surface_parser(surface_parser) + + # Mesh generation parser + mesh_parser = subparsers.add_parser("mesh", help="Generate meshes") + mesh.add_mesh_parser(mesh_parser) return parser @@ -54,6 +58,8 @@ def dispatch(parser: argparse.ArgumentParser) -> int: viz.dispatch(args.pop("viz-command"), args) elif command == "surface": surface.dispatch(args.pop("surface-command"), args) + elif command == "mesh": + mesh.dispatch(args.pop("mesh-command"), args) else: logger.error(f"Unknown command {command}") parser.print_help() diff --git a/src/mri2mesh/mesh/__init__.py b/src/mri2mesh/mesh/__init__.py new file mode 100644 index 0000000..4151fa2 --- /dev/null +++ b/src/mri2mesh/mesh/__init__.py @@ -0,0 +1,52 @@ +import typing +import logging +import argparse +from pathlib import Path + +from . import basic, idealized_brain + +logger = logging.getLogger(__name__) + + +def add_mesh_parser(parser: argparse.ArgumentParser) -> None: + subparsers = parser.add_subparsers(dest="mesh-command") + + template_parser = subparsers.add_parser( + "template", help="Create a sample configuration file called" + ) + template_parser.add_argument("outdir", type=Path, help="Output directory") + template_parser.add_argument( + "--name", type=str, default="config.json", help="Name of the configuration file" + ) + + create_parser = subparsers.add_parser("create", help="Create a mesh") + create_parser.add_argument("filename", type=Path, help="Path to the configuration file") + + convert_parser = subparsers.add_parser("convert", help="Convert mesh to dolfinx") + convert_parser.add_argument("mesh_dir", type=Path, help="Directory containing mesh files") + + idealized_parser = subparsers.add_parser("idealized", help="Generate idealized surface") + idealized_brain.add_arguments(idealized_parser) + + +def dispatch(command, args: dict[str, typing.Any]) -> int: + if command == "template": + basic.generate_sameple_config(**args) + + elif command == "create": + mesh_dir = basic.create_mesh_from_config(**args) + try: + basic.convert_mesh_dolfinx(mesh_dir=mesh_dir) + except ImportError: + logger.debug("dolfinx not installed, skipping conversion to dolfinx") + + elif command == "convert": + basic.convert_mesh_dolfinx(**args) + + elif command == "idealized": + idealized_brain.main(**args) + + else: + raise ValueError(f"Unknown command {command}") + + return 0 diff --git a/src/mri2mesh/mesh/basic.py b/src/mri2mesh/mesh/basic.py new file mode 100644 index 0000000..cf3cab3 --- /dev/null +++ b/src/mri2mesh/mesh/basic.py @@ -0,0 +1,235 @@ +import typing +import json +import logging +import pprint +from pathlib import Path +import numpy as np + +logger = logging.getLogger(__name__) + + +def sample_config(): + tree = { + "operation": "union", + "right": { + "operation": "union", + "left": "surfaces/LV.ply", + "right": "surfaces/V34.ply", + }, + "left": { + "operation": "union", + "left": "surfaces/skull.ply", + "right": "surfaces/parenchyma_incl_ventr.ply", + }, + } + + config = { + "outdir": "mesh", + "epsilon": 0.0009, + "edge_length_r": 0.015, + "coarsen": True, + "stop_quality": 8, + "max_its": 30, + "loglevel": 10, + "disable_filtering": False, + "use_floodfill": False, + "smooth_open_boundary": False, + "manifold_surface": False, + "csg_tree": tree, + } + return config + + +def default_config() -> dict[str, typing.Any]: + config = sample_config() + config["csg_tree"] = {} + return config + + +def generate_sameple_config(outdir: Path, name: str = "config.json"): + logger.info("Generating sample configuration file") + config = sample_config() + path = outdir / name + if path.suffix == ".json": + path.write_text(json.dumps(config, indent=2)) + elif path.suffix == ".toml": + import toml + + path.write_text(toml.dumps(config)) + elif path.suffix == ".yaml": + import yaml + + path.write_text(yaml.dump(config)) + else: + raise ValueError(f"Unknown file extension {path.suffix}") + logger.info("Configuration file written to %s", path) + + +def read_config(path: Path) -> dict[str, typing.Any]: + logger.info("Reading configuration file %s", path) + if path.suffix == ".json": + return json.loads(path.read_text()) + elif path.suffix == ".toml": + import toml + + return toml.loads(path.read_text()) + elif path.suffix == ".yaml": + import yaml + + return yaml.load(path.read_text(), Loader=yaml.FullLoader) + else: + raise ValueError(f"Unknown file extension {path.suffix}") + + +class CSGTree(typing.TypedDict): + operation: str + left: typing.Union[str, "CSGTree"] + right: typing.Union[str, "CSGTree"] + + +def create_mesh_from_config(filename: Path) -> Path: + if not filename.exists(): + raise FileNotFoundError(f"File {filename} not found") + config = default_config() + config.update(read_config(filename)) + create_mesh(**config) + return Path(config["outdir"]) + + +def create_mesh( + outdir: typing.Union[Path, str], + csg_tree: CSGTree, + epsilon: float = 0.0009, + edge_length_r: float = 0.015, + skip_simplify: bool = False, + coarsen: bool = True, + stop_quality: int = 8, + max_its: int = 30, + loglevel: int = 10, + disable_filtering: bool = False, + use_floodfill: bool = False, + smooth_open_boundary: bool = False, + manifold_surface: bool = False, +): + logger.info("Creating mesh, with the following configuration") + params = { + "outdir": str(outdir), + "csg_tree": csg_tree, + "epsilon": epsilon, + "edge_length_r": edge_length_r, + "skip_simplify": skip_simplify, + "coarsen": coarsen, + "stop_quality": stop_quality, + "max_its": max_its, + "loglevel": loglevel, + "disable_filtering": disable_filtering, + "use_floodfill": use_floodfill, + "smooth_open_boundary": smooth_open_boundary, + "manifold_surface": manifold_surface, + } + logger.info(pprint.pformat(params)) + + import wildmeshing as wm + import meshio + import pyvista as pv + + outdir = Path(outdir) + outdir.mkdir(parents=True, exist_ok=True) + (outdir / "mesh_params.json").write_text(json.dumps(params, indent=2)) + + tetra = wm.Tetrahedralizer( + epsilon=epsilon, + edge_length_r=edge_length_r, + coarsen=coarsen, + stop_quality=stop_quality, + max_its=max_its, + skip_simplify=skip_simplify, + ) + tetra.set_log_level(loglevel) + + tetra.load_csg_tree(json.dumps(csg_tree)) + tetra.tetrahedralize() + point_array, cell_array, marker = tetra.get_tet_mesh( + all_mesh=disable_filtering, + smooth_open_boundary=smooth_open_boundary, + floodfill=use_floodfill, + manifold_surface=manifold_surface, + correct_surface_orientation=True, + ) + coords, connections = tetra.get_tracked_surfaces() + + tetra_mesh = meshio.Mesh( + point_array, [("tetra", cell_array)], cell_data={"cell_tags": [marker.ravel()]} + ) + tetra_mesh_pv = pv.from_meshio(tetra_mesh).clean() + pv.save_meshio(outdir / "tetra_mesh.xdmf", tetra_mesh_pv) + + for i, coord in enumerate(coords): + np.save(outdir / f"coords_{i}.npy", coord) + + for i, conn in enumerate(connections): + np.save(outdir / f"connections_{i}.npy", conn) + + +def convert_mesh_dolfinx(mesh_dir: Path): + logger.info("Converting mesh to dolfinx in %s", mesh_dir) + from mpi4py import MPI + from scipy.spatial.distance import cdist + import dolfinx + + threshold = 1.0 + fdim = 2 + + coords = [] + for path in sorted(mesh_dir.glob("coords_*.npy"), key=lambda x: int(x.stem.split("_")[-1])): + coords.append(np.load(path)) + logger.debug(f"Found {len(coords)} coordinates") + + connections = [] + for path in sorted( + mesh_dir.glob("connections_*.npy"), key=lambda x: int(x.stem.split("_")[-1]) + ): + connections.append(np.load(path)) + logger.debug(f"Found {len(connections)} connections") + + assert len(connections) == len(coords) + + logger.debug("Loading mesh") + comm = MPI.COMM_WORLD + with dolfinx.io.XDMFFile(comm, mesh_dir / "tetra_mesh.xdmf", "r") as xdmf: + mesh = xdmf.read_mesh(name="Grid") + cell_tags = xdmf.read_meshtags(mesh, name="Grid") + + logger.debug("Mesh loaded") + + facets = [] + values = [] + for i, coord in enumerate(coords, start=1): + logger.debug(f"Processing coord {i}") + + def locator(x): + # Find the distance to all coordinates + distances = cdist(x.T, coord) + # And return True is they are close + return np.any(distances < threshold, axis=1) + + f = dolfinx.mesh.locate_entities_boundary(mesh, dim=fdim, marker=locator) + v = np.full(f.shape[0], i, dtype=np.int32) + facets.append(f) + values.append(v) + + logger.debug("Create meshtags") + facet_tags = dolfinx.mesh.meshtags( + mesh, + fdim, + np.hstack(facets), + np.hstack(values), + ) + + logger.debug("Save files") + with dolfinx.io.XDMFFile(comm, mesh_dir / "mesh.xdmf", "w") as xdmf: + xdmf.write_mesh(mesh) + xdmf.write_meshtags(facet_tags, mesh.geometry) + xdmf.write_meshtags(cell_tags, mesh.geometry) + + logger.info("Mesh saved to %s", mesh_dir / "mesh.xdmf") diff --git a/src/mri2mesh/mesh/idealized_brain.py b/src/mri2mesh/mesh/idealized_brain.py new file mode 100644 index 0000000..21660f6 --- /dev/null +++ b/src/mri2mesh/mesh/idealized_brain.py @@ -0,0 +1,224 @@ +from pathlib import Path +import logging + +logger = logging.getLogger(__name__) + + +def add_arguments(parser): + parser.add_argument( + "-o", + "--outdir", + type=Path, + required=True, + help="Output directory", + ) + parser.add_argument( + "--r", + type=float, + default=0.1, + help="Radius of the brain", + ) + parser.add_argument( + "--parenchyma-factor", + type=float, + default=0.6, + help="Parenchyma factor", + ) + parser.add_argument( + "--lv-factor", + type=float, + default=0.2, + help="Left ventricle factor", + ) + parser.add_argument( + "--v34-center-factor", + type=float, + default=0.4, + help="Ventricle 3/4 center factor", + ) + parser.add_argument( + "--v34-height-factor", + type=float, + default=0.42, + help="Ventricle 3/4 height factor", + ) + parser.add_argument( + "--v34-radius-factor", + type=float, + default=0.06, + help="Ventricle 3/4 radius factor", + ) + parser.add_argument( + "--skull-x0", + type=float, + default=0.0103891, + help="Skull x0", + ) + parser.add_argument( + "--skull-x1", + type=float, + default=0.155952, + help="Skull x1", + ) + parser.add_argument( + "--skull-y0", + type=float, + default=0.0114499, + help="Skull y0", + ) + parser.add_argument( + "--skull-y1", + type=float, + default=0.173221, + help="Skull y1", + ) + parser.add_argument( + "--skull-z0", + type=float, + default=0.001, + help="Skull z0", + ) + parser.add_argument( + "--skull-z1", + type=float, + default=0.154949, + help="Skull z1", + ) + parser.add_argument( + "--epsilon", + type=float, + default=0.0009, + help="Epsilon", + ) + parser.add_argument( + "--edge-length-r", + type=float, + default=0.015, + help="Edge length", + ) + parser.add_argument( + "--skip-simplify", + action="store_true", + help="Skip simplification", + ) + parser.add_argument( + "--coarsen", + action="store_true", + help="Coarsen", + ) + parser.add_argument( + "--stop-quality", + type=int, + default=8, + help="Stop quality", + ) + parser.add_argument( + "--max-its", + type=int, + default=30, + help="Max iterations", + ) + parser.add_argument( + "--loglevel", + type=int, + default=10, + help="Log level", + ) + parser.add_argument( + "--disable-filtering", + action="store_true", + help="Disable filtering", + ) + parser.add_argument( + "--use-floodfill", + action="store_true", + help="Use floodfill", + ) + parser.add_argument( + "--smooth-open-boundary", + action="store_true", + help="Smooth open boundary", + ) + parser.add_argument( + "--manifold-surface", + action="store_true", + help="Manifold surface", + ) + + +def main( + outdir: Path, + r: float = 0.1, + parenchyma_factor: float = 0.6, + lv_factor: float = 0.2, + v34_center_factor: float = 0.4, + v34_height_factor: float = 0.42, + v34_radius_factor: float = 0.06, + skull_x0: float = 0.0103891, + skull_x1: float = 0.155952, + skull_y0: float = 0.0114499, + skull_y1: float = 0.173221, + skull_z0: float = 0.001, + skull_z1: float = 0.154949, + epsilon: float = 0.0009, + edge_length_r: float = 0.015, + skip_simplify: bool = False, + coarsen: bool = True, + stop_quality: int = 8, + max_its: int = 30, + loglevel: int = 10, + disable_filtering: bool = False, + use_floodfill: bool = False, + smooth_open_boundary: bool = False, + manifold_surface: bool = False, +) -> None: + logger.info("Generating idealized brain surface") + from ..surface.idealized_brain import main as main_surface + + main_surface( + outdir, + r=r, + parenchyma_factor=parenchyma_factor, + lv_factor=lv_factor, + v34_center_factor=v34_center_factor, + v34_height_factor=v34_height_factor, + v34_radius_factor=v34_radius_factor, + skull_x0=skull_x0, + skull_x1=skull_x1, + skull_y0=skull_y0, + skull_y1=skull_y1, + skull_z0=skull_z0, + skull_z1=skull_z1, + ) + from .basic import create_mesh, CSGTree, convert_mesh_dolfinx + + csg_tree: CSGTree = { + "operation": "union", + "right": { + "operation": "union", + "left": f"{outdir}/LV.ply", + "right": f"{outdir}/V34.ply", + }, + "left": { + "operation": "union", + "left": f"{outdir}/skull.ply", + "right": f"{outdir}/parenchyma_incl_ventr.ply", + }, + } + + create_mesh( + outdir, + csg_tree=csg_tree, + epsilon=epsilon, + edge_length_r=edge_length_r, + skip_simplify=skip_simplify, + coarsen=coarsen, + stop_quality=stop_quality, + max_its=max_its, + loglevel=loglevel, + disable_filtering=disable_filtering, + use_floodfill=use_floodfill, + smooth_open_boundary=smooth_open_boundary, + manifold_surface=manifold_surface, + ) + convert_mesh_dolfinx(mesh_dir=outdir) diff --git a/src/mri2mesh/morphology.py b/src/mri2mesh/morphology.py index c1ac793..dabbd32 100644 --- a/src/mri2mesh/morphology.py +++ b/src/mri2mesh/morphology.py @@ -3,7 +3,7 @@ import numpy.typing as npt import skimage.morphology as skim import skimage -import scipy.ndimage as ndi + logger = logging.getLogger(__name__) @@ -34,6 +34,8 @@ def get_closest_point(a: npt.NDArray, b: np.ndarray) -> tuple[np.int64, ...]: >>> assert np.allclose(a_b_index, (5, 5, 5)) """ + import scipy.ndimage as ndi + dist = ndi.distance_transform_edt(np.logical_not(a)) assert isinstance(dist, np.ndarray) dist[np.logical_not(b)] = np.inf diff --git a/src/mri2mesh/reader.py b/src/mri2mesh/reader.py index 291d0ae..ddd7fa8 100644 --- a/src/mri2mesh/reader.py +++ b/src/mri2mesh/reader.py @@ -2,12 +2,10 @@ from pathlib import Path import logging import typing - import numpy as np import numpy.typing as npt from dataclasses import dataclass, field -import nibabel as nib from .segmentation_labels import NEUROQUANT_LABELS, SYNTHSEG_LABELS @@ -54,6 +52,8 @@ def read( label_name: typing.Literal["synthseg", "neuroquant"] = "synthseg", padding: int = 5, ) -> Segmentation: + import nibabel as nib + logger.info(f"Loading segmentation from {input}") seg = nib.load(input) img = np.pad(seg.get_fdata(), padding) diff --git a/src/mri2mesh/surface/__init__.py b/src/mri2mesh/surface/__init__.py index 3d9a974..cf5ac4a 100644 --- a/src/mri2mesh/surface/__init__.py +++ b/src/mri2mesh/surface/__init__.py @@ -2,7 +2,7 @@ import argparse import logging -from . import parenchyma +from . import parenchyma, idealized_brain logger = logging.getLogger(__name__) @@ -13,11 +13,17 @@ def add_surface_parser(parser: argparse.ArgumentParser) -> None: parenchyma_parser = subparsers.add_parser("parenchyma", help="Generate parenchyma surface") parenchyma.add_arguments(parenchyma_parser) + idealized_parser = subparsers.add_parser("idealized", help="Generate idealized surface") + idealized_brain.add_arguments(idealized_parser) + def dispatch(command, args: dict[str, typing.Any]) -> int: if command == "parenchyma": parenchyma.main(**args) + elif command == "idealized": + idealized_brain.main(**args) + else: raise ValueError(f"Unknown command {command}") diff --git a/src/mri2mesh/surface/idealized_brain.py b/src/mri2mesh/surface/idealized_brain.py new file mode 100644 index 0000000..685238c --- /dev/null +++ b/src/mri2mesh/surface/idealized_brain.py @@ -0,0 +1,154 @@ +from pathlib import Path +import argparse +import logging +import json +import datetime +import numpy as np + +logger = logging.getLogger(__name__) + + +def add_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "-o", + "--outdir", + type=Path, + required=True, + help="Output directory", + ) + parser.add_argument( + "--r", + type=float, + default=0.1, + help="Radius of the brain", + ) + parser.add_argument( + "--parenchyma-factor", + type=float, + default=0.6, + help="Parenchyma factor", + ) + parser.add_argument( + "--lv-factor", + type=float, + default=0.2, + help="Left ventricle factor", + ) + parser.add_argument( + "--v34-center-factor", + type=float, + default=0.4, + help="Ventricle 3/4 center factor", + ) + parser.add_argument( + "--v34-height-factor", + type=float, + default=0.42, + help="Ventricle 3/4 height factor", + ) + parser.add_argument( + "--v34-radius-factor", + type=float, + default=0.06, + help="Ventricle 3/4 radius factor", + ) + parser.add_argument( + "--skull-x0", + type=float, + default=0.0103891, + help="Skull x0", + ) + parser.add_argument( + "--skull-x1", + type=float, + default=0.155952, + help="Skull x1", + ) + parser.add_argument( + "--skull-y0", + type=float, + default=0.0114499, + help="Skull y0", + ) + parser.add_argument( + "--skull-y1", + type=float, + default=0.173221, + help="Skull y1", + ) + parser.add_argument( + "--skull-z0", + type=float, + default=0.001, + help="Skull z0", + ) + parser.add_argument( + "--skull-z1", + type=float, + default=0.154949, + help="Skull z1", + ) + + +def main( + outdir: Path, + r: float = 0.1, + parenchyma_factor: float = 0.6, + lv_factor: float = 0.2, + v34_center_factor: float = 0.4, + v34_height_factor: float = 0.42, + v34_radius_factor: float = 0.06, + skull_x0: float = 0.0103891, + skull_x1: float = 0.155952, + skull_y0: float = 0.0114499, + skull_y1: float = 0.173221, + skull_z0: float = 0.001, + skull_z1: float = 0.154949, +) -> None: + import pyvista as pv + + logger.info("Creating idealized brain surface in %s", outdir) + outdir.mkdir(exist_ok=True, parents=True) + + z = np.array([0, 0, 1]) + skull = pv.Box((skull_x0, skull_x1, skull_y0, skull_y1, skull_z0, skull_z1)) + c = skull.center_of_mass() + par = pv.Sphere(r * parenchyma_factor, center=c) + LV = pv.Sphere(r * lv_factor, center=c) + V34 = pv.Cylinder( + c - v34_center_factor * r * z, + direction=z, + height=r * v34_height_factor, + radius=v34_radius_factor * r, + ) + ventricles = pv.merge([LV, V34]) + + for s, n in zip( + [V34, LV, par, skull, ventricles], + ["V34", "LV", "parenchyma_incl_ventr", "skull", "ventricles"], + ): + pv.save_meshio(outdir / f"{n}.ply", s) + + from .. import __version__ + + (outdir / "surface_parameters.json").write_text( + json.dumps( + { + "r": r, + "parenchyma_factor": parenchyma_factor, + "lv_factor": lv_factor, + "v34_center_factor": v34_center_factor, + "v34_height_factor": v34_height_factor, + "v34_radius_factor": v34_radius_factor, + "skull_x0": skull_x0, + "skull_x1": skull_x1, + "skull_y0": skull_y0, + "skull_y1": skull_y1, + "skull_z0": skull_z0, + "skull_z1": skull_z1, + "version": __version__, + "timestamp": datetime.datetime.now().isoformat(), + }, + indent=2, + ) + ) diff --git a/src/mri2mesh/viz/mpl_slice.py b/src/mri2mesh/viz/mpl_slice.py index 9121605..c148a6a 100644 --- a/src/mri2mesh/viz/mpl_slice.py +++ b/src/mri2mesh/viz/mpl_slice.py @@ -2,8 +2,6 @@ import argparse import typing from pathlib import Path -import nibabel as nib -import matplotlib.pyplot as plt import numpy as np @@ -58,6 +56,8 @@ def main( input: Path, output: Path, tag: str, cmap: str, add_colorbar: bool, nx: int, ny: int ) -> int: output.mkdir(parents=True, exist_ok=True) + import nibabel as nib + img = nib.load(input).get_fdata() plot_slices( img, @@ -82,6 +82,8 @@ def plot_slice( ny: int, slice: str, ): + import matplotlib.pyplot as plt + minval = np.min(img) maxval = np.max(img) fig, ax = plt.subplots(nx, ny, figsize=(20, 20)) From 5652625081695b4551d8365cb60a621b24a214f1 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Mon, 24 Feb 2025 21:56:14 +0100 Subject: [PATCH 02/25] Use conda in CI --- .github/workflows/build_docs.yml | 21 ++++++++++++++++++--- .github/workflows/test.yml | 22 +++++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index be71d00..40c2e3d 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -15,7 +15,6 @@ jobs: build: runs-on: ubuntu-22.04 - container: ghcr.io/finsberg/ftfetwild-meta:main env: PUBLISH_DIR: ./_build/html DISPLAY: ":99.0" @@ -28,10 +27,26 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 + - name: Cache conda + uses: actions/cache@v3 + env: + # Increase this value to reset cache if environment.yml has not changed + CACHE_NUMBER: 0 + with: + path: ~/conda_pkgs_dir + key: + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ + hashFiles('environment.yml') }} + + - uses: conda-incubator/setup-miniconda@v3 with: python-version: 3.12 + mamba-version: "*" + channels: conda-forge,defaults + channel-priority: strict + activate-environment: mri2mesh + environment-file: environment.yml + use-only-tar-bz2: true - name: Install dependencies run: python3 -m pip install ".[docs]" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3a1464..41971e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,11 +19,27 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + + - name: Cache conda + uses: actions/cache@v3 + env: + # Increase this value to reset cache if environment.yml has not changed + CACHE_NUMBER: 0 + with: + path: ~/conda_pkgs_dir + key: + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-py-${{ matrix.python-version }}${{ + hashFiles('environment.yml') }} + + - uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ matrix.python-version }} - allow-prereleases: true + mamba-version: "*" + channels: conda-forge,defaults + channel-priority: strict + activate-environment: mri2mesh + environment-file: environment.yml + use-only-tar-bz2: true - name: Install mri2mesh run: | From e754f066ee7ef229241d9def9d3f9b42e0dd803f Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 08:57:17 +0100 Subject: [PATCH 03/25] Add environment.yml file --- environment.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 environment.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..884ca38 --- /dev/null +++ b/environment.yml @@ -0,0 +1,19 @@ +name: mri2mesh +channels: + - conda-forge +dependencies: + - fenics-dolfinx + - numpy + - ipython + - matplotlib + - pyvista + - scikit-image + - meshio + - h5py + - seaborn + - nibabel + - imageio-ffmpeg + - napari + - jupyter + - pip: + - wildmeshing From 7da554a137f573853af2cd197d181a7ce8917e99 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 09:07:02 +0100 Subject: [PATCH 04/25] Do not specify mamba-verrsion --- .github/workflows/build_docs.yml | 1 - .github/workflows/test.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 40c2e3d..a3b850c 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -41,7 +41,6 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: python-version: 3.12 - mamba-version: "*" channels: conda-forge,defaults channel-priority: strict activate-environment: mri2mesh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41971e8..e0d0e72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,6 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ matrix.python-version }} - mamba-version: "*" channels: conda-forge,defaults channel-priority: strict activate-environment: mri2mesh From 289302645b8a8facb82e314de0d7a72c5f30df9a Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 09:12:48 +0100 Subject: [PATCH 05/25] Try to use miniforge --- .github/workflows/build_docs.yml | 1 + .github/workflows/test.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index a3b850c..e17a369 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -46,6 +46,7 @@ jobs: activate-environment: mri2mesh environment-file: environment.yml use-only-tar-bz2: true + miniforge-version: latest - name: Install dependencies run: python3 -m pip install ".[docs]" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0d0e72..65e4c2c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,7 @@ jobs: activate-environment: mri2mesh environment-file: environment.yml use-only-tar-bz2: true + miniforge-version: latest - name: Install mri2mesh run: | From 588c24758253bffae64e39c5a62c2781cd52219d Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 09:15:15 +0100 Subject: [PATCH 06/25] Do not auto-update conda --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65e4c2c..dd1f681 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,7 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ matrix.python-version }} + auto-update-conda: false channels: conda-forge,defaults channel-priority: strict activate-environment: mri2mesh From dad5861625c86612af81fdcb35d92f88429940dd Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 09:16:19 +0100 Subject: [PATCH 07/25] Add pip as dependency --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index 884ca38..fe9c6c9 100644 --- a/environment.yml +++ b/environment.yml @@ -15,5 +15,6 @@ dependencies: - imageio-ffmpeg - napari - jupyter + - pip - pip: - wildmeshing From 0abb14d73673d2a33ab4854853e4d51de5ef5f71 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 09:18:50 +0100 Subject: [PATCH 08/25] Set lower bound on fenicsx --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index fe9c6c9..249007d 100644 --- a/environment.yml +++ b/environment.yml @@ -2,7 +2,7 @@ name: mri2mesh channels: - conda-forge dependencies: - - fenics-dolfinx + - fenics-dolfinx>=0.9.0 - numpy - ipython - matplotlib From da4440dda7c1c7bc711a969267b8590d1f1dd7ec Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 09:19:22 +0100 Subject: [PATCH 09/25] Use python3.12 to build docs --- .github/workflows/build_docs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index e17a369..e222989 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -40,7 +40,8 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: - python-version: 3.12 + python-version: 3.10 + auto-update-conda: false channels: conda-forge,defaults channel-priority: strict activate-environment: mri2mesh From 7e84e724378715668286880c57799eec9f2efa4f Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 09:25:39 +0100 Subject: [PATCH 10/25] Remove option about use_only_tar_bz2 --- .github/workflows/build_docs.yml | 1 - .github/workflows/test.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index e222989..1c27872 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -46,7 +46,6 @@ jobs: channel-priority: strict activate-environment: mri2mesh environment-file: environment.yml - use-only-tar-bz2: true miniforge-version: latest - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd1f681..4c00529 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,6 @@ jobs: channel-priority: strict activate-environment: mri2mesh environment-file: environment.yml - use-only-tar-bz2: true miniforge-version: latest - name: Install mri2mesh From fc696c195f587cfdb0de12494b1ed69b9b2f06ca Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 09:51:57 +0100 Subject: [PATCH 11/25] Skip tests on windows (since fenicsx is not available there) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c00529..8436734 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, macos-latest] steps: From c855352bb2b623ade231c2d45ca8187a62b9ee6c Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 10:31:45 +0100 Subject: [PATCH 12/25] Add license --- pyproject.toml | 1 + src/mri2mesh/mesh/__init__.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f2b8ffb..2918772 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ authors = [ {name = "Marius Causemann", email = "mariusca@simula.no"}, ] +license = {text = "MIT"} keywords = ["mri", "fem", "brain", "meshing"] urls = {Homepage = "https://github.com/scientificcomputing/mri2mesh"} diff --git a/src/mri2mesh/mesh/__init__.py b/src/mri2mesh/mesh/__init__.py index 4151fa2..efcdbc5 100644 --- a/src/mri2mesh/mesh/__init__.py +++ b/src/mri2mesh/mesh/__init__.py @@ -25,7 +25,11 @@ def add_mesh_parser(parser: argparse.ArgumentParser) -> None: convert_parser = subparsers.add_parser("convert", help="Convert mesh to dolfinx") convert_parser.add_argument("mesh_dir", type=Path, help="Directory containing mesh files") - idealized_parser = subparsers.add_parser("idealized", help="Generate idealized surface") + idealized_parser = subparsers.add_parser( + "idealized", + help="Generate idealized surface", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) idealized_brain.add_arguments(idealized_parser) From a6909e63792d5f4554aa1119f373367af0e18549 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 12:58:14 +0100 Subject: [PATCH 13/25] Add cli example --- _toc.yml | 1 + examples/cli.ipynb | 357 ++++++++++++++++++++++++++++ examples/synthseg.ipynb | 2 +- src/mri2mesh/cli.py | 19 +- src/mri2mesh/mesh/basic.py | 3 + src/mri2mesh/segmentation_labels.py | 17 ++ src/mri2mesh/surface/__init__.py | 10 +- 7 files changed, 401 insertions(+), 8 deletions(-) create mode 100644 examples/cli.ipynb diff --git a/_toc.yml b/_toc.yml index bc7a9db..de2f8a3 100644 --- a/_toc.yml +++ b/_toc.yml @@ -5,6 +5,7 @@ parts: - caption: Examples chapters: - file: "examples/synthseg.ipynb" + - file: "examples/cli.ipynb" - caption: Community chapters: - file: "CONTRIBUTING" diff --git a/examples/cli.ipynb b/examples/cli.ipynb new file mode 100644 index 0000000..ff76eac --- /dev/null +++ b/examples/cli.ipynb @@ -0,0 +1,357 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Command line interface\n", + "\n", + "The package comes with a command line interface called `mri2mesh`.\n", + "\n", + "*Note: In this documentation we will start the commands with `!` but this is only to make it run. In the terminal you should ommit the `!`*\n", + "\n", + "You can list the different commands with `--help`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh --help" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Listing segmentation labels\n", + "\n", + "One simeple thing you can do is to list the segmentation labels for different segmentation software. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh labels --help" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "For example for [synthseg](https://github.com/BBillot/SynthSeg) you can do" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh labels synthseg" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Generating surfaces\n", + "\n", + "To see all options for generating surfaces you can again pass the help command" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh surface --help" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "Let ut create an indealized brain and put the surfaces in a new folder" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh surface idealized --help" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "!rm -rf idealized-brain\n", + "!mri2mesh surface idealized -o idealized-brain" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "Let us see which files that was generated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "!ls idealized-brain" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "We can for example take a look at the parameters the was used" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "!cat idealized-brain/surface_parameters.json" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Generating meshes\n", + "\n", + "We will now show how to create a mesh from the surfaces" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh mesh --help" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "We will start by creating a mesh with the `create` command" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh mesh create --help" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "We see that this command takes a config file as input. We can generate a template for this config file using the `template` command" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh mesh template ." + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "Let us have a look at this file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "!cat config.json" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "Most of these key-value pairs are paramerters to the meshing algorithm. The `csg_tree` describes how to combine the surfaces into a mesh and also create proper subdomains. In our case we would like to take the union of all surfaces. We can do this by first taking the union of `LV.ply` and `V34.ply` into a right component and `skull.ply` and `parenchyma_incl_ventr.ply` into a left component, and then finally take the union of the left and right component. In the template however, we need to update the paths because the surfaces are located in the folder `idealized-brain`. Let us also change the output directory to the same folder." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import json\n", + "\n", + "config_fname = Path(\"config.json\")\n", + "config = json.loads(config_fname.read_text())\n", + "surface_folder = \"idealized-brain\"\n", + "config[\"outdir\"] = surface_folder\n", + "csg_tree = {\n", + " \"operation\": \"union\",\n", + " \"right\": {\n", + " \"operation\": \"union\",\n", + " \"left\": f\"{surface_folder}/LV.ply\",\n", + " \"right\": f\"{surface_folder}/V34.ply\"\n", + " },\n", + " \"left\": {\n", + " \"operation\": \"union\",\n", + " \"left\": f\"{surface_folder}/skull.ply\",\n", + " \"right\": f\"{surface_folder}/parenchyma_incl_ventr.ply\"\n", + " }\n", + "}\n", + "config[\"csg_tree\"] = csg_tree\n", + "config_fname.write_text(json.dumps(config))" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "We can now create the mesh by passing in the config file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "!mri2mesh mesh create config.json" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "Let us have a look at the mesh" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "from mpi4py import MPI\n", + "import dolfinx\n", + "\n", + "surface_folder = \"mesh\"\n", + "\n", + "with dolfinx.io.XDMFFile(MPI.COMM_WORLD, f\"{surface_folder}/mesh.xdmf\", \"r\") as xdmf:\n", + " mesh = xdmf.read_mesh(name=\"Grid\")\n", + " cell_tags = xdmf.read_meshtags(mesh, name=\"cell_tags\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "import pyvista as pv \n", + "\n", + "vtk_mesh = dolfinx.plot.vtk_mesh(mesh, cell_tags.dim, cell_tags.indices)\n", + "bgrid = pv.UnstructuredGrid(*vtk_mesh)\n", + "bgrid.cell_data[\"Cell tags\"] = cell_tags.values\n", + "bgrid.set_active_scalars(\"Cell tags\")\n", + "p = pv.Plotter(window_size=[800, 800])\n", + "p.add_mesh_clip_plane(bgrid)\n", + "if not pv.OFF_SCREEN:\n", + " p.show()\n", + "else:\n", + " figure = p.screenshot(\"idealized_brain.png\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/synthseg.ipynb b/examples/synthseg.ipynb index 839c0c5..e1db723 100644 --- a/examples/synthseg.ipynb +++ b/examples/synthseg.ipynb @@ -439,7 +439,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/src/mri2mesh/cli.py b/src/mri2mesh/cli.py index 7a464bf..fdaa150 100644 --- a/src/mri2mesh/cli.py +++ b/src/mri2mesh/cli.py @@ -1,7 +1,8 @@ import logging import argparse +from typing import Sequence -from . import viz, surface, mesh +from . import viz, surface, mesh, segmentation_labels def setup_parser(): @@ -31,6 +32,12 @@ def setup_parser(): # Mesh generation parser mesh_parser = subparsers.add_parser("mesh", help="Generate meshes") mesh.add_mesh_parser(mesh_parser) + + label_parser = subparsers.add_parser("labels", help="List segmentation labels") + label_parser.add_argument( + "name", type=str, choices=["synthseg", "neuroquant"], help="Name of the labels to display" + ) + return parser @@ -39,8 +46,8 @@ def _disable_loggers(): logging.getLogger(libname).setLevel(logging.WARNING) -def dispatch(parser: argparse.ArgumentParser) -> int: - args = vars(parser.parse_args()) +def dispatch(parser: argparse.ArgumentParser, argv: Sequence[str] | None = None) -> int: + args = vars(parser.parse_args(argv)) logging.basicConfig(level=logging.DEBUG if args.pop("verbose") else logging.INFO) _disable_loggers() @@ -60,6 +67,8 @@ def dispatch(parser: argparse.ArgumentParser) -> int: surface.dispatch(args.pop("surface-command"), args) elif command == "mesh": mesh.dispatch(args.pop("mesh-command"), args) + elif command == "labels": + segmentation_labels.dispatch(args.pop("name")) else: logger.error(f"Unknown command {command}") parser.print_help() @@ -70,6 +79,6 @@ def dispatch(parser: argparse.ArgumentParser) -> int: return 0 -def main() -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = setup_parser() - return dispatch(parser) + return dispatch(parser, argv) diff --git a/src/mri2mesh/mesh/basic.py b/src/mri2mesh/mesh/basic.py index cf3cab3..d996e82 100644 --- a/src/mri2mesh/mesh/basic.py +++ b/src/mri2mesh/mesh/basic.py @@ -225,6 +225,9 @@ def locator(x): np.hstack(facets), np.hstack(values), ) + facet_tags.name = "facet_tags" + cell_tags.name = "cell_tags" + mesh.name = "mesh" logger.debug("Save files") with dolfinx.io.XDMFFile(comm, mesh_dir / "mesh.xdmf", "w") as xdmf: diff --git a/src/mri2mesh/segmentation_labels.py b/src/mri2mesh/segmentation_labels.py index 8698c13..8be0d12 100644 --- a/src/mri2mesh/segmentation_labels.py +++ b/src/mri2mesh/segmentation_labels.py @@ -1,3 +1,20 @@ +import logging + +logger = logging.getLogger(__name__) + + +def dispatch(name: str) -> None: + logger.info(f"Listing segmentation labels for {name}") + if name == "synthseg": + for label, values in SYNTHSEG_LABELS.items(): + print(f"{label}: {values}") + elif name == "neuroquant": + for label, values in NEUROQUANT_LABELS.items(): + print(f"{label}: {values}") + else: + raise ValueError(f"Unknown segmentation labels {name}") + + SYNTHSEG_LABELS = { "BACKGROUND": [0], "LEFT_CEREBRAL_WHITE_MATTER": [2], diff --git a/src/mri2mesh/surface/__init__.py b/src/mri2mesh/surface/__init__.py index cf5ac4a..b7b09d0 100644 --- a/src/mri2mesh/surface/__init__.py +++ b/src/mri2mesh/surface/__init__.py @@ -8,12 +8,18 @@ def add_surface_parser(parser: argparse.ArgumentParser) -> None: - subparsers = parser.add_subparsers(dest="surface-command") + subparsers = parser.add_subparsers( + dest="surface-command", + ) parenchyma_parser = subparsers.add_parser("parenchyma", help="Generate parenchyma surface") parenchyma.add_arguments(parenchyma_parser) - idealized_parser = subparsers.add_parser("idealized", help="Generate idealized surface") + idealized_parser = subparsers.add_parser( + "idealized", + help="Generate idealized surface", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) idealized_brain.add_arguments(idealized_parser) From 3163c758c41ba9bc496dcdf06a8cd6345df802bd Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 13:20:14 +0100 Subject: [PATCH 14/25] Do not specify python version in build docs workflow --- .github/workflows/build_docs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 1c27872..c7a0e15 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -40,7 +40,6 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: - python-version: 3.10 auto-update-conda: false channels: conda-forge,defaults channel-priority: strict From 7b072a2b7b60a3ae4724756281ef1c08ec51539e Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 13:49:15 +0100 Subject: [PATCH 15/25] Try add some pvista deps --- .github/workflows/build_docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index c7a0e15..d78edd0 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -27,6 +27,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install dependencies for pyvista + run: apt-get update && apt-get install -y libgl1-mesa-dev xvfb + - name: Cache conda uses: actions/cache@v3 env: From 67dfdb805eb76437137c44f7a5503a4d8f40fdf3 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 13:51:47 +0100 Subject: [PATCH 16/25] Add sudo --- .github/workflows/build_docs.yml | 2 +- tests/test_cli.py | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/test_cli.py diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index d78edd0..9fc238d 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies for pyvista - run: apt-get update && apt-get install -y libgl1-mesa-dev xvfb + run: sudo apt-get update && sudo apt-get install -y libgl1-mesa-dev xvfb - name: Cache conda uses: actions/cache@v3 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..054a41f --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,37 @@ +import pytest + +from mri2mesh import cli + + +@pytest.mark.parametrize("name", ["synthseg", "neuroquant"]) +def test_labels_cli(name, capsys): + cli.main(["labels", name]) + captured = capsys.readouterr() + # Both labels have Background as the first label + assert captured.out.startswith("BACKGROUND: [0]") + + +def test_mesh_idealized_cli(tmp_path): + cli.main(["mesh", "idealized", "-o", str(tmp_path)]) + for name in [ + "connections_3.npy", + "coords_0.npy", + "coords_1.npy", + "connections_2.npy", + "connections_0.npy", + "coords_3.npy", + "coords_2.npy", + "connections_1.npy", + "mesh.h5", + "ventricles.ply", + "skull.ply", + "tetra_mesh.xdmf", + "mesh.xdmf", + "LV.ply", + "surface_parameters.json", + "tetra_mesh.h5", + "parenchyma_incl_ventr.ply", + "V34.ply", + "mesh_params.json", + ]: + assert (tmp_path / name).exists() From f6608374b8701e32aa80a00767dc59ab9ba94a3e Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 14:01:27 +0100 Subject: [PATCH 17/25] Move back to using docker for docs --- .github/workflows/build_docs.yml | 23 +---------------------- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 9fc238d..c3d3871 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -15,6 +15,7 @@ jobs: build: runs-on: ubuntu-22.04 + container: ghcr.io/fenics/dolfinx/lab:stable env: PUBLISH_DIR: ./_build/html DISPLAY: ":99.0" @@ -27,28 +28,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install dependencies for pyvista - run: sudo apt-get update && sudo apt-get install -y libgl1-mesa-dev xvfb - - - name: Cache conda - uses: actions/cache@v3 - env: - # Increase this value to reset cache if environment.yml has not changed - CACHE_NUMBER: 0 - with: - path: ~/conda_pkgs_dir - key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ - hashFiles('environment.yml') }} - - - uses: conda-incubator/setup-miniconda@v3 - with: - auto-update-conda: false - channels: conda-forge,defaults - channel-priority: strict - activate-environment: mri2mesh - environment-file: environment.yml - miniforge-version: latest - name: Install dependencies run: python3 -m pip install ".[docs]" diff --git a/pyproject.toml b/pyproject.toml index 2918772..761bc3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ mri2mesh = "mri2mesh.cli:main" [project.optional-dependencies] test = ["pytest", "pytest-cov"] mesh = ["wildmeshing", "h5py"] -docs = ["pyvista[jupyter]", "jupyter-book"] +docs = ["pyvista[jupyter]", "jupyter-book", "mri2mesh[mesh]"] From 500eabeac85ed431916ae75fc29acaf030d84a70 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 14:02:19 +0100 Subject: [PATCH 18/25] Add wildmeshing to test dependencies --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 761bc3e..abce634 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,8 @@ content-type = "text/markdown" mri2mesh = "mri2mesh.cli:main" [project.optional-dependencies] -test = ["pytest", "pytest-cov"] mesh = ["wildmeshing", "h5py"] +test = ["pytest", "pytest-cov", "mri2mesh[mesh]"] docs = ["pyvista[jupyter]", "jupyter-book", "mri2mesh[mesh]"] From 02ac3fdbfa9a092bc9ac1afa118cd5cea70a8dfe Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 14:34:32 +0100 Subject: [PATCH 19/25] Do not specify lower bound on fenicsx --- environment.yml | 2 +- examples/cli.ipynb | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index 249007d..fe9c6c9 100644 --- a/environment.yml +++ b/environment.yml @@ -2,7 +2,7 @@ name: mri2mesh channels: - conda-forge dependencies: - - fenics-dolfinx>=0.9.0 + - fenics-dolfinx - numpy - ipython - matplotlib diff --git a/examples/cli.ipynb b/examples/cli.ipynb index ff76eac..4ceb5f2 100644 --- a/examples/cli.ipynb +++ b/examples/cli.ipynb @@ -296,8 +296,6 @@ "from mpi4py import MPI\n", "import dolfinx\n", "\n", - "surface_folder = \"mesh\"\n", - "\n", "with dolfinx.io.XDMFFile(MPI.COMM_WORLD, f\"{surface_folder}/mesh.xdmf\", \"r\") as xdmf:\n", " mesh = xdmf.read_mesh(name=\"Grid\")\n", " cell_tags = xdmf.read_meshtags(mesh, name=\"cell_tags\")\n" From c19dcef0a004980079735127f963aa29aa72f8c6 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 15:06:32 +0100 Subject: [PATCH 20/25] Do not specify miniforge verrsioN --- .github/workflows/test.yml | 4 +--- environment.yml | 2 +- examples/cli.ipynb | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8436734..00f8153 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,11 +35,9 @@ jobs: with: python-version: ${{ matrix.python-version }} auto-update-conda: false - channels: conda-forge,defaults - channel-priority: strict + channels: conda-forge activate-environment: mri2mesh environment-file: environment.yml - miniforge-version: latest - name: Install mri2mesh run: | diff --git a/environment.yml b/environment.yml index fe9c6c9..4463aac 100644 --- a/environment.yml +++ b/environment.yml @@ -2,8 +2,8 @@ name: mri2mesh channels: - conda-forge dependencies: - - fenics-dolfinx - numpy + - fenics-dolfinx - ipython - matplotlib - pyvista diff --git a/examples/cli.ipynb b/examples/cli.ipynb index 4ceb5f2..01d67da 100644 --- a/examples/cli.ipynb +++ b/examples/cli.ipynb @@ -297,7 +297,7 @@ "import dolfinx\n", "\n", "with dolfinx.io.XDMFFile(MPI.COMM_WORLD, f\"{surface_folder}/mesh.xdmf\", \"r\") as xdmf:\n", - " mesh = xdmf.read_mesh(name=\"Grid\")\n", + " mesh = xdmf.read_mesh(name=\"mesh\")\n", " cell_tags = xdmf.read_meshtags(mesh, name=\"cell_tags\")\n" ] }, From 0a239667ca3cbe89bf7ece22b828ed0f35cb6e6d Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 15:51:31 +0100 Subject: [PATCH 21/25] Try to set bash shell --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 00f8153..396b2ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,15 +40,18 @@ jobs: environment-file: environment.yml - name: Install mri2mesh + shell: bash -el {0} run: | python -m pip install -e ".[test]" - name: Test with pytest + shell: bash -el {0} run: | python -m pytest --cov=mri2mesh --cov-report html --cov-report xml --cov-report term-missing -v - name: Coverage report if: matrix.python-version == '3.10' && matrix.os == 'ubuntu-latest' + shell: bash -el {0} run: | python3 -m coverage report | sed 's/^/ /' >> $GITHUB_STEP_SUMMARY python3 -m coverage json From d22e207f12b43a6100b519cb6d943cda36b4e4f3 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Tue, 25 Feb 2025 20:41:48 +0100 Subject: [PATCH 22/25] Try to set som lbgl env variable and add __future__ annotations import --- .github/workflows/build_docs.yml | 7 ++++--- src/mri2mesh/cli.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index c3d3871..1795a5c 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -7,9 +7,11 @@ on: workflow_dispatch: env: - DEB_PYTHON_INSTALL_LAYOUT: deb_system + HDF5_MPI: "ON" + HDF5_DIR: "/usr/local/" DISPLAY: ":99.0" - CI: 1 + DEB_PYTHON_INSTALL_LAYOUT: deb_system + LIBGL_ALWAYS_SOFTWARE: 1 jobs: @@ -18,7 +20,6 @@ jobs: container: ghcr.io/fenics/dolfinx/lab:stable env: PUBLISH_DIR: ./_build/html - DISPLAY: ":99.0" PYVISTA_TRAME_SERVER_PROXY_PREFIX: "/proxy/" PYVISTA_TRAME_SERVER_PROXY_ENABLED: "True" PYVISTA_OFF_SCREEN: false diff --git a/src/mri2mesh/cli.py b/src/mri2mesh/cli.py index fdaa150..272480e 100644 --- a/src/mri2mesh/cli.py +++ b/src/mri2mesh/cli.py @@ -1,3 +1,4 @@ +from __future__ import annotations import logging import argparse from typing import Sequence From 148a482c56a3424439a35c43ce17c9a1b392d598 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Wed, 26 Feb 2025 15:11:29 +0100 Subject: [PATCH 23/25] =?UTF-8?q?Use=20optional=20rather=20thant=20|=C2=A0?= =?UTF-8?q?to=20support=20python3.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mri2mesh/cli.py | 7 +++---- src/mri2mesh/mesh/__init__.py | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mri2mesh/cli.py b/src/mri2mesh/cli.py index 272480e..3de48e8 100644 --- a/src/mri2mesh/cli.py +++ b/src/mri2mesh/cli.py @@ -1,7 +1,6 @@ -from __future__ import annotations import logging import argparse -from typing import Sequence +from typing import Sequence, Optional from . import viz, surface, mesh, segmentation_labels @@ -47,7 +46,7 @@ def _disable_loggers(): logging.getLogger(libname).setLevel(logging.WARNING) -def dispatch(parser: argparse.ArgumentParser, argv: Sequence[str] | None = None) -> int: +def dispatch(parser: argparse.ArgumentParser, argv: Optional[Sequence[str]] = None) -> int: args = vars(parser.parse_args(argv)) logging.basicConfig(level=logging.DEBUG if args.pop("verbose") else logging.INFO) _disable_loggers() @@ -80,6 +79,6 @@ def dispatch(parser: argparse.ArgumentParser, argv: Sequence[str] | None = None) return 0 -def main(argv: Sequence[str] | None = None) -> int: +def main(argv: Optional[Sequence[str]] = None) -> int: parser = setup_parser() return dispatch(parser, argv) diff --git a/src/mri2mesh/mesh/__init__.py b/src/mri2mesh/mesh/__init__.py index efcdbc5..9650ae2 100644 --- a/src/mri2mesh/mesh/__init__.py +++ b/src/mri2mesh/mesh/__init__.py @@ -4,6 +4,9 @@ from pathlib import Path from . import basic, idealized_brain +from .basic import create_mesh + +__all__ = ["basic", "idealized_brain", "create_mesh"] logger = logging.getLogger(__name__) From 5e26a4b14e9c5aaa8732afed4c09c341255ef916 Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Wed, 26 Feb 2025 16:05:28 +0100 Subject: [PATCH 24/25] Try to add some more dependencies for pyvista --- .github/workflows/build_docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 1795a5c..c8bc78d 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -29,6 +29,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install dependencies for pyvista + run: apt-get update && apt-get install -y libxrender1 xvfb - name: Install dependencies run: python3 -m pip install ".[docs]" From 2f930faca362d15d645b6021418c725de08cbb6c Mon Sep 17 00:00:00 2001 From: Henrik Finsberg Date: Thu, 27 Feb 2025 09:50:45 +0100 Subject: [PATCH 25/25] Try to add start_xvfb --- examples/cli.ipynb | 1 + examples/synthseg.ipynb | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/cli.ipynb b/examples/cli.ipynb index 01d67da..29222d9 100644 --- a/examples/cli.ipynb +++ b/examples/cli.ipynb @@ -310,6 +310,7 @@ "source": [ "import pyvista as pv \n", "\n", + "pv.start_xvfb()\n", "vtk_mesh = dolfinx.plot.vtk_mesh(mesh, cell_tags.dim, cell_tags.indices)\n", "bgrid = pv.UnstructuredGrid(*vtk_mesh)\n", "bgrid.cell_data[\"Cell tags\"] = cell_tags.values\n", diff --git a/examples/synthseg.ipynb b/examples/synthseg.ipynb index e1db723..6fd3591 100644 --- a/examples/synthseg.ipynb +++ b/examples/synthseg.ipynb @@ -50,6 +50,7 @@ "metadata": {}, "outputs": [], "source": [ + "pv.start_xvfb()\n", "# Path to the Nifty file\n", "outdir = Path(\"results-synthseg\")\n", "outdir.mkdir(exist_ok=True)\n",