def create_roi(rtss, roi_name, roi_coordinates, data_set, rt_roi_interpreted_type="ORGAN"): """ Create new ROI to rtss :param rtss: dataset of RTSS :param roi_name: ROIName :param roi_coordinates: Coordinates of pixels for new ROI :param data_set: Data Set of selected DICOM image file :return: rtss, with added ROI """ patient_dict_container = PatientDictContainer() existing_rois = patient_dict_container.get("rois") roi_exists = False # This is for adding a new slice to an already existing ROI. For Future Development. # Check to see if the ROI already exists for key, value in existing_rois.items(): if value["name"] == roi_name: roi_exists = True if not roi_exists: number_of_contour_points = len(roi_coordinates) / 3 referenced_sop_class_uid = data_set.SOPClassUID referenced_sop_instance_uid = data_set.SOPInstanceUID referenced_frame_of_reference_uid = rtss[ "StructureSetROISequence"].value[0].ReferencedFrameOfReferenceUID roi_number = rtss["StructureSetROISequence"].value[-1].ROINumber + 1 # Colour TBC red = random.randint(0, 255) green = random.randint(0, 255) blue = random.randint(0, 255) rgb = [red, green, blue] # Saving a new StructureSetROISequence structure_set_sequence = Sequence([Dataset()]) original_structure_set = rtss.StructureSetROISequence for structure_set in structure_set_sequence: structure_set.add_new(Tag("ROINumber"), 'IS', roi_number) structure_set.add_new(Tag("ReferencedFrameOfReferenceUID"), 'UI', referenced_frame_of_reference_uid) structure_set.add_new(Tag("ROIName"), 'LO', roi_name) structure_set.add_new(Tag("ROIGenerationAlgorithm"), 'CS', "") # Combine old and new structure set original_structure_set.extend(structure_set_sequence) rtss.add_new(Tag("StructureSetROISequence"), "SQ", original_structure_set) # Saving a new ROIContourSequence, ContourSequence, ContourImageSequence roi_contour_sequence = Sequence([Dataset()]) contour_sequence = Sequence([Dataset()]) contour_image_sequence = Sequence([Dataset()]) # Original File original_ROI_contour = rtss.ROIContourSequence # ROI Contour Sequence for roi_contour in roi_contour_sequence: roi_contour.add_new(Tag("ROIDisplayColor"), "IS", rgb) roi_contour.add_new(Tag("ContourSequence"), "SQ", contour_sequence) # ROI Sequence for contour in contour_sequence: # if data_set.get("ReferencedImageSequence"): contour.add_new(Tag("ContourImageSequence"), "SQ", contour_image_sequence) # Contour Sequence for contour_image in contour_image_sequence: contour_image.add_new( Tag("ReferencedSOPClassUID"), "UI", referenced_sop_class_uid) # CT Image Storage contour_image.add_new(Tag("ReferencedSOPInstanceUID"), "UI", referenced_sop_instance_uid) contour.add_new(Tag("ContourNumber"), "IS", 1) if not _is_closed_contour(roi_coordinates): contour.add_new(Tag("ContourGeometricType"), "CS", "OPEN_PLANAR") contour.add_new(Tag("NumberOfContourPoints"), "IS", number_of_contour_points) contour.add_new(Tag("ContourData"), "DS", roi_coordinates) else: contour.add_new(Tag("ContourGeometricType"), "CS", "CLOSED_PLANAR") contour.add_new(Tag("NumberOfContourPoints"), "IS", number_of_contour_points - 1) contour.add_new(Tag("ContourData"), "DS", roi_coordinates[0:-3]) roi_contour.add_new(Tag("ReferencedROINumber"), "IS", roi_number) # Combine original ROIContourSequence with new original_ROI_contour.extend(roi_contour_sequence) rtss.add_new(Tag("ROIContourSequence"), "SQ", original_ROI_contour) # Saving a new RTROIObservationsSequence RT_ROI_observations_sequence = Sequence([Dataset()]) original_ROI_observation_sequence = rtss.RTROIObservationsSequence for ROI_observations in RT_ROI_observations_sequence: # TODO: Check to make sure that there aren't multiple observations per ROI, e.g. increment from existing Observation Numbers? ROI_observations.add_new(Tag("ObservationNumber"), 'IS', roi_number) ROI_observations.add_new(Tag("ReferencedROINumber"), 'IS', roi_number) ROI_observations.add_new(Tag("RTROIInterpretedType"), 'CS', rt_roi_interpreted_type) original_ROI_observation_sequence.extend(RT_ROI_observations_sequence) rtss.add_new(Tag("RTROIObservationsSequence"), "SQ", original_ROI_observation_sequence) else: # Add contour image data to existing ROI rtss = add_to_roi(rtss, roi_name, roi_coordinates, data_set) return rtss
def read_dicom_image_series(image_folder, modality=None, series_uid=None): # Obtain a list with image files file_list = _find_dicom_image_series(image_folder=image_folder, allowed_modalities=["CT", "PT", "MR"], modality=modality, series_uid=series_uid) # Obtain slice positions for each file image_position_z = [] for file_name in file_list: # Read DICOM header dcm = pydicom.dcmread(os.path.join(image_folder, file_name), stop_before_pixels=True, force=True, specific_tags=[Tag(0x0020, 0x0032)]) # Obtain the z position image_position_z += [ get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0020, 0x0032), tag_type="mult_float")[2] ] # Order ascending position (DICOM: z increases from feet to head) file_table = pd.DataFrame({ "file_name": file_list, "position_z": image_position_z }).sort_values(by="position_z") # Obtain DICOM metadata from the bottom slice. This will be used to fill out all the different details. dcm = pydicom.dcmread(os.path.join(image_folder, file_table.file_name.values[0]), stop_before_pixels=True, force=True) # Find the number of rows (y) and columns (x) in the data set. n_x = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0028, 0x011), tag_type="int") n_y = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0028, 0x010), tag_type="int") # Create an empty voxel grid. Use z, y, x ordering for consistency within MIRP. voxel_grid = np.zeros((len(file_table), n_y, n_x), dtype=np.float32) # Read all dicom slices in order. slice_dcm_list = [ pydicom.dcmread(os.path.join(image_folder, file_name), stop_before_pixels=False, force=True) for file_name in file_table.file_name.values ] # Iterate over the different slices to fill out the voxel_grid. for ii, file_name in enumerate(file_table.file_name.values): # Read the dicom file and extract the slice grid slice_dcm = slice_dcm_list[ii] slice_grid = slice_dcm.pixel_array.astype(np.float32) # Update with scale and intercept. These may change per slice. rescale_intercept = get_pydicom_meta_tag(dcm_seq=slice_dcm, tag=(0x0028, 0x1052), tag_type="float", default=0.0) rescale_slope = get_pydicom_meta_tag(dcm_seq=slice_dcm, tag=(0x0028, 0x1053), tag_type="float", default=1.0) slice_grid = slice_grid * rescale_slope + rescale_intercept # Convert all images to SUV at admin if get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0008, 0x0060), tag_type="str") == "PT": suv_conversion_object = SUVscalingObj(dcm=slice_dcm) scale_factor = suv_conversion_object.get_scale_factor( suv_normalisation="bw") # Convert to SUV slice_grid *= scale_factor # Update the DICOM header slice_dcm = suv_conversion_object.update_dicom_header( dcm=slice_dcm) # Store in voxel grid voxel_grid[ii, :, :] = slice_grid # Obtain the image origin from the dicom header (note: z, y, x order) image_origin = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0020, 0x0032), tag_type="mult_float", default=np.array([0.0, 0.0, 0.0]))[::-1] # Obtain the image spacing from the dicom header and slice positions. image_pixel_spacing = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0028, 0x0030), tag_type="mult_float") image_slice_thickness = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0018, 0x0050), tag_type="float", default=None) if len(file_table) > 1: # Slice spacing can be determined from the slice positions image_slice_spacing = np.median( np.abs(np.diff(file_table.position_z.values))) if image_slice_thickness is None: # TODO: Update slice thickness tag in dcm pass else: # Warn the user if there is a mismatch between slice thickness and the actual slice spacing. if not np.around(image_slice_thickness - image_slice_spacing, decimals=5) == 0.0: warnings.warn( f"Mismatch between slice thickness ({image_slice_thickness}) and actual slice spacing ({image_slice_spacing}). The actual slice spacing will be " f"used.", UserWarning) elif image_slice_thickness is not None: # There is only one slice, and we use the slice thickness as parameter. image_slice_spacing = image_slice_thickness else: # There is only one slice and the slice thickness is unknown. In this situation, we use the pixel spacing image_slice_spacing = np.max(image_pixel_spacing) # Combine pixel spacing and slice spacing into the voxel spacing, using z, y, x order. image_spacing = np.array( [image_slice_spacing, image_pixel_spacing[1], image_pixel_spacing[0]]) # Obtain image orientation and add the 3rd dimension image_orientation = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0020, 0x0037), tag_type="mult_float") image_orientation += [0.0, 0.0, 1.0] # Revert to z, y, x order image_orientation = image_orientation[::-1] # Create an ImageClass object and store dicom meta-data img_obj = ImageClass(voxel_grid=voxel_grid, origin=image_origin, spacing=image_spacing, slice_z_pos=file_table.position_z.values, orientation=image_orientation, modality=get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0008, 0x0060), tag_type="str"), spat_transform="base", no_image=False, metadata=slice_dcm_list[0], metadata_sop_instances=[ get_pydicom_meta_tag(dcm_seq=slice_dcm, tag=(0x0008, 0x0018), tag_type="str") for slice_dcm in slice_dcm_list ]) return img_obj
def c_find(cls, ds: pydicom.Dataset): """C-FIND request handler for Study level :param ds: C-FIND request :type ds: pydicom.Dataset :yield: C-FIND result :rtype: pydicom.Dataset """ joins = set() response_attrs = [] select = [Study] upper_level_filters = [] patient_attrs = [e for e in ds if e.tag in Patient.mapping] skipped = set(e.tag for e in patient_attrs) if patient_attrs: upper_level_filters.extend( _filter_upper_level(Patient, patient_attrs)) for tag, attr, vr, _, attr_name in upper_level_filters: select.append(attr) response_attrs.append((tag, ('patient', attr_name), vr, None)) joins.add((Study, Patient)) if 'ModalitiesInStudy' in ds: _tag = Tag(0x0008, 0x0061) skipped.add(_tag) # TODO: Add modalities in study filter agg_fun = db.string_agg_func() select.append( agg_fun(Series.modality, '\\').alias('modalities_in_study')) func = lambda v: '\\'.join(set(v.split('\\'))) if v else v response_attrs.append((_tag, 'modalities_in_study', 'CS', func)) joins.add((Study, Series)) if 'SOPClassesInStudy' in ds: _tag = Tag(0x0008, 0x0062) skipped.add(_tag) agg_fun = db.string_agg_func() select.append( agg_fun(Instance.sop_class_uid, '\\').alias('sop_classes_in_study')) func = lambda v: '\\'.join(set(v.split('\\'))) if v else v response_attrs.append((_tag, 'sop_classes_in_study', 'UI', func)) joins.update([(Study, Series), (Series, Instance)]) if 'NumberOfStudyRelatedSeries' in ds: _tag = Tag(0x0020, 0x1206) skipped.add(_tag) select.append( peewee.fn.Count(Series.id)\ .alias('number_of_study_related_series') # pylint: disable=no-member ) response_attrs.append( (_tag, 'number_of_study_related_series', 'IS', None)) joins.add((Study, Series)) if 'NumberOfStudyRelatedInstances' in ds: _tag = Tag((0x0020, 0x1208)) skipped.add(_tag) select.append( peewee.fn.Count(Instance.id)\ .alias('number_of_study_related_instances') # pylint: disable=no-member ) response_attrs.append( (_tag, 'number_of_study_related_instances', 'IS', None)) joins.update([(Study, Series), (Series, Instance)]) query = Study.select(*select) for join in joins: query = query.join_from(*join) query, _response_attrs = _build_filters(cls, query, ds, skipped) response_attrs.extend(_response_attrs) for _, attr, vr, elem, _ in upper_level_filters: if not elem.value: continue query = _build_filter(query, attr, vr, elem) encoding = getattr(ds, 'SpecificCharacterSet', 'ISO-IR 6') if not query.count(): return [] yield from (_encode_response(s, response_attrs, encoding) for s in query)
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. 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 ------- list of int 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 offsets
def write_tag(self, tag): """Write a dicom tag (two unsigned shorts) to the file.""" tag = Tag(tag) # make sure is an instance of class, not just a tuple or int self.write_US(tag.group) self.write_US(tag.element)
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_assignment(self): """Check assignment works correctly""" primitive = N_GET() # AffectedSOPClassUID primitive.AffectedSOPClassUID = '1.1.1' assert primitive.AffectedSOPClassUID == UID('1.1.1') assert isinstance(primitive.AffectedSOPClassUID, UID) primitive.AffectedSOPClassUID = UID('1.1.2') assert primitive.AffectedSOPClassUID == UID('1.1.2') assert isinstance(primitive.AffectedSOPClassUID, UID) primitive.AffectedSOPClassUID = b'1.1.3' assert primitive.AffectedSOPClassUID == UID('1.1.3') assert isinstance(primitive.AffectedSOPClassUID, UID) # AffectedSOPInstanceUID primitive.AffectedSOPInstanceUID = b'1.2.1' assert primitive.AffectedSOPInstanceUID == UID('1.2.1') assert isinstance(primitive.AffectedSOPClassUID, UID) primitive.AffectedSOPInstanceUID = UID('1.2.2') assert primitive.AffectedSOPInstanceUID == UID('1.2.2') assert isinstance(primitive.AffectedSOPClassUID, UID) primitive.AffectedSOPInstanceUID = '1.2.3' assert primitive.AffectedSOPInstanceUID == UID('1.2.3') assert isinstance(primitive.AffectedSOPClassUID, UID) # AttributeList ref_ds = Dataset() ref_ds.PatientID = '1234567' primitive.AttributeList = BytesIO(encode(ref_ds, True, True)) ds = decode(primitive.AttributeList, True, True) assert ds.PatientID == '1234567' # AttributeIdentifierList primitive.AttributeIdentifierList = [ 0x00001000, (0x0000, 0x1000), Tag(0x7fe0, 0x0010) ] assert [Tag(0x0000, 0x1000), Tag(0x0000, 0x1000), Tag(0x7fe0, 0x0010)] == primitive.AttributeIdentifierList primitive.AttributeIdentifierList = [(0x7fe0, 0x0010)] assert [Tag(0x7fe0, 0x0010)] == primitive.AttributeIdentifierList primitive.AttributeIdentifierList = (0x7fe0, 0x0010) assert [Tag(0x7fe0, 0x0010)] == primitive.AttributeIdentifierList elem = DataElement((0x0000, 0x0005), 'AT', [Tag(0x0000, 0x1000)]) assert isinstance(elem.value, MutableSequence) primitive.AttributeIdentifierList = elem.value assert [Tag(0x0000, 0x1000)] == primitive.AttributeIdentifierList # MessageID primitive.MessageID = 11 assert 11 == primitive.MessageID # MessageIDBeingRespondedTo primitive.MessageIDBeingRespondedTo = 13 assert 13 == primitive.MessageIDBeingRespondedTo # RequestedSOPClassUID primitive.RequestedSOPClassUID = '1.1.1' assert primitive.RequestedSOPClassUID == UID('1.1.1') assert isinstance(primitive.RequestedSOPClassUID, UID) primitive.RequestedSOPClassUID = UID('1.1.2') assert primitive.RequestedSOPClassUID == UID('1.1.2') assert isinstance(primitive.RequestedSOPClassUID, UID) primitive.RequestedSOPClassUID = b'1.1.3' assert primitive.RequestedSOPClassUID == UID('1.1.3') assert isinstance(primitive.RequestedSOPClassUID, UID) # RequestedSOPInstanceUID primitive.RequestedSOPInstanceUID = b'1.2.1' assert primitive.RequestedSOPInstanceUID == UID('1.2.1') assert isinstance(primitive.RequestedSOPInstanceUID, UID) primitive.RequestedSOPInstanceUID = UID('1.2.2') assert primitive.RequestedSOPInstanceUID == UID('1.2.2') assert isinstance(primitive.RequestedSOPInstanceUID, UID) primitive.RequestedSOPInstanceUID = '1.2.3' assert primitive.RequestedSOPInstanceUID == UID('1.2.3') assert isinstance(primitive.RequestedSOPInstanceUID, UID) # Status primitive.Status = 0x0000 assert primitive.Status == 0x0000
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 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 data_element_generator( fp: BinaryIO, is_implicit_VR: bool, is_little_endian: bool, stop_when: Optional[Callable[[BaseTag, Optional[str], int], bool]] = None, defer_size: Optional[Union[int, str, float]] = None, encoding: Union[str, MutableSequence[str]] = default_encoding, specific_tags: Optional[List[BaseTag]] = None ) -> Iterator[Union[RawDataElement, DataElement]]: """Create a generator to efficiently return the raw data elements. .. note:: This function is used internally - usually there is no need to call it from user code. To read data from a DICOM file, :func:`dcmread` shall be used instead. Parameters ---------- fp : file-like The file-like to read from. is_implicit_VR : bool ``True`` if the data is encoded as implicit VR, ``False`` otherwise. is_little_endian : bool ``True`` if the data is encoded as little endian, ``False`` otherwise. 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 or float, optional See :func:`dcmread` for parameter info. encoding : Union[str, MutableSequence[str]] Encoding scheme specific_tags : list or None See :func:`dcmread` for parameter info. Yields ------- RawDataElement or DataElement Yields DataElement for undefined length UN or SQ, RawDataElement otherwise. """ # 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 from pydicom.values import convert_string if is_little_endian: endian_chr = "<" else: endian_chr = ">" # assign implicit VR struct to variable as use later if VR assumed missing implicit_VR_struct = Struct(endian_chr + "HHL") if is_implicit_VR: element_struct = implicit_VR_struct 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 = {Tag(tag) for tag in specific_tags} if specific_tags else set() has_tag_set = bool(tag_set) if has_tag_set: tag_set.add(Tag(0x00080005)) # Specific Character Set while True: # VR: Optional[str] # 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 = f"{fp.tell() - 8:08x}: {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) # defend against switching to implicit VR, some writer do in SQ's # issue 1067, issue 1035 if not (b'AA' <= VR <= b'ZZ') and config.assume_implicit_vr_switch: # invalid VR, must be 2 cap chrs, assume implicit and continue VR = None group, elem, length = implicit_VR_struct.unpack(bytes_read) else: 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 length > 0 else cast( Optional[bytes], empty_value_for_VR(VR, raw=True))) if debugging: dotdot = "..." if length > 20 else " " displayed_value = value[:20] if value else b'' logger_debug("%08x: %-34s %s %r %s" % (value_tell, bytes2hex(displayed_value), dotdot, displayed_value, dotdot)) # If the tag is (0008,0005) Specific Character Set, then store it if tag == BaseTag(0x00080005): # *Specific Character String* is b'' for empty value encoding = convert_string( cast(bytes, value) or b'', is_little_endian) # Store the encoding value in the generator # for use with future elements (SQs) encoding = convert_encodings(encoding) 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: # VR UN with undefined length shall be handled as SQ # see PS 3.5, section 6.2.2 if VR == 'UN': VR = 'SQ' # 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 or VR == 'UN' and config.replace_un_with_known_vr: try: VR = dictionary_VR(tag) except KeyError: # Look ahead to see if it consists of items # and is thus a SQ next_tag = _unpack_tag(fp_read(4), endian_chr) # Rewind the file fp.seek(fp_tell() - 4) if next_tag == ItemTag: VR = 'SQ' if VR == 'SQ': if debugging: logger_debug( f"{fp_tell():08X}: Reading/parsing undefined length " "sequence") 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) # 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 dcmread( fp: Union[PathType, BinaryIO, DicomFileLike], defer_size: Optional[Union[str, int, float]] = None, stop_before_pixels: bool = False, force: bool = False, specific_tags: Optional[TagListType] = None ) -> Union[FileDataset, DicomDir]: """Read and parse a DICOM dataset stored in the DICOM File Format. Read a DICOM dataset stored in accordance with the :dcm:`DICOM File Format <part10/chapter_7.html>`. If the dataset is not stored in accordance with the File Format (i.e. the preamble and prefix are missing, there are missing required Type 1 *File Meta Information Group* elements or the entire *File Meta Information* is missing) then you will have to set `force` to ``True``. .. deprecated:: 2.2 Returning a :class:`~pydicom.dicomdir.DicomDir` is deprecated and will be removed in v3.0. Use :class:`~pydicom.fileset.FileSet` instead. Examples -------- Read and return a dataset stored in accordance with the DICOM File Format: >>> ds = pydicom.dcmread("CT_small.dcm") >>> ds.PatientName Read and return a dataset not in accordance with the DICOM File Format: >>> ds = pydicom.dcmread("rtplan.dcm", force=True) >>> ds.PatientName Use within a context manager: >>> with pydicom.dcmread("rtplan.dcm") as ds: ... ds.PatientName Parameters ---------- fp : str or PathLike or file-like Either a file-like object, a string containing the file name or the path to the file. The file-like object must have ``seek()``, ``read()`` and ``tell()`` methods and the caller is responsible for closing it (if required). defer_size : int, str or float, optional If not used then all elements are read into memory. If specified, then if a data element's stored value is larger than `defer_size`, the value is not read into memory until it is accessed in code. Should be the number of bytes to be read as :class:`int` or as a :class:`str` with units, e.g. ``'512 KB'``, ``'2 MB'``. stop_before_pixels : bool, optional If ``False`` (default), the full file will be read and parsed. Set ``True`` to stop before reading (7FE0,0010) *Pixel Data* (and all subsequent elements). force : bool, optional If ``False`` (default), raises an :class:`~pydicom.errors.InvalidDicomError` if the file is missing the *File Meta Information* header. Set to ``True`` to force reading even if no *File Meta Information* header is found. specific_tags : list of (int or str or 2-tuple of int), optional If used the only the supplied tags will be returned. The supplied elements can be tags or keywords. Note that the element (0008,0005) *Specific Character Set* is always returned if present - this ensures correct decoding of returned text values. Returns ------- FileDataset or DicomDir An instance of :class:`~pydicom.dataset.FileDataset` that represents a parsed DICOM file, unless the dataset is a *Media Storage Directory* instance in which case it will be a :class:`~pydicom.dicomdir.DicomDir`. Raises ------ InvalidDicomError If `force` is ``False`` and the file is not a valid DICOM file. TypeError If `fp` is ``None`` or of an unsupported type. See Also -------- pydicom.dataset.FileDataset Data class that is returned. pydicom.filereader.read_partial Only read part of a DICOM file, stopping on given conditions. """ # Open file if not already a file object caller_owns_file = True fp = path_from_pathlike(fp) if isinstance(fp, str): # caller provided a file name; we own the file handle caller_owns_file = False logger.debug("Reading file '{0}'".format(fp)) fp = open(fp, 'rb') elif fp is None or not hasattr(fp, "read") or not hasattr(fp, "seek"): raise TypeError("dcmread: Expected a file path or a file-like, " "but got " + type(fp).__name__) if config.debugging: logger.debug("\n" + "-" * 80) logger.debug("Call to dcmread()") msg = ("filename:'%s', defer_size='%s', " "stop_before_pixels=%s, force=%s, specific_tags=%s") logger.debug( msg % (fp.name, defer_size, stop_before_pixels, force, specific_tags)) if caller_owns_file: logger.debug("Caller passed file object") else: logger.debug("Caller passed file name") logger.debug("-" * 80) if specific_tags: specific_tags = [Tag(t) for t in specific_tags] specific_tags = cast(Optional[List[BaseTag]], specific_tags) # Iterate through all items and store them --include file meta if present stop_when = None if stop_before_pixels: stop_when = _at_pixel_data try: dataset = read_partial( fp, stop_when, defer_size=size_in_bytes(defer_size), force=force, specific_tags=specific_tags, ) finally: if not caller_owns_file: fp.close() # XXX need to store transfer syntax etc. return dataset
class DataElementFactory(factory.Factory): """Generates pydicom DataElements. Will always match VR and random value to given values >>> DataElementFactory(tag='PatientName').vr = 'PN' >>> DataElementFactory(tag='PatientName').value = 'JONES^Sarah' You can still set custom values as well: >>> DataElementFactory(tag='PatientName', value='123').value = '123' Notes ----- For an unknown tag without an explicit VR, this factory will assign a LongString (LO) VR: >>> DataElementFactory(tag=('ee011020')).VR = 'LO' If this is not what you want, pass an explicit VR: >>> DataElementFactory(tag=('ee011020'), VR='SL', value=-10.2) """ class Meta: model = pydicom.dataelem.DataElement tag = Tag('PatientID') @factory.lazy_attribute def VR(self): """Find the correct Value Representation for this tag from pydicom""" try: return dictionary_VR(Tag(self.tag)) except KeyError as e: # unknown tag. Just return set value, assuming user want to just # get on with it return VRs.LongString.short_name @factory.lazy_attribute def value(self): """Generate a valid mock value for this type of VR Raises ------ DataElementFactoryException If a value cannot be generated """ faker = Faker() faker.add_provider(DICOMVRProvider) vr = VRs.short_name_to_vr(self.VR) if vr == VRs.ApplicationEntity: return "MockEntity" elif vr == VRs.AgeString: return f"{factory.random.randgen.randint(0,120):03d}Y" elif vr == VRs.AttributeTag: return 0x0010, 0x0010 elif vr == VRs.CodeString: return "MockCodeString" elif vr == VRs.Date: return faker.dicom_date() elif vr == VRs.DecimalString: return "+10.4" elif vr == VRs.DateTime: return faker.dicom_date() + faker.dicom_time() elif vr == VRs.FloatingPointSingle: return 1.1 elif vr == VRs.FloatingPointDouble: return 1.123 elif vr == VRs.IntegerString: return f"{factory.random.randgen.randint(-2**31,2**31)}" elif vr == VRs.LongString: return faker.sentence()[:64] elif vr == VRs.LongText: return faker.text()[:10240] elif vr == VRs.OtherByteString: return b'\x13\00' elif vr == VRs.OtherDoubleString: return "MockDoubleString" elif vr == VRs.OtherFloatString: return "MockFloatString" elif vr == VRs.OtherWordString: return "MockOtherWordString" elif vr == VRs.PersonName: return faker.dicom_person_name() elif vr == VRs.ShortString: return "MockShortString" elif vr == VRs.SignedLong: return factory.random.randgen.randint(-2**32, 2**32) elif vr == VRs.Sequence: return [] elif vr == VRs.SignedShort: return factory.random.randgen.randint(-2**16, 2**16) elif vr == VRs.ShortText: return faker.sentence() elif vr == VRs.Time: return faker.dicom_time() elif vr == VRs.UniqueIdentifier: return faker.dicom_ui() elif vr == VRs.UnsignedLong: return factory.random.randgen.randint(0, 2**32) elif vr == VRs.Unknown: return "MockUnknown" elif vr == VRs.UnsignedShort: return factory.random.randgen.randint(0, 2**16) elif vr == VRs.UnlimitedText: return faker.text() else: raise DataElementFactoryException( f"I dont know how to generate a mock value for" f" {vr}, the VR of '{self.tag}'")
def add_to_roi(rtss, roi_name, roi_coordinates, data_set): """ Add new contour image sequence ROI to rtss :param rtss: dataset of RTSS :param roi_name: ROIName :param roi_coordinates: Coordinates of pixels for new ROI :param data_set: Data Set of selected DICOM image file :return: rtss, with added ROI """ # Creating a new ROIContourSequence, ContourSequence, ContourImageSequence contour_sequence = Sequence([Dataset()]) contour_image_sequence = Sequence([Dataset()]) number_of_contour_points = len(roi_coordinates) / 3 referenced_sop_class_uid = data_set.SOPClassUID referenced_sop_instance_uid = data_set.SOPInstanceUID existing_roi_number = None for item in rtss["StructureSetROISequence"]: if item.ROIName == roi_name: existing_roi_number = item.ROINumber position = None # Get the index of the ROI for index, contour in enumerate(rtss.ROIContourSequence): if contour.ReferencedROINumber == existing_roi_number: position = index new_contour_number = len( rtss.ROIContourSequence[position].ContourSequence) + 1 # ROI Sequence for contour in contour_sequence: # if data_set.get("ReferencedImageSequence"): contour.add_new(Tag("ContourImageSequence"), "SQ", contour_image_sequence) # Contour Sequence for contour_image in contour_image_sequence: contour_image.add_new(Tag("ReferencedSOPClassUID"), "UI", referenced_sop_class_uid) # CT Image Storage contour_image.add_new(Tag("ReferencedSOPInstanceUID"), "UI", referenced_sop_instance_uid) contour.add_new(Tag("ContourNumber"), "IS", new_contour_number) if not _is_closed_contour(roi_coordinates): contour.add_new(Tag("ContourGeometricType"), "CS", "OPEN_PLANAR") contour.add_new(Tag("NumberOfContourPoints"), "IS", number_of_contour_points) contour.add_new(Tag("ContourData"), "DS", roi_coordinates) else: contour.add_new(Tag("ContourGeometricType"), "CS", "CLOSED_PLANAR") contour.add_new(Tag("NumberOfContourPoints"), "IS", number_of_contour_points - 1) contour.add_new(Tag("ContourData"), "DS", roi_coordinates[0:-3]) rtss.ROIContourSequence[position].ContourSequence.extend(contour_sequence) return rtss
def create_initial_rtss_from_ct(img_ds: pydicom.dataset.Dataset, ct_uid_list=[]) -> pydicom.dataset.Dataset: """Pre-populate an RT Structure Set based on a single CT (or MR) and a list of image UIDs The caller should update the Structure Set Label, Name, and Description, which are set to "OnkoDICOM" plus the StudyID from the CT, and must add Structure Set ROI Sequence, ROI Contour Sequence, and RT ROI Observations Sequence Parameters ---------- img_ds : pydicom.dataset.Dataset A CT or MR image that the RT Structure Set will be "drawn" on ct_uid_list : list, optional list of UIDs (as strings) of the entire image volume that the RT SS references, by default [] Returns ------- pydicom.dataset.Dataset the half-baked RT SS, ready for Structure Set ROI Sequence, ROI Contour Sequence, and RT ROI Observations Sequence Raises ------ ValueError [description] """ if (img_ds is None): raise ValueError("No CT data to initialize RT SS") now = datetime.datetime.now() dicom_date = now.strftime("%Y%m%d") dicom_time = now.strftime("%H%M") 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("FrameOfReferenceUID"), Tag("PositionReferenceIndicator"), Tag("InstitutionName"), Tag("InstitutionAddress") ] rt_ss = pydicom.dataset.Dataset() for tag in top_level_tags_to_copy: print("Tag ", tag) if tag in img_ds: print("value of tag in image: ", img_ds[tag]) rt_ss[tag] = deepcopy(img_ds[tag]) # Best to modify the Structure Set Lable with something more interesting in the application. # and populate the Name and Description from the application also. print("Study ID is ", rt_ss.StudyID) rt_ss.StructureSetLabel = "OnkoDICOM rtss of " + rt_ss.StudyID rt_ss.StructureSetName = rt_ss.StructureSetLabel rt_ss.StructureSetDescription = rt_ss.StructureSetLabel # referenced_study_sequence_item = pydicom.dataset.Dataset() # referenced_study_sequence_item["ReferencedSOPInstanceUID"] = img_ds.StudyInstanceUID # referenced_study_sequence_item["ReferencedSOPClassUID"] = img_ds.SOPClassUID # rt_ss.ReferencedStudySequence = [referenced_study_sequence_item] # General Equipment Module rt_ss.Manufacturer = "OnkoDICOM" rt_ss.ManufacturersModelName = "OnkoDICOM" # TODO: Pull this off build information in some way rt_ss.SoftwareVersions = "2020" # RT Series Module rt_ss.SeriesInstanceUID = pydicom.uid.generate_uid() rt_ss.Modality = "RTSTRUCT" rt_ss.SeriesDate = dicom_date rt_ss.SeriesTime = dicom_time rt_ss.SeriesNumber = 1 # RT Referenced Frame Of Reference Sequence, Structure Set Module rt_ref_frame_of_ref_sequence_item = pydicom.dataset.Dataset() rt_ref_frame_of_ref_sequence_item.FrameOfReferenceUID = img_ds.FrameOfReferenceUID rt_ss.ReferencedFrameOfReferenceSequence = [ rt_ref_frame_of_ref_sequence_item ] rt_ref_study_sequence_item = pydicom.dataset.Dataset() rt_ref_study_sequence_item.ReferencedSOPInstanceUID = img_ds.StudyInstanceUID rt_ref_study_sequence_item.ReferencedSOPClassUID = img_ds.SOPClassUID rt_ref_series_sequence_item = pydicom.dataset.Dataset() rt_ref_series_sequence_item.SeriesInstanceUID = img_ds.SeriesInstanceUID contour_image_sequence = [] referenced_image_sequence = [] for uid in ct_uid_list: contour_image_sequence_item = pydicom.dataset.Dataset() referenced_image_sequence_item = pydicom.dataset.Dataset() referenced_image_sequence_item.ReferencedSOPClassUID = img_ds.SOPClassUID contour_image_sequence_item.ReferencedSOPClassUID = img_ds.SOPClassUID referenced_image_sequence_item.ReferencedSOPInstanceUID = uid contour_image_sequence_item.ReferencedSOPInstanceUID = uid contour_image_sequence.append(contour_image_sequence_item) referenced_image_sequence.append(referenced_image_sequence_item) rt_ref_frame_of_ref_sequence_item.ContourImageSequence = contour_image_sequence rt_ss.ReferencedImageSequence = referenced_image_sequence referenced_series_sequence_item = pydicom.dataset.Dataset() referenced_series_sequence_item.SeriesInstanceUID = img_ds.SeriesInstanceUID # acceptable to copy because the contents of each reference sequence item only include # SOP Class and SOP Instance, and not additional elements that are in referenced image seq # but not referenced instance seq referenced_series_sequence_item.ReferencedInstanceSequence = shallowcopy( referenced_image_sequence) rt_ss.ReferencedSeriesSequence = [referenced_series_sequence_item] rt_ref_study_sequence_item.RTReferencedSeriesSequence = [ rt_ref_series_sequence_item ] rt_ref_frame_of_ref_sequence_item.RTReferencedStudySequence = [ rt_ref_study_sequence_item ] rt_ss.StructureSetROISequence = [] rt_ss.ROIContourSequence = [] rt_ss.RTROIObservationsSequence = [] rt_ss.SOPClassUID = "1.2.840.10008.5.1.4.1.1.481.3" rt_ss.SOPInstanceUID = pydicom.uid.generate_uid() rt_ss.InstanceCreationDate = rt_ss.StructureSetDate = dicom_date rt_ss.InstanceCreationTime = rt_ss.StructureSetTime = dicom_time rt_ss.is_little_endian = True rt_ss.is_implicit_VR = True return rt_ss
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)
_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: """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 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)
from pydicom.filebase import DicomBytesIO import warnings # filename = Path(r'D:\Users\user\Desktop\kwmri\DICOM\ST000000\SE000005\MR000003.TRUE') filename = Path( r'D:\Users\user\Desktop\NTUCT\8252\Ct_Without_ContrastBrain - 16683\IAC_2\IM-0008-0002.dcm' ) filename = Path( r'D:\Users\user\Desktop\NTUCT\8252\Ct_Without_ContrastBrain - 16683\AXIAL_303\IM-0009-0003.dcm' ) # coding = 'big5' coding = 'utf8' ds = pydicom.dcmread(str(filename)) DcmTagValDict = {} DcmTagNameDict = { Tag(0x33, 0x1013): 'NTUPatientName', Tag(0x33, 0x1014): 'NTUPatientId', Tag(0x33, 0x1018): 'NTUDoctorName', Tag(0x0028, 0x1050): 'wl', Tag(0x0028, 0x1051): 'ww', Tag(0x20, 0x10): 'StudyID', Tag(0x20, 0x11): 'Series#', Tag(0x20, 0x13): 'Instance#', Tag(0x10, 0x10): 'PatientName', Tag(0x10, 0x20): 'PatientID', Tag(0x10, 0x1010): 'PatientAge', Tag(0x10, 0x40): 'PatientSex', Tag(0x10, 0x30): 'PatientBirthDate', Tag(0x08, 0x20): 'StudyDate', Tag(0x08, 0x30): 'StudyTime', Tag(0x08, 0x1030): 'studyDescription',
def setUp(self): # raw data element -> tag VR length value # value_tell is_implicit_VR is_little_endian' # Unknown (not in DICOM dict), non-private, non-group 0 for this test self.raw1 = RawDataElement(Tag(0x88880002), None, 4, 0x1111, 0, True, True)
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 test_exceptions(self): """ Check incorrect types/values for properties raise exceptions """ primitive = N_GET() # MessageID with pytest.raises(TypeError): primitive.MessageID = 'halp' with pytest.raises(TypeError): primitive.MessageID = 1.111 with pytest.raises(ValueError): primitive.MessageID = 65536 with pytest.raises(ValueError): primitive.MessageID = -1 # MessageIDBeingRespondedTo with pytest.raises(TypeError): primitive.MessageIDBeingRespondedTo = 'halp' with pytest.raises(TypeError): primitive.MessageIDBeingRespondedTo = 1.111 with pytest.raises(ValueError): primitive.MessageIDBeingRespondedTo = 65536 with pytest.raises(ValueError): primitive.MessageIDBeingRespondedTo = -1 # AffectedSOPClassUID with pytest.raises(TypeError): primitive.AffectedSOPClassUID = 45.2 with pytest.raises(TypeError): primitive.AffectedSOPClassUID = 100 with pytest.raises(ValueError): primitive.AffectedSOPClassUID = 'abc' # AffectedSOPInstanceUID with pytest.raises(TypeError): primitive.AffectedSOPInstanceUID = 45.2 with pytest.raises(TypeError): primitive.AffectedSOPInstanceUID = 100 with pytest.raises(ValueError): primitive.AffectedSOPInstanceUID = 'abc' # RequestedSOPClassUID with pytest.raises(TypeError): primitive.RequestedSOPClassUID = 45.2 with pytest.raises(TypeError): primitive.RequestedSOPClassUID = 100 with pytest.raises(ValueError): primitive.RequestedSOPClassUID = 'abc' # RequestedSOPInstanceUID with pytest.raises(TypeError): primitive.RequestedSOPInstanceUID = 45.2 with pytest.raises(TypeError): primitive.RequestedSOPInstanceUID = 100 with pytest.raises(ValueError): primitive.RequestedSOPInstanceUID = 'abc' # AttributeIdentifierList with pytest.raises(ValueError): primitive.AttributeIdentifierList = Tag(0x0000, 0x0000) with pytest.raises(ValueError, match="Attribute Identifier List must"): primitive.AttributeIdentifierList = ['ijk', 'abc'] with pytest.raises(ValueError, match="Attribute Identifier List must"): primitive.AttributeIdentifierList = [] # AttributeList msg = r"'AttributeList' parameter must be a BytesIO object" with pytest.raises(TypeError, match=msg): primitive.AttributeList = 'halp' with pytest.raises(TypeError): primitive.AttributeList = 1.111 with pytest.raises(TypeError): primitive.AttributeList = 50 with pytest.raises(TypeError): primitive.AttributeList = [30, 10] # Status with pytest.raises(TypeError): primitive.Status = 19.4
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_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 read_3d_dicom(dicomFile, flip=False): """ Read dicom file and return an float 3D image """ files = [] try: files.append(pydicom.read_file(dicomFile[0])) except pydicom.errors.InvalidDicomError: ds = pydicom.read_file(dicomFile[0], force=True) ds.file_meta.TransferSyntaxUID = pydicom.uid.ImplicitVRLittleEndian files.append(ds) # skip files with no SliceLocation (eg scout views) slices = [] if len(files) == 1: slices.append(files[0]) else: logger.error('no file available') return dicomProperties = dicom_properties() dicomProperties.read_dicom_properties(slices[0]) # create 3D array if len(slices[0].pixel_array.shape) == 2: dicomProperties.img_shape = [1] + dicomProperties.img_shape img3d = np.zeros(dicomProperties.img_shape) # fill 3D array with the images from the files if len(slices[0].pixel_array.shape) == 2: img3d[0, :, :] = slices[0].pixel_array else: img3d[:, :, :] = slices[0].pixel_array img3d = dicomProperties.rs * img3d + dicomProperties.ri img_result = itk.image_from_array(np.float32(img3d)) img_result.SetSpacing(dicomProperties.spacing) img_result.SetOrigin(dicomProperties.origin) arrayDirection = np.zeros([3, 3], np.float64) arrayDirection[0, 0] = dicomProperties.io[0] arrayDirection[0, 1] = dicomProperties.io[1] arrayDirection[0, 2] = dicomProperties.io[2] arrayDirection[1, 0] = dicomProperties.io[3] arrayDirection[1, 1] = dicomProperties.io[4] arrayDirection[1, 2] = dicomProperties.io[5] arrayDirection[2, :] = np.cross(arrayDirection[0, :], arrayDirection[1, :]) if Tag(0x18, 0x88) in slices[0] and slices[0][0x18, 0x88].value < 0: arrayDirection[2, 2] = -1.0 else: flip = False arrayDirection[2, 2] = 1.0 matrixItk = itk.Matrix[itk.D, 3, 3](itk.GetVnlMatrixFromArray(arrayDirection)) img_result.SetDirection(matrixItk) if flip: flipFilter = itk.FlipImageFilter.New(Input=img_result) flipFilter.SetFlipAxes((False, False, True)) flipFilter.SetFlipAboutOrigin(False) flipFilter.Update() img_result = flipFilter.GetOutput() return img_result
def _create_bvecs(sorted_dicoms, bvec_file): """ Calculate the bvecs and write the to a bvec file # inspired by dicom2nii from mricron # see http://users.fmrib.ox.ac.uk/~robson/internal/Dicom2Nifti111.m """ if type(sorted_dicoms[0]) is list: dicom_headers = sorted_dicoms[0][0] else: dicom_headers = sorted_dicoms[0] # get the patient orientation image_orientation = dicom_headers.ImageOrientationPatient read_vector = numpy.array([ float(image_orientation[0]), float(image_orientation[1]), float(image_orientation[2]) ]) phase_vector = numpy.array([ float(image_orientation[3]), float(image_orientation[4]), float(image_orientation[5]) ]) mosaic_vector = numpy.cross(read_vector, phase_vector) # normalize the vectors read_vector /= numpy.linalg.norm(read_vector) phase_vector /= numpy.linalg.norm(phase_vector) mosaic_vector /= numpy.linalg.norm(mosaic_vector) # create an empty array for the new bvecs bvecs = numpy.zeros([len(sorted_dicoms), 3]) # for each slice calculate the new bvec for index in range(0, len(sorted_dicoms)): if type(sorted_dicoms[0]) is list: dicom_headers = sorted_dicoms[index][0] else: dicom_headers = sorted_dicoms[index] # get the bval als this is needed in some checks bval = common.get_is_value(dicom_headers[Tag(0x0019, 0x100c)]) # get the bvec if it exists in the headers bvec = numpy.array([0, 0, 0]) if Tag(0x0019, 0x100e) in dicom_headers: # in case of implicit VR the private field cannot be split into an array, we do this here bvec = numpy.array( common.get_fd_array_value(dicom_headers[Tag(0x0019, 0x100e)], 3)) # if bval is 0 or the vector is 0 no projection is needed and the vector is 0,0,0 new_bvec = numpy.array([0, 0, 0]) if bval > 0 and not (bvec == [0, 0, 0]).all(): # project the bvec and invert the y direction new_bvec = numpy.array([ numpy.dot(bvec, read_vector), -numpy.dot(bvec, phase_vector), numpy.dot(bvec, mosaic_vector) ]) # normalize the bvec new_bvec /= numpy.linalg.norm(new_bvec) bvecs[index, :] = new_bvec # save the found bvecs to the file common.write_bvec_file(bvecs, bvec_file) return numpy.array(bvecs)
def read_dicom_properties(self, slice, nextSlice=None): # pixel aspects, assuming all slices are the same ps = [1.0, 1.0] if Tag(0x28, 0x30) in slice: ps = slice.PixelSpacing ss = None if Tag(0x18, 0x88) in slice: ss = abs(float(slice[(0x0018, 0X0088)].value)) if ss == '' or ss is None: if Tag(0x3004, 0x000c) in slice: ss = slice[0x3004, 0x000c][1] - slice[0x3004, 0x000c][0] if ss == '' or ss is None: if not nextSlice is None and Tag(0x0020, 0x0032) in slice and Tag( 0x0020, 0x0032) in nextSlice: ss = abs(nextSlice[0x0020, 0x0032][2] - slice[0x0020, 0x0032][2]) if ss == '' or ss is None: ss = 1.0 self.spacing = [ps[1], ps[0], ss] ip = [0.0, 0.0, 0.0] if Tag(0x20, 0x32) in slice: ip = slice[0x20, 0x32].value #Image Position elif Tag(0x54, 0x22) in slice and Tag(0x20, 0x32) in slice[0x54, 0x22][0]: ip = slice[0x54, 0x22][0][0x20, 0x32].value #Image Position self.origin = [ip[0], ip[1], ip[2]] if Tag(0x20, 0x37) in slice: self.io = slice[0x20, 0x37].value #Image Orientation elif Tag(0x54, 0x22) in slice and Tag(0x20, 0x37) in slice[0x54, 0x22][0]: self.io = slice[0x54, 0x22][0][0x20, 0x37].value #Image Orientation if self.io == "" or self.io == None: self.io = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0] #orientation = [io[0], io[1], io[2], io[3], io[4], io[5]] self.read_dicom_slop_intercept(slice) self.img_shape = list(slice.pixel_array.shape)
def dicom_to_nifti(dicom_input, output_file): """ This function will convert an anatomical dicom series to a nifti Examples: See unit test :param output_file: filepath to the output nifti :param dicom_input: directory with the dicom files for a single scan, or list of read in dicoms """ if len(dicom_input) <= 0: raise ConversionError('NO_DICOM_FILES_FOUND') # remove duplicate slices based on position and data dicom_input = remove_duplicate_slices(dicom_input) # remove localizers based on image type dicom_input = remove_localizers_by_imagetype(dicom_input) # if no dicoms remain we should raise exception if len(dicom_input) < 1: raise ConversionValidationError('TOO_FEW_SLICES/LOCALIZER') if settings.validate_slicecount: common.validate_slicecount(dicom_input) # remove_localizers based on image orientation (only valid if slicecount is validated) dicom_input = remove_localizers_by_orientation(dicom_input) # validate all the dicom files for correct orientations common.validate_slicecount(dicom_input) if settings.validate_orientation: # validate that all slices have the same orientation common.validate_orientation(dicom_input) if settings.validate_orthogonal: # validate that we have an orthogonal image (to detect gantry tilting etc) common.validate_orthogonal(dicom_input) # sort the dicoms dicom_input = common.sort_dicoms(dicom_input) # validate slice increment inconsistent slice_increment_inconsistent = False if settings.validate_slice_increment: # validate that all slices have a consistent slice increment common.validate_slice_increment(dicom_input) elif common.is_slice_increment_inconsistent(dicom_input): slice_increment_inconsistent = True if settings.validate_instance_number: # validate that all slices have a consistent instance_number common.validate_instance_number(dicom_input) # if inconsistent increment and we allow resampling then do the resampling based conversion to maintain the correct geometric shape if slice_increment_inconsistent and settings.resample: nii_image, max_slice_increment = _convert_slice_incement_inconsistencies( dicom_input) # do the normal conversion else: # Get data; originally z,y,x, transposed to x,y,z data = common.get_volume_pixeldata(dicom_input) affine, max_slice_increment = common.create_affine(dicom_input) # Convert to nifti nii_image = nibabel.Nifti1Image(data.squeeze(), affine) # Set TR and TE if available if Tag(0x0018, 0x0080) in dicom_input[0] and Tag(0x0018, 0x0081) in dicom_input[0]: common.set_tr_te(nii_image, float(dicom_input[0].RepetitionTime), float(dicom_input[0].EchoTime)) # Save to disk if output_file is not None: logger.info('Saving nifti to disk %s' % output_file) nii_image.to_filename(output_file) return { 'NII_FILE': output_file, 'NII': nii_image, 'MAX_SLICE_INCREMENT': max_slice_increment }
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. .. note:: This function is used internally - usually there is no need to call it from user code. To read data from a DICOM file, :func:`dcmread` shall be used instead. Parameters ---------- fp : file-like The file-like to read from. is_implicit_VR : bool ``True`` if the data is encoded as implicit VR, ``False`` otherwise. is_little_endian : bool ``True`` if the data is encoded as little endian, ``False`` otherwise. 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 :func:`dcmread` for parameter info. encoding : Encoding scheme specific_tags : list or None See :func:`dcmread` for parameter info. Returns ------- VR : str or None ``None`` if implicit VR, otherwise the VR read from the file. length : int The length of the DICOM data element (could be DICOM "undefined length" ``0xFFFFFFFFL``) value_bytes : bytes or str The raw bytes from the DICOM file (not parsed into Python types) is_little_endian : bool ``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() if specific_tags is not None: for tag in specific_tags: if isinstance(tag, str): tag = Tag(tag_for_keyword(tag)) if isinstance(tag, BaseTag): tag_set.add(tag) 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) 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 length > 0 else empty_value_for_VR(VR, raw=True)) if debugging: dotdot = "..." if length > 12 else " " displayed_value = value[:12] if value else b'' logger_debug("%08x: %-34s %s %r %s" % (value_tell, bytes2hex(displayed_value), dotdot, displayed_value, 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 or b'', is_little_endian) # Store the encoding value in the generator # for use with future elements (SQs) encoding = convert_encodings(encoding) 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) # 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 c_find(cls, ds: pydicom.Dataset): """C-FIND handler :param ds: C-FIND request :type ds: pydicom.Dataset :yield: C-FIND results :rtype: pydicom.Dataset """ joins = set() response_attrs = [] select = [Series] upper_level_filters = [] skipped = set() patient_attrs = [e for e in ds if e.tag in Patient.mapping] skipped.update(e.tag for e in patient_attrs) if patient_attrs: _upper_level_filters = list( _filter_upper_level(Patient, patient_attrs)) upper_level_filters.extend(_upper_level_filters) for tag, attr, vr, _, attr_name in _upper_level_filters: select.append(attr) response_attrs.append( (tag, ('study', 'patient', attr_name), vr, None)) joins.update([(Series, Study), (Study, Patient)]) study_attrs = [e for e in ds if e.tag in Study.mapping] skipped.update(e.tag for e in study_attrs) if study_attrs: _upper_level_filters = list(_filter_upper_level( Study, study_attrs)) upper_level_filters.extend(_upper_level_filters) for tag, attr, vr, _, attr_name in _upper_level_filters: select.append(attr) response_attrs.append((tag, ('study', attr_name), vr, None)) joins.update([(Series, Study)]) if 'NumberOfSeriesRelatedInstances' in ds: _tag = Tag((0x0020, 0x1209)) skipped.add(_tag) select.append( peewee.fn.Count(Instance.id)\ .alias('number_of_study_related_series') # pylint: disable=no-member ) response_attrs.append( (_tag, 'number_of_series_related_instances', 'IS', None)) joins.add((Series, Instance)) query = Series.select(*select) for join in joins: query = query.join_from(*join) query, _response_attrs = _build_filters(cls, query, ds, skipped) response_attrs.extend(_response_attrs) for _, attr, vr, elem, _ in upper_level_filters: if not elem.value: continue query = _build_filter(query, attr, vr, elem) encoding = getattr(ds, 'SpecificCharacterSet', 'ISO-IR 6') if not query.count(): return [] yield from (_encode_response(s, response_attrs, encoding) for s in query)
def test_wrong_bytes_length_exception(self, accept_wrong_length): """Check exception when number of raw bytes is not correct.""" raw = RawDataElement(Tag(0x00190000), 'FD', 1, b'1', 0, False, True) with pytest.raises(BytesLengthException): DataElement_from_raw(raw)