Repository: pydicom/pydicom

Issue:
Pixel Representation attribute should be optional for pixel data handler
**Describe the bug**
The NumPy pixel data handler currently [requires the Pixel Representation attribute](https://github.com/pydicom/pydicom/blob/8da0b9b215ebfad5756051c891def88e426787e7/pydicom/pixel_data_handlers/numpy_handler.py#L46). This is problematic, because in case of Float Pixel Data or Double Float Pixel Data the attribute shall be absent. Compare [Floating Point Image Pixel Module Attributes](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.24.html) versus [Image Pixel Description Macro Attributes](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.3.html#table_C.7-11c)

**Expected behavior**
I would expect the `Dataset.pixel_array` property to be able to decode a Float Pixel Data or Double Float Pixel Data element without presence of the Pixel Representation element in the metadata.

**Steps To Reproduce**
```python
import numpy as np
from pydicom.dataset import Dataset, FileMetaDataset


ds = Dataset()
ds.file_meta = FileMetaDataset()
ds.file_meta.TransferSyntaxUID = '1.2.840.10008.1.2.1'

ds.BitsAllocated = 32
ds.SamplesPerPixel = 1
ds.Rows = 5
ds.Columns = 5
ds.PhotometricInterpretation = 'MONOCHROME2'

pixel_array = np.zeros((ds.Rows, ds.Columns), dtype=np.float32)
ds.FloatPixelData = pixel_array.flatten().tobytes()

np.array_equal(ds.pixel_array, pixel_array)
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1458?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
> Merging [#1458](https://codecov.io/gh/pydicom/pydicom/pull/1458?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (5fc497a) into [master](https://codecov.io/gh/pydicom/pydicom/commit/8da0b9b215ebfad5756051c891def88e426787e7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (8da0b9b) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/pydicom/pydicom/pull/1458/graphs/tree.svg?width=650&height=150&src=pr&token=YciLV15Lwr&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)](https://codecov.io/gh/pydicom/pydicom/pull/1458?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)

```diff
@@           Coverage Diff           @@
##           master    #1458   +/-   ##
=======================================
  Coverage   97.42%   97.42%           
=======================================
  Files          66       66           
  Lines       10185    10185           
=======================================
  Hits         9923     9923           
  Misses        262      262           
```


| [Impacted Files](https://codecov.io/gh/pydicom/pydicom/pull/1458?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) | Coverage Δ | |
|---|---|---|
| [pydicom/pixel\_data\_handlers/numpy\_handler.py](https://codecov.io/gh/pydicom/pydicom/pull/1458/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9waXhlbF9kYXRhX2hhbmRsZXJzL251bXB5X2hhbmRsZXIucHk=) | `100.00% <ø> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1458?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1458?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom). Last update [8da0b9b...5fc497a](https://codecov.io/gh/pydicom/pydicom/pull/1458?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).

hackermd: > Maybe add it to the required tags if PixelData is present (e.g. "PixelData" in keywords)? 

I added checks for the missing conditionally required attributes via d7c88b578bfbba2c90fc908551782f166fbe1a43.

> Also, can you adapt the release notes, please? This has already been there in the last release.

Could you kindly clarify which release note you are referring to? Would you mind adding a suggestion?

mrbean-bremen: > Could you kindly clarify which release note you are referring to? Would you mind adding a suggestion?

This would be the [release notes for version 2.2](https://github.com/pydicom/pydicom/blob/master/doc/release_notes/v2.2.0.rst), and you could add a line under fixes, something like:
```
- Fixed decoding for `Float Pixel Data` and `Double Float Pixel Data` in `pydicom.pixel_data_handlers.numpy_handler` (:issue:`1457`)
```
scaramallion: Could you add a couple of unit tests for the additions please?

Patch:
diff --git a/doc/release_notes/v2.2.0.rst b/doc/release_notes/v2.2.0.rst
index 0590e74d0..8275dd362 100644
--- a/doc/release_notes/v2.2.0.rst
+++ b/doc/release_notes/v2.2.0.rst
@@ -116,3 +116,5 @@ Fixes
 * Fixed handling of code extensions with person name component delimiter
   (:pr:`1449`)
 * Fixed bug decoding RBG jpg with APP14 marker due to change in Pillow (:pr:`1444`)
+* Fixed decoding for `FloatPixelData` and `DoubleFloatPixelData` via
+  `pydicom.pixel_data_handlers.numpy_handler` (:issue:`1457`)
diff --git a/pydicom/filereader.py b/pydicom/filereader.py
index 0e2d932d1..e44c9f0ce 100644
--- a/pydicom/filereader.py
+++ b/pydicom/filereader.py
@@ -24,6 +24,7 @@ from pydicom.dataelem import (
 from pydicom.dataset import Dataset, FileDataset, FileMetaDataset
 from pydicom.dicomdir import DicomDir
 from pydicom.errors import InvalidDicomError
+from pydicom.filebase import DicomFileLike
 from pydicom.fileutil import (
     read_undefined_length_value, path_from_pathlike, PathType, _unpack_tag
 )
@@ -901,7 +902,7 @@ def read_partial(
 
 
 def dcmread(
-    fp: Union[PathType, BinaryIO],
+    fp: Union[PathType, BinaryIO, DicomFileLike],
     defer_size: Optional[Union[str, int, float]] = None,
     stop_before_pixels: bool = False,
     force: bool = False,
diff --git a/pydicom/fileutil.py b/pydicom/fileutil.py
index 7bd548204..59df6b97b 100644
--- a/pydicom/fileutil.py
+++ b/pydicom/fileutil.py
@@ -7,6 +7,7 @@ from typing import Union, BinaryIO, Optional, Tuple, cast
 from pydicom.misc import size_in_bytes
 from pydicom.tag import TupleTag, Tag, SequenceDelimiterTag, ItemTag, BaseTag
 from pydicom.datadict import dictionary_description
+from pydicom.filebase import DicomFileLike
 
 from pydicom.config import logger
 
@@ -411,7 +412,7 @@ def length_of_undefined_length(
 
 
 def path_from_pathlike(
-    file_object: Union[PathType, BinaryIO]
+    file_object: Union[PathType, BinaryIO, DicomFileLike]
 ) -> Union[str, BinaryIO]:
     """Returns the path if `file_object` is a path-like object, otherwise the
     original `file_object`.
diff --git a/pydicom/pixel_data_handlers/numpy_handler.py b/pydicom/pixel_data_handlers/numpy_handler.py
index 8e8d319ae..6b25ab2cc 100644
--- a/pydicom/pixel_data_handlers/numpy_handler.py
+++ b/pydicom/pixel_data_handlers/numpy_handler.py
@@ -43,7 +43,9 @@ table below.
 | (0028,0100) | BitsAllocated             | 1    | 1, 8, 16, 32, | Required |
 |             |                           |      | 64            |          |
 +-------------+---------------------------+------+---------------+----------+
-| (0028,0103) | PixelRepresentation       | 1    | 0, 1          | Required |
+| (0028,0101) | BitsStored                | 1    | 1, 8, 12, 16  | Optional |
++-------------+---------------------------+------+---------------+----------+
+| (0028,0103) | PixelRepresentation       | 1C   | 0, 1          | Optional |
 +-------------+---------------------------+------+---------------+----------+
 
 """
@@ -284,16 +286,28 @@ def get_pixeldata(ds: "Dataset", read_only: bool = False) -> "np.ndarray":
             "the dataset"
         )
 
+    # Attributes required by both Floating Point Image Pixel Module Attributes
+    # and Image Pixel Description Macro Attributes
     required_elements = [
-        'BitsAllocated', 'Rows', 'Columns', 'PixelRepresentation',
+        'BitsAllocated', 'Rows', 'Columns',
         'SamplesPerPixel', 'PhotometricInterpretation'
     ]
+    if px_keyword[0] == 'PixelData':
+        # Attributess required by Image Pixel Description Macro Attributes
+        required_elements.extend(['PixelRepresentation', 'BitsStored'])
     missing = [elem for elem in required_elements if elem not in ds]
     if missing:
         raise AttributeError(
             "Unable to convert the pixel data as the following required "
             "elements are missing from the dataset: " + ", ".join(missing)
         )
+    if ds.SamplesPerPixel > 1:
+        if not hasattr(ds, 'PlanarConfiguration'):
+            raise AttributeError(
+                "Unable to convert the pixel data as the following "
+                "conditionally required element is missing from the dataset: "
+                "PlanarConfiguration"
+            )
 
     # May be Pixel Data, Float Pixel Data or Double Float Pixel Data
     pixel_data = getattr(ds, px_keyword[0])


Test patch:
diff --git a/pydicom/tests/test_numpy_pixel_data.py b/pydicom/tests/test_numpy_pixel_data.py
index 5aab4a4af..3a86f7d68 100644
--- a/pydicom/tests/test_numpy_pixel_data.py
+++ b/pydicom/tests/test_numpy_pixel_data.py
@@ -26,6 +26,8 @@ There are the following possibilities:
 * PlanarConfiguration
 """
 
+from copy import deepcopy
+
 import pytest
 
 from pydicom import config
@@ -1068,6 +1070,7 @@ class TestNumpy_NumpyHandler:
         ds.Rows = 10
         ds.Columns = 10
         ds.BitsAllocated = 16
+        ds.BitsStored = 16
         ds.PixelRepresentation = 0
         ds.SamplesPerPixel = 1
         ds.PhotometricInterpretation = 'MONOCHROME2'
@@ -1105,16 +1108,60 @@ class TestNumpy_GetPixelData:
         with pytest.raises(AttributeError, match=msg):
             get_pixeldata(ds)
 
-    def test_missing_required_elem(self):
+    def test_missing_required_elem_pixel_data_monochrome(self):
         """Tet get_pixeldata raises if dataset missing required element."""
-        ds = dcmread(EXPL_16_1_1F)
-        del ds.BitsAllocated
+        required_attrs = (
+            'BitsAllocated',
+            'BitsStored',
+            'Rows',
+            'Columns',
+            'SamplesPerPixel',
+            'PhotometricInterpretation',
+            'PixelRepresentation',
+        )
+        for attr in required_attrs:
+            ds = dcmread(EXPL_16_1_1F)
+            delattr(ds, attr)
+            msg = (
+                r"Unable to convert the pixel data as the following required "
+                r"elements are missing from the dataset: {}".format(attr)
+            )
+            with pytest.raises(AttributeError, match=msg):
+                get_pixeldata(ds)
+
+    def test_missing_required_elem_pixel_data_color(self):
+        """Tet get_pixeldata raises if dataset missing required element."""
+        ds = dcmread(EXPL_8_3_1F)
+        del ds.Rows
+        del ds.Columns
+        msg = (
+            r"Unable to convert the pixel data as the following required "
+            r"elements are missing from the dataset: Rows, Columns"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            get_pixeldata(ds)
+
+    def test_missing_conditionally_required_elem_pixel_data_color(self):
+        """Tet get_pixeldata raises if dataset missing required element."""
+        ds = dcmread(EXPL_8_3_1F)
+        del ds.PlanarConfiguration
+        msg = (
+            r"Unable to convert the pixel data as the following conditionally "
+            r"required element is missing from the dataset: "
+            r"PlanarConfiguration"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            get_pixeldata(ds)
+
+    def test_missing_required_elem_float_pixel_data_monochrome(self):
+        """Tet get_pixeldata raises if dataset missing required element."""
+        ds = dcmread(IMPL_32_1_1F)
+        ds.FloatPixelData = ds.PixelData
+        del ds.PixelData
         del ds.Rows
-        del ds.SamplesPerPixel
         msg = (
             r"Unable to convert the pixel data as the following required "
-            r"elements are missing from the dataset: BitsAllocated, Rows, "
-            r"SamplesPerPixel"
+            r"elements are missing from the dataset: Rows"
         )
         with pytest.raises(AttributeError, match=msg):
             get_pixeldata(ds)