Repository: Project-MONAI/MONAI

Issue:
The Affine transform currently does not have an affine matrix parameter
**Is your feature request related to a problem? Please describe.**
The `Affine` transform forwards its parameters to the `AffineGrid` class.
But it currently does not provide the "affine" parameter which the `AffineGrid` class also has.

This allows to directly define a affine transformation matrix instead of individual parameters for transformations like rotation.
If computations are directly done with affine transformation matrices, this parameter is really helpful (necessary).

**Describe the solution you'd like**
The only change necessary, is to add the `affine` parameter and to forward it to the `AffineGrid` class.

```python
class Affine(Transform):
    ...
    def __init__(
        ...
        affine: Optional[NdarrayOrTensor] = None,
        ...
    ) -> None:
        ...
        self.affine_grid = AffineGrid(
            ...
            affine=affine,
            ...
        )
```

**Describe alternatives you've considered**
We patched the affine transform locally.


Discussion:

Resolving patch:
diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py
--- a/monai/transforms/spatial/dictionary.py
+++ b/monai/transforms/spatial/dictionary.py
@@ -605,6 +605,7 @@ def __init__(
         shear_params: Optional[Union[Sequence[float], float]] = None,
         translate_params: Optional[Union[Sequence[float], float]] = None,
         scale_params: Optional[Union[Sequence[float], float]] = None,
+        affine: Optional[NdarrayOrTensor] = None,
         spatial_size: Optional[Union[Sequence[int], int]] = None,
         mode: GridSampleModeSequence = GridSampleMode.BILINEAR,
         padding_mode: GridSamplePadModeSequence = GridSamplePadMode.REFLECTION,
@@ -631,6 +632,9 @@ def __init__(
                 pixel/voxel relative to the center of the input image. Defaults to no translation.
             scale_params: scale factor for every spatial dims. a tuple of 2 floats for 2D,
                 a tuple of 3 floats for 3D. Defaults to `1.0`.
+            affine: if applied, ignore the params (`rotate_params`, etc.) and use the
+                supplied matrix. Should be square with each side = num of image spatial
+                dimensions + 1.
             spatial_size: output image spatial size.
                 if `spatial_size` and `self.spatial_size` are not defined, or smaller than 1,
                 the transform will use the spatial size of `img`.
@@ -662,6 +666,7 @@ def __init__(
             shear_params=shear_params,
             translate_params=translate_params,
             scale_params=scale_params,
+            affine=affine,
             spatial_size=spatial_size,
             device=device,
         )


Test patch:
diff --git a/tests/test_affine.py b/tests/test_affine.py
--- a/tests/test_affine.py
+++ b/tests/test_affine.py
@@ -56,6 +56,17 @@
                 p(np.array([[[0.0, 0.0, 0.0, 0.0], [0.0, 2.0, 0.0, 0.0], [0.0, 3.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]]])),
             ]
         )
+        TESTS.append(
+            [
+                dict(
+                    affine=p(torch.tensor([[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])),
+                    padding_mode="zeros",
+                    device=device,
+                ),
+                {"img": p(np.arange(4).reshape((1, 2, 2))), "spatial_size": (4, 4)},
+                p(np.array([[[0.0, 0.0, 0.0, 0.0], [0.0, 2.0, 0.0, 0.0], [0.0, 3.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]]])),
+            ]
+        )
         TESTS.append(
             [
                 dict(padding_mode="zeros", device=device),
diff --git a/tests/test_affine_grid.py b/tests/test_affine_grid.py
--- a/tests/test_affine_grid.py
+++ b/tests/test_affine_grid.py
@@ -54,6 +54,35 @@
                 ),
             ]
         )
+        TESTS.append(
+            [
+                {
+                    "affine": p(
+                        torch.tensor(
+                            [[-10.8060, -8.4147, 0.0000], [-16.8294, 5.4030, 0.0000], [0.0000, 0.0000, 1.0000]]
+                        )
+                    )
+                },
+                {"grid": p(torch.ones((3, 3, 3)))},
+                p(
+                    torch.tensor(
+                        [
+                            [
+                                [-19.2208, -19.2208, -19.2208],
+                                [-19.2208, -19.2208, -19.2208],
+                                [-19.2208, -19.2208, -19.2208],
+                            ],
+                            [
+                                [-11.4264, -11.4264, -11.4264],
+                                [-11.4264, -11.4264, -11.4264],
+                                [-11.4264, -11.4264, -11.4264],
+                            ],
+                            [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]],
+                        ]
+                    )
+                ),
+            ]
+        )
         TESTS.append(
             [
                 {"rotate_params": (1.0, 1.0, 1.0), "scale_params": (-20, 10), "device": device},
@@ -99,6 +128,7 @@
             ]
         )
 
+
 _rtol = 5e-2 if is_tf32_env() else 1e-4
 
 
diff --git a/tests/test_affined.py b/tests/test_affined.py
--- a/tests/test_affined.py
+++ b/tests/test_affined.py
@@ -49,6 +49,19 @@
                 p(np.array([[[0.0, 0.0, 0.0, 0.0], [0.0, 2.0, 0.0, 0.0], [0.0, 3.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]]])),
             ]
         )
+        TESTS.append(
+            [
+                dict(
+                    keys="img",
+                    affine=p(torch.tensor([[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]])),
+                    padding_mode="zeros",
+                    spatial_size=(4, 4),
+                    device=device,
+                ),
+                {"img": p(np.arange(4).reshape((1, 2, 2)))},
+                p(np.array([[[0.0, 0.0, 0.0, 0.0], [0.0, 2.0, 0.0, 0.0], [0.0, 3.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0]]])),
+            ]
+        )
         TESTS.append(
             [
                 dict(keys="img", padding_mode="zeros", spatial_size=(-1, 0, 0), device=device),