Repository: mlcommons/GaNDLF

Issue:
code for curation of tiles after patching
**Is your feature request related to a problem? Please describe.**
After patching the WSI, tiles having scanning artifacts, pen markings, tissue where background is more than 70 % are still available in the saved patches.
Example Tiles : 
![image](https://github.com/mlcommons/GaNDLF/assets/50418503/d438a284-6d2c-4802-b1db-4ab82c8425be)


**Describe the solution you'd like**
There is a way to handle the tiles after patching to check each patch of background, penn marking, artifactual content by playing in RGB, HSV and HED colorspace and get rid of patches where tissue is not present.

**Additional context**
Coding the same idea and after rigorous experimentation, creating PR for same.


 



Discussion:

Resolving patch:
diff --git a/GANDLF/cli/patch_extraction.py b/GANDLF/cli/patch_extraction.py
--- a/GANDLF/cli/patch_extraction.py
+++ b/GANDLF/cli/patch_extraction.py
@@ -11,6 +11,8 @@
     parse_config,
     generate_initial_mask,
     get_patch_size_in_microns,
+    patch_artifact_check,
+    # pen_marking_check,
 )
 from GANDLF.utils import (
     parseTrainingCSV,
@@ -46,17 +48,14 @@ def patch_extraction(input_path, output_path, config=None):
     Image.MAX_IMAGE_PIXELS = None
     warnings.simplefilter("ignore")
 
+    # initialize default config
+    cfg = {}
     if config is not None:
         cfg = config
         if isinstance(config, str):
             cfg = parse_config(config)
-    else:
-        cfg = {}
-        cfg["scale"] = 16
-
-    if "patch_size" not in cfg:
-        cfg["patch_size"] = (256, 256)
-
+    cfg["scale"] = cfg.get("scale", 16)
+    cfg["patch_size"] = cfg.get("patch_size", (256, 256))
     original_patch_size = cfg["patch_size"]
 
     if not os.path.exists(output_path):
@@ -83,6 +82,8 @@ def patch_extraction(input_path, output_path, config=None):
         manager.set_valid_mask(mask, scale)
         # Reject patch if any pixels are transparent
         manager.add_patch_criteria(alpha_rgb_2d_channel_check)
+        #manager.add_patch_criteria(pen_marking_check) ### will be added to main code after rigourous experimentation
+        manager.add_patch_criteria(patch_artifact_check)
         # Reject patch if image dimensions are not equal to PATCH_SIZE
         patch_dims_check = partial(
             patch_size_check,
diff --git a/GANDLF/data/patch_miner/opm/patch_manager.py b/GANDLF/data/patch_miner/opm/patch_manager.py
--- a/GANDLF/data/patch_miner/opm/patch_manager.py
+++ b/GANDLF/data/patch_miner/opm/patch_manager.py
@@ -148,10 +148,8 @@ def add_patch(self, patch, overlap_factor, patch_size):
                 max(mined_start_x, 0) : self.width_bound_check(mined_end_x),
                 max(mined_start_y, 0) : self.width_bound_check(mined_end_y),
             ] = True
-
             # Append this patch to the list of patches to be saved
             self.patches.append(patch)
-
             return True
 
         except Exception as e:
diff --git a/GANDLF/data/patch_miner/opm/utils.py b/GANDLF/data/patch_miner/opm/utils.py
--- a/GANDLF/data/patch_miner/opm/utils.py
+++ b/GANDLF/data/patch_miner/opm/utils.py
@@ -9,6 +9,9 @@
 # from skimage.morphology.footprints import disk
 from skimage.morphology import remove_small_holes
 from skimage.color.colorconv import rgb2hsv
+import cv2
+#from skimage.exposure import rescale_intensity
+#from skimage.color import rgb2hed
 
 # import matplotlib.pyplot as plt
 import yaml
@@ -235,6 +238,55 @@ def alpha_rgb_2d_channel_check(img):
     else:
         return False
 
+#def pen_marking_check(img, intensity_thresh=225, intensity_thresh_saturation =50, intensity_thresh_b = 128):
+#    """
+#    This function is used to curate patches from the input image. It is used to remove patches that have pen markings.
+#    Args:
+#        img (np.ndarray): Input Patch Array to check the artifact/background.
+#        intensity_thresh (int, optional): Threshold to check whiteness in the patch. Defaults to 225.
+#        intensity_thresh_saturation (int, optional): Threshold to check saturation in the patch. Defaults to 50.
+#        intensity_thresh_b (int, optional) : Threshold to check blackness in the patch
+#        patch_size (int, optional): Tiling Size of the WSI/patch size. Defaults to 256. patch_size=config["patch_size"]
+#    Returns:
+#        bool: Whether the patch is valid (True) or not (False)
+#    """
+#    patch_size= (256,256)
+#    ihc_hed = rgb2hed(img)
+#    patch_hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
+#    e = rescale_intensity(ihc_hed[:, :, 1], out_range=(0, 255), in_range=(0, np.percentile(ihc_hed[:, :, 1], 99)))
+#    if np.sum(e < 50) / (patch_size[0] * patch_size[1]) > 0.95 or (np.sum(patch_hsv[...,0] < 128) / (patch_size[0] * patch_size[1])) > 0.97:
+#        return False
+#    #Assume patch is valid
+#    return True
+
+def patch_artifact_check(img, intensity_thresh = 250, intensity_thresh_saturation = 5, intensity_thresh_b = 128, patch_size = (256,256)):
+    """
+    This function is used to curate patches from the input image. It is used to remove patches that are mostly background.
+    Args:
+        img (np.ndarray): Input Patch Array to check the artifact/background.
+        intensity_thresh (int, optional): Threshold to check whiteness in the patch. Defaults to 225.
+        intensity_thresh_saturation (int, optional): Threshold to check saturation in the patch. Defaults to 50.
+        intensity_thresh_b (int, optional) : Threshold to check blackness in the patch
+        patch_size (int, optional): Tiling Size of the WSI/patch size. Defaults to 256. patch_size=config["patch_size"]
+    Returns:
+        bool: Whether the patch is valid (True) or not (False)
+    """
+    #patch_size = config["patch_size"]
+    patch_hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
+    count_white_pixels = np.sum(np.logical_and.reduce(img > intensity_thresh, axis=2))
+    percent_pixels = count_white_pixels / (patch_size[0] * patch_size[1])
+    count_black_pixels = np.sum(np.logical_and.reduce(img < intensity_thresh_b, axis=2))
+    percent_pixel_b = count_black_pixels / (patch_size[0] * patch_size[1])
+    percent_pixel_2 = np.sum(patch_hsv[...,1] < intensity_thresh_saturation) / (patch_size[0] * patch_size[1])
+    percent_pixel_3 = np.sum(patch_hsv[...,2] > intensity_thresh) / (patch_size[0] * patch_size[1])
+
+    if percent_pixel_2 > 0.99 or np.mean(patch_hsv[...,1]) < 5 or percent_pixel_3 > 0.99:
+        if percent_pixel_2 < 0.1:
+            return False
+    elif (percent_pixel_2 > 0.99 and percent_pixel_3 > 0.99) or percent_pixel_b > 0.99 or percent_pixels > 0.9:
+        return False
+    # assume that the patch is valid
+    return True
 
 def parse_config(config_file):
     """
@@ -252,6 +304,7 @@ def parse_config(config_file):
     config["value_map"] = config.get("value_map", None)
     config["read_type"] = config.get("read_type", "random")
     config["overlap_factor"] = config.get("overlap_factor", 0.0)
+    config["patch_size"] = config.get("patch_size", [256,256])
 
     return config
 


Test patch: