Exemplo n.º 1
0
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
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
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