Repository: Project-MONAI/MONAI

Issue:
Add Tensor support to DataStats transform
**Is your feature request related to a problem? Please describe.**
Need to add `PyTorch Tensor` support for `DataStats` transform to debug post transforms.



Discussion:

Resolving patch:
diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py
--- a/monai/transforms/utility/array.py
+++ b/monai/transforms/utility/array.py
@@ -227,13 +227,15 @@ class DataStats(Transform):
     """
     Utility transform to show the statistics of data for debug or analysis.
     It can be inserted into any place of a transform chain and check results of previous transforms.
+    It support both `numpy.ndarray` and `torch.tensor` as input data,
+    so it can be used in pre-processing and post-processing.
     """
 
     def __init__(
         self,
         prefix: str = "Data",
         data_shape: bool = True,
-        intensity_range: bool = True,
+        value_range: bool = True,
         data_value: bool = False,
         additional_info: Optional[Callable] = None,
         logger_handler: Optional[logging.Handler] = None,
@@ -242,7 +244,7 @@ def __init__(
         Args:
             prefix: will be printed in format: "{prefix} statistics".
             data_shape: whether to show the shape of input data.
-            intensity_range: whether to show the intensity value range of input data.
+            value_range: whether to show the value range of input data.
             data_value: whether to show the raw value of input data.
                 a typical example is to print some properties of Nifti image: affine, pixdim, etc.
             additional_info: user can define callable function to extract additional info from input data.
@@ -256,7 +258,7 @@ def __init__(
         assert isinstance(prefix, str), "prefix must be a string."
         self.prefix = prefix
         self.data_shape = data_shape
-        self.intensity_range = intensity_range
+        self.value_range = value_range
         self.data_value = data_value
         if additional_info is not None and not callable(additional_info):
             raise ValueError("argument `additional_info` must be a callable.")
@@ -272,7 +274,7 @@ def __call__(
         img,
         prefix: Optional[str] = None,
         data_shape: Optional[bool] = None,
-        intensity_range: Optional[bool] = None,
+        value_range: Optional[bool] = None,
         data_value: Optional[bool] = None,
         additional_info=None,
     ):
@@ -283,8 +285,13 @@ def __call__(
 
         if self.data_shape if data_shape is None else data_shape:
             lines.append(f"Shape: {img.shape}")
-        if self.intensity_range if intensity_range is None else intensity_range:
-            lines.append(f"Intensity range: ({np.min(img)}, {np.max(img)})")
+        if self.value_range if value_range is None else value_range:
+            if isinstance(img, np.ndarray):
+                lines.append(f"Value range: ({np.min(img)}, {np.max(img)})")
+            elif torch.is_tensor(img):
+                lines.append(f"Value range: ({torch.min(img)}, {torch.max(img)})")
+            else:
+                lines.append(f"Value range: (not a PyTorch or Numpy array, type: {type(img)})")
         if self.data_value if data_value is None else data_value:
             lines.append(f"Value: {img}")
         additional_info = self.additional_info if additional_info is None else additional_info
diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py
--- a/monai/transforms/utility/dictionary.py
+++ b/monai/transforms/utility/dictionary.py
@@ -267,7 +267,7 @@ def __init__(
         keys: KeysCollection,
         prefix="Data",
         data_shape=True,
-        intensity_range=True,
+        value_range=True,
         data_value=False,
         additional_info=None,
         logger_handler: Optional[Handler] = None,
@@ -280,7 +280,7 @@ def __init__(
                 it also can be a sequence of string, each element corresponds to a key in ``keys``.
             data_shape (bool or list of bool): whether to show the shape of input data.
                 it also can be a sequence of bool, each element corresponds to a key in ``keys``.
-            intensity_range (bool or list of bool): whether to show the intensity value range of input data.
+            value_range (bool or list of bool): whether to show the value range of input data.
                 it also can be a sequence of bool, each element corresponds to a key in ``keys``.
             data_value (bool or list of bool): whether to show the raw value of input data.
                 it also can be a sequence of bool, each element corresponds to a key in ``keys``.
@@ -295,7 +295,7 @@ def __init__(
         super().__init__(keys)
         self.prefix = ensure_tuple_rep(prefix, len(self.keys))
         self.data_shape = ensure_tuple_rep(data_shape, len(self.keys))
-        self.intensity_range = ensure_tuple_rep(intensity_range, len(self.keys))
+        self.value_range = ensure_tuple_rep(value_range, len(self.keys))
         self.data_value = ensure_tuple_rep(data_value, len(self.keys))
         self.additional_info = ensure_tuple_rep(additional_info, len(self.keys))
         self.logger_handler = logger_handler
@@ -308,7 +308,7 @@ def __call__(self, data):
                 d[key],
                 self.prefix[idx],
                 self.data_shape[idx],
-                self.intensity_range[idx],
+                self.value_range[idx],
                 self.data_value[idx],
                 self.additional_info[idx],
             )


Test patch:
diff --git a/tests/test_data_stats.py b/tests/test_data_stats.py
--- a/tests/test_data_stats.py
+++ b/tests/test_data_stats.py
@@ -15,6 +15,7 @@
 import logging
 import tempfile
 import numpy as np
+import torch
 from parameterized import parameterized
 from monai.transforms import DataStats
 
@@ -22,7 +23,7 @@
     {
         "prefix": "test data",
         "data_shape": False,
-        "intensity_range": False,
+        "value_range": False,
         "data_value": False,
         "additional_info": None,
         "logger_handler": None,
@@ -35,7 +36,7 @@
     {
         "prefix": "test data",
         "data_shape": True,
-        "intensity_range": False,
+        "value_range": False,
         "data_value": False,
         "additional_info": None,
         "logger_handler": None,
@@ -48,55 +49,71 @@
     {
         "prefix": "test data",
         "data_shape": True,
-        "intensity_range": True,
+        "value_range": True,
         "data_value": False,
         "additional_info": None,
         "logger_handler": None,
     },
     np.array([[0, 1], [1, 2]]),
-    "test data statistics:\nShape: (2, 2)\nIntensity range: (0, 2)",
+    "test data statistics:\nShape: (2, 2)\nValue range: (0, 2)",
 ]
 
 TEST_CASE_4 = [
     {
         "prefix": "test data",
         "data_shape": True,
-        "intensity_range": True,
+        "value_range": True,
         "data_value": True,
         "additional_info": None,
         "logger_handler": None,
     },
     np.array([[0, 1], [1, 2]]),
-    "test data statistics:\nShape: (2, 2)\nIntensity range: (0, 2)\nValue: [[0 1]\n [1 2]]",
+    "test data statistics:\nShape: (2, 2)\nValue range: (0, 2)\nValue: [[0 1]\n [1 2]]",
 ]
 
 TEST_CASE_5 = [
     {
         "prefix": "test data",
         "data_shape": True,
-        "intensity_range": True,
+        "value_range": True,
         "data_value": True,
         "additional_info": lambda x: np.mean(x),
         "logger_handler": None,
     },
     np.array([[0, 1], [1, 2]]),
-    "test data statistics:\nShape: (2, 2)\nIntensity range: (0, 2)\nValue: [[0 1]\n [1 2]]\nAdditional info: 1.0",
+    "test data statistics:\nShape: (2, 2)\nValue range: (0, 2)\nValue: [[0 1]\n [1 2]]\nAdditional info: 1.0",
 ]
 
 TEST_CASE_6 = [
+    {
+        "prefix": "test data",
+        "data_shape": True,
+        "value_range": True,
+        "data_value": True,
+        "additional_info": lambda x: torch.mean(x.float()),
+        "logger_handler": None,
+    },
+    torch.tensor([[0, 1], [1, 2]]),
+    (
+        "test data statistics:\nShape: torch.Size([2, 2])\nValue range: (0, 2)\n"
+        "Value: tensor([[0, 1],\n        [1, 2]])\nAdditional info: 1.0"
+    ),
+]
+
+TEST_CASE_7 = [
     np.array([[0, 1], [1, 2]]),
-    "test data statistics:\nShape: (2, 2)\nIntensity range: (0, 2)\nValue: [[0 1]\n [1 2]]\nAdditional info: 1.0\n",
+    "test data statistics:\nShape: (2, 2)\nValue range: (0, 2)\nValue: [[0 1]\n [1 2]]\nAdditional info: 1.0\n",
 ]
 
 
 class TestDataStats(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5])
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6])
     def test_value(self, input_param, input_data, expected_print):
         transform = DataStats(**input_param)
         _ = transform(input_data)
         self.assertEqual(transform.output, expected_print)
 
-    @parameterized.expand([TEST_CASE_6])
+    @parameterized.expand([TEST_CASE_7])
     def test_file(self, input_data, expected_print):
         tempdir = tempfile.mkdtemp()
         filename = os.path.join(tempdir, "test_data_stats.log")
@@ -104,7 +121,7 @@ def test_file(self, input_data, expected_print):
         input_param = {
             "prefix": "test data",
             "data_shape": True,
-            "intensity_range": True,
+            "value_range": True,
             "data_value": True,
             "additional_info": lambda x: np.mean(x),
             "logger_handler": handler,
diff --git a/tests/test_data_statsd.py b/tests/test_data_statsd.py
--- a/tests/test_data_statsd.py
+++ b/tests/test_data_statsd.py
@@ -15,6 +15,7 @@
 import logging
 import tempfile
 import numpy as np
+import torch
 from parameterized import parameterized
 from monai.transforms import DataStatsd
 
@@ -23,7 +24,7 @@
         "keys": "img",
         "prefix": "test data",
         "data_shape": False,
-        "intensity_range": False,
+        "value_range": False,
         "data_value": False,
         "additional_info": None,
     },
@@ -36,7 +37,7 @@
         "keys": "img",
         "prefix": "test data",
         "data_shape": True,
-        "intensity_range": False,
+        "value_range": False,
         "data_value": False,
         "additional_info": None,
     },
@@ -49,12 +50,12 @@
         "keys": "img",
         "prefix": "test data",
         "data_shape": True,
-        "intensity_range": True,
+        "value_range": True,
         "data_value": False,
         "additional_info": None,
     },
     {"img": np.array([[0, 1], [1, 2]])},
-    "test data statistics:\nShape: (2, 2)\nIntensity range: (0, 2)",
+    "test data statistics:\nShape: (2, 2)\nValue range: (0, 2)",
 ]
 
 TEST_CASE_4 = [
@@ -62,12 +63,12 @@
         "keys": "img",
         "prefix": "test data",
         "data_shape": True,
-        "intensity_range": True,
+        "value_range": True,
         "data_value": True,
         "additional_info": None,
     },
     {"img": np.array([[0, 1], [1, 2]])},
-    "test data statistics:\nShape: (2, 2)\nIntensity range: (0, 2)\nValue: [[0 1]\n [1 2]]",
+    "test data statistics:\nShape: (2, 2)\nValue range: (0, 2)\nValue: [[0 1]\n [1 2]]",
 ]
 
 TEST_CASE_5 = [
@@ -75,20 +76,36 @@
         "keys": "img",
         "prefix": "test data",
         "data_shape": True,
-        "intensity_range": True,
+        "value_range": True,
         "data_value": True,
         "additional_info": lambda x: np.mean(x),
     },
     {"img": np.array([[0, 1], [1, 2]])},
-    "test data statistics:\nShape: (2, 2)\nIntensity range: (0, 2)\nValue: [[0 1]\n [1 2]]\nAdditional info: 1.0",
+    "test data statistics:\nShape: (2, 2)\nValue range: (0, 2)\nValue: [[0 1]\n [1 2]]\nAdditional info: 1.0",
 ]
 
 TEST_CASE_6 = [
+    {
+        "keys": "img",
+        "prefix": "test data",
+        "data_shape": True,
+        "value_range": True,
+        "data_value": True,
+        "additional_info": lambda x: torch.mean(x.float()),
+    },
+    {"img": torch.tensor([[0, 1], [1, 2]])},
+    (
+        "test data statistics:\nShape: torch.Size([2, 2])\nValue range: (0, 2)\n"
+        "Value: tensor([[0, 1],\n        [1, 2]])\nAdditional info: 1.0"
+    ),
+]
+
+TEST_CASE_7 = [
     {
         "keys": ("img", "affine"),
         "prefix": ("image", "affine"),
         "data_shape": True,
-        "intensity_range": (True, False),
+        "value_range": (True, False),
         "data_value": (False, True),
         "additional_info": (lambda x: np.mean(x), None),
     },
@@ -96,20 +113,20 @@
     "affine statistics:\nShape: (2, 2)\nValue: [[1. 0.]\n [0. 1.]]",
 ]
 
-TEST_CASE_7 = [
+TEST_CASE_8 = [
     {"img": np.array([[0, 1], [1, 2]])},
-    "test data statistics:\nShape: (2, 2)\nIntensity range: (0, 2)\nValue: [[0 1]\n [1 2]]\nAdditional info: 1.0\n",
+    "test data statistics:\nShape: (2, 2)\nValue range: (0, 2)\nValue: [[0 1]\n [1 2]]\nAdditional info: 1.0\n",
 ]
 
 
 class TestDataStatsd(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6])
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6, TEST_CASE_7])
     def test_value(self, input_param, input_data, expected_print):
         transform = DataStatsd(**input_param)
         _ = transform(input_data)
         self.assertEqual(transform.printer.output, expected_print)
 
-    @parameterized.expand([TEST_CASE_7])
+    @parameterized.expand([TEST_CASE_8])
     def test_file(self, input_data, expected_print):
         tempdir = tempfile.mkdtemp()
         filename = os.path.join(tempdir, "test_stats.log")
@@ -118,7 +135,7 @@ def test_file(self, input_data, expected_print):
             "keys": "img",
             "prefix": "test data",
             "data_shape": True,
-            "intensity_range": True,
+            "value_range": True,
             "data_value": True,
             "additional_info": lambda x: np.mean(x),
             "logger_handler": handler,
diff --git a/tests/test_dataloader.py b/tests/test_dataloader.py
--- a/tests/test_dataloader.py
+++ b/tests/test_dataloader.py
@@ -22,7 +22,7 @@ def test_values(self):
         ]
         transform = Compose(
             [
-                DataStatsd(keys=["image", "label"], data_shape=False, intensity_range=False, data_value=True),
+                DataStatsd(keys=["image", "label"], data_shape=False, value_range=False, data_value=True),
                 SimulateDelayd(keys=["image", "label"], delay_time=0.1),
             ]
         )