Repository: mlcommons/GaNDLF

Issue:
Add an option to perform RGBA to RGB in preprocessing
**Is your feature request related to a problem? Please describe.**
A lot of histology images come with RGBA channels, but the A channel is usually not needed.

**Describe the solution you'd like**
Add a preprocessing routine to take care of this. Some psuedo-code based on PIL:
```python
import PIL.Image

rgba_image = PIL.Image.open(curr_file)
rgb_image = rgba_image.convert('RGB')
```

**Describe alternatives you've considered**
N.A.

**Additional context**
N.A.



Discussion:

Resolving patch:
diff --git a/GANDLF/data/preprocessing/__init__.py b/GANDLF/data/preprocessing/__init__.py
--- a/GANDLF/data/preprocessing/__init__.py
+++ b/GANDLF/data/preprocessing/__init__.py
@@ -18,6 +18,7 @@
     normalize_div_by_255_transform,
 )
 from .resample_minimum import Resample_Minimum
+from .rgba2rgb import rgba2rgb_transform
 
 from torchio.transforms import (
     ZNormalization,
@@ -59,7 +60,7 @@ def nonzero_voxel_mask(image):
     return image != 0
 
 
-def to_canonical_transform(parameters):
+def to_canonical_transform(parameters=None):
     return ToCanonical()
 
 
@@ -90,6 +91,9 @@ def centercrop_transform(patch_size):
     "normalize_nonzero": ZNormalization(masking_method=nonzero_voxel_mask),
     "normalize_nonZero_masked": NonZeroNormalizeOnMaskedRegion(),
     "normalize_nonzero_masked": NonZeroNormalizeOnMaskedRegion(),
+    "rgba2rgb": rgba2rgb_transform,
+    "rgbatorgb": rgba2rgb_transform,
+    "rgba_to_rgb": rgba2rgb_transform,
 }
 
 
@@ -109,31 +113,33 @@ def get_transforms_for_preprocessing(
         list: The list of pre-processing transformations.
     """
 
-    preprocessing = parameters["data_preprocessing"]
+    preprocessing_params_dict = parameters["data_preprocessing"]
     # first, we want to do thresholding, followed by clipping, if it is present - required for inference as well
     normalize_to_apply = None
-    if not (preprocessing is None):
+    if not (preprocessing_params_dict is None):
         # go through preprocessing in the order they are specified
-        for preprocess in preprocessing:
+        for preprocess in preprocessing_params_dict:
             preprocess_lower = preprocess.lower()
             # special check for resample
             if preprocess_lower == "resize":
-                resize_values = generic_3d_check(preprocessing["resize"])
+                resize_values = generic_3d_check(preprocessing_params_dict["resize"])
                 current_transformations.append(Resize(resize_values))
             elif preprocess_lower == "resize_patch":
-                resize_values = generic_3d_check(preprocessing["resize_patch"])
+                resize_values = generic_3d_check(
+                    preprocessing_params_dict["resize_patch"]
+                )
                 current_transformations.append(Resize(resize_values))
             elif preprocess_lower == "resample":
-                if "resolution" in preprocessing[preprocess_lower]:
+                if "resolution" in preprocessing_params_dict[preprocess_lower]:
                     # Need to take a look here
                     resample_values = generic_3d_check(
-                        preprocessing[preprocess_lower]["resolution"]
+                        preprocessing_params_dict[preprocess_lower]["resolution"]
                     )
                     current_transformations.append(Resample(resample_values))
             elif preprocess_lower in ["resample_minimum", "resample_min"]:
-                if "resolution" in preprocessing[preprocess_lower]:
+                if "resolution" in preprocessing_params_dict[preprocess_lower]:
                     resample_values = generic_3d_check(
-                        preprocessing[preprocess_lower]["resolution"]
+                        preprocessing_params_dict[preprocess_lower]["resolution"]
                     )
                     current_transformations.append(Resample_Minimum(resample_values))
             # normalize should be applied at the end
@@ -152,7 +158,7 @@ def get_transforms_for_preprocessing(
             elif preprocess_lower in global_preprocessing_dict:
                 current_transformations.append(
                     global_preprocessing_dict[preprocess_lower](
-                        preprocessing[preprocess]
+                        preprocessing_params_dict[preprocess]
                     )
                 )
 
diff --git a/GANDLF/data/preprocessing/rgba2rgb.py b/GANDLF/data/preprocessing/rgba2rgb.py
new file mode 100644
--- /dev/null
+++ b/GANDLF/data/preprocessing/rgba2rgb.py
@@ -0,0 +1,37 @@
+import torch
+
+import PIL.Image
+import numpy as np
+
+from torchio.transforms.intensity_transform import IntensityTransform
+from torchio.data.subject import Subject
+
+
+class RGBA2RGB(IntensityTransform):
+    """
+    Convert RGBA image to RGB image.
+    """
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+
+    def apply_transform(self, subject: Subject) -> Subject:
+        for image in self.get_images(subject):
+            image_data_to_set = image.data
+            # only proceed with RGBA is detected
+            if image_data_to_set.shape[0] == 4:
+                image_data_array = image_data_to_set.numpy()
+                if image_data_array.shape[-1] == 1:
+                    image_data_array = image_data_array[..., 0]
+                    image_data_array = image_data_array.transpose([1, 2, 0])
+                image_pil = PIL.Image.fromarray(image_data_array.astype(np.uint8))
+                image_pil_rgb = image_pil.convert("RGB")
+                image_data_to_set = torch.from_numpy(
+                    np.array(image_pil_rgb).transpose([2, 0, 1])
+                ).unsqueeze(0)
+            image.set_data(image_data_to_set)
+        return subject
+
+
+def rgba2rgb_transform(parameters=None):
+    return RGBA2RGB()


Test patch:
diff --git a/testing/test_full.py b/testing/test_full.py
--- a/testing/test_full.py
+++ b/testing/test_full.py
@@ -1122,6 +1122,7 @@ def test_cli_function_preprocess():
     parameters["weighted_loss"] = True
     parameters["save_output"] = True
     parameters["data_preprocessing"]["to_canonical"] = None
+    parameters["data_preprocessing"]["rgba_to_rgb"] = None
 
     # store this separately for preprocess testing
     with open(file_config_temp, "w") as outfile:
@@ -1233,6 +1234,9 @@ def test_preprocess_functions():
     print("Starting testing preprocessing functions")
     # initialize an input which has values between [-1,1]
     # checking tensor with last dimension of size 1
+    input_tensor = torch.rand(4, 256, 256, 1)
+    input_transformed = global_preprocessing_dict["rgba2rgb"]()(input_tensor)
+    assert input_transformed.shape[1] == 3, "Number of channels is not 3"
     input_tensor = 2 * torch.rand(3, 256, 256, 1) - 1
     input_transformed = global_preprocessing_dict["normalize_div_by_255"](input_tensor)
     input_tensor = 2 * torch.rand(1, 3, 256, 256) - 1
@@ -1641,6 +1645,7 @@ def test_train_inference_classification_histology_2d(device):
     parameters["model"]["num_channels"] = 3
     parameters["model"]["architecture"] = "densenet121"
     parameters["model"]["norm_type"] = "none"
+    parameters["data_preprocessing"]["rgba2rgb"] = ""
     parameters = populate_header_in_parameters(parameters, parameters["headers"])
     parameters["nested_training"]["testing"] = 1
     parameters["nested_training"]["validation"] = -2