def test_unsupported_dtypes(self): """Test unsupported dtypes raise exception.""" self.ds.BitsAllocated = 24 self.ds.PixelRepresentation = 0 with pytest.raises(NotImplementedError, match="data type 'uint24' needed to contain"): pixel_dtype(self.ds)
def test_unsupported_dtypes(self): """Test unsupported dtypes raise exception.""" self.ds.BitsAllocated = 24 self.ds.PixelRepresentation = 0 with pytest.raises(NotImplementedError, match="data type 'uint24' needed to contain"): pixel_dtype(self.ds)
def test_unknown_pixel_representation_raises(self): """Test an unknown PixelRepresentation value raises exception.""" self.ds.BitsAllocated = 16 self.ds.PixelRepresentation = -1 # The bracket needs to be escaped with pytest.raises(ValueError, match=r"value of '-1' for '\(0028,0103"): pixel_dtype(self.ds) self.ds.PixelRepresentation = 2 with pytest.raises(ValueError, match=r"value of '2' for '\(0028,0103"): pixel_dtype(self.ds)
def test_unknown_pixel_representation_raises(self): """Test an unknown PixelRepresentation value raises exception.""" self.ds.BitsAllocated = 16 self.ds.PixelRepresentation = -1 # The bracket needs to be escaped with pytest.raises(ValueError, match=r"value of '-1' for '\(0028,0103"): pixel_dtype(self.ds) self.ds.PixelRepresentation = 2 with pytest.raises(ValueError, match=r"value of '2' for '\(0028,0103"): pixel_dtype(self.ds)
def test_jpeg2000i(self, fname, info): """Test get_parameters() for the j2k datasets.""" #info: (rows, columns, spp, bps) index = get_indexed_datasets('1.2.840.10008.1.2.4.91') ds = index[fname]['ds'] frame = next(generate_frames(ds)) arr = decode(BytesIO(frame), reshape=False) assert arr.flags.writeable ds.NumberOfFrames = 1 arr = arr.view(pixel_dtype(ds)) arr = reshape_pixel_array(ds, arr) #plt.imshow(arr) #plt.show() if info[2] == 1: assert (info[0], info[1]) == arr.shape else: assert (info[0], info[1], info[2]) == arr.shape if 1 <= info[3] <= 8: if info[4] == 1: assert arr.dtype == 'int8' else: assert arr.dtype == 'uint8' if 9 <= info[3] <= 16: if info[4] == 1: assert arr.dtype == 'int16' else: assert arr.dtype == 'uint16'
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
def generate_frames(ds): """Yield decompressed pixel data frames as :class:`numpy.ndarray`. .. deprecated:: 1.2 Use :func:`~pydicom.pixel_data_handlers.pylibjpeg_handler.generate_frames` instead Parameters ---------- ds : pydicom.dataset.Dataset The dataset containing the pixel data. Yields ------ numpy.ndarray A single frame of the decompressed pixel data. """ try: import pydicom except ImportError: raise RuntimeError("'generate_frames' requires the pydicom package") from pydicom.encaps import generate_pixel_data_frame from pydicom.pixel_data_handlers.util import pixel_dtype decoders = get_pixel_data_decoders() decode = decoders[ds.file_meta.TransferSyntaxUID] p_interp = ds.PhotometricInterpretation nr_frames = getattr(ds, 'NumberOfFrames', 1) for frame in generate_pixel_data_frame(ds.PixelData, nr_frames): arr = decode(frame, ds.group_dataset(0x0028)).view(pixel_dtype(ds)) yield reshape_frame(ds, arr)
def test_byte_swapping(self): """Test that the endianess of the system is taken into account.""" # The main problem is that our testing environments are probably # all little endian, but we'll try our best self.ds.BitsAllocated = 16 self.ds.PixelRepresentation = 0 # < is little, = is native, > is big if byteorder == 'little': self.ds.is_little_endian = True assert pixel_dtype(self.ds).byteorder in ['<', '='] self.ds.is_little_endian = False assert pixel_dtype(self.ds).byteorder == '>' elif byteorder == 'big': self.ds.is_little_endian = True assert pixel_dtype(self.ds).byteorder == '<' self.ds.is_little_endian = False assert pixel_dtype(self.ds).byteorder in ['>', '=']
def test_byte_swapping(self): """Test that the endianess of the system is taken into account.""" # The main problem is that our testing environments are probably # all little endian, but we'll try our best self.ds.BitsAllocated = 16 self.ds.PixelRepresentation = 0 # < is little, = is native, > is big if byteorder == 'little': self.ds.is_little_endian = True assert pixel_dtype(self.ds).byteorder in ['<', '='] self.ds.is_little_endian = False assert pixel_dtype(self.ds).byteorder == '>' elif byteorder == 'big': self.ds.is_little_endian = True assert pixel_dtype(self.ds).byteorder == '<' self.ds.is_little_endian = False assert pixel_dtype(self.ds).byteorder in ['>', '=']
def test_supported_dtypes(self, bits, pixel_repr, dtype): """Test supported dtypes.""" self.ds.BitsAllocated = bits self.ds.PixelRepresentation = pixel_repr # Correct for endianness of system ref_dtype = np.dtype(dtype) endianness = self.ds.file_meta.TransferSyntaxUID.is_little_endian if endianness != (byteorder == 'little'): ref_dtype = ref_dtype.newbyteorder('S') assert ref_dtype == pixel_dtype(self.ds)
def test_supported_dtypes(self, bits, pixel_repr, dtype): """Test supported dtypes.""" self.ds.BitsAllocated = bits self.ds.PixelRepresentation = pixel_repr # Correct for endianness of system ref_dtype = np.dtype(dtype) endianness = self.ds.file_meta.TransferSyntaxUID.is_little_endian if endianness != (byteorder == 'little'): ref_dtype = ref_dtype.newbyteorder('S') assert ref_dtype == pixel_dtype(self.ds)
def get_pixeldata(ds: "Dataset") -> "numpy.ndarray": """Return the *Pixel Data* as a :class:`numpy.ndarray`. Returns ------- numpy.ndarray A correctly sized (but not shaped) numpy array of the *Pixel Data*. Raises ------ ImportError If the required packages are not available. NotImplementedError If the transfer syntax is not supported. TypeError If the pixel data type is unsupported. """ tsyntax = ds.file_meta.TransferSyntaxUID if tsyntax not in SUPPORTED_TRANSFER_SYNTAXES: raise NotImplementedError( f"The jpeg_ls does not support this transfer syntax {tsyntax.name}" ) if not HAVE_JPEGLS: raise ImportError( "The jpeg_ls package is required to use pixel_array for this " f"transfer syntax {tsyntax.name}, and jpeg_ls could not be " "imported") pixel_bytes = bytearray() nr_frames = getattr(ds, "NumberOfFrames", 1) or 1 if nr_frames > 1: for src in decode_data_sequence(ds.PixelData): frame = jpeg_ls.decode(numpy.frombuffer(src, dtype='u1')) pixel_bytes.extend(frame.tobytes()) else: src = defragment_data(ds.PixelData) frame = jpeg_ls.decode(numpy.frombuffer(src, dtype='u1')) pixel_bytes.extend(frame.tobytes()) arr = numpy.frombuffer(pixel_bytes, pixel_dtype(ds)) if should_change_PhotometricInterpretation_to_RGB(ds): ds.PhotometricInterpretation = "RGB" return cast("numpy.ndarray", arr)
def test_unknown_bits_allocated_raises(self): """Test an unknown BitsAllocated value raises exception.""" self.ds.BitsAllocated = 0 self.ds.PixelRepresentation = 0 # The bracket needs to be escaped with pytest.raises(ValueError, match=r"value of '0' for '\(0028,0100"): pixel_dtype(self.ds) self.ds.BitsAllocated = 2 with pytest.raises(ValueError, match=r"value of '2' for '\(0028,0100"): pixel_dtype(self.ds) self.ds.BitsAllocated = 15 with pytest.raises(ValueError, match=r"value of '15' for '\(0028,0100"): pixel_dtype(self.ds)
def GetPixelArray(self): """Generate a memory mapped numpy accessor to the pixel array.""" if self.memmap_pixel_array is False: return self.pixel_array data = self.GetImageData() filename = self.filename dtype = pixel_dtype(self.ds) offset = self.offset frames = int(data['frames']) shape = (int(self.GetNumberOfFrames()), data['rows'], data['columns']) if frames > 1 \ else (data['rows'], data['columns']) def get_pixel_array(filename, dtype, offset, shape): array = np.memmap(filename, dtype=dtype, mode="r", offset=offset, shape=shape) yield array del array return list(get_pixel_array(filename, dtype, offset, shape))[0]
def test_unknown_bits_allocated_raises(self): """Test an unknown BitsAllocated value raises exception.""" self.ds.BitsAllocated = 0 self.ds.PixelRepresentation = 0 # The bracket needs to be escaped with pytest.raises(ValueError, match=r"value of '0' for '\(0028,0100"): pixel_dtype(self.ds) self.ds.BitsAllocated = 2 with pytest.raises(ValueError, match=r"value of '2' for '\(0028,0100"): pixel_dtype(self.ds) self.ds.BitsAllocated = 15 with pytest.raises(ValueError, match=r"value of '15' for '\(0028,0100"): pixel_dtype(self.ds)
def generate_frames(ds): """Yield decompressed pixel data frames as :class:`numpy.ndarray`. Parameters ---------- ds : pydicom.dataset.Dataset The dataset containing the pixel data. Yields ------ numpy.ndarray A single frame of the decompressed pixel data. """ from pydicom.encaps import generate_pixel_data_frame from pydicom.pixel_data_handlers.util import pixel_dtype decoders = get_pixel_data_decoders() decode = decoders[ds.file_meta.TransferSyntaxUID] p_interp = ds.PhotometricInterpretation nr_frames = getattr(ds, 'NumberOfFrames', 1) for frame in generate_pixel_data_frame(ds.PixelData, nr_frames): arr = decode(frame, ds.group_dataset(0x0028)).view(pixel_dtype(ds)) yield reshape_frame(ds, arr)
def get_pixeldata(ds): """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 """ logger.debug( "Trying to use Pillow to read pixel array " "(has pillow = %s)", HAVE_PIL) transfer_syntax = ds.file_meta.TransferSyntaxUID logger.debug("Transfer Syntax UID: '{}'".format(transfer_syntax)) if not HAVE_PIL: msg = ("The pillow package is required to use pixel_array for " "this transfer syntax {0}, and pillow could not be " "imported.".format(transfer_syntax.name)) raise ImportError(msg) if not HAVE_JPEG and transfer_syntax in PillowJPEGTransferSyntaxes: msg = ("this transfer syntax {0}, can not be read because " "Pillow lacks the jpeg decoder plugin".format( transfer_syntax.name)) raise NotImplementedError(msg) if not HAVE_JPEG2K and transfer_syntax in PillowJPEG2000TransferSyntaxes: msg = ("this transfer syntax {0}, can not be read because " "Pillow lacks the jpeg 2000 decoder plugin".format( transfer_syntax.name)) raise NotImplementedError(msg) if transfer_syntax == pydicom.uid.JPEGExtended and ds.BitsAllocated != 8: raise NotImplementedError( "{} - {} only supported by Pillow if Bits Allocated = 8".format( pydicom.uid.JPEGExtended, pydicom.uid.JPEGExtended.name)) pixel_bytes = bytearray() if getattr(ds, 'NumberOfFrames', 1) > 1: j2k_precision = None # multiple compressed frames for frame in decode_data_sequence(ds.PixelData): im = Image.open(io.BytesIO(frame)) if 'YBR' in ds.PhotometricInterpretation: im.draft('YCbCr', (ds.Rows, ds.Columns)) pixel_bytes.extend(im.tobytes()) if not j2k_precision: j2k_precision = _get_j2k_precision(frame) else: # single compressed frame pixel_data = defragment_data(ds.PixelData) im = Image.open(io.BytesIO(pixel_data)) if 'YBR' in ds.PhotometricInterpretation: im.draft('YCbCr', (ds.Rows, ds.Columns)) pixel_bytes.extend(im.tobytes()) j2k_precision = _get_j2k_precision(pixel_data) logger.debug("Successfully read %s pixel bytes", len(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 if ds.PixelRepresentation == 1: # Pillow converts signed data to unsigned # so we need to undo this conversion arr -= 2**(ds.BitsAllocated - 1) if j2k_precision and j2k_precision != ds.BitsStored: warnings.warn( "The (0028,0101) 'Bits Stored' value doesn't match the " "sample bit depth of the JPEG2000 pixel data ({} vs {} bit). " "It's recommended that you first change the 'Bits Stored' " "value to match the JPEG2000 bit depth in order to get the " "correct pixel data".format(ds.BitsStored, j2k_precision)) shift = ds.BitsAllocated - ds.BitsStored if shift: logger.debug("Shifting right by {} bits".format(shift)) numpy.right_shift(arr, shift, out=arr) if should_change_PhotometricInterpretation_to_RGB(ds): ds.PhotometricInterpretation = "RGB" return arr
def test_pixel_dtype_raises(self): """Test that pixel_dtype raises exception without numpy.""" with pytest.raises(ImportError, match="Numpy is required to determine the dtype"): pixel_dtype(None)
def test_pixel_dtype_raises(self): """Test that pixel_dtype raises exception without numpy.""" with pytest.raises(ImportError, match="Numpy is required to determine the dtype"): pixel_dtype(None)
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))
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", rle_segment_order: str = '>') -> "np.ndarray": """Return an :class:`numpy.ndarray` of the *Pixel Data*. Parameters ---------- ds : dataset.Dataset The :class:`Dataset` containing an Image Pixel module and the RLE encoded *Pixel Data* to be converted. rle_segment_order : str The order of segments used by the RLE decoder when dealing with *Bits Allocated* > 8. Each RLE segment contains 8-bits of the pixel data, and segments are supposed to be ordered from MSB to LSB. A value of ``'>'`` means interpret the segments as being in big endian order (default) while a value of ``'<'`` means interpret the segments as being in little endian order which may be possible if the encoded data is non-conformant. Returns ------- numpy.ndarray The decoded 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. """ file_meta = cast("FileMetaDataset", ds.file_meta) # type: ignore[has-type] transfer_syntax = 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 RLE 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)) nr_bits = cast(int, ds.BitsAllocated) nr_samples = cast(int, ds.SamplesPerPixel) nr_frames = cast(int, getattr(ds, 'NumberOfFrames', 1) or 1) rows = cast(int, ds.Rows) cols = cast(int, ds.Columns) # Decompress each frame of the pixel data pixel_data = bytearray() if nr_frames > 1: for rle_frame in decode_data_sequence(ds.PixelData): frame = _rle_decode_frame(rle_frame, rows, cols, nr_samples, nr_bits, rle_segment_order) pixel_data.extend(frame) else: frame = _rle_decode_frame(defragment_data(ds.PixelData), rows, cols, nr_samples, nr_bits, rle_segment_order) pixel_data.extend(frame) arr = np.frombuffer(pixel_data, pixel_dtype(ds)) if should_change_PhotometricInterpretation_to_RGB(ds): ds.PhotometricInterpretation = "RGB" return arr
def get_pixeldata(ds, rle_segment_order='>'): """Return an ndarray of the Pixel Data. Parameters ---------- ds : dataset.Dataset The DICOM dataset containing an Image Pixel module and the RLE encoded Pixel Data to be converted. rle_segment_order : str The order of segments used by the RLE decoder when dealing with Bits Allocated > 8. Each RLE segment contains 8-bits of the pixel data, and segments are supposed to be ordered from MSB to LSB. A value of '>' means interpret the segments as being in big endian order (default) while a value of '<' means interpret the segments as being in little endian order which may be possible if the encoded data is non-conformant. Returns ------- np.ndarray The decoded 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 RLE 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)) nr_bits = ds.BitsAllocated nr_samples = ds.SamplesPerPixel nr_frames = getattr(ds, 'NumberOfFrames', 1) rows = ds.Rows cols = ds.Columns # Decompress each frame of the pixel data pixel_data = bytearray() if nr_frames > 1: for rle_frame in decode_data_sequence(ds.PixelData): frame = _rle_decode_frame(rle_frame, rows, cols, nr_samples, nr_bits) pixel_data.extend(frame) else: frame = _rle_decode_frame(defragment_data(ds.PixelData), rows, cols, nr_samples, nr_bits) pixel_data.extend(frame) # The segment order should be big endian by default but make it possible # to switch if the RLE is non-conformant dtype = pixel_dtype(ds).newbyteorder(rle_segment_order) arr = np.frombuffer(pixel_data, dtype) if should_change_PhotometricInterpretation_to_RGB(ds): ds.PhotometricInterpretation = "RGB" return arr
def get_pixeldata(ds): """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 """ logger.debug( "Trying to use Pillow to read pixel array " "(has pillow = %s)", HAVE_PIL) transfer_syntax = ds.file_meta.TransferSyntaxUID if not HAVE_PIL: msg = ("The pillow package is required to use pixel_array for " "this transfer syntax {0}, and pillow could not be " "imported.".format(transfer_syntax.name)) raise ImportError(msg) if not HAVE_JPEG and transfer_syntax in PillowJPEGTransferSyntaxes: msg = ("this transfer syntax {0}, can not be read because " "Pillow lacks the jpeg decoder plugin".format( transfer_syntax.name)) raise NotImplementedError(msg) if not HAVE_JPEG2K and transfer_syntax in PillowJPEG2000TransferSyntaxes: msg = ("this transfer syntax {0}, can not be read because " "Pillow lacks the jpeg 2000 decoder plugin".format( transfer_syntax.name)) raise NotImplementedError(msg) if transfer_syntax not in PillowSupportedTransferSyntaxes: msg = ("this transfer syntax {0}, can not be read because " "Pillow does not support this syntax".format( transfer_syntax.name)) raise NotImplementedError(msg) if transfer_syntax in PillowJPEGTransferSyntaxes: logger.debug("This is a JPEG lossy format") if ds.BitsAllocated > 8: raise NotImplementedError("JPEG Lossy only supported if " "Bits Allocated = 8") elif transfer_syntax in PillowJPEG2000TransferSyntaxes: logger.debug("This is a JPEG 2000 format") else: logger.debug("This is a another pillow supported format") pixel_bytes = bytearray() if getattr(ds, 'NumberOfFrames', 1) > 1: # multiple compressed frames for frame in decode_data_sequence(ds.PixelData): decompressed_image = Image.open(io.BytesIO(frame)) pixel_bytes.extend(decompressed_image.tobytes()) else: # single compressed frame pixel_data = defragment_data(ds.PixelData) decompressed_image = Image.open(io.BytesIO(pixel_data)) pixel_bytes.extend(decompressed_image.tobytes()) logger.debug("Successfully read %s pixel bytes", len(pixel_bytes)) arr = numpy.frombuffer(pixel_bytes, pixel_dtype(ds)) if (transfer_syntax in PillowJPEG2000TransferSyntaxes and ds.BitsStored == 16): # WHY IS THIS EVEN NECESSARY?? arr &= 0x7FFF if should_change_PhotometricInterpretation_to_RGB(ds): ds.PhotometricInterpretation = "RGB" return arr
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, 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
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()
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()
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()
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
def generate_frames(ds: "Dataset", reshape: bool = True) -> "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" else: plugin = "pylibjpeg-libjpeg" 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) image_px_module = ds.group_dataset(0x0028) dtype = pixel_dtype(ds) for frame in generate_pixel_data_frame(ds.PixelData, nr_frames): arr = decoder(frame, image_px_module) # 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: # JPEG, JPEG-LS and JPEG 2000 are all Planar Configuration 0 yield arr.reshape(ds.Rows, ds.Columns, ds.SamplesPerPixel)
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))
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
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