def test_encapsulate_single_fragment_per_frame_bot(self): """Test encapsulating single fragment per frame with BOT values.""" ds = dcmread(JP2K_10FRAME_NOBOT) frames = decode_data_sequence(ds.PixelData) assert len(frames) == 10 data = encapsulate(frames, fragments_per_frame=1, has_bot=True) test_frames = decode_data_sequence(data) for a, b in zip(test_frames, frames): assert a == b fp = DicomBytesIO(data) fp.is_little_endian = True offsets = get_frame_offsets(fp) assert offsets == [ 0x0000, # 0 0x0eee, # 3822 0x1df6, # 7670 0x2cf8, # 11512 0x3bfc, # 15356 0x4ade, # 19166 0x59a2, # 22946 0x6834, # 26676 0x76e2, # 30434 0x8594 # 34196 ]
def test_encapsulate_single_fragment_per_frame_bot(self): """Test encapsulating single fragment per frame with BOT values.""" ds = dcmread(JP2K_10FRAME_NOBOT) frames = decode_data_sequence(ds.PixelData) assert len(frames) == 10 data = encapsulate(frames, fragments_per_frame=1, has_bot=True) test_frames = decode_data_sequence(data) for a, b in zip(test_frames, frames): assert a == b fp = DicomBytesIO(data) fp.is_little_endian = True length, offsets = get_frame_offsets(fp) assert offsets == [ 0x0000, # 0 0x0eee, # 3822 0x1df6, # 7670 0x2cf8, # 11512 0x3bfc, # 15356 0x4ade, # 19166 0x59a2, # 22946 0x6834, # 26676 0x76e2, # 30434 0x8594 # 34196 ]
def test_encapsulate_single_fragment_per_frame_no_bot(self): """Test encapsulating single fragment per frame with no BOT values.""" ds = dcmread(JP2K_10FRAME_NOBOT) frames = decode_data_sequence(ds.PixelData) assert len(frames) == 10 data = encapsulate(frames, fragments_per_frame=1, has_bot=False) test_frames = decode_data_sequence(data) for a, b in zip(test_frames, frames): assert a == b # Original data has no BOT values assert data == ds.PixelData
def test_multi_frame_varied_ratio(self): """Test a multi-frame image where each frames is random fragments""" # 3 frames, 1st is 1 fragment, 2nd is 3 fragments, 3rd is 2 fragments bytestream = b'\xFE\xFF\x00\xE0' \ b'\x0C\x00\x00\x00' \ b'\x00\x00\x00\x00' \ b'\x0E\x00\x00\x00' \ b'\x32\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x06\x00\x00\x00\x01\x00\x00\x00\x00\x01' \ b'\xFE\xFF\x00\xE0' \ b'\x02\x00\x00\x00\x02\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x06\x00\x00\x00\x03\x00\x00\x00\x00\x02' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00\x03\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x02\x00\x00\x00\x02\x04' frames = decode_data_sequence(bytestream) assert frames == [ b'\x01\x00\x00\x00\x00\x01', b'\x02\x00', b'\x02\x00\x00\x00', b'\x03\x00\x00\x00\x00\x02', b'\x03\x00\x00\x00', b'\x02\x04' ]
def setup(self): # MONOCHROME2, 64x64, 1 sample/pixel, 16 bits allocated, 12 bits stored self.ds = dcmread(EMRI_RLE_10F) self.frames = decode_data_sequence(self.ds.PixelData) assert len(self.frames) == 10 self.no_runs = 1
def setup(self): # MONOCHROME2, 64x64, 1 sample/pixel, 16 bits allocated, 12 bits stored self.ds = dcmread(EMRI_RLE_10F) self.frames = decode_data_sequence(self.ds.PixelData) assert len(self.frames) == 10 self.no_runs = 100
def __getitem__(self, key): if (key) in self._dsequence: return self._dsequence[key] elif key not in self._dsstore: raise ValueError('Entry does not exist') else: self._dsequence[key] = decode_data_sequence(self._dsstore[key].PixelData) return self._dsequence[key]
def parse_comppressed(ds): try: print(f"pixeldata:{len(ds.PixelData)}") if getattr(ds, 'NumberOfFrames', 1) > 1: print("multi frame") j2k_precision, j2k_sign = None, None # multiple compressed frames # working case (50): # 1. 0002.dcm, some are [-5], [-4], [-6]. 512x512 # 2. color3d_jpeg_baseline , some frames needs [-1] but some do not need. size unknown? frame_count = 0 for frame in decode_data_sequence(ds.PixelData): frame_count += 1 print(f"frame i:{frame_count}, len:{len(frame)}") # a = frame[0] # b = frame[1] # c = frame[len(frame)-2] # d = frame[len(frame)-1] # print(f"{a},{b},{c},{d}") if frame_count == 1: pixel_data = frame # im = _decompress_single_frame( # frame, # transfer_syntax, # ds.PhotometricInterpretation # ) # if 'YBR' in ds.PhotometricInterpretation: # im.draft('YCbCr', (ds.Rows, ds.Columns)) # pixel_bytes.extend(im.tobytes()) # if not j2k_precision: # params = get_j2k_parameters(frame) # j2k_precision = params.setdefault("precision", ds.BitsStored) # j2k_sign = params.setdefault("is_signed", None) # TODO: what is the rule of -5/-1? But even not using pixel_data[:-1], pixel_data[:-5], still work p2 = pixel_data else: print("single frame") # working case but browser can not render DICOM made JPEG Lossless : # JPGLosslessP14SV1_1s_1f_8b.dcm, 70. 1024x768 pixel_data = defragment_data(ds.PixelData) p2 = pixel_data print(f"pixel_data:{len(pixel_data)}") # return None, pixel_data # try: # fio = BytesIO(ds.PixelData) # pixel_data) # image = Image.open(fio) # except Exception as e: # print(f"pillow error:{e}") # print('pillow done') # JPEG57-MR-MONO2-12-shoulder data:718940 -> data:718924 return None, p2 except Exception as e: print("failed to get compressed data") raise e
def test_empty_bot_single_fragment(self): """Test a single-frame image where the frame is one fragments""" # 1 frame, 1 fragment long bytestream = b'\xFE\xFF\x00\xE0' \ b'\x00\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x01\x00\x00\x00' frames = decode_data_sequence(bytestream) assert frames == [b'\x01\x00\x00\x00']
def get_pixel_data(ds): print(f"PixelData:{len(ds.PixelData)}") if getattr(ds, 'NumberOfFrames', 1) > 1: j2k_precision, j2k_sign = None, None # multiple compressed frames frame_count = 0 for frame in decode_data_sequence(ds.PixelData): frame_count += 1 print(f"frame i:{frame_count}, len:{len(frame)}") if frame_count == 1: pixel_data = frame else: pixel_data = defragment_data(ds.PixelData) print(f"pixel_data:{len(pixel_data)}") return pixel_data
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_empty_bot_triple_fragment_single_frame(self): """Test a single-frame image where the frame is three fragments""" # 1 frame, 3 fragments long bytestream = b'\xFE\xFF\x00\xE0' \ b'\x00\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x01\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x03\x00\x00\x00' frames = decode_data_sequence(bytestream) assert frames == [b'\x01\x00\x00\x00', b'\x02\x00\x00\x00', b'\x03\x00\x00\x00']
def test_empty_bot_triple_fragment_single_frame(self): """Test a single-frame image where the frame is three fragments""" # 1 frame, 3 fragments long bytestream = b'\xFE\xFF\x00\xE0' \ b'\x00\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x01\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x03\x00\x00\x00' frames = decode_data_sequence(bytestream) assert frames == [ b'\x01\x00\x00\x00', b'\x02\x00\x00\x00', b'\x03\x00\x00\x00' ]
def test_multi_frame_one_to_one(self): """Test a multi-frame image where each frame is one fragment""" # 3 frames, each 1 fragment long bytestream = b'\xFE\xFF\x00\xE0' \ b'\x0C\x00\x00\x00' \ b'\x00\x00\x00\x00' \ b'\x0C\x00\x00\x00' \ b'\x18\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x01\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x03\x00\x00\x00' frames = decode_data_sequence(bytestream) assert frames == [ b'\x01\x00\x00\x00', b'\x02\x00\x00\x00', b'\x03\x00\x00\x00' ]
def test_multi_frame_one_to_one(self): """Test a multi-frame image where each frame is one fragment""" # 3 frames, each 1 fragment long bytestream = b'\xFE\xFF\x00\xE0' \ b'\x0C\x00\x00\x00' \ b'\x00\x00\x00\x00' \ b'\x0C\x00\x00\x00' \ b'\x18\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x01\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0' \ b'\x04\x00\x00\x00' \ b'\x03\x00\x00\x00' frames = decode_data_sequence(bytestream) assert frames == [b'\x01\x00\x00\x00', b'\x02\x00\x00\x00', b'\x03\x00\x00\x00']
def test_encapsulate_bot(self): """Test the Basic Offset Table is correct.""" ds = dcmread(JP2K_10FRAME_NOBOT) frames = decode_data_sequence(ds.PixelData) assert len(frames) == 10 data = encapsulate(frames, fragments_per_frame=1, has_bot=True) assert data[:56] == ( b'\xfe\xff\x00\xe0' # Basic offset table item tag b'\x28\x00\x00\x00' # Basic offset table length b'\x00\x00\x00\x00' # First offset b'\xee\x0e\x00\x00' b'\xf6\x1d\x00\x00' b'\xf8\x2c\x00\x00' b'\xfc\x3b\x00\x00' b'\xde\x4a\x00\x00' b'\xa2\x59\x00\x00' b'\x34\x68\x00\x00' b'\xe2\x76\x00\x00' b'\x94\x85\x00\x00' # Last offset b'\xfe\xff\x00\xe0' # Next item tag b'\xe6\x0e\x00\x00' # Next item length )
def test_multi_frame_three_to_one(self): """Test a multi-frame image where each frame is three fragments""" # 2 frames, each 3 fragments long bytestream = b'\xFE\xFF\x00\xE0' \ b'\x0C\x00\x00\x00' \ b'\x00\x00\x00\x00' \ b'\x20\x00\x00\x00' \ b'\x40\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x01\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x03\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x03\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x03\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x02\x00\x00\x00' \ b'\xFE\xFF\x00\xE0\x04\x00\x00\x00\x03\x00\x00\x00' frames = decode_data_sequence(bytestream) assert frames == [ b'\x01\x00\x00\x00', b'\x02\x00\x00\x00', b'\x03\x00\x00\x00', b'\x02\x00\x00\x00', b'\x02\x00\x00\x00', b'\x03\x00\x00\x00', b'\x03\x00\x00\x00', b'\x02\x00\x00\x00', b'\x03\x00\x00\x00' ]
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 setup(self): """Setup the test""" ds = dcmread(JP2K_10FRAME) self.test_data = decode_data_sequence(ds.PixelData) assert len(self.test_data) == 10 self.no_runs = 1000
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_manufacturer_independent_pixel_image2d_array(ds, has_TransferSyntax): # '1.2.840.10008.1.2.4.90' # # ds.file_meta.TransferSyntaxUID = '1.2.840.10008.1.2.4.99' # '1.2.840.10008.1.2.1.99' # has_TransferSyntax = True # print(f"syntax:{ds.file_meta.TransferSyntaxUID}") # RLE 1.2.840.10008.1.2.5: US-PAL-8-10x-echo.dcm is automatically handled as uncompressed case if (has_TransferSyntax and ds.file_meta.TransferSyntaxUID in [ "1.2.840.10008.1.2.4.50", "1.2.840.10008.1.2.4.51", "1.2.840.10008.1.2.4.57", "1.2.840.10008.1.2.4.70", "1.2.840.10008.1.2.4.80", "1.2.840.10008.1.2.4.81", "1.2.840.10008.1.2.4.90", "1.2.840.10008.1.2.4.91" ]): print("compressed case !!!!!!!!!") # return None, ds.PixelData # ref: https://github.com/pydicom/pydicom/blob/master/pydicom/pixel_data_handlers/pillow_handler.py print( "try to get compressed dicom's pixel data manually, can not handle by pydicom in pyodide, lack of some pyodide extension" ) try: print(f"pixeldata:{len(ds.PixelData)}") # TODO: only get 1st frame for multiple frame case and will improve later if getattr(ds, 'NumberOfFrames', 1) > 1: print("multi frame") j2k_precision, j2k_sign = None, None # multiple compressed frames # working case (50): # 1. 0002.dcm, some are [-5], [-4], [-6]. 512x512 # 2. color3d_jpeg_baseline , some frames needs [-1] but some do not need. size unknown? frame_count = 0 for frame in decode_data_sequence(ds.PixelData): frame_count += 1 # print(f"frame i:{frame_count}, len:{len(frame)}") # a = frame[0] # b = frame[1] # c = frame[len(frame)-2] # d = frame[len(frame)-1] # print(f"{a},{b},{c},{d}") if frame_count == 1: pixel_data = frame # im = _decompress_single_frame( # frame, # transfer_syntax, # ds.PhotometricInterpretation # ) # if 'YBR' in ds.PhotometricInterpretation: # im.draft('YCbCr', (ds.Rows, ds.Columns)) # pixel_bytes.extend(im.tobytes()) # if not j2k_precision: # params = get_j2k_parameters(frame) # j2k_precision = params.setdefault("precision", ds.BitsStored) # j2k_sign = params.setdefault("is_signed", None) # TODO: what is the rule of -5/-1? But even not using pixel_data[:-1], pixel_data[:-5], still work p2 = pixel_data else: print("single frame") # working case but browser can not render : # - JPGLosslessP14SV1_1s_1f_8b.dcm, DICOM made JPEG Lossless, 1.2.840.10008.1.2.4.70. 1024x768. local mac is able to view. # - JPEG57-MR-MONO2-12-shoulder.dcm from https://barre.dev/medical/samples/, JPEG Lossless, 1.2.840.10008.1.2.4.57. # https://products.groupdocs.app/viewer/jpg can be used to view. local mac seeme not able to view (all black) # - JPEG-lossy.dcm 1.2.840.10008.1.2.4.51 from https://github.com/pydicom/pydicom/blob/master/pydicom/data/test_files/JPEG-lossy.dcm, # https://products.groupdocs.app/viewer/jpg can be used to view, local mac seems not able to view (all black) pixel_data = defragment_data(ds.PixelData) p2 = pixel_data print(f"pixel_data:{len(pixel_data)}") # return None, pixel_data # try: # fio = BytesIO(ds.PixelData) # pixel_data) # image = Image.open(fio) # except Exception as e: # print(f"pillow error:{e}") # print('pillow done') # JPEG57-MR-MONO2-12-shoulder data:718940 -> data:718924 return None, p2 except Exception as e: print("failed to get compressed data") raise e print("incompressed") print( "start reading dicom pixel_array, uncompressed case uses apply_modality_lut" ) try: arr = ds.pixel_array except Exception as e: if has_TransferSyntax == True: raise e else: # http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_10.html print( "read data fail may due to no TransferSyntaxUID, set it as most often used and default ImplicitVRLittleEndian and try read dicom again" ) ds.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian arr = ds.pixel_array print(f"read dicom pixel_array ok, shape:{arr.shape}") image2d = apply_modality_lut(arr, ds) return image2d, None
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 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 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'