Repository: Project-MONAI/MONAI

Issue:
RandBiasField missing np.exp
**Describe the bug**
The bias field generated by `RandBiasField._generate_random_field` ([link](https://github.com/Project-MONAI/MONAI/blob/0ad9e73639e30f4f1af5a1f4a45da9cb09930179/monai/transforms/intensity/array.py#L434)) should be exponentiated, as done so in [NiftyNet's implementation](https://github.com/NifTK/NiftyNet/blob/935bf4334cd00fa9f9d50f6a95ddcbfdde4031e0/niftynet/layer/rand_bias_field.py#L99).

**To Reproduce**
```
import matplotlib.pyplot as plt
import numpy as np
from monai.transforms import RandBiasField

rand_bias_field = RandBiasField()
arr = np.ones((1,100,100))

bias_field = rand_bias_field(arr)

plt.figure(figsize=(12,6))
plt.subplot(121)
plt.imshow(bias_field[0])

plt.subplot(122)
plt.hist(bias_field.flatten())
plt.show()
```
![image](https://user-images.githubusercontent.com/20669652/126690750-2d55cbcc-42ee-44bf-828d-7b2738a5b2dd.png)

As shown on the histogram on the right, the values generated by the bias field are around 0 and can also be negative. This is not a proper bias field and multiplying an image by this bias field may invert values.

**Expected behavior**
The bias field should have values around 1. This is usually done by exponentiating the bias field since the bias field generated from the polynomial basis function is actually the log-transformed bias field (Refer to [last equation in section 3.1.1](https://www.sciencedirect.com/science/article/pii/S1361841517300257?via%3Dihub) and the NiftyNet implementation of RandBiasField (linked above). 

```
import matplotlib.pyplot as plt
import numpy as np
from monai.transforms import RandBiasField

rand_bias_field = RandBiasField()
arr = np.ones((1,100,100))

bias_field = np.exp(rand_bias_field(arr))

plt.figure(figsize=(12,6))
plt.subplot(121)
plt.imshow(bias_field[0])

plt.subplot(122)
plt.hist(bias_field.flatten())
plt.show()
```

![image](https://user-images.githubusercontent.com/20669652/126691848-dd9838a2-eb0f-4dca-8960-a13ef55113fc.png)

**Additional comments**
Are there any plans to make a batched Tensor-compatible implementation of RandBiasField so that this transform can theoretically be done on the GPU after batching? At the moment, the only critical numpy dependencies are the calls to `np.polynomial.legendre.leggrid2d` and `np.polynomial.legendre.leggrid3d`, but these can be easily implemented using PyTorch.


Discussion:

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
@@ -431,22 +431,19 @@ def __init__(
         self.coeff_range = coeff_range
         self.dtype = dtype
 
-    def _generate_random_field(
-        self,
-        spatial_shape: Tuple[int, ...],
-        rank: int,
-        degree: int,
-        coeff: Tuple[int, ...],
-    ):
+        self._coeff = [1.0]
+
+    def _generate_random_field(self, spatial_shape: Sequence[int], degree: int, coeff: Sequence[float]):
         """
         products of polynomials as bias field estimations
         """
+        rank = len(spatial_shape)
         coeff_mat = np.zeros((degree + 1,) * rank)
         coords = [np.linspace(-1.0, 1.0, dim, dtype=np.float32) for dim in spatial_shape]
         if rank == 2:
             coeff_mat[np.tril_indices(degree + 1)] = coeff
-            field = np.polynomial.legendre.leggrid2d(coords[0], coords[1], coeff_mat)
-        elif rank == 3:
+            return np.polynomial.legendre.leggrid2d(coords[0], coords[1], coeff_mat)
+        if rank == 3:
             pts: List[List[int]] = [[0, 0, 0]]
             for i in range(degree + 1):
                 for j in range(degree + 1 - i):
@@ -456,16 +453,12 @@ def _generate_random_field(
                 pts = pts[1:]
             np_pts = np.stack(pts)
             coeff_mat[np_pts[:, 0], np_pts[:, 1], np_pts[:, 2]] = coeff
-            field = np.polynomial.legendre.leggrid3d(coords[0], coords[1], coords[2], coeff_mat)
-        else:
-            raise NotImplementedError("only supports 2D or 3D fields")
-        return field
+            return np.polynomial.legendre.leggrid3d(coords[0], coords[1], coords[2], coeff_mat)
+        raise NotImplementedError("only supports 2D or 3D fields")
 
     def randomize(self, data: np.ndarray) -> None:
         super().randomize(None)
-        self.spatial_shape = data.shape[1:]
-        self.rank = len(self.spatial_shape)
-        n_coeff = int(np.prod([(self.degree + k) / k for k in range(1, self.rank + 1)]))
+        n_coeff = int(np.prod([(self.degree + k) / k for k in range(1, len(data.shape[1:]) + 1)]))
         self._coeff = self.R.uniform(*self.coeff_range, n_coeff).tolist()
 
     def __call__(self, img: np.ndarray):
@@ -475,17 +468,15 @@ def __call__(self, img: np.ndarray):
         self.randomize(data=img)
         if not self._do_transform:
             return img
-        num_channels = img.shape[0]
+        num_channels, *spatial_shape = img.shape
         _bias_fields = np.stack(
             [
-                self._generate_random_field(
-                    spatial_shape=self.spatial_shape, rank=self.rank, degree=self.degree, coeff=self._coeff
-                )
+                self._generate_random_field(spatial_shape=spatial_shape, degree=self.degree, coeff=self._coeff)
                 for _ in range(num_channels)
             ],
             axis=0,
         )
-        return (img * _bias_fields).astype(self.dtype)
+        return (img * np.exp(_bias_fields)).astype(self.dtype)
 
 
 class NormalizeIntensity(Transform):


Test patch:
diff --git a/tests/test_random_bias_field.py b/tests/test_random_bias_field.py
--- a/tests/test_random_bias_field.py
+++ b/tests/test_random_bias_field.py
@@ -18,17 +18,12 @@
 
 TEST_CASES_2D = [{}, (3, 32, 32)]
 TEST_CASES_3D = [{}, (3, 32, 32, 32)]
-TEST_CASES_2D_ZERO_RANGE = [{"coeff_range": (0.0, 0.0)}, (3, 32, 32)]
-TEST_CASES_2D_ONES = [{"coeff_range": (1.0, 1.0)}, np.asarray([[[2, -2], [2, 10]]])]
+TEST_CASES_2D_ZERO_RANGE = [{"coeff_range": (0.0, 0.0)}, (2, 3, 3)]
+TEST_CASES_2D_ONES = [{"coeff_range": (1.0, 1.0)}, np.asarray([[[7.389056, 0.1353353], [7.389056, 22026.46]]])]
 
 
 class TestRandBiasField(unittest.TestCase):
-    @parameterized.expand(
-        [
-            TEST_CASES_2D,
-            TEST_CASES_3D,
-        ]
-    )
+    @parameterized.expand([TEST_CASES_2D, TEST_CASES_3D])
     def test_output_shape(self, class_args, img_shape):
         for degree in [1, 2, 3]:
             bias_field = RandBiasField(degree=degree, **class_args)
@@ -44,16 +39,16 @@ def test_output_shape(self, class_args, img_shape):
     @parameterized.expand([TEST_CASES_2D_ZERO_RANGE])
     def test_zero_range(self, class_args, img_shape):
         bias_field = RandBiasField(**class_args)
-        img = np.random.rand(*img_shape)
+        img = np.ones(img_shape)
         output = bias_field(img)
-        np.testing.assert_equal(output, np.zeros(img_shape))
+        np.testing.assert_allclose(output, np.ones(img_shape), rtol=1e-3)
 
     @parameterized.expand([TEST_CASES_2D_ONES])
     def test_one_range_input(self, class_args, expected):
         bias_field = RandBiasField(**class_args)
         img = np.ones([1, 2, 2])
         output = bias_field(img)
-        np.testing.assert_equal(output, expected.astype(bias_field.dtype))
+        np.testing.assert_allclose(output, expected.astype(bias_field.dtype), rtol=1e-3)
 
     def test_zero_prob(self):
         bias_field = RandBiasField(prob=0.0)
diff --git a/tests/test_random_bias_fieldd.py b/tests/test_random_bias_fieldd.py
--- a/tests/test_random_bias_fieldd.py
+++ b/tests/test_random_bias_fieldd.py
@@ -19,16 +19,14 @@
 TEST_CASES_2D = [{}, (3, 32, 32)]
 TEST_CASES_3D = [{}, (3, 32, 32, 32)]
 TEST_CASES_2D_ZERO_RANGE = [{"coeff_range": (0.0, 0.0)}, (3, 32, 32)]
-TEST_CASES_2D_ONES = [{"coeff_range": (1.0, 1.0)}, np.asarray([[[2, -2], [2, 10]]])]
+TEST_CASES_2D_ONES = [
+    {"coeff_range": (1.0, 1.0)},
+    np.asarray([[[7.3890562e00, 1.3533528e-01], [7.3890562e00, 2.2026465e04]]]),
+]
 
 
 class TestRandBiasFieldd(unittest.TestCase):
-    @parameterized.expand(
-        [
-            TEST_CASES_2D,
-            TEST_CASES_3D,
-        ]
-    )
+    @parameterized.expand([TEST_CASES_2D, TEST_CASES_3D])
     def test_output_shape(self, class_args, img_shape):
         key = "img"
         bias_field = RandBiasFieldd(keys=[key], **class_args)
@@ -41,9 +39,9 @@ def test_output_shape(self, class_args, img_shape):
     def test_zero_range(self, class_args, img_shape):
         key = "img"
         bias_field = RandBiasFieldd(keys=[key], **class_args)
-        img = np.random.rand(*img_shape)
+        img = np.ones(img_shape)
         output = bias_field({key: img})
-        np.testing.assert_equal(output[key], np.zeros(img_shape))
+        np.testing.assert_allclose(output[key], np.ones(img_shape))
 
     @parameterized.expand([TEST_CASES_2D_ONES])
     def test_one_range_input(self, class_args, expected):
@@ -51,7 +49,7 @@ def test_one_range_input(self, class_args, expected):
         bias_field = RandBiasFieldd(keys=[key], **class_args)
         img = np.ones([1, 2, 2])
         output = bias_field({key: img})
-        np.testing.assert_equal(output[key], expected.astype(bias_field.rand_bias_field.dtype))
+        np.testing.assert_allclose(output[key], expected.astype(bias_field.rand_bias_field.dtype), rtol=1e-3)
 
     def test_zero_prob(self):
         key = "img"