Repository: pydicom/pydicom

Issue:
Memory leaks when accessing sequence tags with Dataset.__getattr__.
**Describe the bug**
Accessing sequences via `Dataset.__getattr__` seems to leak memory. The bug occurred for me when I was processing many DICOMs and manipulating some tags contained in sequences and each leaked a bit of memory, ultimately crashing the process.

**Expected behavior**
Memory should not leak. It works correctly when you replace the `__getattr__` call with `__getitem__` (by manually constructing the necessary tag beforehand).

Without being an expert in the codebase, one difference I think that could explain it is that `__getattr__` sets `value.parent = self` for sequences while `__getitem__` doesn't seem to do that. Maybe this loop of references somehow confuses Python's garbage collection?

**Steps To Reproduce**
This increases the memory consumption of the Python process by about 700 MB on my machine. The DICOM file I've tested it with is 27MB and has one item in `SourceImageSequence`. Note that the memory leak plateaus after a while in this example, maybe because it's the same file. In my actual workflow when iterating over many different files, the process filled all memory and crashed.

```python
import pydicom
for i in range(100):
  dcm = pydicom.dcmread("my_dicom.dcm")
  test = dcm.SourceImageSequence
```

For comparison, this keeps the memory constant. `(0x0008, 0x2112)` is `SourceImageSequence`: 

```python
import pydicom
import pydicom.tag
for i in range(100):
  dcm = pydicom.dcmread("my_dicom.dcm")
  test = dcm[pydicom.tag.TupleTag((0x0008, 0x2112))]
```

**Your environment**

```bash
Linux-4.15.0-72-generic-x86_64-with-Ubuntu-18.04-bionic
Python  3.6.8 (default, Jan 14 2019, 11:02:34)
pydicom  1.3.0
```


Discussion:

Patch:
diff --git a/doc/old/working_with_pixel_data.rst b/doc/old/working_with_pixel_data.rst
index 9d948d1fe..09d379b6e 100644
--- a/doc/old/working_with_pixel_data.rst
+++ b/doc/old/working_with_pixel_data.rst
@@ -11,8 +11,23 @@ Working with Pixel Data
 Introduction
 ------------
 
-pydicom tends to be "lazy" in interpreting DICOM data. For example, by default
-it doesn't do anything with pixel data except read in the raw bytes::
+Many DICOM SOP classes contain bulk pixel data, which is usually used to
+represent one or more image frames (although :dcm:`other types of data
+<part03/sect_A.18.3.html>` are possible). In these SOP classes the pixel
+data is (almost) always contained in the (7fe0,0010) *Pixel Data* element.
+The only exception to this is :dcm:`Parametric Map Storage
+<part03/sect_A.75.3.html>` which may instead contain data in the (7fe0,0008)
+*Float Pixel Data* or (7fe0,0009) *Double Float Pixel Data* elements.
+
+.. note::
+
+    In the following the term *pixel data* will be used to refer to
+    the bulk data from *Pixel Data*, *Float Pixel Data* and *Double Float
+    Pixel Data* elements. While the examples use ``PixelData``,
+    ``FloatPixelData`` or ``DoubleFloatPixelData`` could also be used
+    interchangeably provided the dataset contains the corresponding element.
+
+By default *pydicom* reads in pixel data as the raw bytes found in the file::
 
   >>> from pydicom import dcmread
   >>> from pydicom.data import get_testdata_file
@@ -21,11 +36,10 @@ it doesn't do anything with pixel data except read in the raw bytes::
   >>> ds.PixelData # doctest: +ELLIPSIS
   b'\x89\x03\xfb\x03\xcb\x04\xeb\x04\xf9\x02\x94\x01\x7f...
 
-``PixelData`` contains the ``bytes`` exactly as found in the file and is not
-typically in an immediately useful form as data may be stored in a variety
-of different ways:
+``PixelData`` is often not immediately useful as data may be
+stored in a variety of different ways:
 
- - The pixel values may be signed or unsigned
+ - The pixel values may be signed or unsigned integers, or floats
  - There may be multiple image frames
  - There may be :dcm:`multiple planes per frame
    <part03/sect_C.7.6.3.html#sect_C.7.6.3.1.1>` (i.e. RGB) and the :dcm:`order
@@ -36,8 +50,8 @@ of different ways:
    be :dcm:`encapsulated<part05/sect_A.4.html>` and each encapsulated image
    frame may be broken up into one or more fragments.
 
-Because of the complexity in getting the raw *Pixel Data* bytes into
-a usable form, *pydicom* provides a convenient way to get it:
+Because of the complexity in interpreting the pixel data, *pydicom* provides
+an easy way to get it in a convenient form:
 :attr:`Dataset.pixel_array<pydicom.dataset.Dataset.pixel_array>`.
 
 
@@ -50,7 +64,7 @@ a usable form, *pydicom* provides a convenient way to get it:
     requires `NumPy <http://numpy.org/>`_.
 
 :attr:`Dataset.pixel_array<pydicom.dataset.Dataset.pixel_array>` returns a
-:class:`numpy.ndarray` containing the *Pixel Data*::
+:class:`numpy.ndarray` containing the pixel data::
 
   >>> ds.pixel_array # doctest: +NORMALIZE_WHITESPACE
   array([[ 905, 1019, 1227, ...,  302,  304,  328],
@@ -63,14 +77,21 @@ a usable form, *pydicom* provides a convenient way to get it:
   >>> ds.pixel_array.shape
   (64, 64)
 
-If the *Pixel Data* is compressed then
+If the pixel data is compressed then
 :attr:`~pydicom.dataset.Dataset.pixel_array` will return the uncompressed data,
-provided the dependencies of the required pixel data handler are available. See
+provided the dependencies of the required :ref:`pixel data handler
+<api_handlers_pixeldata>` are available. See
 :doc:`handling compressed image data <image_data_handlers>` for more
 information.
 
-NumPy can be used to modify the pixel data, but if the changes are to be saved,
-they must be written back to the dataset's ``PixelData`` element:
+NumPy can be used to modify the data, but if the changes are to be saved,
+they must be written back to the dataset's ``PixelData`` element.
+
+.. warning::
+
+    Converting data from an ``ndarray`` back to ``bytes`` may not be
+    as straightforward as in the following example, particularly for
+    multi-planar images or where compression is required.
 
 .. code-block:: python
 
@@ -81,9 +102,9 @@ they must be written back to the dataset's ``PixelData`` element:
   ds.save_as("temp.dcm")
 
 Some changes may require other DICOM tags to be modified. For example, if the
-pixel data is reduced (e.g. a :math:`512 \times 512` image is collapsed to
+image size is reduced (e.g. a :math:`512 \times 512` image is shrunk to
 :math:`256 \times 256`) then ``Rows`` and ``Columns`` should be set
-appropriately. You must explicitly set these yourself; pydicom does not do so
+appropriately. You must explicitly set these yourself; *pydicom* does not do so
 automatically.
 
 See :ref:`sphx_glr_auto_examples_image_processing_plot_downsize_image.py` for
@@ -191,7 +212,7 @@ return values with applied VOI LUT or windowing. When a dataset contains
 multiple VOI or windowing views then a particular view can be returned by
 using the `index` keyword parameter.
 
-When a dataset requires multiple greyscale transformations, then its assumed
+When a dataset requires multiple grayscale transformations, then it's assumed
 that the modality LUT or rescale operation has already been applied.
 
 .. code-block:: python
diff --git a/doc/release_notes/v1.4.0.rst b/doc/release_notes/v1.4.0.rst
index 93b254055..9eae3372a 100644
--- a/doc/release_notes/v1.4.0.rst
+++ b/doc/release_notes/v1.4.0.rst
@@ -20,11 +20,12 @@ Fixes
   Descriptor* elements (0028,1101-1103) and (0028,3002) (:issue:`942`)
 * Fixed handling of ISO IR 159 encoding (:issue: `917`)
 * Fixed propagation of bulk data handler in Dataset.from_json (:issue:`971`)
-* Correctly handle  DICOMDIR files with records in reverse-hierarchical order
+* Correctly handle DICOMDIR files with records in reverse-hierarchical order
   (:issue:`822`)
 * *Pixel Data* encoded using JPEG2000 and decoded using the Pillow handler no
   longer returns RGB data when the (0028,0004) *Photometric Interpretation* is
   YBR_FULL or YBR_FULL_422. (:issue:`263`, :issue:`273`, :issue:`826`)
+* Avoid possible high memory usage while reading sequences (:issue:`994`)
 
 Enhancements
 ............
@@ -52,3 +53,6 @@ Enhancements
   particular handler, rather than following pydicom's default order (:issue:`537`)
 * Added :func:`~pydicom.pixel_data_handlers.util.apply_voi_lut` function for
   applying VOI LUTs or windowing operations.
+* Added support for (7fe0,0008) *Float Pixel Data* and (7fe0,0009) *Double
+  Float Pixel Data* to :attr:`~pydicom.dataset.Dataset.pixel_array`
+  (:issue:`452`)
diff --git a/pydicom/dataset.py b/pydicom/dataset.py
index 3312902fa..0e10642de 100644
--- a/pydicom/dataset.py
+++ b/pydicom/dataset.py
@@ -769,13 +769,7 @@ class Dataset(dict):
         if tag is not None:  # `name` isn't a DICOM element keyword
             tag = Tag(tag)
             if tag in self._dict:  # DICOM DataElement not in the Dataset
-                data_elem = self[tag]
-                value = data_elem.value
-                if data_elem.VR == 'SQ':
-                    # let a sequence know its parent dataset, as sequence items
-                    # may need parent dataset tags to resolve ambiguous tags
-                    value.parent = self
-                return value
+                return self[tag].value
 
         # no tag or tag not contained in the dataset
         if name == '_dict':
@@ -854,6 +848,10 @@ class Dataset(dict):
         data_elem = self._dict[tag]
 
         if isinstance(data_elem, DataElement):
+            if data_elem.VR == 'SQ' and data_elem.value:
+                # let a sequence know its parent dataset, as sequence items
+                # may need parent dataset tags to resolve ambiguous tags
+                data_elem.value.parent = self
             return data_elem
         elif isinstance(data_elem, tuple):
             # If a deferred read, then go get the value now
@@ -1269,8 +1267,7 @@ class Dataset(dict):
         return default
 
     def convert_pixel_data(self, handler_name=''):
-        """Convert the (7fe0,0010) *Pixel Data* to a :class:`numpy.ndarray`
-        internally.
+        """Convert pixel data to a :class:`numpy.ndarray` internally.
 
         Parameters
         ----------
@@ -1289,7 +1286,7 @@ class Dataset(dict):
         Raises
         ------
         ValueError
-            If `name` is not a valid handler name.
+            If `handler_name` is not a valid handler name.
         NotImplementedError
             If the given handler or any handler, if none given, is able to
             decompress pixel data with the current transfer syntax
@@ -1304,10 +1301,19 @@ class Dataset(dict):
         """
         # Check if already have converted to a NumPy array
         # Also check if self.PixelData has changed. If so, get new NumPy array
+        keywords = ['PixelData', 'FloatPixelData', 'DoubleFloatPixelData']
+        px_keyword = [kw for kw in keywords if kw in self]
+        if len(px_keyword) != 1:
+            raise AttributeError(
+                "Unable to convert the pixel data: one of Pixel Data, Float "
+                "Pixel Data or Double Float Pixel Data must be present in "
+                "the dataset"
+            )
+
         already_have = True
         if not hasattr(self, "_pixel_array"):
             already_have = False
-        elif self._pixel_id != id(self.PixelData):
+        elif self._pixel_id != id(getattr(self, px_keyword[0])):
             already_have = False
 
         if already_have:
@@ -1432,17 +1438,20 @@ class Dataset(dict):
         """Do the actual data conversion using the given handler."""
 
         # Use the handler to get a 1D numpy array of the pixel data
+        # Will raise an exception if no pixel data element
         arr = handler.get_pixeldata(self)
         self._pixel_array = reshape_pixel_array(self, arr)
 
         # Some handler/transfer syntax combinations may need to
         #   convert the color space from YCbCr to RGB
         if handler.needs_to_convert_to_RGB(self):
-            self._pixel_array = convert_color_space(self._pixel_array,
-                                                    'YBR_FULL',
-                                                    'RGB')
+            self._pixel_array = convert_color_space(
+                self._pixel_array, 'YBR_FULL', 'RGB'
+            )
 
-        self._pixel_id = id(self.PixelData)
+        keywords = ['PixelData', 'FloatPixelData', 'DoubleFloatPixelData']
+        px_keyword = [kw for kw in keywords if kw in self]
+        self._pixel_id = id(getattr(self, px_keyword[0]))
 
     def decompress(self, handler_name=''):
         """Decompresses *Pixel Data* and modifies the :class:`Dataset`
@@ -1583,12 +1592,14 @@ class Dataset(dict):
 
     @property
     def pixel_array(self):
-        """Return the *Pixel Data* as a :class:`numpy.ndarray`.
+        """Return the pixel data as a :class:`numpy.ndarray`.
 
         Returns
         -------
         numpy.ndarray
-            The (7fe0,0010) *Pixel Data* converted to a :class:`numpy.ndarray`.
+            The (7fe0,0008) *Float Pixel Data*, (7fe0,0009) *Double Float
+            Pixel Data* or (7fe0,0010) *Pixel Data* converted to a
+            :class:`numpy.ndarray`.
         """
         self.convert_pixel_data()
         return self._pixel_array
diff --git a/pydicom/filewriter.py b/pydicom/filewriter.py
index a8c7982c3..9d3b6e399 100644
--- a/pydicom/filewriter.py
+++ b/pydicom/filewriter.py
@@ -70,8 +70,8 @@ def _correct_ambiguous_vr_element(elem, ds, is_little_endian):
         #   For references, see the list at
         #   https://github.com/darcymason/pydicom/pull/298
         # PixelRepresentation is usually set in the root dataset
-        while 'PixelRepresentation' not in ds and ds.parent:
-            ds = ds.parent
+        while 'PixelRepresentation' not in ds and ds.parent and ds.parent():
+            ds = ds.parent()
         # if no pixel data is present, none if these tags is used,
         # so we can just ignore a missing PixelRepresentation in this case
         if ('PixelRepresentation' not in ds and 'PixelData' not in ds or
diff --git a/pydicom/pixel_data_handlers/numpy_handler.py b/pydicom/pixel_data_handlers/numpy_handler.py
index 7b550d72f..af928026d 100644
--- a/pydicom/pixel_data_handlers/numpy_handler.py
+++ b/pydicom/pixel_data_handlers/numpy_handler.py
@@ -1,6 +1,6 @@
 # Copyright 2008-2018 pydicom authors. See LICENSE file for details.
-"""Use the `numpy <https://numpy.org/>`_ package to convert supported *Pixel
-Data* to a :class:`numpy.ndarray`.
+"""Use the `numpy <https://numpy.org/>`_ package to convert supported pixel
+data to a :class:`numpy.ndarray`.
 
 **Supported transfer syntaxes**
 
@@ -11,36 +11,40 @@ Data* to a :class:`numpy.ndarray`.
 
 **Supported data**
 
-The numpy handler supports the conversion of data in the (7fe0,0010)
-*Pixel Data* element to a :class:`~numpy.ndarray` provided the
-related :dcm:`Image Pixel<part03/sect_C.7.6.3.html>` module elements have
-values given in the table below.
-
-+------------------------------------------------+--------------+----------+
-| Element                                        | Supported    |          |
-+-------------+---------------------------+------+ values       |          |
-| Tag         | Keyword                   | Type |              |          |
-+=============+===========================+======+==============+==========+
-| (0028,0002) | SamplesPerPixel           | 1    | N            | Required |
-+-------------+---------------------------+------+--------------+----------+
-| (0028,0004) | PhotometricInterpretation | 1    | MONOCHROME1  | Required |
-|             |                           |      | MONOCHROME2  |          |
-|             |                           |      | RGB          |          |
-|             |                           |      | YBR_FULL     |          |
-|             |                           |      | YBR_FULL_422 |          |
-+-------------+---------------------------+------+--------------+----------+
-| (0028,0006) | PlanarConfiguration       | 1C   | 0, 1         | Optional |
-+-------------+---------------------------+------+--------------+----------+
-| (0028,0008) | NumberOfFrames            | 1C   | N            | Optional |
-+-------------+---------------------------+------+--------------+----------+
-| (0028,0010) | Rows                      | 1    | N            | Required |
-+-------------+---------------------------+------+--------------+----------+
-| (0028,0011) | Columns                   | 1    | N            | Required |
-+-------------+---------------------------+------+--------------+----------+
-| (0028,0100) | BitsAllocated             | 1    | 1, 8, 16, 32 | Required |
-+-------------+---------------------------+------+--------------+----------+
-| (0028,0103) | PixelRepresentation       | 1    | 0, 1         | Required |
-+-------------+---------------------------+------+--------------+----------+
+The numpy handler supports the conversion of data in the (7fe0,0008) *Float
+Pixel Data*, (7fe0,0009) *Double Float Pixel Data* and (7fe0,0010)
+*Pixel Data* elements to a :class:`~numpy.ndarray` provided the
+related :dcm:`Image Pixel<part03/sect_C.7.6.3.html>`, :dcm:`Floating Point
+Image Pixel<part03/sect_C.7.6.24.html>` or  :dcm:`Double Floating Point Image
+Pixel<part03/sect_C.7.6.25.html>` module elements have values given in the
+table below.
+
++------------------------------------------------+---------------+----------+
+| Element                                        | Supported     |          |
++-------------+---------------------------+------+ values        |          |
+| Tag         | Keyword                   | Type |               |          |
++=============+===========================+======+===============+==========+
+| (0028,0002) | SamplesPerPixel           | 1    | N             | Required |
++-------------+---------------------------+------+---------------+----------+
+| (0028,0004) | PhotometricInterpretation | 1    | MONOCHROME1,  | Required |
+|             |                           |      | MONOCHROME2,  |          |
+|             |                           |      | RGB,          |          |
+|             |                           |      | YBR_FULL,     |          |
+|             |                           |      | YBR_FULL_422  |          |
++-------------+---------------------------+------+---------------+----------+
+| (0028,0006) | PlanarConfiguration       | 1C   | 0, 1          | Optional |
++-------------+---------------------------+------+---------------+----------+
+| (0028,0008) | NumberOfFrames            | 1C   | N             | Optional |
++-------------+---------------------------+------+---------------+----------+
+| (0028,0010) | Rows                      | 1    | N             | Required |
++-------------+---------------------------+------+---------------+----------+
+| (0028,0011) | Columns                   | 1    | N             | Required |
++-------------+---------------------------+------+---------------+----------+
+| (0028,0100) | BitsAllocated             | 1    | 1, 8, 16, 32, | Required |
+|             |                           |      | 64            |          |
++-------------+---------------------------+------+---------------+----------+
+| (0028,0103) | PixelRepresentation       | 1    | 0, 1          | Required |
++-------------+---------------------------+------+---------------+----------+
 
 """
 
@@ -199,14 +203,16 @@ def unpack_bits(bytestream):
 
 
 def get_pixeldata(ds, read_only=False):
-    """Return a :class:`numpy.ndarray` of the *Pixel Data*.
+    """Return a :class:`numpy.ndarray` of the pixel data.
 
     Parameters
     ----------
     ds : Dataset
-        The :class:`Dataset` containing an Image Pixel module and the
-        *Pixel Data* to be converted. If (0028,0004) *Photometric
-        Interpretation* is `'YBR_FULL_422'` then the pixel data will be
+        The :class:`Dataset` containing an Image Pixel, Floating Point Image
+        Pixel or Double Floating Point Image Pixel module and the
+        *Pixel Data*, *Float Pixel Data* or *Double Float Pixel Data* to be
+        converted. If (0028,0004) *Photometric Interpretation* is
+        `'YBR_FULL_422'` then the pixel data will be
         resampled to 3 channel data as per Part 3, :dcm:`Annex C.7.6.3.1.2
         <part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2>` of the DICOM Standard.
     read_only : bool, optional
@@ -219,7 +225,8 @@ def get_pixeldata(ds, read_only=False):
     Returns
     -------
     np.ndarray
-        The contents of (7FE0,0010) *Pixel Data* as a 1D array.
+        The contents of (7FE0,0010) *Pixel Data*, (7FE0,0008) *Float Pixel
+        Data* or (7FE0,0009) *Double Float Pixel Data* as a 1D array.
 
     Raises
     ------
@@ -240,8 +247,17 @@ def get_pixeldata(ds, read_only=False):
         )
 
     # Check required elements
+    keywords = ['PixelData', 'FloatPixelData', 'DoubleFloatPixelData']
+    px_keyword = [kw for kw in keywords if kw in ds]
+    if len(px_keyword) != 1:
+        raise AttributeError(
+            "Unable to convert the pixel data: one of Pixel Data, Float "
+            "Pixel Data or Double Float Pixel Data must be present in "
+            "the dataset"
+        )
+
     required_elements = [
-        'PixelData', 'BitsAllocated', 'Rows', 'Columns', 'PixelRepresentation',
+        'BitsAllocated', 'Rows', 'Columns', 'PixelRepresentation',
         'SamplesPerPixel', 'PhotometricInterpretation'
     ]
     missing = [elem for elem in required_elements if elem not in ds]
@@ -251,12 +267,15 @@ def get_pixeldata(ds, read_only=False):
             "elements are missing from the dataset: " + ", ".join(missing)
         )
 
+    # May be Pixel Data, Float Pixel Data or Double Float Pixel Data
+    pixel_data = getattr(ds, px_keyword[0])
+
     # Calculate the expected length of the pixel data (in bytes)
     #   Note: this does NOT include the trailing null byte for odd length data
     expected_len = get_expected_length(ds)
 
     # Check that the actual length of the pixel data is as expected
-    actual_length = len(ds.PixelData)
+    actual_length = len(pixel_data)
 
     # Correct for the trailing NULL byte padding for odd length data
     padded_expected_len = expected_len + expected_len % 2
@@ -301,15 +320,15 @@ def get_pixeldata(ds, read_only=False):
     if ds.BitsAllocated == 1:
         # Skip any trailing padding bits
         nr_pixels = get_expected_length(ds, unit='pixels')
-        arr = unpack_bits(ds.PixelData)[:nr_pixels]
+        arr = unpack_bits(pixel_data)[:nr_pixels]
     else:
         # Skip the trailing padding byte(s) if present
-        data = ds.PixelData[:expected_len]
-        arr = np.frombuffer(data, dtype=pixel_dtype(ds))
+        dtype = pixel_dtype(ds, as_float=('Float' in px_keyword[0]))
+        arr = np.frombuffer(pixel_data[:expected_len], dtype=dtype)
         if ds.PhotometricInterpretation == 'YBR_FULL_422':
             # PS3.3 C.7.6.3.1.2: YBR_FULL_422 data needs to be resampled
             # Y1 Y2 B1 R1 -> Y1 B1 R1 Y2 B1 R1
-            out = np.zeros(expected_len // 2 * 3, dtype=pixel_dtype(ds))
+            out = np.zeros(expected_len // 2 * 3, dtype=dtype)
             out[::6] = arr[::4]  # Y1
             out[3::6] = arr[1::4]  # Y2
             out[1::6], out[4::6] = arr[2::4], arr[2::4]  # B
diff --git a/pydicom/pixel_data_handlers/util.py b/pydicom/pixel_data_handlers/util.py
index 3f680334e..a03a90dcf 100644
--- a/pydicom/pixel_data_handlers/util.py
+++ b/pydicom/pixel_data_handlers/util.py
@@ -750,31 +750,37 @@ def get_expected_length(ds, unit='bytes'):
     return length
 
 
-def pixel_dtype(ds):
-    """Return a :class:`numpy.dtype` for the *Pixel Data* in `ds`.
-
-    Suitable for use with IODs containing the Image Pixel module.
-
-    +------------------------------------------+--------------+
-    | Element                                  | Supported    |
-    +-------------+---------------------+------+ values       |
-    | Tag         | Keyword             | Type |              |
-    +=============+=====================+======+==============+
-    | (0028,0101) | BitsAllocated       | 1    | 1, 8, 16, 32 |
-    +-------------+---------------------+------+--------------+
-    | (0028,0103) | PixelRepresentation | 1    | 0, 1         |
-    +-------------+---------------------+------+--------------+
+def pixel_dtype(ds, as_float=False):
+    """Return a :class:`numpy.dtype` for the pixel data in `ds`.
+
+    Suitable for use with IODs containing the Image Pixel module (with
+    ``as_float=False``) and the Floating Point Image Pixel and Double Floating
+    Point Image Pixel modules (with ``as_float=True``).
+
+    +------------------------------------------+------------------+
+    | Element                                  | Supported        |
+    +-------------+---------------------+------+ values           |
+    | Tag         | Keyword             | Type |                  |
+    +=============+=====================+======+==================+
+    | (0028,0101) | BitsAllocated       | 1    | 1, 8, 16, 32, 64 |
+    +-------------+---------------------+------+------------------+
+    | (0028,0103) | PixelRepresentation | 1    | 0, 1             |
+    +-------------+---------------------+------+------------------+
 
     Parameters
     ----------
     ds : Dataset
-        The :class:`~pydicom.dataset.Dataset` containing the *Pixel Data* you
+        The :class:`~pydicom.dataset.Dataset` containing the pixel data you
         wish to get the data type for.
+    as_float : bool, optional
+        If ``True`` then return a float dtype, otherwise return an integer
+        dtype (default ``False``). Float dtypes are only supported when
+        (0028,0101) *Bits Allocated* is 32 or 64.
 
     Returns
     -------
     numpy.dtype
-        A :class:`numpy.dtype` suitable for containing the *Pixel Data*.
+        A :class:`numpy.dtype` suitable for containing the pixel data.
 
     Raises
     ------
@@ -788,21 +794,24 @@ def pixel_dtype(ds):
     if ds.is_little_endian is None:
         ds.is_little_endian = ds.file_meta.TransferSyntaxUID.is_little_endian
 
-    # (0028,0103) Pixel Representation, US, 1
-    #   Data representation of the pixel samples
-    #   0x0000 - unsigned int
-    #   0x0001 - 2's complement (signed int)
-    pixel_repr = ds.PixelRepresentation
-    if pixel_repr == 0:
-        dtype_str = 'uint'
-    elif pixel_repr == 1:
-        dtype_str = 'int'
+    if not as_float:
+        # (0028,0103) Pixel Representation, US, 1
+        #   Data representation of the pixel samples
+        #   0x0000 - unsigned int
+        #   0x0001 - 2's complement (signed int)
+        pixel_repr = ds.PixelRepresentation
+        if pixel_repr == 0:
+            dtype_str = 'uint'
+        elif pixel_repr == 1:
+            dtype_str = 'int'
+        else:
+            raise ValueError(
+                "Unable to determine the data type to use to contain the "
+                "Pixel Data as a value of '{}' for '(0028,0103) Pixel "
+                "Representation' is invalid".format(pixel_repr)
+            )
     else:
-        raise ValueError(
-            "Unable to determine the data type to use to contain the "
-            "Pixel Data as a value of '{}' for '(0028,0103) Pixel "
-            "Representation' is invalid".format(pixel_repr)
-        )
+        dtype_str = 'float'
 
     # (0028,0100) Bits Allocated, US, 1
     #   The number of bits allocated for each pixel sample
diff --git a/pydicom/sequence.py b/pydicom/sequence.py
index 8ed09267d..481145637 100644
--- a/pydicom/sequence.py
+++ b/pydicom/sequence.py
@@ -3,6 +3,7 @@
 
 Sequence is a list of pydicom Dataset objects.
 """
+import weakref
 
 from pydicom.dataset import Dataset
 from pydicom.multival import MultiValue
@@ -57,7 +58,8 @@ class Sequence(MultiValue):
 
     @property
     def parent(self):
-        """Return the parent :class:`~pydicom.dataset.Dataset`."""
+        """Return a weak reference to the parent
+        :class:`~pydicom.dataset.Dataset`."""
         return self._parent
 
     @parent.setter
@@ -66,7 +68,7 @@ class Sequence(MultiValue):
         :class:`Sequence` items.
         """
         if value != self._parent:
-            self._parent = value
+            self._parent = weakref.ref(value)
             for item in self._list:
                 item.parent = self._parent
 


Test patch:
diff --git a/pydicom/tests/test_dataset.py b/pydicom/tests/test_dataset.py
index 5ce1af49d..7efcda5cf 100644
--- a/pydicom/tests/test_dataset.py
+++ b/pydicom/tests/test_dataset.py
@@ -1277,6 +1277,17 @@ class TestDataset(object):
         ds2.update(ds)
         assert 'TestC' == ds2.PatientName
 
+    def test_convert_pixel_data_no_px(self):
+        """Test convert_pixel_data() with no pixel data elements."""
+        ds = Dataset()
+        msg = (
+            r"Unable to convert the pixel data: one of Pixel Data, Float "
+            r"Pixel Data or Double Float Pixel Data must be present in "
+            r"the dataset"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            ds.convert_pixel_data()
+
 
 class TestDatasetElements(object):
     """Test valid assignments of data elements"""
diff --git a/pydicom/tests/test_filewriter.py b/pydicom/tests/test_filewriter.py
index 48efabb6b..8bd55d8a4 100644
--- a/pydicom/tests/test_filewriter.py
+++ b/pydicom/tests/test_filewriter.py
@@ -875,47 +875,90 @@ class TestCorrectAmbiguousVR(object):
         assert ds[0x00283002].VR == 'US'
         assert ds.LUTDescriptor == [1, 0]
 
-    def test_ambiguous_element_in_sequence_explicit(self):
-        """Test that writing a sequence with an ambiguous element
-        as explicit transfer syntax works."""
-        # regression test for #804
+    def dataset_with_modality_lut_sequence(self, pixel_repr):
         ds = Dataset()
-        ds.PixelRepresentation = 0
+        ds.PixelRepresentation = pixel_repr
         ds.ModalityLUTSequence = [Dataset()]
         ds.ModalityLUTSequence[0].LUTDescriptor = [0, 0, 16]
         ds.ModalityLUTSequence[0].LUTExplanation = None
         ds.ModalityLUTSequence[0].ModalityLUTType = 'US'  # US = unspecified
         ds.ModalityLUTSequence[0].LUTData = b'\x0000\x149a\x1f1c\xc2637'
-
         ds.is_little_endian = True
+        return ds
+
+    def test_ambiguous_element_in_sequence_explicit_using_attribute(self):
+        """Test that writing a sequence with an ambiguous element
+        as explicit transfer syntax works if accessing the tag via keyword."""
+        # regression test for #804
+        ds = self.dataset_with_modality_lut_sequence(pixel_repr=0)
         ds.is_implicit_VR = False
         fp = BytesIO()
         ds.save_as(fp, write_like_original=True)
-
         ds = dcmread(fp, force=True)
         assert 'US' == ds.ModalityLUTSequence[0][0x00283002].VR
 
-    def test_ambiguous_element_in_sequence_implicit(self):
+        ds = self.dataset_with_modality_lut_sequence(pixel_repr=1)
+        ds.is_implicit_VR = False
+        fp = BytesIO()
+        ds.save_as(fp, write_like_original=True)
+        ds = dcmread(fp, force=True)
+        assert 'SS' == ds.ModalityLUTSequence[0][0x00283002].VR
+
+    def test_ambiguous_element_in_sequence_explicit_using_index(self):
+        """Test that writing a sequence with an ambiguous element
+        as explicit transfer syntax works if accessing the tag
+        via the tag number."""
+        ds = self.dataset_with_modality_lut_sequence(pixel_repr=0)
+        ds.is_implicit_VR = False
+        fp = BytesIO()
+        ds.save_as(fp, write_like_original=True)
+        ds = dcmread(fp, force=True)
+        assert 'US' == ds[0x00283000][0][0x00283002].VR
+
+        ds = self.dataset_with_modality_lut_sequence(pixel_repr=1)
+        ds.is_implicit_VR = False
+        fp = BytesIO()
+        ds.save_as(fp, write_like_original=True)
+        ds = dcmread(fp, force=True)
+        assert 'SS' == ds[0x00283000][0][0x00283002].VR
+
+    def test_ambiguous_element_in_sequence_implicit_using_attribute(self):
         """Test that reading a sequence with an ambiguous element
-        from a file with implicit transfer syntax works."""
+        from a file with implicit transfer syntax works if accessing the
+        tag via keyword."""
         # regression test for #804
-        ds = Dataset()
-        ds.PixelRepresentation = 0
-        ds.ModalityLUTSequence = [Dataset()]
-        ds.ModalityLUTSequence[0].LUTDescriptor = [0, 0, 16]
-        ds.ModalityLUTSequence[0].LUTExplanation = None
-        ds.ModalityLUTSequence[0].ModalityLUTType = 'US'  # US = unspecified
-        ds.ModalityLUTSequence[0].LUTData = b'\x0000\x149a\x1f1c\xc2637'
-
-        ds.is_little_endian = True
+        ds = self.dataset_with_modality_lut_sequence(pixel_repr=0)
         ds.is_implicit_VR = True
         fp = BytesIO()
         ds.save_as(fp, write_like_original=True)
         ds = dcmread(fp, force=True)
-        # we first have to access the value to trigger correcting the VR
-        assert 16 == ds.ModalityLUTSequence[0].LUTDescriptor[2]
         assert 'US' == ds.ModalityLUTSequence[0][0x00283002].VR
 
+        ds = self.dataset_with_modality_lut_sequence(pixel_repr=1)
+        ds.is_implicit_VR = True
+        fp = BytesIO()
+        ds.save_as(fp, write_like_original=True)
+        ds = dcmread(fp, force=True)
+        assert 'SS' == ds.ModalityLUTSequence[0][0x00283002].VR
+
+    def test_ambiguous_element_in_sequence_implicit_using_index(self):
+        """Test that reading a sequence with an ambiguous element
+        from a file with implicit transfer syntax works if accessing the tag
+        via the tag number."""
+        ds = self.dataset_with_modality_lut_sequence(pixel_repr=0)
+        ds.is_implicit_VR = True
+        fp = BytesIO()
+        ds.save_as(fp, write_like_original=True)
+        ds = dcmread(fp, force=True)
+        assert 'US' == ds[0x00283000][0][0x00283002].VR
+
+        ds = self.dataset_with_modality_lut_sequence(pixel_repr=1)
+        ds.is_implicit_VR = True
+        fp = BytesIO()
+        ds.save_as(fp, write_like_original=True)
+        ds = dcmread(fp, force=True)
+        assert 'SS' == ds[0x00283000][0][0x00283002].VR
+
 
 class TestCorrectAmbiguousVRElement(object):
     """Test filewriter.correct_ambiguous_vr_element"""
diff --git a/pydicom/tests/test_handler_util.py b/pydicom/tests/test_handler_util.py
index 6f0aba813..a7c1f4c96 100644
--- a/pydicom/tests/test_handler_util.py
+++ b/pydicom/tests/test_handler_util.py
@@ -78,11 +78,12 @@ class TestNoNumpy(object):
 
 # Tests with Numpy available
 REFERENCE_DTYPE = [
-    # BitsAllocated, PixelRepresentation, numpy dtype string
-    (1, 0, 'uint8'), (1, 1, 'uint8'),
-    (8, 0, 'uint8'), (8, 1, 'int8'),
-    (16, 0, 'uint16'), (16, 1, 'int16'),
-    (32, 0, 'uint32'), (32, 1, 'int32'),
+    # BitsAllocated, PixelRepresentation, as_float, numpy dtype string
+    (1, 0, False, 'uint8'), (1, 1, False, 'uint8'),
+    (8, 0, False, 'uint8'), (8, 1, False, 'int8'),
+    (16, 0, False, 'uint16'), (16, 1, False, 'int16'),
+    (32, 0, False, 'uint32'), (32, 1, False, 'int32'),
+    (32, 0, True, 'float32'), (64, 0, True, 'float64'),
 ]
 
 
@@ -137,8 +138,10 @@ class TestNumpy_PixelDtype(object):
                            match="data type 'uint24' needed to contain"):
             pixel_dtype(self.ds)
 
-    @pytest.mark.parametrize('bits, pixel_repr, dtype', REFERENCE_DTYPE)
-    def test_supported_dtypes(self, bits, pixel_repr, dtype):
+    @pytest.mark.parametrize(
+        'bits, pixel_repr, as_float, dtype', REFERENCE_DTYPE
+    )
+    def test_supported_dtypes(self, bits, pixel_repr, as_float, dtype):
         """Test supported dtypes."""
         self.ds.BitsAllocated = bits
         self.ds.PixelRepresentation = pixel_repr
@@ -148,7 +151,7 @@ class TestNumpy_PixelDtype(object):
         if endianness != (byteorder == 'little'):
             ref_dtype = ref_dtype.newbyteorder('S')
 
-        assert ref_dtype == pixel_dtype(self.ds)
+        assert ref_dtype == pixel_dtype(self.ds, as_float=as_float)
 
     def test_byte_swapping(self):
         """Test that the endianess of the system is taken into account."""
diff --git a/pydicom/tests/test_numpy_pixel_data.py b/pydicom/tests/test_numpy_pixel_data.py
index 92c940cb6..e3658711c 100644
--- a/pydicom/tests/test_numpy_pixel_data.py
+++ b/pydicom/tests/test_numpy_pixel_data.py
@@ -974,6 +974,72 @@ class TestNumpy_NumpyHandler(object):
             # Frame 2 is frame 1 inverted
             assert np.array_equal((2 ** ds.BitsAllocated - 1) - arr[1], arr[0])
 
+    def test_little_32bit_float_1frame(self):
+        """Test pixel_array for float pixel data, 1 frame."""
+        ds = dcmread(IMPL_32_1_1F)
+        ds.FloatPixelData = ds.PixelData
+        del ds.PixelData
+        for uid in SUPPORTED_SYNTAXES[:3]:
+            ds.file_meta.TransferSyntaxUID = uid
+
+            arr = ds.pixel_array
+
+            assert arr.flags.writeable
+            assert (10, 10) == arr.shape
+            assert 1.75e-39 == pytest.approx(arr[0, 0], abs=0.01e-39)
+            assert 1.44e-39 == pytest.approx(arr[4, 3], abs=0.01e-39)
+            assert 1.13e-39 == pytest.approx(arr[-1, -3], abs=0.01e-39)
+
+    def test_little_32bit_float_15frame(self):
+        """Test pixel_array for float pixel data, 15 frames."""
+        ds = dcmread(IMPL_32_1_15F)
+        ds.FloatPixelData = ds.PixelData
+        del ds.PixelData
+        for uid in SUPPORTED_SYNTAXES[:3]:
+            ds.file_meta.TransferSyntaxUID = uid
+
+            arr = ds.pixel_array
+
+            assert arr.flags.writeable
+            assert (15, 10, 10) == arr.shape
+            assert 1.75e-39 == pytest.approx(arr[0, 0, 0], abs=0.01e-39)
+            assert 1.44e-39 == pytest.approx(arr[0, 4, 3], abs=0.01e-39)
+            assert 1.13e-39 == pytest.approx(arr[0, -1, -3], abs=0.01e-39)
+
+    def test_little_64bit_float_1frame(self):
+        """Test pixel_array for double float pixel data, 1 frame."""
+        ds = dcmread(IMPL_32_1_1F)
+        ds.DoubleFloatPixelData = ds.PixelData + ds.PixelData
+        del ds.PixelData
+        ds.BitsAllocated = 64
+        for uid in SUPPORTED_SYNTAXES[:3]:
+            ds.file_meta.TransferSyntaxUID = uid
+
+            arr = ds.pixel_array
+
+            assert arr.flags.writeable
+            assert (10, 10) == arr.shape
+            assert 2.65e-308 == pytest.approx(arr[0, 0], abs=0.01e-308)
+            assert 1.80e-308 == pytest.approx(arr[4, 3], abs=0.01e-308)
+            assert 1.69e-308 == pytest.approx(arr[-1, -3], abs=0.01e-308)
+
+    def test_little_64bit_float_15frame(self):
+        """Test pixel_array for double float pixel data, 15 frames."""
+        ds = dcmread(IMPL_32_1_15F)
+        ds.DoubleFloatPixelData = ds.PixelData + ds.PixelData
+        del ds.PixelData
+        ds.BitsAllocated = 64
+        for uid in SUPPORTED_SYNTAXES[:3]:
+            ds.file_meta.TransferSyntaxUID = uid
+
+            arr = ds.pixel_array
+
+            assert arr.flags.writeable
+            assert (15, 10, 10) == arr.shape
+            assert 2.65e-308 == pytest.approx(arr[0, 0, 0], abs=0.01e-308)
+            assert 1.80e-308 == pytest.approx(arr[0, 4, 3], abs=0.01e-308)
+            assert 1.69e-308 == pytest.approx(arr[0, -1, -3], abs=0.01e-308)
+
     # Big endian datasets
     @pytest.mark.parametrize('little, big', MATCHING_DATASETS)
     def test_big_endian_datasets(self, little, big):
@@ -1015,13 +1081,33 @@ class TestNumpy_NumpyHandler(object):
 @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available')
 class TestNumpy_GetPixelData(object):
     """Tests for numpy_handler.get_pixeldata with numpy."""
-
     def test_no_pixel_data_raises(self):
         """Test get_pixeldata raises if dataset has no PixelData."""
         ds = dcmread(EXPL_16_1_1F)
         del ds.PixelData
         assert 'PixelData' not in ds
-        with pytest.raises(AttributeError, match=' dataset: PixelData'):
+        assert 'FloatPixelData' not in ds
+        assert 'DoubleFloatPixelData' not in ds
+        msg = (
+            r"Unable to convert the pixel data: one of Pixel Data, Float "
+            r"Pixel Data or Double Float Pixel Data must be present in "
+            r"the dataset"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            get_pixeldata(ds)
+
+    def test_missing_required_elem(self):
+        """Tet get_pixeldata raises if dataset missing required element."""
+        ds = dcmread(EXPL_16_1_1F)
+        del ds.BitsAllocated
+        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"
+        )
+        with pytest.raises(AttributeError, match=msg):
             get_pixeldata(ds)
 
     def test_unknown_pixel_representation_raises(self):
@@ -1146,6 +1232,26 @@ class TestNumpy_GetPixelData(object):
         # Resulting data will be nonsense but of correct shape
         assert (100, 100, 3) == arr.shape
 
+    def test_float_pixel_data(self):
+        """Test handling of Float Pixel Data."""
+        # Only 1 sample per pixel allowed
+        ds = dcmread(IMPL_32_1_1F)
+        ds.FloatPixelData = ds.PixelData
+        del ds.PixelData
+        assert 32 == ds.BitsAllocated
+        arr = get_pixeldata(ds)
+        assert 'float32' == arr.dtype
+
+    def test_double_float_pixel_data(self):
+        """Test handling of Double Float Pixel Data."""
+        # Only 1 sample per pixel allowed
+        ds = dcmread(IMPL_32_1_1F)
+        ds.DoubleFloatPixelData = ds.PixelData + ds.PixelData
+        del ds.PixelData
+        ds.BitsAllocated = 64
+        arr = get_pixeldata(ds)
+        assert 'float64' == arr.dtype
+
 
 REFERENCE_PACK_UNPACK = [
     (b'', []),