diff --git a/.binder/requirements.txt b/.binder/requirements.txt index 47b6bddb..bc99a70d 100644 --- a/.binder/requirements.txt +++ b/.binder/requirements.txt @@ -1,4 +1,4 @@ -itk-elastix>=0.12.0 +itk-elastix>=0.17.1 itkwidgets>=0.32.0 jupyterlab>=2.2.0 imageio diff --git a/examples/ITK_Example09_PointSetAndMaskTransformation.ipynb b/examples/ITK_Example09_PointSetAndMaskTransformation.ipynb index 320d6078..6fb1058b 100644 --- a/examples/ITK_Example09_PointSetAndMaskTransformation.ipynb +++ b/examples/ITK_Example09_PointSetAndMaskTransformation.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -8,6 +9,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -19,6 +21,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -57,6 +60,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -65,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -77,6 +81,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -85,25 +90,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# Import Mask\n", - "moving_mask = itk.imread('data/CT_3D_lung_moving_mask.mha', itk.F)" + "# Note: Version v0.17.0 and onwards supports various image types for elastix and transformix functions/objects\n", + "moving_mask = itk.imread('data/CT_3D_lung_moving_mask.mha', itk.UC)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# Mask image is a binary image, therefore the FinalBSplineInterpolationOrder should be 0\n", - "result_transform_parameters.SetParameter('FinalBSplineInterpolationOrder','0')\n" + "result_transform_parameters.SetParameter('FinalBSplineInterpolationOrder','0')" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -112,7 +119,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -123,6 +130,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -131,7 +139,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -148,6 +156,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -155,6 +164,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -163,7 +173,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -180,12 +190,12 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "# Import groundtruth segmentation\n", - "fixed_mask = itk.imread('data/CT_3D_lung_fixed_mask.mha', itk.F)\n", + "fixed_mask = itk.imread('data/CT_3D_lung_fixed_mask.mha', itk.UC)\n", "\n", "# Cast itk images to numpy arrays and round result image to boolean array.\n", "fixed_mask_np = np.asarray(fixed_mask)\n", @@ -194,23 +204,47 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dice loss: 0.9862246731348647\n" + ] + } + ], "source": [ "print(\"Dice loss:\",dice_loss(fixed_mask_np, result_mask_np))" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c76e48b027c84e37963cf1e02ee8a702", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "AppLayout(children=(HBox(children=(Label(value='Link:'), Checkbox(value=False, description='cmap'), Checkbox(v…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "compare(fixed_mask, result_moving_mask)" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -219,7 +253,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -231,6 +265,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -239,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -262,9 +297,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3a044dd051e04f2f92d3dae77639590e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Viewer(geometries=[], gradient_opacity=0.22, point_set_colors=array([[0.8392157, 0. , 0. ]], dtype…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "view(moving_image, point_sets=[result_point_set])" ] @@ -272,7 +322,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -286,7 +336,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.9.0" } }, "nbformat": 4, diff --git a/examples/ITK_Example20_PixelTypes.ipynb b/examples/ITK_Example20_PixelTypes.ipynb new file mode 100644 index 00000000..f1346fa8 --- /dev/null +++ b/examples/ITK_Example20_PixelTypes.ipynb @@ -0,0 +1,325 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "ae01d8cd", + "metadata": {}, + "source": [ + "# Input, output and internal pixel types" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c0e4e8c1", + "metadata": {}, + "source": [ + "itk-elastix `v0.17.0` and higher supports various types for the input images, while the output image type is defined to match the inputs. That way the users do not need to pay special attention to convert their images before using elastix or transformix" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "32d8a7a1", + "metadata": {}, + "outputs": [], + "source": [ + "import itk\n", + "import numpy as np" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1ef001b8", + "metadata": {}, + "source": [ + "### Elastix pixel types" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b77be03e", + "metadata": {}, + "source": [ + "In the following example, we create itk images from corresponding numpy arrays of different data types, and demonstrate that elastix is able to register them. More details on the inner workings of elastix are explained in the final section of the current notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "59e15e87", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input images type Output image type Corresponding numpy dtype\n", + " \n", + " \n", + " \n", + " \n", + " \n" + ] + } + ], + "source": [ + "# Define a rigid parameter object\n", + "parameter_object = itk.ParameterObject.New()\n", + "default_rigid_parameter_map = parameter_object.GetDefaultParameterMap('rigid')\n", + "parameter_object.AddParameterMap(default_rigid_parameter_map)\n", + "\n", + "# Non-exhaustive list of image types supported by itk-elastix\n", + "np_dtypes = [np.uint8, np.int16, np.uint16, np.float32, np.double]\n", + "\n", + "# Function to create a random 3D itk image\n", + "def create_random_image(dtype):\n", + " arr = (100 * np.random.rand(64, 64, 64)).astype(dtype)\n", + " return itk.image_from_array(arr)\n", + "\n", + "# Loop over different dtypes\n", + "indent = 30\n", + "print(\"Input images type\".ljust(indent) + \"Output image type\".ljust(indent) + \"Corresponding numpy dtype\")\n", + "for dtype in np_dtypes:\n", + " image = create_random_image(dtype)\n", + " result_image, result_transform_parameters = itk.elastix_registration_method(\n", + " image, image,\n", + " parameter_object=parameter_object,\n", + " log_to_console=False)\n", + " \n", + " print(f\"{itk.template(image)[1][0]}\".ljust(indent) + f\"{itk.template(result_image)[1][0]}\".ljust(indent) + f\"{dtype}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1cb169fe", + "metadata": {}, + "source": [ + "### Transformix pixel types" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7f67cfa3", + "metadata": {}, + "source": [ + "The previous example used elastix, but the same functionality works also for transformix" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0548b67e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Input images type Output image type Corresponding numpy dtype\n", + " \n", + " \n", + " \n", + " \n", + " \n" + ] + } + ], + "source": [ + "print(\"Input images type\".ljust(indent) + \"Output image type\".ljust(indent) + \"Corresponding numpy dtype\")\n", + "for dtype in np_dtypes:\n", + " image = create_random_image(dtype)\n", + " transformed_image = itk.transformix_filter(\n", + " image,\n", + " transform_parameter_object=result_transform_parameters,\n", + " log_to_console=False)\n", + " \n", + " print(f\"{itk.template(image)[1][0]}\".ljust(indent) + f\"{itk.template(transformed_image)[1][0]}\".ljust(indent) + f\"{dtype}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6935081a", + "metadata": {}, + "source": [ + "### Internal pixel types" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "59c0389f", + "metadata": {}, + "source": [ + "Under the hood, elastix/transformix will [cast](https://itk.org/Doxygen/html/classitk_1_1CastImageFilter.html) the pixels of the input image(s) to an internal pixel type. By default, the internal pixel type is `float`, but for 3D and 4D images, it can be `short`, according to the value of the parameters `FixedInternalPixelType` and `MovingInternalPixelType` specified in the parameter object. When the registration or transformation is finished, the image will be converted back (cast) to original pixel type before it is output to the user." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "62864e1b", + "metadata": {}, + "source": [ + "Let's do a simple test to show that actually choosing `short` as internal pixel type will truncate the floating values of the pixels:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "69c5eeb5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ParameterObject (000002CA822664B0)\n", + " RTTI typeinfo: class elastix::ParameterObject\n", + " Reference Count: 1\n", + " Modified Time: 173699\n", + " Debug: Off\n", + " Object Name: \n", + " Observers: \n", + " none\n", + "ParameterMap 0: \n", + " (CenterOfRotationPoint 31.5 31.5 31.5)\n", + " (CompressResultImage \"false\")\n", + " (ComputeZYX \"false\")\n", + " (DefaultPixelValue 0)\n", + " (Direction 1 0 0 0 1 0 0 0 1)\n", + " (FinalBSplineInterpolationOrder 1)\n", + " (FixedImageDimension 3)\n", + " (FixedInternalImagePixelType \"float\")\n", + " (HowToCombineTransforms \"Compose\")\n", + " (Index 0 0 0)\n", + " (InitialTransformParametersFileName \"NoInitialTransform\")\n", + " (MovingImageDimension 3)\n", + " (MovingInternalImagePixelType \"float\")\n", + " (NumberOfParameters 6)\n", + " (Origin 0 0 0)\n", + " (ResampleInterpolator \"FinalBSplineInterpolator\")\n", + " (Resampler \"DefaultResampler\")\n", + " (ResultImageFormat \"nii\")\n", + " (ResultImagePixelType \"double\")\n", + " (Size 64 64 64)\n", + " (Spacing 1 1 1)\n", + " (Transform \"EulerTransform\")\n", + " (TransformParameters 0 0 0 0 0 0)\n", + " (UseDirectionCosines \"true\")\n", + "\n" + ] + } + ], + "source": [ + "# Copy the previous transformation result to two new parameter objects\n", + "transform_parameters_float = itk.ParameterObject.New()\n", + "transform_parameters_short = itk.ParameterObject.New()\n", + "\n", + "transform_parameters_float.AddParameterMap(result_transform_parameters.GetParameterMap(0))\n", + "transform_parameters_short.AddParameterMap(result_transform_parameters.GetParameterMap(0))\n", + "\n", + "# Switch to linear interpolation for resampling\n", + "transform_parameters_float.SetParameter(0, 'FinalBSplineInterpolationOrder', '1')\n", + "transform_parameters_short.SetParameter(0, 'FinalBSplineInterpolationOrder', '1')\n", + "\n", + "# Set transform to zero so that the input image matches the output image\n", + "transform_parameters_float.SetParameter(0, 'TransformParameters', 6 * ['0']) # 6 transformation parameters to zero\n", + "transform_parameters_short.SetParameter(0, 'TransformParameters', 6 * ['0'])\n", + "\n", + "print(transform_parameters_float)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "7d610c09", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "For float internal pixel type - Diff min: 0.0 | max: 0.0\n", + "For short internal pixel type - Diff min: -0.9999923706054688 | max: -6.811788807681296e-06\n" + ] + } + ], + "source": [ + "# Create the input image as float\n", + "image = create_random_image(np.float32)\n", + "\n", + "# Transform the image using 'float' internal pixel type\n", + "transformed_image_float = itk.transformix_filter(image,\n", + " transform_parameter_object=transform_parameters_float,\n", + " )\n", + "\n", + "# Transform the image using 'short' internal pixel type\n", + "transform_parameters_short.SetParameter(0, 'FixedInternalImagePixelType', 'short')\n", + "transform_parameters_short.SetParameter(0, 'MovingInternalImagePixelType', 'short')\n", + "transformed_image_short = itk.transformix_filter(image,\n", + " transform_parameter_object=transform_parameters_short,\n", + " )\n", + "\n", + "# Compute the differences between the result and the input image\n", + "diff_float = transformed_image_float[:] - image[:]\n", + "diff_short = transformed_image_short[:] - image[:]\n", + "\n", + "print(f\"For float internal pixel type - Diff min: {diff_float.min()} | max: {diff_float.max()}\")\n", + "print(f\"For short internal pixel type - Diff min: {diff_short.min()} | max: {diff_short.max()}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d40cf32a", + "metadata": {}, + "source": [ + "We observe that difference between input and output images is zero. On the other hand, for the case of `short` internal type we see that there is a difference that corresponds to the rounding error (truncation of the floating part). " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f675ed9e", + "metadata": {}, + "source": [ + "Note: Since specifying the internal pixel type to short is meant as a way to reduce the memory requirements for the registration, elastix supports it by default only for `>=3D` images. There is a CMake option to enable support for `short` with `2D` images when building elastix on your own." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81afe524", + "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.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}