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]
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)
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)
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()