internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: Project-MONAI/MONAI

Issue:
Checkpoint handler save files with wrong permissions
**Describe the bug**
We always run programs in dockers, usually it's root user, and found an issue that: pure PyTorch program saves model(torch.save) and has the read permission for non-root users, but ignite Checkpoint handler saves model and only has read permission for root users.
Reproduce this issue by below 2 MONAI examples:
1. https://github.com/Project-MONAI/tutorials/blob/master/3d_segmentation/torch/unet_training_dict.py
The saved model will be:
```
-rw-r--r-- 1 root root 19253862 Mar 26 02:08 best_metric_model_segmentation3d_dict.pth
```
2. https://github.com/Project-MONAI/tutorials/blob/master/3d_segmentation/ignite/unet_training_dict.py
The saved model will be:
```
-rw------- 1 root root 57738580 Mar 26 01:54 net_checkpoint_50.pt
```


Discussion:

Resolving patch:
diff --git a/monai/handlers/checkpoint_saver.py b/monai/handlers/checkpoint_saver.py
--- a/monai/handlers/checkpoint_saver.py
+++ b/monai/handlers/checkpoint_saver.py
@@ -116,7 +116,9 @@ class _DiskSaver(DiskSaver):
             """
 
             def __init__(self, dirname: str, filename: Optional[str] = None):
-                super().__init__(dirname=dirname, require_empty=False)
+                # set `atomic=False` as `atomic=True` only gives read/write permission to the user who saved the file,
+                # without group/others read permission
+                super().__init__(dirname=dirname, require_empty=False, atomic=False)
                 self.filename = filename
 
             def __call__(self, checkpoint: Dict, filename: str, metadata: Optional[Dict] = None) -> None:


Test patch:
Repository: Project-MONAI/MONAI

Issue:
allows for single key in `ConcatItemsd` 
Hi everyone I'm using `ConcatItemsd` for concatenating the inputs from multi-modalities images. During development sometimes I adjust the number of modalities of inputs, even to single modality. However I found that `ConcatItemsd` in monai currently does not allow this, with an exception raised [here](https://github.com/Project-MONAI/MONAI/blob/master/monai/transforms/utility/dictionary.py#L678-L679). 

This restriction is inconvenient for me and personally I can't find the point of this restriction because both `numpy.concatenate` and `torch.cat` work fine with an input list of length 1. I'd like to hear your opinions on this :)

_Originally posted by @function2-llx in https://github.com/Project-MONAI/MONAI/discussions/1880_


Discussion:

Resolving patch:
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
@@ -675,8 +675,6 @@ def __init__(self, keys: KeysCollection, name: str, dim: int = 0, allow_missing_
 
         """
         super().__init__(keys, allow_missing_keys)
-        if len(self.keys) < 2:
-            raise ValueError("Concatenation requires at least 2 keys.")
         self.name = name
         self.dim = dim
 


Test patch:
diff --git a/tests/test_concat_itemsd.py b/tests/test_concat_itemsd.py
--- a/tests/test_concat_itemsd.py
+++ b/tests/test_concat_itemsd.py
@@ -38,6 +38,20 @@ def test_numpy_values(self):
         np.testing.assert_allclose(result["img1"], np.array([[0, 1], [1, 2]]))
         np.testing.assert_allclose(result["cat_img"], np.array([[1, 2], [2, 3], [1, 2], [2, 3]]))
 
+    def test_single_numpy(self):
+        input_data = {"img": np.array([[0, 1], [1, 2]])}
+        result = ConcatItemsd(keys="img", name="cat_img")(input_data)
+        result["cat_img"] += 1
+        np.testing.assert_allclose(result["img"], np.array([[0, 1], [1, 2]]))
+        np.testing.assert_allclose(result["cat_img"], np.array([[1, 2], [2, 3]]))
+
+    def test_single_tensor(self):
+        input_data = {"img": torch.tensor([[0, 1], [1, 2]])}
+        result = ConcatItemsd(keys="img", name="cat_img")(input_data)
+        result["cat_img"] += 1
+        torch.testing.assert_allclose(result["img"], torch.tensor([[0, 1], [1, 2]]))
+        torch.testing.assert_allclose(result["cat_img"], torch.tensor([[1, 2], [2, 3]]))
+
 
 if __name__ == "__main__":
     unittest.main()
Repository: Project-MONAI/MONAI

Issue:
ConvertToMultiChannelBasedOnBratsClasses should support channel first input
**Describe the bug**
Currently, `ConvertToMultiChannelBasedOnBratsClasses` transform expects the input data has no channel dim, should also support channel-first data.


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
@@ -659,6 +659,10 @@ class ConvertToMultiChannelBasedOnBratsClasses(Transform):
     """
 
     def __call__(self, img: np.ndarray) -> np.ndarray:
+        # if img has channel dim, squeeze it
+        if img.ndim == 4 and img.shape[0] == 1:
+            img = np.squeeze(img, axis=0)
+
         result = []
         # merge labels 1 (tumor non-enh) and 4 (tumor enh) to TC
         result.append(np.logical_or(img == 1, img == 4))


Test patch:
diff --git a/tests/test_convert_to_multi_channel.py b/tests/test_convert_to_multi_channel.py
--- a/tests/test_convert_to_multi_channel.py
+++ b/tests/test_convert_to_multi_channel.py
@@ -16,14 +16,25 @@
 
 from monai.transforms import ConvertToMultiChannelBasedOnBratsClasses
 
-TEST_CASE = [
+TEST_CASE_1 = [
     np.array([[0, 1, 2], [1, 2, 4], [0, 1, 4]]),
     np.array([[[0, 1, 0], [1, 0, 1], [0, 1, 1]], [[0, 1, 1], [1, 1, 1], [0, 1, 1]], [[0, 0, 0], [0, 0, 1], [0, 0, 1]]]),
 ]
 
+TEST_CASE_2 = [
+    np.array([[[[0, 1], [1, 2]], [[2, 4], [4, 4]]]]),
+    np.array(
+        [
+            [[[0, 1], [1, 0]], [[0, 1], [1, 1]]],
+            [[[0, 1], [1, 1]], [[1, 1], [1, 1]]],
+            [[[0, 0], [0, 0]], [[0, 1], [1, 1]]],
+        ]
+    ),
+]
+
 
 class TestConvertToMultiChannel(unittest.TestCase):
-    @parameterized.expand([TEST_CASE])
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2])
     def test_type_shape(self, data, expected_result):
         result = ConvertToMultiChannelBasedOnBratsClasses()(data)
         np.testing.assert_equal(result, expected_result)
Repository: Project-MONAI/MONAI

Issue:
unit test ThreadDataLoader error
**Describe the bug**
```
/opt/monai# python -m tests.test_thread_buffer
```
output:
```
Exception in thread Thread-1:
Traceback (most recent call last):
  File "/opt/conda/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/opt/conda/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/opt/monai/monai/data/thread_buffer.py", line 45, in enqueue_values
    for src_val in self.src:
TypeError: 'super' object is not iterable
...
----------------------------------------------------------------------
Ran 3 tests in 3.625s
```

e.g. https://github.com/Project-MONAI/MONAI/runs/2266946281?check_suite_focus=true#step:6:3770



Discussion:

Resolving patch:
diff --git a/monai/data/thread_buffer.py b/monai/data/thread_buffer.py
--- a/monai/data/thread_buffer.py
+++ b/monai/data/thread_buffer.py
@@ -88,7 +88,7 @@ def __init__(self, dataset: Dataset, num_workers: int = 0, **kwargs):
         super().__init__(dataset, num_workers, **kwargs)
 
         # ThreadBuffer will use the inherited __iter__ instead of the one defined below
-        self.buffer = ThreadBuffer(super())
+        self.buffer = ThreadBuffer(super().__iter__())
 
     def __iter__(self):
         yield from self.buffer


Test patch:
Repository: Project-MONAI/MONAI

Issue:
Always print warning message about min_package
**Describe the bug**
/opt/monai/monai/utils/module.py:100: UserWarning: <module 'pkg_resources' from '/opt/conda/lib/python3.8/site-packages/pkg_resources/__init__.py'> has no attribute __version__ in min_version check.
  warnings.warn(f"{the_module} has no attribute __version__ in min_version check.")



Discussion:

Resolving patch:
diff --git a/monai/utils/module.py b/monai/utils/module.py
--- a/monai/utils/module.py
+++ b/monai/utils/module.py
@@ -96,11 +96,9 @@ def min_version(the_module, min_version_str: str = "") -> bool:
     Returns True if the module's version is greater or equal to the 'min_version'.
     When min_version_str is not provided, it always returns True.
     """
-    if not min_version_str:
+    if not min_version_str or not hasattr(the_module, "__version__"):
         return True  # always valid version
-    if not hasattr(the_module, "__version__"):
-        warnings.warn(f"{the_module} has no attribute __version__ in min_version check.")
-        return True  # min_version is the default, shouldn't be noisy
+
     mod_version = tuple(int(x) for x in the_module.__version__.split(".")[:2])
     required = tuple(int(x) for x in min_version_str.split(".")[:2])
     return mod_version >= required


Test patch:
Repository: Project-MONAI/MONAI

Issue:
Need to doc-string how to get image meta data in SegmentationSaver
**Is your feature request related to a problem? Please describe.**
We already received several questions about how to get the image meta data and apply to the model output.
Need to enhance the doc-string of SegmentationSaver handler.


Discussion:

Resolving patch:
diff --git a/monai/handlers/segmentation_saver.py b/monai/handlers/segmentation_saver.py
--- a/monai/handlers/segmentation_saver.py
+++ b/monai/handlers/segmentation_saver.py
@@ -28,6 +28,9 @@
 class SegmentationSaver:
     """
     Event handler triggered on completing every iteration to save the segmentation predictions into files.
+    It can extract the input image meta data(filename, affine, original_shape, etc.) and resample the predictions
+    based on the meta data.
+
     """
 
     def __init__(
@@ -96,6 +99,7 @@ def __init__(
                 output will be: /output/test1/image/image_seg.nii.gz
             batch_transform: a callable that is used to transform the
                 ignite.engine.batch into expected format to extract the meta_data dictionary.
+                it can be used to extract the input image meta data: filename, affine, original_shape, etc.
             output_transform: a callable that is used to transform the
                 ignite.engine.output into the form expected image data.
                 The first dimension of this transform's output will be treated as the
diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py
--- a/monai/transforms/io/dictionary.py
+++ b/monai/transforms/io/dictionary.py
@@ -132,9 +132,10 @@ class SaveImaged(MapTransform):
         keys: keys of the corresponding items to be transformed.
             See also: :py:class:`monai.transforms.compose.MapTransform`
         meta_key_postfix: `key_{postfix}` was used to store the metadata in `LoadImaged`.
-            So need the key to extract metadata to save images, default is `meta_dict`.
-            The meta data is a dictionary object, if no corresponding metadata, set to `None`.
-            For example, for data with key `image`, the metadata by default is in `image_meta_dict`.
+            so need the key to extract metadata to save images, default is `meta_dict`.
+            for example, for data with key `image`, the metadata by default is in `image_meta_dict`.
+            the meta data is a dictionary object which contains: filename, affine, original_shape, etc.
+            if no corresponding metadata, set to `None`.
         output_dir: output image directory.
         output_postfix: a string appended to all output file names, default to `trans`.
         output_ext: output file extension name, available extensions: `.nii.gz`, `.nii`, `.png`.


Test patch:
Repository: Project-MONAI/MONAI

Issue:
MapLabelValue is incorrect when mapping strings
**Describe the bug**
the issue is mainly with the string mapping

**To Reproduce**
```py
from monai.transforms import MapLabelValue
xform = MapLabelValue(["label3", "label2", "label1"], ["label3", "label2", "label1"])
xform([["label3", "label1"], ["label1", "label2"]])
```
output:
```
array([[0., 0.],
       [0., 0.]], dtype=float32)
```


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
@@ -777,6 +777,9 @@ def __init__(self, orig_labels: Sequence, target_labels: Sequence, dtype: DtypeL
         """
         if len(orig_labels) != len(target_labels):
             raise ValueError("orig_labels and target_labels must have the same length.")
+        if all([o == z for o, z in zip(orig_labels, target_labels)]):
+            raise ValueError("orig_labels and target_labels are exactly the same, should be different to map.")
+
         self.orig_labels = orig_labels
         self.target_labels = target_labels
         self.dtype = dtype


Test patch:
diff --git a/tests/test_map_label_value.py b/tests/test_map_label_value.py
--- a/tests/test_map_label_value.py
+++ b/tests/test_map_label_value.py
@@ -58,9 +58,26 @@
     np.array([["label0", "label2"], ["label2", "label1"]]),
 ]
 
+TEST_CASE_8 = [
+    {"orig_labels": ["label3", "label2", "label1"], "target_labels": ["label1", "label2", "label3"], "dtype": "str"},
+    np.array([["label3", "label1"], ["label1", "label2"]]),
+    np.array([["label1", "label3"], ["label3", "label2"]]),
+]
+
 
 class TestMapLabelValue(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6, TEST_CASE_7])
+    @parameterized.expand(
+        [
+            TEST_CASE_1,
+            TEST_CASE_2,
+            TEST_CASE_3,
+            TEST_CASE_4,
+            TEST_CASE_5,
+            TEST_CASE_6,
+            TEST_CASE_7,
+            TEST_CASE_8,
+        ]
+    )
     def test_shape(self, input_param, input_data, expected_value):
         result = MapLabelValue(**input_param)(input_data)
         np.testing.assert_equal(result, expected_value)
diff --git a/tests/test_sailency_inferer.py b/tests/test_saliency_inferer.py
similarity index 97%
rename from tests/test_sailency_inferer.py
rename to tests/test_saliency_inferer.py
--- a/tests/test_sailency_inferer.py
+++ b/tests/test_saliency_inferer.py
@@ -25,7 +25,7 @@
 TEST_CASE_3 = ["GradCAMpp"]
 
 
-class TestGradientClassActivationMap(unittest.TestCase):
+class TestSaliencyInferer(unittest.TestCase):
     @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3])
     def test_shape(self, cam_name):
         model = DenseNet(
Repository: Project-MONAI/MONAI

Issue:
Inverse of SpacingD transform can't work with SegmentationSaver resample
**Describe the bug**
If we set `resample=True` in the SegmentationSaver and save the inverted results of SpacingD transform, it will output a data with wrong content, seems somewhere modified `affine`.



Discussion:

Resolving patch:
diff --git a/monai/handlers/segmentation_saver.py b/monai/handlers/segmentation_saver.py
--- a/monai/handlers/segmentation_saver.py
+++ b/monai/handlers/segmentation_saver.py
@@ -10,6 +10,7 @@
 # limitations under the License.
 
 import logging
+import warnings
 from typing import TYPE_CHECKING, Callable, Optional, Union
 
 import numpy as np
@@ -120,6 +121,7 @@ def __init__(
             squeeze_end_dims=squeeze_end_dims,
             data_root_dir=data_root_dir,
         )
+        self.resample = resample
         self.batch_transform = batch_transform
         self.output_transform = output_transform
 
@@ -148,6 +150,9 @@ def __call__(self, engine: Engine) -> None:
         engine_output = self.output_transform(engine.state.output)
         if isinstance(engine_output, (tuple, list)):
             # if a list of data in shape: [channel, H, W, [D]], save every item separately
+            if self.resample:
+                warnings.warn("if saving inverted data, please set `resample=False` as it's already resampled.")
+
             self._saver.save_batch = False
             for i, d in enumerate(engine_output):
                 self._saver(d, {k: meta_data[k][i] for k in meta_data} if meta_data is not None else None)


Test patch:
Repository: Project-MONAI/MONAI

Issue:
ignore `label` in the workflow if it's not tensor
**Is your feature request related to a problem? Please describe.**
During evaluation, now we use `prepare_batch` to extract `image` and `label` fields if existing the key. But maybe we don't want to load `label` for inference and didn't apply transforms, so need to ignore it for this case.



Discussion:

Resolving patch:
diff --git a/monai/engines/utils.py b/monai/engines/utils.py
--- a/monai/engines/utils.py
+++ b/monai/engines/utils.py
@@ -107,7 +107,7 @@ def default_prepare_batch(
     """
     if not isinstance(batchdata, dict):
         raise AssertionError("default prepare_batch expects dictionary input data.")
-    if CommonKeys.LABEL in batchdata:
+    if isinstance(batchdata.get(CommonKeys.LABEL, None), torch.Tensor):
         return (
             batchdata[CommonKeys.IMAGE].to(device=device, non_blocking=non_blocking),
             batchdata[CommonKeys.LABEL].to(device=device, non_blocking=non_blocking),


Test patch:
Repository: Project-MONAI/MONAI

Issue:
Add support to compute metrics on list of images and tensor label
**Is your feature request related to a problem? Please describe.**
Need to support the case that the image is a list of tensors and label is only 1 tensor during metrics computation.



Discussion:

Resolving patch:
diff --git a/monai/handlers/iteration_metric.py b/monai/handlers/iteration_metric.py
--- a/monai/handlers/iteration_metric.py
+++ b/monai/handlers/iteration_metric.py
@@ -80,8 +80,8 @@ def _compute(y_pred, y):
             score = self.metric_fn(y_pred, y)
             return score[0] if isinstance(score, (tuple, list)) else score
 
-        if isinstance(y_pred, (list, tuple)) and isinstance(y, (list, tuple)):
-            # if a list of channel-first data, add batch dim and compute metric, then concat the scores
+        if isinstance(y_pred, (list, tuple)) or isinstance(y, (list, tuple)):
+            # if y_pred or y is a list of channel-first data, add batch dim and compute metric, then concat the scores
             score = torch.cat([_compute(p_.unsqueeze(0), y_.unsqueeze(0)) for p_, y_ in zip(y_pred, y)], dim=0)
         else:
             score = _compute(y_pred, y)


Test patch:
diff --git a/tests/test_handler_mean_dice.py b/tests/test_handler_mean_dice.py
--- a/tests/test_handler_mean_dice.py
+++ b/tests/test_handler_mean_dice.py
@@ -40,7 +40,7 @@ def _val_func(engine, batch):
         dice_metric.update([y_pred, y])
 
         y_pred = [torch.Tensor([[0], [1]]), torch.Tensor([[1], [0]])]
-        y = [torch.Tensor([[0], [1]]), torch.Tensor([[1], [0]])]
+        y = torch.Tensor([[[0], [1]], [[1], [0]]])
         dice_metric.update([y_pred, y])
 
         avg_dice = dice_metric.compute()
Repository: Project-MONAI/MONAI

Issue:
Enhance the log of SegmentationSaver
**Is your feature request related to a problem? Please describe.**
We need to print out more info when saving segmentation results, now it's only:
```
saved all the model outputs into files.
```



Discussion:

Resolving patch:
diff --git a/monai/data/nifti_saver.py b/monai/data/nifti_saver.py
--- a/monai/data/nifti_saver.py
+++ b/monai/data/nifti_saver.py
@@ -153,6 +153,7 @@ def save(self, data: Union[torch.Tensor, np.ndarray], meta_data: Optional[Dict]
             dtype=self.dtype,
             output_dtype=self.output_dtype,
         )
+        print(f"file written: {path}.")
 
     def save_batch(self, batch_data: Union[torch.Tensor, np.ndarray], meta_data: Optional[Dict] = None) -> None:
         """


Test patch:
Repository: Project-MONAI/MONAI

Issue:
persistent dataset default `cache_dir`
**Is your feature request related to a problem? Please describe.**
https://github.com/Project-MONAI/MONAI/blob/45c6cf6117d2e25e88272a7001d84696e449a1cc/monai/data/dataset.py#L136-L142
the default value of `cache_dir` is `None`, which means no caching. this is somewhat unexpected for a caching based dataset.

**Describe the solution you'd like**
remove the default value so that the user has to provide a location for the cache files



Discussion:

Resolving patch:
diff --git a/monai/data/dataset.py b/monai/data/dataset.py
--- a/monai/data/dataset.py
+++ b/monai/data/dataset.py
@@ -137,7 +137,7 @@ def __init__(
         self,
         data: Sequence,
         transform: Union[Sequence[Callable], Callable],
-        cache_dir: Optional[Union[Path, str]] = None,
+        cache_dir: Optional[Union[Path, str]],
         hash_func: Callable[..., bytes] = pickle_hashing,
     ) -> None:
         """
@@ -150,7 +150,8 @@ def __init__(
                 of pre-computed transformed data tensors. The cache_dir is computed once, and
                 persists on disk until explicitly removed.  Different runs, programs, experiments
                 may share a common cache dir provided that the transforms pre-processing is consistent.
-                If the cache_dir doesn't exist, will automatically create it.
+                If `cache_dir` doesn't exist, will automatically create it.
+                If `cache_dir` is `None`, there is effectively no caching.
             hash_func: a callable to compute hash from data items to be cached.
                 defaults to `monai.data.utils.pickle_hashing`.
 
@@ -271,7 +272,7 @@ def __init__(
         data: Sequence,
         transform: Union[Sequence[Callable], Callable],
         cache_n_trans: int,
-        cache_dir: Optional[Union[Path, str]] = None,
+        cache_dir: Optional[Union[Path, str]],
         hash_func: Callable[..., bytes] = pickle_hashing,
     ) -> None:
         """
@@ -285,7 +286,8 @@ def __init__(
                 of pre-computed transformed data tensors. The cache_dir is computed once, and
                 persists on disk until explicitly removed.  Different runs, programs, experiments
                 may share a common cache dir provided that the transforms pre-processing is consistent.
-                If the cache_dir doesn't exist, will automatically create it.
+                If `cache_dir` doesn't exist, will automatically create it.
+                If `cache_dir` is `None`, there is effectively no caching.
             hash_func: a callable to compute hash from data items to be cached.
                 defaults to `monai.data.utils.pickle_hashing`.
 


Test patch: