From 5a1479a4dfd14daf4c894dcf8dcb0964b9802e53 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Mon, 29 Jan 2024 13:11:34 -0500 Subject: [PATCH 1/3] DOC: Use monai stable instead of monai-weekly in example --- environment.yml | 2 +- examples/EnvironmentCheck.ipynb | 2 +- .../MONAI/transform_visualization.ipynb | 137 ++++++++---------- 3 files changed, 60 insertions(+), 81 deletions(-) diff --git a/environment.yml b/environment.yml index d50e29c1..4655eab5 100644 --- a/environment.yml +++ b/environment.yml @@ -12,7 +12,7 @@ dependencies: - imjoy-elfinder - imjoy-jupyter-extension - imjoy-jupyterlab-extension - - monai-weekly[nibabel, matplotlib, tqdm] + - monai[nibabel, matplotlib, tqdm] - imageio - pyvista - dask[diagnostics] diff --git a/examples/EnvironmentCheck.ipynb b/examples/EnvironmentCheck.ipynb index 4b1434c4..6c325fc2 100644 --- a/examples/EnvironmentCheck.ipynb +++ b/examples/EnvironmentCheck.ipynb @@ -111,7 +111,7 @@ " \"imjoy-jupyter-extension\",\n", " \"imjoy-jupyterlab-extension\",\n", " \"itk\",\n", - " \"monai-weekly[nibabel, matplotlib, tqdm]\",\n", + " \"monai[nibabel, matplotlib, tqdm]\",\n", " \"imageio\",\n", " \"pyvista\",\n", " \"dask[diagnostics]\",\n", diff --git a/examples/integrations/MONAI/transform_visualization.ipynb b/examples/integrations/MONAI/transform_visualization.ipynb index f485c70a..fbeb8f49 100644 --- a/examples/integrations/MONAI/transform_visualization.ipynb +++ b/examples/integrations/MONAI/transform_visualization.ipynb @@ -16,30 +16,25 @@ "cell_type": "code", "execution_count": 1, "id": "ffa61cef-5f69-49e0-ba91-58d7bc1d517d", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# Install dependencies for this example\n", "import sys\n", "\n", - "!{sys.executable} -m pip install -q \"monai-weekly[nibabel, matplotlib, tqdm]\" \"itkwidgets[all]>=1.0a23\"" + "!{sys.executable} -m pip install -q \"monai[nibabel, matplotlib, tqdm]\" \"itkwidgets[all]>=1.0a47\"" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "bc209052-ce21-421f-82e8-36760679a764", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/matt/bin/mambaforge/envs/itkwidgets-ng/lib/python3.10/site-packages/tqdm/auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - } - ], + "metadata": { + "tags": [] + }, + "outputs": [], "source": [ "# Copyright 2020 MONAI Consortium\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", @@ -73,15 +68,17 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "205efb9f-f41b-4df2-8c48-5636b637856b", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/tmp/tmpngse4imf\n" + "/home/matt/data/monai\n" ] } ], @@ -93,40 +90,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "53005634-ea45-45ed-8bcd-594ed3d861ee", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Task09_Spleen.tar: 1.50GB [05:19, 5.04MB/s] " - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2022-10-20 09:12:38,912 - INFO - Downloaded: /tmp/tmpngse4imf/Task09_Spleen.tar\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2022-10-20 09:12:40,657 - INFO - Verified 'Task09_Spleen.tar', md5: 410d4a301da4e5b2f6f86ec3ddba524e.\n", - "2022-10-20 09:12:40,657 - INFO - Writing into directory: /tmp/tmpngse4imf.\n" - ] - } - ], + "metadata": { + "tags": [] + }, + "outputs": [], "source": [ "resource = \"https://msd-for-monai.s3-us-west-2.amazonaws.com/Task09_Spleen.tar\"\n", "md5 = \"410d4a301da4e5b2f6f86ec3ddba524e\"\n", @@ -139,9 +108,11 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "d3bf04a0-dd98-4129-b753-4cc55a800ab4", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "train_images = sorted(glob.glob(os.path.join(data_dir, \"imagesTr\", \"*.nii.gz\")))\n", @@ -154,9 +125,11 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "8ebecf84-dd46-4326-9830-e071c3435f02", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "set_determinism(seed=0)" @@ -164,10 +137,20 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "a70eef61-c7a5-49f6-83d5-174b1044656d", - "metadata": {}, - "outputs": [], + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "monai.transforms.croppad.dictionary CropForegroundd.__init__:allow_smaller: Current default value of argument `allow_smaller=True` has been deprecated since version 1.2. It will be changed to `allow_smaller=False` in version 1.5.\n" + ] + } + ], "source": [ "transform = Compose([\n", " LoadImaged(keys=[\"image\", \"label\"]),\n", @@ -182,15 +165,17 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "7a22527f-f68c-44ee-98e1-546f36f64606", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "image shape: (1, 1, 282, 329, 136), label shape: (1, 1, 282, 329, 136)\n" + "image shape: torch.Size([1, 1, 282, 329, 136]), label shape: torch.Size([1, 1, 282, 329, 136])\n" ] } ], @@ -203,19 +188,21 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "id": "7806b959-160b-41c8-9aa7-e3ab62f2cd27", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "text/html": [ "\n", - " \n", + " \n", " \n", @@ -231,7 +218,7 @@ { "data": { "application/javascript": [ - "window.connectPlugin && window.connectPlugin(\"0fed3519-4123-4985-b08f-76cc06c49284\")" + "window.connectPlugin && window.connectPlugin(\"5558e519-3c3b-4cd3-a867-33f8c1e004f3\")" ], "text/plain": [ "" @@ -243,7 +230,7 @@ { "data": { "text/html": [ - "
" + "
" ], "text/plain": [ "" @@ -255,10 +242,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 9, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -266,14 +253,6 @@ "source": [ "view(image=data[\"image\"][0, 0, :, :, :] * 255, gradient_opacity=0.4)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "278dc2c6-9fcf-4d83-9894-4401e7348d34", - "metadata": { "tags": ["skip-execution"] }, - "outputs": [], - "source": [] } ], "metadata": { @@ -292,7 +271,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.4" } }, "nbformat": 4, From 68ea8b489a2ab751e1b90fbee1a7c7beed44a762 Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Mon, 29 Jan 2024 15:17:32 -0500 Subject: [PATCH 2/3] PERF: use importlib_metadata instead of imports for package detection --- itkwidgets/integrations/itk.py | 12 +++++++----- itkwidgets/integrations/pytorch.py | 6 ++++-- itkwidgets/integrations/vtk.py | 11 +++++++---- itkwidgets/integrations/xarray.py | 10 ++++++---- pyproject.toml | 1 + 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/itkwidgets/integrations/itk.py b/itkwidgets/integrations/itk.py index 183f3898..58563c0f 100644 --- a/itkwidgets/integrations/itk.py +++ b/itkwidgets/integrations/itk.py @@ -1,18 +1,20 @@ import itkwasm from packaging import version +import importlib_metadata HAVE_ITK = False try: - import itk - if not hasattr(itk, '__version__') or version.parse(itk.__version__) < version.parse('5.3.0'): - raise RuntimeError('itk 5.3 or newer is required. `pip install itk>=5.3.0`') + itk_version = importlib_metadata.version('itk') + if version.parse(itk_version) < version.parse('5.3.0'): + raise RuntimeError('itk 5.3 or newer is required. `pip install itk>=5.3.0`') HAVE_ITK = True -except ImportError: +except importlib_metadata.PackageNotFoundError: pass - + if HAVE_ITK: def itk_group_spatial_object_to_wasm_point_set(point_set): + import itk point_set_dict = itk.dict_from_pointset(point_set) wasm_point_set = itkwasm.PointSet(**point_set_dict) return wasm_point_set diff --git a/itkwidgets/integrations/pytorch.py b/itkwidgets/integrations/pytorch.py index 8700e73e..d83250bc 100644 --- a/itkwidgets/integrations/pytorch.py +++ b/itkwidgets/integrations/pytorch.py @@ -1,6 +1,8 @@ +import importlib_metadata + HAVE_TORCH = False try: - import torch + importlib_metadata.metadata("torch") HAVE_TORCH = True -except ImportError: +except importlib_metadata.PackageNotFoundError: pass diff --git a/itkwidgets/integrations/vtk.py b/itkwidgets/integrations/vtk.py index ee893e30..2e380046 100644 --- a/itkwidgets/integrations/vtk.py +++ b/itkwidgets/integrations/vtk.py @@ -1,15 +1,17 @@ +import importlib_metadata + HAVE_VTK = False try: - import vtk + importlib_metadata.metadata("vtk") HAVE_VTK = True - from vtk.util.numpy_support import vtk_to_numpy -except ImportError: +except importlib_metadata.PackageNotFoundError: pass -from ngff_zarr import ngff_image, to_ngff_image +from ngff_zarr import to_ngff_image def vtk_image_to_ngff_image(image): + from vtk.util.numpy_support import vtk_to_numpy array = vtk_to_numpy(image.GetPointData().GetScalars()) dimensions = list(image.GetDimensions()) array.shape = dimensions[::-1] @@ -25,5 +27,6 @@ def vtk_image_to_ngff_image(image): return ngff_image def vtk_polydata_to_vtkjs(point_set): + from vtk.util.numpy_support import vtk_to_numpy array = vtk_to_numpy(point_set.GetPoints().GetData()) return array diff --git a/itkwidgets/integrations/xarray.py b/itkwidgets/integrations/xarray.py index 6548fd48..e57fe2ad 100644 --- a/itkwidgets/integrations/xarray.py +++ b/itkwidgets/integrations/xarray.py @@ -1,15 +1,17 @@ +import importlib_metadata + HAVE_XARRAY = False try: - import xarray + importlib_metadata.metadata("xarray") HAVE_XARRAY = True -except ImportError: +except importlib_metadata.PackageNotFoundError: pass HAVE_MULTISCALE_SPATIAL_IMAGE = False try: - import multiscale_spatial_image + importlib_metadata.metadata("multiscale-spatial-image") HAVE_MULTISCALE_SPATIAL_IMAGE = True -except ImportError: +except importlib_metadata.PackageNotFoundError: pass def xarray_data_array_to_numpy(data_array): diff --git a/pyproject.toml b/pyproject.toml index 4b38330c..a9b9d402 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "itkwasm >= 1.0b.78", "imjoy-rpc >= 0.5.42", "imjoy-utils >= 0.1.2", + "importlib_metadata", "ngff-zarr[dask-image] >= 0.4.3", "numcodecs", "zarr", From c21d5bd84d731be2b4b2f9b9e05e2b6840ec975c Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Mon, 29 Jan 2024 17:02:03 -0500 Subject: [PATCH 3/3] ENH: Add monai MetaTensor support Applies the .affine matrix. --- .../MONAI/transform_visualization.ipynb | 175 ++++++++++++++---- itkwidgets/integrations/__init__.py | 10 + itkwidgets/integrations/monai.py | 8 + 3 files changed, 158 insertions(+), 35 deletions(-) diff --git a/examples/integrations/MONAI/transform_visualization.ipynb b/examples/integrations/MONAI/transform_visualization.ipynb index fbeb8f49..9c9dcf23 100644 --- a/examples/integrations/MONAI/transform_visualization.ipynb +++ b/examples/integrations/MONAI/transform_visualization.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "ffa61cef-5f69-49e0-ba91-58d7bc1d517d", "metadata": { "tags": [] @@ -24,12 +24,12 @@ "# Install dependencies for this example\n", "import sys\n", "\n", - "!{sys.executable} -m pip install -q \"monai[nibabel, matplotlib, tqdm]\" \"itkwidgets[all]>=1.0a47\"" + "!{sys.executable} -m pip install -q \"monai[nibabel,itk,matplotlib,tqdm]\" \"itkwidgets[all]>=1.0a47\"" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "bc209052-ce21-421f-82e8-36760679a764", "metadata": { "tags": [] @@ -68,20 +68,12 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "205efb9f-f41b-4df2-8c48-5636b637856b", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/home/matt/data/monai\n" - ] - } - ], + "outputs": [], "source": [ "directory = os.environ.get(\"MONAI_DATA_DIRECTORY\")\n", "root_dir = tempfile.mkdtemp() if directory is None else directory\n", @@ -137,20 +129,12 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "a70eef61-c7a5-49f6-83d5-174b1044656d", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "monai.transforms.croppad.dictionary CropForegroundd.__init__:allow_smaller: Current default value of argument `allow_smaller=True` has been deprecated since version 1.2. It will be changed to `allow_smaller=False` in version 1.5.\n" - ] - } - ], + "outputs": [], "source": [ "transform = Compose([\n", " LoadImaged(keys=[\"image\", \"label\"]),\n", @@ -175,20 +159,71 @@ "name": "stdout", "output_type": "stream", "text": [ - "image shape: torch.Size([1, 1, 282, 329, 136]), label shape: torch.Size([1, 1, 282, 329, 136])\n" + "image shape: torch.Size([1, 282, 329, 136]), label shape: torch.Size([1, 282, 329, 136])\n", + "image affine: tensor([[ 0.0000, -1.5000, 0.0000, 0.4768],\n", + " [ -1.5000, 0.0000, 0.0000, -64.0232],\n", + " [ 0.0000, 0.0000, 2.0000, 0.0000],\n", + " [ 0.0000, 0.0000, 0.0000, 1.0000]], dtype=torch.float64),\n", + " label shape: tensor([[ 0.0000, -1.5000, 0.0000, 0.4768],\n", + " [ -1.5000, 0.0000, 0.0000, -64.0232],\n", + " [ 0.0000, 0.0000, 2.0000, 0.0000],\n", + " [ 0.0000, 0.0000, 0.0000, 1.0000]], dtype=torch.float64)\n" ] } ], "source": [ "check_ds = Dataset(data=data_dicts, transform=transform)\n", "check_loader = DataLoader(check_ds, batch_size=1)\n", - "data = first(check_loader)\n", - "print(f\"image shape: {data['image'].shape}, label shape: {data['label'].shape}\")" + "\n", + "data = first(check_ds)\n", + "print(f\"image shape: {data['image'].shape}, label shape: {data['label'].shape}\")\n", + "print(f\"image affine: {data['image'].affine},\\n label shape: {data['label'].affine}\")" + ] + }, + { + "cell_type": "markdown", + "id": "52ee36a1-a8e6-4c5a-93b4-4c11c66ae845", + "metadata": {}, + "source": [ + "The data in the `Dataset` is a `monai` `MetaTensor`, which is a `torch.Tensor` with an additional `.meta` metadata attribute and `.affine` spatial affine matrix.\n", + "\n", + "`itkwidgets` understands this metadata." ] }, { "cell_type": "code", "execution_count": 8, + "id": "df0fbfcd-3936-48fb-8edf-07cafa02095c", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "image shape: torch.Size([1, 282, 329, 136]), label shape: torch.Size([1, 282, 329, 136])\n", + "image affine: tensor([[ 0.0000, -1.5000, 0.0000, 0.4768],\n", + " [ -1.5000, 0.0000, 0.0000, -64.0232],\n", + " [ 0.0000, 0.0000, 2.0000, 0.0000],\n", + " [ 0.0000, 0.0000, 0.0000, 1.0000]], dtype=torch.float64),\n", + "label shape: tensor([[ 0.0000, -1.5000, 0.0000, 0.4768],\n", + " [ -1.5000, 0.0000, 0.0000, -64.0232],\n", + " [ 0.0000, 0.0000, 2.0000, 0.0000],\n", + " [ 0.0000, 0.0000, 0.0000, 1.0000]], dtype=torch.float64)\n" + ] + } + ], + "source": [ + "print(type(data['image']))\n", + "print(f\"image shape: {data['image'].shape}, label shape: {data['label'].shape}\")\n", + "print(f\"image affine: {data['image'].affine},\\nlabel shape: {data['label'].affine}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, "id": "7806b959-160b-41c8-9aa7-e3ab62f2cd27", "metadata": { "tags": [] @@ -198,11 +233,81 @@ "data": { "text/html": [ "\n", - " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "window.connectPlugin && window.connectPlugin(\"7c270698-57a8-4229-a2ce-eda8392f5af0\")" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "view(image=data[\"image\"] * 255, gradient_opacity=0.4)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9412a4e2-4702-432d-a1a9-212a5be51a85", + "metadata": { + "tags": [ + "skip-execution" + ] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", " \n", @@ -218,7 +323,7 @@ { "data": { "application/javascript": [ - "window.connectPlugin && window.connectPlugin(\"5558e519-3c3b-4cd3-a867-33f8c1e004f3\")" + "window.connectPlugin && window.connectPlugin(\"7c270698-57a8-4229-a2ce-eda8392f5af0\")" ], "text/plain": [ "" @@ -230,7 +335,7 @@ { "data": { "text/html": [ - "
" + "
" ], "text/plain": [ "" @@ -242,16 +347,16 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "view(image=data[\"image\"][0, 0, :, :, :] * 255, gradient_opacity=0.4)" + "view(image=data[\"image\"] * 255, label_image=data['label'], gradient_opacity=0.4)" ] } ], diff --git a/itkwidgets/integrations/__init__.py b/itkwidgets/integrations/__init__.py index c3c23914..8baa204b 100644 --- a/itkwidgets/integrations/__init__.py +++ b/itkwidgets/integrations/__init__.py @@ -6,6 +6,7 @@ import dask from .itk import HAVE_ITK from .pytorch import HAVE_TORCH +from .monai import HAVE_MONAI from .vtk import HAVE_VTK, vtk_image_to_ngff_image, vtk_polydata_to_vtkjs from .xarray import HAVE_XARRAY, HAVE_MULTISCALE_SPATIAL_IMAGE, xarray_data_array_to_numpy, xarray_data_set_to_numpy from ..render_types import RenderType @@ -102,6 +103,15 @@ def _get_viewer_image(image, label=False): to_ngff_zarr(store, multiscales, chunk_store=chunk_store) return store + if HAVE_MONAI: + from monai.data import MetaTensor, metatensor_to_itk_image + if isinstance(image, MetaTensor): + itk_image = metatensor_to_itk_image(image) + ngff_image = itk_image_to_ngff_image(itk_image) + multiscales = to_multiscales(ngff_image, method=method) + to_ngff_zarr(store, multiscales, chunk_store=chunk_store) + return store + if HAVE_TORCH: import torch if isinstance(image, torch.Tensor): diff --git a/itkwidgets/integrations/monai.py b/itkwidgets/integrations/monai.py index e69de29b..92a2ac4f 100644 --- a/itkwidgets/integrations/monai.py +++ b/itkwidgets/integrations/monai.py @@ -0,0 +1,8 @@ +import importlib_metadata + +HAVE_MONAI = False +try: + importlib_metadata.metadata("monai") + HAVE_MONAI = True +except importlib_metadata.PackageNotFoundError: + pass