def get_item(self, key): """Return the raw data element if possible. It will be raw if the user has never accessed the value, or set their own value. Note if the data element is a deferred-read element, then it is read and converted before being returned. Parameters ---------- key The DICOM (group, element) tag in any form accepted by pydicom.tag.Tag such as [0x0010, 0x0010], (0x10, 0x10), 0x00100010, etc. Returns ------- pydicom.dataelem.DataElement """ tag = Tag(key) data_elem = dict.__getitem__(self, tag) # If a deferred read, return using __getitem__ to read and convert it if isinstance(data_elem, tuple) and data_elem.value is None: return self[key] return data_elem
def _get_grouped_dicoms(dicom_input): """ Search all dicoms in the dicom directory, sort and validate them fast_read = True will only read the headers not the data """ # if all dicoms have an instance number try sorting by instance number else by position if [d for d in dicom_input if 'InstanceNumber' in d]: dicoms = sorted(dicom_input, key=lambda x: x.InstanceNumber) else: dicoms = common.sort_dicoms(dicom_input) # now group per stack grouped_dicoms = [[]] # list with first element a list timepoint_index = 0 previous_stack_position = -1 # loop over all sorted dicoms stack_position_tag = Tag( 0x2001, 0x100a) # put this there as this is a slow step and used a lot for index in range(0, len(dicoms)): dicom_ = dicoms[index] stack_position = 0 if stack_position_tag in dicom_: stack_position = common.get_is_value(dicom_[stack_position_tag]) if previous_stack_position == stack_position: # if the stack number is the same we move to the next timepoint timepoint_index += 1 if len(grouped_dicoms) <= timepoint_index: grouped_dicoms.append([]) else: # if it changes move back to the first timepoint timepoint_index = 0 grouped_dicoms[timepoint_index].append(dicom_) previous_stack_position = stack_position return grouped_dicoms
def find_delimiter( fp: BinaryIO, delimiter: BaseTag, is_little_endian: bool, read_size: int = 128, rewind: bool = True ) -> Optional[int]: """Return file position where 4-byte delimiter is located. Parameters ---------- delimiter : int The delimiter to search for. is_little_endian : bool ``True`` if little endian, ``False`` otherwise. read_size : int See :func:`find_bytes` for parameter info. rewind : bool Flag to rewind to initial position after searching. Returns ------- int or None Returns ``None`` if end of file is reached without finding the delimiter, otherwise the byte offset to the delimiter. """ struct_format = "<H" if not is_little_endian: struct_format = ">H" delimiter = Tag(delimiter) bytes_to_find = ( pack(struct_format, delimiter.group) + pack(struct_format, delimiter.elem) ) return find_bytes(fp, bytes_to_find, read_size=read_size, rewind=rewind)
def _create_affine_multiframe(multiframe_dicom): """ Function to create the affine matrix for a siemens mosaic dataset This will work for siemens dti and 4D if in mosaic format """ first_frame = multiframe_dicom[Tag(0x5200, 0x9230)][0] # Create affine matrix (http://nipy.sourceforge.net/nibabel/dicom/dicom_orientation.html#dicom-slice-affine) image_orient1 = numpy.array(first_frame.PlaneOrientationSequence[0]. ImageOrientationPatient)[0:3].astype(float) image_orient2 = numpy.array(first_frame.PlaneOrientationSequence[0]. ImageOrientationPatient)[3:6].astype(float) normal = numpy.cross(image_orient1, image_orient2) delta_r = float(first_frame[0x2005, 0x140f][0].PixelSpacing[0]) delta_c = float(first_frame[0x2005, 0x140f][0].PixelSpacing[1]) image_pos = numpy.array( first_frame.PlanePositionSequence[0].ImagePositionPatient).astype( float) delta_s = float(multiframe_dicom.SpacingBetweenSlices) return numpy.matrix([[ -image_orient1[0] * delta_c, -image_orient2[0] * delta_r, -delta_s * normal[0], -image_pos[0] ], [ -image_orient1[1] * delta_c, -image_orient2[1] * delta_r, -delta_s * normal[1], -image_pos[1] ], [ image_orient1[2] * delta_c, image_orient2[2] * delta_r, delta_s * normal[2], image_pos[2] ], [0, 0, 0, 1]])
def _convert(self, val: Any) -> Any: """Convert `val` to an appropriate type for the element's VR.""" # If the value is bytes and has a VR that can only be encoded # using the default character repertoire, convert it to a string if self.VR in DEFAULT_CHARSET_VR and isinstance(val, bytes): val = val.decode() if self.VR == VR_.IS: return pydicom.valuerep.IS(val, self.validation_mode) if self.VR == VR_.DA and config.datetime_conversion: return pydicom.valuerep.DA(val, validation_mode=self.validation_mode) if self.VR == VR_.DS: return pydicom.valuerep.DS(val, False, self.validation_mode) if self.VR == VR_.DT and config.datetime_conversion: return pydicom.valuerep.DT(val, validation_mode=self.validation_mode) if self.VR == VR_.TM and config.datetime_conversion: return pydicom.valuerep.TM(val, validation_mode=self.validation_mode) if self.VR == VR_.UI: return UID(val, self.validation_mode) if val is not None else None if self.VR == VR_.PN: return PersonName(val, validation_mode=self.validation_mode) if self.VR == VR_.AT and (val == 0 or val): return val if isinstance(val, BaseTag) else Tag(val) self.validate(val) return val
def test_identifier_comparison(): """Tag Identifiers are hashable and should be sortable as well""" # these should equal each other assert SingleTag(tag=Tag("PatientID")) == SingleTag(tag=Tag("PatientID")) # You can use any initialization that pydicom.tag.Tag allows, still equal assert SingleTag(tag=Tag(0x0010, 0x0020)) == SingleTag( tag=Tag("PatientID")) assert RepeatingGroup(tag="0010,10xx") == RepeatingGroup(tag="0010,10xx") # it should be possible to sort them as well taglist = [ SingleTag(tag=Tag("0020000e")), SingleTag(tag=Tag("00100020")), RepeatingGroup(tag="001010xx"), ] taglist.sort(reverse=True) assert str(taglist[1]) == "(0010, 10xx)"
def test_tag_single_list(self): """Test creating a Tag from a single list.""" assert Tag([0x0000, 0x0000]) == BaseTag(0x00000000) assert Tag([0x99, 0xFE]) == BaseTag(0x009900FE) assert Tag([15, 0xE]) == BaseTag(0x000F000E) assert Tag([0x1000, 0x2000]) == BaseTag(0x10002000) assert Tag(['0x01', '0x02']) == BaseTag(0x00010002) # Must be 2 list pytest.raises(ValueError, Tag, [0x1000, 0x2000, 0x0030]) pytest.raises(ValueError, Tag, ['0x10', '0x20', '0x03']) pytest.raises(ValueError, Tag, [0x1000]) pytest.raises(ValueError, Tag, ['0x10']) # Must be int or string msg = (r"Unable to create an element tag from '\[1.0, 2.0\]': both " r"arguments must be the same type and str or int") with pytest.raises(TypeError, match=msg): Tag([1., 2.]) # Must be 32-bit pytest.raises(OverflowError, Tag, [65536, 0]) pytest.raises(OverflowError, Tag, [0, 65536]) pytest.raises(OverflowError, Tag, ('0xFFFF', '0xFFFF1')) # Must be positive pytest.raises(ValueError, Tag, [-1, 0]) pytest.raises(ValueError, Tag, [0, -1]) pytest.raises(ValueError, Tag, ('0x0', '-0x1')) pytest.raises(ValueError, Tag, ('-0x1', '0x0')) # Can't have second parameter msg = r"Unable to create an element tag from '\(\[1, 2\], 1\)'" with pytest.raises(TypeError, match=msg): Tag([0x01, 0x02], 0x01) pytest.raises(TypeError, Tag, [0x01, 0x02], '0x01') pytest.raises(TypeError, Tag, ['0x01', '0x02'], '0x01') pytest.raises(TypeError, Tag, ['0x01', '0x02'], 0x01)
def test_core_deidentify_safe_private(a_dataset, a_safe_private_definition): """Private elements marked as safe should not be removed by Clean()""" assert Tag("00b10010") in a_dataset # a private creator tag assert Tag("00b11001") in a_dataset # and a private tag # A core instance that should clean() private tags, but one tag is deemed safe ruleset = RuleSet( [Rule(PrivateTags(), Clean(safe_private=a_safe_private_definition))]) core = Core(profile=Profile([ruleset])) # One tag should be kept deltas = extract_signature(deidentifier=core, dataset=a_dataset) assert {x.tag: x for x in deltas}[Tag("00b10010")].status == "REMOVED" assert {x.tag: x for x in deltas}[Tag("00b11001")].status == "UNCHANGED" # but only so long as dataset has modality = CT a_dataset.Modality = "US" deltas = extract_signature(deidentifier=core, dataset=a_dataset) assert {x.tag: x for x in deltas}[Tag("00b10010")].status == "REMOVED" assert {x.tag: x for x in deltas}[Tag("00b11001")].status == "REMOVED"
def _create_singleframe_bvals_bvecs(grouped_dicoms, bval_file, bvec_file, nifti, nifti_file): """ Write the bvals from the sorted dicom files to a bval file """ # create the empty arrays bvals = numpy.zeros([len(grouped_dicoms)], dtype=numpy.int32) bvecs = numpy.zeros([len(grouped_dicoms), 3]) # loop over all timepoints and create a list with all bvals and bvecs if _is_bval_type_a(grouped_dicoms): bval_tag = Tag(0x2001, 0x1003) bvec_x_tag = Tag(0x2005, 0x10b0) bvec_y_tag = Tag(0x2005, 0x10b1) bvec_z_tag = Tag(0x2005, 0x10b2) for stack_index in range(0, len(grouped_dicoms)): bvals[stack_index] = common.get_fl_value(grouped_dicoms[stack_index][0][bval_tag]) bvecs[stack_index, :] = [common.get_fl_value(grouped_dicoms[stack_index][0][bvec_x_tag]), common.get_fl_value(grouped_dicoms[stack_index][0][bvec_y_tag]), common.get_fl_value(grouped_dicoms[stack_index][0][bvec_z_tag])] elif _is_bval_type_b(grouped_dicoms): bval_tag = Tag(0x0018, 0x9087) bvec_tag = Tag(0x0018, 0x9089) for stack_index in range(0, len(grouped_dicoms)): bvals[stack_index] = common.get_fd_value(grouped_dicoms[stack_index][0][bval_tag]) bvecs[stack_index, :] = common.get_fd_array_value(grouped_dicoms[stack_index][0][bvec_tag], 3) # truncate nifti if needed nifti, bvals, bvecs = _fix_diffusion_images(bvals, bvecs, nifti, nifti_file) # save the found bvecs to the file if numpy.count_nonzero(bvals) > 0 or numpy.count_nonzero(bvecs) > 0: common.write_bval_file(bvals, bval_file) common.write_bvec_file(bvecs, bvec_file) else: bval_file = None bvec_file = None bvals = None bvecs = None return nifti, bvals, bvecs, bval_file, bvec_file
def test_data_element_without_encoding(self): """RawDataElement: no encoding needed.""" raw = RawDataElement(Tag(0x00104000), 'LT', 23, b'comment\\comment2\\comment3', 0, False, True) element = DataElement_from_raw(raw) assert 'Patient Comments' == element.name
def generate_pixel_data_fragment(fp): """Yield the encapsulated pixel data fragments. For compressed (encapsulated) Transfer Syntaxes, the (7FE0,0010) *Pixel Data* element is encoded in an encapsulated format. **Encapsulation** The encoded pixel data stream is fragmented into one or more Items. The stream may represent a single or multi-frame image. Each *Data Stream Fragment* shall have tag of (FFFE,E000), followed by a 4 byte *Item Length* field encoding the explicit number of bytes in the Item. All Items containing an encoded fragment shall have an even number of bytes greater than or equal to 2, with the last fragment being padded if necessary. The first Item in the Sequence of Items shall be a 'Basic Offset Table', however the Basic Offset Table item value is not required to be present. It is assumed that the Basic Offset Table item has already been read prior to calling this function (and that `fp` is positioned past this item). The remaining items in the Sequence of Items are the pixel data fragments and it is these items that will be read and returned by this function. The Sequence of Items is terminated by a (FFFE,E0DD) *Sequence Delimiter Item* with an Item Length field of value ``0x00000000``. The presence or absence of the *Sequence Delimiter Item* in `fp` has no effect on the returned fragments. *Encoding* The encoding of the data shall be little endian. Parameters ---------- fp : filebase.DicomBytesIO The encoded (7FE0,0010) *Pixel Data* element value, positioned at the start of the item tag for the first item after the Basic Offset Table item. ``fp.is_little_endian`` should be set to ``True``. Yields ------ bytes A pixel data fragment. Raises ------ ValueError If the data contains an item with an undefined length or an unknown tag. References ---------- DICOM Standard Part 5, :dcm:`Annex A.4 <part05/sect_A.4.html>` """ if not fp.is_little_endian: raise ValueError("'fp.is_little_endian' must be True") # We should be positioned at the start of the Item Tag for the first # fragment after the Basic Offset Table while True: try: tag = Tag(fp.read_tag()) except EOFError: break if tag == 0xFFFEE000: # Item length = fp.read_UL() if length == 0xFFFFFFFF: raise ValueError("Undefined item length at offset {} when " "parsing the encapsulated pixel data " "fragments.".format(fp.tell() - 4)) yield fp.read(length) elif tag == 0xFFFEE0DD: # Sequence Delimiter # Behave nicely and rewind back to the end of the items fp.seek(-4, 1) break else: raise ValueError("Unexpected tag '{0}' at offset {1} when parsing " "the encapsulated pixel data fragment items." .format(tag, fp.tell() - 4))
def test_command_set_keywords(): """Check the keywords are valid and unique""" for kw_list in _COMMAND_SET_KEYWORDS.values(): assert len(kw_list) == len(list(set(kw_list))) for kw in kw_list: Tag(kw)
def test_mixed_long_int(self): assert Tag([0x1000, long(0x2000)]) == BaseTag(0x10002000) assert Tag([long(0x1000), 0x2000]) == BaseTag(0x10002000) assert Tag([long(0x1000), long(0x2000)]) == BaseTag(0x10002000)
def testTagWithoutEncodingPython2(self): """RawDataElement: no encoding needed in Python 2.""" raw = RawDataElement(Tag(0x00104000), 'LT', 23, b'comment\\comment2\\comment3', 0, False, True) element = DataElement_from_raw(raw) self.assertEqual(element.name, 'Patient Comments')
def test_offset(self): """Test convert_tag with an offset""" bytestring = b'\x12\x23\x10\x00\x20\x00\x34\x45' assert convert_tag(bytestring, True, 0) == Tag(0x2312, 0x0010) assert convert_tag(bytestring, True, 2) == Tag(0x0010, 0x0020)
def test_big_endian(self): """Test convert_tag with a big endian byte string""" bytestring = b'\x00\x10\x00\x20' assert convert_tag(bytestring, False) == Tag(0x0010, 0x0020)
def test_little_endian(self): """Test convert_tag with a little endian byte string""" bytestring = b'\x10\x00\x20\x00' assert convert_tag(bytestring, True) == Tag(0x0010, 0x0020)
_MSG_TO_PRIMITVE = { 'C_ECHO': C_ECHO, 'C_STORE': C_STORE, 'C_FIND': C_FIND, 'C_GET': C_GET, 'C_MOVE': C_MOVE, 'C_CANCEL': C_CANCEL, 'N_EVENT_REPORT': N_EVENT_REPORT, 'N_GET': N_GET, 'N_SET': N_SET, 'N_ACTION': N_ACTION, 'N_CREATE': N_CREATE, 'N_DELETE': N_DELETE, } _MULTIVALUE_TAGS = [Tag('OffendingElement'), Tag('AttributeIdentifierList')] class DIMSEMessage(object): """Represents a DIMSE Message. Information is communicated across the DICOM network interface in a DICOM Message. A Message is composed of a Command Set followed by a conditional Data Set. The Command Set is used to indicate the operations/notifications to be performed on or with the Data Set (see PS3.7 6.2-3). :: primitive_to_message() encode_msg() +-------------+ ---> +-----------+ ---> +-------------+
def write_file_meta_info(fp, file_meta, enforce_standard=True): """Write the File Meta Information elements in `file_meta` to `fp`. If `enforce_standard` is True then the file-like `fp` should be positioned past the 128 byte preamble + 4 byte prefix (which should already have been written). DICOM File Meta Information Group Elements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From the DICOM standard, Part 10 Section 7.1, any DICOM file shall contain a 128-byte preamble, a 4-byte DICOM prefix 'DICM' and (at a minimum) the following Type 1 DICOM Elements (from Table 7.1-1): * (0002,0000) FileMetaInformationGroupLength, UL, 4 * (0002,0001) FileMetaInformationVersion, OB, 2 * (0002,0002) MediaStorageSOPClassUID, UI, N * (0002,0003) MediaStorageSOPInstanceUID, UI, N * (0002,0010) TransferSyntaxUID, UI, N * (0002,0012) ImplementationClassUID, UI, N If `enforce_standard` is True then (0002,0000) will be added/updated, (0002,0001) and (0002,0012) will be added if not already present and the other required elements will be checked to see if they exist. If `enforce_standard` is False then `file_meta` will be written as is after minimal validation checking. The following Type 3/1C Elements may also be present: * (0002,0013) ImplementationVersionName, SH, N * (0002,0016) SourceApplicationEntityTitle, AE, N * (0002,0017) SendingApplicationEntityTitle, AE, N * (0002,0018) ReceivingApplicationEntityTitle, AE, N * (0002,0100) PrivateInformationCreatorUID, UI, N * (0002,0102) PrivateInformation, OB, N If `enforce_standard` is True then (0002,0013) will be added/updated. Encoding ~~~~~~~~ The encoding of the File Meta Information shall be Explicit VR Little Endian Parameters ---------- fp : file-like The file-like to write the File Meta Information to. file_meta : pydicom.dataset.Dataset The File Meta Information DataElements. enforce_standard : bool If False, then only the File Meta Information elements already in `file_meta` will be written to `fp`. If True (default) then a DICOM Standards conformant File Meta will be written to `fp`. Raises ------ ValueError If `enforce_standard` is True and any of the required File Meta Information elements are missing from `file_meta`, with the exception of (0002,0000), (0002,0001) and (0002,0012). ValueError If any non-Group 2 Elements are present in `file_meta`. """ # Check that no non-Group 2 Elements are present for elem in file_meta: if elem.tag.group != 0x0002: raise ValueError("Only File Meta Information Group (0002,eeee) " "elements must be present in 'file_meta'.") # The Type 1 File Meta Elements are only required when `enforce_standard` # is True, except for FileMetaInformationGroupLength and # FileMetaInformationVersion, which may or may not be present if enforce_standard: # Will be updated with the actual length later if 'FileMetaInformationGroupLength' not in file_meta: file_meta.FileMetaInformationGroupLength = 0 if 'FileMetaInformationVersion' not in file_meta: file_meta.FileMetaInformationVersion = b'\x00\x01' if 'ImplementationClassUID' not in file_meta: file_meta.ImplementationClassUID = PYDICOM_IMPLEMENTATION_UID if 'ImplementationVersionName' not in file_meta: file_meta.ImplementationVersionName = 'PYDICOM ' + __version__ # Check that required File Meta Elements are present missing = [] for element in [0x0002, 0x0003, 0x0010]: if Tag(0x0002, element) not in file_meta: missing.append(Tag(0x0002, element)) if missing: msg = "Missing required File Meta Information elements from " \ "'file_meta':\n" for tag in missing: msg += '\t{0} {1}\n'.format(tag, keyword_for_tag(tag)) raise ValueError(msg[:-1]) # Remove final newline # Only used if FileMetaInformationGroupLength is present. # FileMetaInformationGroupLength has a VR of 'UL' and so has a value that # is 4 bytes fixed. The total length of when encoded as Explicit VR must # therefore be 12 bytes. end_group_length_elem = fp.tell() + 12 # The 'is_little_endian' and 'is_implicit_VR' attributes will need to be # set correctly after the File Meta Info has been written. fp.is_little_endian = True fp.is_implicit_VR = False # Write the File Meta Information Group elements to `fp` write_dataset(fp, file_meta) # If FileMetaInformationGroupLength is present it will be the first written # element and we must update its value to the correct length. if 'FileMetaInformationGroupLength' in file_meta: # Save end of file meta to go back to end_of_file_meta = fp.tell() # Update the FileMetaInformationGroupLength value, which is the number # of bytes from the end of the FileMetaInformationGroupLength element # to the end of all the File Meta Information elements group_length = int(end_of_file_meta - end_group_length_elem) file_meta.FileMetaInformationGroupLength = group_length fp.seek(end_group_length_elem - 12) write_data_element(fp, file_meta[0x00020000]) # Return to end of the file meta, ready to write remainder of the file fp.seek(end_of_file_meta)
def testCleanName(self): """dicom_dictionary: CleanName returns correct strings.............""" self.assertTrue(CleanName(0x00100010) == "PatientsName") self.assertTrue(CleanName(Tag((0x0010, 0x0010))) == "PatientsName")
def testTagWithoutEncodingPython3(self): """RawDataElement: raises if no encoding given in Python 3.""" self.assertRaises( TypeError, RawDataElement(Tag(0x00104000), 'LT', 14, b'comment1\\comment2', 0, False, True))
def testPrivate1(self): """private dict: """ self.assertTrue(CleanName(0x00100010) == "PatientsName") self.assertTrue(CleanName(Tag((0x0010, 0x0010))) == "PatientsName")
def test(): tag = Tag(0x00100010) with tag_in_exception(tag) as tag: raise ValueError('Test message.')
def primitive_to_message(self, primitive): """Convert a DIMSE `primitive` to the current DIMSEMessage object. Parameters ---------- primitive A DIMSE message primitive from pynetdicom3.dimse_primitives to convert to the current DIMSEMessage object. """ # pylint: disable=too-many-branches,too-many-statements # Command Set # Due to the del self.command_set[elem.tag] line below this may # end up permanently removing the element from the DIMSE message class # so we refresh the command set elements cls_type_name = self.__class__.__name__.replace('_', '-') command_set_tags = [elem.tag for elem in self.command_set] if cls_type_name not in _COMMAND_SET_ELEM: raise ValueError("Can't convert primitive to message for unknown " "DIMSE message type '{}'".format(cls_type_name)) for tag in _COMMAND_SET_ELEM[cls_type_name]: if tag not in command_set_tags: tag = Tag(tag) vr = dcm_dict[tag][0] try: self.command_set.add_new(tag, vr, None) except TypeError: self.command_set.add_new(tag, vr, '') # Convert the message command set to the primitive attributes for elem in self.command_set: # Use the short version of the element names as these should # match the parameter names in the primitive if hasattr(primitive, elem.keyword): # If value hasn't been set for a parameter then delete # the corresponding element attr = getattr(primitive, elem.keyword) if attr is not None: elem.value = attr else: del self.command_set[elem.tag] # Careful! # Theres a one-to-one relationship in the _MESSAGE_TYPES dict, so # invert it for convenience rev_type = {} for value in _MESSAGE_TYPES: rev_type[_MESSAGE_TYPES[value]] = value self.command_set.CommandField = rev_type[cls_type_name] # Data Set # Default to no Data Set self.data_set = BytesIO() self.command_set.CommandDataSetType = 0x0101 try: # These message types *may* have a dataset dataset_keyword = _DATASET_KEYWORDS[self.__class__.__name__] self.data_set = getattr(primitive, dataset_keyword) if self.data_set: self.command_set.CommandDataSetType = 0x0001 except KeyError: # The following message types never have a dataset # 'C_ECHO_RQ', 'C_ECHO_RSP', 'N_DELETE_RQ', 'C_STORE_RSP', # 'C_CANCEL_RQ', 'N_DELETE_RSP', 'C_FIND_RSP', 'N_GET_RQ' pass # Set the Command Set length self._set_command_group_length()
def get_frame_offsets(fp): """Return a list of the fragment offsets from the Basic Offset Table. **Basic Offset Table** The Basic Offset Table Item must be present and have a tag (FFFE,E000) and a length, however it may or may not have a value. Basic Offset Table with no value :: Item Tag | Length | FE FF 00 E0 00 00 00 00 Basic Offset Table with value (2 frames) :: Item Tag | Length | Offset 1 | Offset 2 | FE FF 00 E0 08 00 00 00 00 00 00 00 10 00 00 00 For single or multi-frame images with only one frame, the Basic Offset Table may or may not have a value. When it has no value then its length shall be ``0x00000000``. For multi-frame images with more than one frame, the Basic Offset Table should have a value containing concatenated 32-bit unsigned integer values that are the byte offsets to the first byte of the Item tag of the first fragment of each frame as measured from the first byte of the first item tag following the Basic Offset Table Item. All decoders, both for single and multi-frame images should accept both an empty Basic Offset Table and one containing offset values. .. versionchanged:: 1.4 Changed to return (is BOT empty, list of offsets). Parameters ---------- fp : filebase.DicomBytesIO The encapsulated pixel data positioned at the start of the Basic Offset Table. ``fp.is_little_endian`` should be set to ``True``. Returns ------- bool, list of int Whether or not the BOT is empty, and a list of the byte offsets to the first fragment of each frame, as measured from the start of the first item following the Basic Offset Table item. Raises ------ ValueError If the Basic Offset Table item's tag is not (FFEE,E000) or if the length in bytes of the item's value is not a multiple of 4. References ---------- DICOM Standard, Part 5, :dcm:`Annex A.4 <part05/sect_A.4.html>` """ if not fp.is_little_endian: raise ValueError("'fp.is_little_endian' must be True") tag = Tag(fp.read_tag()) if tag != 0xfffee000: raise ValueError("Unexpected tag '{}' when parsing the Basic Table " "Offset item.".format(tag)) length = fp.read_UL() if length % 4: raise ValueError("The length of the Basic Offset Table item is not " "a multiple of 4.") offsets = [] # Always return at least a 0 offset if length == 0: offsets.append(0) for ii in range(length // 4): offsets.append(fp.read_UL()) return bool(length), offsets
def primitive_to_message(self, primitive): """Convert a DIMSE `primitive` to the current DIMSEMessage object. Parameters ---------- primitive The pynetdicom3.dimse_primitives DIMSE service primitive to convert to the current DIMSEMessage object. """ # pylint: disable=too-many-branches,too-many-statements # Command Set # Due to the del self.command_set[elem.tag] line below this may # end up permanently removing the element from the DIMSE message class # so we refresh the command set elements cls_type_name = self.__class__.__name__.replace('_', '-') command_set_tags = [elem.tag for elem in self.command_set] if cls_type_name not in _COMMAND_SET_ELEM: raise ValueError("Can't convert primitive to message for unknown " "DIMSE message type '{}'".format(cls_type_name)) for tag in _COMMAND_SET_ELEM[cls_type_name]: if tag not in command_set_tags: tag = Tag(tag) vr = dcm_dict[tag][0] try: self.command_set.add_new(tag, vr, None) except TypeError: self.command_set.add_new(tag, vr, '') # Convert the message command set to the primitive attributes for elem in self.command_set: # Use the short version of the element names as these should # match the parameter names in the primitive if hasattr(primitive, elem.keyword): # If value hasn't been set for a parameter then delete # the corresponding element attr = getattr(primitive, elem.keyword) if attr is not None: elem.value = attr else: del self.command_set[elem.tag] # Careful! # Theres a one-to-one relationship in the _MESSAGE_TYPES dict, so invert # it for convenience rev_type = {} for value in _MESSAGE_TYPES: rev_type[_MESSAGE_TYPES[value]] = value self.command_set.CommandField = rev_type[cls_type_name] # Data Set # Default to no Data Set self.data_set = BytesIO() self.command_set.CommandDataSetType = 0x0101 # TODO: This can probably be refactored to be cleaner # dict = {['C_STORE_RQ'] : 'DataSet', # ['C_FIND_RQ', 'C_GET_RQ'] : 'Identifier'} # for cls_names in dict.keys(): # if cls_type_name in cls_names and hasattr(primitive, dict[cls_type_name]): # self.data_set = getattr(primitive.dict[cls_type_name]) # self.command_set.CommandDataSetType = 0x0001 cls_type_name = self.__class__.__name__ if cls_type_name == 'C_STORE_RQ': self.data_set = primitive.DataSet self.command_set.CommandDataSetType = 0x0001 elif cls_type_name in ['C_FIND_RQ', 'C_GET_RQ', 'C_MOVE_RQ', 'C_FIND_RSP', 'C_GET_RSP', 'C_MOVE_RSP'] and \ primitive.Identifier: self.data_set = primitive.Identifier self.command_set.CommandDataSetType = 0x0001 elif cls_type_name == 'N_EVENT_REPORT_RQ': self.data_set = primitive.EventInformation self.command_set.CommandDataSetType = 0x0001 elif cls_type_name == 'N_EVENT_REPORT_RSP': self.data_set = primitive.EventReply self.command_set.CommandDataSetType = 0x0001 elif cls_type_name in [ 'N_GET_RSP', 'N_SET_RSP', 'N_CREATE_RQ', 'N_CREATE_RSP' ]: self.data_set = primitive.AttributeList self.command_set.CommandDataSetType = 0x0001 elif cls_type_name == 'N_SET_RQ': self.data_set = primitive.ModificationList self.command_set.CommandDataSetType = 0x0001 elif cls_type_name == 'N_ACTION_RQ': self.data_set = primitive.ActionInformation self.command_set.CommandDataSetType = 0x0001 elif cls_type_name == 'N_ACTION_RSP': self.data_set = primitive.ActionReply self.command_set.CommandDataSetType = 0x0001 # The following message types don't have a dataset # 'C_ECHO_RQ', 'C_ECHO_RSP', 'N_DELETE_RQ', 'C_STORE_RSP', # 'C_CANCEL_RQ', 'N_DELETE_RSP', 'C_FIND_RSP' # Set the Command Set length self._set_command_group_length()
def data_element_generator(fp, is_implicit_VR, is_little_endian, stop_when=None, defer_size=None, encoding=default_encoding, specific_tags=None): """Create a generator to efficiently return the raw data elements. Parameters ---------- fp : file-like object is_implicit_VR : boolean is_little_endian : boolean stop_when : None, callable, optional If None (default), then the whole file is read. A callable which takes tag, VR, length, and returns True or False. If it returns True, read_data_element will just return. defer_size : int, str, None, optional See ``dcmread`` for parameter info. encoding : Encoding scheme specific_tags : list or None See ``dcmread`` for parameter info. Returns ------- VR : None if implicit VR, otherwise the VR read from the file length : the length as in the DICOM data element (could be DICOM "undefined length" 0xffffffffL) value_bytes : the raw bytes from the DICOM file (not parsed into python types) is_little_endian : boolean True if transfer syntax is little endian; else False. """ # Summary of DICOM standard PS3.5-2008 chapter 7: # If Implicit VR, data element is: # tag, 4-byte length, value. # The 4-byte length can be FFFFFFFF (undefined length)* # # If Explicit VR: # if OB, OW, OF, SQ, UN, or UT: # tag, VR, 2-bytes reserved (both zero), 4-byte length, value # For all but UT, the length can be FFFFFFFF (undefined length)* # else: (any other VR) # tag, VR, (2 byte length), value # * for undefined length, a Sequence Delimitation Item marks the end # of the Value Field. # Note, except for the special_VRs, both impl and expl VR use 8 bytes; # the special VRs follow the 8 bytes with a 4-byte length # With a generator, state is stored, so we can break down # into the individual cases, and not have to check them again for each # data element if is_little_endian: endian_chr = "<" else: endian_chr = ">" if is_implicit_VR: element_struct = Struct(endian_chr + "HHL") else: # Explicit VR # tag, VR, 2-byte length (or 0 if special VRs) element_struct = Struct(endian_chr + "HH2sH") extra_length_struct = Struct(endian_chr + "L") # for special VRs extra_length_unpack = extra_length_struct.unpack # for lookup speed # Make local variables so have faster lookup fp_read = fp.read fp_tell = fp.tell logger_debug = logger.debug debugging = config.debugging element_struct_unpack = element_struct.unpack defer_size = size_in_bytes(defer_size) tag_set = set() has_specific_char_set = True if specific_tags is not None: for tag in specific_tags: if isinstance(tag, (str, compat.text_type)): tag = Tag(tag_for_keyword(tag)) if isinstance(tag, BaseTag): tag_set.add(tag) has_specific_char_set = Tag(0x08, 0x05) in tag_set tag_set.add(Tag(0x08, 0x05)) has_tag_set = len(tag_set) > 0 while True: # Read tag, VR, length, get ready to read value bytes_read = fp_read(8) if len(bytes_read) < 8: return # at end of file if debugging: debug_msg = "{0:08x}: {1}".format(fp.tell() - 8, bytes2hex(bytes_read)) if is_implicit_VR: # must reset VR each time; could have set last iteration (e.g. SQ) VR = None group, elem, length = element_struct_unpack(bytes_read) else: # explicit VR group, elem, VR, length = element_struct_unpack(bytes_read) if not in_py2: VR = VR.decode(default_encoding) if VR in extra_length_VRs: bytes_read = fp_read(4) length = extra_length_unpack(bytes_read)[0] if debugging: debug_msg += " " + bytes2hex(bytes_read) if debugging: debug_msg = "%-47s (%04x, %04x)" % (debug_msg, group, elem) if not is_implicit_VR: debug_msg += " %s " % VR if length != 0xFFFFFFFF: debug_msg += "Length: %d" % length else: debug_msg += "Length: Undefined length (FFFFFFFF)" logger_debug(debug_msg) # Positioned to read the value, but may not want to -- check stop_when value_tell = fp_tell() tag = TupleTag((group, elem)) if stop_when is not None: # XXX VR may be None here!! Should stop_when just take tag? if stop_when(tag, VR, length): if debugging: logger_debug("Reading ended by stop_when callback. " "Rewinding to start of data element.") rewind_length = 8 if not is_implicit_VR and VR in extra_length_VRs: rewind_length += 4 fp.seek(value_tell - rewind_length) return # Reading the value # First case (most common): reading a value with a defined length if length != 0xFFFFFFFF: # don't defer loading of Specific Character Set value as it is # needed immediately to get the character encoding for other tags if has_tag_set and tag not in tag_set: # skip the tag if not in specific tags fp.seek(fp_tell() + length) continue if (defer_size is not None and length > defer_size and tag != BaseTag(0x00080005)): # Flag as deferred by setting value to None, and skip bytes value = None logger_debug("Defer size exceeded. " "Skipping forward to next data element.") fp.seek(fp_tell() + length) else: value = fp_read(length) if debugging: dotdot = " " if length > 12: dotdot = "..." logger_debug("%08x: %-34s %s %r %s" % (value_tell, bytes2hex( value[:12]), dotdot, value[:12], dotdot)) # If the tag is (0008,0005) Specific Character Set, then store it if tag == BaseTag(0x00080005): from pydicom.values import convert_string encoding = convert_string(value, is_little_endian, encoding=default_encoding) # Store the encoding value in the generator # for use with future elements (SQs) encoding = convert_encodings(encoding) if not has_specific_char_set: continue yield RawDataElement(tag, VR, length, value, value_tell, is_implicit_VR, is_little_endian) # Second case: undefined length - must seek to delimiter, # unless is SQ type, in which case is easier to parse it, because # undefined length SQs and items of undefined lengths can be nested # and it would be error-prone to read to the correct outer delimiter else: # Try to look up type to see if is a SQ # if private tag, won't be able to look it up in dictionary, # in which case just ignore it and read the bytes unless it is # identified as a Sequence if VR is None: try: VR = dictionary_VR(tag) except KeyError: # Look ahead to see if it consists of items # and is thus a SQ next_tag = TupleTag(unpack(endian_chr + "HH", fp_read(4))) # Rewind the file fp.seek(fp_tell() - 4) if next_tag == ItemTag: VR = 'SQ' if VR == 'SQ': if debugging: msg = "{0:08x}: Reading/parsing undefined length sequence" logger_debug(msg.format(fp_tell())) seq = read_sequence(fp, is_implicit_VR, is_little_endian, length, encoding) if has_tag_set and tag not in tag_set: continue yield DataElement(tag, VR, seq, value_tell, is_undefined_length=True) else: delimiter = SequenceDelimiterTag if debugging: logger_debug("Reading undefined length data element") value = read_undefined_length_value(fp, is_little_endian, delimiter, defer_size) # If the tag is (0008,0005) Specific Character Set, # then store it if tag == (0x08, 0x05): from pydicom.values import convert_string encoding = convert_string(value, is_little_endian, encoding=default_encoding) # Store the encoding value in the generator for use # with future elements (SQs) encoding = convert_encodings(encoding) if not has_specific_char_set: continue # tags with undefined length are skipped after read if has_tag_set and tag not in tag_set: continue yield RawDataElement(tag, VR, length, value, value_tell, is_implicit_VR, is_little_endian)
def _build_message_classes(message_name): """ Create a new subclass instance of DIMSEMessage for the given DIMSE `message_name`. Parameters ---------- message_name : str The name/type of message class to construct, one of the following: * C-ECHO-RQ * C-ECHO-RSP * C-STORE-RQ * C-STORE-RSP * C-FIND-RQ * C-FIND-RSP * C-GET-RQ * C-GET-RSP * C-MOVE-RQ * C-MOVE-RSP * C-CANCEL-RQ * N-EVENT-REPORT-RQ * N-EVENT-REPORT-RSP * N-GET-RQ * N-GET-RSP * N-SET-RQ * N-SET-RSP * N-ACTION-RQ * N-ACTION-RSP * N-CREATE-RQ * N-CREATE-RSP * N-DELETE-RQ * N-DELETE-RSP """ def __init__(self): DIMSEMessage.__init__(self) # Create new subclass of DIMSE Message using the supplied name # but replace hyphens with underscores cls = type(message_name.replace('-', '_'), (DIMSEMessage, ), {"__init__": __init__}) # Create a new Dataset object for the command_set attributes ds = Dataset() for elem_tag in _COMMAND_SET_ELEM[message_name]: tag = Tag(elem_tag) vr = dcm_dict[elem_tag][0] # If the required command set elements are expanded this will need # to be checked to ensure it functions OK try: ds.add_new(tag, vr, None) except TypeError: ds.add_new(tag, vr, '') # Add the Command Set dataset to the class cls.command_set = ds # Add the class to the module globals()[cls.__name__] = cls return cls
def test_unknown_vr(self): """Test converting a raw element with unknown VR""" raw = RawDataElement(Tag(0x00080000), 'AA', 8, b'20170101', 0, False, True) with pytest.raises(NotImplementedError): DataElement_from_raw(raw, default_encoding)
def generate_dicom_sr(file_path, img_ds, data, series_description): """ Generates DICOM Structured Report files for the given file path. :param file_path: the file name and directory to save the DICOM SR file in. :param img_ds: A CT or MR image from the dataset used to pull general information for the DICOM SR. :param data: Text data to be written to the DICOM SR file. :param series_description: Description of text data written to SR. :return: dicom_sr, a dataset for the new DICOM SR file. """ if img_ds is None: raise ValueError("No CT data to initialize RT SS") # Create file meta file_meta = FileMetaDataset() file_meta.FileMetaInformationGroupLength = 238 file_meta.FileMetaInformationVersion = b'\x00\x01' file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.88.33' file_meta.MediaStorageSOPInstanceUID = pydicom.uid.generate_uid() file_meta.TransferSyntaxUID = ImplicitVRLittleEndian validate_file_meta(file_meta) # Create dataset dicom_sr = pydicom.dataset.FileDataset(file_path, {}, preamble=b"\0" * 128, file_meta=file_meta) dicom_sr.fix_meta_info() # Get current date and time now = datetime.datetime.now() dicom_date = now.strftime("%Y%m%d") dicom_time = now.strftime("%H%M") # List of tags to copy from CT/MR image top_level_tags_to_copy: list = [ Tag("PatientName"), Tag("PatientID"), Tag("PatientBirthDate"), Tag("PatientSex"), Tag("StudyDate"), Tag("StudyTime"), Tag("ReferringPhysicianName"), Tag("StudyDescription"), Tag("StudyInstanceUID"), Tag("StudyID"), Tag("RequestingService"), Tag("PatientAge"), Tag("PatientSize"), Tag("PatientWeight"), Tag("MedicalAlerts"), Tag("Allergies"), Tag("PregnancyStatus"), Tag("InstitutionName"), Tag("InstitutionAddress") ] # Copy tags from CT/MR image for tag in top_level_tags_to_copy: if tag in img_ds: dicom_sr[tag] = deepcopy(img_ds[tag]) dicom_sr.AccessionNumber = "" # == SR Document Series Module dicom_sr.SeriesDate = dicom_date dicom_sr.SeriesTime = dicom_time dicom_sr.Modality = "SR" dicom_sr.SeriesDescription = series_description # Can be empty referenced_performed_procedure_step_sequence = Sequence() dicom_sr.ReferencedPerformedProcedureStepSequence = \ referenced_performed_procedure_step_sequence dicom_sr.SeriesInstanceUID = pydicom.uid.generate_uid() dicom_sr.SeriesNumber = 1 # == General Equipment Module dicom_sr.Manufacturer = "OnkoDICOM" dicom_sr.ManufacturersModelName = "OnkoDICOM" # TODO: Pull this off build information in some way dicom_sr.SoftwareVersions = "2021" # == SR Document General Module dicom_sr.ContentDate = dicom_date dicom_sr.ContentTime = dicom_time dicom_sr.InstanceNumber = 1 # Empty if unknown performed_procedure_code_sequence = Sequence() dicom_sr.PerformedProcedureCodeSequence = performed_procedure_code_sequence # Do not want to mark as complete in case it isn't! dicom_sr.CompletionFlag = "PARTIAL" dicom_sr.VerificationFlag = "UNVERIFIED" # == SR Document Content Module referenced_sop_sequence = Sequence([Dataset()]) referenced_sop_sequence[0].ReferencedSOPClassUID = '' referenced_sop_sequence[0].ReferencedSOPInstanceUID = '' dicom_sr.ReferencedSOPSequence = referenced_sop_sequence dicom_sr.ValueType = "CONTAINER" dicom_sr.ContinuityOfContent = "CONTINUOUS" dicom_sr.TemporalRangeTime = "" dicom_sr.ReferencedTimeOffsets = "" dicom_sr.ReferencedDateTime = "" dicom_sr.MeasuredValueSequence = Sequence() og_frame_of_reference_UID = \ deepcopy(img_ds[Tag("FrameOfReferenceUID")].value) dicom_sr.ReferencedFrameOfReferenceUID = og_frame_of_reference_UID # == Content Sequence content_sequence = Sequence([Dataset()]) content_sequence[0].RelationshipType = 'CONTAINS' content_sequence[0].ValueType = 'TEXT' concept_name_code_sequence = Sequence([Dataset()]) concept_name_code_sequence[0].CodeValue = '' concept_name_code_sequence[0].CodingSchemeDesignator = '' concept_name_code_sequence[0].CodeMeaning = '' content_sequence[0].ConceptNameCodeSequence = concept_name_code_sequence content_sequence[0].TextValue = data dicom_sr.ContentSequence = content_sequence # == SOP Common Module dicom_sr.SOPClassUID = '1.2.840.10008.5.1.4.1.1.88.33' dicom_sr.SOPInstanceUID = file_meta.MediaStorageSOPInstanceUID dicom_sr.is_little_endian = True dicom_sr.is_implicit_VR = True return dicom_sr