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:

Patch:
diff --git a/pydicom/pixel_data_handlers/numpy_handler.py b/pydicom/pixel_data_handlers/numpy_handler.py
--- a/pydicom/pixel_data_handlers/numpy_handler.py
+++ b/pydicom/pixel_data_handlers/numpy_handler.py
@@ -43,7 +43,9 @@
 | (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
--- a/pydicom/tests/test_numpy_pixel_data.py
+++ b/pydicom/tests/test_numpy_pixel_data.py
@@ -26,6 +26,8 @@
 * PlanarConfiguration
 """
 
+from copy import deepcopy
+
 import pytest
 
 from pydicom import config
@@ -1068,6 +1070,7 @@ def test_endianness_not_set(self):
         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 @@ def test_no_pixel_data_raises(self):
         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)