diff --git a/aucmedi/data_processing/io_loader/sitk_loader.py b/aucmedi/data_processing/io_loader/sitk_loader.py index e71a31da..6c299dc3 100644 --- a/aucmedi/data_processing/io_loader/sitk_loader.py +++ b/aucmedi/data_processing/io_loader/sitk_loader.py @@ -99,8 +99,7 @@ def sitk_loader(sample, path_imagedir, image_format=None, grayscale=True, sample_itk.GetOrigin(), new_spacing, sample_itk.GetDirection(), - outside_value, - sitk.sitkFloat32) + outside_value) # Skip resampling if None else : sample_itk_resampled = sample_itk # Convert to NumPy diff --git a/aucmedi/data_processing/subfunctions/clip.py b/aucmedi/data_processing/subfunctions/clip.py index 7588975a..3d99f81d 100644 --- a/aucmedi/data_processing/subfunctions/clip.py +++ b/aucmedi/data_processing/subfunctions/clip.py @@ -37,22 +37,45 @@ class Clip(Subfunction_Base): #---------------------------------------------# # Initialization # #---------------------------------------------# - def __init__(self, min=None, max=None): + def __init__(self, min=None, max=None, per_channel=False): """ Initialization function for creating a Clip Subfunction which can be passed to a [DataGenerator][aucmedi.data_processing.data_generator.DataGenerator]. Args: - min (float or int): Desired minimum value for clipping (if `None`, no lower limit is applied). - max (float or int): Desired maximum value for clipping (if `None`, no upper limit is applied). + min (float or int or list): Desired minimum value for clipping (if `None`, no lower limit is applied). + Also possible to pass a list of minimum values if `per_channel=True`. + max (float or int or list): Desired maximum value for clipping (if `None`, no upper limit is applied). + Also possible to pass a list of maximum values if `per_channel=True`. + per_channel (bool): Option if clipping should be applied per channel with different clipping ranges. """ self.min = min self.max = max + self.per_channel = per_channel #---------------------------------------------# # Transformation # #---------------------------------------------# def transform(self, image): - # Perform clipping - image_clipped = np.clip(image, a_min=self.min, a_max=self.max) + # Perform clipping on all channels + if not self.per_channel: + image_clipped = np.clip(image, a_min=self.min, a_max=self.max) + # Perform clipping on each channel + else: + image_clipped = image.copy() + for c in range(0, image.shape[-1]): + # Identify minimum to clip + if self.min is not None and \ + type(self.min) in [list, tuple, np.ndarray]: + min = self.min[c] + else : min = self.min + # Identify maximum to clip + if self.max is not None and \ + type(self.max) in [list, tuple, np.ndarray]: + max = self.max[c] + else : max = self.max + # Perform clipping + image_clipped[..., c] = np.clip(image[...,c], + a_min=min, + a_max=max) # Return clipped image return image_clipped diff --git a/aucmedi/data_processing/subfunctions/standardize.py b/aucmedi/data_processing/subfunctions/standardize.py index 9c6826dc..6d0de87e 100644 --- a/aucmedi/data_processing/subfunctions/standardize.py +++ b/aucmedi/data_processing/subfunctions/standardize.py @@ -57,25 +57,41 @@ class Standardize(Subfunction_Base): #---------------------------------------------# # Initialization # #---------------------------------------------# - def __init__(self, mode="z-score", smooth=0.000001): + def __init__(self, mode="z-score", per_channel=False, smooth=0.000001): """ Initialization function for creating a Standardize Subfunction which can be passed to a [DataGenerator][aucmedi.data_processing.data_generator.DataGenerator]. Args: - mode (str): Selected mode which standardization/normalization technique should be applied. - smooth (float): Smoothing factor to avoid zero devisions (epsilon). + mode (str): Selected mode which standardization/normalization technique should be applied. + per_channel (bool): Option to apply standardization per channel instead of across complete image. + smooth (float): Smoothing factor to avoid zero devisions (epsilon). """ # Verify mode existence if mode not in ["z-score", "minmax", "grayscale", "tf", "caffe", "torch"]: raise ValueError("Subfunction - Standardize: Unknown modus", mode) # Cache class variables self.mode = mode + self.per_channel = per_channel self.e = smooth #---------------------------------------------# # Transformation # #---------------------------------------------# def transform(self, image): + # Apply normalization per channel + if self.per_channel: + image_norm = image.copy() + for c in range(0, image.shape[-1]): + image_norm[..., c] = self.normalize(image[..., c]) + # Apply normalization across complete image + else : image_norm = self.normalize(image) + # Return standardized image + return image_norm + + #---------------------------------------------# + # Internal Function: Normalization # + #---------------------------------------------# + def normalize(self, image): # Perform z-score normalization if self.mode == "z-score": # Compute mean and standard deviation @@ -108,5 +124,5 @@ def transform(self, image): "['tf', 'caffe', 'torch']") # Perform architecture standardization image_norm = imagenet_utils.preprocess_input(image, mode=self.mode) - # Return standardized image - return image_norm + # Return normalized image + return image_norm \ No newline at end of file diff --git a/tests/test_ioloader.py b/tests/test_ioloader.py index 55eeb7d7..63be389e 100644 --- a/tests/test_ioloader.py +++ b/tests/test_ioloader.py @@ -41,6 +41,7 @@ def setUpClass(self): self.img_2d_rgb = np.random.rand(16, 16, 3) * 255 self.img_3d_gray = np.random.rand(16, 16, 16, 1) * 255 self.img_3d_rgb = np.random.rand(16, 16, 16, 3) * 255 + self.img_3d_rgb = self.img_3d_rgb.astype(int) self.img_3d_hu = np.float32(np.random.rand(16, 16, 16, 1) * 1500 - 500) #-------------------------------------------------# @@ -220,6 +221,25 @@ def test_sitk_loader_DataGenerator(self): else: self.assertTrue(np.array_equal(batch[0].shape, (1, 12, 20, 28, 1))) + # Test for rgb 3D images + def test_sitk_loader_3Drgb(self): + # Create temporary directory + tmp_data = tempfile.TemporaryDirectory(prefix="tmp.aucmedi.", + suffix=".data") + # Run analysis + for i in range(0, 6): + if i < 3: format = ".mha" + else : format = ".nii" + # Create image + index = "3Dimage.sample_" + str(i) + format + path_sample = os.path.join(tmp_data.name, index) + image_sitk = sitk.GetImageFromArray(self.img_3d_rgb) + image_sitk.SetSpacing([0.5,1.5,2.0]) + sitk.WriteImage(image_sitk, path_sample) + # Load image via loader + img = sitk_loader(index, tmp_data.name, image_format=None) + self.assertTrue(np.array_equal(img.shape, (32, 24, 8, 3))) + # Test for hu 3D images def test_sitk_loader_3Dhu(self): # Create temporary directory diff --git a/tests/test_subfunctios.py b/tests/test_subfunctios.py index 9f6fcdf3..f37073cb 100644 --- a/tests/test_subfunctios.py +++ b/tests/test_subfunctios.py @@ -151,6 +151,30 @@ def test_STANDARDIZE_transform(self): self.assertTrue(np.amin(img_pp) <= 0) self.assertTrue(np.amax(img_pp) >= 0) # self.assertRaises(ValueError, sf.transform, self.img3Dhu.copy()) + # Per channel normalization + for mode in ["z-score", "minmax", "grayscale", "tf"]: + sf = Standardize(mode=mode, per_channel=True) + for data in [self.img2Drgb, self.img3Drgb]: + img_pp = sf.transform(data.copy()) + for c in range(0, img_pp.shape[-1]): + if mode == "z-score": + self.assertTrue(np.amin(img_pp[..., c]) <= 0) + self.assertTrue(np.amax(img_pp[..., c]) >= 0) + if c != 0: + self.assertFalse(np.amax(img_pp[..., 0]) == \ + np.amax(img_pp[..., c])) + elif mode == "minmax": + self.assertTrue(np.amin(img_pp[..., c]) >= 0) + self.assertTrue(np.amax(img_pp[..., c]) <= 1) + elif mode == "grayscale": + self.assertTrue(np.amin(img_pp[..., c]) >= 0) + self.assertTrue(np.amax(img_pp[..., c]) <= 255) + elif mode == "tf": + self.assertTrue(np.amin(img_pp[..., c]) >= -1) + self.assertTrue(np.amax(img_pp[..., c]) <= 1) + if c != 0: + self.assertFalse(np.amax(img_pp[..., 0]) == \ + np.amax(img_pp[..., c])) #-------------------------------------------------# # Subfunction: Cropping # @@ -203,6 +227,7 @@ def test_CLIP_create(self): sf = Clip() def test_CLIP_transform(self): + # global clipping sf = Clip(min=10) img_clipped = sf.transform(self.img3Dhu.copy()) self.assertTrue(np.amin(img_clipped) >= 10) @@ -213,6 +238,33 @@ def test_CLIP_transform(self): img_clipped = sf.transform(self.img3Dhu.copy()) self.assertTrue(np.amin(img_clipped) >= 10) self.assertTrue(np.amax(img_clipped) <= 50) + # per channel clipping + sf = Clip(min=10, max=50, per_channel=True) + img_clipped = sf.transform(self.img3Dgray.copy()) + self.assertTrue(np.amin(img_clipped[:,:,:,0]) >= 10) + self.assertTrue(np.amax(img_clipped[:,:,:,0]) <= 50) + sf = Clip(min=10, max=50, per_channel=True) + img_clipped = sf.transform(self.img3Drgb.copy()) + for c in range(0,self.img3Drgb.shape[-1]): + self.assertTrue(np.amin(img_clipped[:,:,:,c]) >= 10) + self.assertTrue(np.amax(img_clipped[:,:,:,c]) <= 50) + min_list = [10,20,60] + max_list = [20,40,80] + sf = Clip(min=min_list, max=max_list, per_channel=True) + img_clipped = sf.transform(self.img2Drgb.copy()) + for c in range(0,self.img2Drgb.shape[-1]): + self.assertTrue(np.amin(img_clipped[:,:,c]) >= min_list[c]) + self.assertTrue(np.amax(img_clipped[:,:,c]) <= max_list[c]) + sf = Clip(min=20, max=max_list, per_channel=True) + img_clipped = sf.transform(self.img3Drgb.copy()) + for c in range(0,self.img3Drgb.shape[-1]): + self.assertTrue(np.amin(img_clipped[:,:,:,c]) >= 20) + self.assertTrue(np.amax(img_clipped[:,:,:,c]) <= max_list[c]) + sf = Clip(min=min_list, max=max_list, per_channel=True) + img_clipped = sf.transform(self.img3Drgb.copy()) + for c in range(0,self.img3Drgb.shape[-1]): + self.assertTrue(np.amin(img_clipped[:,:,:,c]) >= min_list[c]) + self.assertTrue(np.amax(img_clipped[:,:,:,c]) <= max_list[c]) #-------------------------------------------------# # Subfunction: Chromer #