Repository: Project-MONAI/MONAI

Issue:
Add ConcatItems transform
**Is your feature request related to a problem? Please describe.**
In order to combine several transform results together, need a `ConcatItems` transform.
For example, take 3 different intensity ranges from 1 image and then concat them together as a big 3-channels array.



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
@@ -18,6 +18,7 @@
 from logging import Handler
 from typing import Optional
 import copy
+import torch
 import numpy as np
 
 from monai.config.type_definitions import KeysCollection
@@ -363,6 +364,47 @@ def __call__(self, data):
         return d
 
 
+class ConcatItemsd(MapTransform):
+    """
+    Concatenate specified items from data dictionary together on the first dim to construct a big array.
+    Expect all the items are numpy array or PyTorch Tensor.
+
+    """
+
+    def __init__(self, keys: KeysCollection, name: str, dim: int = 0):
+        """
+        Args:
+            keys: keys of the corresponding items to be concatenated together.
+                See also: :py:class:`monai.transforms.compose.MapTransform`
+            name: the name coresponding to the key to store the concatenated data.
+            dim: on which dimension to concatenate the items, default is 0.
+
+        """
+        super().__init__(keys)
+        if len(self.keys) < 2:
+            raise ValueError("must provide must than 1 items to concat.")
+        self.name = name
+        self.dim = dim
+
+    def __call__(self, data):
+        d = dict(data)
+        output = list()
+        data_type = None
+        for key in self.keys:
+            if data_type is None:
+                data_type = type(d[key])
+            elif not isinstance(d[key], data_type):
+                raise TypeError("not all the items are with same data type.")
+            output.append(d[key])
+        if data_type == np.ndarray:
+            d[self.name] = np.concatenate(output, axis=self.dim)
+        elif data_type == torch.Tensor:
+            d[self.name] = torch.cat(output, dim=self.dim)
+        else:
+            raise TypeError(f"unsupported data type to concat: {data_type}.")
+        return d
+
+
 IdentityD = IdentityDict = Identityd
 AsChannelFirstD = AsChannelFirstDict = AsChannelFirstd
 AsChannelLastD = AsChannelLastDict = AsChannelLastd
@@ -375,3 +417,4 @@ def __call__(self, data):
 DataStatsD = DataStatsDict = DataStatsd
 SimulateDelayD = SimulateDelayDict = SimulateDelayd
 CopyItemsD = CopyItemsDict = CopyItemsd
+ConcatItemsD = ConcatItemsDict = ConcatItemsd


Test patch:
diff --git a/tests/test_concat_itemsd.py b/tests/test_concat_itemsd.py
new file mode 100644
--- /dev/null
+++ b/tests/test_concat_itemsd.py
@@ -0,0 +1,43 @@
+# Copyright 2020 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import time
+import sys
+import torch
+import numpy as np
+from monai.transforms import ConcatItemsd
+
+
+class TestConcatItemsd(unittest.TestCase):
+    def test_tensor_values(self):
+        device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu:0")
+        input_data = {
+            "img1": torch.tensor([[0, 1], [1, 2]], device=device),
+            "img2": torch.tensor([[0, 1], [1, 2]], device=device),
+        }
+        result = ConcatItemsd(keys=["img1", "img2"], name="cat_img")(input_data)
+        self.assertTrue("cat_img" in result)
+        result["cat_img"] += 1
+        torch.testing.assert_allclose(result["img1"], torch.tensor([[0, 1], [1, 2]], device=device))
+        torch.testing.assert_allclose(result["cat_img"], torch.tensor([[1, 2], [2, 3], [1, 2], [2, 3]], device=device))
+
+    def test_numpy_values(self):
+        input_data = {"img1": np.array([[0, 1], [1, 2]]), "img2": np.array([[0, 1], [1, 2]])}
+        result = ConcatItemsd(keys=["img1", "img2"], name="cat_img")(input_data)
+        self.assertTrue("cat_img" in result)
+        result["cat_img"] += 1
+        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]]))
+
+
+if __name__ == "__main__":
+    unittest.main()