Repository: Project-MONAI/MONAI

Issue:
MetaTensor support for pathology wsi patch datasets: `PatchWSIDatatset`, `SlidingPatchWSIDatatse`, and `MaskedPatchWSIDatatset`



Discussion:

Patch:
diff --git a/monai/data/wsi_datasets.py b/monai/data/wsi_datasets.py
--- a/monai/data/wsi_datasets.py
+++ b/monai/data/wsi_datasets.py
@@ -14,13 +14,15 @@
 from typing import Callable, Dict, Optional, Sequence, Tuple, Union
 
 import numpy as np
+import torch
 
 from monai.data import Dataset
+from monai.data.meta_tensor import MetaTensor
 from monai.data.utils import iter_patch_position
 from monai.data.wsi_reader import BaseWSIReader, WSIReader
 from monai.transforms import ForegroundMask, Randomizable, apply_transform
-from monai.utils import CommonKeys, ProbMapKeys, convert_to_dst_type, ensure_tuple_rep
-from monai.utils.enums import WSIPatchKeys
+from monai.utils import convert_to_dst_type, ensure_tuple_rep
+from monai.utils.enums import CommonKeys, ProbMapKeys, WSIPatchKeys
 
 __all__ = ["PatchWSIDataset", "SlidingPatchWSIDataset", "MaskedPatchWSIDataset"]
 
@@ -46,6 +48,10 @@ class PatchWSIDataset(Dataset):
 
         kwargs: additional arguments to pass to `WSIReader` or provided whole slide reader class
 
+    Returns:
+        dict: a dictionary of loaded image (in MetaTensor format) along with the labels (if requested).
+        {"image": MetaTensor, "label": torch.Tensor}
+
     Note:
         The input data has the following form as an example:
 
@@ -110,7 +116,7 @@ def _get_wsi_object(self, sample: Dict):
         return self.wsi_object_dict[image_path]
 
     def _get_label(self, sample: Dict):
-        return np.array(sample[CommonKeys.LABEL], dtype=np.float32)
+        return torch.tensor(sample[CommonKeys.LABEL], dtype=torch.float32)
 
     def _get_location(self, sample: Dict):
         if self.center_location:
@@ -145,15 +151,18 @@ def _transform(self, index: int):
 
         # Extract patch image and associated metadata
         image, metadata = self._get_data(sample)
-        output = {CommonKeys.IMAGE: image, CommonKeys.METADATA: metadata}
+
+        # Add additional metadata from sample
+        for key in self.additional_meta_keys:
+            metadata[key] = sample[key]
+
+        # Create MetaTensor output for image
+        output = {CommonKeys.IMAGE: MetaTensor(image, meta=metadata)}
 
         # Include label in the output
         if self.include_label:
             output[CommonKeys.LABEL] = self._get_label(sample)
 
-        for key in self.additional_meta_keys:
-            metadata[key] = sample[key]
-
         # Apply transforms and return it
         return apply_transform(self.transform, output) if self.transform else output
 
@@ -333,7 +342,7 @@ class MaskedPatchWSIDataset(PatchWSIDataset):
 
             [
                 {"image": "path/to/image1.tiff"},
-                {"image": "path/to/image2.tiff", "patch_size": [20, 20], "patch_level": 2}
+                {"image": "path/to/image2.tiff", "size": [20, 20], "level": 2}
             ]
 
     """
diff --git a/monai/data/wsi_reader.py b/monai/data/wsi_reader.py
--- a/monai/data/wsi_reader.py
+++ b/monai/data/wsi_reader.py
@@ -47,8 +47,8 @@ class BaseWSIReader(ImageReader):
     - `read` reads a whole slide image object from a given file
     - `get_size` returns the size of the whole slide image of a given wsi object at a given level.
     - `get_level_count` returns the number of levels in the whole slide image
-    - `get_patch` extracts and returns a patch image form the whole slide image
-    - `get_metadata` extracts and returns metadata for a whole slide image and a specific patch.
+    - `_get_patch` extracts and returns a patch image form the whole slide image
+    - `_get_metadata` extracts and returns metadata for a whole slide image and a specific patch.
 
 
     """
@@ -56,7 +56,7 @@ class BaseWSIReader(ImageReader):
     supported_suffixes: List[str] = []
     backend = ""
 
-    def __init__(self, level: int, channel_dim: int = 0, **kwargs):
+    def __init__(self, level: int = 0, channel_dim: int = 0, **kwargs):
         super().__init__()
         self.level = level
         self.channel_dim = channel_dim
@@ -64,7 +64,7 @@ def __init__(self, level: int, channel_dim: int = 0, **kwargs):
         self.metadata: Dict[Any, Any] = {}
 
     @abstractmethod
-    def get_size(self, wsi, level: int) -> Tuple[int, int]:
+    def get_size(self, wsi, level: Optional[int] = None) -> Tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
@@ -87,13 +87,14 @@ def get_level_count(self, wsi) -> int:
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
 
     @abstractmethod
-    def get_downsample_ratio(self, wsi, level: int) -> float:
+    def get_downsample_ratio(self, wsi, level: Optional[int] = None) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
@@ -104,7 +105,19 @@ def get_file_path(self, wsi) -> str:
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
 
     @abstractmethod
-    def get_patch(
+    def get_mpp(self, wsi, level: Optional[int] = None) -> Tuple[float, float]:
+        """
+        Returns the micro-per-pixel resolution of the whole slide image at a given level.
+
+        Args:
+            wsi: a whole slide image object loaded from a file
+            level: the level number where the size is calculated
+
+        """
+        raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
+
+    @abstractmethod
+    def _get_patch(
         self, wsi, location: Tuple[int, int], size: Tuple[int, int], level: int, dtype: DtypeLike, mode: str
     ) -> np.ndarray:
         """
@@ -122,7 +135,7 @@ def get_patch(
         """
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
 
-    def get_metadata(
+    def _get_metadata(
         self, wsi, patch: np.ndarray, location: Tuple[int, int], size: Tuple[int, int], level: int
     ) -> Dict:
         """
@@ -146,7 +159,7 @@ def get_metadata(
             "backend": self.backend,
             "original_channel_dim": channel_dim,
             "spatial_shape": np.array(patch.shape[:channel_dim] + patch.shape[channel_dim + 1 :]),
-            "num_patches": 1,
+            WSIPatchKeys.COUNT.value: 1,
             WSIPatchKeys.PATH.value: self.get_file_path(wsi),
             WSIPatchKeys.LOCATION.value: np.asarray(location),
             WSIPatchKeys.SIZE.value: np.asarray(size),
@@ -209,7 +222,7 @@ def get_data(
                     raise ValueError(f"Patch size should be greater than zero, provided: patch size = {size}")
 
             # Extract a patch or the entire image
-            patch = self.get_patch(each_wsi, location=location, size=size, level=level, dtype=dtype, mode=mode)
+            patch = self._get_patch(each_wsi, location=location, size=size, level=level, dtype=dtype, mode=mode)
 
             # check if the image has three dimensions (2D + color)
             if patch.ndim != 3:
@@ -231,7 +244,7 @@ def get_data(
                     f"{patch.shape[self.channel_dim]}. "
                 )
             # Get patch-related metadata
-            metadata: dict = self.get_metadata(wsi=each_wsi, patch=patch, location=location, size=size, level=level)
+            metadata: dict = self._get_metadata(wsi=each_wsi, patch=patch, location=location, size=size, level=level)
             # Create a list of patches and metadata
             patch_list.append(patch)
             metadata_list.append(metadata)
@@ -296,33 +309,56 @@ def get_level_count(self, wsi) -> int:
         """
         return self.reader.get_level_count(wsi)
 
-    def get_size(self, wsi, level: int) -> Tuple[int, int]:
+    def get_size(self, wsi, level: Optional[int] = None) -> Tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
+        if level is None:
+            level = self.level
+
         return self.reader.get_size(wsi, level)
 
-    def get_downsample_ratio(self, wsi, level: int) -> float:
+    def get_downsample_ratio(self, wsi, level: Optional[int] = None) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
+        if level is None:
+            level = self.level
+
         return self.reader.get_downsample_ratio(wsi, level)
 
     def get_file_path(self, wsi) -> str:
         """Return the file path for the WSI object"""
         return self.reader.get_file_path(wsi)
 
-    def get_patch(
+    def get_mpp(self, wsi, level: Optional[int] = None) -> Tuple[float, float]:
+        """
+        Returns the micro-per-pixel resolution of the whole slide image at a given level.
+
+        Args:
+            wsi: a whole slide image object loaded from a file
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
+
+        """
+        if level is None:
+            level = self.level
+
+        return self.reader.get_mpp(wsi, level)
+
+    def _get_patch(
         self, wsi, location: Tuple[int, int], size: Tuple[int, int], level: int, dtype: DtypeLike, mode: str
     ) -> np.ndarray:
         """
@@ -338,7 +374,7 @@ def get_patch(
             mode: the output image mode, 'RGB' or 'RGBA'
 
         """
-        return self.reader.get_patch(wsi=wsi, location=location, size=size, level=level, dtype=dtype, mode=mode)
+        return self.reader._get_patch(wsi=wsi, location=location, size=size, level=level, dtype=dtype, mode=mode)
 
     def read(self, data: Union[Sequence[PathLike], PathLike, np.ndarray], **kwargs):
         """
@@ -388,34 +424,57 @@ def get_level_count(wsi) -> int:
         """
         return wsi.resolutions["level_count"]  # type: ignore
 
-    @staticmethod
-    def get_size(wsi, level: int) -> Tuple[int, int]:
+    def get_size(self, wsi, level: Optional[int] = None) -> Tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
+        if level is None:
+            level = self.level
+
         return (wsi.resolutions["level_dimensions"][level][1], wsi.resolutions["level_dimensions"][level][0])
 
-    @staticmethod
-    def get_downsample_ratio(wsi, level: int) -> float:
+    def get_downsample_ratio(self, wsi, level: Optional[int] = None) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
+        if level is None:
+            level = self.level
+
         return wsi.resolutions["level_downsamples"][level]  # type: ignore
 
-    def get_file_path(self, wsi) -> str:
+    @staticmethod
+    def get_file_path(wsi) -> str:
         """Return the file path for the WSI object"""
         return str(abspath(wsi.path))
 
+    def get_mpp(self, wsi, level: Optional[int] = None) -> Tuple[float, float]:
+        """
+        Returns the micro-per-pixel resolution of the whole slide image at a given level.
+
+        Args:
+            wsi: a whole slide image object loaded from a file
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
+
+        """
+        if level is None:
+            level = self.level
+
+        factor = float(wsi.resolutions["level_downsamples"][level])
+        return (wsi.metadata["cucim"]["spacing"][1] * factor, wsi.metadata["cucim"]["spacing"][0] * factor)
+
     def read(self, data: Union[Sequence[PathLike], PathLike, np.ndarray], **kwargs):
         """
         Read whole slide image objects from given file or list of files.
@@ -440,7 +499,7 @@ def read(self, data: Union[Sequence[PathLike], PathLike, np.ndarray], **kwargs):
 
         return wsi_list if len(filenames) > 1 else wsi_list[0]
 
-    def get_patch(
+    def _get_patch(
         self, wsi, location: Tuple[int, int], size: Tuple[int, int], level: int, dtype: DtypeLike, mode: str
     ) -> np.ndarray:
         """
@@ -510,34 +569,68 @@ def get_level_count(wsi) -> int:
         """
         return wsi.level_count  # type: ignore
 
-    @staticmethod
-    def get_size(wsi, level: int) -> Tuple[int, int]:
+    def get_size(self, wsi, level: Optional[int] = None) -> Tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
+        if level is None:
+            level = self.level
+
         return (wsi.level_dimensions[level][1], wsi.level_dimensions[level][0])
 
-    @staticmethod
-    def get_downsample_ratio(wsi, level: int) -> float:
+    def get_downsample_ratio(self, wsi, level: Optional[int] = None) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
+        if level is None:
+            level = self.level
+
         return wsi.level_downsamples[level]  # type: ignore
 
-    def get_file_path(self, wsi) -> str:
+    @staticmethod
+    def get_file_path(wsi) -> str:
         """Return the file path for the WSI object"""
         return str(abspath(wsi._filename))
 
+    def get_mpp(self, wsi, level: Optional[int] = None) -> Tuple[float, float]:
+        """
+        Returns the micro-per-pixel resolution of the whole slide image at a given level.
+
+        Args:
+            wsi: a whole slide image object loaded from a file
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
+
+        """
+        if level is None:
+            level = self.level
+        unit = wsi.properties["tiff.ResolutionUnit"]
+        if unit == "centimeter":
+            factor = 10000.0
+        elif unit == "milimeter":
+            factor = 1000.0
+        elif unit == "micrometer":
+            factor = 1.0
+        elif unit == "inch":
+            factor = 25400.0
+        else:
+            raise ValueError(f"The resolution unit is not a valid tiff resolution: {unit}")
+
+        factor *= wsi.level_downsamples[level]
+        return (factor / float(wsi.properties["tiff.YResolution"]), factor / float(wsi.properties["tiff.XResolution"]))
+
     def read(self, data: Union[Sequence[PathLike], PathLike, np.ndarray], **kwargs):
         """
         Read whole slide image objects from given file or list of files.
@@ -561,7 +654,7 @@ def read(self, data: Union[Sequence[PathLike], PathLike, np.ndarray], **kwargs):
 
         return wsi_list if len(filenames) > 1 else wsi_list[0]
 
-    def get_patch(
+    def _get_patch(
         self, wsi, location: Tuple[int, int], size: Tuple[int, int], level: int, dtype: DtypeLike, mode: str
     ) -> np.ndarray:
         """
@@ -623,34 +716,71 @@ def get_level_count(wsi) -> int:
         """
         return len(wsi.pages)
 
-    @staticmethod
-    def get_size(wsi, level: int) -> Tuple[int, int]:
+    def get_size(self, wsi, level: Optional[int] = None) -> Tuple[int, int]:
         """
         Returns the size (height, width) of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
+        if level is None:
+            level = self.level
+
         return (wsi.pages[level].imagelength, wsi.pages[level].imagewidth)
 
-    @staticmethod
-    def get_downsample_ratio(wsi, level: int) -> float:
+    def get_downsample_ratio(self, wsi, level: Optional[int] = None) -> float:
         """
         Returns the down-sampling ratio of the whole slide image at a given level.
 
         Args:
             wsi: a whole slide image object loaded from a file
-            level: the level number where the size is calculated
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
 
         """
+        if level is None:
+            level = self.level
+
         return float(wsi.pages[0].imagelength) / float(wsi.pages[level].imagelength)
 
-    def get_file_path(self, wsi) -> str:
+    @staticmethod
+    def get_file_path(wsi) -> str:
         """Return the file path for the WSI object"""
         return str(abspath(wsi.filehandle.path))
 
+    def get_mpp(self, wsi, level: Optional[int] = None) -> Tuple[float, float]:
+        """
+        Returns the micro-per-pixel resolution of the whole slide image at a given level.
+
+        Args:
+            wsi: a whole slide image object loaded from a file
+            level: the level number where the size is calculated. If not provided the default level (from `self.level`)
+                will be used.
+
+        """
+        if level is None:
+            level = self.level
+
+        unit = wsi.pages[level].tags["ResolutionUnit"].value
+        if unit == unit.CENTIMETER:
+            factor = 10000.0
+        elif unit == unit.MILLIMETER:
+            factor = 1000.0
+        elif unit == unit.MICROMETER:
+            factor = 1.0
+        elif unit == unit.INCH:
+            factor = 25400.0
+        else:
+            raise ValueError(f"The resolution unit is not a valid tiff resolution or missing: {unit.name}")
+
+        # Here x and y resolutions are rational numbers so each of them is represented by a tuple.
+        yres = wsi.pages[level].tags["YResolution"].value
+        xres = wsi.pages[level].tags["XResolution"].value
+        return (factor * yres[1] / yres[0], factor * xres[1] / xres[0])
+
     def read(self, data: Union[Sequence[PathLike], PathLike, np.ndarray], **kwargs):
         """
         Read whole slide image objects from given file or list of files.
@@ -674,7 +804,7 @@ def read(self, data: Union[Sequence[PathLike], PathLike, np.ndarray], **kwargs):
 
         return wsi_list if len(filenames) > 1 else wsi_list[0]
 
-    def get_patch(
+    def _get_patch(
         self, wsi, location: Tuple[int, int], size: Tuple[int, int], level: int, dtype: DtypeLike, mode: str
     ) -> np.ndarray:
         """
diff --git a/monai/utils/enums.py b/monai/utils/enums.py
--- a/monai/utils/enums.py
+++ b/monai/utils/enums.py
@@ -464,10 +464,10 @@ class WSIPatchKeys(StrEnum):
     The keys to be used for metadata of patches extracted from whole slide images
     """
 
-    LOCATION = "patch_location"
-    LEVEL = "patch_level"
-    SIZE = "patch_size"
-    COUNT = "num_patches"
+    LOCATION = "location"
+    LEVEL = "level"
+    SIZE = "size"
+    COUNT = "count"
     PATH = "path"
 
 


Test patch:
diff --git a/tests/test_masked_patch_wsi_dataset.py b/tests/test_masked_patch_wsi_dataset.py
--- a/tests/test_masked_patch_wsi_dataset.py
+++ b/tests/test_masked_patch_wsi_dataset.py
@@ -87,13 +87,13 @@ def test_gen_patches(self, input_parameters, expected):
                 self.assertTrue(d1[ProbMapKeys.NAME] == os.path.basename(d2["image"]))
 
             for i, sample in enumerate(dataset):
-                self.assertEqual(sample["metadata"][WSIPatchKeys.LEVEL], expected["patch_level"])
-                assert_array_equal(sample["metadata"][WSIPatchKeys.SIZE], expected["patch_size"])
+                self.assertEqual(sample["image"].meta[WSIPatchKeys.LEVEL], expected["patch_level"])
+                assert_array_equal(sample["image"].meta[WSIPatchKeys.SIZE], expected["patch_size"])
                 assert_array_equal(sample["image"].shape[1:], expected["patch_size"])
-                self.assertTrue(sample["metadata"][WSIPatchKeys.LOCATION][0] >= 0)
-                self.assertTrue(sample["metadata"][WSIPatchKeys.LOCATION][0] < expected["wsi_size"][0])
-                self.assertTrue(sample["metadata"][WSIPatchKeys.LOCATION][1] >= 0)
-                self.assertTrue(sample["metadata"][WSIPatchKeys.LOCATION][1] < expected["wsi_size"][1])
+                self.assertTrue(sample["image"].meta[WSIPatchKeys.LOCATION][0] >= 0)
+                self.assertTrue(sample["image"].meta[WSIPatchKeys.LOCATION][0] < expected["wsi_size"][0])
+                self.assertTrue(sample["image"].meta[WSIPatchKeys.LOCATION][1] >= 0)
+                self.assertTrue(sample["image"].meta[WSIPatchKeys.LOCATION][1] < expected["wsi_size"][1])
                 if i > 10:
                     break
 
diff --git a/tests/test_patch_wsi_dataset.py b/tests/test_patch_wsi_dataset.py
--- a/tests/test_patch_wsi_dataset.py
+++ b/tests/test_patch_wsi_dataset.py
@@ -21,6 +21,7 @@
 from monai.data import PatchWSIDataset
 from monai.data.wsi_reader import CuCIMWSIReader, OpenSlideWSIReader
 from monai.utils import deprecated, optional_import
+from monai.utils.enums import WSIPatchKeys
 from tests.utils import download_url_or_skip_test, testing_data_config
 
 cucim, has_cim = optional_import("cucim")
@@ -37,7 +38,7 @@
 
 TEST_CASE_DEP_0 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
         "region_size": (1, 1),
         "grid_shape": (1, 1),
         "patch_size": 1,
@@ -48,7 +49,7 @@
 
 TEST_CASE_DEP_0_L1 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
         "region_size": (1, 1),
         "grid_shape": (1, 1),
         "patch_size": 1,
@@ -60,7 +61,7 @@
 
 TEST_CASE_DEP_0_L2 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
         "region_size": (1, 1),
         "grid_shape": (1, 1),
         "patch_size": 1,
@@ -73,7 +74,7 @@
 
 TEST_CASE_DEP_1 = [
     {
-        "data": [{"image": FILE_PATH, "location": [10004, 20004], "label": [0, 0, 0, 1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [10004, 20004], "label": [0, 0, 0, 1]}],
         "region_size": (8, 8),
         "grid_shape": (2, 2),
         "patch_size": 1,
@@ -90,7 +91,7 @@
 
 TEST_CASE_DEP_1_L0 = [
     {
-        "data": [{"image": FILE_PATH, "location": [10004, 20004], "label": [0, 0, 0, 1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [10004, 20004], "label": [0, 0, 0, 1]}],
         "region_size": (8, 8),
         "grid_shape": (2, 2),
         "patch_size": 1,
@@ -108,7 +109,7 @@
 
 TEST_CASE_DEP_1_L1 = [
     {
-        "data": [{"image": FILE_PATH, "location": [10004, 20004], "label": [0, 0, 0, 1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [10004, 20004], "label": [0, 0, 0, 1]}],
         "region_size": (8, 8),
         "grid_shape": (2, 2),
         "patch_size": 1,
@@ -124,7 +125,7 @@
 ]
 TEST_CASE_DEP_2 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
         "region_size": 1,
         "grid_shape": 1,
         "patch_size": 1,
@@ -135,7 +136,7 @@
 
 TEST_CASE_DEP_3 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [[[0, 1], [1, 0]]]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [[[0, 1], [1, 0]]]}],
         "region_size": 1,
         "grid_shape": 1,
         "patch_size": 1,
@@ -146,7 +147,7 @@
 
 TEST_CASE_DEP_OPENSLIDE_0 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
         "region_size": (1, 1),
         "grid_shape": (1, 1),
         "patch_size": 1,
@@ -157,7 +158,7 @@
 
 TEST_CASE_DEP_OPENSLIDE_0_L0 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
         "region_size": (1, 1),
         "grid_shape": (1, 1),
         "patch_size": 1,
@@ -169,7 +170,7 @@
 
 TEST_CASE_DEP_OPENSLIDE_0_L1 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
         "region_size": (1, 1),
         "grid_shape": (1, 1),
         "patch_size": 1,
@@ -182,7 +183,7 @@
 
 TEST_CASE_DEP_OPENSLIDE_0_L2 = [
     {
-        "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
         "region_size": (1, 1),
         "grid_shape": (1, 1),
         "patch_size": 1,
@@ -194,7 +195,7 @@
 
 TEST_CASE_DEP_OPENSLIDE_1 = [
     {
-        "data": [{"image": FILE_PATH, "location": [10004, 20004], "label": [0, 0, 0, 1]}],
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [10004, 20004], "label": [0, 0, 0, 1]}],
         "region_size": (8, 8),
         "grid_shape": (2, 2),
         "patch_size": 1,
@@ -210,39 +211,54 @@
 
 
 TEST_CASE_0 = [
-    {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1], "patch_level": 0}], "patch_size": (1, 1)},
+    {
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1], "patch_level": 0}],
+        "patch_size": (1, 1),
+    },
     {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])},
 ]
 
 TEST_CASE_0_L1 = [
-    {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": (1, 1), "patch_level": 1},
+    {
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
+        "patch_size": (1, 1),
+        "patch_level": 1,
+    },
     {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])},
 ]
 
 TEST_CASE_0_L2 = [
-    {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": (1, 1), "patch_level": 1},
+    {
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
+        "patch_size": (1, 1),
+        "patch_level": 1,
+    },
     {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])},
 ]
 TEST_CASE_1 = [
-    {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "patch_size": 1, "label": [1]}]},
+    {"data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], WSIPatchKeys.SIZE.value: 1, "label": [1]}]},
     {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])},
 ]
 
 TEST_CASE_2 = [
-    {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": 1, "patch_level": 0},
+    {
+        "data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [1]}],
+        "patch_size": 1,
+        "patch_level": 0,
+    },
     {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])},
 ]
 
 TEST_CASE_3 = [
-    {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [[[0, 1], [1, 0]]]}], "patch_size": 1},
+    {"data": [{"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [[[0, 1], [1, 0]]]}], "patch_size": 1},
     {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[0, 1], [1, 0]]])},
 ]
 
 TEST_CASE_4 = [
     {
         "data": [
-            {"image": FILE_PATH, "patch_location": [0, 0], "label": [[[0, 1], [1, 0]]]},
-            {"image": FILE_PATH, "patch_location": [0, 0], "label": [[[1, 0], [0, 0]]]},
+            {"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [[[0, 1], [1, 0]]]},
+            {"image": FILE_PATH, WSIPatchKeys.LOCATION.value: [0, 0], "label": [[[1, 0], [0, 0]]]},
         ],
         "patch_size": 1,
     },
@@ -257,17 +273,17 @@
         "data": [
             {
                 "image": FILE_PATH,
-                "patch_location": [0, 0],
+                WSIPatchKeys.LOCATION.value: [0, 0],
                 "label": [[[0, 1], [1, 0]]],
-                "patch_size": 1,
-                "patch_level": 1,
+                WSIPatchKeys.SIZE.value: 1,
+                WSIPatchKeys.LEVEL.value: 1,
             },
             {
                 "image": FILE_PATH,
-                "patch_location": [100, 100],
+                WSIPatchKeys.LOCATION.value: [100, 100],
                 "label": [[[1, 0], [0, 0]]],
-                "patch_size": 1,
-                "patch_level": 1,
+                WSIPatchKeys.SIZE.value: 1,
+                WSIPatchKeys.LEVEL.value: 1,
             },
         ]
     },
diff --git a/tests/test_sliding_patch_wsi_dataset.py b/tests/test_sliding_patch_wsi_dataset.py
--- a/tests/test_sliding_patch_wsi_dataset.py
+++ b/tests/test_sliding_patch_wsi_dataset.py
@@ -244,11 +244,11 @@ def test_read_patches_large(self, input_parameters, expected):
             dataset = SlidingPatchWSIDataset(reader=self.backend, **input_parameters)
             self.assertEqual(len(dataset), len(expected))
             for i, sample in enumerate(dataset):
-                self.assertEqual(sample["metadata"][WSIPatchKeys.LEVEL], expected[i]["patch_level"])
-                assert_array_equal(sample["metadata"][WSIPatchKeys.SIZE], expected[i]["patch_size"])
+                self.assertEqual(sample["image"].meta[WSIPatchKeys.LEVEL], expected[i]["patch_level"])
+                assert_array_equal(sample["image"].meta[WSIPatchKeys.SIZE], expected[i]["patch_size"])
                 steps = [round(expected[i]["ratio"] * s) for s in expected[i]["patch_size"]]
                 expected_location = tuple(expected[i]["step_loc"][j] * steps[j] for j in range(len(steps)))
-                assert_array_equal(sample["metadata"][WSIPatchKeys.LOCATION], expected_location)
+                assert_array_equal(sample["image"].meta[WSIPatchKeys.LOCATION], expected_location)
 
 
 @skipUnless(has_cucim, "Requires cucim")
diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py
--- a/tests/test_wsireader.py
+++ b/tests/test_wsireader.py
@@ -20,9 +20,9 @@
 from monai.data import DataLoader, Dataset
 from monai.data.image_reader import WSIReader as WSIReaderDeprecated
 from monai.data.wsi_reader import WSIReader
-from monai.transforms import Compose, FromMetaTensord, LoadImaged, ToTensord
+from monai.transforms import Compose, LoadImaged, ToTensord
 from monai.utils import deprecated, first, optional_import
-from monai.utils.enums import PostFix
+from monai.utils.enums import PostFix, WSIPatchKeys
 from tests.utils import assert_allclose, download_url_or_skip_test, testing_data_config
 
 cucim, has_cucim = optional_import("cucim")
@@ -107,6 +107,14 @@
     np.moveaxis(np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), 0, -1),
 ]
 
+
+TEST_CASE_5 = [
+    FILE_PATH,
+    {"level": 2},
+    {"location": (0, 0), "size": (2, 1)},
+    np.array([[[239], [239]], [[239], [239]], [[239], [239]]]),
+]
+
 TEST_CASE_MULTI_WSI = [
     [FILE_PATH, FILE_PATH],
     {"location": (0, 0), "size": (2, 1), "level": 2},
@@ -129,6 +137,8 @@
 TEST_CASE_ERROR_2C = [np.ones((16, 16, 2), dtype=np.uint8)]  # two color channels
 TEST_CASE_ERROR_3D = [np.ones((16, 16, 16, 3), dtype=np.uint8)]  # 3D + color
 
+TEST_CASE_MPP_0 = [FILE_PATH, 0, (1000.0, 1000.0)]
+
 
 def save_rgba_tiff(array: np.ndarray, filename: str, mode: str):
     """
@@ -248,7 +258,6 @@ def test_with_dataloader(self, file_path, level, expected_spatial_shape, expecte
             train_transform = Compose(
                 [
                     LoadImaged(keys=["image"], reader=WSIReaderDeprecated, backend=self.backend, level=level),
-                    FromMetaTensord(keys=["image"]),
                     ToTensord(keys=["image"]),
                 ]
             )
@@ -271,15 +280,16 @@ def test_read_whole_image(self, file_path, level, expected_shape):
                 img, meta = reader.get_data(img_obj)
             self.assertTupleEqual(img.shape, expected_shape)
             self.assertEqual(meta["backend"], self.backend)
-            self.assertEqual(meta["path"], str(os.path.abspath(file_path)))
-            self.assertEqual(meta["patch_level"], level)
-            assert_array_equal(meta["patch_size"], expected_shape[1:])
-            assert_array_equal(meta["patch_location"], (0, 0))
+            self.assertEqual(meta[WSIPatchKeys.PATH], str(os.path.abspath(file_path)))
+            self.assertEqual(meta[WSIPatchKeys.LEVEL], level)
+            assert_array_equal(meta[WSIPatchKeys.SIZE], expected_shape[1:])
+            assert_array_equal(meta[WSIPatchKeys.LOCATION], (0, 0))
 
-        @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4])
+        @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5])
         def test_read_region(self, file_path, kwargs, patch_info, expected_img):
             reader = WSIReader(self.backend, **kwargs)
-            if self.backend == "tifffile" and patch_info["level"] < 2:
+            level = patch_info.get("level", kwargs.get("level"))
+            if self.backend == "tifffile" and level < 2:
                 return
             with reader.read(file_path) as img_obj:
                 # Read twice to check multiple calls
@@ -290,10 +300,10 @@ def test_read_region(self, file_path, kwargs, patch_info, expected_img):
             self.assertTupleEqual(img.shape, expected_img.shape)
             self.assertIsNone(assert_array_equal(img, expected_img))
             self.assertEqual(meta["backend"], self.backend)
-            self.assertEqual(meta["path"], str(os.path.abspath(file_path)))
-            self.assertEqual(meta["patch_level"], patch_info["level"])
-            assert_array_equal(meta["patch_size"], patch_info["size"])
-            assert_array_equal(meta["patch_location"], patch_info["location"])
+            self.assertEqual(meta[WSIPatchKeys.PATH], str(os.path.abspath(file_path)))
+            self.assertEqual(meta[WSIPatchKeys.LEVEL], level)
+            assert_array_equal(meta[WSIPatchKeys.SIZE], patch_info["size"])
+            assert_array_equal(meta[WSIPatchKeys.LOCATION], patch_info["location"])
 
         @parameterized.expand([TEST_CASE_MULTI_WSI])
         def test_read_region_multi_wsi(self, file_path_list, patch_info, expected_img):
@@ -310,10 +320,10 @@ def test_read_region_multi_wsi(self, file_path_list, patch_info, expected_img):
             self.assertTupleEqual(img.shape, expected_img.shape)
             self.assertIsNone(assert_array_equal(img, expected_img))
             self.assertEqual(meta["backend"], self.backend)
-            self.assertEqual(meta["path"][0], str(os.path.abspath(file_path_list[0])))
-            self.assertEqual(meta["patch_level"][0], patch_info["level"])
-            assert_array_equal(meta["patch_size"][0], expected_img.shape[1:])
-            assert_array_equal(meta["patch_location"][0], patch_info["location"])
+            self.assertEqual(meta[WSIPatchKeys.PATH][0], str(os.path.abspath(file_path_list[0])))
+            self.assertEqual(meta[WSIPatchKeys.LEVEL][0], patch_info["level"])
+            assert_array_equal(meta[WSIPatchKeys.SIZE][0], expected_img.shape[1:])
+            assert_array_equal(meta[WSIPatchKeys.LOCATION][0], patch_info["location"])
 
         @parameterized.expand([TEST_CASE_RGB_0, TEST_CASE_RGB_1])
         @skipUnless(has_tiff, "Requires tifffile.")
@@ -353,7 +363,6 @@ def test_with_dataloader(self, file_path, level, expected_spatial_shape, expecte
             train_transform = Compose(
                 [
                     LoadImaged(keys=["image"], reader=WSIReader, backend=self.backend, level=level),
-                    FromMetaTensord(keys=["image"]),
                     ToTensord(keys=["image"]),
                 ]
             )
@@ -369,7 +378,6 @@ def test_with_dataloader_batch(self, file_path, level, expected_spatial_shape, e
             train_transform = Compose(
                 [
                     LoadImaged(keys=["image"], reader=WSIReader, backend=self.backend, level=level),
-                    FromMetaTensord(keys=["image"]),
                     ToTensord(keys=["image"]),
                 ]
             )
@@ -389,10 +397,10 @@ def test_read_whole_image_multi_thread(self, file_path, level, expected_shape):
                     img, meta = reader.get_data(img_obj)
                 self.assertTupleEqual(img.shape, expected_shape)
                 self.assertEqual(meta["backend"], self.backend)
-                self.assertEqual(meta["path"], str(os.path.abspath(file_path)))
-                self.assertEqual(meta["patch_level"], level)
-                assert_array_equal(meta["patch_size"], expected_shape[1:])
-                assert_array_equal(meta["patch_location"], (0, 0))
+                self.assertEqual(meta[WSIPatchKeys.PATH], str(os.path.abspath(file_path)))
+                self.assertEqual(meta[WSIPatchKeys.LEVEL], level)
+                assert_array_equal(meta[WSIPatchKeys.SIZE], expected_shape[1:])
+                assert_array_equal(meta[WSIPatchKeys.LOCATION], (0, 0))
 
         @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4])
         def test_read_region_multi_thread(self, file_path, kwargs, patch_info, expected_img):
@@ -407,10 +415,17 @@ def test_read_region_multi_thread(self, file_path, kwargs, patch_info, expected_
                     self.assertTupleEqual(img.shape, expected_img.shape)
                     self.assertIsNone(assert_array_equal(img, expected_img))
                     self.assertEqual(meta["backend"], self.backend)
-                    self.assertEqual(meta["path"], str(os.path.abspath(file_path)))
-                    self.assertEqual(meta["patch_level"], patch_info["level"])
-                    assert_array_equal(meta["patch_size"], patch_info["size"])
-                    assert_array_equal(meta["patch_location"], patch_info["location"])
+                    self.assertEqual(meta[WSIPatchKeys.PATH], str(os.path.abspath(file_path)))
+                    self.assertEqual(meta[WSIPatchKeys.LEVEL], patch_info["level"])
+                    assert_array_equal(meta[WSIPatchKeys.SIZE], patch_info["size"])
+                    assert_array_equal(meta[WSIPatchKeys.LOCATION], patch_info["location"])
+
+        @parameterized.expand([TEST_CASE_MPP_0])
+        def test_resolution_mpp(self, file_path, level, expected_mpp):
+            reader = WSIReader(self.backend, level=level)
+            with reader.read(file_path) as img_obj:
+                mpp = reader.get_mpp(img_obj, level)
+            self.assertTupleEqual(mpp, expected_mpp)
 
 
 @skipUnless(has_cucim, "Requires cucim")