Repository: Project-MONAI/MONAI

Issue:
min max function bug in `safe_dtype_range`
```
Traceback (most recent call last):
  File "/opt/conda/lib/python3.8/site-packages/parameterized/parameterized.py", line 533, in standalone_func
    return func(*(a + p.args), **p.kwargs)
  File "tests/test_safe_dtype_range.py", line 59, in test_safe_dtype_range
    result = safe_dtype_range(in_image, out_dtype)
  File "/workspace/Code/MONAI/monai/utils/type_conversion.py", line 446, in safe_dtype_range
    return _safe_dtype_range(data, dtype)
  File "/workspace/Code/MONAI/monai/utils/type_conversion.py", line 430, in _safe_dtype_range
    data_bound = (min(data), max(data))
  File "/workspace/Code/MONAI/monai/data/meta_tensor.py", line 268, in __torch_function__
    ret = super().__torch_function__(func, types, args, kwargs)
  File "/opt/conda/lib/python3.8/site-packages/torch/_tensor.py", line 1265, in __torch_function__
    ret = func(*args, **kwargs)
RuntimeError: Boolean value of Tensor with more than one value is ambiguous
```


Discussion:

Resolving patch:
diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py
--- a/monai/transforms/intensity/array.py
+++ b/monai/transforms/intensity/array.py
@@ -220,12 +220,15 @@ class ShiftIntensity(Transform):
 
     Args:
         offset: offset value to shift the intensity of image.
+        safe: if `True`, then do safe dtype convert when intensity overflow. default to `False`.
+            E.g., `[256, -12]` -> `[array(0), array(244)]`. If `True`, then `[256, -12]` -> `[array(255), array(0)]`.
     """
 
     backend = [TransformBackends.TORCH, TransformBackends.NUMPY]
 
-    def __init__(self, offset: float) -> None:
+    def __init__(self, offset: float, safe: bool = False) -> None:
         self.offset = offset
+        self.safe = safe
 
     def __call__(self, img: NdarrayOrTensor, offset: Optional[float] = None) -> NdarrayOrTensor:
         """
@@ -235,7 +238,7 @@ def __call__(self, img: NdarrayOrTensor, offset: Optional[float] = None) -> Ndar
         img = convert_to_tensor(img, track_meta=get_track_meta())
         offset = self.offset if offset is None else offset
         out = img + offset
-        out, *_ = convert_data_type(data=out, dtype=img.dtype)
+        out, *_ = convert_data_type(data=out, dtype=img.dtype, safe=self.safe)
 
         return out
 
@@ -247,11 +250,13 @@ class RandShiftIntensity(RandomizableTransform):
 
     backend = [TransformBackends.TORCH, TransformBackends.NUMPY]
 
-    def __init__(self, offsets: Union[Tuple[float, float], float], prob: float = 0.1) -> None:
+    def __init__(self, offsets: Union[Tuple[float, float], float], safe: bool = False, prob: float = 0.1) -> None:
         """
         Args:
             offsets: offset range to randomly shift.
                 if single number, offset value is picked from (-offsets, offsets).
+            safe: if `True`, then do safe dtype convert when intensity overflow. default to `False`.
+                E.g., `[256, -12]` -> `[array(0), array(244)]`. If `True`, then `[256, -12]` -> `[array(255), array(0)]`.
             prob: probability of shift.
         """
         RandomizableTransform.__init__(self, prob)
@@ -262,7 +267,7 @@ def __init__(self, offsets: Union[Tuple[float, float], float], prob: float = 0.1
         else:
             self.offsets = (min(offsets), max(offsets))
         self._offset = self.offsets[0]
-        self._shifter = ShiftIntensity(self._offset)
+        self._shifter = ShiftIntensity(self._offset, safe)
 
     def randomize(self, data: Optional[Any] = None) -> None:
         super().randomize(None)
diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py
--- a/monai/transforms/intensity/dictionary.py
+++ b/monai/transforms/intensity/dictionary.py
@@ -304,6 +304,7 @@ def __init__(
         self,
         keys: KeysCollection,
         offset: float,
+        safe: bool = False,
         factor_key: Optional[str] = None,
         meta_keys: Optional[KeysCollection] = None,
         meta_key_postfix: str = DEFAULT_POST_FIX,
@@ -314,6 +315,8 @@ def __init__(
             keys: keys of the corresponding items to be transformed.
                 See also: :py:class:`monai.transforms.compose.MapTransform`
             offset: offset value to shift the intensity of image.
+            safe: if `True`, then do safe dtype convert when intensity overflow. default to `False`.
+                E.g., `[256, -12]` -> `[array(0), array(244)]`. If `True`, then `[256, -12]` -> `[array(255), array(0)]`.
             factor_key: if not None, use it as the key to extract a value from the corresponding
                 metadata dictionary of `key` at runtime, and multiply the `offset` to shift intensity.
                 Usually, `IntensityStatsd` transform can pre-compute statistics of intensity values
@@ -336,7 +339,7 @@ def __init__(
         if len(self.keys) != len(self.meta_keys):
             raise ValueError("meta_keys should have the same length as keys.")
         self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys))
-        self.shifter = ShiftIntensity(offset)
+        self.shifter = ShiftIntensity(offset, safe)
 
     def __call__(self, data) -> Dict[Hashable, NdarrayOrTensor]:
         d = dict(data)
@@ -361,6 +364,7 @@ def __init__(
         self,
         keys: KeysCollection,
         offsets: Union[Tuple[float, float], float],
+        safe: bool = False,
         factor_key: Optional[str] = None,
         meta_keys: Optional[KeysCollection] = None,
         meta_key_postfix: str = DEFAULT_POST_FIX,
@@ -373,6 +377,8 @@ def __init__(
                 See also: :py:class:`monai.transforms.compose.MapTransform`
             offsets: offset range to randomly shift.
                 if single number, offset value is picked from (-offsets, offsets).
+            safe: if `True`, then do safe dtype convert when intensity overflow. default to `False`.
+                E.g., `[256, -12]` -> `[array(0), array(244)]`. If `True`, then `[256, -12]` -> `[array(255), array(0)]`.
             factor_key: if not None, use it as the key to extract a value from the corresponding
                 metadata dictionary of `key` at runtime, and multiply the random `offset` to shift intensity.
                 Usually, `IntensityStatsd` transform can pre-compute statistics of intensity values
@@ -399,7 +405,7 @@ def __init__(
         if len(self.keys) != len(self.meta_keys):
             raise ValueError("meta_keys should have the same length as keys.")
         self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys))
-        self.shifter = RandShiftIntensity(offsets=offsets, prob=1.0)
+        self.shifter = RandShiftIntensity(offsets=offsets, safe=safe, prob=1.0)
 
     def set_random_state(
         self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None
diff --git a/monai/utils/type_conversion.py b/monai/utils/type_conversion.py
--- a/monai/utils/type_conversion.py
+++ b/monai/utils/type_conversion.py
@@ -423,7 +423,10 @@ def _safe_dtype_range(data, dtype):
         if data.ndim == 0:
             data_bound = (data, data)
         else:
-            data_bound = (min(data), max(data))
+            if isinstance(data, torch.Tensor):
+                data_bound = (torch.min(data), torch.max(data))
+            else:
+                data_bound = (np.min(data), np.max(data))
         if (data_bound[1] > dtype_bound_value[1]) or (data_bound[0] < dtype_bound_value[0]):
             if isinstance(data, torch.Tensor):
                 return torch.clamp(data, dtype_bound_value[0], dtype_bound_value[1])


Test patch:
diff --git a/tests/test_safe_dtype_range.py b/tests/test_safe_dtype_range.py
--- a/tests/test_safe_dtype_range.py
+++ b/tests/test_safe_dtype_range.py
@@ -28,6 +28,8 @@
     if in_type is not float:
         TESTS.append((in_type(np.array(256)), in_type(np.array(255)), np.uint8))  # type: ignore
         TESTS.append((in_type(np.array(-12)), in_type(np.array(0)), np.uint8))  # type: ignore
+for in_type in TEST_NDARRAYS_ALL:
+    TESTS.append((in_type(np.array([[256, 255], [-12, 0]])), in_type(np.array([[255, 255], [0, 0]])), np.uint8))  # type: ignore
 
 TESTS_LIST: List[Tuple] = []
 for in_type in TEST_NDARRAYS_ALL + (int, float):