示例#1
0
    def test_pixel_rep_mismatch(self):
        """Test mismatched j2k sign and Pixel Representation."""
        ds = dcmread(J2KR_16_13_1_1_1F_M2_MISMATCH)
        assert 1 == ds.PixelRepresentation
        assert 13 == ds.BitsStored

        bs = defragment_data(ds.PixelData)
        params = get_j2k_parameters(bs)
        assert 13 == params["precision"]
        assert not params["is_signed"]

        msg = r"value '1' \(signed\)"
        with pytest.warns(UserWarning, match=msg):
            arr = ds.pixel_array

        assert 'int16' == arr.dtype
        assert (512, 512) == arr.shape
        assert arr.flags.writeable

        assert -2000 == arr[0, 0]
        assert [621, 412, 138, -193, -520, -767, -907, -966, -988, -995] == (
            arr[47:57, 279].tolist()
        )
        assert [-377, -121, 141, 383, 633, 910, 1198, 1455, 1638, 1732] == (
            arr[328:338, 106].tolist()
        )
    def test_properties(self, fpath, data):
        """Test dataset and pixel array properties are as expected."""
        if data[0] not in JPEG2K_SUPPORTED_SYNTAXES:
            return

        ds = dcmread(fpath)
        assert ds.file_meta.TransferSyntaxUID == data[0]
        assert ds.BitsAllocated == data[1]
        assert ds.SamplesPerPixel == data[2]
        assert ds.PixelRepresentation == data[3]
        assert getattr(ds, 'NumberOfFrames', 1) == data[4]

        bs = defragment_data(ds.PixelData)
        if get_j2k_parameters(bs)["precision"] != ds.BitsStored:
            with pytest.warns(UserWarning, match=r"doesn't match the JPEG 20"):
                arr = ds.pixel_array
        else:
            arr = ds.pixel_array

        assert arr.flags.writeable
        assert data[5] == arr.shape
        assert arr.dtype == data[6]
示例#3
0
def generate_frames(ds: "Dataset",
                    reshape: bool = True) -> Iterable["np.ndarray"]:
    """Yield a *Pixel Data* frame from `ds` as an :class:`~numpy.ndarray`.

    .. 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.
    reshape : bool, optional
        If ``True`` (default), then the returned :class:`~numpy.ndarray` will
        be reshaped to the correct dimensions. If ``False`` then no reshaping
        will be performed.

    Yields
    -------
    numpy.ndarray
        A single frame of (7FE0,0010) *Pixel Data* as an
        :class:`~numpy.ndarray` with an appropriate dtype for the data.

    Raises
    ------
    AttributeError
        If `ds` is missing a required element.
    RuntimeError
        If the plugin required to decode the pixel data is not installed.
    """
    tsyntax = ds.file_meta.TransferSyntaxUID
    # The check of transfer syntax must be first
    if tsyntax not in _DECODERS:
        if tsyntax in _OPENJPEG_SYNTAXES:
            plugin = "pylibjpeg-openjpeg"
        elif tsyntax in _LIBJPEG_SYNTAXES:
            plugin = "pylibjpeg-libjpeg"
        else:
            plugin = "pylibjpeg-rle"

        raise RuntimeError(
            f"Unable to convert the Pixel Data as the '{plugin}' plugin is "
            f"not installed")

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

    decoder = _DECODERS[tsyntax]
    LOGGER.debug(f"Decoding {tsyntax.name} encoded Pixel Data using {decoder}")

    nr_frames = getattr(ds, "NumberOfFrames", 1)
    pixel_module = ds.group_dataset(0x0028)
    dtype = pixel_dtype(ds)

    bits_stored = cast(int, ds.BitsStored)
    bits_allocated = cast(int, ds.BitsAllocated)

    for frame in generate_pixel_data_frame(ds.PixelData, nr_frames):
        arr = decoder(frame, pixel_module)

        if (tsyntax in [JPEG2000, JPEG2000Lossless]
                and config.APPLY_J2K_CORRECTIONS):
            param = get_j2k_parameters(frame)
            j2k_sign = param.setdefault('is_signed', True)
            j2k_precision = cast(int, param.setdefault('precision',
                                                       bits_stored))
            shift = bits_allocated - j2k_precision
            if shift and not j2k_sign and j2k_sign != ds.PixelRepresentation:
                # Convert unsigned J2K data to 2s complement
                # Can only get here if parsed J2K codestream OK
                pixel_module.PixelRepresentation = 0
                arr = arr.view(pixel_dtype(pixel_module))
                arr = np.left_shift(arr, shift)
                arr = arr.astype(dtype)
                arr = np.right_shift(arr, shift)

        if arr.dtype != dtype:
            # Re-view as pylibjpeg returns a 1D uint8 ndarray
            arr = arr.view(dtype)

        if not reshape:
            yield arr
            continue

        if ds.SamplesPerPixel == 1:
            yield arr.reshape(ds.Rows, ds.Columns)
        else:
            if tsyntax == RLELossless:
                # RLE Lossless is Planar Configuration 1
                arr = arr.reshape(ds.SamplesPerPixel, ds.Rows, ds.Columns)
                yield arr.transpose(1, 2, 0)
            else:
                # JPEG, JPEG-LS and JPEG 2000 are all Planar Configuration 0
                yield arr.reshape(ds.Rows, ds.Columns, ds.SamplesPerPixel)
示例#4
0
def get_pixeldata(ds: "Dataset") -> "numpy.ndarray":
    """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 decompressed and returned.

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

    Raises
    ------
    ImportError
        If Pillow is not available.
    NotImplementedError
        If the transfer syntax is not supported
    """
    transfer_syntax = ds.file_meta.TransferSyntaxUID

    if not HAVE_PIL:
        raise ImportError(
            f"The pillow package is required to use pixel_array for "
            f"this transfer syntax {transfer_syntax.name}, and pillow could "
            f"not be imported.")

    if not HAVE_JPEG and transfer_syntax in PillowJPEGTransferSyntaxes:
        raise NotImplementedError(
            f"The pixel data with transfer syntax {transfer_syntax.name}, "
            f"cannot be read because Pillow lacks the JPEG plugin")

    if not HAVE_JPEG2K and transfer_syntax in PillowJPEG2000TransferSyntaxes:
        raise NotImplementedError(
            f"The pixel data with transfer syntax {transfer_syntax.name}, "
            f"cannot be read because Pillow lacks the JPEG 2000 plugin")

    if transfer_syntax == JPEGExtended12Bit and ds.BitsAllocated != 8:
        raise NotImplementedError(
            f"{JPEGExtended12Bit} - {JPEGExtended12Bit.name} only supported "
            "by Pillow if Bits Allocated = 8")

    photometric_interpretation = cast(str, ds.PhotometricInterpretation)
    rows = cast(int, ds.Rows)
    columns = cast(int, ds.Columns)
    bits_stored = cast(int, ds.BitsStored)
    bits_allocated = cast(int, ds.BitsAllocated)
    nr_frames = getattr(ds, 'NumberOfFrames', 1) or 1

    pixel_bytes = bytearray()
    if nr_frames > 1:
        j2k_precision, j2k_sign = None, None
        # multiple compressed frames
        for frame in decode_data_sequence(ds.PixelData):
            im = _decompress_single_frame(frame, transfer_syntax,
                                          photometric_interpretation)
            if 'YBR' in photometric_interpretation:
                im.draft('YCbCr', (rows, columns))
            pixel_bytes.extend(im.tobytes())

            if not j2k_precision:
                params = get_j2k_parameters(frame)
                j2k_precision = cast(
                    int, params.setdefault("precision", bits_stored))
                j2k_sign = params.setdefault("is_signed", None)

    else:
        # single compressed frame
        pixel_data = defragment_data(ds.PixelData)
        im = _decompress_single_frame(pixel_data, transfer_syntax,
                                      photometric_interpretation)
        if 'YBR' in photometric_interpretation:
            im.draft('YCbCr', (rows, columns))
        pixel_bytes.extend(im.tobytes())

        params = get_j2k_parameters(pixel_data)
        j2k_precision = cast(int, params.setdefault("precision", bits_stored))
        j2k_sign = params.setdefault("is_signed", None)

    logger.debug(f"Successfully read {len(pixel_bytes)} pixel bytes")

    arr = numpy.frombuffer(pixel_bytes, pixel_dtype(ds))

    if transfer_syntax in PillowJPEG2000TransferSyntaxes:
        # Pillow converts N-bit data to 8- or 16-bit unsigned data,
        # See Pillow src/libImaging/Jpeg2KDecode.c::j2ku_gray_i
        shift = bits_allocated - bits_stored
        if j2k_precision and j2k_precision != bits_stored:
            warnings.warn(
                f"The (0028,0101) 'Bits Stored' value ({bits_stored}-bit) "
                f"doesn't match the JPEG 2000 data ({j2k_precision}-bit). "
                f"It's recommended that you change the 'Bits Stored' value")

        if config.APPLY_J2K_CORRECTIONS and j2k_precision:
            # Corrections based on J2K data
            shift = bits_allocated - j2k_precision
            if not j2k_sign and j2k_sign != ds.PixelRepresentation:
                # Convert unsigned J2K data to 2's complement
                arr = numpy.right_shift(arr, shift)
            else:
                if ds.PixelRepresentation == 1:
                    # Pillow converts signed data to unsigned
                    #   so we need to undo this conversion
                    arr -= 2**(bits_allocated - 1)

                if shift:
                    arr = numpy.right_shift(arr, shift)
        else:
            # Corrections based on dataset elements
            if ds.PixelRepresentation == 1:
                arr -= 2**(bits_allocated - 1)

            if shift:
                arr = numpy.right_shift(arr, shift)

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

    return cast("numpy.ndarray", arr)
示例#5
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()