def has_metadata(self, tag): if self.metadata is None: return None else: return get_pydicom_meta_tag(dcm_seq=self.metadata, tag=tag, test_tag=True)
def rename(self, new): if self.metadata is not None: # Obtain the old name old = self.name if not self.has_metadata(tag=(0x3006, 0x0020)): raise ValueError( f"The DICOM metaheader does not contain a Structure Set ROI sequence." ) # Iterate over roi elements in the roi sequence for ii, roi_element in enumerate(self.metadata[0x3006, 0x0020]): # Find ROI name that matches the old name if get_pydicom_meta_tag(dcm_seq=roi_element, tag=(0x3006, 0x0026), tag_type="str") == old: set_pydicom_meta_tag(dcm_seq=roi_element, tag=(0x3006, 0x0026), value=new) # Assign a new name self.name = new else: # Assign a new name self.name = new
def get_all_dicom_headers(image_folder, modality=None, series_uid=None, sop_instance_uid=None): # Parse to list if sop_instance_uid is not None: if isinstance(sop_instance_uid, str): sop_instance_uid = [sop_instance_uid] # 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 dicom metadata for each file slice_dcm = [ pydicom.dcmread(os.path.join(image_folder, file_name), stop_before_pixels=True, force=True) for file_name in file_list ] # Filter slices according to sop instance UIDs that are in the provided list. if sop_instance_uid is not None: slice_dcm = [ slice_dcm_metadata for slice_sop_instance_uid in sop_instance_uid for slice_dcm_metadata in slice_dcm if get_pydicom_meta_tag(dcm_seq=slice_dcm_metadata, tag=(0x0008, 0x0018), tag_type="str") == slice_sop_instance_uid ] return slice_dcm
def get_metadata(self, tag, tag_type, default=None): # Do not attempt to read the metadata if no metadata is present. if self.metadata is None: return return get_pydicom_meta_tag(dcm_seq=self.metadata, tag=tag, tag_type=tag_type, default=default)
def update_dicom_header(self, dcm): # Update unit of pixel values voxel_unit = "CM2ML" if self.suv_type == "BSA" else "GML" set_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x1001), value=voxel_unit) # Update the SUV type set_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x1006), value=self.suv_type) # Decay correction set_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x1102), value=self.decay_correction) # Add DECY to the image corrections, if this was not done previously. image_corrections = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0028, 0x0051), tag_type="mult_str", default=[]) if "DECY" not in image_corrections: image_corrections += ["DECY"] set_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0028, 0x0051), value=image_corrections) # Update the image type image_type = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0008, 0x0008), tag_type="mult_str", default=[]) if len(image_type) > 2: image_type[0] = "DERIVED" image_type[1] = "SECONDARY" else: image_type = ["DERIVED", "SECONDARY"] set_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0008, 0x0008), value=image_type) return dcm
def _get_frame_of_reference_uid(dcm): # Try to obtain a frame of reference UID if has_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0020, 0x0052)): return get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0020, 0x0052), tag_type="str") # For RT structure sets, the FOR UID may be tucked away in the Structure Set ROI Sequence if has_pydicom_meta_tag(dcm_seq=dcm, tag=(0x3006, 0x0020)): structure_set_roi_sequence = dcm[(0x3006, 0x0020)] for structure_set_roi_element in structure_set_roi_sequence: if has_pydicom_meta_tag(dcm_seq=structure_set_roi_element, tag=(0x3006, 0x0024)): return get_pydicom_meta_tag(dcm_seq=structure_set_roi_element, tag=(0x3006, 0x0024), tag_type="str") return None
def _find_dicom_roi_names(dcm, with_roi_number=False): # Placeholder roi_names list roi_names = [] roi_sequence_numbers = [] # Check if a Structure Set ROI Sequence exists (3006, 0020) if not get_pydicom_meta_tag( dcm_seq=dcm, tag=(0x3006, 0x0020), test_tag=True): warnings.warn( "The RT-structure set did not contain any ROI sequences.") if with_roi_number: return roi_names, roi_sequence_numbers else: return roi_names # Iterate over roi elements in the roi sequence for roi_element in dcm[0x3006, 0x0020]: # Check if the roi element contains a name (3006, 0026) if get_pydicom_meta_tag(dcm_seq=roi_element, tag=(0x3006, 0x0026), test_tag=True): roi_names += [ get_pydicom_meta_tag(dcm_seq=roi_element, tag=(0x3006, 0x0026), tag_type="str") ] roi_sequence_numbers += [ get_pydicom_meta_tag(dcm_seq=roi_element, tag=(0x3006, 0x0022), tag_type="str") ] if with_roi_number: return roi_names, roi_sequence_numbers else: return roi_names
def __init__(self, dcm): # Start of image acquisition for the current position acquisition_start_date = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0008, 0x0022), tag_type="str") acquisition_start_time = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0008, 0x0032), tag_type="str") self.acquisition_ref_time = convert_dicom_time( date_str=acquisition_start_date, time_str=acquisition_start_time) self.start_ref_time = deepcopy(self.acquisition_ref_time) # Frame reference time frame (ms) self.frame_duration = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0018, 0x1242), tag_type="float") # Radionuclide administration if get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x0016), test_tag=True): radio_admin_start_time = get_pydicom_meta_tag( dcm_seq=dcm[0x0054, 0x0016][0], tag=(0x0018, 0x1078), tag_type="str") self.radio_admin_ref_time = convert_dicom_time( datetime_str=radio_admin_start_time) if self.radio_admin_ref_time is None: # If unsuccessful, attempt determining administration time from (0x0018, 0x1072) radio_admin_start_time = get_pydicom_meta_tag( dcm_seq=dcm[0x0054, 0x0016][0], tag=(0x0018, 0x1072), tag_type="str") self.radio_admin_ref_time = convert_dicom_time( date_str=acquisition_start_date, time_str=radio_admin_start_time) else: self.radio_admin_ref_time = None if self.radio_admin_ref_time is None: # If neither (0x0018, 0x1078) or (0x0018, 0x1072) are present, attempt to read private tags # GE tags ge_acquistion_ref_time = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0009, 0x100d), tag_type="str") ge_radio_admin_ref_time = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0009, 0x103b), tag_type="str") if ge_acquistion_ref_time is not None and ge_radio_admin_ref_time is not None: self.acquisition_ref_time = convert_dicom_time( datetime_str=ge_acquistion_ref_time) self.radio_admin_ref_time = convert_dicom_time( datetime_str=ge_radio_admin_ref_time) if self.radio_admin_ref_time is not None and self.acquisition_ref_time is not None: day_diff = abs(self.radio_admin_ref_time - self.acquisition_ref_time).days if day_diff > 1: # Correct for de-identification mistakes (i.e. administration time was de-identified correctly, but acquisition time not) # We do not expect that the difference between the two is more than a day, or even more than a few hours at most. if self.radio_admin_ref_time > self.acquisition_ref_time: self.radio_admin_ref_time -= datetime.timedelta( days=day_diff) else: self.radio_admin_ref_time += datetime.timedelta( days=day_diff) if self.radio_admin_ref_time > self.acquisition_ref_time: # Correct for overnight self.radio_admin_ref_time -= datetime.timedelta(days=1) # Radionuclide total dose and radionuclide half-life if get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x0016), test_tag=True): self.total_dose = get_pydicom_meta_tag(dcm_seq=dcm[0x0054, 0x0016][0], tag=(0x0018, 0x1074), tag_type="float") self.half_life = get_pydicom_meta_tag(dcm_seq=dcm[0x0054, 0x0016][0], tag=(0x0018, 0x1075), tag_type="float") else: self.total_dose = None self.half_life = None # Type of intensity in a voxel self.voxel_unit = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x1001), tag_type="str") # Type of decay correction that is used self.decay_correction = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x1102), tag_type="str", default="NONE") # Decay factor for the image self.decay_factor = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x1321), tag_type="float", default=1.0) # TODO: Determine if any decay correction took place from (0018,9758), which has either YES or NO, if present. If YES, (0018,9701) should be present as well. # TODO: Use Decay Correction DateTime (0018,9701) as alternative for determining the correction time. # Type of SUV self.suv_type = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0054, 0x1006), tag_type="str", default="BW") # Patient data self.patient_gender = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0010, 0x0040), tag_type="str") self.patient_height = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0010, 0x1020), tag_type="float") if self.patient_height is not None: if self.patient_height > 3.0: # Interpret patient height as cm and convert to meter self.patient_height = self.patient_height / 100.0 self.patient_weight = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0010, 0x1030), tag_type="float") if self.patient_weight is None: logging.warning( "Patient weight was not found in the DICOM header. SUV normalisation cannot take place." ) # Private scale factors self.philips_suv_scale = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x7053, 0x1000), tag_type="float") self.philips_count_scale = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x7053, 0x1009), tag_type="float")
def _convert_rtstruct_to_segmentation(dcm: FileDataset, roi: str, image_object: ImageClass): # Deparse roi deparsed_roi = parse_roi_name(roi=roi) # Keep only structure data corresponding to the current ROIs. dcm = _filter_rt_structure_set(dcm=dcm, roi_names=deparsed_roi) # Check if the roi is found. if dcm is None: return None # Initialise a data list contour_data_list = [] # Obtain segmentation for roi_contour_sequence in dcm[(0x3006, 0x0039)]: for contour_sequence in roi_contour_sequence[(0x3006, 0x0040)]: # Check if the geometric type exists (3006, 0042) if not get_pydicom_meta_tag(dcm_seq=contour_sequence, tag=(0x3006, 0x0042), test_tag=True): continue # Check if the geometric type equals "CLOSED_PLANAR" if get_pydicom_meta_tag(dcm_seq=contour_sequence, tag=(0x3006, 0x0042), tag_type="str") != "CLOSED_PLANAR": continue # Check if contour data exists (3006, 0050) if not get_pydicom_meta_tag(dcm_seq=contour_sequence, tag=(0x3006, 0x0050), test_tag=True): continue contour_data = np.array(get_pydicom_meta_tag( dcm_seq=contour_sequence, tag=(0x3006, 0x0050), tag_type="mult_float"), dtype=np.float64) contour_data = contour_data.reshape((-1, 3)) # Determine if there is an offset (3006, 0045) contour_offset = np.array(get_pydicom_meta_tag( dcm_seq=contour_sequence, tag=(0x3006, 0x0045), tag_type="mult_float", default=[0.0, 0.0, 0.0]), dtype=np.float64) # Remove the offset from the data contour_data -= contour_offset # Obtain the sop instance uid. if get_pydicom_meta_tag(dcm_seq=contour_sequence, tag=(0x3006, 0x0016), test_tag=True): sop_instance_uid = get_pydicom_meta_tag( dcm_seq=contour_sequence[(0x3006, 0x0016)][0], tag=(0x0008, 0x1155), tag_type="str", default=None) else: sop_instance_uid = None # Store as contour. contour = ContourClass(contour=contour_data, sop_instance_uid=sop_instance_uid) # Add contour data to the contour list contour_data_list += [contour] if len(contour_data_list) > 0: # Create a new ROI object. roi_obj = RoiClass(name="+".join(deparsed_roi), contour=contour_data_list, metadata=dcm) # Convert contour into segmentation object roi_obj.create_mask_from_contours(img_obj=image_object, disconnected_segments="keep_as_is") return roi_obj else: return None
def _filter_rt_structure_set(dcm, roi_numbers=None, roi_names=None): from pydicom.sequence import Sequence # We need to update a few sequences in the RT structure set: # * The Structure Set ROI sequence (3006, 0020) # * The ROI Contour sequence (3006, 0039) # * The RT ROI Observations Sequence (3006, 0080) # Initialise new sequences new_structure_set_roi_sequence = Sequence() new_roi_contour_sequence = Sequence() new_rt_roi_observations_sequence = Sequence() if get_pydicom_meta_tag( dcm_seq=dcm, tag=(0x3006, 0x0080), test_tag=True) else None if not get_pydicom_meta_tag( dcm_seq=dcm, tag=(0x3006, 0x0020), test_tag=True): return None if not get_pydicom_meta_tag( dcm_seq=dcm, tag=(0x3006, 0x0039), test_tag=True): return None # Check that either roi_numbers is provided, or roi_names is provided. if roi_numbers is None and roi_names is None: raise ValueError( "Either the ROI Reference numbers or the ROI names should be provided." ) elif roi_numbers is None and roi_names is not None: roi_names_available, roi_numbers_available = _find_dicom_roi_names( dcm=dcm, with_roi_number=True) roi_numbers = [ roi_number for ii, roi_number in enumerate(roi_numbers_available) if roi_names_available[ii] in roi_names ] if len(roi_numbers) == 0: return None for ii, current_roi_number in enumerate(roi_numbers): # Look through the existing Structure Set ROI sequence for matching roi_number for structure_set_elem in dcm[(0x3006, 0x0020)]: if not get_pydicom_meta_tag(dcm_seq=structure_set_elem, tag=(0x3006, 0x0022), test_tag=True): # No ROI number present. continue elif get_pydicom_meta_tag(dcm_seq=structure_set_elem, tag=(0x3006, 0x0022), tag_type="str") == current_roi_number: # ROI number matches new_structure_set_roi_sequence.append(structure_set_elem) else: continue # Look through existing ROI contour sequence for matching roi_number for roi_contour_elem in dcm[(0x3006, 0x0039)]: if not get_pydicom_meta_tag(dcm_seq=roi_contour_elem, tag=(0x3006, 0x0084), test_tag=True): # No ROI number present continue elif get_pydicom_meta_tag(dcm_seq=roi_contour_elem, tag=(0x3006, 0x0084), tag_type="str") == current_roi_number: # ROI number matches new_roi_contour_sequence.append(roi_contour_elem) else: continue if get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x3006, 0x0080), test_tag=True): for roi_observation_elem in dcm[(0x3006, 0x0080)]: if not get_pydicom_meta_tag(dcm_seq=roi_observation_elem, tag=(0x3006, 0x0084), tag_type="str"): # No ROI number present continue elif get_pydicom_meta_tag( dcm_seq=roi_observation_elem, tag=(0x3006, 0x0084), tag_type="str") == current_roi_number: # ROI number matches new_rt_roi_observations_sequence.append( roi_observation_elem) else: continue # Local copy of dcm dcm = deepcopy(dcm) # Add as data element if new_structure_set_roi_sequence is not None: dcm[(0x3006, 0x0020)].value = new_structure_set_roi_sequence if new_roi_contour_sequence is not None: dcm[(0x3006, 0x0039)].value = new_roi_contour_sequence if new_rt_roi_observations_sequence is not None: dcm[(0x3006, 0x0080)].value = new_rt_roi_observations_sequence return dcm
def _find_dicom_image_series(image_folder, allowed_modalities, modality=None, series_uid=None, frame_of_ref_uid=None): # Check folder contents, keep only files that are recognised as DICOM images. file_list = os.listdir(image_folder) file_list = [ file_name for file_name in file_list if not os.path.isdir(os.path.join(image_folder, file_name)) ] file_list = [ file_name for file_name in file_list if file_name.lower().endswith(".dcm") ] if len(file_list) == 0: raise FileNotFoundError( f"The image folder does not contain any DICOM files.") # Modality and series UID series_modality = [] series_series_uid = [] series_FOR_uid = [] # Identify modality of the files for file_name in file_list: # Read DICOM header using pydicom dcm = pydicom.dcmread(os.path.join(image_folder, file_name), stop_before_pixels=True, force=True) # Read modality series_modality += [ get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0008, 0x0060), tag_type="str") ] # Read series UID series_series_uid += [ get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0020, 0x000e), tag_type="str") ] # Frame of reference UID series_FOR_uid += [_get_frame_of_reference_uid(dcm=dcm)] # User-provided modality if modality is not None: if modality.lower() in ["ct"]: requested_modality = ["CT"] elif modality.lower() in ["pet", "pt"]: requested_modality = ["PT"] elif modality.lower() in ["mri", "mr"]: requested_modality = ["MR"] elif modality.lower() in ["rtstruct", "structure_set"]: requested_modality = ["RTSTRUCT"] else: raise ValueError( f"Unknown modality requested. Available choices are CT, PT, MR, RTSTRUCT. Found: {modality}" ) if not any([ tmp_modality in allowed_modalities for tmp_modality in requested_modality ]): raise ValueError( f"The selected modality ({modality}) cannot be used within the current context. This error can occur when attempting to image files instead of" "segmentations when segmentations are intended.") else: # Any supported modality requested_modality = allowed_modalities # Filter file list file_list = [ file_list[ii] for ii, file_modality in enumerate(series_modality) if file_modality in requested_modality ] series_series_uid = [ series_series_uid[ii] for ii, file_modality in enumerate(series_modality) if file_modality in requested_modality ] series_FOR_uid = [ series_FOR_uid[ii] for ii, file_modality in enumerate(series_modality) if file_modality in requested_modality ] # Check if the requested modality was found. if len(file_list) == 0: raise ValueError( f"The DICOM folder does not contain any DICOM images with a currently supported modality ({allowed_modalities})." ) # Check uniqueness of series UID if len(list(set(series_series_uid)) ) > 1 and series_uid is None and frame_of_ref_uid is None: raise ValueError( f"Multiple series UID were found in the DICOM folder. Please select one using the series_uid argument. Found: {list(set(series_series_uid))}" ) elif len(list(set(series_series_uid)) ) > 1 and series_uid is None and frame_of_ref_uid is not None: series_uid = [ series_series_uid[ii] for ii, file_FOR_uid in enumerate(series_FOR_uid) if file_FOR_uid == frame_of_ref_uid ] if len(list(set(series_uid))) > 1: raise ValueError( f"Multiple series UID that share a frame of reference UID were found in the DICOM folder. Please select one using the series_uid argument." f"Found: {list(set(series_uid))}") else: series_uid = series_uid[0] elif series_uid is not None: # Check if the series_uid exists if series_uid not in series_series_uid: raise ValueError( f"The requested series UID ({series_uid}) was not found in the DICOM folder. Found: {list(set(series_series_uid))}" ) else: series_uid = series_series_uid[0] # Check uniqueness of FOR_uid if len(list(set(series_FOR_uid)) ) > 1 and frame_of_ref_uid is None and series_uid is None: raise ValueError( f"Multiple series with different frame of reference UIDs were found in the DICOM folder." ) elif len(list(set(series_FOR_uid)) ) > 1 and frame_of_ref_uid is None and series_uid is not None: frame_of_ref_uid = [ series_FOR_uid[ii] for ii, file_series_uid in enumerate(series_series_uid) if file_series_uid == series_uid ] if len(list(set(frame_of_ref_uid))) > 1: raise ValueError( f"Multiple frame of reference UIDs where found for the same DICOM series. This may indicate corruption of the DICOM files." ) else: frame_of_ref_uid = frame_of_ref_uid[0] elif frame_of_ref_uid is not None: if frame_of_ref_uid not in series_FOR_uid: raise ValueError( f"The requested frame of reference UID ({frame_of_ref_uid}) was not found in the DICOM folder {image_folder}. Found: {list(set(series_FOR_uid))}" ) else: frame_of_ref_uid = series_FOR_uid[0] # Filter series with the particular frame of reference uid and series uid return [ file_list[ii] for ii in range(len(file_list)) if series_series_uid[ii] == series_uid and series_FOR_uid[ii] == frame_of_ref_uid ]
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 file_table = pd.DataFrame({ "file_name": file_list, "position_z": 0.0, "position_y": 0.0, "position_x": 0.0, "sop_instance_uid": "" }) image_position_x = [] image_position_y = [] image_position_z = [] sop_instance_uid = [] for file_name in file_list: # Load the dicom header dcm = pydicom.dcmread(os.path.join(image_folder, file_name), stop_before_pixels=True, force=True) # Find the origin of each slice. slice_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] # Update with slice positions image_position_x += [slice_origin[2]] image_position_y += [slice_origin[1]] image_position_z += [slice_origin[0]] # Find the sop instance UID of each slice. slice_sop_instance_uid = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0008, 0x0018), tag_type="str") # Update with the slice SOP instance UID. sop_instance_uid += [slice_sop_instance_uid] # Order ascending position (DICOM: z increases from feet to head) file_table = pd.DataFrame({ "file_name": file_list, "position_z": image_position_z, "position_y": image_position_y, "position_x": image_position_x, "sop_instance_uid": sop_instance_uid }).sort_values(by=["position_z", "position_y", "position_x"]) # Obtain DICOM metadata from the bottom slice. This will be used to fill most of 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: # Compute the distance between the origins of the slices. This is the slice spacing. image_slice_spacing = np.median( np.sqrt( np.sum(np.array([ np.power(np.diff(file_table.position_z.values), 2.0), np.power(np.diff(file_table.position_y.values), 2.0), np.power(np.diff(file_table.position_x.values), 2.0) ]), axis=0))) 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=3) == 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 (Xx, Xy, Xz, Yx, Yy, Yz; see DICOM C.7.6.2 Image Plane Module) image_orientation = get_pydicom_meta_tag(dcm_seq=dcm, tag=(0x0020, 0x0037), tag_type="mult_float") # Add (Zx, Zy, Zz) if len(file_table) > 1: # Compute distance between subsequent origins. slice_origin_distance = np.sqrt( np.power(np.diff(file_table.position_x.values), 2.0) + np.power(np.diff(file_table.position_y.values), 2.0) + np.power(np.diff(file_table.position_z.values), 2.0)) # Find unique distance values. slice_origin_distance = np.unique(np.around(slice_origin_distance, 3)) # If there is more than one value, this means that there is an unexpected shift in origins. if len(slice_origin_distance) > 1: raise ValueError( f"Inconsistent distance between slice origins of subsequent slices: " f"{slice_origin_distance}. Slices cannot be aligned correctly. This is likely due to " f"missing slices.") z_orientation = np.array([ np.median(np.diff(file_table.position_x.values)), np.median(np.diff(file_table.position_y.values)), np.median(np.diff(file_table.position_z.values)) ]) / image_slice_spacing # Append orientation. image_orientation += z_orientation.tolist() else: 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, 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], slice_table=file_table) return img_obj
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 _convert_to_parametric_map_iod(self): if self.metadata is None: return None # Create a copy of the metadata. old_dcm: FileDataset = copy.deepcopy(self.metadata) # Update the SOP class to that of a parametric map image self.set_metadata(tag=(0x0008, 0x0016), value="1.2.840.10008.5.1.4.1.1.30") # Update the image type attribute image_type = self.get_metadata(tag=(0x0008, 0x0008), tag_type="mult_str", default=[]) image_type = [image_type[ii] if ii < len(image_type) else "" for ii in range(4)] image_type[0] = "DERIVED" image_type[1] = "PRIMARY" image_type[2] = image_type[2] if not image_type[2] == "" else "STATIC" image_type[3] = "MIXED" if self.spat_transform == "base" else "FILTERED" self.set_metadata(tag=(0x0008, 0x0008), value=image_type) # Parametric Map Image module attributes that may be missing. self.cond_set_metadata(tag=(0x2050, 0x0020), value="IDENTITY") # Presentation LUT shape self.cond_set_metadata(tag=(0x0018, 0x9004), value="RESEARCH") # Content qualification self.cond_set_metadata(tag=(0x0028, 0x0301), value="NO") # Burned-in Annotation self.cond_set_metadata(tag=(0x0028, 0x0302), value="YES") # Recognisable facial features self.cond_set_metadata(tag=(0x0070, 0x0080), value=self.get_export_descriptor().upper().strip()[:15]) # Content label self.cond_set_metadata(tag=(0x0070, 0x0081), value=self.get_export_descriptor()[:63]) # Content description self.cond_set_metadata(tag=(0x0070, 0x0084), value="Doe^John") # Set the source instance sequence source_instance_list = [] for reference_instance_sop_uid in self.slice_table.sop_instance_uid: ref_inst = Dataset() set_pydicom_meta_tag(dcm_seq=ref_inst, tag=(0x0008, 0x1150), value=get_pydicom_meta_tag(dcm_seq=old_dcm, tag=(0x0008, 0x0016), tag_type="str")) set_pydicom_meta_tag(dcm_seq=ref_inst, tag=(0x0008, 0x1155), value=reference_instance_sop_uid) source_instance_list += [ref_inst] self.set_metadata(tag=(0x0008, 0x2112), value=Sequence(source_instance_list)) # Attributes from the enhanced general equipment module may be missing. self.cond_set_metadata(tag=(0x0008, 0x0070), value="unknown") # Manufacturer self.cond_set_metadata(tag=(0x0008, 0x1090), value="unknown") # Model name self.cond_set_metadata(tag=(0x0018, 0x1000), value="unknown") # Device Serial Number self.set_metadata(tag=(0x0018, 0x1020), value="MIRP " + get_version()) # Items from multi-frame function groups may be missing. We currently only use a single frame. self.set_metadata(tag=(0x5200, 0x9229), value=Sequence()) # Shared functional groups sequence self.set_metadata(tag=(0x5200, 0x9230), value=Sequence()) # Per-frame functional groups sequence # Multi-frame Dimension module # Dimension organisation sequence. We copy the frame of reference as UID. dim_org_seq_elem = Dataset() set_pydicom_meta_tag(dim_org_seq_elem, tag=(0x0020, 0x9164), value=self.get_metadata(tag=(0x0020, 0x0052), tag_type="str")) # Dimension organisation UID self.set_metadata(tag=(0x0020, 0x9221), value=Sequence([dim_org_seq_elem])) # Dimension Index sequence. We point to the instance number. dim_index_seq_elem = Dataset() set_pydicom_meta_tag(dim_index_seq_elem, tag=(0x0020, 0x9165), value=(0x0020, 0x0013)) # Dimension index pointer set_pydicom_meta_tag(dim_index_seq_elem, tag=(0x0020, 0x9164), value=self.get_metadata(tag=(0x00200052), tag_type="str")) # Dimension organisation UID self.set_metadata(tag=(0x0020, 0x9222), value=Sequence([dim_index_seq_elem]))