Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature.multi modality3 d #199

Merged
merged 5 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions aucmedi/data_processing/io_loader/sitk_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions aucmedi/data_processing/subfunctions/clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 21 additions & 5 deletions aucmedi/data_processing/subfunctions/standardize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
20 changes: 20 additions & 0 deletions tests/test_ioloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

#-------------------------------------------------#
Expand Down Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions tests/test_subfunctios.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
Expand Down Expand Up @@ -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)
Expand All @@ -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 #
Expand Down
Loading