Exemple #1
0
 def test_enc_dataset_multiframe(self):
     """Test encoding a multiframe compressed dataset"""
     ds = self.ds_enc_mf
     assert ds.file_meta.TransferSyntaxUID.is_compressed
     out = self.enc.encode(ds, idx=0)
     uncompressed_len = get_expected_length(ds, 'bytes')
     assert uncompressed_len / ds.NumberOfFrames > len(out)
Exemple #2
0
def get_pixeldata(ds: "Dataset") -> "np.ndarray":
    """Return a :class:`numpy.ndarray` of the pixel data.

    .. versionadded:: 2.1

    Parameters
    ----------
    ds : pydicom.dataset.Dataset
        The :class:`Dataset` containing an :dcm:`Image Pixel
        <part03/sect_C.7.6.3.html>` module and the *Pixel Data* to be
        converted.

    Returns
    -------
    numpy.ndarray
        The contents of (7FE0,0010) *Pixel Data* as a 1D array.
    """
    expected_len = get_expected_length(ds, 'pixels')
    frame_len = expected_len // getattr(ds, "NumberOfFrames", 1)
    # Empty destination array for our decoded pixel data
    arr = np.empty(expected_len, pixel_dtype(ds))

    generate_offsets = range(0, expected_len, frame_len)
    for frame, offset in zip(generate_frames(ds, False), generate_offsets):
        arr[offset:offset + frame_len] = frame

    return arr
Exemple #3
0
 def test_enc_dataset(self):
     """Test encoding a compressed dataset"""
     ds = self.ds_enc
     assert ds.file_meta.TransferSyntaxUID.is_compressed
     out = self.enc.encode(ds)
     uncompressed_len = get_expected_length(ds, 'bytes')
     assert uncompressed_len > len(out)
    def test_length_in_pixels(self, shape, bits, length):
        """Test get_expected_length(ds, unit='pixels')."""
        ds = Dataset()
        ds.Rows = shape[1]
        ds.Columns = shape[2]
        ds.BitsAllocated = bits
        if shape[0] != 0:
            ds.NumberOfFrames = shape[0]
        ds.SamplesPerPixel = shape[3]

        assert length[1] == get_expected_length(ds, unit='pixels')
    def test_length_in_pixels(self, shape, bits, length):
        """Test get_expected_length(ds, unit='pixels')."""
        ds = Dataset()
        ds.Rows = shape[1]
        ds.Columns = shape[2]
        ds.BitsAllocated = bits
        if shape[0] != 0:
            ds.NumberOfFrames = shape[0]
        ds.SamplesPerPixel = shape[3]

        assert length[1] == get_expected_length(ds, unit='pixels')
Exemple #6
0
    def test_length_in_bytes(self, shape, bits, length):
        """Test get_expected_length(ds, unit='bytes')."""
        ds = Dataset()
        ds.PhotometricInterpretation = 'MONOCHROME2'
        ds.Rows = shape[1]
        ds.Columns = shape[2]
        ds.BitsAllocated = bits
        if shape[0] != 0:
            ds.NumberOfFrames = shape[0]
        ds.SamplesPerPixel = shape[3]

        assert length[0] == get_expected_length(ds, unit='bytes')
Exemple #7
0
 def test_encode(self):
     """Test encoding"""
     ds = dcmread(EXPL)
     assert 'PlanarConfiguration' not in ds
     expected = get_expected_length(ds, 'bytes')
     assert expected == len(ds.PixelData)
     ref = ds.pixel_array
     del ds.PixelData
     ds.compress(RLELossless, ref, encoding_plugin='pylibjpeg')
     assert expected > len(ds.PixelData)
     assert np.array_equal(ref, ds.pixel_array)
     assert id(ref) != id(ds.pixel_array)
Exemple #8
0
    def test_length_ybr_422(self, shape, bits, length):
        """Test get_expected_length for YBR_FULL_422."""
        if shape[3] != 3 or bits == 1:
            return

        ds = Dataset()
        ds.PhotometricInterpretation = 'YBR_FULL_422'
        ds.Rows = shape[1]
        ds.Columns = shape[2]
        ds.BitsAllocated = bits
        if shape[0] != 0:
            ds.NumberOfFrames = shape[0]
        ds.SamplesPerPixel = shape[3]

        assert length[2] == get_expected_length(ds, unit='bytes')
Exemple #9
0
    def add_segments(
            self,
            pixel_array: np.ndarray,
            segment_descriptions: Sequence[SegmentDescription],
            plane_positions: Optional[Sequence[PlanePositionSequence]] = None
        ) -> Dataset:
        """Adds one or more segments to the segmentation image.

        Parameters
        ----------
        pixel_array: numpy.ndarray
            Array of segmentation pixel data of boolean, unsigned integer or
            floating point data type representing a mask image. If `pixel_array`
            is a floating-point array or a binary array (containing only the
            values ``True`` and ``False`` or ``0`` and ``1``), the segment number
            used to encode the segment is taken from segment_descriptions.
            Otherwise, if pixel_array contains multiple integer values, each value
            is treated as a different segment whose segment number is that integer
            value. In this case, all segments found in the array must be described
            in `segment_descriptions`. Note that this is valid for both ``"BINARY"``
            and ``"FRACTIONAL"`` segmentations.
            For ``"FRACTIONAL"`` segmentations, values either encode the probability
            of a given pixel belonging to a segment
            (if `fractional_type` is ``"PROBABILITY"``)
            or the extent to which a segment occupies the pixel
            (if `fractional_type` is ``"OCCUPANCY"``).
            When `pixel_array` has a floating point data type, only one segment can be
            encoded. Additional segments can be subsequently
            added to the `Segmentation` instance using the ``add_segments()``
            method.
            If `pixel_array` represents a 3D image, the first dimension
            represents individual 2D planes and these planes must be ordered
            based on their position in the three-dimensional patient
            coordinate system (first along the X axis, second along the Y axis,
            and third along the Z axis).
            If `pixel_array` represents a tiled 2D image, the first dimension
            represents individual 2D tiles (for one channel and z-stack) and
            these tiles must be ordered based on their position in the tiled
            total pixel matrix (first along the row dimension and second along
            the column dimension, which are defined in the three-dimensional
            slide coordinate system by the direction cosines encoded by the
            *Image Orientation (Slide)* attribute).
        segment_descriptions: Sequence[highdicom.seg.content.SegmentDescription]
            Description of each segment encoded in `pixel_array`. In the case of
            pixel arrays with multiple integer values, the segment description
            with the corresponding segment number is used to describe each
            segment.
        plane_positions: Sequence[highdicom.content.PlanePositionSequence], optional
            Position of each plane in `pixel_array` relative to the
            three-dimensional patient or slide coordinate system.

        Note
        ----
        Items of `segment_descriptions` must be sorted by segment number in
        ascending order.
        In case `segmentation_type` is ``"BINARY"``, the number of items per
        sequence must match the number of unique positive pixel values in
        `pixel_array`. In case `segmentation_type` is ``"FRACTIONAL"``, only
        one segment can be encoded by `pixel_array` and hence only one item is
        permitted per sequence.

        """  # noqa
        if pixel_array.ndim == 2:
            pixel_array = pixel_array[np.newaxis, ...]
        if pixel_array.ndim != 3:
            raise ValueError('Pixel array must be a 2D or 3D array.')

        if pixel_array.shape[1:3] != (self.Rows, self.Columns):
            raise ValueError(
                'Pixel array representing segments has the wrong number of '
                'rows and columns.'
            )

        described_segment_numbers = np.array([
            int(item.SegmentNumber)
            for item in segment_descriptions
        ])
        # Check that there are no duplicated segment numbers in the segment
        # descriptions
        if not (np.diff(described_segment_numbers) > 0).all():
            raise ValueError(
                'Segment descriptions must be sorted by segment number.'
            )

        if pixel_array.dtype in (np.bool, np.uint8, np.uint16):
            segments_present = np.unique(
                pixel_array[pixel_array > 0].astype(np.uint16)
            )

            # Special case where the mask is binary and there is a single
            # segment description. Allow the mark the positive segment with
            # the correct segment number
            if (np.array_equal(segments_present, np.array([1])) and
                    len(segment_descriptions) == 1):
                pixel_array = pixel_array.astype(np.uint8)
                pixel_array *= described_segment_numbers.item()

            # Otherwise, the pixel values in the pixel array must all belong to
            # a described segment
            else:
                if not np.all(
                        np.in1d(segments_present, described_segment_numbers)
                    ):
                    raise ValueError(
                        'Pixel array contains segments that lack descriptions.'
                    )

        elif (pixel_array.dtype == np.float):
            unique_values = np.unique(pixel_array)
            if np.min(unique_values) < 0.0 or np.max(unique_values) > 1.0:
                raise ValueError(
                    'Floating point pixel array values must be in the '
                    'range [0, 1].'
                )
            if len(segment_descriptions) != 1:
                raise ValueError(
                    'When providing a float-valued pixel array, provide only '
                    'a single segment description'
                )
            if self.SegmentationType == SegmentationTypeValues.BINARY.value:
                non_boolean_values = np.logical_and(
                    unique_values > 0.0,
                    unique_values < 1.0
                )
                if np.any(non_boolean_values):
                    raise ValueError(
                        'Floating point pixel array values must be either '
                        '0.0 or 1.0 in case of BINARY segmentation type.'
                    )
                pixel_array = pixel_array.astype(np.bool)
        else:
            raise TypeError('Pixel array has an invalid data type.')

        # Check that the new segments do not already exist
        if len(set(described_segment_numbers) & self._segment_inventory) > 0:
            raise ValueError('Segment with given segment number already exists')

        # Set the optional tag value SegmentsOverlapValues to NO to indicate
        # that the segments do not overlap. We can know this for sure if it's
        # the first segment (or set of segments) to be added because they are
        # contained within a single pixel array.
        if len(self._segment_inventory) == 0:
            self.SegmentsOverlap = SegmentsOverlapValues.NO.value
        else:
            # If this is not the first set of segments to be added, we cannot
            # be sure whether there is overlap with the existing segments
            self.SegmentsOverlap = SegmentsOverlapValues.UNDEFINED.value

        src_img = self._source_images[0]
        is_multiframe = hasattr(src_img, 'NumberOfFrames')
        if self._coordinate_system == CoordinateSystemNames.SLIDE:
            if hasattr(src_img, 'PerFrameFunctionalGroupsSequence'):
                source_plane_positions = [
                    item.PlanePositionSlideSequence
                    for item in src_img.PerFrameFunctionalGroupsSequence
                ]
            else:
                # If Dimension Organization Type is TILED_FULL, plane
                # positions are implicit and need to be computed.
                image_origin = src_img.TotalPixelMatrixOriginSequence[0]
                orientation = tuple(
                    float(v) for v in src_img.ImageOrientationSlide
                )
                tiles_per_column = int(
                    np.ceil(
                        src_img.TotalPixelMatrixRows /
                        src_img.Rows
                    )
                )
                tiles_per_row = int(
                    np.ceil(
                        src_img.TotalPixelMatrixColumns /
                        src_img.Columns
                    )
                )
                num_focal_planes = getattr(
                    src_img,
                    'NumberOfFocalPlanes',
                    1
                )
                row_range = range(1, tiles_per_column + 1)
                column_range = range(1, tiles_per_row + 1)
                depth_range = range(1, num_focal_planes + 1)

                shared_fg = self.SharedFunctionalGroupsSequence[0]
                pixel_measures = shared_fg.PixelMeasuresSequence[0]
                pixel_spacing = tuple(
                    float(v) for v in pixel_measures.PixelSpacing
                )
                slice_thickness = getattr(
                    pixel_measures,
                    'SliceThickness',
                    1.0
                )
                spacing_between_slices = getattr(
                    pixel_measures,
                    'SpacingBetweenSlices',
                    1.0
                )
                source_plane_positions = [
                    compute_plane_positions_tiled_full(
                        row_index=r,
                        column_index=c,
                        depth_index=d,
                        x_offset=image_origin.XOffsetInSlideCoordinateSystem,
                        y_offset=image_origin.YOffsetInSlideCoordinateSystem,
                        z_offset=1.0,  # TODO
                        rows=self.Rows,
                        columns=self.Columns,
                        image_orientation=orientation,
                        pixel_spacing=pixel_spacing,
                        slice_thickness=slice_thickness,
                        spacing_between_slices=spacing_between_slices
                    )
                    for r, c, d in itertools.product(
                        row_range,
                        column_range,
                        depth_range
                    )
                ]
        else:
            if is_multiframe:
                source_plane_positions = [
                    item.PlanePositionSequence
                    for item in src_img.PerFrameFunctionalGroupsSequence
                ]
            else:
                source_plane_positions = [
                    PlanePositionSequence(
                        coordinate_system=CoordinateSystemNames.PATIENT,
                        image_position=src_img.ImagePositionPatient
                    )
                    for src_img in self._source_images
                ]

        if plane_positions is None:
            plane_positions = source_plane_positions
        are_spatial_locations_preserved = (
            all(
                plane_positions[i] == source_plane_positions[i]
                for i in range(len(plane_positions))
            ) and
            self._plane_orientation == self._source_plane_orientation
        )

        if pixel_array.shape[0] != len(plane_positions):
            raise ValueError(
                'Number of pixel array planes does not match number of '
                'provided image positions.'
            )

        # For each dimension other than the Referenced Segment Number,
        # obtain the value of the attribute that the Dimension Index Pointer
        # points to in the element of the Plane Position Sequence or
        # Plane Position Slide Sequence.
        # Per definition, this is the Image Position Patient attribute
        # in case of the patient coordinate system, or the
        # X/Y/Z Offset In Slide Coordinate System and the Column/Row
        # Position in Total Image Pixel Matrix attributes in case of the
        # the slide coordinate system.
        plane_position_values = np.array([
            [
                np.array(p[0][indexer.DimensionIndexPointer].value)
                for indexer in self.DimensionIndexSequence[1:]
            ]
            for p in plane_positions
        ])

        # Planes need to be sorted according to the Dimension Index Value
        # based on the order of the items in the Dimension Index Sequence.
        # Here we construct an index vector that we can subsequently use to
        # sort planes before adding them to the Pixel Data element.
        _, plane_sort_index = np.unique(
            plane_position_values,
            axis=0,
            return_index=True
        )

        # Get unique values of attributes in the Plane Position Sequence or
        # Plane Position Slide Sequence, which define the position of the plane
        # with respect to the three dimensional patient or slide coordinate
        # system, respectively. These can subsequently be used to look up the
        # relative position of a plane relative to the indexed dimension.
        dimension_position_values = [
            np.unique(plane_position_values[:, index], axis=0)
            for index in range(plane_position_values.shape[1])
        ]

        # When using binary segmentation type, the previous frames may have been
        # padded to be a multiple of 8. In this case, we need to decode the
        # pixel data, add the new pixels and then re-encode. This process
        # should be avoided if it is not necessary in order to improve
        # efficiency.
        if (self.SegmentationType == SegmentationTypeValues.BINARY.value and
                ((self.Rows * self.Columns * self.SamplesPerPixel) % 8) > 0):
            re_encode_pixel_data = True
            logger.warning(
                'pixel data needs to be re-encoded for binary bitpacking - '
                'consider using FRACTIONAL instead of BINARY segmentation type'
            )
            # If this is the first segment added, the pixel array is empty
            if hasattr(self, 'PixelData') and len(self.PixelData) > 0:
                full_pixel_array = self.pixel_array.flatten()
            else:
                full_pixel_array = np.array([], np.bool)
        else:
            re_encode_pixel_data = False

            # Before adding new pixel data, remove trailing null padding byte
            if len(self.PixelData) == get_expected_length(self) + 1:
                self.PixelData = self.PixelData[:-1]

        for i, segment_number in enumerate(described_segment_numbers):
            if pixel_array.dtype == np.float:
                # Floating-point numbers must be mapped to 8-bit integers in
                # the range [0, max_fractional_value].
                planes = np.around(
                    pixel_array * float(self.MaximumFractionalValue)
                )
                planes = planes.astype(np.uint8)
            elif pixel_array.dtype in (np.uint8, np.uint16):
                # Labeled masks must be converted to binary masks.
                planes = np.zeros(pixel_array.shape, dtype=np.bool)
                planes[pixel_array == segment_number] = True
            elif pixel_array.dtype == np.bool:
                planes = pixel_array

            contained_plane_index = []
            for j in plane_sort_index:
                if np.sum(planes[j]) == 0:
                    logger.info(
                        'skip empty plane {} of segment #{}'.format(
                            j, segment_number
                        )
                    )
                    continue
                contained_plane_index.append(j)
                logger.info(
                    'add plane #{} for segment #{}'.format(
                        j, segment_number
                    )
                )

                pffp_item = Dataset()
                frame_content_item = Dataset()
                frame_content_item.DimensionIndexValues = [segment_number]

                # Look up the position of the plane relative to the indexed
                # dimension.
                try:
                    if self._coordinate_system == CoordinateSystemNames.SLIDE:
                        index_values = [
                            np.where(
                                (dimension_position_values[idx] == pos)
                            )[0][0] + 1
                            for idx, pos in enumerate(plane_position_values[j])
                        ]
                    else:
                        # In case of the patient coordinate system, the
                        # value of the attribute the Dimension Index Sequence
                        # points to (Image Position Patient) has a value
                        # multiplicity greater than one.
                        index_values = [
                            np.where(
                                (dimension_position_values[idx] == pos).all(
                                    axis=1
                                )
                            )[0][0] + 1
                            for idx, pos in enumerate(plane_position_values[j])
                        ]
                except IndexError as error:
                    raise IndexError(
                        'Could not determine position of plane #{} in '
                        'three dimensional coordinate system based on '
                        'dimension index values: {}'.format(j, error)
                    )
                frame_content_item.DimensionIndexValues.extend(index_values)
                pffp_item.FrameContentSequence = [frame_content_item]
                if self._coordinate_system == CoordinateSystemNames.SLIDE:
                    pffp_item.PlanePositionSlideSequence = plane_positions[j]
                else:
                    pffp_item.PlanePositionSequence = plane_positions[j]

                # Determining the source images that map to the frame is not
                # always trivial. Since DerivationImageSequence is a type 2
                # attribute, we leave its value empty.
                pffp_item.DerivationImageSequence = []

                if are_spatial_locations_preserved:
                    derivation_image_item = Dataset()
                    derivation_code = codes.cid7203.Segmentation
                    derivation_image_item.DerivationCodeSequence = [
                        CodedConcept(
                            derivation_code.value,
                            derivation_code.scheme_designator,
                            derivation_code.meaning,
                            derivation_code.scheme_version
                        ),
                    ]

                    derivation_src_img_item = Dataset()
                    if len(plane_sort_index) > len(self._source_images):
                        # A single multi-frame source image
                        src_img_item = self.SourceImageSequence[0]
                        # Frame numbers are one-based
                        derivation_src_img_item.ReferencedFrameNumber = j + 1
                    else:
                        # Multiple single-frame source images
                        src_img_item = self.SourceImageSequence[j]
                    derivation_src_img_item.ReferencedSOPClassUID = \
                        src_img_item.ReferencedSOPClassUID
                    derivation_src_img_item.ReferencedSOPInstanceUID = \
                        src_img_item.ReferencedSOPInstanceUID
                    purpose_code = \
                        codes.cid7202.SourceImageForImageProcessingOperation
                    derivation_src_img_item.PurposeOfReferenceCodeSequence = [
                        CodedConcept(
                            purpose_code.value,
                            purpose_code.scheme_designator,
                            purpose_code.meaning,
                            purpose_code.scheme_version
                        ),
                    ]
                    derivation_src_img_item.SpatialLocationsPreserved = 'YES'
                    derivation_image_item.SourceImageSequence = [
                        derivation_src_img_item,
                    ]
                    pffp_item.DerivationImageSequence.append(
                        derivation_image_item
                    )
                else:
                    logger.warning('spatial locations not preserved')

                identification = Dataset()
                identification.ReferencedSegmentNumber = segment_number
                pffp_item.SegmentIdentificationSequence = [
                    identification,
                ]
                self.PerFrameFunctionalGroupsSequence.append(pffp_item)
                self.NumberOfFrames += 1

            contained_plane_index = np.array(contained_plane_index, dtype=int)
            if re_encode_pixel_data:
                full_pixel_array = np.concatenate([
                    full_pixel_array,
                    planes[contained_plane_index].flatten()
                ])
            else:
                self.PixelData += self._encode_pixels(
                    planes[contained_plane_index]
                )

            # In case of a tiled Total Pixel Matrix pixel data for the same
            # segment may be added.
            if segment_number not in self._segment_inventory:
                self.SegmentSequence.append(segment_descriptions[i])
            self._segment_inventory.add(segment_number)

        # Re-encode the whole pixel array at once if necessary
        if re_encode_pixel_data:
            self.PixelData = self._encode_pixels(full_pixel_array)

        # Add back the null trailing byte if required
        if len(self.PixelData) % 2 == 1:
            self.PixelData += b'0'
Exemple #10
0
def get_pixeldata(ds):
    """Return a :class:`numpy.ndarray` of the pixel data.

    Parameters
    ----------
    ds : pydicom.dataset.Dataset
        The :class:`Dataset` containing an Image Pixel module and the
        *Pixel Data* to be converted.

    Returns
    -------
    numpy.ndarray
        The contents of (7FE0,0010) *Pixel Data* as a 1D array.

    Raises
    ------
    AttributeError
        If `ds` is missing a required element.
    NotImplementedError
        If `ds` contains pixel data in an unsupported format.
    ValueError
        If the actual length of the pixel data doesn't match the expected
        length.
    """
    tsyntax = ds.file_meta.TransferSyntaxUID
    # The check of transfer syntax must be first
    if tsyntax not in SUPPORTED_TRANSFER_SYNTAXES:
        raise NotImplementedError(
            "Unable to convert the pixel data as there are no pylibjpeg "
            "plugins available to decode pixel data encoded using '{}'".format(
                tsyntax.name))

    # Check required elements
    required_elements = [
        'BitsAllocated',
        'Rows',
        'Columns',
        'PixelRepresentation',
        'SamplesPerPixel',
        'PhotometricInterpretation',
        'PixelData',
    ]
    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))

    # 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)
    if ds.PhotometricInterpretation == 'YBR_FULL_422':
        # JPEG Transfer Syntaxes
        # Plugin should have already resampled the pixel data
        #   see PS3.3 C.7.6.3.1.2
        expected_len = expected_len // 2 * 3

    p_interp = ds.PhotometricInterpretation

    # How long each frame is in bytes
    nr_frames = getattr(ds, 'NumberOfFrames', 1)
    frame_len = expected_len // nr_frames

    # The decoded data will be placed here
    arr = np.empty(expected_len, np.uint8)

    decoder = _DECODERS[tsyntax]
    LOGGER.debug("Decoding {} Pixel Data using {}".format(tsyntax, decoder))

    # Generators for the encoded JPEG image frame(s) and insertion offsets
    generate_frames = generate_pixel_data_frame(ds.PixelData, nr_frames)
    generate_offsets = range(0, expected_len, frame_len)
    pixel_module = ds.group_dataset(0x0028)
    for frame, offset in zip(generate_frames, generate_offsets):
        # Encoded JPEG data to be sent to the decoder
        arr[offset:offset + frame_len] = decoder(frame, pixel_module)

    if tsyntax in [JPEG2000, JPEG2000Lossless] and APPLY_J2K_CORRECTIONS:
        j2k_parameters = get_j2k_parameters(frame)
        if j2k_parameters:
            shift = ds.BitsAllocated - j2k_parameters['precision']
            if (shift and not j2k_parameters['is_signed']
                    and bool(ds.PixelRepresentation)):
                # Correct for a mismatch between J2K and Pixel Representation
                #  by converting unsigned data to signed (2's complement)
                pixel_module.PixelRepresentation = 0
                # This probably isn't very efficient
                arr = arr.view(pixel_dtype(pixel_module))
                np.left_shift(arr, shift, out=arr)
                arr = arr.astype(pixel_dtype(ds))

                return np.right_shift(arr, shift)

    return arr.view(pixel_dtype(ds))
Exemple #11
0
def get_pixeldata(ds: "Dataset") -> "numpy.ndarray":
    """Use the GDCM package to decode *Pixel Data*.

    Returns
    -------
    numpy.ndarray
        A correctly sized (but not shaped) array of the entire data volume

    Raises
    ------
    ImportError
        If the required packages are not available.
    TypeError
        If the image could not be read by GDCM or if the *Pixel Data* type is
        unsupported.
    AttributeError
        If the decoded amount of data does not match the expected amount.
    """
    if not HAVE_GDCM:
        raise ImportError("The GDCM handler requires both gdcm and numpy")

    if HAVE_GDCM_IN_MEMORY_SUPPORT:
        gdcm_data_element = create_data_element(ds)
        gdcm_image = create_image(ds, gdcm_data_element)
    else:
        gdcm_image_reader = create_image_reader(ds)
        if not gdcm_image_reader.Read():
            raise TypeError("GDCM could not read DICOM image")
        gdcm_image = gdcm_image_reader.GetImage()

    # GDCM returns char* as type str. Python 3 decodes this to
    # unicode strings by default.
    # The SWIG docs mention that they always decode byte streams
    # as utf-8 strings for Python 3, with the `surrogateescape`
    # error handler configured.
    # Therefore, we can encode them back to their original bytearray
    # representation on Python 3 by using the same parameters.

    pixel_bytearray = gdcm_image.GetBuffer().encode("utf-8", "surrogateescape")

    # Here we need to be careful because in some cases, GDCM reads a
    # buffer that is too large, so we need to make sure we only include
    # the first n_rows * n_columns * dtype_size bytes.
    expected_length_bytes = get_expected_length(ds)
    if ds.PhotometricInterpretation == 'YBR_FULL_422':
        # GDCM has already resampled the pixel data, see PS3.3 C.7.6.3.1.2
        expected_length_bytes = expected_length_bytes // 2 * 3

    if len(pixel_bytearray) > expected_length_bytes:
        # We make sure that all the bytes after are in fact zeros
        padding = pixel_bytearray[expected_length_bytes:]
        if numpy.any(numpy.frombuffer(padding, numpy.byte)):
            pixel_bytearray = pixel_bytearray[:expected_length_bytes]
        else:
            # We revert to the old behavior which should then result
            #   in a Numpy error later on.
            pass

    numpy_dtype = pixel_dtype(ds)
    arr = numpy.frombuffer(pixel_bytearray, dtype=numpy_dtype)

    expected_length_pixels = get_expected_length(ds, 'pixels')
    if arr.size != expected_length_pixels:
        raise AttributeError(
            f"Amount of pixel data {arr.size} does not match the "
            f"expected data {expected_length_pixels}"
        )

    file_meta: "FileMetaDataset" = ds.file_meta  # type: ignore[has-type]
    tsyntax = cast(UID, file_meta.TransferSyntaxUID)
    if (
        config.APPLY_J2K_CORRECTIONS
        and tsyntax in [JPEG2000, JPEG2000Lossless]
    ):
        nr_frames = getattr(ds, 'NumberOfFrames', 1)
        codestream = next(generate_pixel_data(ds.PixelData, nr_frames))[0]

        params = get_j2k_parameters(codestream)
        j2k_precision = cast(
            int, params.setdefault("precision", ds.BitsStored)
        )
        j2k_sign = params.setdefault("is_signed", None)

        if not j2k_sign and ds.PixelRepresentation == 1:
            # Convert unsigned J2K data to 2's complement
            shift = cast(int, ds.BitsAllocated) - j2k_precision
            pixel_module = ds.group_dataset(0x0028)
            pixel_module.PixelRepresentation = 0
            dtype = pixel_dtype(pixel_module)
            arr = (arr.astype(dtype) << shift).astype(numpy_dtype) >> shift

    if should_change_PhotometricInterpretation_to_RGB(ds):
        ds.PhotometricInterpretation = "RGB"

    return arr.copy()
Exemple #12
0
def get_pixeldata(ds, read_only=False):
    """Return an ndarray of the Pixel Data.

    Parameters
    ----------
    ds : dataset.Dataset
        The DICOM dataset containing an Image Pixel module and the Pixel Data
        to be converted.
    read_only : bool, optional
        If False (default) then returns a writeable array that no longer uses
        the original memory. If True and the value of (0028,0100) *Bits
        Allocated* > 1 then returns a read-only array that uses the original
        memory buffer of the pixel data. If *Bits Allocated* = 1 then always
        returns a writeable array.

    Returns
    -------
    np.ndarray
        The contents of the Pixel Data element (7FE0,0010) as a 1D array.

    Raises
    ------
    AttributeError
        If the dataset is missing a required element.
    NotImplementedError
        If the dataset contains pixel data in an unsupported format.
    ValueError
        If the actual length of the pixel data doesn't match the expected
        length.
    """
    transfer_syntax = ds.file_meta.TransferSyntaxUID
    # The check of transfer syntax must be first
    if transfer_syntax not in SUPPORTED_TRANSFER_SYNTAXES:
        raise NotImplementedError(
            "Unable to convert the pixel data as the transfer syntax "
            "is not supported by the numpy pixel data handler.")

    # Check required elements
    required_elements = [
        'PixelData', 'BitsAllocated', 'Rows', 'Columns', 'PixelRepresentation',
        'SamplesPerPixel'
    ]
    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))

    # 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)
    # Correct for the trailing NULL byte padding for odd length data
    if actual_length != (expected_len + expected_len % 2):
        raise ValueError(
            "The length of the pixel data in the dataset doesn't match the "
            "expected amount ({0} vs. {1} bytes). The dataset may be "
            "corrupted or there may be an issue with the pixel data handler.".
            format(actual_length, expected_len + expected_len % 2))

    # Unpack the pixel data into a 1D ndarray
    if ds.BitsAllocated == 1:
        # Skip any trailing padding bits
        nr_pixels = get_expected_length(ds, unit='pixels')
        arr = unpack_bits(ds.PixelData)[:nr_pixels]
    else:
        # Skip the trailing padding byte if present
        arr = np.frombuffer(ds.PixelData[:expected_len], dtype=pixel_dtype(ds))

    if should_change_PhotometricInterpretation_to_RGB(ds):
        ds.PhotometricInterpretation = "RGB"

    if not read_only and ds.BitsAllocated > 1:
        return arr.copy()

    return arr
Exemple #13
0
def get_pixeldata(dicom_dataset):
    """
    Use the GDCM package to decode the PixelData attribute

    Returns
    -------
    numpy.ndarray

        A correctly sized (but not shaped) numpy array
        of the entire data volume

    Raises
    ------
    ImportError
        if the required packages are not available

    TypeError
        if the image could not be read by GDCM
        if the pixel data type is unsupported

    AttributeError
        if the decoded amount of data does not match the expected amount
    """

    if not HAVE_GDCM:
        msg = ("GDCM requires both the gdcm package and numpy "
               "and one or more could not be imported")
        raise ImportError(msg)

    if HAVE_GDCM_IN_MEMORY_SUPPORT:
        gdcm_data_element = create_data_element(dicom_dataset)
        gdcm_image = create_image(dicom_dataset, gdcm_data_element)
    else:
        gdcm_image_reader = create_image_reader(dicom_dataset.filename)
        if not gdcm_image_reader.Read():
            raise TypeError("GDCM could not read DICOM image")
        gdcm_image = gdcm_image_reader.GetImage()

    # GDCM returns char* as type str. Under Python 2 `str` are
    # byte arrays by default. Python 3 decodes this to
    # unicode strings by default.
    # The SWIG docs mention that they always decode byte streams
    # as utf-8 strings for Python 3, with the `surrogateescape`
    # error handler configured.
    # Therefore, we can encode them back to their original bytearray
    # representation on Python 3 by using the same parameters.
    if compat.in_py2:
        pixel_bytearray = gdcm_image.GetBuffer()
    else:
        pixel_bytearray = gdcm_image.GetBuffer().encode(
            "utf-8", "surrogateescape")

    # Here we need to be careful because in some cases, GDCM reads a
    # buffer that is too large, so we need to make sure we only include
    # the first n_rows * n_columns * dtype_size bytes.
    expected_length_bytes = get_expected_length(dicom_dataset)
    if len(pixel_bytearray) > expected_length_bytes:
        # We make sure that all the bytes after are in fact zeros
        padding = pixel_bytearray[expected_length_bytes:]
        if numpy.any(numpy.frombuffer(padding, numpy.byte)):
            pixel_bytearray = pixel_bytearray[:expected_length_bytes]
        else:
            # We revert to the old behavior which should then result
            #   in a Numpy error later on.
            pass

    numpy_dtype = pixel_dtype(dicom_dataset)
    pixel_array = numpy.frombuffer(pixel_bytearray, dtype=numpy_dtype)

    expected_length_pixels = get_expected_length(dicom_dataset, 'pixels')
    if pixel_array.size != expected_length_pixels:
        raise AttributeError("Amount of pixel data %d does "
                             "not match the expected data %d" %
                             (pixel_array.size, expected_length_pixels))

    if should_change_PhotometricInterpretation_to_RGB(dicom_dataset):
        dicom_dataset.PhotometricInterpretation = "RGB"

    return pixel_array.copy()
Exemple #14
0
    def clean(self, fix_interpretation=True, pixel_data_attribute="PixelData"):
        """
        take a dicom image and a list of pixel coordinates, and return
        a cleaned file (if output file is specified) or simply plot
        the cleaned result (if no file is specified)

        Parameters
        ==========
            add_padding: add N=margin pixels of padding
            margin: pixels of padding to add, if add_padding True
            fix_interpretation: fix the photometric interpretation if found off
        """

        if not self.results:
            bot.warning("Use %s.detect() to find coordinates first." % self)

        else:
            bot.info("Scrubbing %s." % self.dicom_file)

            # Load in dicom file, and image data
            dicom = read_file(self.dicom_file, force=True)
            pixel_data = getattr(dicom, pixel_data_attribute)

            # Get expected and actual length of the pixel data (bytes, expected does not include trailing null byte)
            expected_length = get_expected_length(dicom)
            actual_length = len(pixel_data)
            padded_expected_length = expected_length + expected_length % 2
            full_length = expected_length / 2 * 3  # upsampled data is a third larger
            full_length += (
                1 if full_length % 2 else 0
            )  # trailing padding byte if even length

            # If we have YBR_FULL_2, must be RGB to obtain pixel data
            if (
                not dicom.file_meta.TransferSyntaxUID.is_compressed
                and dicom.PhotometricInterpretation == "YBR_FULL_422"
                and fix_interpretation
                and actual_length >= full_length
            ):
                bot.warning(
                    "Updating dicom.PhotometricInterpretation to RGB, set fix_interpretation to False to skip."
                )
                photometric_original = dicom.PhotometricInterpretation
                dicom.PhotometricInterpretation = "RGB"
                self.original = dicom.pixel_array
                dicom.PhotometricInterpretation = photometric_original
            else:
                self.original = dicom.pixel_array

            # Compile coordinates from result, generate list of tuples with coordinate and value
            # keepcoordinates == 1 (included in mask) and coordinates == 0 (remove).
            coordinates = []

            for item in self.results["results"]:

                # We iterate through coordinates in order specified in file
                for coordinate_set in item.get("coordinates", []):

                    # Each is a list with [value, coordinate]
                    mask_value, new_coordinates = coordinate_set

                    if not isinstance(new_coordinates, list):
                        new_coordinates = [new_coordinates]

                    for new_coordinate in new_coordinates:

                        # Case 1: an "all" indicates applying to entire image
                        if new_coordinate.lower() == "all":

                            # no frames, just X, Y
                            if len(self.original.shape) == 2:
                                # minr, minc, maxr, maxc = [0, 0, Y, X]
                                new_coordinate = [
                                    0,
                                    0,
                                    self.original.shape[1],
                                    self.original.shape[0],
                                ]

                            # (frames, X, Y, channel) OR (frames, X,Y)
                            if len(self.original.shape) >= 3:
                                new_coordinate = [
                                    0,
                                    0,
                                    self.original.shape[2],
                                    self.original.shape[1],
                                ]
                        else:
                            new_coordinate = [int(x) for x in new_coordinate.split(",")]
                        coordinates.append(
                            (mask_value, new_coordinate)
                        )  # [(1, [1,2,3,4]),...(0, [1,2,3,4])]

            # Instead of writing directly to data, create a mask of 1s (start keeping all)
            # For 4D, (frames, X, Y, channel)
            if len(self.original.shape) == 4:
                mask = numpy.ones(self.original.shape[1:3], dtype=numpy.uint8)

            # For 2D, (X, Y) or 3D (X, Y channel)
            else:
                mask = numpy.ones(self.original.shape[0:2], dtype=numpy.uint8)

            # Here we apply the coordinates to the mask, 1==keep, 0==clean
            for coordinate_value, coordinate in coordinates:
                minr, minc, maxr, maxc = coordinate

                # Update the mask: values set to 0 to be black
                mask[minc:maxc, minr:maxr] = coordinate_value

            # Now apply finished mask to the data
            if len(self.original.shape) == 4:

                # np.tile does the copying and stacking of masks into the channel dim to produce 3D masks
                # transposition to convert tile output (channel, X, Y)  into (X, Y, channel)
                # see: https://github.com/nquach/anonymize/blob/master/anonymize.py#L154
                channel3mask = numpy.transpose(numpy.tile(mask, (3, 1, 1)), (1, 2, 0))

                # use numpy.tile to copy and stack the 3D masks into 4D array to apply to 4D pixel data
                # tile converts (X, Y, channels) -> (frames, X, Y, channels), presumed ordering for 4D pixel data
                final_mask = numpy.tile(channel3mask, (self.original.shape[0], 1, 1, 1))

                # apply final 4D mask to 4D pixel data
                self.cleaned = final_mask * self.original

            # greyscale: no need to stack into the channel dim since it doesnt exist
            elif len(self.original.shape) == 3:

                # numpy.tile converts (X, Y) -> (frames, X, Y)
                final_mask = numpy.tile(mask, (self.original.shape[0], 1, 1))
                self.cleaned = final_mask * self.original

            elif len(self.original.shape) == 2:
                self.cleaned = mask * self.original

            else:
                bot.warning(
                    "Pixel array dimension %s is not recognized."
                    % (str(self.original.shape))
                )
Exemple #15
0
    def add_segments(
        self,
        pixel_array: np.ndarray,
        segment_descriptions: Sequence[SegmentDescription],
        plane_positions: Optional[Sequence[PlanePositionSequence]] = None
    ) -> None:
        """Adds one or more segments to the segmentation image.

        Parameters
        ----------
        pixel_array: numpy.ndarray
            Array of segmentation pixel data of boolean, unsigned integer or
            floating point data type representing a mask image. If `pixel_array`
            is a floating-point array or a binary array (containing only the
            values ``True`` and ``False`` or ``0`` and ``1``), the segment
            number used to encode the segment is taken from
            `segment_descriptions`.
            Otherwise, if `pixel_array` contains multiple integer values, each
            value is treated as a different segment whose segment number is
            that integer value. In this case, all segments found in the array
            must be described in `segment_descriptions`. Note that this is
            valid for both ``"BINARY"`` and ``"FRACTIONAL"`` segmentations.
            For ``"FRACTIONAL"`` segmentations, values either encode the
            probability of a given pixel belonging to a segment
            (if `fractional_type` is ``"PROBABILITY"``)
            or the extent to which a segment occupies the pixel
            (if `fractional_type` is ``"OCCUPANCY"``).
            When `pixel_array` has a floating point data type, only one segment
            can be encoded. Additional segments can be subsequently
            added to the `Segmentation` instance using the ``add_segments()``
            method.
            If `pixel_array` represents a 3D image, the first dimension
            represents individual 2D planes and these planes must be ordered
            based on their position in the three-dimensional patient
            coordinate system (first along the X axis, second along the Y axis,
            and third along the Z axis).
            If `pixel_array` represents a tiled 2D image, the first dimension
            represents individual 2D tiles (for one channel and z-stack) and
            these tiles must be ordered based on their position in the tiled
            total pixel matrix (first along the row dimension and second along
            the column dimension, which are defined in the three-dimensional
            slide coordinate system by the direction cosines encoded by the
            *Image Orientation (Slide)* attribute).
        segment_descriptions: Sequence[highdicom.seg.content.SegmentDescription]
            Description of each segment encoded in `pixel_array`. In the case of
            pixel arrays with multiple integer values, the segment description
            with the corresponding segment number is used to describe each
            segment.
        plane_positions: Sequence[highdicom.content.PlanePositionSequence], optional
            Position of each plane in `pixel_array` relative to the
            three-dimensional patient or slide coordinate system.

        Raises
        ------
        ValueError
            When
                - The pixel array is not 2D or 3D numpy array
                - The shape of the pixel array does not match the source images
                - The numbering of the segment descriptions is not
                  monotonically increasing by 1
                - The numbering of the segment descriptions does
                  not begin at 1 (for the first segments added to the instance)
                  or at one greater than the last added segment (for
                  subsequent segments)
                - One or more segments already exist within the
                  segmentation instance
                - The segmentation is binary and the pixel array contains
                  integer values that belong to segments that are not described
                  in the segment descriptions
                - The segmentation is binary and pixel array has floating point
                  values not equal to 0.0 or 1.0
                - The segmentation is fractional and pixel array has floating
                  point values outside the range 0.0 to 1.0
                - The segmentation is fractional and pixel array has floating
                  point values outside the range 0.0 to 1.0
                - Plane positions are provided but the length of the array
                  does not match the number of frames in the pixel array
        TypeError
            When the dtype of the pixel array is invalid


        Note
        ----
        Segments must be sorted by segment number in ascending order and
        increase by 1.  Additionally, the first segment description must have a
        segment number one greater than the segment number of the last segment
        added to the segmentation, or 1 if this is the first segment added.

        In case `segmentation_type` is ``"BINARY"``, the number of items in
        `segment_descriptions` must be greater than or equal to the number of
        unique positive pixel values in `pixel_array`. It is possible for some
        segments described in `segment_descriptions` not to appear in the
        `pixel_array`. In case `segmentation_type` is ``"FRACTIONAL"``, only
        one segment can be encoded by `pixel_array` and hence only one item is
        permitted in `segment_descriptions`.

        """  # noqa
        if pixel_array.ndim == 2:
            pixel_array = pixel_array[np.newaxis, ...]
        if pixel_array.ndim != 3:
            raise ValueError('Pixel array must be a 2D or 3D array.')

        if pixel_array.shape[1:3] != (self.Rows, self.Columns):
            raise ValueError(
                'Pixel array representing segments has the wrong number of '
                'rows and columns.')

        # Determine the expected starting number of the segments to ensure
        # they will be continuous with existing segments
        if self._segment_inventory:
            # Next segment number is one greater than the largest existing
            # segment number
            seg_num_start = max(self._segment_inventory) + 1
        else:
            # No existing segments so start at 1
            seg_num_start = 1

        # Check segment numbers
        # Check the existing descriptions
        described_segment_numbers = np.array(
            [int(item.SegmentNumber) for item in segment_descriptions])
        # Check segment numbers in the segment descriptions are
        # monotonically increasing by 1
        if not (np.diff(described_segment_numbers) == 1).all():
            raise ValueError(
                'Segment descriptions must be sorted by segment number '
                'and monotonically increasing by 1.')
        if described_segment_numbers[0] != seg_num_start:
            if seg_num_start == 1:
                msg = ('Segment descriptions should be numbered starting '
                       f'from 1. Found {described_segment_numbers[0]}. ')
            else:
                msg = ('Segment descriptions should be numbered to '
                       'continue from existing segments. Expected the first '
                       f'segment to be numbered {seg_num_start} but found '
                       f'{described_segment_numbers[0]}.')
            raise ValueError(msg)

        if pixel_array.dtype in (np.bool_, np.uint8, np.uint16):
            segments_present = np.unique(pixel_array[pixel_array > 0].astype(
                np.uint16))

            # Special case where the mask is binary and there is a single
            # segment description. Mark the positive segment with
            # the correct segment number
            if (np.array_equal(segments_present, np.array([1]))
                    and len(segment_descriptions) == 1):
                pixel_array = pixel_array.astype(np.uint8)
                pixel_array *= described_segment_numbers.item()

            # Otherwise, the pixel values in the pixel array must all belong to
            # a described segment
            else:
                if not np.all(
                        np.in1d(segments_present, described_segment_numbers)):
                    raise ValueError('Pixel array contains segments that lack '
                                     'descriptions.')

        elif (pixel_array.dtype in (np.float_, np.float32, np.float64)):
            unique_values = np.unique(pixel_array)
            if np.min(unique_values) < 0.0 or np.max(unique_values) > 1.0:
                raise ValueError(
                    'Floating point pixel array values must be in the '
                    'range [0, 1].')
            if len(segment_descriptions) != 1:
                raise ValueError(
                    'When providing a float-valued pixel array, provide only '
                    'a single segment description')
            if self.SegmentationType == SegmentationTypeValues.BINARY.value:
                non_boolean_values = np.logical_and(unique_values > 0.0,
                                                    unique_values < 1.0)
                if np.any(non_boolean_values):
                    raise ValueError(
                        'Floating point pixel array values must be either '
                        '0.0 or 1.0 in case of BINARY segmentation type.')
                pixel_array = pixel_array.astype(np.bool_)
        else:
            raise TypeError('Pixel array has an invalid data type.')

        # Check that the new segments do not already exist
        if len(set(described_segment_numbers) & self._segment_inventory) > 0:
            raise ValueError(
                'Segment with given segment number already exists')

        # Set the optional tag value SegmentsOverlapValues to NO to indicate
        # that the segments do not overlap. We can know this for sure if it's
        # the first segment (or set of segments) to be added because they are
        # contained within a single pixel array.
        if len(self._segment_inventory) == 0:
            self.SegmentsOverlap = SegmentsOverlapValues.NO.value
        else:
            # If this is not the first set of segments to be added, we cannot
            # be sure whether there is overlap with the existing segments
            self.SegmentsOverlap = SegmentsOverlapValues.UNDEFINED.value

        src_image = self._source_images[0]
        is_multiframe = hasattr(src_image, 'NumberOfFrames')
        if is_multiframe:
            source_plane_positions = \
                self.DimensionIndexSequence.get_plane_positions_of_image(
                    src_image
                )
        else:
            source_plane_positions = \
                self.DimensionIndexSequence.get_plane_positions_of_series(
                    self._source_images
                )

        if plane_positions is None:
            if pixel_array.shape[0] != len(source_plane_positions):
                if is_multiframe:
                    raise ValueError(
                        'Number of frames in pixel array does not match number '
                        ' of frames in source image.')
                else:
                    raise ValueError(
                        'Number of frames in pixel array does not match number '
                        'of source images.')
            plane_positions = source_plane_positions
        else:
            if pixel_array.shape[0] != len(plane_positions):
                raise ValueError(
                    'Number of pixel array planes does not match number of '
                    'provided plane positions.')

        plane_position_values, plane_sort_index = \
            self.DimensionIndexSequence.get_index_values(plane_positions)

        are_spatial_locations_preserved = (
            all(plane_positions[i] == source_plane_positions[i]
                for i in range(len(plane_positions)))
            and self._plane_orientation == self._source_plane_orientation)

        # Get unique values of attributes in the Plane Position Sequence or
        # Plane Position Slide Sequence, which define the position of the plane
        # with respect to the three dimensional patient or slide coordinate
        # system, respectively. These can subsequently be used to look up the
        # relative position of a plane relative to the indexed dimension.
        dimension_position_values = [
            np.unique(plane_position_values[:, index], axis=0)
            for index in range(plane_position_values.shape[1])
        ]

        # In certain circumstances, we can add new pixels without unpacking the
        # previous ones, which is more efficient. This can be done when using
        # non-encapsulated transfer syntaxes when there is no padding required
        # for each frame to be a multiple of 8 bits.
        framewise_encoding = False
        is_encaps = self.file_meta.TransferSyntaxUID.is_encapsulated
        if not is_encaps:
            if self.SegmentationType == SegmentationTypeValues.FRACTIONAL.value:
                framewise_encoding = True
            elif self.SegmentationType == SegmentationTypeValues.BINARY.value:
                # Framewise encoding can only be used if there is no padding
                # This requires the number of pixels in each frame to be
                # multiple of 8
                if (self.Rows * self.Columns * self.SamplesPerPixel) % 8 == 0:
                    framewise_encoding = True
                else:
                    logger.warning(
                        'pixel data needs to be re-encoded for binary '
                        'bitpacking - consider using FRACTIONAL instead of '
                        'BINARY segmentation type')

        if framewise_encoding:
            # Before adding new pixel data, remove trailing null padding byte
            if len(self.PixelData) == get_expected_length(self) + 1:
                self.PixelData = self.PixelData[:-1]
        else:
            # In the case of encapsulated transfer syntaxes, we will accumulate
            # a list of encoded frames to re-encapsulate at the end
            if is_encaps:
                if hasattr(self, 'PixelData') and len(self.PixelData) > 0:
                    # Undo the encapsulation but not the encoding within each
                    # frame
                    full_frames_list = decode_data_sequence(self.PixelData)
                else:
                    full_frames_list = []
            else:
                if hasattr(self, 'PixelData') and len(self.PixelData) > 0:
                    full_pixel_array = self.pixel_array.flatten()
                else:
                    full_pixel_array = np.array([], np.bool_)

        for i, segment_number in enumerate(described_segment_numbers):
            if pixel_array.dtype in (np.float_, np.float32, np.float64):
                # Floating-point numbers must be mapped to 8-bit integers in
                # the range [0, max_fractional_value].
                planes = np.around(pixel_array *
                                   float(self.MaximumFractionalValue))
                planes = planes.astype(np.uint8)
            elif pixel_array.dtype in (np.uint8, np.uint16):
                # Labeled masks must be converted to binary masks.
                planes = np.zeros(pixel_array.shape, dtype=np.bool_)
                planes[pixel_array == segment_number] = True
            elif pixel_array.dtype == np.bool_:
                planes = pixel_array
            else:
                raise TypeError('Pixel array has an invalid data type.')

            contained_plane_index = []
            for j in plane_sort_index:
                if np.sum(planes[j]) == 0:
                    logger.info('skip empty plane {} of segment #{}'.format(
                        j, segment_number))
                    continue
                contained_plane_index.append(j)
                logger.info('add plane #{} for segment #{}'.format(
                    j, segment_number))

                pffp_item = Dataset()
                frame_content_item = Dataset()
                frame_content_item.DimensionIndexValues = [segment_number]

                # Look up the position of the plane relative to the indexed
                # dimension.
                try:
                    if self._coordinate_system == CoordinateSystemNames.SLIDE:
                        index_values = [
                            np.where((dimension_position_values[idx]
                                      == pos))[0][0] + 1
                            for idx, pos in enumerate(plane_position_values[j])
                        ]
                    else:
                        # In case of the patient coordinate system, the
                        # value of the attribute the Dimension Index Sequence
                        # points to (Image Position Patient) has a value
                        # multiplicity greater than one.
                        index_values = [
                            np.where((dimension_position_values[idx]
                                      == pos).all(axis=1))[0][0] + 1
                            for idx, pos in enumerate(plane_position_values[j])
                        ]
                except IndexError as error:
                    raise IndexError(
                        'Could not determine position of plane #{} in '
                        'three dimensional coordinate system based on '
                        'dimension index values: {}'.format(j, error))
                frame_content_item.DimensionIndexValues.extend(index_values)
                pffp_item.FrameContentSequence = [frame_content_item]
                if self._coordinate_system == CoordinateSystemNames.SLIDE:
                    pffp_item.PlanePositionSlideSequence = plane_positions[j]
                else:
                    pffp_item.PlanePositionSequence = plane_positions[j]

                # Determining the source images that map to the frame is not
                # always trivial. Since DerivationImageSequence is a type 2
                # attribute, we leave its value empty.
                pffp_item.DerivationImageSequence = []

                if are_spatial_locations_preserved:
                    derivation_image_item = Dataset()
                    derivation_code = codes.cid7203.Segmentation
                    derivation_image_item.DerivationCodeSequence = [
                        CodedConcept(derivation_code.value,
                                     derivation_code.scheme_designator,
                                     derivation_code.meaning,
                                     derivation_code.scheme_version),
                    ]

                    derivation_src_img_item = Dataset()
                    if len(plane_sort_index) > len(self._source_images):
                        # A single multi-frame source image
                        src_img_item = self.SourceImageSequence[0]
                        # Frame numbers are one-based
                        derivation_src_img_item.ReferencedFrameNumber = j + 1
                    else:
                        # Multiple single-frame source images
                        src_img_item = self.SourceImageSequence[j]
                    derivation_src_img_item.ReferencedSOPClassUID = \
                        src_img_item.ReferencedSOPClassUID
                    derivation_src_img_item.ReferencedSOPInstanceUID = \
                        src_img_item.ReferencedSOPInstanceUID
                    purpose_code = \
                        codes.cid7202.SourceImageForImageProcessingOperation
                    derivation_src_img_item.PurposeOfReferenceCodeSequence = [
                        CodedConcept(purpose_code.value,
                                     purpose_code.scheme_designator,
                                     purpose_code.meaning,
                                     purpose_code.scheme_version),
                    ]
                    derivation_src_img_item.SpatialLocationsPreserved = 'YES'
                    derivation_image_item.SourceImageSequence = [
                        derivation_src_img_item,
                    ]
                    pffp_item.DerivationImageSequence.append(
                        derivation_image_item)
                else:
                    logger.warning('spatial locations not preserved')

                identification = Dataset()
                identification.ReferencedSegmentNumber = segment_number
                pffp_item.SegmentIdentificationSequence = [
                    identification,
                ]
                self.PerFrameFunctionalGroupsSequence.append(pffp_item)
                self.NumberOfFrames += 1

            if framewise_encoding:
                # Straightforward concatenation of the binary data
                self.PixelData += self._encode_pixels(
                    planes[contained_plane_index])
            else:
                if is_encaps:
                    # Encode this frame and add to the list for encapsulation
                    # at the end
                    for f in contained_plane_index:
                        full_frames_list.append(self._encode_pixels(planes[f]))
                else:
                    # Concatenate the 1D array for re-encoding at the end
                    full_pixel_array = np.concatenate([
                        full_pixel_array,
                        planes[contained_plane_index].flatten()
                    ])

            # In case of a tiled Total Pixel Matrix pixel data for the same
            # segment may be added.
            if segment_number not in self._segment_inventory:
                self.SegmentSequence.append(segment_descriptions[i])
            self._segment_inventory.add(segment_number)

        # Re-encode the whole pixel array at once if necessary
        if not framewise_encoding:
            if is_encaps:
                self.PixelData = encapsulate(full_frames_list)
            else:
                self.PixelData = self._encode_pixels(full_pixel_array)

        # Add back the null trailing byte if required
        if len(self.PixelData) % 2 == 1:
            self.PixelData += b'0'
def get_pixeldata(ds):
    """Return a :class:`numpy.ndarray` of the pixel data.

    Parameters
    ----------
    ds : Dataset
        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.

    Returns
    -------
    np.ndarray
        The contents of (7FE0,0010) *Pixel Data* as a 1D array.
    """
    tsyntax = ds.file_meta.TransferSyntaxUID
    # The check of transfer syntax must be first
    if tsyntax not in SUPPORTED_TRANSFER_SYNTAXES:
        raise NotImplementedError(
            "Unable to convert the pixel data as the transfer syntax "
            "is not supported by the pylibjpeg pixel data handler."
        )

    # Check required elements
    required_elements = [
        'BitsAllocated', 'Rows', 'Columns', 'PixelRepresentation',
        'SamplesPerPixel', 'PhotometricInterpretation', 'PixelData',
    ]
    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)
        )

    # 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)
    if ds.PhotometricInterpretation == 'YBR_FULL_422':
        # libjpeg has already resampled the pixel data, see PS3.3 C.7.6.3.1.2
        expected_len = expected_len // 2 * 3

    p_interp = ds.PhotometricInterpretation

    # How long each frame is in bytes
    nr_frames = getattr(ds, 'NumberOfFrames', 1)
    frame_len = expected_len // nr_frames

    # The decoded data will be placed here
    arr = np.empty(expected_len, np.uint8)

    # Generators for the encoded JPG image frame(s) and insertion offsets
    generate_frames = generate_pixel_data_frame(ds.PixelData, nr_frames)
    generate_offsets = range(0, expected_len, frame_len)
    for frame, offset in zip(generate_frames, generate_offsets):
        # Encoded JPG data to be sent to the decoder
        frame = np.frombuffer(frame, np.uint8)
        arr[offset:offset + frame_len] = decode_pixel_data(
            frame, ds.group_dataset(0x0028)
        )

    return arr.view(pixel_dtype(ds))
Exemple #17
0
def get_pixeldata(ds, read_only=False):
    """Return an ndarray of the Pixel Data.

    Parameters
    ----------
    ds : dataset.Dataset
        The DICOM dataset containing an Image Pixel module and the Pixel Data
        to be converted.
    read_only : bool, optional
        If False (default) then returns a writeable array that no longer uses
        the original memory. If True and the value of (0028,0100) *Bits
        Allocated* > 1 then returns a read-only array that uses the original
        memory buffer of the pixel data. If *Bits Allocated* = 1 then always
        returns a writeable array.

    Returns
    -------
    np.ndarray
        The contents of the Pixel Data element (7FE0,0010) as a 1D array.

    Raises
    ------
    AttributeError
        If the dataset is missing a required element.
    NotImplementedError
        If the dataset contains pixel data in an unsupported format.
    ValueError
        If the actual length of the pixel data doesn't match the expected
        length.
    """
    transfer_syntax = ds.file_meta.TransferSyntaxUID
    # The check of transfer syntax must be first
    if transfer_syntax not in SUPPORTED_TRANSFER_SYNTAXES:
        raise NotImplementedError(
            "Unable to convert the pixel data as the transfer syntax "
            "is not supported by the numpy pixel data handler."
        )

    # Check required elements
    required_elements = ['PixelData', 'BitsAllocated', 'Rows', 'Columns',
                         'PixelRepresentation', 'SamplesPerPixel']
    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)
        )

    # 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)
    # 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:
        raise ValueError(
            "The length of the pixel data in the dataset doesn't match the "
            "expected amount ({0} vs. {1} bytes). The dataset may be "
            "corrupted or there may be an issue with the pixel data handler."
            .format(actual_length, padded_expected_len)
        )
    elif actual_length > padded_expected_len:
        # PS 3.5, Section 8.1.1
        msg = (
            "The length of the pixel data in the dataset ({} bytes) indicates "
            "it contains excess padding. {} bytes will be removed from the "
            "end of the data"
            .format(actual_length, actual_length - expected_len)
        )
        warnings.warn(msg)

    # Unpack the pixel data into a 1D ndarray
    if ds.BitsAllocated == 1:
        # Skip any trailing padding bits
        nr_pixels = get_expected_length(ds, unit='pixels')
        arr = unpack_bits(ds.PixelData)[:nr_pixels]
    else:
        # Skip the trailing padding byte if present
        arr = np.frombuffer(ds.PixelData[:expected_len],
                            dtype=pixel_dtype(ds))

    if should_change_PhotometricInterpretation_to_RGB(ds):
        ds.PhotometricInterpretation = "RGB"

    if not read_only and ds.BitsAllocated > 1:
        return arr.copy()

    return arr
Exemple #18
0
def get_pixeldata(dicom_dataset):
    """
    Use the GDCM package to decode the PixelData attribute

    Returns
    -------
    numpy.ndarray

        A correctly sized (but not shaped) numpy array
        of the entire data volume

    Raises
    ------
    ImportError
        if the required packages are not available

    TypeError
        if the image could not be read by GDCM
        if the pixel data type is unsupported

    AttributeError
        if the decoded amount of data does not match the expected amount
    """

    if not HAVE_GDCM:
        msg = ("GDCM requires both the gdcm package and numpy "
               "and one or more could not be imported")
        raise ImportError(msg)

    if HAVE_GDCM_IN_MEMORY_SUPPORT:
        gdcm_data_element = create_data_element(dicom_dataset)
        gdcm_image = create_image(dicom_dataset, gdcm_data_element)
    else:
        gdcm_image_reader = create_image_reader(dicom_dataset.filename)
        if not gdcm_image_reader.Read():
            raise TypeError("GDCM could not read DICOM image")
        gdcm_image = gdcm_image_reader.GetImage()

    # GDCM returns char* as type str. Under Python 2 `str` are
    # byte arrays by default. Python 3 decodes this to
    # unicode strings by default.
    # The SWIG docs mention that they always decode byte streams
    # as utf-8 strings for Python 3, with the `surrogateescape`
    # error handler configured.
    # Therefore, we can encode them back to their original bytearray
    # representation on Python 3 by using the same parameters.
    if compat.in_py2:
        pixel_bytearray = gdcm_image.GetBuffer()
    else:
        pixel_bytearray = gdcm_image.GetBuffer().encode(
            "utf-8", "surrogateescape")

    # Here we need to be careful because in some cases, GDCM reads a
    # buffer that is too large, so we need to make sure we only include
    # the first n_rows * n_columns * dtype_size bytes.
    expected_length_bytes = get_expected_length(dicom_dataset)
    if len(pixel_bytearray) > expected_length_bytes:
        # We make sure that all the bytes after are in fact zeros
        padding = pixel_bytearray[expected_length_bytes:]
        if numpy.any(numpy.frombuffer(padding, numpy.byte)):
            pixel_bytearray = pixel_bytearray[:expected_length_bytes]
        else:
            # We revert to the old behavior which should then result
            #   in a Numpy error later on.
            pass

    numpy_dtype = pixel_dtype(dicom_dataset)
    pixel_array = numpy.frombuffer(pixel_bytearray, dtype=numpy_dtype)

    expected_length_pixels = get_expected_length(dicom_dataset, 'pixels')
    if pixel_array.size != expected_length_pixels:
        raise AttributeError("Amount of pixel data %d does "
                             "not match the expected data %d" %
                             (pixel_array.size, expected_length_pixels))

    if should_change_PhotometricInterpretation_to_RGB(dicom_dataset):
        dicom_dataset.PhotometricInterpretation = "RGB"

    return pixel_array.copy()
Exemple #19
0
    def clean(self, fix_interpretation=True, pixel_data_attribute="PixelData"):
        """
        take a dicom image and a list of pixel coordinates, and return
        a cleaned file (if output file is specified) or simply plot 
        the cleaned result (if no file is specified)
    
        Parameters
        ==========
            add_padding: add N=margin pixels of padding
            margin: pixels of padding to add, if add_padding True
            fix_interpretation: fix the photometric interpretation if found off
        """

        if not self.results:
            bot.warning("Use %s.detect() to find coordinates first." % self)

        else:
            bot.info("Scrubbing %s." % self.dicom_file)

            # Load in dicom file, and image data
            dicom = read_file(self.dicom_file, force=True)
            pixel_data = getattr(dicom, pixel_data_attribute)

            # Get expected and actual length of the pixel data (bytes, expected does not include trailing null byte)
            expected_length = get_expected_length(dicom)
            actual_length = len(pixel_data)
            padded_expected_length = expected_length + expected_length % 2
            full_length = expected_length / 2 * 3  # upsampled data is a third larger
            full_length += (1 if full_length % 2 else 0
                            )  # trailing padding byte if even length

            # If we have YBR_FULL_2, must be RGB to obtain pixel data
            if (not dicom.file_meta.TransferSyntaxUID.is_compressed
                    and dicom.PhotometricInterpretation == "YBR_FULL_422"
                    and fix_interpretation and actual_length >= full_length):
                bot.warning(
                    "Updating dicom.PhotometricInterpretation to RGB, set fix_interpretation to False to skip."
                )
                photometric_original = dicom.PhotometricInterpretation
                dicom.PhotometricInterpretation = "RGB"
                self.original = dicom.pixel_array
                dicom.PhotometricInterpretation = photometric_original
            else:
                self.original = dicom.pixel_array

            # Compile coordinates from result
            coordinates = []
            for item in self.results["results"]:
                if len(item["coordinates"]) > 0:
                    for coordinate_set in item["coordinates"]:
                        # Coordinates expected to be list separated by commas
                        new_coordinates = [
                            int(x) for x in coordinate_set.split(",")
                        ]
                        coordinates.append(
                            new_coordinates)  # [[1,2,3,4],...[1,2,3,4]]

            # Instead of writing directly to data, create a mask
            # For 4D, (frames, X, Y, channel)
            if len(self.original.shape) == 4:
                mask = numpy.zeros(self.original.shape[1:3], dtype=numpy.uint8)

            # For 3D, (X, Y, channel)
            else:
                mask = numpy.zeros(self.original.shape[0:2], dtype=numpy.uint8)

            for coordinate in coordinates:
                minr, minc, maxr, maxc = coordinate

                # Update the mask: values set to 0 to be black
                mask[minc:maxc, minr:maxr] = 1

            # Now apply finished mask to the data
            if len(self.original.shape) == 4:

                # np.tile does the copying and stacking of masks into the channel dim to produce 3D masks
                # transposition to convert tile output (channel, X, Y)  into (X, Y, channel)
                # see: https://github.com/nquach/anonymize/blob/master/anonymize.py#L154
                channel3mask = numpy.transpose(numpy.tile(mask, (3, 1, 1)),
                                               (1, 2, 0))

                # use numpy.tile to copy and stack the 3D masks into 4D array to apply to 4D pixel data
                # tile converts (X, Y, channels) -> (frames, X, Y, channels), presumed ordering for 4D pixel data
                final_mask = numpy.tile(channel3mask,
                                        (self.original.shape[0], 1, 1, 1))

                # apply final 4D mask to 4D pixel data
                self.cleaned = final_mask * self.original

            # greyscale: no need to stack into the channel dim since it doesnt exist
            elif len(self.original.shape) == 3:

                # numpy.tile converts (X, Y) -> (frames, X, Y)
                final_mask = numpy.tile(mask, (self.original.shape[0], 1, 1))
                self.cleaned = final_mask * self.original

            else:
                bot.warning("Pixel array dimension %s is not recognized." %
                            (self.original.shape))
def get_pixeldata(ds, read_only=False):
    """Return a :class:`numpy.ndarray` of the pixel data.

    .. versionchanged:: 1.4

        * Added support for uncompressed pixel data with a *Photometric
          Interpretation* of ``YBR_FULL_422``.
        * Added support for *Float Pixel Data* and *Double Float Pixel Data*


    Parameters
    ----------
    ds : Dataset
        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
        If ``False`` (default) then returns a writeable array that no longer
        uses the original memory. If ``True`` and the value of (0028,0100)
        *Bits Allocated* > 1 then returns a read-only array that uses the
        original memory buffer of the pixel data. If *Bits Allocated* = 1 then
        always returns a writeable array.

    Returns
    -------
    np.ndarray
        The contents of (7FE0,0010) *Pixel Data*, (7FE0,0008) *Float Pixel
        Data* or (7FE0,0009) *Double Float Pixel Data* as a 1D array.

    Raises
    ------
    AttributeError
        If `ds` is missing a required element.
    NotImplementedError
        If `ds` contains pixel data in an unsupported format.
    ValueError
        If the actual length of the pixel data doesn't match the expected
        length.
    """
    transfer_syntax = ds.file_meta.TransferSyntaxUID
    # The check of transfer syntax must be first
    if transfer_syntax not in SUPPORTED_TRANSFER_SYNTAXES:
        raise NotImplementedError(
            "Unable to convert the pixel data as the transfer syntax "
            "is not supported by the numpy pixel data handler.")

    # 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 = [
        'BitsAllocated', 'Rows', 'Columns', 'PixelRepresentation',
        'SamplesPerPixel', 'PhotometricInterpretation'
    ]
    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))

    # 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(pixel_data)

    # 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 odd length pixel data is missing a trailing padding byte")
        else:
            raise ValueError(
                "The length of the pixel data in the dataset ({} bytes) "
                "doesn't match the expected length ({} bytes). "
                "The dataset may be corrupted or there may be an issue "
                "with the pixel data handler.".format(actual_length,
                                                      padded_expected_len))
    elif actual_length > padded_expected_len:
        # PS 3.5, Section 8.1.1
        msg = (
            "The length of the pixel data in the dataset ({} bytes) indicates "
            "it contains excess padding. {} bytes will be removed from the "
            "end of the data".format(actual_length,
                                     actual_length - expected_len))
        # PS 3.3, Annex C.7.6.3
        if ds.PhotometricInterpretation == 'YBR_FULL_422':
            # Check to ensure we do have subsampled YBR 422 data
            ybr_full_length = expected_len / 2 * 3 + expected_len / 2 * 3 % 2
            # >= as may also include excess padding
            if actual_length >= ybr_full_length:
                msg = ("The Photometric Interpretation of the dataset is "
                       "YBR_FULL_422, however the length of the pixel data "
                       "({} bytes) is a third larger than expected ({} bytes) "
                       "which indicates that this may be incorrect. You may "
                       "need to change the Photometric Interpretation to "
                       "the correct value.".format(actual_length,
                                                   expected_len))
        warnings.warn(msg)

    # Unpack the pixel data into a 1D ndarray
    if ds.BitsAllocated == 1:
        # Skip any trailing padding bits
        nr_pixels = get_expected_length(ds, unit='pixels')
        arr = unpack_bits(pixel_data)[:nr_pixels]
    else:
        # Skip the trailing padding byte(s) if present
        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=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
            out[2::6], out[5::6] = arr[3::4], arr[3::4]  # R
            arr = out

    if should_change_PhotometricInterpretation_to_RGB(ds):
        ds.PhotometricInterpretation = "RGB"

    if not read_only and ds.BitsAllocated > 1:
        return arr.copy()

    return arr
Exemple #21
0
def get_pixeldata(ds, read_only=False):
    """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.
    read_only : bool, optional
        If ``False`` (default) then returns a writeable array that no longer
        uses the original memory. If ``True`` and the value of (0028,0100)
        *Bits Allocated* > 1 then returns a read-only array that uses the
        original memory buffer of the pixel data. If *Bits Allocated* = 1 then
        always returns a writeable array.

    Returns
    -------
    np.ndarray
        The contents of (7FE0,0010) *Pixel Data* as a 1D array.

    Raises
    ------
    AttributeError
        If `ds` is missing a required element.
    NotImplementedError
        If `ds` contains pixel data in an unsupported format.
    ValueError
        If the actual length of the pixel data doesn't match the expected
        length.
    """
    transfer_syntax = ds.file_meta.TransferSyntaxUID
    # The check of transfer syntax must be first
    if transfer_syntax not in SUPPORTED_TRANSFER_SYNTAXES:
        raise NotImplementedError(
            "Unable to convert the pixel data as the transfer syntax "
            "is not supported by the numpy pixel data handler."
        )

    # Check required elements
    required_elements = ['PixelData', 'BitsAllocated', 'Rows', 'Columns',
                         'PixelRepresentation', 'SamplesPerPixel']
    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)
        )

    # 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)
    # 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 pixel data length is odd and misses a padding byte.")
        else:
            raise ValueError(
                "The length of the pixel data in the dataset ({} bytes) "
                "doesn't match the expected length ({} bytes). "
                "The dataset may be corrupted or there may be an issue "
                "with the pixel data handler."
                .format(actual_length, padded_expected_len)
            )
    elif actual_length > padded_expected_len:
        # PS 3.5, Section 8.1.1
        msg = (
            "The length of the pixel data in the dataset ({} bytes) indicates "
            "it contains excess padding. {} bytes will be removed from the "
            "end of the data"
            .format(actual_length, actual_length - expected_len)
        )
        warnings.warn(msg)

    # Unpack the pixel data into a 1D ndarray
    if ds.BitsAllocated == 1:
        # Skip any trailing padding bits
        nr_pixels = get_expected_length(ds, unit='pixels')
        arr = unpack_bits(ds.PixelData)[:nr_pixels]
    else:
        # Skip the trailing padding byte if present
        arr = np.frombuffer(ds.PixelData[:expected_len],
                            dtype=pixel_dtype(ds))

    if should_change_PhotometricInterpretation_to_RGB(ds):
        ds.PhotometricInterpretation = "RGB"

    if not read_only and ds.BitsAllocated > 1:
        return arr.copy()

    return arr