Repository: mlcommons/GaNDLF

Issue:
Enable parsing real-world definitions for Histology dataloader
**Is your feature request related to a problem? Please describe.**
Real-world WSIs are defined in terms of **microns** rather than **pixels**, but GaNDLF only processes images in terms of the latter, which is fine for radiology since resampling can take of that issue in radiology.

**Describe the solution you'd like**
For histology **inference**, it would sense to be able to process patches in terms of **microns** rather than **pixels**.

**Describe alternatives you've considered**
Resize WSI outside the main computation pipeline and then feed it in, which would be very inefficient in terms of both computation and storage.

**Additional context**
- A lot of logic could be leveraged from the solution to https://github.com/CBICA/OPM/issues/31
- A `resize` operation would be mandatory to ensure the model only sees patches of a specific size


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,7 @@
     patch_size_check,
     parse_config,
     generate_initial_mask,
+    get_patch_size_in_microns,
 )
 
 
@@ -48,6 +49,8 @@ def patch_extraction(input_path, output_path, config=None):
     if "patch_size" not in cfg:
         cfg["patch_size"] = (256, 256)
 
+    original_patch_size = cfg["patch_size"]
+
     if not os.path.exists(output_path):
         Path(output_path).mkdir(parents=True, exist_ok=True)
 
@@ -63,6 +66,8 @@ def patch_extraction(input_path, output_path, config=None):
         manager.set_image_header("Channel_0")
         manager.set_mask_header("Label")
 
+        cfg["patch_size"] = get_patch_size_in_microns(slide, original_patch_size)
+
         # Generate an initial validity mask
         mask, scale = generate_initial_mask(slide, cfg["scale"])
         print("Setting valid mask...")
diff --git a/GANDLF/data/inference_dataloader_histopath.py b/GANDLF/data/inference_dataloader_histopath.py
--- a/GANDLF/data/inference_dataloader_histopath.py
+++ b/GANDLF/data/inference_dataloader_histopath.py
@@ -16,11 +16,12 @@
 from skimage.morphology import binary_closing, disk
 from scipy.ndimage import binary_fill_holes
 
+from GANDLF.OPM.opm.utils import get_patch_size_in_microns
+
 
 def tissue_mask_generation(img_rgb, rgb_min=50):
     """
-    This function is used to generate tissue masks
-    works for patches too i guess
+    This function is used to generate tissue masks; works for patches as well
 
     Args:
         img_rgb (numpy.array): Input image.
@@ -70,6 +71,7 @@ def __init__(
         self._patch_size = patch_size
         if self._patch_size[-1] == 1:
             self._patch_size = self._patch_size[:-1]
+        self._patch_size = get_patch_size_in_microns(wsi_path, self._patch_size)
         self._stride_size = stride_size
         self._selected_level = selected_level
         self._mask_level = mask_level
@@ -95,7 +97,7 @@ def _basic_preprocessing(self):
         # then this logic straight up does not work
         # You would have to scale the patch size appropriately for this to work correctly
         # Remove all the points which are closer to the boundary of the wsi
-        # by accsesing the WSI level properties with
+        # by accessing the WSI level properties with
         # self._os_image.level_dimensions[self._selected_level]
         # Logic as if point + self.patch_size > wsi_dimensions
         # The move the point by the wsi_dimensions - (patch_size + self.points)
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -75,7 +75,7 @@ def run(self):
     "medcam",
     "opencv-python",
     "torchmetrics==0.5.1",  # newer versions have changed api for f1 invocation
-    "OpenPatchMiner==0.1.6",
+    "OpenPatchMiner==0.1.8",
     "zarr==2.10.3",
     "pydicom",
     "onnx",


Test patch:
diff --git a/testing/test_full.py b/testing/test_full.py
--- a/testing/test_full.py
+++ b/testing/test_full.py
@@ -1638,7 +1638,8 @@ def test_train_inference_segmentation_histology_2d(device):
     parameters_patch = {}
     # extracting minimal number of patches to ensure that the test does not take too long
     parameters_patch["num_patches"] = 3
-    parameters_patch["patch_size"] = [128, 128]
+    # define patches to be extracted in terms of microns
+    parameters_patch["patch_size"] = ["1000m", "1000m"]
 
     with open(file_config_temp, "w") as file:
         yaml.dump(parameters_patch, file)
@@ -1667,6 +1668,7 @@ def test_train_inference_segmentation_histology_2d(device):
     parameters["nested_training"]["validation"] = -2
     parameters["metrics"] = ["dice"]
     parameters["model"]["onnx_export"] = True
+    parameters["data_preprocessing"]["resize_image"] = [128, 128]
     modelDir = os.path.join(outputDir, "modelDir")
     Path(modelDir).mkdir(parents=True, exist_ok=True)
     TrainingManager(