# Copyright 2008-2019 pydicom authors. See LICENSE file for details.
"""Use the `numpy `_ package to convert supported *Overlay
Data* to a :class:`numpy.ndarray`.
**Supported data**
The numpy handler supports the conversion of data in the (60xx,3000)
*Overlay Data* element to a :class:`~numpy.ndarray` provided the
related :dcm:`Overlay Plane` and :dcm:`Multi-frame
Overlay` module elements have values given in the
table below.
+------------------------------------------------+--------------+
| Element | Supported |
+-------------+---------------------------+------+ values |
| Tag | Keyword | Type | |
+=============+===========================+======+==============+
| (60xx,0010) | OverlayRows | 1 | N > 0 |
+-------------+---------------------------+------+--------------+
| (60xx,0011) | OverlayColumns | 1 | N > 0 |
+-------------+---------------------------+------+--------------+
| (60xx,0015) | NumberOfFramesInOverlay | 1 | N > 0 |
+-------------+---------------------------+------+--------------+
| (60xx,0100) | OverlayBitsAllocated | 1 | 1 |
+-------------+---------------------------+------+--------------+
| (60xx,0102) | OverlayBitPosition | 1 | 0 |
+-------------+---------------------------+------+--------------+
"""
from typing import TYPE_CHECKING, cast, Dict, Any, Optional
import warnings
try:
import numpy as np
HAVE_NP = True
except ImportError:
HAVE_NP = False
from pydicom.pixel_data_handlers import unpack_bits
if TYPE_CHECKING: # pragma: no cover
from pydicom.dataset import Dataset
from pydicom.dataelem import DataElement
HANDLER_NAME = 'Numpy Overlay'
DEPENDENCIES = {'numpy': ('http://www.numpy.org/', 'NumPy')}
def is_available() -> bool:
"""Return ``True`` if the handler has its dependencies met.
.. versionadded:: 1.4
"""
return HAVE_NP
def get_expected_length(elem: Dict[str, Any], unit: str = 'bytes') -> int:
"""Return the expected length (in terms of bytes or pixels) of the *Overlay
Data*.
.. versionadded:: 1.4
+------------------------------------------------+-------------+
| Element | Required or |
+-------------+---------------------------+------+ optional |
| Tag | Keyword | Type | |
+=============+===========================+======+=============+
| (60xx,0010) | OverlayRows | 1 | Required |
+-------------+---------------------------+------+-------------+
| (60xx,0011) | OverlayColumns | 1 | Required |
+-------------+---------------------------+------+-------------+
| (60xx,0015) | NumberOfFramesInOverlay | 1 | Required |
+-------------+---------------------------+------+-------------+
Parameters
----------
elem : dict
A :class:`dict` with the keys as the element keywords and values the
corresponding element values (such as ``{'OverlayRows': 512, ...}``)
for the elements listed in the table above.
unit : str, optional
If ``'bytes'`` then returns the expected length of the *Overlay Data*
in whole bytes and NOT including an odd length trailing NULL padding
byte. If ``'pixels'`` then returns the expected length of the *Overlay
Data* in terms of the total number of pixels (default ``'bytes'``).
Returns
-------
int
The expected length of the *Overlay Data* in either whole bytes or
pixels, excluding the NULL trailing padding byte for odd length data.
"""
length: int = elem['OverlayRows'] * elem['OverlayColumns']
length *= elem['NumberOfFramesInOverlay']
if unit == 'pixels':
return length
# Determine the nearest whole number of bytes needed to contain
# 1-bit pixel data. e.g. 10 x 10 1-bit pixels is 100 bits, which
# are packed into 12.5 -> 13 bytes
return length // 8 + (length % 8 > 0)
def reshape_overlay_array(
elem: Dict[str, Any], arr: "np.ndarray"
) -> "np.ndarray":
"""Return a reshaped :class:`numpy.ndarray` `arr`.
.. versionadded:: 1.4
+------------------------------------------------+--------------+
| Element | Supported |
+-------------+---------------------------+------+ values |
| Tag | Keyword | Type | |
+=============+===========================+======+==============+
| (60xx,0010) | OverlayRows | 1 | N > 0 |
+-------------+---------------------------+------+--------------+
| (60xx,0011) | OverlayColumns | 1 | N > 0 |
+-------------+---------------------------+------+--------------+
| (60xx,0015) | NumberOfFramesInOverlay | 1 | N > 0 |
+-------------+---------------------------+------+--------------+
Parameters
----------
elem : dict
A :class:`dict` with the keys as the element keywords and values the
corresponding element values (such as ``{'OverlayRows': 512, ...}``)
for the elements listed in the table above.
arr : numpy.ndarray
A 1D array containing the overlay data.
Returns
-------
numpy.ndarray
A reshaped array containing the overlay data. The shape of the array
depends on the contents of the dataset:
* For single frame data (rows, columns)
* For multi-frame data (frames, rows, columns)
References
----------
* DICOM Standard, Part 3, Sections :dcm:`C.9.2`
and :dcm:`C.9.3`
* DICOM Standard, Part 5, :dcm:`Section 8.2`
"""
if not HAVE_NP:
raise ImportError("Numpy is required to reshape the overlay array.")
nr_frames = elem['NumberOfFramesInOverlay']
nr_rows = elem['OverlayRows']
nr_columns = elem['OverlayColumns']
if nr_frames < 1:
raise ValueError(
f"Unable to reshape the overlay array as a value of {nr_frames} "
"for (60xx,0015) 'Number of Frames in Overlay' is invalid."
)
if nr_frames > 1:
return arr.reshape(nr_frames, nr_rows, nr_columns)
return arr.reshape(nr_rows, nr_columns)
def get_overlay_array(ds: "Dataset", group: int) -> "np.ndarray":
"""Return a :class:`numpy.ndarray` of the *Overlay Data*.
.. versionadded:: 1.4
Parameters
----------
ds : Dataset
The :class:`Dataset` containing an Overlay Plane module and the
*Overlay Data* to be converted.
group : int
The group part of the *Overlay Data* element tag, e.g. ``0x6000``,
``0x6010``, etc. Must be between 0x6000 and 0x60FF.
Returns
-------
np.ndarray
The contents of (`group`,3000) *Overlay Data* as an array.
Raises
------
AttributeError
If `ds` is missing a required element.
ValueError
If the actual length of the overlay data doesn't match the expected
length.
"""
if not HAVE_NP:
raise ImportError("The overlay data handler requires numpy")
# Check required elements
elem = {
'OverlayData': ds.get((group, 0x3000), None),
'OverlayBitsAllocated': ds.get((group, 0x0100), None),
'OverlayRows': ds.get((group, 0x0010), None),
'OverlayColumns': ds.get((group, 0x0011), None),
}
missing = [kk for kk, vv in elem.items() if vv is None]
if missing:
raise AttributeError(
"Unable to convert the overlay data as the following required "
f"elements are missing from the dataset: {', '.join(missing)}"
)
# Grab the element values
elem_values = {kk: vv.value for kk, vv in elem.items()}
# Add in if not present
nr_frames: Optional["DataElement"] = ds.get((group, 0x0015), None)
if nr_frames is None:
elem_values['NumberOfFramesInOverlay'] = 1
else:
elem_values['NumberOfFramesInOverlay'] = nr_frames.value
# 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(elem_values)
# Check that the actual length of the pixel data is as expected
actual_length = len(cast(bytes, elem_values['OverlayData']))
# Correct for the trailing NULL byte padding for odd length data
padded_expected_len = expected_len + expected_len % 2
if actual_length < padded_expected_len:
if actual_length == expected_len:
warnings.warn(
"The overlay data length is odd and misses a padding byte."
)
else:
raise ValueError(
"The length of the overlay data in the dataset "
f"({actual_length} bytes) doesn't match the expected length "
f"({padded_expected_len} bytes). The dataset may be corrupted "
"or there may be an issue with the overlay data handler."
)
elif actual_length > padded_expected_len:
# PS 3.5, Section 8.1.1
warnings.warn(
f"The length of the overlay data in the dataset ({actual_length} "
"bytes) indicates it contains excess padding. "
f"{actual_length - expected_len} bytes will be removed "
"from the end of the data"
)
# Unpack the pixel data into a 1D ndarray, skipping any trailing padding
nr_pixels = get_expected_length(elem_values, unit='pixels')
arr = cast(
"np.ndarray", unpack_bits(elem_values['OverlayData'])[:nr_pixels]
)
return reshape_overlay_array(elem_values, arr)