From 2fd7d6d143a6153b95a2aee10e0fca624cf29de7 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot <17726042+ericspod@users.noreply.github.com> Date: Wed, 22 Jan 2020 14:10:43 +0000 Subject: [PATCH] 2 nifti stream and patch transforms (#28) * initial unit tests for 2d/3d unet * adding license info * Adding definitions for reading Nifti files in streams and stream transforms for selecting patches (windowing) * Remove tests * Renaming fix * Update arrayutils.py * Removed blank lines in comments * Added Dataset based Nifti reader, grid patch sampler, and transforms * Added example segmentation notebook * Cleanup deletion * Update cardiac_segmentation.ipynb Co-authored-by: Wenqi Li --- examples/cardiac_segmentation.ipynb | 252 -------------- examples/nifti_read_example.ipynb | 233 +++++++++++++ examples/unet_segmentation_3d.ipynb | 241 +++++++++++++ monai/data/README.md | 48 +-- monai/data/augments/__init__.py | 10 - monai/data/augments/augments.py | 239 ------------- monai/data/augments/augmentstream.py | 65 ---- monai/data/augments/decorators.py | 87 ----- monai/data/readers/arrayreader.py | 115 ------- monai/data/readers/niftireader.py | 100 ++++++ monai/data/readers/npzreader.py | 41 --- monai/data/streams/__init__.py | 10 - monai/data/streams/datastream.py | 324 ------------------ monai/data/streams/threadbufferstream.py | 71 ---- monai/data/transforms/dataset_transforms.py | 83 +++++ monai/data/transforms/grid_dataset.py | 66 ++++ monai/data/transforms/image_props.py | 26 -- monai/data/transforms/image_reader.py | 50 --- .../transforms/multi_format_transformer.py | 66 ---- monai/data/transforms/nifti_reader.py | 170 --------- monai/data/transforms/nifti_writer.py | 80 ----- monai/data/transforms/noise_adder.py | 27 -- monai/data/transforms/shape_format.py | 45 --- monai/utils/arrayutils.py | 127 ++++++- 24 files changed, 844 insertions(+), 1732 deletions(-) delete mode 100644 examples/cardiac_segmentation.ipynb create mode 100644 examples/nifti_read_example.ipynb create mode 100644 examples/unet_segmentation_3d.ipynb delete mode 100644 monai/data/augments/__init__.py delete mode 100644 monai/data/augments/augments.py delete mode 100644 monai/data/augments/augmentstream.py delete mode 100644 monai/data/augments/decorators.py delete mode 100644 monai/data/readers/arrayreader.py create mode 100644 monai/data/readers/niftireader.py delete mode 100644 monai/data/readers/npzreader.py delete mode 100644 monai/data/streams/__init__.py delete mode 100644 monai/data/streams/datastream.py delete mode 100644 monai/data/streams/threadbufferstream.py create mode 100644 monai/data/transforms/dataset_transforms.py create mode 100644 monai/data/transforms/grid_dataset.py delete mode 100644 monai/data/transforms/image_props.py delete mode 100644 monai/data/transforms/image_reader.py delete mode 100644 monai/data/transforms/multi_format_transformer.py delete mode 100644 monai/data/transforms/nifti_reader.py delete mode 100644 monai/data/transforms/nifti_writer.py delete mode 100644 monai/data/transforms/noise_adder.py delete mode 100644 monai/data/transforms/shape_format.py diff --git a/examples/cardiac_segmentation.ipynb b/examples/cardiac_segmentation.ipynb deleted file mode 100644 index 112a661fb4..0000000000 --- a/examples/cardiac_segmentation.ipynb +++ /dev/null @@ -1,252 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MONAI version: 0.0.1\n", - "Python version: 3.6.9 |Anaconda, Inc.| (default, Jul 30 2019, 19:07:31) [GCC 7.3.0]\n", - "Numpy version: 1.18.0\n", - "Pytorch version: 1.3.1\n", - "Ignite version: 0.2.1\n" - ] - } - ], - "source": [ - "%matplotlib inline\n", - "\n", - "import os, sys\n", - "from functools import partial\n", - "\n", - "import torch\n", - "import torch.nn as nn\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "from ignite.engine import Events, create_supervised_trainer\n", - "\n", - "# assumes the framework is found here, change as necessary\n", - "sys.path.append(\"..\")\n", - "\n", - "from monai import application, data, networks, utils\n", - "import monai.data.augments.augments as augments\n", - "\n", - "application.config.print_config()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Download the downsampled segmented images from the Sunnybrook Cardiac Dataset. This is a simple low-res dataset I put together for a workshop. The task is to segment the left ventricle in the image which shows up as an annulus. " - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "! [ ! -f scd_lvsegs.npz ] && wget -q https://github.com/ericspod/VPHSummerSchool2019/raw/master/scd_lvsegs.npz" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create the reader to bring the images in, these are initially in uint16 format with no channels:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "imSrc = data.readers.NPZReader(\"scd_lvsegs.npz\", [\"images\", \"segs\"], other_values=data.streams.OrderType.CHOICE)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Define a stream to convert the image format, apply some basic augments using multiple threads, and buffer the stream behind a thread so that batching can be done in parallel with the training process." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(200, 1, 64, 64) float32 (200, 1, 64, 64) int32\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "def normalizeImg(im, seg):\n", - " im = utils.arrayutils.rescale_array(im)\n", - " im = im[None].astype(np.float32)\n", - " seg = seg[None].astype(np.int32)\n", - " return im, seg\n", - "\n", - "\n", - "augs = [\n", - " normalizeImg,\n", - " augments.rot90,\n", - " augments.transpose,\n", - " augments.flip,\n", - " partial(augments.shift, dim_fract=5, order=0, nonzero_index=1),\n", - "]\n", - "\n", - "src = data.augments.augmentstream.ThreadAugmentStream(imSrc, 200, augments=augs)\n", - "src = data.streams.ThreadBufferStream(src)\n", - "\n", - "im, seg = utils.mathutils.first(src)\n", - "print(im.shape, im.dtype, seg.shape, seg.dtype)\n", - "plt.imshow(np.hstack([im[0, 0], seg[0, 0]]))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Define the network, loss, and optimizer:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "lr = 1e-3\n", - "\n", - "net = networks.nets.UNet(\n", - " dimensions=2,\n", - " in_channels=1,\n", - " num_classes=1,\n", - " channels=(16, 32, 64, 128, 256),\n", - " strides=(2, 2, 2, 2),\n", - " num_res_units=2,\n", - ")\n", - "\n", - "loss = networks.losses.DiceLoss()\n", - "opt = torch.optim.Adam(net.parameters(), lr)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Train using an Ignite Engine:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trainSteps = 100\n", - "trainEpochs = 20\n", - "trainSubsteps = 1\n", - "\n", - "\n", - "def _prepare_batch(batch, device=None, non_blocking=False):\n", - " x, y = batch\n", - " return torch.from_numpy(x).to(device), torch.from_numpy(y).to(device)\n", - "\n", - "\n", - "loss_fn = lambda i, j: loss(i[0], j)\n", - "\n", - "trainer = create_supervised_trainer( net, opt, loss_fn, torch.device(\"cuda:0\"), False, _prepare_batch)\n", - "\n", - "\n", - "@trainer.on(Events.EPOCH_COMPLETED)\n", - "def log_training_loss(engine):\n", - " print(\"Epoch\", engine.state.epoch, \"Loss:\", engine.state.output)\n", - "\n", - "\n", - "fsrc = data.streams.FiniteStream(\n", - " src, trainSteps\n", - ") # finite stream to train only for as many steps as we specify\n", - "state = trainer.run(fsrc, trainEpochs)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "im, seg = utils.mathutils.first(imSrc)\n", - "testim = utils.arrayutils.rescale_array(im[None, None])\n", - "\n", - "pred = net.cpu()(torch.from_numpy(testim))\n", - "\n", - "pseg = pred[1].data.numpy()\n", - "\n", - "plt.imshow(np.hstack([testim[0, 0], pseg[0]]))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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.6.9" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/nifti_read_example.ipynb b/examples/nifti_read_example.ipynb new file mode 100644 index 0000000000..9636c7275f --- /dev/null +++ b/examples/nifti_read_example.ipynb @@ -0,0 +1,233 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Nifti Read Example\n", + "\n", + "The purpose of this notebook is to illustrate reading Nifti files and iterating over patches of the volumes loaded from them." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MONAI version: 0.0.1\n", + "Python version: 3.7.3 (default, Mar 27 2019, 22:11:17) [GCC 7.3.0]\n", + "Numpy version: 1.16.4\n", + "Pytorch version: 1.3.1\n", + "Ignite version: 0.2.1\n" + ] + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "import os\n", + "import sys\n", + "from glob import glob\n", + "import tempfile\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import nibabel as nib\n", + "\n", + "\n", + "import torch\n", + "from torch.utils.data import DataLoader\n", + "import torchvision.transforms as transforms\n", + "\n", + "sys.path.append('..') # assumes this is where MONAI is\n", + "\n", + "from monai import application, data, networks, utils\n", + "from monai.data.readers import NiftiDataset\n", + "from monai.data.transforms import AddChannel, Transpose, Rescale, ToTensor, UniformRandomPatch, GridPatchDataset\n", + "\n", + "application.config.print_config()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define a function for creating test images and segmentations:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def create_test_image_3d(height, width, depth, numObjs=12, radMax=30, noiseMax=0.0, numSegClasses=5):\n", + " '''Return a noisy 3D image and segmentation.'''\n", + " image = np.zeros((width, height,depth))\n", + "\n", + " for i in range(numObjs):\n", + " x = np.random.randint(radMax, width - radMax)\n", + " y = np.random.randint(radMax, height - radMax)\n", + " z = np.random.randint(radMax, depth - radMax)\n", + " rad = np.random.randint(5, radMax)\n", + " spy, spx, spz = np.ogrid[-x:width - x, -y:height - y, -z:depth - z]\n", + " circle = (spx * spx + spy * spy + spz * spz) <= rad * rad\n", + "\n", + " if numSegClasses > 1:\n", + " image[circle] = np.ceil(np.random.random() * numSegClasses)\n", + " else:\n", + " image[circle] = np.random.random() * 0.5 + 0.5\n", + "\n", + " labels = np.ceil(image).astype(np.int32)\n", + "\n", + " norm = np.random.uniform(0, numSegClasses * noiseMax, size=image.shape)\n", + " noisyimage = utils.arrayutils.rescale_array(np.maximum(image, norm))\n", + "\n", + " return noisyimage, labels" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a number of test Nifti files:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "tempdir = tempfile.mkdtemp()\n", + "\n", + "for i in range(5):\n", + " im, seg = create_test_image_3d(256,256,256)\n", + " \n", + " n = nib.Nifti1Image(im, np.eye(4))\n", + " nib.save(n, os.path.join(tempdir, 'im%i.nii.gz'%i))\n", + " \n", + " n = nib.Nifti1Image(seg, np.eye(4))\n", + " nib.save(n, os.path.join(tempdir, 'seg%i.nii.gz'%i))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a data loader which yields uniform random patches from loaded Nifti files:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([5, 1, 64, 64, 64]) torch.Size([5, 1, 64, 64, 64])\n" + ] + } + ], + "source": [ + "images = sorted(glob(os.path.join(tempdir,'im*.nii.gz')))\n", + "segs = sorted(glob(os.path.join(tempdir,'seg*.nii.gz')))\n", + "\n", + "imtrans=transforms.Compose([\n", + " Rescale(),\n", + " AddChannel(),\n", + " UniformRandomPatch((64, 64, 64)),\n", + " ToTensor()\n", + "]) \n", + "\n", + "segtrans=transforms.Compose([\n", + " AddChannel(),\n", + " UniformRandomPatch((64, 64, 64)),\n", + " ToTensor()\n", + "]) \n", + " \n", + "ds = NiftiDataset(images, segs, imtrans, segtrans)\n", + "\n", + "loader = DataLoader(ds, batch_size=10, num_workers=2, pin_memory=torch.cuda.is_available())\n", + "im, seg = utils.mathutils.first(loader)\n", + "print(im.shape, seg.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively create a data loader which yields patches in regular grid order from loaded images:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([10, 1, 64, 64, 64]) torch.Size([10, 1, 64, 64, 64])\n" + ] + } + ], + "source": [ + "imtrans=transforms.Compose([\n", + " Rescale(),\n", + " AddChannel(),\n", + " ToTensor()\n", + "]) \n", + "\n", + "segtrans=transforms.Compose([\n", + " AddChannel(),\n", + " ToTensor()\n", + "]) \n", + " \n", + "ds = NiftiDataset(images, segs, imtrans, segtrans)\n", + "ds = GridPatchDataset(ds, (64, 64, 64))\n", + "\n", + "loader = DataLoader(ds, batch_size=10, num_workers=2, pin_memory=torch.cuda.is_available())\n", + "im, seg = utils.mathutils.first(loader)\n", + "print(im.shape, seg.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "!rm -rf {tempdir}" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/unet_segmentation_3d.ipynb b/examples/unet_segmentation_3d.ipynb new file mode 100644 index 0000000000..5a305be150 --- /dev/null +++ b/examples/unet_segmentation_3d.ipynb @@ -0,0 +1,241 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MONAI version: 0.0.1\n", + "Python version: 3.7.3 (default, Mar 27 2019, 22:11:17) [GCC 7.3.0]\n", + "Numpy version: 1.16.4\n", + "Pytorch version: 1.3.1\n", + "Ignite version: 0.2.1\n" + ] + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "import os\n", + "import sys\n", + "import tempfile\n", + "from glob import glob\n", + "from functools import partial\n", + "\n", + "import torch\n", + "import torch.nn as nn\n", + "from torch.utils.data import DataLoader\n", + "import torchvision.transforms as transforms\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import nibabel as nib\n", + "\n", + "from ignite.engine import Events, create_supervised_trainer\n", + "\n", + "# assumes the framework is found here, change as necessary\n", + "sys.path.append(\"..\")\n", + "\n", + "from monai import application, data, networks, utils\n", + "from monai.data.readers import NiftiDataset\n", + "from monai.data.transforms import AddChannel, Transpose, Rescale, ToTensor, UniformRandomPatch, GridPatchDataset\n", + "\n", + "\n", + "application.config.print_config()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def create_test_image_3d(height, width, depth, numObjs=12, radMax=30, noiseMax=0.0, numSegClasses=5):\n", + " '''Return a noisy 3D image and segmentation.'''\n", + " image = np.zeros((width, height,depth))\n", + "\n", + " for i in range(numObjs):\n", + " x = np.random.randint(radMax, width - radMax)\n", + " y = np.random.randint(radMax, height - radMax)\n", + " z = np.random.randint(radMax, depth - radMax)\n", + " rad = np.random.randint(5, radMax)\n", + " spy, spx, spz = np.ogrid[-x:width - x, -y:height - y, -z:depth - z]\n", + " circle = (spx * spx + spy * spy + spz * spz) <= rad * rad\n", + "\n", + " if numSegClasses > 1:\n", + " image[circle] = np.ceil(np.random.random() * numSegClasses)\n", + " else:\n", + " image[circle] = np.random.random() * 0.5 + 0.5\n", + "\n", + " labels = np.ceil(image).astype(np.int32)\n", + "\n", + " norm = np.random.uniform(0, numSegClasses * noiseMax, size=image.shape)\n", + " noisyimage = utils.arrayutils.rescale_array(np.maximum(image, norm))\n", + "\n", + " return noisyimage, labels" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "tempdir = tempfile.mkdtemp()\n", + "\n", + "for i in range(50):\n", + " im, seg = create_test_image_3d(256,256,256)\n", + " \n", + " n = nib.Nifti1Image(im, np.eye(4))\n", + " nib.save(n, os.path.join(tempdir, 'im%i.nii.gz'%i))\n", + " \n", + " n = nib.Nifti1Image(seg, np.eye(4))\n", + " nib.save(n, os.path.join(tempdir, 'seg%i.nii.gz'%i))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([10, 1, 64, 64, 64]) torch.Size([10, 1, 64, 64, 64])\n" + ] + } + ], + "source": [ + "images = sorted(glob(os.path.join(tempdir,'im*.nii.gz')))\n", + "segs = sorted(glob(os.path.join(tempdir,'seg*.nii.gz')))\n", + "\n", + "imtrans=transforms.Compose([\n", + " Rescale(),\n", + " AddChannel(),\n", + " UniformRandomPatch((64, 64, 64)),\n", + " ToTensor()\n", + "]) \n", + "\n", + "segtrans=transforms.Compose([\n", + " AddChannel(),\n", + " UniformRandomPatch((64, 64, 64)),\n", + " ToTensor()\n", + "]) \n", + " \n", + "ds = NiftiDataset(images, segs, imtrans, segtrans)\n", + "\n", + "loader = DataLoader(ds, batch_size=10, num_workers=2, pin_memory=torch.cuda.is_available())\n", + "im, seg = utils.mathutils.first(loader)\n", + "print(im.shape, seg.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "lr = 1e-3\n", + "\n", + "net = networks.nets.UNet(\n", + " dimensions=3,\n", + " in_channels=1,\n", + " num_classes=1,\n", + " channels=(16, 32, 64, 128, 256),\n", + " strides=(2, 2, 2, 2),\n", + " num_res_units=2,\n", + ")\n", + "\n", + "loss = networks.losses.DiceLoss()\n", + "opt = torch.optim.Adam(net.parameters(), lr)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1 Loss: 0.8619852662086487\n", + "Epoch 2 Loss: 0.8307779431343079\n", + "Epoch 3 Loss: 0.8064168691635132\n", + "Epoch 4 Loss: 0.7981672883033752\n", + "Epoch 5 Loss: 0.7950631976127625\n", + "Epoch 6 Loss: 0.7949732542037964\n", + "Epoch 7 Loss: 0.7963427901268005\n", + "Epoch 8 Loss: 0.7939450144767761\n", + "Epoch 9 Loss: 0.7926643490791321\n", + "Epoch 10 Loss: 0.7911991477012634\n", + "Epoch 11 Loss: 0.7886414527893066\n", + "Epoch 12 Loss: 0.7867528796195984\n", + "Epoch 13 Loss: 0.7857398390769958\n", + "Epoch 14 Loss: 0.7833380699157715\n", + "Epoch 15 Loss: 0.7791398763656616\n", + "Epoch 16 Loss: 0.7720394730567932\n", + "Epoch 17 Loss: 0.7671006917953491\n", + "Epoch 18 Loss: 0.7646064758300781\n", + "Epoch 19 Loss: 0.7672612071037292\n", + "Epoch 20 Loss: 0.7600041627883911\n", + "Epoch 21 Loss: 0.7583478689193726\n", + "Epoch 22 Loss: 0.7571365833282471\n", + "Epoch 23 Loss: 0.7545363306999207\n", + "Epoch 24 Loss: 0.7499511241912842\n", + "Epoch 25 Loss: 0.7481640577316284\n", + "Epoch 26 Loss: 0.7469437122344971\n", + "Epoch 27 Loss: 0.7460543513298035\n", + "Epoch 28 Loss: 0.74577796459198\n", + "Epoch 29 Loss: 0.7429620027542114\n", + "Epoch 30 Loss: 0.7424858808517456\n" + ] + } + ], + "source": [ + "trainEpochs = 30\n", + "\n", + "loss_fn = lambda i, j: loss(i[0], j)\n", + "device = torch.device(\"cuda:0\")\n", + "\n", + "trainer = create_supervised_trainer(net, opt, loss_fn, device, False)\n", + "\n", + "\n", + "@trainer.on(Events.EPOCH_COMPLETED)\n", + "def log_training_loss(engine):\n", + " print(\"Epoch\", engine.state.epoch, \"Loss:\", engine.state.output)\n", + "\n", + "\n", + "loader = DataLoader(ds, batch_size=20, num_workers=8, pin_memory=torch.cuda.is_available())\n", + " \n", + "state = trainer.run(loader, trainEpochs)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/monai/data/README.md b/monai/data/README.md index a22ce3028c..86ecaf3b08 100644 --- a/monai/data/README.md +++ b/monai/data/README.md @@ -1,50 +1,4 @@ # Data -This implements the data streams classes and contains a few example datasets. Data streams are iterables which produce -single data items or batches thereof from source iterables (usually). Chaining these together is how data pipelines are -implemented in the framework. Data augmentation routines are also provided here which can applied to data items as they -pass through the stream, either singly or in parallel. - -For example, the following stream reads image/segmentation pairs from `imSrc` (any iterable), applies the augmentations -to convert the array format and apply simple augmentations (rotation, transposing, flipping, shifting) using mutliple -threads, and wraps the whole stream in a buffering thread stream: - -``` -def normalizeImg(im,seg): - im=utils.arrayutils.rescaleArray(im) - im=im[None].astype(np.float32) - seg=seg[None].astype(np.int32) - return im, seg - -augs=[ - normalizeImg, - augments.rot90, - augments.transpose, - augments.flip, - partial(augments.shift,dimFract=5,order=0,nonzeroIndex=1), -] - -src=data.augments.augmentstream.ThreadAugmentStream(imSrc,200,augments=augs) -src=data.streams.ThreadBufferStream(src) -``` - -In this code, `src` is now going to yield batches of 200 images in a separate thread when iterated over. This can be -fed directly into a `NetworkManager` class as its `src` parameter. - -Module breakdown: - -* **augments**: Contains definitions and stream types for doing data augmentation. An augment is simply a callable which -accepts one or more Numpy arrays and returns the augmented result. The provided decorators are for adding probability -and other facilities to a function. - -* **readers**: Subclasses of `DataStream` for reading data from arrays and various file formats. - -* **streams**: Contains the definitions of the stream classes which implement a number of operations on streams. The -root of the stream classes is `DataStream` which provides a very simple iterable facility. It iterates over its `src` -member, passes each item into its `generate()` generator method and yields each resulting value. This allows subclasses -to implement `generate` to modify data as it moves through the stream. The `streamgen` decorator is provided to simplify -this by being applied to a generator function to fill this role in a new object. Other subclasses implement buffering, -batching, merging from multiple sources, cycling between sources, prefetching, and fetching data from the source in a -separate thread. - +This implements readers and transforms for data. diff --git a/monai/data/augments/__init__.py b/monai/data/augments/__init__.py deleted file mode 100644 index d0044e3563..0000000000 --- a/monai/data/augments/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/monai/data/augments/augments.py b/monai/data/augments/augments.py deleted file mode 100644 index fa78b6f14b..0000000000 --- a/monai/data/augments/augments.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This contains the definitions of the commonly used argumentation functions. These apply operations to single instances -of data objects, which are tuples of numpy arrays where the first dimension if the channel dimension and others are -component, height/width (CHW), or height/width/depth (CHWD). -""" -from functools import partial - -import numpy as np -import scipy.fftpack as ft -import scipy.ndimage - -from monai.data.augments.decorators import augment, check_segment_margin -from monai.utils.arrayutils import (copypaste_arrays, rand_choice, rescale_array, resize_center) -from monai.utils.convutils import one_hot - -try: - from PIL import Image - - PILAvailable = True -except ImportError: - PILAvailable = False - - -@augment() -def transpose(*arrs): - """Transpose axes 1 and 2 for each of `arrs'.""" - return partial(np.swapaxes, axis1=1, axis2=2) - - -@augment() -def flip(*arrs): - """Flip each of `arrs' with a random choice of up-down or left-right.""" - - def _flip(arr): - return arr[:, :, ::-1] if rand_choice() else arr[:, ::-1] - - return _flip - - -@augment() -def rot90(*arrs): - """Rotate each of `arrs' a random choice of quarter, half, or three-quarter circle rotations.""" - return partial(np.rot90, k=np.random.randint(1, 3), axes=(1, 2)) - - -@augment(prob=1.0) -def normalize(*arrs): - """Normalize each of `arrs'.""" - return rescale_array - - -@augment(prob=1.0) -def rand_patch(*arrs, patch_size=(32, 32)): - """Randomly choose a patch from `arrs' of dimensions `patch_size'.""" - ph, pw = patch_size - - def _rand_patch(im): - h, w = im.shape[1:3] - ry = np.random.randint(0, h - ph) - rx = np.random.randint(0, w - pw) - - return im[:, ry:ry + ph, rx:rx + pw] - - return _rand_patch - - -@augment() -@check_segment_margin -def shift(*arrs, dim_fract=2, order=3): - """Shift arrays randomly by `dimfract' fractions of the array dimensions.""" - testim = arrs[0] - x, y = testim.shape[1:3] - shiftx = np.random.randint(-x // dim_fract, x // dim_fract) - shifty = np.random.randint(-y // dim_fract, y // dim_fract) - - def _shift(im): - c, h, w = im.shape[:3] - dest = np.zeros_like(im) - - srcslices, destslices = copypaste_arrays(im, dest, (0, h // 2 + shiftx, w // 2 + shifty), (0, h // 2, w // 2), - (c, h, w)) - dest[destslices] = im[srcslices] - - return dest - - return _shift - - -@augment() -@check_segment_margin -def rotate(*arrs): - """Shift arrays randomly around the array center.""" - - angle = np.random.random() * 360 - - def _rotate(im): - return scipy.ndimage.rotate(im, angle=angle, reshape=False, axes=(1, 2)) - - return _rotate - - -@augment() -@check_segment_margin -def zoom(*arrs, zoomrange=0.2): - """Return the image/mask pair zoomed by a random amount with the mask kept within `margin' pixels of the edges.""" - - z = zoomrange - np.random.random() * zoomrange * 2 - zx = z + 1.0 + zoomrange * 0.25 - np.random.random() * zoomrange * 0.5 - zy = z + 1.0 + zoomrange * 0.25 - np.random.random() * zoomrange * 0.5 - - def _zoom(im): - ztemp = scipy.ndimage.zoom(im, (0, zx, zy) + tuple(1 for _ in range(1, im.ndim)), order=2) - return resize_center(ztemp, *im.shape) - - return _zoom - - -@augment() -@check_segment_margin -def rotate_zoom_pil(*arrs, margin=5, min_fract=0.5, max_fract=2, resample=0): - assert all(a.ndim >= 2 for a in arrs) - assert PILAvailable, "PIL (pillow) not installed" - - testim = arrs[0] - x, y = testim.shape[1:3] - - angle = np.random.random() * 360 - zoomx = x + np.random.randint(-x * min_fract, x * max_fract) - zoomy = y + np.random.randint(-y * min_fract, y * max_fract) - - filters = (Image.NEAREST, Image.LINEAR, Image.BICUBIC) - - def _trans(im): - if im.dtype != np.float32: - return _trans(im.astype(np.float32)).astype(im.dtype) - if im.ndim > 2: - return np.stack(list(map(_trans, im))) - elif im.ndim == 2: - im = Image.fromarray(im) - - # rotation - im = im.rotate(angle, filters[resample]) - - # zoom - zoomsize = (zoomx, zoomy) - pastesize = (im.size[0] // 2 - zoomsize[0] // 2, im.size[1] // 2 - zoomsize[1] // 2) - newim = Image.new("F", im.size) - newim.paste(im.resize(zoomsize, filters[resample]), pastesize) - im = newim - - return np.array(im) - - raise ValueError("Incorrect image shape: %r" % (im.shape,)) - - return _trans - - -@augment() -def deform_pil(*arrs, defrange=25, num_controls=3, margin=2, map_order=1): - """Deforms arrays randomly with a deformation grid of size `num_controls'**2 with `margins' grid values fixed.""" - assert PILAvailable, "PIL (pillow) not installed" - - h, w = arrs[0].shape[1:3] - - imshift = np.zeros((2, num_controls + margin * 2, num_controls + margin * 2)) - imshift[:, margin:-margin, margin:-margin] = np.random.randint(-defrange, defrange, (2, num_controls, num_controls)) - - imshiftx = np.array(Image.fromarray(imshift[0]).resize((w, h), Image.QUAD)) - imshifty = np.array(Image.fromarray(imshift[1]).resize((w, h), Image.QUAD)) - - y, x = np.meshgrid(np.arange(w), np.arange(h)) - indices = np.reshape(x + imshiftx, (-1, 1)), np.reshape(y + imshifty, (-1, 1)) - - def _map_channels(im): - if im.ndim > 2: - return np.stack(list(map(_map_channels, im))) - elif im.ndim == 2: - result = scipy.ndimage.map_coordinates(im, indices, order=map_order, mode="constant") - return result.reshape(im.shape) - - raise ValueError("Incorrect image shape: %r" % (im.shape,)) - - return _map_channels - - -@augment() -def distort_fft(*arrs, min_dist=0.1, max_dist=1.0): - """Distorts arrays by applying dropout in k-space with a per-pixel probability based on distance from center.""" - h, w = arrs[0].shape[:2] - - x, y = np.meshgrid(np.linspace(-1, 1, h), np.linspace(-1, 1, w)) - probfield = np.sqrt(x**2 + y**2) - - if arrs[0].ndim == 3: - probfield = np.repeat(probfield[..., np.newaxis], arrs[0].shape[2], 2) - - dropout = np.random.uniform(min_dist, max_dist, arrs[0].shape) > probfield - - def _distort(im): - if im.ndim == 2: - result = ft.fft2(im) - result = ft.fftshift(result) - result = result * dropout[:, :, 0] - result = ft.ifft2(result) - result = np.abs(result) - else: - result = np.dstack([_distort(im[..., i]) for i in range(im.shape[-1])]) - - return result - - return _distort - - -def split_segmentation(*arrs, num_labels=2, seg_index=-1): - arrs = list(arrs) - seg = arrs[seg_index] - seg = one_hot(seg, num_labels) - arrs[seg_index] = seg - - return tuple(arrs) - - -def merge_segmentation(*arrs, seg_index=-1): - arrs = list(arrs) - seg = arrs[seg_index] - seg = np.argmax(seg, 2) - arrs[seg_index] = seg - - return tuple(arrs) diff --git a/monai/data/augments/augmentstream.py b/monai/data/augments/augmentstream.py deleted file mode 100644 index 6a763e6b20..0000000000 --- a/monai/data/augments/augmentstream.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from multiprocessing.pool import ThreadPool - -import numpy as np - -from monai.data.streams.datastream import BatchStream, DataStream, OrderType - - -class AugmentStream(DataStream): - """Applies the given augmentations in generate() to each given value and yields the results.""" - - def __init__(self, src, augments=[]): - super().__init__(src) - self.augments = list(augments) - - def generate(self, val): - yield self.apply_augments(val) - - def apply_augments(self, arrays): - """Applies augments to the data tuple `arrays` and returns the result.""" - to_tuple = isinstance(arrays, np.ndarray) - arrays = (arrays,) if to_tuple else arrays - - for aug in self.augments: - arrays = aug(*arrays) - - return arrays[0] if to_tuple else arrays - - -class ThreadAugmentStream(BatchStream, AugmentStream): - """ - Applies the given augmentations to each value from the source using multiple threads. Resulting batches are yielded - synchronously so the client must wait for the threads to complete. - """ - - def __init__(self, src, batch_size, num_threads=None, augments=[], order_type=OrderType.LINEAR): - BatchStream.__init__(self, src, batch_size, False, order_type) - AugmentStream.__init__(self, src, augments) - self.num_threads = num_threads - self.pool = None - - def _augment_thread_func(self, index, arrays): - self.buffer[index] = self.apply_augments(arrays) - - def apply_augments_threaded(self): - self.pool.starmap(self._augment_thread_func, enumerate(self.buffer)) - - def buffer_full(self): - self.apply_augments_threaded() - super().buffer_full() - - def __iter__(self): - with ThreadPool(self.num_threads) as self.pool: - for src_val in super().__iter__(): - yield src_val diff --git a/monai/data/augments/decorators.py b/monai/data/augments/decorators.py deleted file mode 100644 index 9dc785a374..0000000000 --- a/monai/data/augments/decorators.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from functools import wraps - -import numpy as np - -from monai.utils.arrayutils import rand_choice, zero_margins - - -def augment(prob=0.5, apply_indices=None): - """ - Creates an augmentation function when decorating to a function returning an array-modifying callable. The function - this decorates is given the list of input arrays as positional arguments and then should return a callable operation - which performs the augmentation. This wrapper then chooses whether to apply the operation to the arguments and if so - to which ones. The `prob' argument states the probability the augment is applied, `apply_indices' gives indices of - the arrays to apply to (or None for all). The arguments are also keyword arguments in the resulting function. - """ - - def _inner(func): - - @wraps(func) - def _func(*args, **kwargs): - _prob = kwargs.pop("prob", prob) # get the probability of applying this augment - - if _prob < 1.0 and not rand_choice(_prob): # if not chosen just return the original argument - return args - - _apply_indices = kwargs.pop("apply_indices", apply_indices) - - op = func(*args, **kwargs) - indices = list(_apply_indices or range(len(args))) - - return tuple((op(im) if i in indices else im) for i, im in enumerate(args)) - - if _func.__doc__: - _func.__doc__ += """ - -Added keyword arguments: - prob: probability of applying this augment (default: 0.5) - apply_indices: indices of arrays to apply augment to (default: None meaning all) -""" - return _func - - return _inner - - -def check_segment_margin(func): - """ - Decorate an augment callable `func` with a check to ensure a given segmentation image in the set does not - touch the margins of the image when geometric transformations are applied. The keyword arguments `margin`, - `max_count` and `nonzero_index` are used to check the image at index `nonzero_index` has the given margin of - pixels around its edges, trying `max_count` number of times to get a modifier by calling `func` before - giving up and producing a identity modifier in its place. - """ - - @wraps(func) - def _check(*args, **kwargs): - margin = max(1, kwargs.pop("margin", 5)) - max_count = max(1, kwargs.pop("max_count", 5)) - nonzero_index = kwargs.pop("nonzero_index", -1) - accepted_output = False - - while max_count > 0 and not accepted_output: - op = func(*args, **kwargs) - max_count -= 1 - - if nonzero_index == -1: - accepted_output = True - else: - seg = op(args[nonzero_index]).astype(np.int32) - accepted_output = zero_margins(seg, margin) - - if not accepted_output: - return lambda arr: arr - - return op - - return _check diff --git a/monai/data/readers/arrayreader.py b/monai/data/readers/arrayreader.py deleted file mode 100644 index f7ec8792ae..0000000000 --- a/monai/data/readers/arrayreader.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import Lock - -import numpy as np - -import monai -from monai.data.streams import DataStream, OrderType -from monai.utils.decorators import RestartGenerator - - -@monai.utils.export("monai.data.readers") -class ArrayReader(DataStream): - """ - Creates a data source from one or more equal length arrays. Each data item yielded is a tuple of slices - containing a single index in the 0th dimension (ie. batch dimension) for each array. By default values - are drawn in sequential order but can be set to shuffle the order so that each value appears exactly once - per epoch, or to choose a random selection which may include items multiple times or not at all based off - an optional probability distribution. By default the stream will iterate over the arrays indefinitely or - optionally only once. - """ - - def __init__(self, *arrays, order_type=OrderType.LINEAR, do_once=False, choice_probs=None): - if order_type not in (OrderType.SHUFFLE, OrderType.CHOICE, OrderType.LINEAR): - raise ValueError("Invalid order_type value %r" % (order_type,)) - - self.arrays = () - self.order_type = order_type - self.do_once = do_once - self.choice_probs = None - self.lock = Lock() - - super().__init__(RestartGenerator(self.yield_arrays)) - - self.append_arrays(*arrays, choice_probs=choice_probs) - - def yield_arrays(self): - while self.is_running: - with self.lock: - # capture locally so that emptying the reader doesn't interfere with an on-going interation - arrays = self.arrays - choice_probs = self.choice_probs - - indices = np.arange(arrays[0].shape[0] if arrays else 0) - - if self.order_type == OrderType.SHUFFLE: - np.random.shuffle(indices) - elif self.order_type == OrderType.CHOICE: - indices = np.random.choice(indices, indices.shape, p=choice_probs) - - for i in indices: - yield tuple(arr[i] for arr in arrays) - - if self.do_once or not arrays: # stop first time through or if empty - break - - def get_sub_arrays(self, indices): - """Get a new ArrayReader with a subset of this one's data defined by the `indices` list.""" - with self.lock: - sub_arrays = [a[indices] for a in self.arrays] - sub_probs = None - - if self.choice_probs is not None: - sub_probs = self.choice_probs[indices] - sub_probs = sub_probs / np.sum(sub_probs) - - return ArrayReader(*sub_arrays, order_type=self.order_type, do_once=self.do_once, choice_probs=sub_probs) - - def append_arrays(self, *arrays, choice_probs=None): - """ - Append the given arrays to the existing entries in self.arrays, or replacing self.arrays if this is empty. If - `choice_probs` is provided this is appended to self.choice_probs, or replaces it if the latter is None or empty. - """ - array_len = arrays[0].shape[0] if arrays else 0 - - if array_len > 0 and any(arr.shape[0] != array_len for arr in arrays): - raise ValueError("All input arrays must have the same length for dimension 0") - - with self.lock: - if not self.arrays and arrays: - self.arrays = tuple(arrays) - elif array_len > 0: - self.arrays = tuple(np.concatenate(ht) for ht in zip(self.arrays, arrays)) - - if self.arrays and choice_probs is not None and choice_probs.shape[0] > 0: - choice_probs = np.atleast_1d(choice_probs) - - if choice_probs.shape[0] != array_len: - raise ValueError("Length of choice_probs (%i) must match that of input arrays (%i)" % - (self.choice_probs.shape[0], array_len)) - - if self.choice_probs is None: - self.choice_probs = choice_probs - else: - self.choice_probs = np.concatenate([self.choice_probs, choice_probs]) - - self.choice_probs = self.choice_probs / np.sum(self.choice_probs) - - def empty_arrays(self): - """Clear the stored arrays and choice_probs so that this reader is empty but functional.""" - with self.lock: - self.arrays = () - self.choice_probs = None if self.choice_probs is None else self.choice_probs[:0] - - def __len__(self): - return len(self.arrays[0]) if self.arrays else 0 diff --git a/monai/data/readers/niftireader.py b/monai/data/readers/niftireader.py new file mode 100644 index 0000000000..34622819ca --- /dev/null +++ b/monai/data/readers/niftireader.py @@ -0,0 +1,100 @@ +# Copyright 2020 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import nibabel as nib +import random + +from torch.utils.data import Dataset + +from monai.utils.moduleutils import export + + +def load_nifti(filename_or_obj, as_closest_canonical=False, image_only=True, dtype=None): + """ + Loads a Nifti file from the given path or file-like object. + + Args: + filename_or_obj (str or file): path to file or file-like object + as_closest_canonical (bool): if True, load the image as closest to canonical axis format + image_only (bool): if True return only the image volume, other return image volume and header dict + dtype (np.dtype, optional): if not None convert the loaded image to this data type + + Returns: + The loaded image volume if `image_only` is True, or a tuple containing the volume and the Nifti + header in dict format otherwise + """ + + img = nib.load(filename_or_obj) + + if as_closest_canonical: + img = nib.as_closest_canonical(img) + + if dtype is not None: + dat = img.get_fdata(dtype=dtype) + else: + dat = np.asanyarray(img.dataobj) + + header = dict(img.header) + header['filename_or_obj'] = filename_or_obj + + if image_only: + return dat + else: + return dat, header + + +@export("monai.data.readers") +class NiftiDataset(Dataset): + """ + Loads image/segmentation pairs of Nifti files from the given filename lists. Transformations can be specified + for the image and segmentation arrays separately. + """ + + def __init__(self, image_files, seg_files, transform=None, seg_transform=None): + """ + Initializes the dataset with the image and segmentation filename lists. The transform `transform` is applied + to the images and `seg_transform` to the segmentations. + + Args: + image_files (list of str): list of image filenames + seg_files (list of str): list of segmentation filenames + transform (Callable, optional): transform to apply to image arrays + seg_transform (Callable, optional): transform to apply to segmentation arrays + """ + + if len(image_files) != len(seg_files): + raise ValueError('Must have same number of image and segmentation files') + + self.image_files = image_files + self.seg_files = seg_files + self.transform = transform + self.seg_transform = seg_transform + + def __len__(self): + return len(self.image_files) + + def __getitem__(self, index): + img = load_nifti(self.image_files[index]) + seg = load_nifti(self.seg_files[index]) + + # https://github.com/pytorch/vision/issues/9#issuecomment-304224800 + seed = np.random.randint(2147483647) + + if self.transform is not None: + random.seed(seed) + img = self.transform(img) + + if self.seg_transform is not None: + random.seed(seed) # ensure randomized transforms roll the same values for segmentations as images + seg = self.seg_transform(seg) + + return img, seg diff --git a/monai/data/readers/npzreader.py b/monai/data/readers/npzreader.py deleted file mode 100644 index b17175ba0e..0000000000 --- a/monai/data/readers/npzreader.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import monai -from monai.data.streams import OrderType -from .arrayreader import ArrayReader -import numpy as np - - -@monai.utils.export("monai.data.readers") -class NPZReader(ArrayReader): - """ - Loads arrays from an .npz file as the source data. Other values can be loaded from the file and stored in - `other_values` rather than used as source data. - """ - - def __init__(self, obj_or_file_name, array_names, other_values=[], - order_type=OrderType.LINEAR, do_once=False, choice_probs=None): - self.objOrFileName = obj_or_file_name - - dat = np.load(obj_or_file_name) - - keys = set(dat.keys()) - missing = set(array_names) - keys - - if missing: - raise ValueError("Array name(s) %r not in loaded npz file" % (missing,)) - - arrays = [dat[name] for name in array_names] - - super().__init__(*arrays, order_type=order_type, do_once=do_once, choice_probs=choice_probs) - - self.otherValues = {n: dat[n] for n in other_values if n in keys} diff --git a/monai/data/streams/__init__.py b/monai/data/streams/__init__.py deleted file mode 100644 index d0044e3563..0000000000 --- a/monai/data/streams/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/monai/data/streams/datastream.py b/monai/data/streams/datastream.py deleted file mode 100644 index 755067aa22..0000000000 --- a/monai/data/streams/datastream.py +++ /dev/null @@ -1,324 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from functools import wraps - -import numpy as np - -import monai -from monai.utils.aliases import alias -from monai.utils.decorators import RestartGenerator -from monai.utils.mathutils import zip_with - -export = monai.utils.export("monai.data.streams") - - -@export -@alias("ordertype") -class OrderType(object): - SHUFFLE = "shuffle" - CHOICE = "choice" - LINEAR = "linear" - - -@export -@alias("datastream") -class DataStream(object): - """ - The DataStream class represents a chain of iterable objects where one iterates over its source and in turn yields - values which are possibly transformed. This allows an intermediate object in the stream to modify a data element - which passes through the stream or generate more than one output value for each input. A sequence of stream objects - is created by using one stream as the source to another. - - This relies on an input source which must be an iterable. Values are taken from this in order and then passed to the - generate() generator method to produce one or more items, which are then yielded. Subclasses can override generate() - to produce filter or transformer types to place in a sequence of DataStream objects. The `streamgen` decorator can - be used to do the same. - - Internal infrastructure can be setup when the iteration starts and can rely on the self.is_running to indicate when - generation is expected. When this changes to False methods are expected to cleanup and exit gracefully, and be able - to be called again with is_running set back to True. This allows restarting a complex stream object which may use - threads requiring starting and stopping. The stop() method when called set is_running to False and attempts to call - the same on self.src, this is meant to be used to stop any internal processes (ie. threads) when iteration stops - with the expectation that it can be restarted later. Reading is_running or assigning a literal value to it is atomic - thus thread-safe but keep this in mind when assigning a compound expression. - """ - - def __init__(self, src): - """Initialize with `src' as the source iterable, and self.is_running as True.""" - self.src = src - self.is_running = True - - def __iter__(self): - """ - Iterate over every value from self.src, passing through self.generate() and yielding the - values it generates. - """ - self.is_running = True - for src_val in self.src: - for out_val in self.generate(src_val): - yield out_val # yield with syntax too new? - - def generate(self, val): - """Generate values from input `val`, by default just yields that. """ - yield val - - def stop(self): - """Sets self.is_running to False and calls stop() on self.src if it has this method.""" - self.is_running = False - if callable(getattr(self.src, "stop", None)): - self.src.stop() - - def get_gen_func(self): - """Returns a callable taking no arguments which produces the next item in the stream whenever called.""" - stream = iter(self) - return lambda: next(stream) - - -class FuncStream(DataStream): - """For use with `streamgen`, the given callable is used as the generator in place of generate().""" - - def __init__(self, src, func, fargs, fkwargs): - super().__init__(src) - self.func = func - self.fargs = fargs - self.fkwargs = fkwargs - - def generate(self, val): - for out_val in self.func(val, *self.fargs, **self.fkwargs): - yield out_val - - -@export -def streamgen(func): - """ - Converts a generator function into a constructor for creating FuncStream instances - using the function as the generator. - """ - - @wraps(func) - def _wrapper(src, *args, **kwargs): - return FuncStream(src, func, args, kwargs) - - return _wrapper - - -@export -@alias("cachestream") -class CacheStream(DataStream): - """ - Reads a finite number of items from the source, or everything, into a cache then yields them either in - order, shuffled, or by choice indefinitely. - """ - - def __init__(self, src, buffer_size=None, order_type=OrderType.LINEAR): - super().__init__(src) - self.buffer_size = buffer_size - self.order_type = order_type - self.buffer = [] - - def __iter__(self): - self.buffer = [item for i, item in enumerate(self.src) if self.buffer_size is None or i < self.buffer_size] - - while self.is_running: - inds = np.arange(0, len(self.buffer)) - - if self.order_type == OrderType.SHUFFLE: - np.random.shuffle(inds) - elif self.order_type == OrderType.CHOICE: - inds = np.random.choice(inds, len(self.buffer)) - - for i in inds: - for out_val in self.generate(self.buffer[i]): - yield out_val - - -@export -@alias("bufferstream") -class BufferStream(DataStream): - """ - Accumulates a buffer of generated items, starting to yield them only when the buffer is filled and doing so until the - buffer is empty. The buffer is filled by generate() which calls buffer_full() when full to allow subclasses to react. - After this the buffer contents are yielded in order until the buffer is empty, then the filling process restarts. - """ - - def __init__(self, src, buffer_size=10, order_type=OrderType.LINEAR): - super().__init__(src) - self.buffer_size = buffer_size - self.orderType = order_type - self.buffer = [] - - def buffer_full(self): - """Called when the buffer is full and before emptying it.""" - - def generate(self, val): - if len(self.buffer) == self.buffer_size: - self.buffer_full() # call overridable callback to trigger action when buffer full - - if self.orderType == OrderType.SHUFFLE: - np.random.shuffle(self.buffer) - elif self.orderType == OrderType.CHOICE: - inds = np.random.choice(np.arange(len(self.buffer)), len(self.buffer)) - self.buffer = [self.buffer[i] for i in inds] - - while len(self.buffer) > 0: - yield self.buffer.pop(0) - - self.buffer.append(val) - - -@export -@alias("batchstream") -class BatchStream(BufferStream): - """Collects values from the source together into a batch of the stated size, ie. stacks buffered items.""" - - def __init__(self, src, batch_size, send_short_batch=False, order_type=OrderType.LINEAR): - super().__init__(src, batch_size, order_type) - self.send_short_batch = send_short_batch - - def buffer_full(self): - """Replaces the buffer's contents with the arrays stacked together into a single item.""" - if isinstance(self.buffer[0], np.ndarray): - # stack all the arrays together - batch = np.stack(self.buffer) - else: - # stack the arrays from each item into one - batch = tuple(zip_with(np.stack, *self.buffer)) - - self.buffer[:] = [batch] # yield only the one item when emptying the buffer - - def __iter__(self): - for src_val in super().__iter__(): - yield src_val - - # only true if the iteration has completed but items are left to make up a shortened batch - if len(self.buffer) > 0 and self.send_short_batch: - self.buffer_full() - yield self.buffer.pop() - - -@export -@alias("mergestream") -class MergeStream(DataStream): - """Merge data from multiple iterators into generated tuples.""" - - def __init__(self, *srcs): - self.srcs = srcs - super().__init__(RestartGenerator(self.yield_merged_values)) - - def yield_merged_values(self): - iters = [iter(s) for s in self.srcs] - can_continue = True - - while self.is_running and can_continue: - try: - values = [] - for it in iters: - val = next(it) # raises StopIteration when a source runs out of data at which point we quit - - if not isinstance(val, (list, tuple)): - val = (val,) - - values.append(tuple(val)) - - src_val = sum(values, ()) - - for out_val in self.generate(src_val): - yield out_val - # must be caught as StopIteration won't propagate but magically mutate into RuntimeError - except StopIteration: - can_continue = False - - -@export -@alias("cyclingstream") -class CyclingStream(DataStream): - - def __init__(self, *srcs): - self.srcs = srcs - super().__init__(RestartGenerator(self.yield_alternating_values)) - - def yield_alternating_values(self): - iters = [iter(s) for s in self.srcs] - can_continue = True - - while self.is_running and can_continue: - try: - for it in iters: - src_val = next(it) # raises StopIteration when a source runs out of data at which point we quit - for out_val in self.generate(src_val): - yield out_val - - # must be caught as StopIteration won't propagate but magically mutate into RuntimeError - except StopIteration: - can_continue = False - - -@export -class PrefetchStream(DataStream): - """ - Calculates item dtype and shape before iteration. This will get a value from `src` in the constructor, assign it to - self.src_val, then assign the dtypes and shapes of the arrays to self.dtypes and self.shapes respectively. When it is - iterated over self.src_val is yielded first followed by whatever else `src` produces so no data is lost. - """ - - def __init__(self, src): - self.origSrc = src - self.it = iter(src) - self.src_val = next(self.it) - - if isinstance(self.src_val, np.ndarray): - self.dtypes = self.src_val.dtype - self.shapes = self.src_val.shape - else: - self.dtypes = tuple(b.dtype for b in self.src_val) - self.shapes = tuple(b.shape for b in self.src_val) - - super().__init__(RestartGenerator(self._get_src)) - - def _get_src(self): - if self.it is not None: - yield self.src_val - else: - self.it = iter(self.origSrc) # self.it is None when restarting so recreate the iterator here - - for src_val in self.it: - yield src_val - - self.it = None - - -@export -@alias("finitestream") -class FiniteStream(DataStream): - """Yields only the specified number of items before quiting.""" - - def __init__(self, src, num_items): - super().__init__(src) - self.num_items = num_items - - def __iter__(self): - for _, item in zip(range(self.num_items), super().__iter__()): - yield item - - -@export -@alias("tracestream") -class TraceStream(DataStream): - - def generate(self, val): - vals = val if isinstance(val, (tuple, list)) else (val,) - - sizes = ", ".join("%s%s" % (s.dtype, s.shape) for s in vals) - - print("Stream -> %s" % sizes, flush=True) - - yield val diff --git a/monai/data/streams/threadbufferstream.py b/monai/data/streams/threadbufferstream.py deleted file mode 100644 index 6a3c96aee6..0000000000 --- a/monai/data/streams/threadbufferstream.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from queue import Empty, Full, Queue -from threading import Thread - -import monai -from monai.data.streams import DataStream -from monai.utils.aliases import alias - - -@monai.utils.export("monai.data.streams") -@alias("threadbufferstream") -class ThreadBufferStream(DataStream): - """ - Iterates over values from self.src in a separate thread but yielding them in the current thread. This allows values - to be queued up asynchronously. The internal thread will continue running so long as the source has values or until - the stop() method is called. - - One issue raised by using a thread in this way is that during the lifetime of the thread the source object is being - iterated over, so if the thread hasn't finished another attempt to iterate over it will raise an exception or yield - inexpected results. To ensure the thread releases the iteration and proper cleanup is done the stop() method must - be called which will join with the thread. - """ - - def __init__(self, src, buffer_size=1, timeout=0.01): - super().__init__(src) - self.buffer_size = buffer_size - self.timeout = timeout - self.buffer = Queue(self.buffer_size) - self.gen_thread = None - - def enqueue_values(self): - # allows generate() to be overridden and used here (instead of iter(self.src)) - for src_val in super().__iter__(): - while self.is_running: - try: - self.buffer.put(src_val, timeout=self.timeout) - except Full: - pass # try to add the item again - else: - break # successfully added the item, quit trying - else: # quit the thread cleanly when requested to stop - break - - def stop(self): - super().stop() - if self.gen_thread is not None: - self.gen_thread.join() - - def __iter__(self): - self.gen_thread = Thread(target=self.enqueue_values, daemon=True) - self.gen_thread.start() - self.is_running = True - - try: - while self.is_running and (self.gen_thread.is_alive() or not self.buffer.empty()): - try: - yield self.buffer.get(timeout=self.timeout) - except Empty: - pass # queue was empty this time, try again - finally: - self.stop() diff --git a/monai/data/transforms/dataset_transforms.py b/monai/data/transforms/dataset_transforms.py new file mode 100644 index 0000000000..a29cc5f179 --- /dev/null +++ b/monai/data/transforms/dataset_transforms.py @@ -0,0 +1,83 @@ +# Copyright 2020 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import torch +import numpy as np + +import monai +from monai.utils.arrayutils import get_valid_patch_size, get_random_patch, rescale_array + +export = monai.utils.export("monai.data.transforms") + + +@export +class AddChannel: + """ + Adds a 1-length channel dimension to the input image. + """ + + def __call__(self, img): + return img[None] + + +@export +class Transpose: + """ + Transposes the input image based on the given `indices` dimension ordering. + """ + + def __init__(self, indices): + self.indices = indices + + def __call__(self, img): + return img.transpose(self.indices) + + +@export +class Rescale: + """ + Rescales the input image to the given value range. + """ + + def __init__(self, minv=0.0, maxv=1.0, dtype=np.float32): + self.minv = minv + self.maxv = maxv + self.dtype = dtype + + def __call__(self, img): + return rescale_array(img, self.minv, self.maxv, self.dtype) + + +@export +class ToTensor: + """ + Converts the input image to a tensor without applying any other transformations. + """ + + def __call__(self, img): + return torch.from_numpy(img) + + +@export +class UniformRandomPatch: + """ + Selects a patch of the given size chosen at a uniformly random position in the image. + """ + + def __init__(self, patch_size): + self.patch_size = (None,) + tuple(patch_size) + + def __call__(self, img): + patch_size = get_valid_patch_size(img.shape, self.patch_size) + slices = get_random_patch(img.shape, patch_size) + + return img[slices] diff --git a/monai/data/transforms/grid_dataset.py b/monai/data/transforms/grid_dataset.py new file mode 100644 index 0000000000..68c28ff626 --- /dev/null +++ b/monai/data/transforms/grid_dataset.py @@ -0,0 +1,66 @@ +# Copyright 2020 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import math + +import torch +from torch.utils.data import IterableDataset + +from monai.utils.moduleutils import export +from monai.utils.arrayutils import iter_patch + +@export("monai.data.transforms") +class GridPatchDataset(IterableDataset): + """ + Yields patches from arrays read from an input dataset. The patches are chosen in a contiguous grid sampling scheme. + """ + + def __init__(self, dataset, patch_size, start_pos=(), pad_mode="wrap", **pad_opts): + """ + Initializes this dataset in terms of the input dataset and patch size. The `patch_size` is the size of the + patch to sample from the input arrays. Tt is assumed the arrays first dimension is the channel dimension which + will be yielded in its entirety so this should not be specified in `patch_size`. For example, for an input 3D + array with 1 channel of size (1, 20, 20, 20) a regular grid sampling of eight patches (1, 10, 10, 10) would be + specified by a `patch_size` of (10, 10, 10). + + Args: + dataset (Dataset): the dataset to read array data from + patch_size (tuple of int or None): size of patches to generate slices for, 0/None selects whole dimension + start_pos (tuple of it, optional): starting position in the array, default is 0 for each dimension + pad_mode (str, optional): padding mode, see numpy.pad + pad_opts (dict, optional): padding options, see numpy.pad + """ + + self.dataset = dataset + self.patch_size = (None,) + tuple(patch_size) + self.start_pos = start_pos + self.pad_mode = pad_mode + self.pad_opts = pad_opts + + def __iter__(self): + worker_info = torch.utils.data.get_worker_info() + iter_start = 0 + iter_end = len(self.dataset) + + if worker_info is not None: + # split workload + per_worker = int(math.ceil((iter_end - iter_start) / float(worker_info.num_workers))) + worker_id = worker_info.id + iter_start = iter_start + worker_id * per_worker + iter_end = min(iter_start + per_worker, iter_end) + + for index in range(iter_start, iter_end): + arrays = self.dataset[index] + + iters = [iter_patch(a, self.patch_size, self.start_pos, False, self.pad_mode, **self.pad_opts) for a in arrays] + + yield from zip(*iters) diff --git a/monai/data/transforms/image_props.py b/monai/data/transforms/image_props.py deleted file mode 100644 index add56e9924..0000000000 --- a/monai/data/transforms/image_props.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -class ImageProperty: - """Key names for image properties. - - """ - DATA = 'data' - FILENAME = 'file_name' - AFFINE = 'affine' # image affine matrix - ORIGINAL_SHAPE = 'original_shape' - ORIGINAL_SHAPE_FORMAT = 'original_shape_format' - SPACING = 'spacing' # itk naming convention for pixel/voxel size - FORMAT = 'file_format' - NIFTI_FORMAT = 'nii' - IS_CANONICAL = 'is_canonical' - SHAPE_FORMAT = 'shape_format' - BACKGROUND_INDEX = 'background_index' # which index is background diff --git a/monai/data/transforms/image_reader.py b/monai/data/transforms/image_reader.py deleted file mode 100644 index 234f072330..0000000000 --- a/monai/data/transforms/image_reader.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import numpy as np - - -class ImageReader(object): - """Base class for Image Loader.""" - - def __init__(self, dtype=np.float32): - self._logger = logging.getLogger(self.__class__.__name__) - self._dtype = dtype - - def _read_from_file_list(self, file_names): - raise NotImplementedError('{} cannot load from file list'.format(self.__class__.__name__)) - - def _read_from_file(self, file_name): - raise NotImplementedError('{} cannot load from file'.format(self.__class__.__name__)) - - def read(self, file_name_spec): - if isinstance(file_name_spec, np.ndarray): - file_name_spec = file_name_spec.tolist() - if isinstance(file_name_spec, list): - assert len(file_name_spec) > 0, 'file_name_spec must not be empty list' - - file_names = [] - for file_name in file_name_spec: - if isinstance(file_name, (bytes, bytearray)): - file_name = file_name.decode('UTF-8') - file_names.append(file_name) - - result = self._read_from_file_list(file_names) - else: - file_name = file_name_spec - if isinstance(file_name, (bytes, bytearray)): - file_name = file_name.decode('UTF-8') - assert isinstance(file_name, str), 'file_name_spec must be a str' - assert len(file_name) > 0, 'file_name_spec must not be empty' - result = self._read_from_file(file_name) - - return result diff --git a/monai/data/transforms/multi_format_transformer.py b/monai/data/transforms/multi_format_transformer.py deleted file mode 100644 index 3e74da4a91..0000000000 --- a/monai/data/transforms/multi_format_transformer.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import numpy as np -from .shape_format import ShapeFormat -from .shape_format import get_shape_format - - -class MultiFormatTransformer: - """Base class for multi-format transformer. - - 12 numpy data formats are specified based on image dimension, batch mode, and channel mode - """ - - def __init__(self): - - self._format_handlers = { - ShapeFormat.CHWD: self._handle_chwd, - ShapeFormat.CHW: self._handle_chw - } - self._logger = logging.getLogger(self.__class__.__name__) - - def _handle_any(self, *args, **kwargs): - return None - - def _handle_chw(self, *args, **kwargs): - return None - - def _handle_chwd(self, *args, **kwargs): - return None - - def transform(self, img, *args, **kwargs): - - assert isinstance(img, np.ndarray), 'img must be np.ndarray' - - shape_format = get_shape_format(img) - if not shape_format: - raise ValueError('the image data has invalid shape format') - - h = self._format_handlers.get(shape_format, None) - if h is None: - raise ValueError('unsupported image shape format: {}'.format(shape_format)) - - result = h(img, *args, **kwargs) - if result is not None: - return result - - result = self._handle_any(img, *args, **kwargs) - - if result is None: - raise NotImplementedError( - 'transform {} does not support format {}'.format(self.__class__.__name__, shape_format)) - - return result - - def __call__(self, *args, **kwargs): - return self.transform(*args, **kwargs) diff --git a/monai/data/transforms/nifti_reader.py b/monai/data/transforms/nifti_reader.py deleted file mode 100644 index a09fc5675b..0000000000 --- a/monai/data/transforms/nifti_reader.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import nibabel as nib -import numpy as np - -from .image_props import ImageProperty -from .image_reader import ImageReader - - -class NiftiReader(ImageReader): - """ Reads nifti files. - - Args: - dtype(np) : type for loaded data. - nii_is_channels(bool): Is nifti channels first. (Default: False) - as_closest_canonical (bool): Load in canonical orientation. (Default: True) - - Returns: - img: image data - img_props: dict of image properties - - """ - - def __init__(self, dtype=np.float32, nii_is_channels_first=False, as_closest_canonical=True): - ImageReader.__init__(self, dtype) - - # Make a list of fields to be loaded - self.nii_is_channels_first = nii_is_channels_first - self.as_closest_canonical = as_closest_canonical - self._dtype = dtype - - def _load_data(self, file_name): - self._logger.debug("Loading nifti file {}".format(file_name)) - epi_img = nib.load(file_name) - assert epi_img is not None - - if self.as_closest_canonical: - epi_img = nib.as_closest_canonical(epi_img) - - img_array = epi_img.get_fdata(dtype=self._dtype) - - affine = epi_img.affine - shape = epi_img.header.get_data_shape() - spacing = epi_img.header.get_zooms() - if len(spacing) > 3: # Possible temporal spacing in 4th dimension - spacing = spacing[:3] - return img_array, affine, shape, spacing, self.as_closest_canonical - - def _read_from_file(self, file_name): - """ Loads a nifti file. - - Args: - file_name (str): path to nifti file. - - Returns: - Loaded MedicalImage. - """ - img_array, affine, shape, spacing, is_canonical = self._load_data(file_name) - num_dims = len(img_array.shape) - img_array = img_array.astype(self._dtype) - - if num_dims == 2: - img_array = np.expand_dims(img_array, axis=0) - elif num_dims == 3: - img_array = np.expand_dims(img_array, axis=0) - elif num_dims <= 5: - # if 4d data, we assume 4th dimension is channels. - # if 5d data, try to squeeze 5th dimension. - if num_dims == 5: - img_array = np.squeeze(img_array) - if len(img_array.shape) != 4: - raise ValueError("NiftiReader doesn't support time based data.") - - if not self.nii_is_channels_first: - # convert to channel first - img_array = np.transpose(img_array, (3, 0, 1, 2)) - else: - raise NotImplementedError('NifitReader does not support image of dims {}'.format(num_dims)) - - img_props = { - ImageProperty.AFFINE: affine, - ImageProperty.FILENAME: file_name, - ImageProperty.FORMAT: ImageProperty.NIFTI_FORMAT, - ImageProperty.ORIGINAL_SHAPE: shape, - ImageProperty.SPACING: spacing, - ImageProperty.IS_CANONICAL: is_canonical - } - - return img_array, img_props - - def _read_from_file_list(self, file_names): - """Loads a multi-channel nifti file (1 channel per file) - - Args: - file_names (list): list of file names. - - Returns: - Loaded MedicalImage. - """ - img_array = [] - affine = None - shape = None - spacing = None - is_canonical = None - - for file_name in file_names: - _img_array, _affine, _shape, _spacing, _is_canonical = self._load_data(file_name) - - # Check if next data array matches the previous one - # warnings if affine or spacing does not match - if affine is None: - affine = _affine - elif not np.array_equal(_affine, affine): - self._logger.warning( - 'Affine matrix of [{}] is not consistent with previous data entry'.format(file_name)) - - if spacing is None: - spacing = _spacing - elif _spacing != spacing: - self._logger.warning( - 'Spacing of [{}] is not consistent with previous data entry'.format(file_name)) - - # error if shapes do not match as this will cause errors later - if shape is None: - shape = _shape - elif _shape != shape: - error_message = 'Shape of [{}] is not consistent with previous data entry' \ - .format(file_name) - - self._logger.error(error_message) - raise ValueError(error_message) - - # Check if canonical settings are same. - if is_canonical is None: - is_canonical = _is_canonical - elif _is_canonical != is_canonical: - self._logger.warning( - 'File {} is loaded in different canonical settings than previous files.'.format(file_name)) - - # append image array for stacking - img_array.append(_img_array) - - # load and stack channels along first dimension - img_array = np.stack(img_array, axis=0) - shape = np.shape(img_array) # update to new shape - num_dims = len(shape) - img_array = img_array.astype(self._dtype) - - if num_dims != 3 and num_dims != 4: - raise NotImplementedError('NiftiReader does not support image of dims {}'.format(num_dims)) - - img_props = { - ImageProperty.AFFINE: affine, - ImageProperty.FILENAME: file_names, - ImageProperty.FORMAT: ImageProperty.NIFTI_FORMAT, - ImageProperty.ORIGINAL_SHAPE: shape, - ImageProperty.SPACING: spacing, - ImageProperty.IS_CANONICAL: is_canonical - } - - return img_array, img_props diff --git a/monai/data/transforms/nifti_writer.py b/monai/data/transforms/nifti_writer.py deleted file mode 100644 index b239e520b3..0000000000 --- a/monai/data/transforms/nifti_writer.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import nibabel as nib -from .multi_format_transformer import MultiFormatTransformer - - -class NiftiWriter(MultiFormatTransformer): - """Write nifti files to disk. - - Args: - use_identity (bool): If true, affine matrix of data is ignored. (Default: False) - compressed (bool): Should save in compressed format. (Default: True) - """ - - def __init__(self, dtype="float32", use_identity=False, compressed=True): - MultiFormatTransformer.__init__(self) - self._dtype = dtype - self._use_identity = use_identity - self._compressed = compressed - - def _handle_chw(self, img): - # convert to channels-last - return np.transpose(img, (1, 2, 0)) - - def _handle_chwd(self, img): - # convert to channels-last - return np.transpose(img, (1, 2, 3, 0)) - - def _write_file(self, data, affine, file_name, revert_canonical): - if affine is None: - affine = np.eye(4) - - if revert_canonical: - codes = nib.orientations.axcodes2ornt(nib.orientations.aff2axcodes(np.linalg.inv(affine))) - reverted_results = nib.orientations.apply_orientation(np.squeeze(data), codes) - results_img = nib.Nifti1Image(reverted_results.astype(self._dtype), affine) - else: - results_img = nib.Nifti1Image(np.squeeze(data).astype(self._dtype), np.squeeze(affine)) - - nib.save(results_img, file_name) - - def write(self, img, affine, revert_canonical: bool, file_basename: str): - """Write Nifti file from given data. - - Args: - img: image data. - affine: the affine matrix - revert_canonical: whether to revert canonical when writing the file - file_basename (str): path for written nifti file. - - Returns: - """ - assert isinstance(file_basename, str), 'file_basename must be str' - assert file_basename, 'file_basename must not be empty' - - file_name = file_basename - if self._compressed: - file_name = file_basename + ".nii.gz" - - # create and save the nifti image - # check for existing affine matrix from LoadNifti - if self._use_identity: - affine = None - - if affine: - assert affine.shape == (4, 4), \ - 'Affine must shape (4, 4) but is shape {}'.format(affine.shape) - - img = self.transform(img) - self._write_file(img, affine, file_name, revert_canonical) diff --git a/monai/data/transforms/noise_adder.py b/monai/data/transforms/noise_adder.py deleted file mode 100644 index 5273abf614..0000000000 --- a/monai/data/transforms/noise_adder.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .multi_format_transformer import MultiFormatTransformer - - -class NoiseAdder(MultiFormatTransformer): - """Adds noise to the entire image. - - Args: - No argument - """ - - def __init__(self, noise): - MultiFormatTransformer.__init__(self) - self.noise = noise - - def _handle_any(self, img): - return img + self.noise diff --git a/monai/data/transforms/shape_format.py b/monai/data/transforms/shape_format.py deleted file mode 100644 index 2e374b9757..0000000000 --- a/monai/data/transforms/shape_format.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2020 MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np - - -class ShapeFormat: - """ShapeFormat defines meanings for the data in a MedicalImage. - Image data is a numpy's ndarray. Without shape format, it is impossible to know what each - dimension means. - - NOTE: ShapeFormat objects are immutable. - - """ - - CHW = 'CHW' - CHWD = 'CHWD' - - -def get_shape_format(img: np.ndarray): - """Return the shape format of the image data - - Args: - img (np.ndarray): the image data - - Returns: a shape format or None - - Raise: AssertionError if any of the specified args is invalid - - """ - assert isinstance(img, np.ndarray), 'invalid value img - must be np.ndarray' - if img.ndim == 3: - return ShapeFormat.CHW - elif img.ndim == 4: - return ShapeFormat.CHWD - else: - return None diff --git a/monai/utils/arrayutils.py b/monai/utils/arrayutils.py index ecafa92e9b..79cf96ffb5 100644 --- a/monai/utils/arrayutils.py +++ b/monai/utils/arrayutils.py @@ -1,4 +1,3 @@ - # Copyright 2020 MONAI Consortium # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,6 +11,7 @@ import random +from itertools import product, starmap import numpy as np @@ -119,9 +119,10 @@ def copypaste_arrays(src, dest, srccenter, destcenter, dims): for i, ss, ds, sc, dc, dim in zip(range(src.ndim), src.shape, dest.shape, srccenter, destcenter, dims): if dim: - d1 = np.clip(dim // 2, 0, min(sc, dc)) # dimension before midpoint, clip to size fitting in both arrays - d2 = np.clip(dim // 2 + 1, 0, min(ss - sc, - ds - dc)) # dimension after midpoint, clip to size fitting in both arrays + # dimension before midpoint, clip to size fitting in both arrays + d1 = np.clip(dim // 2, 0, min(sc, dc)) + # dimension after midpoint, clip to size fitting in both arrays + d2 = np.clip(dim // 2 + 1, 0, min(ss - sc, ds - dc)) srcslices[i] = slice(sc - d1, sc + d2) destslices[i] = slice(dc - d1, dc + d2) @@ -139,9 +140,121 @@ def resize_center(img, *resize_dims, fill_value=0): resize_dims = tuple(resize_dims[i] or img.shape[i] for i in range(len(resize_dims))) dest = np.full(resize_dims, fill_value, img.dtype) - srcslices, destslices = copypaste_arrays(img, dest, - np.asarray(img.shape) // 2, - np.asarray(dest.shape) // 2, resize_dims) + half_img_shape = np.asarray(img.shape) // 2 + half_dest_shape = np.asarray(dest.shape) // 2 + + srcslices, destslices = copypaste_arrays(img, dest, half_img_shape, half_dest_shape, resize_dims) dest[destslices] = img[srcslices] return dest + + +def get_valid_patch_size(dims, patch_size): + """ + Given an image of dimensions `dims`, return a patch size tuple taking the dimension from `patch_size` if this is + not 0/None. Otherwise, or if `patch_size` is shorter than `dims`, the dimension from `dims` is taken. This ensures + the returned patch size is within the bounds of `dims`. If `patch_size` is a single number this is interpreted as a + patch of the same dimensionality of `dims` with that size in each dimension. + """ + ndim = len(dims) + + try: + # if a single value was given as patch size, treat this as the size of the patch over all dimensions + single_patch_size = int(patch_size) + patch_size = (single_patch_size,) * ndim + except TypeError: # raised if the patch size is multiple values + # ensure patch size is at least as long as number of dimensions + patch_size = ensure_tuple_size(patch_size, ndim) + + # ensure patch size dimensions are not larger than image dimension, if a dimension is None or 0 use whole dimension + return tuple(min(ms, ps or ms) for ms, ps in zip(dims, patch_size)) + + +def get_random_patch(dims, patch_size): + """ + Returns a tuple of slices to define a random patch in an array of shape `dims` with size `patch_size` or the as + close to it as possible within the given dimension. It is expected that `patch_size` is a valid patch for a source + of shape `dims` as returned by `get_valid_patch_size`. + + Args: + dims (tuple of int): shape of source array + patch_size (tuple of int): shape of patch size to generate + + Returns: + (tuple of slice): a tuple of slice objects defining the patch + """ + + # choose the minimal corner of the patch + min_corner = tuple(np.random.randint(0, ms - ps) if ms > ps else 0 for ms, ps in zip(dims, patch_size)) + + # create the slices for each dimension which define the patch in the source array + return tuple(slice(mc, mc + ps) for mc, ps in zip(min_corner, patch_size)) + + +def iter_patch_slices(dims, patch_size, start_pos=()): + """ + Yield successive tuples of slices defining patches of size `patch_size` from an array of dimensions `dims`. The + iteration starts from position `start_pos` in the array, or starting at the origin if this isn't provided. Each + patch is chosen in a contiguous grid using a first dimension as least significant ordering. + + Args: + dims (tuple of int): dimensions of array to iterate over + patch_size (tuple of int or None): size of patches to generate slices for, 0 or None selects whole dimension + start_pos (tuple of it, optional): starting position in the array, default is 0 for each dimension + + Yields: + Tuples of slice objects defining each patch + """ + + # ensure patchSize and startPos are the right length + ndim = len(dims) + patch_size = get_valid_patch_size(dims, patch_size) + start_pos = ensure_tuple_size(start_pos, ndim) + + # collect the ranges to step over each dimension + ranges = tuple(starmap(range, zip(start_pos, dims, patch_size))) + + # choose patches by applying product to the ranges + for position in product(*ranges[::-1]): # reverse ranges order to iterate in index order + yield tuple(slice(s, s + p) for s, p in zip(position[::-1], patch_size)) + + +def iter_patch(arr, patch_size, start_pos=(), copy_back=True, pad_mode="wrap", **pad_opts): + """ + Yield successive patches from `arr' of size `patchSize'. The iteration can start from position `startPos' in `arr' + but drawing from a padded array extended by the `patchSize' in each dimension (so these coordinates can be negative + to start in the padded region). If `copyBack' is True the values from each patch are written back to `arr'. + + Args: + arr (np.ndarray): array to iterate over + patch_size (tuple of int or None): size of patches to generate slices for, 0 or None selects whole dimension + start_pos (tuple of it, optional): starting position in the array, default is 0 for each dimension + copy_back (bool): if True data from the yielded patches is copied back to `arr` once the generator completes + pad_mode (str, optional): padding mode, see numpy.pad + pad_opts (dict, optional): padding options, see numpy.pad + + Yields: + Patches of array data from `arr` which are views into a padded array which can be modified, if `copy_back` is + True these changes will be reflected in `arr` once the iteration completes + """ + # ensure patchSize and startPos are the right length + patch_size = get_valid_patch_size(arr.shape, patch_size) + start_pos = ensure_tuple_size(start_pos, arr.ndim) + + # pad image by maximum values needed to ensure patches are taken from inside an image + arrpad = np.pad(arr, tuple((p, p) for p in patch_size), pad_mode, **pad_opts) + + # choose a start position in the padded image + start_pos_padded = tuple(s + p for s, p in zip(start_pos, patch_size)) + + # choose a size to iterate over which is smaller than the actual padded image to prevent producing + # patches which are only in the padded regions + iter_size = tuple(s + p for s, p in zip(arr.shape, patch_size)) + + for slices in iter_patch_slices(iter_size, patch_size, start_pos_padded): + yield arrpad[slices] + + # copy back data from the padded image if required + if copy_back: + slices = tuple(slice(p, p + s) for p, s in zip(patch_size, arr.shape)) + arr[...] = arrpad[slices]