From 2a92e9e16afa9a7cf89b92f6e4e35a5fe853ec93 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Tue, 14 Jan 2020 14:26:13 +0000 Subject: [PATCH 01/11] initial unit tests for 2d/3d unet --- tests/test_unet.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_unet.py diff --git a/tests/test_unet.py b/tests/test_unet.py new file mode 100644 index 0000000000..362dd26626 --- /dev/null +++ b/tests/test_unet.py @@ -0,0 +1,43 @@ +import unittest + +import torch +from parameterized import parameterized + +from monai.networks.nets.unet import UNet + + +class TestUNET(unittest.TestCase): + + @parameterized.expand([ + [ + { + 'dimensions': 2, + 'in_channels': 1, + 'num_classes': 3, + 'channels': (16, 32, 64), + 'strides': (2, 2), + 'num_res_units': 1, + }, + torch.randn(16, 1, 32, 32), + (16, 32, 32), + ], + [ + { + 'dimensions': 3, + 'in_channels': 1, + 'num_classes': 3, + 'channels': (16, 32, 64), + 'strides': (2, 2), + 'num_res_units': 1, + }, + torch.randn(16, 1, 32, 32, 32), + (16, 32, 32, 32), + ], + ]) + def test_shape(self, input_param, input_data, expected_shape): + result = UNet(**input_param).forward(input_data)[1] + self.assertEqual(result.shape, expected_shape) + + +if __name__ == '__main__': + unittest.main() From f73e7e60520fa13472b343e91ba492e904b49902 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Tue, 14 Jan 2020 15:22:24 +0000 Subject: [PATCH 02/11] adding license info --- tests/test_unet.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_unet.py b/tests/test_unet.py index 362dd26626..ddd6bb9049 100644 --- a/tests/test_unet.py +++ b/tests/test_unet.py @@ -1,3 +1,14 @@ +# 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 unittest import torch From 9ef17c8fa505a0a7fe25fbd9c766ee0d998fb677 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Thu, 16 Jan 2020 14:53:23 +0000 Subject: [PATCH 03/11] Adding definitions for reading Nifti files in streams and stream transforms for selecting patches (windowing) --- examples/nifti_read_example.ipynb | 350 +++++++++++++++++++++++++ monai/__init__.py | 2 +- monai/data/readers/arrayreader.py | 7 +- monai/data/readers/niftireader.py | 82 ++++++ monai/data/streams/datastream.py | 49 +++- monai/data/streams/generators.py | 50 ++++ monai/data/transforms/patch_streams.py | 108 ++++++++ monai/utils/arrayutils.py | 105 +++++++- 8 files changed, 741 insertions(+), 12 deletions(-) create mode 100644 examples/nifti_read_example.ipynb create mode 100644 monai/data/readers/niftireader.py create mode 100644 monai/data/streams/generators.py create mode 100644 monai/data/transforms/patch_streams.py diff --git a/examples/nifti_read_example.ipynb b/examples/nifti_read_example.ipynb new file mode 100644 index 0000000000..eadc500016 --- /dev/null +++ b/examples/nifti_read_example.ipynb @@ -0,0 +1,350 @@ +{ + "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", + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "import torch\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "import sys\n", + "import glob\n", + "import tempfile\n", + "\n", + "import nibabel as nib\n", + "\n", + "sys.path.append('..')\n", + "\n", + "from monai import application, data, networks, utils\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=nib.Nifti1Image(im,np.eye(4))\n", + " nib.save(n,os.path.join(tempdir,'im%i.nii.gz'%i))\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 stream generator which yields the file paths according to a defined glob pattern:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[('/tmp/tmpm9s_qy4p/im0.nii.gz',), ('/tmp/tmpm9s_qy4p/im1.nii.gz',), ('/tmp/tmpm9s_qy4p/im2.nii.gz',), ('/tmp/tmpm9s_qy4p/im3.nii.gz',), ('/tmp/tmpm9s_qy4p/im4.nii.gz',)]\n" + ] + } + ], + "source": [ + "names=os.path.join(tempdir,'im*.nii.gz')\n", + "gsrc=data.streams.GlobPathGenerator(names,do_once=True)\n", + "\n", + "print(list(gsrc))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a reader which loads the nifti file names as they come from the source:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im0.nii.gz\n", + "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im1.nii.gz\n", + "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im2.nii.gz\n", + "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im3.nii.gz\n", + "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im4.nii.gz\n" + ] + } + ], + "source": [ + "src=data.readers.NiftiCacheReader(gsrc,5,image_only=False)\n", + "\n", + "for im in src:\n", + " vol,header=im\n", + " print(vol.shape,vol.dtype, header['filename_or_obj'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively create a path generator which reads two sets of names for the images and segmentation, then the reader to load these pairs:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(256, 256, 256) (256, 256, 256)\n", + "(256, 256, 256) (256, 256, 256)\n", + "(256, 256, 256) (256, 256, 256)\n", + "(256, 256, 256) (256, 256, 256)\n", + "(256, 256, 256) (256, 256, 256)\n" + ] + } + ], + "source": [ + "names=os.path.join(tempdir,'im*.nii.gz')\n", + "segs=os.path.join(tempdir,'seg*.nii.gz')\n", + "\n", + "gsrc=data.streams.GlobPathGenerator(names,segs,do_once=True)\n", + "\n", + "src=data.readers.NiftiCacheReader(gsrc,5,image_only=True)\n", + "\n", + "for im,seg in src:\n", + " print(im.shape,seg.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Filenames don't need to come from generators, a list of names also works:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['/tmp/tmpm9s_qy4p/im4.nii.gz', '/tmp/tmpm9s_qy4p/im2.nii.gz', '/tmp/tmpm9s_qy4p/im1.nii.gz', '/tmp/tmpm9s_qy4p/im0.nii.gz', '/tmp/tmpm9s_qy4p/im3.nii.gz']\n", + "Number of loaded images: 5\n" + ] + } + ], + "source": [ + "images=glob.glob(os.path.join(tempdir,'im*.nii.gz'))\n", + "print(images)\n", + "\n", + "src=data.readers.NiftiCacheReader(images,5,image_only=True)\n", + "\n", + "print('Number of loaded images:',len(tuple(src)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The stream transforms can then be applied to the images coming from the Nifti sources, eg. selecing each 2D image in the XY dimension:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(256,) (256,)\n" + ] + } + ], + "source": [ + "dimsrc=data.transforms.patch_streams.select_over_dimension(src)\n", + "\n", + "for xy in dimsrc:\n", + " print(xy[0].shape,xy[1].shape)\n", + " break # only need to see one" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also sample uniform patches from the read volumes:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(64, 64) (64, 64)\n" + ] + } + ], + "source": [ + "randsrc=data.transforms.patch_streams.uniform_random_patches(src)\n", + "\n", + "for patches in randsrc:\n", + " print(patches[0].shape,patches[1].shape)\n", + " break # only need to see one" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Putting it all together into a stream which loads the images only, iterates through the depth dimension of each, and selects 2 random patches from each 2D image:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(25, 44)\n", + "Number of 2D images: 2560\n" + ] + } + ], + "source": [ + "gsrc=data.streams.GlobPathGenerator(names,segs,do_once=True)\n", + "src=data.readers.NiftiCacheReader(gsrc,5,image_only=True)\n", + "src=data.transforms.patch_streams.select_over_dimension(src)\n", + "src=data.transforms.patch_streams.uniform_random_patches(src,(25,44),2)\n", + "\n", + "for im in src:\n", + " print(im[0].shape)\n", + " break\n", + " \n", + "# expected size is 5 * 256 * 2:\n", + "print('Number of 2D images:',len(tuple(src)))" + ] + } + ], + "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/__init__.py b/monai/__init__.py index 9dc1300c7b..e86508dd19 100644 --- a/monai/__init__.py +++ b/monai/__init__.py @@ -12,7 +12,7 @@ import os import sys -from .utils.moduleutils import load_submodules, loadSubmodules +from .utils.moduleutils import load_submodules __copyright__ = "(c) 2020 MONAI Consortium" __version__tuple__ = (0, 0, 1) diff --git a/monai/data/readers/arrayreader.py b/monai/data/readers/arrayreader.py index f7ec8792ae..3007b83fb7 100644 --- a/monai/data/readers/arrayreader.py +++ b/monai/data/readers/arrayreader.py @@ -13,12 +13,12 @@ import numpy as np -import monai from monai.data.streams import DataStream, OrderType from monai.utils.decorators import RestartGenerator +from monai.utils.moduleutils import export -@monai.utils.export("monai.data.readers") +@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 @@ -50,7 +50,8 @@ def yield_arrays(self): arrays = self.arrays choice_probs = self.choice_probs - indices = np.arange(arrays[0].shape[0] if arrays else 0) + min_len=min(a.shape[0] for a in arrays) if arrays else 0 + indices = np.arange(min_len) if self.order_type == OrderType.SHUFFLE: np.random.shuffle(indices) diff --git a/monai/data/readers/niftireader.py b/monai/data/readers/niftireader.py new file mode 100644 index 0000000000..133722e903 --- /dev/null +++ b/monai/data/readers/niftireader.py @@ -0,0 +1,82 @@ +# 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 monai.data.streams.datastream import CacheStream +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 NiftiCacheReader(CacheStream): + """ + Read Nifti files from incoming file names. Multiple filenames for data item can be defined which will load + multiple Nifti files. As this inherits from CacheStream this will cache nifti image volumes in their entirety. + The arguments for load() other than `names` must be passed to the constructor. + + Args: + src (Iterable): source iterable object + indices (tuple or None, optional): indices of values from source to load + 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 + """ + + def load(self, names, indices=None, as_closest_canonical=False, image_only=True, dtype=None): + if isinstance(names, str): + names = [names] + indices = [0] + else: + if len(names) == 1 and not isinstance(names[0],str): # names may be a tuple containing a single np.ndarray containing file names + names = names[0] + + indices=indices or list(range(len(names))) + + filenames = [names[i] for i in indices] + result = tuple(load_nifti(f, as_closest_canonical, image_only, dtype) for f in filenames) + + return result if len(result)>1 else result[0] + + diff --git a/monai/data/streams/datastream.py b/monai/data/streams/datastream.py index 755067aa22..1e2db094de 100644 --- a/monai/data/streams/datastream.py +++ b/monai/data/streams/datastream.py @@ -9,7 +9,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from functools import wraps +from functools import wraps, lru_cache import numpy as np @@ -262,6 +262,53 @@ def yield_alternating_values(self): can_continue = False +@export +@alias('cachestream') +class CacheStream(DataStream): + """ + Caches a fixed number of incoming items using lru-cache. The load() method is used to load items based on the input + values, by default this just returns the values themselves. + """ + + def __init__(self,src,cache_size,*load_args,**load_kwargs): + """ + Constructs a cache with the given input and cache size. The position and keyword arguments are passed to load() + when a items is requested to be cached and yielded. + + Args: + src (Iterable): input source iterable + cache_size (int): immutable cache size stating how many items to retain + load_args (tuple): arguments passed to load() + load_kwargs (dict): keyword arguments passed to load() + """ + + super().__init__(src) + + @lru_cache(maxsize=cache_size) + def _loader(vals): + return self.load(vals,*load_args,**load_kwargs) + + self._cache_loader=_loader + + def empty_cache(self): + """ + Empties all the cached items. + """ + self._cache_loader.cache_clear() + + def generate(self,vals): + """ + Yields an item loaded from the cache with `vals` as the input value. + """ + yield self._cache_loader(vals) + + def load(self,vals,*args,**kwargs): + """ + Loads an item based on `vals` and other defined arguments, the returned object will be cached internally. + """ + return vals + + @export class PrefetchStream(DataStream): """ diff --git a/monai/data/streams/generators.py b/monai/data/streams/generators.py new file mode 100644 index 0000000000..520b64ca64 --- /dev/null +++ b/monai/data/streams/generators.py @@ -0,0 +1,50 @@ +# 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 glob import glob + +import numpy as np + +from monai.data.readers.arrayreader import ArrayReader +from monai.data.streams.datastream import OrderType +from monai.utils.moduleutils import export + + +@export("monai.data.streams") +class GlobPathGenerator(ArrayReader): + """ + Generates file paths from given glob patterns, expanded using glob.glob. This will yield the file names as tuples + of strings, if multiple patterns are given the a file from each expansion is yielded in the tuple. + """ + + def __init__(self,*glob_paths, sort_paths=True, order_type=OrderType.LINEAR, do_once=False, choice_probs=None): + """ + Construct the generator using the given glob patterns `glob_paths`. If `sort_paths` is True each list of files + is sorted independently. + + Args: + glob_paths (list of str): list of glob patterns to expand + sort_paths (bool): if True, each file list is sorted + order_type (OrderType): the type of order to yield tuples in + do_once (bool): if True, the list of files is iterated through only once, indefinitely loops otherwise + choice_probs (np.ndarray): list of per-item probabilities for OrderType.CHOICE + """ + + expanded_paths=list(map(glob,glob_paths)) + if sort_paths: + expanded_paths=list(map(sorted,expanded_paths)) + + expanded_paths=list(map(np.asarray,expanded_paths)) + + super().__init__(*expanded_paths,order_type=order_type, do_once=do_once, choice_probs=choice_probs) + self.glob_paths=glob_paths + self.sort_paths=sort_paths + \ No newline at end of file diff --git a/monai/data/transforms/patch_streams.py b/monai/data/transforms/patch_streams.py new file mode 100644 index 0000000000..765cace615 --- /dev/null +++ b/monai/data/transforms/patch_streams.py @@ -0,0 +1,108 @@ +# 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 + +from monai.data.streams.datastream import streamgen +from monai.utils.arrayutils import get_valid_patch_size, iter_patch + + +@streamgen +def select_over_dimension(imgs,dim=-1, indices=None): + """ + Select and yield data from the images in `imgs` by iterating over the selected dimension. This will yield images + with one fewer dimension than the inputs. + + Args: + imgs (tuple): tuple of np.ndarrays of 2+ dimensions + dim (int, optional): dimension to iterate over, default is last dimension + indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all + + Yields: + Arrays chosen the members of `imgs` with one fewer dimension, iterating over dimension `dim` in order + """ + # select only certain images to iterate over + indices = indices or list(range(len(imgs))) + imgs = [imgs[i] for i in indices] + + slices=[slice(None)]*imgs[0].ndim # define slices selecting the whole image + + for i in range(imgs[0].shape[dim]): + slices[dim]=i # select index in dimension + yield tuple(im[tuple(slices)] for im in imgs) + + +@streamgen +def uniform_random_patches(imgs, patch_size=64, num_patches=10, indices=None): + """ + Choose patches from the input image(s) of a given size at random. The choice of patch position is uniformly + distributed over the image. + + Args: + imgs (tuple): tuple of np.ndarrays of 2+ dimensions + patch_size (int or tuple, optional): a single dimension or a tuple of dimension indicating the patch size, this + can be a different dimensionality from the source image to produce smaller dimension patches, and None or 0 + can be used to select the whole dimension from the input image + num_patches (int, optional): number of patches to produce per image set + indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all + + Yields: + Patches from the source image(s) from uniformly random positions of size specified by `patch_size` + """ + + # select only certain images to iterate over + indices = indices or list(range(len(imgs))) + imgs = [imgs[i] for i in indices] + + patch_size = get_valid_patch_size(imgs[0].shape, patch_size) + + for _ in range(num_patches): + # choose the minimal corner of the patch to yield + min_corner = tuple(np.random.randint(0, ms - ps) if ms > ps else 0 for ms, ps in zip(imgs[0].shape, patch_size)) + + # create the slices for each dimension which define the patch in the source volume + slices = tuple(slice(mc, mc + ps) for mc, ps in zip(min_corner, patch_size)) + + # select out a patch from each image volume + yield tuple(im[slices] for im in imgs) + + +@streamgen +def ordered_patches(imgs, patch_size=64, start_pos=(), indices=None, pad_mode="wrap", **pad_opts): + """ + Choose patches from the input image(s) of a given size in a contiguous grid. Patches are selected iterating by the + patch size in the first dimension, followed by second, etc. This allows the sampling of images in a uniform grid- + wise manner that ensures the whole image is visited. The images can be padded to include margins if the patch size + is not an even multiple of the image size. A start position can also be specified to start the iteration from a + position other than 0. + + Args: + imgs (tuple): tuple of np.ndarrays of 2+ dimensions + patch_size (int or tuple, optional): a single dimension or a tuple of dimension indicating the patch size, this + can be a different dimensionality from the source image to produce smaller dimension patches, and None or 0 + can be used to select the whole dimension from the input image + start_pos (tuple, optional): starting position in the image, default is 0 in each dimension + indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all + pad_mode (str, optional): padding mode, see numpy.pad + pad_opts (dict, optional): padding options, see numpy.pad + + Yields: + Patches from the source image(s) in grid ordering of size specified by `patch_size` + """ + + # select only certain images to iterate over + indices = indices or list(range(len(imgs))) + imgs = [imgs[i] for i in indices] + + iters = [iter_patch(i, patch_size, start_pos, False, pad_mode, **pad_opts) for i in imgs] + + yield from zip(*iters) diff --git a/monai/utils/arrayutils.py b/monai/utils/arrayutils.py index ecafa92e9b..a400eec1fa 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,99 @@ 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 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) + startPos = 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] From 4df6f0919d12204fc5a8012cafa9d59104eb9984 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Thu, 16 Jan 2020 14:59:30 +0000 Subject: [PATCH 04/11] Remove tests --- tests/test_unet.py | 54 ---------------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 tests/test_unet.py diff --git a/tests/test_unet.py b/tests/test_unet.py deleted file mode 100644 index ddd6bb9049..0000000000 --- a/tests/test_unet.py +++ /dev/null @@ -1,54 +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 unittest - -import torch -from parameterized import parameterized - -from monai.networks.nets.unet import UNet - - -class TestUNET(unittest.TestCase): - - @parameterized.expand([ - [ - { - 'dimensions': 2, - 'in_channels': 1, - 'num_classes': 3, - 'channels': (16, 32, 64), - 'strides': (2, 2), - 'num_res_units': 1, - }, - torch.randn(16, 1, 32, 32), - (16, 32, 32), - ], - [ - { - 'dimensions': 3, - 'in_channels': 1, - 'num_classes': 3, - 'channels': (16, 32, 64), - 'strides': (2, 2), - 'num_res_units': 1, - }, - torch.randn(16, 1, 32, 32, 32), - (16, 32, 32, 32), - ], - ]) - def test_shape(self, input_param, input_data, expected_shape): - result = UNet(**input_param).forward(input_data)[1] - self.assertEqual(result.shape, expected_shape) - - -if __name__ == '__main__': - unittest.main() From 95ec7f3acf1048aaae52569e393ba3d6e1e9af3e Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Thu, 16 Jan 2020 15:29:23 +0000 Subject: [PATCH 05/11] Renaming fix --- .gitignore | 1 + monai/data/readers/arrayreader.py | 2 +- monai/data/readers/niftireader.py | 19 ++++++++------- monai/data/streams/datastream.py | 32 +++++++++++++------------- monai/data/streams/generators.py | 21 ++++++++--------- monai/data/transforms/patch_streams.py | 14 +++++------ monai/utils/arrayutils.py | 6 ++--- 7 files changed, 47 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index 9949bc981c..c30f242fd2 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,4 @@ venv.bak/ # mypy .mypy_cache/ examples/scd_lvsegs.npz +.idea/ diff --git a/monai/data/readers/arrayreader.py b/monai/data/readers/arrayreader.py index 3007b83fb7..ad44655be1 100644 --- a/monai/data/readers/arrayreader.py +++ b/monai/data/readers/arrayreader.py @@ -50,7 +50,7 @@ def yield_arrays(self): arrays = self.arrays choice_probs = self.choice_probs - min_len=min(a.shape[0] for a in arrays) if arrays else 0 + min_len = min(a.shape[0] for a in arrays) if arrays else 0 indices = np.arange(min_len) if self.order_type == OrderType.SHUFFLE: diff --git a/monai/data/readers/niftireader.py b/monai/data/readers/niftireader.py index 133722e903..9073b2cd23 100644 --- a/monai/data/readers/niftireader.py +++ b/monai/data/readers/niftireader.py @@ -12,7 +12,7 @@ import numpy as np import nibabel as nib -from monai.data.streams.datastream import CacheStream +from monai.data.streams.datastream import LRUCacheStream from monai.utils.moduleutils import export @@ -39,8 +39,8 @@ def load_nifti(filename_or_obj, as_closest_canonical=False, image_only=True, dty dat = img.get_fdata(dtype=dtype) else: dat = np.asanyarray(img.dataobj) - - header=dict(img.header) + + header = dict(img.header) header['filename_or_obj'] = filename_or_obj if image_only: @@ -50,7 +50,7 @@ def load_nifti(filename_or_obj, as_closest_canonical=False, image_only=True, dty @export("monai.data.readers") -class NiftiCacheReader(CacheStream): +class NiftiCacheReader(LRUCacheStream): """ Read Nifti files from incoming file names. Multiple filenames for data item can be defined which will load multiple Nifti files. As this inherits from CacheStream this will cache nifti image volumes in their entirety. @@ -69,14 +69,13 @@ def load(self, names, indices=None, as_closest_canonical=False, image_only=True, names = [names] indices = [0] else: - if len(names) == 1 and not isinstance(names[0],str): # names may be a tuple containing a single np.ndarray containing file names + # names may be a tuple containing a single np.ndarray containing file names + if len(names) == 1 and not isinstance(names[0], str): names = names[0] - indices=indices or list(range(len(names))) - + indices = indices or list(range(len(names))) + filenames = [names[i] for i in indices] result = tuple(load_nifti(f, as_closest_canonical, image_only, dtype) for f in filenames) - - return result if len(result)>1 else result[0] - + return result if len(result) > 1 else result[0] diff --git a/monai/data/streams/datastream.py b/monai/data/streams/datastream.py index 1e2db094de..762bcc9f0d 100644 --- a/monai/data/streams/datastream.py +++ b/monai/data/streams/datastream.py @@ -263,14 +263,14 @@ def yield_alternating_values(self): @export -@alias('cachestream') -class CacheStream(DataStream): +@alias('lrucachestream') +class LRUCacheStream(DataStream): """ Caches a fixed number of incoming items using lru-cache. The load() method is used to load items based on the input values, by default this just returns the values themselves. """ - - def __init__(self,src,cache_size,*load_args,**load_kwargs): + + def __init__(self, src, cache_size, *load_args, **load_kwargs): """ Constructs a cache with the given input and cache size. The position and keyword arguments are passed to load() when a items is requested to be cached and yielded. @@ -281,34 +281,34 @@ def __init__(self,src,cache_size,*load_args,**load_kwargs): load_args (tuple): arguments passed to load() load_kwargs (dict): keyword arguments passed to load() """ - + super().__init__(src) - + @lru_cache(maxsize=cache_size) def _loader(vals): - return self.load(vals,*load_args,**load_kwargs) - - self._cache_loader=_loader - + return self.load(vals, *load_args, **load_kwargs) + + self._cache_loader = _loader + def empty_cache(self): """ Empties all the cached items. """ self._cache_loader.cache_clear() - - def generate(self,vals): + + def generate(self, vals): """ Yields an item loaded from the cache with `vals` as the input value. """ yield self._cache_loader(vals) - - def load(self,vals,*args,**kwargs): + + def load(self, vals, *args, **kwargs): """ Loads an item based on `vals` and other defined arguments, the returned object will be cached internally. """ return vals - - + + @export class PrefetchStream(DataStream): """ diff --git a/monai/data/streams/generators.py b/monai/data/streams/generators.py index 520b64ca64..6199019a82 100644 --- a/monai/data/streams/generators.py +++ b/monai/data/streams/generators.py @@ -24,8 +24,8 @@ class GlobPathGenerator(ArrayReader): Generates file paths from given glob patterns, expanded using glob.glob. This will yield the file names as tuples of strings, if multiple patterns are given the a file from each expansion is yielded in the tuple. """ - - def __init__(self,*glob_paths, sort_paths=True, order_type=OrderType.LINEAR, do_once=False, choice_probs=None): + + def __init__(self, *glob_paths, sort_paths=True, order_type=OrderType.LINEAR, do_once=False, choice_probs=None): """ Construct the generator using the given glob patterns `glob_paths`. If `sort_paths` is True each list of files is sorted independently. @@ -37,14 +37,13 @@ def __init__(self,*glob_paths, sort_paths=True, order_type=OrderType.LINEAR, do_ do_once (bool): if True, the list of files is iterated through only once, indefinitely loops otherwise choice_probs (np.ndarray): list of per-item probabilities for OrderType.CHOICE """ - - expanded_paths=list(map(glob,glob_paths)) + + expanded_paths = list(map(glob, glob_paths)) if sort_paths: - expanded_paths=list(map(sorted,expanded_paths)) + expanded_paths = list(map(sorted, expanded_paths)) - expanded_paths=list(map(np.asarray,expanded_paths)) - - super().__init__(*expanded_paths,order_type=order_type, do_once=do_once, choice_probs=choice_probs) - self.glob_paths=glob_paths - self.sort_paths=sort_paths - \ No newline at end of file + expanded_paths = list(map(np.asarray, expanded_paths)) + + super().__init__(*expanded_paths, order_type=order_type, do_once=do_once, choice_probs=choice_probs) + self.glob_paths = glob_paths + self.sort_paths = sort_paths diff --git a/monai/data/transforms/patch_streams.py b/monai/data/transforms/patch_streams.py index 765cace615..3e6edae6d0 100644 --- a/monai/data/transforms/patch_streams.py +++ b/monai/data/transforms/patch_streams.py @@ -17,7 +17,7 @@ @streamgen -def select_over_dimension(imgs,dim=-1, indices=None): +def select_over_dimension(imgs, dim=-1, indices=None): """ Select and yield data from the images in `imgs` by iterating over the selected dimension. This will yield images with one fewer dimension than the inputs. @@ -33,13 +33,13 @@ def select_over_dimension(imgs,dim=-1, indices=None): # select only certain images to iterate over indices = indices or list(range(len(imgs))) imgs = [imgs[i] for i in indices] - - slices=[slice(None)]*imgs[0].ndim # define slices selecting the whole image - + + slices = [slice(None)] * imgs[0].ndim # define slices selecting the whole image + for i in range(imgs[0].shape[dim]): - slices[dim]=i # select index in dimension + slices[dim] = i # select index in dimension yield tuple(im[tuple(slices)] for im in imgs) - + @streamgen def uniform_random_patches(imgs, patch_size=64, num_patches=10, indices=None): @@ -98,7 +98,7 @@ def ordered_patches(imgs, patch_size=64, start_pos=(), indices=None, pad_mode="w Yields: Patches from the source image(s) in grid ordering of size specified by `patch_size` """ - + # select only certain images to iterate over indices = indices or list(range(len(imgs))) imgs = [imgs[i] for i in indices] diff --git a/monai/utils/arrayutils.py b/monai/utils/arrayutils.py index a400eec1fa..517427e43a 100644 --- a/monai/utils/arrayutils.py +++ b/monai/utils/arrayutils.py @@ -120,9 +120,9 @@ 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: # dimension before midpoint, clip to size fitting in both arrays - d1 = np.clip(dim // 2, 0, min(sc, dc)) + 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)) + 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) @@ -142,7 +142,7 @@ def resize_center(img, *resize_dims, fill_value=0): dest = np.full(resize_dims, fill_value, img.dtype) 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] From d19717a852cff65b50a19644df30395538324dad Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Thu, 16 Jan 2020 15:32:57 +0000 Subject: [PATCH 06/11] Update arrayutils.py --- monai/utils/arrayutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/utils/arrayutils.py b/monai/utils/arrayutils.py index 517427e43a..ea1b642fbb 100644 --- a/monai/utils/arrayutils.py +++ b/monai/utils/arrayutils.py @@ -187,7 +187,7 @@ def iter_patch_slices(dims, patch_size, start_pos=()): # ensure patchSize and startPos are the right length ndim = len(dims) patch_size = get_valid_patch_size(dims, patch_size) - startPos = ensure_tuple_size(start_pos, ndim) + 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))) From f8ddf7b7a81a501a06e137e2d60d38639491c304 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Thu, 16 Jan 2020 16:03:58 +0000 Subject: [PATCH 07/11] Removed blank lines in comments --- monai/data/readers/niftireader.py | 6 +++--- monai/data/streams/datastream.py | 2 +- monai/data/streams/generators.py | 2 +- monai/data/transforms/patch_streams.py | 12 ++++++------ monai/utils/arrayutils.py | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/monai/data/readers/niftireader.py b/monai/data/readers/niftireader.py index 9073b2cd23..e16e310ea5 100644 --- a/monai/data/readers/niftireader.py +++ b/monai/data/readers/niftireader.py @@ -19,13 +19,13 @@ 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 @@ -55,7 +55,7 @@ class NiftiCacheReader(LRUCacheStream): Read Nifti files from incoming file names. Multiple filenames for data item can be defined which will load multiple Nifti files. As this inherits from CacheStream this will cache nifti image volumes in their entirety. The arguments for load() other than `names` must be passed to the constructor. - + Args: src (Iterable): source iterable object indices (tuple or None, optional): indices of values from source to load diff --git a/monai/data/streams/datastream.py b/monai/data/streams/datastream.py index 762bcc9f0d..ddd67c8ab1 100644 --- a/monai/data/streams/datastream.py +++ b/monai/data/streams/datastream.py @@ -274,7 +274,7 @@ def __init__(self, src, cache_size, *load_args, **load_kwargs): """ Constructs a cache with the given input and cache size. The position and keyword arguments are passed to load() when a items is requested to be cached and yielded. - + Args: src (Iterable): input source iterable cache_size (int): immutable cache size stating how many items to retain diff --git a/monai/data/streams/generators.py b/monai/data/streams/generators.py index 6199019a82..626f06594a 100644 --- a/monai/data/streams/generators.py +++ b/monai/data/streams/generators.py @@ -29,7 +29,7 @@ def __init__(self, *glob_paths, sort_paths=True, order_type=OrderType.LINEAR, do """ Construct the generator using the given glob patterns `glob_paths`. If `sort_paths` is True each list of files is sorted independently. - + Args: glob_paths (list of str): list of glob patterns to expand sort_paths (bool): if True, each file list is sorted diff --git a/monai/data/transforms/patch_streams.py b/monai/data/transforms/patch_streams.py index 3e6edae6d0..26b2124031 100644 --- a/monai/data/transforms/patch_streams.py +++ b/monai/data/transforms/patch_streams.py @@ -21,12 +21,12 @@ def select_over_dimension(imgs, dim=-1, indices=None): """ Select and yield data from the images in `imgs` by iterating over the selected dimension. This will yield images with one fewer dimension than the inputs. - + Args: imgs (tuple): tuple of np.ndarrays of 2+ dimensions dim (int, optional): dimension to iterate over, default is last dimension indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all - + Yields: Arrays chosen the members of `imgs` with one fewer dimension, iterating over dimension `dim` in order """ @@ -46,7 +46,7 @@ def uniform_random_patches(imgs, patch_size=64, num_patches=10, indices=None): """ Choose patches from the input image(s) of a given size at random. The choice of patch position is uniformly distributed over the image. - + Args: imgs (tuple): tuple of np.ndarrays of 2+ dimensions patch_size (int or tuple, optional): a single dimension or a tuple of dimension indicating the patch size, this @@ -54,7 +54,7 @@ def uniform_random_patches(imgs, patch_size=64, num_patches=10, indices=None): can be used to select the whole dimension from the input image num_patches (int, optional): number of patches to produce per image set indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all - + Yields: Patches from the source image(s) from uniformly random positions of size specified by `patch_size` """ @@ -84,7 +84,7 @@ def ordered_patches(imgs, patch_size=64, start_pos=(), indices=None, pad_mode="w wise manner that ensures the whole image is visited. The images can be padded to include margins if the patch size is not an even multiple of the image size. A start position can also be specified to start the iteration from a position other than 0. - + Args: imgs (tuple): tuple of np.ndarrays of 2+ dimensions patch_size (int or tuple, optional): a single dimension or a tuple of dimension indicating the patch size, this @@ -94,7 +94,7 @@ def ordered_patches(imgs, patch_size=64, start_pos=(), indices=None, pad_mode="w indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all pad_mode (str, optional): padding mode, see numpy.pad pad_opts (dict, optional): padding options, see numpy.pad - + Yields: Patches from the source image(s) in grid ordering of size specified by `patch_size` """ diff --git a/monai/utils/arrayutils.py b/monai/utils/arrayutils.py index ea1b642fbb..67156e800d 100644 --- a/monai/utils/arrayutils.py +++ b/monai/utils/arrayutils.py @@ -175,12 +175,12 @@ 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 """ @@ -202,7 +202,7 @@ def iter_patch(arr, patch_size, start_pos=(), copy_back=True, pad_mode="wrap", * 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 @@ -210,7 +210,7 @@ def iter_patch(arr, patch_size, start_pos=(), copy_back=True, pad_mode="wrap", * 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 From a95fc4e67ceee7ce0be69858a3423a3ad802e87e Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Tue, 21 Jan 2020 16:03:26 +0000 Subject: [PATCH 08/11] Added Dataset based Nifti reader, grid patch sampler, and transforms --- examples/nifti_read_example.ipynb | 241 +++++--------------- monai/data/readers/niftireader.py | 67 ++++-- monai/data/transforms/dataset_transforms.py | 83 +++++++ monai/data/transforms/grid_dataset.py | 66 ++++++ monai/data/transforms/patch_streams.py | 108 --------- monai/utils/arrayutils.py | 22 ++ 6 files changed, 276 insertions(+), 311 deletions(-) create mode 100644 monai/data/transforms/dataset_transforms.py create mode 100644 monai/data/transforms/grid_dataset.py delete mode 100644 monai/data/transforms/patch_streams.py diff --git a/examples/nifti_read_example.ipynb b/examples/nifti_read_example.ipynb index eadc500016..9636c7275f 100644 --- a/examples/nifti_read_example.ipynb +++ b/examples/nifti_read_example.ipynb @@ -28,22 +28,26 @@ ], "source": [ "%matplotlib inline\n", - "%load_ext autoreload\n", - "%autoreload 2\n", "\n", - "import torch\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", "import os\n", "import sys\n", - "import glob\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", - "sys.path.append('..')\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()" ] @@ -90,7 +94,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Create a number of test Nifti files " + "Create a number of test Nifti files:" ] }, { @@ -99,21 +103,23 @@ "metadata": {}, "outputs": [], "source": [ - "tempdir=tempfile.mkdtemp()\n", + "tempdir = tempfile.mkdtemp()\n", "\n", "for i in range(5):\n", - " im,seg=create_test_image_3d(256,256,256)\n", - " n=nib.Nifti1Image(im,np.eye(4))\n", - " nib.save(n,os.path.join(tempdir,'im%i.nii.gz'%i))\n", - " n=nib.Nifti1Image(seg,np.eye(4))\n", - " nib.save(n,os.path.join(tempdir,'seg%i.nii.gz'%i))" + " 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 stream generator which yields the file paths according to a defined glob pattern:" + "Create a data loader which yields uniform random patches from loaded Nifti files:" ] }, { @@ -125,22 +131,39 @@ "name": "stdout", "output_type": "stream", "text": [ - "[('/tmp/tmpm9s_qy4p/im0.nii.gz',), ('/tmp/tmpm9s_qy4p/im1.nii.gz',), ('/tmp/tmpm9s_qy4p/im2.nii.gz',), ('/tmp/tmpm9s_qy4p/im3.nii.gz',), ('/tmp/tmpm9s_qy4p/im4.nii.gz',)]\n" + "torch.Size([5, 1, 64, 64, 64]) torch.Size([5, 1, 64, 64, 64])\n" ] } ], "source": [ - "names=os.path.join(tempdir,'im*.nii.gz')\n", - "gsrc=data.streams.GlobPathGenerator(names,do_once=True)\n", + "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", - "print(list(gsrc))" + "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": [ - "Create a reader which loads the nifti file names as they come from the source:" + "Alternatively create a data loader which yields patches in regular grid order from loaded images:" ] }, { @@ -152,177 +175,37 @@ "name": "stdout", "output_type": "stream", "text": [ - "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im0.nii.gz\n", - "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im1.nii.gz\n", - "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im2.nii.gz\n", - "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im3.nii.gz\n", - "(256, 256, 256) float32 /tmp/tmpm9s_qy4p/im4.nii.gz\n" + "torch.Size([10, 1, 64, 64, 64]) torch.Size([10, 1, 64, 64, 64])\n" ] } ], "source": [ - "src=data.readers.NiftiCacheReader(gsrc,5,image_only=False)\n", + "imtrans=transforms.Compose([\n", + " Rescale(),\n", + " AddChannel(),\n", + " ToTensor()\n", + "]) \n", "\n", - "for im in src:\n", - " vol,header=im\n", - " print(vol.shape,vol.dtype, header['filename_or_obj'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Alternatively create a path generator which reads two sets of names for the images and segmentation, then the reader to load these pairs:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(256, 256, 256) (256, 256, 256)\n", - "(256, 256, 256) (256, 256, 256)\n", - "(256, 256, 256) (256, 256, 256)\n", - "(256, 256, 256) (256, 256, 256)\n", - "(256, 256, 256) (256, 256, 256)\n" - ] - } - ], - "source": [ - "names=os.path.join(tempdir,'im*.nii.gz')\n", - "segs=os.path.join(tempdir,'seg*.nii.gz')\n", - "\n", - "gsrc=data.streams.GlobPathGenerator(names,segs,do_once=True)\n", - "\n", - "src=data.readers.NiftiCacheReader(gsrc,5,image_only=True)\n", - "\n", - "for im,seg in src:\n", - " print(im.shape,seg.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Filenames don't need to come from generators, a list of names also works:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['/tmp/tmpm9s_qy4p/im4.nii.gz', '/tmp/tmpm9s_qy4p/im2.nii.gz', '/tmp/tmpm9s_qy4p/im1.nii.gz', '/tmp/tmpm9s_qy4p/im0.nii.gz', '/tmp/tmpm9s_qy4p/im3.nii.gz']\n", - "Number of loaded images: 5\n" - ] - } - ], - "source": [ - "images=glob.glob(os.path.join(tempdir,'im*.nii.gz'))\n", - "print(images)\n", - "\n", - "src=data.readers.NiftiCacheReader(images,5,image_only=True)\n", - "\n", - "print('Number of loaded images:',len(tuple(src)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The stream transforms can then be applied to the images coming from the Nifti sources, eg. selecing each 2D image in the XY dimension:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(256,) (256,)\n" - ] - } - ], - "source": [ - "dimsrc=data.transforms.patch_streams.select_over_dimension(src)\n", - "\n", - "for xy in dimsrc:\n", - " print(xy[0].shape,xy[1].shape)\n", - " break # only need to see one" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also sample uniform patches from the read volumes:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(64, 64) (64, 64)\n" - ] - } - ], - "source": [ - "randsrc=data.transforms.patch_streams.uniform_random_patches(src)\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", - "for patches in randsrc:\n", - " print(patches[0].shape,patches[1].shape)\n", - " break # only need to see one" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Putting it all together into a stream which loads the images only, iterates through the depth dimension of each, and selects 2 random patches from each 2D image:" + "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": 10, + "execution_count": 6, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(25, 44)\n", - "Number of 2D images: 2560\n" - ] - } - ], + "outputs": [], "source": [ - "gsrc=data.streams.GlobPathGenerator(names,segs,do_once=True)\n", - "src=data.readers.NiftiCacheReader(gsrc,5,image_only=True)\n", - "src=data.transforms.patch_streams.select_over_dimension(src)\n", - "src=data.transforms.patch_streams.uniform_random_patches(src,(25,44),2)\n", - "\n", - "for im in src:\n", - " print(im[0].shape)\n", - " break\n", - " \n", - "# expected size is 5 * 256 * 2:\n", - "print('Number of 2D images:',len(tuple(src)))" + "!rm -rf {tempdir}" ] } ], diff --git a/monai/data/readers/niftireader.py b/monai/data/readers/niftireader.py index e16e310ea5..34622819ca 100644 --- a/monai/data/readers/niftireader.py +++ b/monai/data/readers/niftireader.py @@ -11,8 +11,10 @@ import numpy as np import nibabel as nib +import random + +from torch.utils.data import Dataset -from monai.data.streams.datastream import LRUCacheStream from monai.utils.moduleutils import export @@ -30,6 +32,7 @@ def load_nifti(filename_or_obj, as_closest_canonical=False, image_only=True, dty 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: @@ -50,32 +53,48 @@ def load_nifti(filename_or_obj, as_closest_canonical=False, image_only=True, dty @export("monai.data.readers") -class NiftiCacheReader(LRUCacheStream): +class NiftiDataset(Dataset): """ - Read Nifti files from incoming file names. Multiple filenames for data item can be defined which will load - multiple Nifti files. As this inherits from CacheStream this will cache nifti image volumes in their entirety. - The arguments for load() other than `names` must be passed to the constructor. - - Args: - src (Iterable): source iterable object - indices (tuple or None, optional): indices of values from source to load - 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 + Loads image/segmentation pairs of Nifti files from the given filename lists. Transformations can be specified + for the image and segmentation arrays separately. """ - def load(self, names, indices=None, as_closest_canonical=False, image_only=True, dtype=None): - if isinstance(names, str): - names = [names] - indices = [0] - else: - # names may be a tuple containing a single np.ndarray containing file names - if len(names) == 1 and not isinstance(names[0], str): - names = names[0] + 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) - indices = indices or list(range(len(names))) + if self.transform is not None: + random.seed(seed) + img = self.transform(img) - filenames = [names[i] for i in indices] - result = tuple(load_nifti(f, as_closest_canonical, image_only, dtype) for f in filenames) + 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 result if len(result) > 1 else result[0] + return img, seg 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/patch_streams.py b/monai/data/transforms/patch_streams.py deleted file mode 100644 index 26b2124031..0000000000 --- a/monai/data/transforms/patch_streams.py +++ /dev/null @@ -1,108 +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 - -from monai.data.streams.datastream import streamgen -from monai.utils.arrayutils import get_valid_patch_size, iter_patch - - -@streamgen -def select_over_dimension(imgs, dim=-1, indices=None): - """ - Select and yield data from the images in `imgs` by iterating over the selected dimension. This will yield images - with one fewer dimension than the inputs. - - Args: - imgs (tuple): tuple of np.ndarrays of 2+ dimensions - dim (int, optional): dimension to iterate over, default is last dimension - indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all - - Yields: - Arrays chosen the members of `imgs` with one fewer dimension, iterating over dimension `dim` in order - """ - # select only certain images to iterate over - indices = indices or list(range(len(imgs))) - imgs = [imgs[i] for i in indices] - - slices = [slice(None)] * imgs[0].ndim # define slices selecting the whole image - - for i in range(imgs[0].shape[dim]): - slices[dim] = i # select index in dimension - yield tuple(im[tuple(slices)] for im in imgs) - - -@streamgen -def uniform_random_patches(imgs, patch_size=64, num_patches=10, indices=None): - """ - Choose patches from the input image(s) of a given size at random. The choice of patch position is uniformly - distributed over the image. - - Args: - imgs (tuple): tuple of np.ndarrays of 2+ dimensions - patch_size (int or tuple, optional): a single dimension or a tuple of dimension indicating the patch size, this - can be a different dimensionality from the source image to produce smaller dimension patches, and None or 0 - can be used to select the whole dimension from the input image - num_patches (int, optional): number of patches to produce per image set - indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all - - Yields: - Patches from the source image(s) from uniformly random positions of size specified by `patch_size` - """ - - # select only certain images to iterate over - indices = indices or list(range(len(imgs))) - imgs = [imgs[i] for i in indices] - - patch_size = get_valid_patch_size(imgs[0].shape, patch_size) - - for _ in range(num_patches): - # choose the minimal corner of the patch to yield - min_corner = tuple(np.random.randint(0, ms - ps) if ms > ps else 0 for ms, ps in zip(imgs[0].shape, patch_size)) - - # create the slices for each dimension which define the patch in the source volume - slices = tuple(slice(mc, mc + ps) for mc, ps in zip(min_corner, patch_size)) - - # select out a patch from each image volume - yield tuple(im[slices] for im in imgs) - - -@streamgen -def ordered_patches(imgs, patch_size=64, start_pos=(), indices=None, pad_mode="wrap", **pad_opts): - """ - Choose patches from the input image(s) of a given size in a contiguous grid. Patches are selected iterating by the - patch size in the first dimension, followed by second, etc. This allows the sampling of images in a uniform grid- - wise manner that ensures the whole image is visited. The images can be padded to include margins if the patch size - is not an even multiple of the image size. A start position can also be specified to start the iteration from a - position other than 0. - - Args: - imgs (tuple): tuple of np.ndarrays of 2+ dimensions - patch_size (int or tuple, optional): a single dimension or a tuple of dimension indicating the patch size, this - can be a different dimensionality from the source image to produce smaller dimension patches, and None or 0 - can be used to select the whole dimension from the input image - start_pos (tuple, optional): starting position in the image, default is 0 in each dimension - indices (None or tuple, optional): indices for which arrays in `imgs` to produce patches for, None for all - pad_mode (str, optional): padding mode, see numpy.pad - pad_opts (dict, optional): padding options, see numpy.pad - - Yields: - Patches from the source image(s) in grid ordering of size specified by `patch_size` - """ - - # select only certain images to iterate over - indices = indices or list(range(len(imgs))) - imgs = [imgs[i] for i in indices] - - iters = [iter_patch(i, patch_size, start_pos, False, pad_mode, **pad_opts) for i in imgs] - - yield from zip(*iters) diff --git a/monai/utils/arrayutils.py b/monai/utils/arrayutils.py index 67156e800d..79cf96ffb5 100644 --- a/monai/utils/arrayutils.py +++ b/monai/utils/arrayutils.py @@ -170,6 +170,27 @@ def get_valid_patch_size(dims, patch_size): 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 @@ -184,6 +205,7 @@ def iter_patch_slices(dims, patch_size, start_pos=()): 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) From d14431ba80a92a5e97188abcd9f40babb77024cb Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Tue, 21 Jan 2020 18:20:27 +0000 Subject: [PATCH 09/11] Added example segmentation notebook --- examples/unet_segmentation_3d.ipynb | 241 ++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 examples/unet_segmentation_3d.ipynb 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 +} From 102fece63689b371886accf704a3c25467936119 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 22 Jan 2020 12:52:08 +0000 Subject: [PATCH 10/11] Cleanup deletion --- 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 | 116 ------ monai/data/readers/npzreader.py | 41 -- monai/data/streams/__init__.py | 10 - monai/data/streams/datastream.py | 371 ------------------ monai/data/streams/generators.py | 49 --- monai/data/streams/threadbufferstream.py | 71 ---- 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 --- 18 files changed, 1 insertion(+), 1570 deletions(-) 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 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/generators.py delete mode 100644 monai/data/streams/threadbufferstream.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/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 ad44655be1..0000000000 --- a/monai/data/readers/arrayreader.py +++ /dev/null @@ -1,116 +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 - -from monai.data.streams import DataStream, OrderType -from monai.utils.decorators import RestartGenerator -from monai.utils.moduleutils import export - - -@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 - - min_len = min(a.shape[0] for a in arrays) if arrays else 0 - indices = np.arange(min_len) - - 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/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 ddd67c8ab1..0000000000 --- a/monai/data/streams/datastream.py +++ /dev/null @@ -1,371 +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, lru_cache - -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 -@alias('lrucachestream') -class LRUCacheStream(DataStream): - """ - Caches a fixed number of incoming items using lru-cache. The load() method is used to load items based on the input - values, by default this just returns the values themselves. - """ - - def __init__(self, src, cache_size, *load_args, **load_kwargs): - """ - Constructs a cache with the given input and cache size. The position and keyword arguments are passed to load() - when a items is requested to be cached and yielded. - - Args: - src (Iterable): input source iterable - cache_size (int): immutable cache size stating how many items to retain - load_args (tuple): arguments passed to load() - load_kwargs (dict): keyword arguments passed to load() - """ - - super().__init__(src) - - @lru_cache(maxsize=cache_size) - def _loader(vals): - return self.load(vals, *load_args, **load_kwargs) - - self._cache_loader = _loader - - def empty_cache(self): - """ - Empties all the cached items. - """ - self._cache_loader.cache_clear() - - def generate(self, vals): - """ - Yields an item loaded from the cache with `vals` as the input value. - """ - yield self._cache_loader(vals) - - def load(self, vals, *args, **kwargs): - """ - Loads an item based on `vals` and other defined arguments, the returned object will be cached internally. - """ - return vals - - -@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/generators.py b/monai/data/streams/generators.py deleted file mode 100644 index 626f06594a..0000000000 --- a/monai/data/streams/generators.py +++ /dev/null @@ -1,49 +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 glob import glob - -import numpy as np - -from monai.data.readers.arrayreader import ArrayReader -from monai.data.streams.datastream import OrderType -from monai.utils.moduleutils import export - - -@export("monai.data.streams") -class GlobPathGenerator(ArrayReader): - """ - Generates file paths from given glob patterns, expanded using glob.glob. This will yield the file names as tuples - of strings, if multiple patterns are given the a file from each expansion is yielded in the tuple. - """ - - def __init__(self, *glob_paths, sort_paths=True, order_type=OrderType.LINEAR, do_once=False, choice_probs=None): - """ - Construct the generator using the given glob patterns `glob_paths`. If `sort_paths` is True each list of files - is sorted independently. - - Args: - glob_paths (list of str): list of glob patterns to expand - sort_paths (bool): if True, each file list is sorted - order_type (OrderType): the type of order to yield tuples in - do_once (bool): if True, the list of files is iterated through only once, indefinitely loops otherwise - choice_probs (np.ndarray): list of per-item probabilities for OrderType.CHOICE - """ - - expanded_paths = list(map(glob, glob_paths)) - if sort_paths: - expanded_paths = list(map(sorted, expanded_paths)) - - expanded_paths = list(map(np.asarray, expanded_paths)) - - super().__init__(*expanded_paths, order_type=order_type, do_once=do_once, choice_probs=choice_probs) - self.glob_paths = glob_paths - self.sort_paths = sort_paths 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/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 From 77352cf86bf7967ce9dea375693bc257806aee8b Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 22 Jan 2020 13:03:07 +0000 Subject: [PATCH 11/11] Update cardiac_segmentation.ipynb --- examples/cardiac_segmentation.ipynb | 295 ---------------------------- 1 file changed, 295 deletions(-) delete mode 100644 examples/cardiac_segmentation.ipynb diff --git a/examples/cardiac_segmentation.ipynb b/examples/cardiac_segmentation.ipynb deleted file mode 100644 index f96a14a5db..0000000000 --- a/examples/cardiac_segmentation.ipynb +++ /dev/null @@ -1,295 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 8, - "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, 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": 2, - "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": 9, - "metadata": {}, - "outputs": [], - "source": [ - "imSrc = data.readers.NPZReader(\"scd_lvsegs.npz\", [\"images\", \"segs\"], orderType=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": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(200, 1, 64, 64) float32 (200, 1, 64, 64) int32\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 10, - "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.rescaleArray(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, dimFract=5, order=0, nonzeroIndex=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": 11, - "metadata": {}, - "outputs": [], - "source": [ - "lr = 1e-3\n", - "\n", - "net = networks.nets.UNet(\n", - " dimensions=2,\n", - " inChannels=1,\n", - " numClasses=1,\n", - " channels=(16, 32, 64, 128, 256),\n", - " strides=(2, 2, 2, 2),\n", - " numResUnits=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": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Epoch 1 Loss: 0.8310171365737915\n", - "Epoch 2 Loss: 0.8060150742530823\n", - "Epoch 3 Loss: 0.7623872756958008\n", - "Epoch 4 Loss: 0.6729476451873779\n", - "Epoch 5 Loss: 0.6116510629653931\n", - "Epoch 6 Loss: 0.5286673903465271\n", - "Epoch 7 Loss: 0.4480087161064148\n", - "Epoch 8 Loss: 0.41203784942626953\n", - "Epoch 9 Loss: 0.3519987463951111\n", - "Epoch 10 Loss: 0.30135440826416016\n", - "Epoch 11 Loss: 0.274499773979187\n", - "Epoch 12 Loss: 0.2519426941871643\n", - "Epoch 13 Loss: 0.23030847311019897\n", - "Epoch 14 Loss: 0.22828155755996704\n", - "Epoch 15 Loss: 0.22576206922531128\n", - "Epoch 16 Loss: 0.23023653030395508\n", - "Epoch 17 Loss: 0.21913212537765503\n", - "Epoch 18 Loss: 0.22168612480163574\n", - "Epoch 19 Loss: 0.2222415804862976\n", - "Epoch 20 Loss: 0.20740610361099243\n" - ] - } - ], - "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": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "im, seg = utils.mathutils.first(imSrc)\n", - "testim = utils.arrayutils.rescaleArray(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]]))" - ] - } - ], - "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 -}