Repository: fepegar/torchio

Issue:
AttributeError: 'NoneType' object has no attribute 'read' when converting to PIL
**🐛Bug**
<!-- A clear and concise description of what the bug is -->


**To reproduce**

<!-- Please provide a minimal working example: https://stackoverflow.com/help/minimal-reproducible-example -->

```python
In [1]: import torchio as tio

In [2]: import numpy as np

In [3]: x = 100 * np.random.rand(3, 50, 50, 1)

In [4]: x.as_pil()
```

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

```python-traceback
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-bf55f6468bbb> in <module>
----> 1 x.as_pil()

AttributeError: 'numpy.ndarray' object has no attribute 'as_pil'

In [5]: im = tio.ScalarImage(tensor=x)

In [6]: im.as_pil()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/usr/local/Caskroom/miniconda/base/envs/torchio/lib/python3.8/site-packages/PIL/Image.py in open(fp, mode)
   2881     try:
-> 2882         fp.seek(0)
   2883     except (AttributeError, io.UnsupportedOperation):

AttributeError: 'NoneType' object has no attribute 'seek'

During handling of the above exception, another exception occurred:

AttributeError                            Traceback (most recent call last)
<ipython-input-6-bc29f2434f0a> in <module>
----> 1 im.as_pil()

~/git/torchio/torchio/data/image.py in as_pil(self)
    466         """Get the image as an instance of :class:`PIL.Image`."""
    467         self.check_is_2d()
--> 468         return ImagePIL.open(self.path)
    469
    470     def get_center(self, lps: bool = False) -> TypeTripletFloat:

/usr/local/Caskroom/miniconda/base/envs/torchio/lib/python3.8/site-packages/PIL/Image.py in open(fp, mode)
   2882         fp.seek(0)
   2883     except (AttributeError, io.UnsupportedOperation):
-> 2884         fp = io.BytesIO(fp.read())
   2885         exclusive_fp = True
   2886
```


**Expected behavior**

<!-- A clear and concise description of what you expected to happen. -->

**Actual behavior**

<!-- A clear and concise description of what actually happens. -->


**TorchIO version**

<!-- Can be retrieved running: -->
<!-- $ python -c "import torchio; print(torchio.__version__)" -->
<!-- `major.minor.patch`, e.g. `0.5.3` -->


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/fepegar/torchio/pull/385?src=pr&el=h1) Report
> Merging [#385](https://codecov.io/gh/fepegar/torchio/pull/385?src=pr&el=desc) (be2dcda) into [master](https://codecov.io/gh/fepegar/torchio/commit/48e8a7d27b4f45f90f961cce0faeac5156931c20?el=desc) (48e8a7d) will **increase** coverage by `0.08%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/fepegar/torchio/pull/385/graphs/tree.svg?width=650&height=150&src=pr&token=TL9bitRrRZ)](https://codecov.io/gh/fepegar/torchio/pull/385?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #385      +/-   ##
==========================================
+ Coverage   92.83%   92.92%   +0.08%     
==========================================
  Files         116      116              
  Lines        5823     5840      +17     
==========================================
+ Hits         5406     5427      +21     
+ Misses        417      413       -4     
```


| [Impacted Files](https://codecov.io/gh/fepegar/torchio/pull/385?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/data/test\_image.py](https://codecov.io/gh/fepegar/torchio/pull/385/diff?src=pr&el=tree#diff-dGVzdHMvZGF0YS90ZXN0X2ltYWdlLnB5) | `100.00% <100.00%> (ø)` | |
| [torchio/data/image.py](https://codecov.io/gh/fepegar/torchio/pull/385/diff?src=pr&el=tree#diff-dG9yY2hpby9kYXRhL2ltYWdlLnB5) | `91.48% <100.00%> (+1.74%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/fepegar/torchio/pull/385?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/fepegar/torchio/pull/385?src=pr&el=footer). Last update [48e8a7d...be2dcda](https://codecov.io/gh/fepegar/torchio/pull/385?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/torchio/data/image.py b/torchio/data/image.py
index 0f63adf..bb81619 100644
--- a/torchio/data/image.py
+++ b/torchio/data/image.py
@@ -335,7 +335,7 @@ class Image(dict):
     def _parse_path(
             self,
             path: Union[TypePath, Sequence[TypePath]]
-            ) -> Union[Path, List[Path]]:
+            ) -> Optional[Union[Path, List[Path]]]:
         if path is None:
             return None
         if isinstance(path, (str, Path)):
@@ -452,9 +452,19 @@ class Image(dict):
         return nib_to_sitk(self.data, self.affine, **kwargs)
 
     def as_pil(self) -> ImagePIL:
-        """Get the image as an instance of :class:`PIL.Image`."""
+        """Get the image as an instance of :class:`PIL.Image`.
+
+        .. note:: Values will be clamped to 0-255 and cast to uint8.
+        """
         self.check_is_2d()
-        return ImagePIL.open(self.path)
+        tensor = self.data
+        if len(tensor) == 1:
+            tensor = torch.cat(3 * [tensor])
+        if len(tensor) != 3:
+            raise RuntimeError('The image must have 1 or 3 channels')
+        tensor = tensor.permute(3, 1, 2, 0)[0]
+        array = tensor.clamp(0, 255).numpy()
+        return ImagePIL.fromarray(array.astype(np.uint8))
 
     def get_center(self, lps: bool = False) -> TypeTripletFloat:
         """Get image center in RAS+ or LPS+ coordinates.


Test patch:
diff --git a/tests/data/test_image.py b/tests/data/test_image.py
index d18415f..4eb79a8 100644
--- a/tests/data/test_image.py
+++ b/tests/data/test_image.py
@@ -160,3 +160,17 @@ class TestImage(TorchioTestCase):
             with tempfile.NamedTemporaryFile(suffix='.nii') as f:
                 nib.save(img, f.name)
                 tio.ScalarImage(f.name).load()
+
+    def test_pil_3d(self):
+        with self.assertRaises(RuntimeError):
+            tio.ScalarImage(tensor=torch.rand(1, 2, 3, 4)).as_pil()
+
+    def test_pil_1(self):
+        tio.ScalarImage(tensor=torch.rand(1, 2, 3, 1)).as_pil()
+
+    def test_pil_2(self):
+        with self.assertRaises(RuntimeError):
+            tio.ScalarImage(tensor=torch.rand(2, 2, 3, 1)).as_pil()
+
+    def test_pil_3(self):
+        tio.ScalarImage(tensor=torch.rand(3, 2, 3, 1)).as_pil()