def cardiac_service(data_objects, working_dir, settings): """ Implements the platipy framework to provide cardiac atlas based segmentation. """ logger.info("Running Cardiac Segmentation") logger.info("Using settings: " + str(settings)) output_objects = [] for data_object in data_objects: logger.info("Running on data object: " + data_object.path) # Read the image series load_path = data_object.path if data_object.type == "DICOM": load_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames( data_object.path) img = sitk.ReadImage(load_path) results = run_cardiac_segmentation(img, settings) # Save resulting masks and add to output for service for output in results.keys(): mask_file = os.path.join(working_dir, "{0}.nii.gz".format(output)) sitk.WriteImage(results[output], mask_file) output_data_object = DataObject(type="FILE", path=mask_file, parent=data_object) output_objects.append(output_data_object) # If the input was a DICOM, then we can use it to generate an output RTStruct # if data_object.type == 'DICOM': # dicom_file = load_path[0] # logger.info('Will write Dicom using file: {0}'.format(dicom_file)) # masks = {settings['outputContourName']: mask_file} # # Use the image series UID for the file of the RTStruct # suid = pydicom.dcmread(dicom_file).SeriesInstanceUID # output_file = os.path.join(working_dir, 'RS.{0}.dcm'.format(suid)) # # Use the convert nifti function to generate RTStruct from nifti masks # convert_nifti(dicom_file, masks, output_file) # # Create the Data Object for the RTStruct and add it to the list # do = DataObject(type='DICOM', path=output_file, parent=d) # output_objects.append(do) # logger.info('RTStruct generated') return output_objects
def cardiac_structure_guided_service(data_objects, working_dir, settings): """Runs the structure guided cardiac segmentation service""" logger.info("Running Structure Guided Cardiac Segmentation") logger.info("Using settings: " + str(settings)) output_objects = [] for data_object in data_objects: logger.info("Running on data object: " + data_object.path) # Read the image series load_path = data_object.path if data_object.type == "DICOM": load_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames( data_object.path) img = sitk.ReadImage(load_path) # Load the WHOLEHEART contour (child of Image) if len(data_object.children) == 0: logger.error( "Wholeheart structure needed for structure guided cardiac " f"segmentation, skipping {data_object.id}") continue wholeheart = sitk.ReadImage(data_object.children[0].path) results, _ = run_cardiac_segmentation(img, wholeheart, settings) # Save resulting masks and add to output for service for output in results: mask_file = os.path.join(working_dir, "{0}.nii.gz".format(output)) sitk.WriteImage(results[output], mask_file) output_data_object = DataObject(type="FILE", path=mask_file, parent=data_object) output_objects.append(output_data_object) return output_objects
def primitive_body_segmentation(data_objects, working_dir, settings): logger.info("Running Primitive Body Segmentation") logger.info("Using settings: " + str(settings)) output_objects = [] for d in data_objects: logger.info("Running on data object: " + d.path) # Read the image series load_path = d.path if d.type == "DICOM": load_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames(d.path) img = sitk.ReadImage(load_path) # Region growing using Connected Threshold Image Filter seg_con = sitk.ConnectedThreshold( img, seedList=[tuple(settings["seed"])], lower=settings["lowerThreshold"], upper=settings["upperThreshold"], ) # Clean up the segmentation vector_radius = tuple(settings["vectorRadius"]) kernel = sitk.sitkBall seg_clean = sitk.BinaryMorphologicalClosing(seg_con, vector_radius, kernel) mask = sitk.BinaryNot(seg_clean) # Write the mask to a file in the working_dir mask_file = os.path.join( working_dir, "{0}.nii.gz".format(settings["outputContourName"])) sitk.WriteImage(mask, mask_file) # Create the output Data Object and add it to the list of output_objects data_object = DataObject(type="FILE", path=mask_file, parent=d) output_objects.append(data_object) # If the input was a DICOM, then we can use it to generate an output RTStruct if d.type == "DICOM": dicom_file = load_path[0] logger.info("Will write Dicom using file: {0}".format(dicom_file)) masks = {settings["outputContourName"]: mask_file} # Use the image series UID for the file of the RTStruct suid = pydicom.dcmread(dicom_file).SeriesInstanceUID output_file = os.path.join(working_dir, "RS.{0}.dcm".format(suid)) # Use the convert nifti function to generate RTStruct from nifti masks convert_nifti(dicom_file, masks, output_file) # Create the Data Object for the RTStruct and add it to the list do = DataObject(type="DICOM", path=output_file, parent=d) output_objects.append(do) logger.info("RTStruct generated") return output_objects
def pinnacle_export_service(data_objects, working_dir, settings): """ Implements the platipy framework to provide a pinnacle tar export service """ logger.info("Running Pinnacle Export") logger.info("Using settings: " + str(settings)) return_objects = [] for data_object in data_objects: logger.info("Running on data object: " + data_object.path) if not data_object.type == "FILE" or not tarfile.is_tarfile(data_object.path): logger.error( f"Can only process TAR file. Skipping file: {data_object.path}" ) continue archive_path = tempfile.mkdtemp() # Extract the tar archive tar = tarfile.open(data_object.path) for member in tar.getmembers(): if not ":" in member.name: tar.extract(member, path=archive_path) # Read the path to the patient directory from the data object meta data pat_path = data_object.meta_data["patient_path"] pinn_extracted = os.path.join(archive_path, pat_path) pinn = PinnacleExport(pinn_extracted, None) # Find the plan we want to export in the list of plans if len(pinn.plans) == 0: logger.error("No Plans found for patient") continue export_plan = None for plan in pinn.plans: if ( "plan_name" in data_object.meta_data.keys() and plan.plan_info["PlanName"] == data_object.meta_data["plan_name"] ): export_plan = plan break if export_plan is None: export_plan = plan # If a trial was given, try to find it and set it for trial in export_plan.trials: trial_name = trial["Name"] if ( "trial" in data_object.meta_data.keys() and trial_name == data_object.meta_data["trial"] ): export_plan.active_trial = trial_name output_dir = os.path.join(working_dir, str(data_object.id)) if os.path.exists(output_dir): # Just in case it was already run for this data object, lets remove all old output shutil.rmtree(output_dir) os.makedirs(output_dir) if "CT" in settings["exportModalities"]: logger.info("Exporting Primary CT") pinn.export_image(export_plan.primary_image, export_path=output_dir) if "RTSTRUCT" in settings["exportModalities"]: logger.info("Exporting RTSTRUCT") pinn.export_struct(export_plan, output_dir) if "RTPLAN" in settings["exportModalities"]: logger.info("Exporting RTPLAN") pinn.export_plan(export_plan, output_dir) if "RTDOSE" in settings["exportModalities"]: logger.info("Exporting RTDOSE") pinn.export_dose(export_plan, output_dir) for image in pinn.images: if image.image_info[0]["SeriesUID"] in settings["exportSeriesUIDs"]: pinn.export_image(image, export_path=output_dir) # Find the output files output_files = os.listdir(output_dir) output_files.sort() output_objects = [os.path.join(output_dir, f) for f in output_files] # Create the output data objects for obj in output_objects: # Write some meta data to patient comments field file_name = os.path.basename(obj) if file_name.startswith("R"): # Don't add to the image series dicom_dataset = pydicom.read_file(obj) meta_data = {} meta_data["service"] = { "tool": "Pinnacel Export Tool", "trial": export_plan.active_trial["Name"], "plan_date": export_plan.active_trial["ObjectVersion"][ "WriteTimeStamp" ], "plan_locked": export_plan.plan_info["PlanIsLocked"], } if dicom_dataset.Modality == "RTPLAN": meta_data["warning"] = ( "WARNING: OUTPUT GENERATED FOR RTPLAN FILE IS " "UNVERIFIED AND MOST LIKELY INCORRECT!" ) if "meta" in data_object.meta_data.keys(): meta_data["meta"] = data_object.meta_data["meta"] if dicom_dataset.Modality == "RTPLAN": dicom_dataset.RTPlanDescription = ( "Pinnacle Export Meta Data written to " "SOPAuthorizationComment" ) dicom_dataset.SOPAuthorizationComment = json.dumps(meta_data) dicom_dataset.save_as(obj) output_data_object = DataObject(type="DICOM", path=obj, parent=data_object) return_objects.append(output_data_object) # Delete files extracted from TAR shutil.rmtree(archive_path) logger.info("Finished Pinnacle Export") return return_objects
def nnunet_service(data_objects, working_dir, settings): """ Run a nnUNet task """ output_objects = [] logger.info("Running nnUNet") logger.info("Using settings: {0}".format(settings)) logger.info("Working Dir: {0}".format(working_dir)) input_path = Path(working_dir).joinpath("input") input_path.mkdir() output_path = Path(working_dir).joinpath("output") output_path.mkdir() for data_object in data_objects: # Create a symbolic link for each image to auto-segment using the nnUNet do_path = Path(data_object.path) io_path = input_path.joinpath(f"{settings['task']}_0000.nii.gz") load_path = data_object.path if data_object.type == "DICOM": load_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames( data_object.path) img = sitk.ReadImage(load_path) sitk.WriteImage(img, str(io_path)) command = [ "nnUNet_predict", "-i", str(input_path), "-o", str(output_path), "-t", settings["task"], "-m", settings["config"], ] if settings["trainer"]: command += ["-tr", settings["trainer"]] logger.info(f"Running command: {command}") subprocess.call(command) for op in output_path.glob("*.nii.gz"): if settings["clean_sup_slices"]: mask = sitk.ReadImage(str(op)) mask = clean_sup_slices(mask) sitk.WriteImage(mask, str(op)) output_data_object = DataObject(type="FILE", path=str(op), parent=data_object) output_objects.append(output_data_object) os.remove(io_path) logger.info("Finished running nnUNet") return output_objects
def mri_dixon_analysis(data_objects, working_dir, settings): """Calculate Fat Water fraction for appropriate MRI Dixon images Args: data_objects (list): List of data objects, should contain one fat and one water image working_dir (str): Path to directory used for working settings ([type]): The settings to use for analysis Returns: list: List of output data objects """ logger.info("Running Dixon analysis Calculation") logger.info("Using settings: " + str(settings)) output_objects = [] fat_obj = None water_obj = None for data_obj in data_objects: if data_obj.meta_data["image_type"] == "fat": fat_obj = data_obj if data_obj.meta_data["image_type"] == "water": water_obj = data_obj if fat_obj is None or water_obj is None: logger.error("Both Fat and Water Images are required") return [] # Read the image series fat_load_path = fat_obj.path if fat_obj.type == "DICOM": fat_load_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames( fat_obj.path) fat_img = sitk.ReadImage(fat_load_path) water_load_path = water_obj.path if water_obj.type == "DICOM": water_load_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames( water_obj.path) water_img = sitk.ReadImage(water_load_path) # Cast to float for calculation fat_img = sitk.Cast(fat_img, sitk.sitkFloat32) water_img = sitk.Cast(water_img, sitk.sitkFloat32) # Let's do the calcuation using NumPy fat_arr = sitk.GetArrayFromImage(fat_img) water_arr = sitk.GetArrayFromImage(water_img) # Do the calculation divisor = water_arr + fat_arr fat_fraction_arr = (fat_arr * 100) / divisor fat_fraction_arr[ divisor == 0] = 0 # Sets those voxels which were divided by zero to 0 water_fraction_arr = (water_arr * 100) / divisor water_fraction_arr[ divisor == 0] = 0 # Sets those voxels which were divided by zero to 0 fat_fraction_img = sitk.GetImageFromArray(fat_fraction_arr) water_fraction_img = sitk.GetImageFromArray(water_fraction_arr) fat_fraction_img.CopyInformation(fat_img) water_fraction_img.CopyInformation(water_img) # Create the output Data Objects and add it to output_ob fat_fraction_file = os.path.join(working_dir, "fat.nii.gz") sitk.WriteImage(fat_fraction_img, fat_fraction_file) water_fraction_file = os.path.join(working_dir, "water.nii.gz") sitk.WriteImage(water_fraction_img, water_fraction_file) fat_data_object = DataObject(type="FILE", path=fat_fraction_file, parent=fat_obj) output_objects.append(fat_data_object) water_data_object = DataObject(type="FILE", path=water_fraction_file, parent=water_obj) output_objects.append(water_data_object) return output_objects
def pyradiomics_extractor(data_objects, working_dir, settings): """Run to extract radiomics from data objects Args: data_objects (list): List of data objects to process working_dir (str): Path to directory used for working settings ([type]): The settings to use for processing radiomics Returns: list: List of output data objects """ logger.info("Running PyRadiomics Extract") logger.info("Using settings: " + str(settings)) pyrad_settings = settings["pyradiomics_settings"] # If no Radiomics are supplied then extract for all first order radiomics if len(settings["radiomics"].keys()) == 0: features = firstorder.RadiomicsFirstOrder.getFeatureNames() settings["radiomics"] = {"firstorder": [f for f in features if not features[f]]} results = None meta_data_cols = [("", "Contour")] for data_obj in data_objects: try: if len(data_obj.children) > 0: logger.info("Running on data object: " + data_obj.path) # Read the image series load_path = data_obj.path if data_obj.type == "DICOM": load_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames(data_obj.path) # Children of Image Data Object are masks, compute PyRadiomics for all of them! output_frame = pd.DataFrame() for child_obj in data_obj.children: contour_name = child_obj.path.split("/")[-1].split(".")[0] if len(settings["contours"]) > 0 and not contour_name in settings["contours"]: # If a contour list is provided and this contour isn't in the list then # skip it logger.debug("Skipping Contour: ", contour_name) continue # Reload the image for each new contour in case resampling is occuring, # should start fresh each time. image = sitk.ReadImage(load_path) mask = sitk.ReadImage(child_obj.path) logger.debug("Image Origin: " + str(image.GetOrigin())) logger.debug("Mask Origin: " + str(mask.GetOrigin())) logger.debug("Image Direction: " + str(image.GetDirection())) logger.debug("Mask Direction: " + str(mask.GetDirection())) logger.debug("Image Size: " + str(image.GetSize())) logger.debug("Mask Size: " + str(mask.GetSize())) logger.info(child_obj.path) interpolator = pyrad_settings.get("interpolator") resample_pixel_spacing = pyrad_settings.get("resampledPixelSpacing") if settings["resample_to_image"]: logger.info("Will resample to spacing of image") resample_pixel_spacing = list(image.GetSpacing()) pyrad_settings["resampledPixelSpacing"] = resample_pixel_spacing if interpolator is not None and resample_pixel_spacing is not None: logger.info("Resampling Image and Mask") image, mask = imageoperations.resampleImage(image, mask, **pyrad_settings) # output[contour_name] = {"Contour": contour_name} df_contour = pd.DataFrame() logger.info("Computing Radiomics for contour: {0}", contour_name) for rad in settings["radiomics"].keys(): logger.info("Computing {0} radiomics".format(rad)) if rad not in AVAILABLE_RADIOMICS.keys(): logger.warning("Radiomic Class not found: {0}", rad) continue radiomics_obj = AVAILABLE_RADIOMICS[rad] features = radiomics_obj(image, mask, **pyrad_settings) features.disableAllFeatures() # All features seem to be computed if all are disabled (possible # pyradiomics bug?). Skip if all features in a class are disabled. if len(settings["radiomics"][rad]) == 0: continue for feature in settings["radiomics"][rad]: try: features.enableFeatureByName(feature, True) except LookupError: # Feature not available in this set logger.warning("Feature not found: {0}", feature) feature_result = features.execute() feature_result = dict( ((rad, key), value) for (key, value) in feature_result.items() ) df_feature_result = pd.DataFrame(feature_result, index=[contour_name]) # Merge the results df_contour = pd.concat([df_contour, df_feature_result], axis=1) df_contour[("", "Contour")] = contour_name output_frame = pd.concat([output_frame, df_contour]) # Add the meta data for this contour if there is any if child_obj.meta_data: for key in child_obj.meta_data: col_key = ("", key) output_frame[col_key] = child_obj.meta_data[key] if col_key not in meta_data_cols: meta_data_cols.append(col_key) # Add Image Series Data Object's Meta Data to the table if data_obj.meta_data: for key in data_obj.meta_data.keys(): col_key = ("", key) output_frame[col_key] = pd.Series( [data_obj.meta_data[key] for p in range(len(output_frame.index))], index=output_frame.index, ) if col_key not in meta_data_cols: meta_data_cols.append(col_key) if results is None: results = output_frame else: results = results.append(output_frame) except Exception as exception: # pylint: disable=broad-except logger.error("An Error occurred while computing the Radiomics: {0}", exception) # Set the order of the columns output cols = results.columns.tolist() new_cols = list(meta_data_cols) new_cols += [c for c in cols if not c in meta_data_cols] results = results[new_cols] # Write output to file output_file = os.path.join(working_dir, "output.csv") results = results.reset_index() results = results.drop(columns=["index"]) results.to_csv(output_file) logger.info("Radiomics written to {0}".format(output_file)) # Create the output Data Object and add it to output_objects data_object = DataObject(type="FILE", path=output_file) output_objects = [data_object] return output_objects
def dirqa_service(data_objects, working_dir, settings): """ Implements the platipy framework to provide a DIR QA service based on SIFT """ logger.info("Running DIR QA") logger.info("Using settings: {0}".format(settings)) logger.info("Working Dir: {0}".format(working_dir)) # First figure out what data object is which primary = None secondary = None for data_object in data_objects: if "type" in data_object.meta_data: if data_object.meta_data["type"] == "primary": primary = data_object if data_object.meta_data["type"] == "secondary": secondary = data_object if not primary or not secondary: logger.error("Unable to find primary and secondary data object.") logger.error("Set the type on the data objects meta data.") return [] logger.info(f"Primary: {primary.path}") logger.info(f"Secondary: {secondary.path}") # Compute SIFT point matches within each of the child contours # Contours with corresponding names set in metadata are expected in both the primary and # secondary child objects output_objects = [] for primary_contour_object in primary.children: logger.info(f"Contour: {primary_contour_object.path}") # Make sure that the 'name' is set in the meta data if not "name" in primary_contour_object.meta_data.keys(): logger.error( "'name' not set in contour meta data. Set matching name in " "primary and secondary contours.") continue logger.info( f"Primary Contour: {primary_contour_object.meta_data['name']}") secondary_contour_object = None for search_contour_object in secondary.children: if not "name" in search_contour_object.meta_data.keys(): logger.error( "'name' not set in contour meta data. Set matching name in " "primary and secondary contours.") continue if (search_contour_object.meta_data["name"] == primary_contour_object.meta_data["name"]): secondary_contour_object = search_contour_object if not secondary_contour_object: logger.error( f"No matching contour found for {primary_contour_object.meta_data['name']}" ) continue logger.info( f"Secondary Contour: {secondary_contour_object.meta_data['name']}") # Read the images primary_path = primary.path if primary.type == "DICOM": primary_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames( primary.path) secondary_path = secondary.path if secondary.type == "DICOM": secondary_path = sitk.ImageSeriesReader().GetGDCMSeriesFileNames( secondary.path) primary_image = sitk.ReadImage(primary_path) secondary_image = sitk.ReadImage(secondary_path) # Read the contour masks primary_contour_mask = sitk.ReadImage(primary_contour_object.path) secondary_contour_mask = sitk.ReadImage(secondary_contour_object.path) # Crop to the contour bounding box primary_image = crop_to_contour_bounding_box(primary_image, primary_contour_mask) secondary_image = crop_to_contour_bounding_box(secondary_image, secondary_contour_mask) # Threshold intensities low_range = settings["intensityRange"][0] high_range = settings["intensityRange"][1] primary_image = sitk.Threshold(primary_image, lower=low_range, upper=10000000, outsideValue=low_range) primary_image = sitk.Threshold(primary_image, lower=-10000000, upper=high_range, outsideValue=high_range) secondary_image = sitk.Threshold(secondary_image, lower=low_range, upper=10000000, outsideValue=low_range) secondary_image = sitk.Threshold(secondary_image, lower=-10000000, upper=high_range, outsideValue=high_range) # Save cropped volumes and compute SIFT points primary_cropped_path = "cropped_primary.nii.gz" secondary_cropped_path = "cropped_secondary.nii.gz" sitk.WriteImage(primary_image, primary_cropped_path) sitk.WriteImage(secondary_image, secondary_cropped_path) primary_cropped_match = os.path.join( working_dir, "primary_{0}_match.csv".format( primary_contour_object.meta_data["name"]), ) secondary_cropped_match = os.path.join( working_dir, "secondary_{0}_match.csv".format( secondary_contour_object.meta_data["name"]), ) subprocess.call([ "plastimatch", "sift", primary_cropped_path, secondary_cropped_path, "--output-match-1", primary_cropped_match, "--output-match-2", secondary_cropped_match, ]) if not os.path.exists(primary_cropped_match) or not os.path.exists( secondary_cropped_match): logger.warning("No output from platimatch SIFT computation") continue # Need to negate values in dim 0 & 1 (not sure why plastimatch outputs these negated) primary_points = pd.read_csv(primary_cropped_match, header=None) secondary_points = pd.read_csv(secondary_cropped_match, header=None) primary_points[1] = -primary_points[1] primary_points[2] = -primary_points[2] secondary_points[1] = -secondary_points[1] secondary_points[2] = -secondary_points[2] # Prefix point names with structure name primary_points[0] = (primary_contour_object.meta_data["name"] + "_" + primary_points[0].astype(str)) secondary_points[0] = (secondary_contour_object.meta_data["name"] + "_" + secondary_points[0].astype(str)) if settings["includePointsMode"] == "CONTOUR": # Filter out points which fall outside of contour logger.info("Filtering out points outside the contour") remove_point_names = [] for point in primary_points.iterrows(): phys_point = list(point[1][1:4]) mask_point = primary_contour_mask.TransformPhysicalPointToIndex( phys_point) is_in_contour = primary_contour_mask[mask_point] if not is_in_contour: remove_point_names.append(point[1][0]) for point in secondary_points.iterrows(): phys_point = list(point[1][1:4]) mask_point = secondary_contour_mask.TransformPhysicalPointToIndex( phys_point) is_in_contour = secondary_contour_mask[mask_point] if not is_in_contour: remove_point_names.append(point[1][0]) primary_points = primary_points[~primary_points[0]. isin(remove_point_names)] secondary_points = secondary_points[~secondary_points[0]. isin(remove_point_names)] # Save the updated points primary_points.to_csv(primary_cropped_match, index=False, header=None) secondary_points.to_csv(secondary_cropped_match, index=False, header=None) # Create the output Data Object and add it to output_objects primary_output_object = DataObject(type="FILE", path=primary_cropped_match, parent=primary) secondary_output_object = DataObject(type="FILE", path=secondary_cropped_match, parent=secondary) output_objects.append(primary_output_object) output_objects.append(secondary_output_object) os.remove(primary_cropped_path) os.remove(secondary_cropped_path) logger.info("Finished DIR QA") return output_objects