From 733e2397c834bbbbbd4d46ca2fb4a97701eeda8a Mon Sep 17 00:00:00 2001 From: Fernando Perez-Garcia Date: Thu, 11 Oct 2018 13:59:11 +0100 Subject: [PATCH 1/6] Add Gaussian smoothing before undersampling Gaussian smoothing is performed previous to the scaling operation if zoom factor < 1. This reduces the aliasing introduced by the undersampling operation. Choice of sigma according to: Cardoso et al., "Scale factor point spread function matching: beyond aliasing in image resampling", MICCAI 2015 --- niftynet/layer/rand_spatial_scaling.py | 45 ++++++++++++++++++++------ 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/niftynet/layer/rand_spatial_scaling.py b/niftynet/layer/rand_spatial_scaling.py index 483e9a92..88e5bad8 100755 --- a/niftynet/layer/rand_spatial_scaling.py +++ b/niftynet/layer/rand_spatial_scaling.py @@ -4,7 +4,7 @@ import warnings import numpy as np -import scipy.ndimage +import scipy.ndimage as ndi from niftynet.layer.base_layer import RandomisedLayer @@ -22,7 +22,7 @@ def __init__(self, max_percentage=10.0, name='random_spatial_scaling'): super(RandomSpatialScalingLayer, self).__init__(name=name) - assert min_percentage < max_percentage + assert min_percentage <= max_percentage self._min_percentage = max(min_percentage, -99.9) self._max_percentage = max_percentage self._rand_zoom = None @@ -34,6 +34,18 @@ def randomise(self, spatial_rank=3): size=(spatial_rank,)) self._rand_zoom = (rand_zoom + 100.0) / 100.0 + def _get_sigma(self, zoom): + """ + Compute optimal standard deviation for Gaussian kernel. + + Cardoso et al., "Scale factor point spread function matching: + beyond aliasing in image resampling", MICCAI 2015 + """ + k = 1 / zoom + variance = (k ** 2 - 1 ** 2) * (2 * np.sqrt(2 * np.log(2))) ** (-2) + sigma = np.sqrt(variance) + return sigma + def _apply_transformation(self, image, interp_order=3): if interp_order < 0: return image @@ -41,20 +53,33 @@ def _apply_transformation(self, image, interp_order=3): full_zoom = np.array(self._rand_zoom) while len(full_zoom) < image.ndim: full_zoom = np.hstack((full_zoom, [1.0])) + smooth = all(full_zoom[:3] < 1) # perform smoothing if undersampling + smooth = False + if smooth: + sigma = self._get_sigma(full_zoom[:3]) if image.ndim == 4: output = [] for mod in range(image.shape[-1]): - scaled = scipy.ndimage.zoom(image[..., mod], - full_zoom[:3], - order=interp_order) + if smooth: + original_resolution = ndi.gaussian_filter( + image[..., mod], sigma) + else: + original_resolution = image[..., mod] + scaled = ndi.zoom( + original_resolution, full_zoom[:3], order=interp_order) output.append(scaled[..., np.newaxis]) return np.concatenate(output, axis=-1) - if image.ndim == 3: - scaled = scipy.ndimage.zoom(image, - full_zoom[:3], - order=interp_order) + elif image.ndim == 3: + if smooth: + original_resolution = ndi.gaussian_filter( + image, sigma) + else: + original_resolution = image + scaled = ndi.zoom( + original_resolution, full_zoom[:3], order=interp_order) return scaled[..., np.newaxis] - raise NotImplementedError('not implemented random scaling') + else: + raise NotImplementedError('not implemented random scaling') def layer_op(self, inputs, interp_orders, *args, **kwargs): if inputs is None: From f64427aa7439c2005fdd19015359c8518dc3ce1b Mon Sep 17 00:00:00 2001 From: Fernando Perez-Garcia Date: Thu, 11 Oct 2018 14:31:17 +0100 Subject: [PATCH 2/6] Remove hardcoded test line --- niftynet/layer/rand_spatial_scaling.py | 1 - 1 file changed, 1 deletion(-) diff --git a/niftynet/layer/rand_spatial_scaling.py b/niftynet/layer/rand_spatial_scaling.py index 88e5bad8..210b344c 100755 --- a/niftynet/layer/rand_spatial_scaling.py +++ b/niftynet/layer/rand_spatial_scaling.py @@ -54,7 +54,6 @@ def _apply_transformation(self, image, interp_order=3): while len(full_zoom) < image.ndim: full_zoom = np.hstack((full_zoom, [1.0])) smooth = all(full_zoom[:3] < 1) # perform smoothing if undersampling - smooth = False if smooth: sigma = self._get_sigma(full_zoom[:3]) if image.ndim == 4: From 3b91e2546d04ef837de7c8487238438b5465a5a6 Mon Sep 17 00:00:00 2001 From: Fernando Perez-Garcia Date: Thu, 11 Oct 2018 17:17:57 +0100 Subject: [PATCH 3/6] Add flag for antialiasing filter --- niftynet/layer/rand_spatial_scaling.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/niftynet/layer/rand_spatial_scaling.py b/niftynet/layer/rand_spatial_scaling.py index 210b344c..043c3496 100755 --- a/niftynet/layer/rand_spatial_scaling.py +++ b/niftynet/layer/rand_spatial_scaling.py @@ -46,7 +46,7 @@ def _get_sigma(self, zoom): sigma = np.sqrt(variance) return sigma - def _apply_transformation(self, image, interp_order=3): + def _apply_transformation(self, image, interp_order=3, antialiasing=True): if interp_order < 0: return image assert self._rand_zoom is not None @@ -59,7 +59,7 @@ def _apply_transformation(self, image, interp_order=3): if image.ndim == 4: output = [] for mod in range(image.shape[-1]): - if smooth: + if smooth and antialiasing: original_resolution = ndi.gaussian_filter( image[..., mod], sigma) else: @@ -69,7 +69,7 @@ def _apply_transformation(self, image, interp_order=3): output.append(scaled[..., np.newaxis]) return np.concatenate(output, axis=-1) elif image.ndim == 3: - if smooth: + if smooth and antialiasing: original_resolution = ndi.gaussian_filter( image, sigma) else: From b15bedbfa7bf5bb77c99cfe29d7e5c15617f1af4 Mon Sep 17 00:00:00 2001 From: Fernando Perez-Garcia Date: Thu, 11 Oct 2018 17:23:57 +0100 Subject: [PATCH 4/6] Use more descriptive boolean variables --- niftynet/layer/rand_spatial_scaling.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/niftynet/layer/rand_spatial_scaling.py b/niftynet/layer/rand_spatial_scaling.py index 043c3496..abd6acad 100755 --- a/niftynet/layer/rand_spatial_scaling.py +++ b/niftynet/layer/rand_spatial_scaling.py @@ -46,20 +46,21 @@ def _get_sigma(self, zoom): sigma = np.sqrt(variance) return sigma - def _apply_transformation(self, image, interp_order=3, antialiasing=True): + def _apply_transformation(self, image, interp_order=3, smooth=True): if interp_order < 0: return image assert self._rand_zoom is not None full_zoom = np.array(self._rand_zoom) while len(full_zoom) < image.ndim: full_zoom = np.hstack((full_zoom, [1.0])) - smooth = all(full_zoom[:3] < 1) # perform smoothing if undersampling - if smooth: + is_undersampling = all(full_zoom[:3] < 1) + run_antialiasing_filter = smooth and is_undersampling + if run_antialiasing_filter: sigma = self._get_sigma(full_zoom[:3]) if image.ndim == 4: output = [] for mod in range(image.shape[-1]): - if smooth and antialiasing: + if run_antialiasing_filter: original_resolution = ndi.gaussian_filter( image[..., mod], sigma) else: @@ -69,7 +70,7 @@ def _apply_transformation(self, image, interp_order=3, antialiasing=True): output.append(scaled[..., np.newaxis]) return np.concatenate(output, axis=-1) elif image.ndim == 3: - if smooth and antialiasing: + if run_antialiasing_filter: original_resolution = ndi.gaussian_filter( image, sigma) else: From d8b859fa02168d64b4708c14e4666100db5e5e30 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Tue, 16 Oct 2018 13:59:10 +0100 Subject: [PATCH 5/6] style&docs --- doc/source/config_spec.md | 4 +++ .../application/classification_application.py | 3 ++- niftynet/application/gan_application.py | 3 ++- .../application/regression_application.py | 3 ++- .../application/segmentation_application.py | 3 ++- .../contrib/preprocessors/preprocessing.py | 3 ++- niftynet/layer/rand_spatial_scaling.py | 25 ++++++++----------- niftynet/utilities/user_parameters_default.py | 7 ++++++ 8 files changed, 31 insertions(+), 20 deletions(-) diff --git a/doc/source/config_spec.md b/doc/source/config_spec.md index 48c0fe38..f9d9aab7 100644 --- a/doc/source/config_spec.md +++ b/doc/source/config_spec.md @@ -567,6 +567,7 @@ Value should be in `[0, 1]`. ---- | ---- | ------- | ------- [rotation_angle](#rotation-angle) | `float array` | `rotation_angle=-10.0,10.0` | `''` [scaling_percentage](#scaling-percentage) | `float array` | `scaling_percentage=-20.0,20.0` | `''` +[antialiasing](#antialiasing) | `boolean` | `antialiasing=True` | `True` [random_flipping_axes](#random-flipping-axes) | `integer array` | `random_flipping_axes=1,2` | `-1` ###### `rotation_angle` @@ -580,6 +581,9 @@ The option accepts percentages relative to 100 (the original input size). E.g, `(-50, 50)` indicates transforming image (size `d`) to image with its size in between `0.5*d` and `1.5d`. +###### `antialiasing` +Boolean value indicates if antialiasing should be performed +when randomly downsampling the input images. ###### `random_flipping_axes` The axes which can be flipped to augment the data. diff --git a/niftynet/application/classification_application.py b/niftynet/application/classification_application.py index be2bf410..098e9b6d 100755 --- a/niftynet/application/classification_application.py +++ b/niftynet/application/classification_application.py @@ -135,7 +135,8 @@ def initialise_dataset_loader( if train_param.scaling_percentage: augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=train_param.scaling_percentage[0], - max_percentage=train_param.scaling_percentage[1])) + max_percentage=train_param.scaling_percentage[1], + antialiasing=train_param.antialiasing)) if train_param.rotation_angle or \ self.action_param.rotation_angle_x or \ self.action_param.rotation_angle_y or \ diff --git a/niftynet/application/gan_application.py b/niftynet/application/gan_application.py index 9e2fc8ee..ae09ee5f 100755 --- a/niftynet/application/gan_application.py +++ b/niftynet/application/gan_application.py @@ -103,7 +103,8 @@ def initialise_dataset_loader( if self.action_param.scaling_percentage: augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=self.action_param.scaling_percentage[0], - max_percentage=self.action_param.scaling_percentage[1])) + max_percentage=self.action_param.scaling_percentage[1], + antialiasing=self.action_param.antialiasing)) if self.action_param.rotation_angle: augmentation_layers.append(RandomRotationLayer()) augmentation_layers[-1].init_uniform_angle( diff --git a/niftynet/application/regression_application.py b/niftynet/application/regression_application.py index d454353e..9194c0f7 100755 --- a/niftynet/application/regression_application.py +++ b/niftynet/application/regression_application.py @@ -124,7 +124,8 @@ def initialise_dataset_loader( if train_param.scaling_percentage: augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=train_param.scaling_percentage[0], - max_percentage=train_param.scaling_percentage[1])) + max_percentage=train_param.scaling_percentage[1], + antialiasing=train_param.antialiasing)) if train_param.rotation_angle: rotation_layer = RandomRotationLayer() if train_param.rotation_angle: diff --git a/niftynet/application/segmentation_application.py b/niftynet/application/segmentation_application.py index c0be19c4..a9343ee2 100755 --- a/niftynet/application/segmentation_application.py +++ b/niftynet/application/segmentation_application.py @@ -150,7 +150,8 @@ def initialise_dataset_loader( if train_param.scaling_percentage: augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=train_param.scaling_percentage[0], - max_percentage=train_param.scaling_percentage[1])) + max_percentage=train_param.scaling_percentage[1], + antialiasing=train_param.antialiasing)) if train_param.rotation_angle or \ train_param.rotation_angle_x or \ train_param.rotation_angle_y or \ diff --git a/niftynet/contrib/preprocessors/preprocessing.py b/niftynet/contrib/preprocessors/preprocessing.py index 356ab133..6199815b 100644 --- a/niftynet/contrib/preprocessors/preprocessing.py +++ b/niftynet/contrib/preprocessors/preprocessing.py @@ -57,7 +57,8 @@ def prepare_augmentation_layers(self): if self.action_param.scaling_percentage: augmentation_layers.append(RandomSpatialScalingLayer( min_percentage=self.action_param.scaling_percentage[0], - max_percentage=self.action_param.scaling_percentage[1])) + max_percentage=self.action_param.scaling_percentage[1], + antialiasing=self.action_param.antialiasing)) if self.action_param.rotation_angle or \ self.action_param.rotation_angle_x or \ self.action_param.rotation_angle_y or \ diff --git a/niftynet/layer/rand_spatial_scaling.py b/niftynet/layer/rand_spatial_scaling.py index abd6acad..8767feba 100755 --- a/niftynet/layer/rand_spatial_scaling.py +++ b/niftynet/layer/rand_spatial_scaling.py @@ -20,11 +20,13 @@ class RandomSpatialScalingLayer(RandomisedLayer): def __init__(self, min_percentage=-10.0, max_percentage=10.0, + antialiasing=True, name='random_spatial_scaling'): super(RandomSpatialScalingLayer, self).__init__(name=name) assert min_percentage <= max_percentage self._min_percentage = max(min_percentage, -99.9) self._max_percentage = max_percentage + self.antialiasing = antialiasing self._rand_zoom = None def randomise(self, spatial_rank=3): @@ -46,7 +48,7 @@ def _get_sigma(self, zoom): sigma = np.sqrt(variance) return sigma - def _apply_transformation(self, image, interp_order=3, smooth=True): + def _apply_transformation(self, image, interp_order=3): if interp_order < 0: return image assert self._rand_zoom is not None @@ -54,29 +56,22 @@ def _apply_transformation(self, image, interp_order=3, smooth=True): while len(full_zoom) < image.ndim: full_zoom = np.hstack((full_zoom, [1.0])) is_undersampling = all(full_zoom[:3] < 1) - run_antialiasing_filter = smooth and is_undersampling + run_antialiasing_filter = self.antialiasing and is_undersampling if run_antialiasing_filter: sigma = self._get_sigma(full_zoom[:3]) if image.ndim == 4: output = [] for mod in range(image.shape[-1]): - if run_antialiasing_filter: - original_resolution = ndi.gaussian_filter( - image[..., mod], sigma) - else: - original_resolution = image[..., mod] - scaled = ndi.zoom( - original_resolution, full_zoom[:3], order=interp_order) + to_scale = ndi.gaussian_filter(image[..., mod], sigma) if \ + run_antialiasing_filter else image[..., mod] + scaled = ndi.zoom(to_scale, full_zoom[:3], order=interp_order) output.append(scaled[..., np.newaxis]) return np.concatenate(output, axis=-1) elif image.ndim == 3: - if run_antialiasing_filter: - original_resolution = ndi.gaussian_filter( - image, sigma) - else: - original_resolution = image + to_scale = ndi.gaussian_filter(image, sigma) \ + if run_antialiasing_filter else image scaled = ndi.zoom( - original_resolution, full_zoom[:3], order=interp_order) + to_scale, full_zoom[:3], order=interp_order) return scaled[..., np.newaxis] else: raise NotImplementedError('not implemented random scaling') diff --git a/niftynet/utilities/user_parameters_default.py b/niftynet/utilities/user_parameters_default.py index e90b3db1..1c74ef3f 100755 --- a/niftynet/utilities/user_parameters_default.py +++ b/niftynet/utilities/user_parameters_default.py @@ -484,6 +484,13 @@ def add_training_args(parser): type=float_array, default=()) + parser.add_argument( + "--antialiasing", + help="Indicates if antialiasing must be performed " + "when randomly scaling the input images", + type=str2boolean, + default=True) + parser.add_argument( "--bias_field_range", help="[Training only] The range of bias field coeffs in [min_coeff, " From 5e306a67047bc6c79c6799aa03e1ccb2e351e086 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Tue, 16 Oct 2018 14:15:57 +0100 Subject: [PATCH 6/6] docs for random bias field and elastic deform. --- doc/source/config_spec.md | 27 ++++++++++++++++--- niftynet/utilities/user_parameters_default.py | 3 +++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/doc/source/config_spec.md b/doc/source/config_spec.md index f9d9aab7..098ca176 100644 --- a/doc/source/config_spec.md +++ b/doc/source/config_spec.md @@ -567,8 +567,15 @@ Value should be in `[0, 1]`. ---- | ---- | ------- | ------- [rotation_angle](#rotation-angle) | `float array` | `rotation_angle=-10.0,10.0` | `''` [scaling_percentage](#scaling-percentage) | `float array` | `scaling_percentage=-20.0,20.0` | `''` -[antialiasing](#antialiasing) | `boolean` | `antialiasing=True` | `True` +[antialiasing](#scaling-percentage) | `boolean` | `antialiasing=True` | `True` [random_flipping_axes](#random-flipping-axes) | `integer array` | `random_flipping_axes=1,2` | `-1` +[do_elastic_deformation](#do-elastic-deformation) | `boolean` | `do_elastic_deformation=True` | `False` +[num_ctrl_points](#do-elastic-deformation) | `integer` | `num_ctrl_points=1` | `4` +[deformation_sigma](#do-elastic-deformation) | `float` | `deformation_sigma=1` | `15` +[proportion_to_deform](#do-elastic-deformation) | `float` | `proportion_to_deform=0.7` | `0.5` +[bias_field_range](#bias-field-range) | `float array` | `bias_field_range=-10.0,10.0` | `''` +[bf_order](#bias-field-range) | `integer` | `bf_order=1` | `3` + ###### `rotation_angle` Float array, indicates a random rotation operation should be applied to the @@ -581,8 +588,8 @@ The option accepts percentages relative to 100 (the original input size). E.g, `(-50, 50)` indicates transforming image (size `d`) to image with its size in between `0.5*d` and `1.5d`. -###### `antialiasing` -Boolean value indicates if antialiasing should be performed +When random scaling is enabled, it is possible to further specify: +- `antialiasing` indicating if antialiasing should be performed when randomly downsampling the input images. ###### `random_flipping_axes` @@ -590,6 +597,20 @@ The axes which can be flipped to augment the data. Supply as comma-separated values within single quotes, e.g. '0,1'. Note that these are 0-indexed, so choose some combination of 0, 1. +###### `do_elastic_deformation` +Boolean value indicates data augmentation using elastic deformations + +When `do_elastic_deformation=True`, it is possible to further specify: +- `num_ctrl_points` -- number of control points for the elastic deformation, +- `deformation_sigma` -- the standard deviation for the elastic deformation, +- `proportion_to_deform` -- what fraction of samples to deform elastically. + +###### `bias_field_range` +Float array, indicates data augmentation with randomised bias field + +When `bias_field_range` is not None, it is possible to further specify: +- `bf_order` -- maximal polynomial order to use for the bias field augmentation. + ### INFERENCE diff --git a/niftynet/utilities/user_parameters_default.py b/niftynet/utilities/user_parameters_default.py index 1c74ef3f..f0cf6c8d 100755 --- a/niftynet/utilities/user_parameters_default.py +++ b/niftynet/utilities/user_parameters_default.py @@ -520,16 +520,19 @@ def add_training_args(parser): help="Enables elastic deformation", type=str2boolean, default=False) + parser.add_argument( "--num_ctrl_points", help="Number of control points for the elastic deformation", type=int, default=4) + parser.add_argument( "--deformation_sigma", help="The standard deviation for elastic deformation.", type=float, default=15) + parser.add_argument( "--proportion_to_deform", help="What fraction of samples to deform elastically.",