Example #1
0
 def save_rtss(cls):
     """
     Saves the RT Struct.
     """
     patient_dict_container = PatientDictContainer()
     rtss_directory = Path(patient_dict_container.get("file_rtss"))
     patient_dict_container.get("dataset_rtss").save_as(rtss_directory)
Example #2
0
def get_fused_window(level, window):
    """
    Apply windowing on the fixed and moving (linear-registered) images.
    
    Args:
        level(int): the level (midpoint) of windowing
        window(any): the window (range) of windowing
    
    Return:
        color_axial (QtGui.QPixmap): pixmap of the registered image from axial 
        view
        color_sagittal (QtGui.QPixmap): pixmap of the registered image from 
        sagittal view
        color_coronal (QtGui.QPixmap): pixmap of the registered image from 
        coronal view
        tfm (sitk.CompositeTransform): transformation object containing data 
        that is a product from linear_registration
    """

    patient_dict_container = PatientDictContainer()
    old_images = patient_dict_container.get("sitk_original")
    fused_image = patient_dict_container.get("fused_images")
    tfm = fused_image[1]
    array = sitk.GetArrayFromImage(old_images).shape

    axial_slice_count = array[0]
    coronal_slice_count = array[1]
    sagittal_slice_count = array[1]

    sp_plane, _, sp_slice = old_images.GetSpacing()
    asp = (1.0 * sp_slice) / sp_plane

    color_axial = {}
    color_sagittal = {}
    color_coronal = {}

    windowing = (int(level - CT_RESCALE_INTERCEPT), int(window))

    for i in range(axial_slice_count):
        color_axial[i] = \
            get_fused_pixmap(old_images, fused_image[0], asp, i, "axial",
                             windowing)

    for i in range(sagittal_slice_count):
        color_sagittal[i] = \
            get_fused_pixmap(old_images, fused_image[0], asp, i, "sagittal",
                             windowing)

    for i in range(coronal_slice_count):
        color_coronal[i] = \
            get_fused_pixmap(old_images, fused_image[0], asp, i, "coronal",
                             windowing)

    return color_axial, color_sagittal, color_coronal, tfm
Example #3
0
def read_images_for_fusion(level=0, window=0):
    """
    Performs initial image fusion, this is by converting the old and
    new images for transformations into SITK object. Images are co-registered 
    using SITK library. Images and SITK.CompositeTransformation objects are 
    added to the patient dataset.
    
    Args:
        level(int): midpoint of window
        window(Any): range of values, should at least contain low bound and 
        high bound
    """
    patient_dict_container = PatientDictContainer()
    moving_dict_container = MovingDictContainer()
    if level == 0 or window == 0:
        level = patient_dict_container.get("level")
        window = patient_dict_container.get("window")

    amount = len(patient_dict_container.filepaths)
    orig_fusion_list = []

    for i in range(amount):
        try:
            orig_fusion_list.append(patient_dict_container.filepaths[i])
        except KeyError:
            continue

    orig_image = sitk.ReadImage(orig_fusion_list)
    patient_dict_container.set("sitk_original", orig_image)

    amount = len(moving_dict_container.filepaths)
    new_fusion_list = []

    for i in range(amount):
        try:
            new_fusion_list.append(moving_dict_container.filepaths[i])
        except KeyError:
            continue

    new_image = sitk.ReadImage(new_fusion_list)
    moving_dict_container.set("sitk_moving", new_image)

    create_fused_model(orig_image, new_image)
    color_axial, color_sagittal, color_coronal, tfm = \
        get_fused_window(level, window)

    patient_dict_container.set("color_axial", color_axial)
    patient_dict_container.set("color_sagittal", color_sagittal)
    patient_dict_container.set("color_coronal", color_coronal)
    moving_dict_container.set("tfm", tfm)
Example #4
0
def test_create_roi():
    rt_ss = dataset.Dataset()

    rt_ss.StructureSetROISequence = []
    rt_ss.StructureSetROISequence.append(dataset.Dataset())
    rt_ss.StructureSetROISequence[0].ReferencedFrameOfReferenceUID = "1.2.3"
    rt_ss.StructureSetROISequence[0].ROINumber = "1"

    rt_ss.ROIContourSequence = []

    rt_ss.RTROIObservationsSequence = []

    roi_name = "NewTestROI"
    roi_coordinates = [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0,
                       0]  # a closed right triangle
    image_ds = dataset.Dataset()
    image_ds.SOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
    image_ds.SOPInstanceUID = "1.2.3.4.5.6.7.8.9"
    patient_dict_container = PatientDictContainer()
    # container has to be initialised with kwargs content for the get/set to not fail for lack of an
    # "additional_parameters" dict.
    patient_dict_container.set_initial_values(None,
                                              None,
                                              None,
                                              blah="blah",
                                              rois={})
    if patient_dict_container.get("rois") is not None:
        print("rois are present in patient dict container")
    updated_rtss = create_roi(rt_ss, roi_name, roi_coordinates, image_ds)
    first_contour = updated_rtss.ROIContourSequence[0].ContourSequence[0]
    assert (first_contour.ContourImageSequence[0].ReferencedSOPClassUID ==
            image_ds.SOPClassUID)
    assert (first_contour.ContourGeometricType == "CLOSED_PLANAR")
    assert (rt_ss.RTROIObservationsSequence[0].RTROIInterpretedType == "ORGAN")
Example #5
0
    def on_loaded_suv2roi(self):
        """
        Called when progress bar has finished. Closes the progress
        window and refreshes the main screen.
        """
        if self.suv2roi.suv2roi_status:
            patient_dict_container = PatientDictContainer()
            self.structures_tab.fixed_container_structure_modified((
                patient_dict_container.get('dataset_rtss'), {"draw": None}))
        else:
            # Alert user that SUV2ROI failed and for what reason
            if self.suv2roi.failure_reason == "UNIT":
                failure_reason = \
                    "PET units are not Bq/mL. OnkoDICOM can currently only\n" \
                    "perform SUV2ROI on PET images stored in these units."
            elif self.suv2roi.failure_reason == "DECY":
                failure_reason = \
                    "PET is not decay corrected. OnkoDICOM can currently " \
                    "only\nperform SUV2ROI on PET images that are decay " \
                    "corrected."
            else:
                failure_reason = "The SUV2ROI process has failed."
            button_reply = \
                QtWidgets.QMessageBox(
                    QtWidgets.QMessageBox.Icon.Warning,
                    "SUV2ROI Failed",
                    failure_reason,
                    QtWidgets.QMessageBox.StandardButton.Ok, self)
            button_reply.button(
                QtWidgets.QMessageBox.StandardButton.Ok).setStyleSheet(
                self.stylesheet)
            button_reply.exec_()

        # Close progress window
        self.suv2roi_progress_window.close()
Example #6
0
    def update_ui(self):
        # Instantiate a local new PatientDictContainer
        patient_dict_container = PatientDictContainer()
        patient = patient_dict_container.get("basic_info")

        # Compare local patient with previous instance of ImageFusion
        if self.patient_id != patient['id']:
            self.update_patient()
Example #7
0
    def calculate_isodose_boundaries(self, isodose_levels):
        """
        Calculates isodose boundaries for each isodose level.
        :return: coutours, a list containing the countours for each
                 isodose level.
        """
        # Initialise variables needed to find isodose levels
        patient_dict_container = PatientDictContainer()
        pixmaps = patient_dict_container.get("pixmaps_axial")
        slider_min = 0
        slider_max = len(pixmaps)

        rt_plan_dose = patient_dict_container.dataset['rtdose']
        rt_dose_dose = patient_dict_container.get("rx_dose_in_cgray")

        # If rt_dose_dose does not exist, return None
        if not rt_dose_dose:
            return None

        contours = {}

        for item in isodose_levels:
            # Calculate boundaries for each isodose level for each slice
            contours[item] = []
            for slider_id in range(slider_min, slider_max):
                contours[item].append([])
                temp_ds = patient_dict_container.dataset[slider_id]
                z = temp_ds.ImagePositionPatient[2]
                grid = get_dose_grid(rt_plan_dose, float(z))

                if not (grid == []):
                    if isodose_levels[item][0]:
                        dose_level = isodose_levels[item][1] / \
                                     (rt_plan_dose.DoseGridScaling * 100)
                        contours[item][slider_id] =\
                            (measure.find_contours(grid, dose_level))
                    else:
                        dose_level = isodose_levels[item][1] * \
                                     rt_dose_dose / \
                                     (rt_plan_dose.DoseGridScaling * 10000)
                        contours[item][slider_id] = \
                            (measure.find_contours(grid, dose_level))

        # Return list of contours for each isodose level for each slice
        return contours
Example #8
0
    def calculate_contours(self):
        """
        Calculate SUV boundaries for each slice from an SUV value of 1
        all the way to the maximum SUV value in that slice.
        :return: Dictionary where key is SUV ROI name and value is
                 a list containing tuples of slice id and lists of
                 contours.
        """
        # Create dictionary to store contour data
        contour_data = {}

        # Initialise variables needed for function
        patient_dict_container = PatientDictContainer()
        slider_min = 0
        slider_max = len(patient_dict_container.get("pixmaps_axial"))

        # Loop through each PET image in the dataset
        for slider_id in range(slider_min, slider_max):
            # Get SUV data from PET file
            temp_ds = patient_dict_container.dataset[slider_id]
            suv_data = self.pet2suv(temp_ds)

            # Return None if PET 2 SUV failed
            if suv_data is None:
                return None

            # Set current and max SUV for the current slice
            current_suv = 1
            max_suv = numpy.amax(suv_data)

            # Continue calculating SUV contours for the slice until the
            # max SUV has been reached.
            while current_suv < max_suv:
                # Find the contours for the SUV (i)
                contours = measure.find_contours(suv_data, current_suv)

                # Get the SUV name
                name = "SUV-" + str(current_suv)
                if name not in contour_data:
                    contour_data[name] = []
                contour_data[name].append((slider_id, contours))
                current_suv += 1

        # Return contour data
        return contour_data
Example #9
0
class CalculateDVHProgressWindow(QtWidgets.QDialog):

    signal_dvh_calculated = QtCore.pyqtSignal()

    def __init__(self, *args, **kwargs):
        super(CalculateDVHProgressWindow, self).__init__(*args, **kwargs)
        layout = QtWidgets.QVBoxLayout()
        text = QtWidgets.QLabel(
            "Calculating DVHs... (This may take several minutes)")
        layout.addWidget(text)
        self.setWindowTitle("Please wait...")
        self.setLayout(layout)

        self.threadpool = QtCore.QThreadPool()
        self.patient_dict_container = PatientDictContainer()

        dataset_rtss = self.patient_dict_container.dataset["rtss"]
        dataset_rtdose = self.patient_dict_container.dataset["rtdose"]
        rois = self.patient_dict_container.get("rois")

        dict_thickness = ImageLoading.get_thickness_dict(
            dataset_rtss, self.patient_dict_container.dataset)

        interrupt_flag = threading.Event()
        fork_safe_platforms = ['Linux']
        if platform.system() in fork_safe_platforms:
            worker = Worker(ImageLoading.multi_calc_dvh, dataset_rtss,
                            dataset_rtdose, rois, dict_thickness)
        else:
            worker = Worker(ImageLoading.calc_dvhs, dataset_rtss,
                            dataset_rtdose, rois, dict_thickness,
                            interrupt_flag)

        worker.signals.result.connect(self.dvh_calculated)

        self.threadpool.start(worker)

    def dvh_calculated(self, result):
        dvh_x_y = ImageLoading.converge_to_0_dvh(result)
        self.patient_dict_container.set("raw_dvh", result)
        self.patient_dict_container.set("dvh_x_y", dvh_x_y)
        self.signal_dvh_calculated.emit()
        self.close()
Example #10
0
def test_add_to_roi():
    rt_ss = dataset.Dataset()

    rt_ss.StructureSetROISequence = []
    rt_ss.StructureSetROISequence.append(dataset.Dataset())
    rt_ss.StructureSetROISequence[0].ReferencedFrameOfReferenceUID = "1.2.3"
    rt_ss.StructureSetROISequence[0].ROINumber = "1"
    rt_ss.StructureSetROISequence[0].ROIName = "NewTestROI"

    rt_ss.ROIContourSequence = []

    rt_ss.RTROIObservationsSequence = []

    roi_name = "NewTestROI"
    roi_coordinates = [0, 0, 0, 0, 1, 0, 1, 0, 0]  # a right triangle
    image_ds = dataset.Dataset()
    image_ds.SOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
    image_ds.SOPInstanceUID = "1.2.3.4.5.6.7.8.9"
    patient_dict_container = PatientDictContainer()
    # container has to be initialised with kwargs content
    # for the get/set to not fail for lack of an "additional_parameters" dict.
    patient_dict_container.set_initial_values(None, None, None,
                                              blah="blah", rois={})
    if patient_dict_container.get("rois") is not None:
        print("rois are present in patient dict container")
    updated_rtss = create_roi(rt_ss, roi_name,
                              [{'coords': roi_coordinates, 'ds': image_ds}])
    # clearly the above is an opportunity to factor out to a fixture or similar
    rtss_with_added_roi = add_to_roi(updated_rtss, roi_name,
                                     roi_coordinates, image_ds)
    first_contour = rtss_with_added_roi.ROIContourSequence[0].\
        ContourSequence[0]
    second_contour = rtss_with_added_roi.ROIContourSequence[0].\
        ContourSequence[1]
    assert (
        second_contour
        .ContourImageSequence[0]
        .ReferencedSOPClassUID
        == image_ds.SOPClassUID
    )
    assert(first_contour.ContourGeometricType == "OPEN_PLANAR")
    assert (second_contour.ContourGeometricType == "OPEN_PLANAR")
Example #11
0
    def closeEvent(self, event: QtGui.QCloseEvent) -> None:
        patient_dict_container = PatientDictContainer()
        if patient_dict_container.get("rtss_modified") \
                and hasattr(self, "structures_tab"):
            confirmation_dialog = QMessageBox.information(
                self, 'Close without saving?',
                'The RTSTRUCT file has been modified. Would you like to save '
                'before exiting the program?',
                QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)

            if confirmation_dialog == QMessageBox.Save:
                self.structures_tab.save_new_rtss_to_fixed_image_set()
                event.accept()
                self.cleanup()
            elif confirmation_dialog == QMessageBox.Discard:
                event.accept()
                self.cleanup()
            else:
                event.ignore()
        else:
            self.cleanup()
Example #12
0
    def __init__(self, roi_id, color, text, structure_tab):
        super(StructureWidget, self).__init__()

        patient_dict_container = PatientDictContainer()
        self.dataset_rtss = patient_dict_container.get("dataset_rtss")
        self.roi_id = roi_id
        self.color = color
        self.text = text
        self.structure_tab = structure_tab
        self.standard_name = None
        self.layout = QtWidgets.QHBoxLayout()

        # Create color square
        color_square_label = QtWidgets.QLabel()
        color_square_pix = QtGui.QPixmap(15, 15)
        color_square_pix.fill(self.color)
        color_square_label.setPixmap(color_square_pix)
        self.layout.addWidget(color_square_label)

        # Create checkbox
        checkbox = QtWidgets.QCheckBox()
        checkbox.setFocusPolicy(QtCore.Qt.NoFocus)
        checkbox.clicked.connect(lambda state, text_=roi_id: structure_tab.
                                 structure_checked(state, text_))
        if text in structure_tab.standard_organ_names or text in structure_tab.standard_volume_names:
            self.standard_name = True
            checkbox.setStyleSheet("font: 10pt \"Laksaman\";")
        else:
            self.standard_name = False
            checkbox.setStyleSheet("font: 10pt \"Laksaman\"; color: red;")
        for item in structure_tab.standard_volume_names:  # Any suffix number will still be considered standard.
            if text.startswith(item):
                self.standard_name = True
                checkbox.setStyleSheet("font: 10pt \"Laksaman\";")
        checkbox.setText(text)
        self.layout.addWidget(checkbox)

        self.layout.setAlignment(Qt.AlignLeft)

        self.setLayout(self.layout)
    def update_rtss(self, patient):
        """
        Updates the patient dict container with the newly created RTSS (if a
        process generates one), so it can be used by future processes.
        :param patient: The patient with the newly-created RTSS.
        """
        # Get new RTSS
        rtss = PatientDictContainer().dataset['rtss']

        # Create a series and image from the RTSS
        rtss_series = Series(rtss.SeriesInstanceUID)
        rtss_series.series_description = rtss.get("SeriesDescription")
        rtss_image = Image(PatientDictContainer().filepaths['rtss'],
                           rtss.SOPInstanceUID, rtss.SOPClassUID,
                           rtss.Modality)
        rtss_series.add_image(rtss_image)

        # Add the new study to the patient
        patient.studies[rtss.StudyInstanceUID].add_series(rtss_series)

        # Update the patient dict container
        PatientDictContainer().set("rtss_modified", False)
Example #14
0
    def set_selected_roi_name(self, roi_name):
        """
        function to set selected roi name
        :param roi_name: roi name selected
        """
        roi_exists = False

        patient_dict_container = PatientDictContainer()
        existing_rois = patient_dict_container.get("rois")
        number_of_rois = len(existing_rois)

        # Check to see if the ROI already exists
        for key, value in existing_rois.items():
            if roi_name in value['name']:
                roi_exists = True

        if roi_exists:
            QMessageBox.about(self.draw_roi_window_instance,
                              "ROI already exists in RTSS",
                              "Would you like to continue?")

        self.ROI_name = roi_name
        self.roi_name_line_edit.setText(self.ROI_name)
Example #15
0
class IsodoseTab(QtWidgets.QWidget):

    request_update_isodoses = QtCore.pyqtSignal()

    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.rx_dose_in_cgray = self.patient_dict_container.get("rx_dose_in_cgray")
        self.color_dict = self.init_color_isod()
        self.color_squares = self.init_color_squares()
        self.checkboxes = self.init_checkboxes()

        self.isodose_tab_layout = QtWidgets.QVBoxLayout()
        self.isodose_tab_layout.setAlignment(QtCore.Qt.AlignTop)
        self.isodose_tab_layout.setSpacing(0)
        self.init_layout()

        self.setLayout(self.isodose_tab_layout)

    def init_layout(self):
        for i in range(0, len(self.checkboxes)):
            widget_isodose = QtWidgets.QWidget()
            layout_isodose = QtWidgets.QHBoxLayout(widget_isodose)
            layout_isodose.setAlignment(QtCore.Qt.AlignLeft)
            layout_isodose.addWidget(self.color_squares[i])
            layout_isodose.addWidget(self.checkboxes[i])
            self.isodose_tab_layout.addWidget(widget_isodose)

    def init_color_isod(self):
        """
        Create a list containing the colors for each isodose.

        :return: Dictionary where the key is the percentage of isodose and the value a QColor object.
        """
        roi_color = {
            107: QtGui.QColor(131, 0, 0),
            105: QtGui.QColor(185, 0, 0),
            100: QtGui.QColor(255, 46, 0),
            95: QtGui.QColor(255, 161, 0),
            90: QtGui.QColor(253, 255, 0),
            80: QtGui.QColor(0, 255, 0),
            70: QtGui.QColor(0, 143, 0),
            60: QtGui.QColor(0, 255, 255),
            30: QtGui.QColor(33, 0, 255),
            10: QtGui.QColor(11, 0, 134)
        }

        return roi_color

    def init_color_squares(self):
        """
        Create a color square.
        """
        list_of_squares = []
        for key, color in self.color_dict.items():
            list_of_squares.append(self.draw_color_square(color))

        return list_of_squares

    def init_checkboxes(self):
        """
        Initialize the checkbox objects.
        """
        list_of_checkboxes = []
        # Values of Isodoses
        list_of_doses = []
        for percentage in isodose_percentages:
            dose = int(self.rx_dose_in_cgray * (percentage / 100))
            list_of_doses.append(dose)

        # Checkboxes
        first_iteration = True
        for i in range(10):
            if first_iteration:
                checkbox = QtWidgets.QCheckBox("%s %% / %s cGy [Max]" % (str(isodose_percentages[i]),
                                                                         str(list_of_doses[i])))
                first_iteration = False
            else:
                checkbox = QtWidgets.QCheckBox("%s %% / %s cGy" % (str(isodose_percentages[i]), str(list_of_doses[i])))
            checkbox.clicked.connect(lambda state, text=isodose_percentages[i]: self.checked_dose(state, text))
            checkbox.setStyleSheet("font: 10pt \"Laksaman\";")
            list_of_checkboxes.append(checkbox)

        return list_of_checkboxes

    # Function triggered when a dose level selected
    # Updates the list of selected isodoses and dicom view
    def checked_dose(self, state, isod_value):
        """
        Function triggered when the checkbox of a structure is checked / unchecked.
        Update the list of selected structures.
        Update the DICOM view.

        :param state: True if the checkbox is checked, False otherwise.
        :param isod_value: Percentage of isodose.
        """

        selected_doses = self.patient_dict_container.get("selected_doses")

        if state:
            # Add the dose to the list of selected doses
            selected_doses.append(isod_value)
        else:
            # Remove dose from list of previously selected doses
            selected_doses.remove(isod_value)

        self.patient_dict_container.set("selected_doses", selected_doses)

        # Update the dicom view
        self.request_update_isodoses.emit()

    def draw_color_square(self, color):
        """
        Create a color square.
        :param color: QColor object
        :return: Color square widget.
        """
        color_square_label = QtWidgets.QLabel()
        color_square_pix = QtGui.QPixmap(15, 15)
        color_square_pix.fill(color)
        color_square_label.setPixmap(color_square_pix)

        return color_square_label
Example #16
0
class IsodoseTab(QtWidgets.QWidget):

    request_update_isodoses = QtCore.Signal()
    request_update_ui = QtCore.Signal(tuple)

    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.rx_dose_in_cgray = self.patient_dict_container.get(
            "rx_dose_in_cgray")
        self.color_dict = self.init_color_isod()
        self.color_squares = self.init_color_squares()
        self.checkboxes = self.init_checkboxes()

        # Create and initialise ISO2ROI button and layout
        self.iso2roi_button = QtWidgets.QPushButton()
        self.iso2roi_button.setText("Convert Isodoses to ROIs")
        self.iso2roi_button.clicked.connect(self.iso2roi_button_clicked)

        self.iso2roi_layout = QtWidgets.QHBoxLayout()
        self.iso2roi_layout.setContentsMargins(0, 0, 0, 0)
        self.iso2roi_layout.addWidget(self.iso2roi_button)

        self.isodose_tab_layout = QtWidgets.QVBoxLayout()
        self.isodose_tab_layout.setAlignment(QtCore.Qt.AlignTop
                                             | QtCore.Qt.AlignTop)
        self.isodose_tab_layout.setSpacing(0)
        self.init_layout()
        self.iso2roi = ISO2ROI()

        # Add button to tab
        self.isodose_tab_layout.addStretch()
        self.isodose_tab_layout.addLayout(self.iso2roi_layout)

        self.setLayout(self.isodose_tab_layout)
        self.progress_window = ProgressWindow(
            self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
        self.progress_window.signal_loaded.connect(self.on_loaded_iso2roi)

    def init_layout(self):
        for i in range(0, len(self.checkboxes)):
            widget_isodose = QtWidgets.QWidget()
            layout_isodose = QtWidgets.QHBoxLayout(widget_isodose)
            layout_isodose.setAlignment(QtCore.Qt.AlignLeft
                                        | QtCore.Qt.AlignLeft)
            layout_isodose.addWidget(self.color_squares[i])
            layout_isodose.addWidget(self.checkboxes[i])
            self.isodose_tab_layout.addWidget(widget_isodose)

    def init_color_isod(self):
        """
        Create a list containing the colors for each isodose.

        :return: Dictionary where the key is the percentage of isodose and the value a QColor object.
        """
        roi_color = {
            107: QtGui.QColor(131, 0, 0),
            105: QtGui.QColor(185, 0, 0),
            100: QtGui.QColor(255, 46, 0),
            95: QtGui.QColor(255, 161, 0),
            90: QtGui.QColor(253, 255, 0),
            80: QtGui.QColor(0, 255, 0),
            70: QtGui.QColor(0, 143, 0),
            60: QtGui.QColor(0, 255, 255),
            30: QtGui.QColor(33, 0, 255),
            10: QtGui.QColor(11, 0, 134)
        }

        return roi_color

    def init_color_squares(self):
        """
        Create a color square.
        """
        list_of_squares = []
        for key, color in self.color_dict.items():
            list_of_squares.append(self.draw_color_square(color))

        return list_of_squares

    def init_checkboxes(self):
        """
        Initialize the checkbox objects.
        """
        list_of_checkboxes = []
        # Values of Isodoses
        list_of_doses = []
        for percentage in isodose_percentages:
            dose = int(self.rx_dose_in_cgray * (percentage / 100))
            list_of_doses.append(dose)

        # Checkboxes
        def generate_clicked_handler(text):
            def handler(state):
                self.checked_dose(state, text)

            return handler

        first_iteration = True
        for i in range(10):
            if first_iteration:
                checkbox = QtWidgets.QCheckBox(
                    "%s %% / %s cGy [Max]" %
                    (str(isodose_percentages[i]), str(list_of_doses[i])))
                first_iteration = False
            else:
                checkbox = QtWidgets.QCheckBox(
                    "%s %% / %s cGy" %
                    (str(isodose_percentages[i]), str(list_of_doses[i])))
            checkbox.clicked.connect(
                generate_clicked_handler(isodose_percentages[i]))
            checkbox.setStyleSheet("font: 10pt \"Laksaman\";")
            list_of_checkboxes.append(checkbox)

        return list_of_checkboxes

    # Function triggered when a dose level selected
    # Updates the list of selected isodoses and dicom view
    def checked_dose(self, state, isod_value):
        """
        Function triggered when the checkbox of a structure is checked / unchecked.
        Update the list of selected structures.
        Update the DICOM view.

        :param state: True if the checkbox is checked, False otherwise.
        :param isod_value: Percentage of isodose.
        """

        selected_doses = self.patient_dict_container.get("selected_doses")

        if state:
            # Add the dose to the list of selected doses
            selected_doses.append(isod_value)
        else:
            # Remove dose from list of previously selected doses
            selected_doses.remove(isod_value)

        self.patient_dict_container.set("selected_doses", selected_doses)

        # Update the dicom view
        self.request_update_isodoses.emit()

    def draw_color_square(self, color):
        """
        Create a color square.
        :param color: QColor object
        :return: Color square widget.
        """
        color_square_label = QtWidgets.QLabel()
        color_square_pix = QtGui.QPixmap(15, 15)
        color_square_pix.fill(color)
        color_square_label.setPixmap(color_square_pix)

        return color_square_label

    def iso2roi_button_clicked(self):
        """
        Clicked action handler for the ISO2ROI button.
        Opens a progress window and Initiates the
        ISO2ROI conversion process.
        """
        self.progress_window.start(self.iso2roi.start_conversion)

    def on_loaded_iso2roi(self):
        """
        Called when progress bar has finished.
        Closes the progress window and refreshes
        the main screen.
        """
        self.request_update_ui.emit(
            (self.patient_dict_container.get('dataset_rtss'), {
                "draw": None
            }))
        self.progress_window.close()
Example #17
0
class StructureTab(QtWidgets.QWidget):
    request_update_structures = QtCore.Signal()

    def __init__(self, moving=False):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.moving_dict_container = MovingDictContainer()
        self.rois = self.patient_dict_container.get("rois")
        self.color_dict = self.init_color_roi(self.patient_dict_container)
        self.patient_dict_container.set("roi_color_dict", self.color_dict)
        self.structure_tab_layout = QtWidgets.QVBoxLayout()

        self.roi_delete_handler = ROIDelOption(
            self.fixed_container_structure_modified)
        self.roi_draw_handler = ROIDrawOption(
            self.fixed_container_structure_modified)
        self.roi_manipulate_handler = ROIManipulateOption(
            self.fixed_container_structure_modified)

        # Create scrolling area widget to contain the content.
        self.scroll_area = QtWidgets.QScrollArea()
        self.scroll_area.setWidgetResizable(True)

        self.scroll_area_content = QtWidgets.QWidget(self.scroll_area)
        self.scroll_area.ensureWidgetVisible(self.scroll_area_content)

        # Create layout for checkboxes and colour squares
        self.layout_content = QtWidgets.QVBoxLayout(self.scroll_area_content)
        self.layout_content.setContentsMargins(0, 0, 0, 0)
        self.layout_content.setSpacing(0)
        self.layout_content.setAlignment(QtCore.Qt.AlignTop
                                         | QtCore.Qt.AlignTop)

        # Create list of standard organ and volume names
        self.standard_organ_names = []
        self.standard_volume_names = []
        self.init_standard_names()

        # Create StructureWidget objects
        self.update_content()

        # Create a modified indicator
        self.modified_indicator_widget = QtWidgets.QWidget()
        self.modified_indicator_widget.setContentsMargins(8, 5, 8, 5)
        modified_indicator_layout = QtWidgets.QHBoxLayout()
        modified_indicator_layout.setAlignment(QtCore.Qt.AlignLeft
                                               | QtCore.Qt.AlignLeft)

        modified_indicator_icon = QtWidgets.QLabel()
        modified_indicator_icon.setPixmap(
            QtGui.QPixmap(
                resource_path("res/images/btn-icons/alert_icon.png")))
        modified_indicator_layout.addWidget(modified_indicator_icon)

        modified_indicator_text = QtWidgets.QLabel(
            "Structures have been modified")
        modified_indicator_text.setStyleSheet("color: red")
        modified_indicator_layout.addWidget(modified_indicator_text)

        self.modified_indicator_widget.setLayout(modified_indicator_layout)
        self.modified_indicator_widget.mouseReleaseEvent = self.\
            save_new_rtss_to_fixed_image_set
        self.modified_indicator_widget.setVisible(False)

        # Create ROI manipulation buttons
        self.button_roi_manipulate = QtWidgets.QPushButton()
        self.button_roi_draw = QtWidgets.QPushButton()
        self.button_roi_delete = QtWidgets.QPushButton()
        self.roi_buttons = QtWidgets.QWidget()
        self.init_roi_buttons()

        # Set layout
        self.structure_tab_layout.addWidget(self.scroll_area)
        self.structure_tab_layout.addWidget(self.modified_indicator_widget)
        self.structure_tab_layout.addWidget(self.roi_buttons)
        self.setLayout(self.structure_tab_layout)

    def init_color_roi(self, dict_container):
        """
        Create a dictionary containing the colors for each structure.
        :param: either PatientDictContainer or MovingDictContainer
        :return: Dictionary where the key is the ROI number and the value a
        QColor object.
        """
        roi_color = dict()
        roi_contour_info = dict_container.get(
            "dict_dicom_tree_rtss")['ROI Contour Sequence']

        if len(roi_contour_info) > 0:
            for item, roi_dict in roi_contour_info.items():
                # Note: the keys of roiContourInfo are "item 0", "item 1",
                # etc. As all the ROI structures are identified by the ROI
                # numbers in the whole code, we get the ROI number 'roi_id'
                # by using the member 'list_roi_numbers'
                id = item.split()[1]
                roi_id = dict_container.get("list_roi_numbers")[int(id)]
                if 'ROI Display Color' in roi_contour_info[item]:
                    RGB_list = roi_contour_info[item]['ROI Display Color'][0]
                    red = RGB_list[0]
                    green = RGB_list[1]
                    blue = RGB_list[2]
                else:
                    seed(1)
                    red = randint(0, 255)
                    green = randint(0, 255)
                    blue = randint(0, 255)

                roi_color[roi_id] = QtGui.QColor(red, green, blue)

        return roi_color

    def init_standard_names(self):
        """
        Create two lists containing standard organ and standard volume names
        as set by the Add-On options.
        """
        with open(data_path('organName.csv'), 'r') as f:
            self.standard_organ_names = []

            csv_input = csv.reader(f)
            header = next(f)  # Ignore the "header" of the column
            for row in csv_input:
                self.standard_organ_names.append(row[0])

        with open(data_path('volumeName.csv'), 'r') as f:
            self.standard_volume_names = []

            csv_input = csv.reader(f)
            header = next(f)  # Ignore the "header" of the column
            for row in csv_input:
                self.standard_volume_names.append(row[1])

    def init_roi_buttons(self):
        icon_roi_delete = QtGui.QIcon()
        icon_roi_delete.addPixmap(
            QtGui.QPixmap(
                resource_path('res/images/btn-icons/delete_icon.png')),
            QtGui.QIcon.Normal, QtGui.QIcon.On)

        icon_roi_draw = QtGui.QIcon()
        icon_roi_draw.addPixmap(
            QtGui.QPixmap(resource_path('res/images/btn-icons/draw_icon.png')),
            QtGui.QIcon.Normal, QtGui.QIcon.On)

        icon_roi_manipulate = QtGui.QIcon()
        icon_roi_manipulate.addPixmap(
            QtGui.QPixmap(
                resource_path('res/images/btn-icons/manipulate_icon.png')),
            QtGui.QIcon.Normal, QtGui.QIcon.On)

        self.button_roi_delete.setIcon(icon_roi_delete)
        self.button_roi_delete.setText("Delete ROI")
        self.button_roi_delete.clicked.connect(self.roi_delete_clicked)

        self.button_roi_draw.setIcon(icon_roi_draw)
        self.button_roi_draw.setText("Draw ROI")
        self.button_roi_draw.clicked.connect(self.roi_draw_clicked)

        self.button_roi_manipulate.setIcon(icon_roi_manipulate)
        self.button_roi_manipulate.setText("Manipulate ROI")
        self.button_roi_manipulate.clicked.connect(self.roi_manipulate_clicked)

        layout_roi_buttons = QtWidgets.QVBoxLayout(self.roi_buttons)
        layout_roi_buttons.setContentsMargins(0, 0, 0, 0)
        layout_roi_buttons.addWidget(self.button_roi_draw)
        layout_roi_buttons.addWidget(self.button_roi_manipulate)
        layout_roi_buttons.addWidget(self.button_roi_delete)

    def update_ui(self, moving=False):
        """
        Update the UI of Structure Tab when a new patient is opened
        """
        self.patient_dict_container = PatientDictContainer()
        self.rois = self.patient_dict_container.get("rois")
        self.color_dict = self.init_color_roi(self.patient_dict_container)
        self.patient_dict_container.set("roi_color_dict", self.color_dict)
        if hasattr(self, "modified_indicator_widget"):
            self.modified_indicator_widget.setParent(None)
        self.update_content()

    def update_content(self):
        """
        Add the contents (color square and checkbox) in the scrolling area
        widget.
        """
        # Clear the children
        for i in reversed(range(self.layout_content.count())):
            self.layout_content.itemAt(i).widget().setParent(None)

        row = 0
        for roi_id, roi_dict in self.rois.items():
            # Creates a widget representing each ROI
            color = self.color_dict[roi_id]
            color.setAlpha(255)
            structure = StructureWidget(roi_id, color, roi_dict['name'], self)
            if roi_id in self.patient_dict_container.get("selected_rois"):
                structure.checkbox.setChecked(Qt.Checked)
            structure.structure_renamed.\
                connect(self.fixed_container_structure_modified)
            self.layout_content.addWidget(structure)
            row += 1

        self.scroll_area.setStyleSheet(
            "QScrollArea {background-color: #ffffff; border-style: none;}")
        self.scroll_area_content.setStyleSheet(
            "QWidget {background-color: #ffffff; border-style: none;}")

        self.scroll_area.setWidget(self.scroll_area_content)

    def roi_delete_clicked(self):
        self.roi_delete_handler.show_roi_delete_options()

    def roi_draw_clicked(self):
        self.roi_draw_handler.show_roi_draw_options()

    def roi_manipulate_clicked(self):
        """ Open ROI Manipulate Window """
        self.roi_manipulate_handler.show_roi_manipulate_options(
            self.color_dict)

    def moving_container_structure_modified(self, changes):
        """
        Executes when a structure of moving container is modified.
        Changes is a tuple of (new_dataset,
        description_of_changes)
        description_of_changes follows the format
        {"type_of_change": value_of_change}.
        Examples:
        {"rename": ["TOOTH", "TEETH"]} represents that the TOOTH structure has
            been renamed to TEETH.
        {"delete": ["TEETH", "MAXILLA"]} represents that the TEETH and MAXILLA
            structures have been deleted.
        {"draw": "AORTA"} represents that a new structure AORTA has been drawn.
        Note: Use {"draw": None} after multiple ROIs are generated
        (E.g., from ISO2ROI functionality), and use {"transfer":None} for
         ROI Transfer instead of calling this function
        multiple times. This will trigger auto save.
        """
        new_dataset = changes[0]
        change_description = changes[1]

        # If this is the first change made to the RTSS file, update the
        # dataset with the new one so that OnkoDICOM starts working off this
        # dataset rather than the original RTSS file.
        self.moving_dict_container.set("rtss_modified", True)
        self.moving_dict_container.set("dataset_rtss", new_dataset)

        # Refresh ROIs in main page
        self.moving_dict_container.set("rois",
                                       ImageLoading.get_roi_info(new_dataset))
        self.rois = self.moving_dict_container.get("rois")
        contour_data = ImageLoading.get_raw_contour_data(new_dataset)
        self.moving_dict_container.set("raw_contour", contour_data[0])
        self.moving_dict_container.set("num_points", contour_data[1])
        pixluts = ImageLoading.get_pixluts(self.moving_dict_container.dataset)
        self.moving_dict_container.set("pixluts", pixluts)
        self.moving_dict_container.set(
            "list_roi_numbers",
            ordered_list_rois(self.moving_dict_container.get("rois")))
        self.moving_dict_container.set("selected_rois", [])
        self.moving_dict_container.set("dict_polygons_axial", {})
        self.moving_dict_container.set("dict_polygons_sagittal", {})
        self.moving_dict_container.set("dict_polygons_coronal", {})

        if "draw" in change_description or "transfer" in change_description:
            dicom_tree_rtss = DicomTree(None)
            dicom_tree_rtss.dataset = new_dataset
            dicom_tree_rtss.dict = dicom_tree_rtss.dataset_to_dict(
                dicom_tree_rtss.dataset)
            self.moving_dict_container.set("dict_dicom_tree_rtss",
                                           dicom_tree_rtss.dict)
            self.color_dict = self.init_color_roi(self.moving_dict_container)
            self.moving_dict_container.set("roi_color_dict", self.color_dict)
            if self.moving_dict_container.has_attribute("raw_dvh"):
                # DVH will be outdated once changes to it are made, and
                # recalculation will be required.
                self.moving_dict_container.set("dvh_outdated", True)

        if self.moving_dict_container.has_modality("raw_dvh"):
            # Rename structures in DVH list
            if "rename" in change_description:
                new_raw_dvh = self.moving_dict_container.get("raw_dvh")
                for key, dvh in new_raw_dvh.items():
                    if dvh.name == change_description["rename"][0]:
                        dvh.name = change_description["rename"][1]
                        break

                self.moving_dict_container.set("raw_dvh", new_raw_dvh)

            # Remove structures from DVH list - the only visible effect of
            # this section is the exported DVH csv
            if "delete" in change_description:
                list_of_deleted = []
                new_raw_dvh = self.moving_dict_container.get("raw_dvh")
                for key, dvh in new_raw_dvh.items():
                    if dvh.name in change_description["delete"]:
                        list_of_deleted.append(key)
                for key in list_of_deleted:
                    new_raw_dvh.pop(key)
                self.moving_dict_container.set("raw_dvh", new_raw_dvh)

        if "transfer" in change_description \
                and change_description["transfer"] is None:
            self.save_new_rtss_to_moving_image_set()

    def fixed_container_structure_modified(self, changes):
        """
        Executes when a structure of fixed patient container is modified
        Displays indicator that structure has changed.
        Changes is a tuple of (new_dataset,
        description_of_changes)
        description_of_changes follows the format
        {"type_of_change": value_of_change}.
        Examples:
        {"rename": ["TOOTH", "TEETH"]} represents that the TOOTH structure has
            been renamed to TEETH.
        {"delete": ["TEETH", "MAXILLA"]} represents that the TEETH and MAXILLA
            structures have been deleted.
        {"draw": "AORTA"} represents that a new structure AORTA has been drawn.
        Note: Use {"draw": None} after multiple ROIs are generated
        (E.g., from ISO2ROI functionality), and use {"transfer":None} for
         ROI Transfer instead of calling this function
        multiple times. This will trigger auto save.
        """

        new_dataset = changes[0]
        change_description = changes[1]

        # Only show the modified indicator if description_of_changes is
        # not {"draw": None}, as this description means that the RTSS
        # is autosaved, and therefore there is no need to tell the user
        # that the RTSS has been modified
        if not("draw" in change_description
               and change_description["draw"] is None) and \
                not ("transfer" in change_description):
            self.show_modified_indicator()

        # If this is the first change made to the RTSS file, update the
        # dataset with the new one so that OnkoDICOM starts working off this
        # dataset rather than the original RTSS file.
        self.patient_dict_container.set("rtss_modified", True)
        self.patient_dict_container.set("dataset_rtss", new_dataset)

        # Refresh ROIs in main page
        self.patient_dict_container.set("rois",
                                        ImageLoading.get_roi_info(new_dataset))
        self.rois = self.patient_dict_container.get("rois")
        contour_data = ImageLoading.get_raw_contour_data(new_dataset)
        self.patient_dict_container.set("raw_contour", contour_data[0])
        self.patient_dict_container.set("num_points", contour_data[1])
        pixluts = ImageLoading.get_pixluts(self.patient_dict_container.dataset)
        self.patient_dict_container.set("pixluts", pixluts)
        self.patient_dict_container.set(
            "list_roi_numbers",
            ordered_list_rois(self.patient_dict_container.get("rois")))
        self.patient_dict_container.set("selected_rois", [])
        self.patient_dict_container.set("dict_polygons_axial", {})
        self.patient_dict_container.set("dict_polygons_sagittal", {})
        self.patient_dict_container.set("dict_polygons_coronal", {})

        if "draw" in change_description or "transfer" in change_description:
            dicom_tree_rtss = DicomTree(None)
            dicom_tree_rtss.dataset = new_dataset
            dicom_tree_rtss.dict = dicom_tree_rtss.dataset_to_dict(
                dicom_tree_rtss.dataset)
            self.patient_dict_container.set("dict_dicom_tree_rtss",
                                            dicom_tree_rtss.dict)
            self.color_dict = self.init_color_roi(self.patient_dict_container)
            self.patient_dict_container.set("roi_color_dict", self.color_dict)
            if self.patient_dict_container.has_attribute("raw_dvh"):
                # DVH will be outdated once changes to it are made, and
                # recalculation will be required.
                self.patient_dict_container.set("dvh_outdated", True)

        if self.patient_dict_container.has_attribute("raw_dvh"):
            # Rename structures in DVH list
            if "rename" in change_description:
                new_raw_dvh = self.patient_dict_container.get("raw_dvh")
                for key, dvh in new_raw_dvh.items():
                    if dvh.name == change_description["rename"][0]:
                        dvh.name = change_description["rename"][1]
                        break

                self.patient_dict_container.set("raw_dvh", new_raw_dvh)
                dvh2rtdose(new_raw_dvh)

            # Remove structures from DVH list - the only visible effect of
            # this section is the exported DVH csv
            if "delete" in change_description:
                list_of_deleted = []
                new_raw_dvh = self.patient_dict_container.get("raw_dvh")
                for key, dvh in new_raw_dvh.items():
                    if dvh.name in change_description["delete"]:
                        list_of_deleted.append(key)
                for key in list_of_deleted:
                    new_raw_dvh.pop(key)
                self.patient_dict_container.set("raw_dvh", new_raw_dvh)
                dvh2rtdose(new_raw_dvh)

        # Refresh ROIs in DVH tab and DICOM View
        self.request_update_structures.emit()

        # Refresh structure tab
        self.update_content()

        if "draw" in change_description and change_description["draw"] is None:
            self.save_new_rtss_to_fixed_image_set(auto=True)
        elif "transfer" in change_description \
                and change_description["transfer"] is None:
            self.save_new_rtss_to_fixed_image_set(auto=True)

    def show_modified_indicator(self):
        self.modified_indicator_widget.setVisible(True)

    def structure_checked(self, state, roi_id):
        """
        Function triggered when the checkbox of a structure is
        checked / unchecked.
        Update the list of selected structures.
        Update the plot of the DVH and the DICOM view.

        :param state: True if the checkbox is checked, False otherwise.
        :param roi_id: ROI number
        """

        selected_rois = self.patient_dict_container.get("selected_rois")
        if state:
            selected_rois.append(roi_id)
        else:
            selected_rois.remove(roi_id)

        self.patient_dict_container.set("selected_rois", selected_rois)
        self.update_dict_polygons(state, roi_id)

        self.request_update_structures.emit()

    def update_dict_polygons(self, state, roi_id):
        """
        Update the polygon dictionaries (axial, coronal, sagittal) used to
        display the ROIs.
        :param state: True if the ROI is selected, False otherwise
        :param roi_id: ROI number
        """
        rois = self.patient_dict_container.get("rois")
        new_dict_polygons_axial = self.patient_dict_container.get(
            "dict_polygons_axial")
        new_dict_polygons_coronal = self.patient_dict_container.get(
            "dict_polygons_coronal")
        new_dict_polygons_sagittal = self.patient_dict_container.get(
            "dict_polygons_sagittal")
        aspect = self.patient_dict_container.get("pixmap_aspect")
        roi_name = rois[roi_id]['name']

        if state:
            new_dict_polygons_axial[roi_name] = {}
            new_dict_polygons_coronal[roi_name] = {}
            new_dict_polygons_sagittal[roi_name] = {}
            dict_rois_contours_axial = get_roi_contour_pixel(
                self.patient_dict_container.get("raw_contour"), [roi_name],
                self.patient_dict_container.get("pixluts"))
            dict_rois_contours_coronal, dict_rois_contours_sagittal = \
                transform_rois_contours(
                    dict_rois_contours_axial)

            for slice_id in self.patient_dict_container.get(
                    "dict_uid").values():
                polygons = calc_roi_polygon(roi_name, slice_id,
                                            dict_rois_contours_axial)
                new_dict_polygons_axial[roi_name][slice_id] = polygons

            for slice_id in range(
                    0,
                    len(self.patient_dict_container.get("pixmaps_coronal"))):
                polygons_coronal = calc_roi_polygon(
                    roi_name, slice_id, dict_rois_contours_coronal,
                    aspect["coronal"])
                polygons_sagittal = calc_roi_polygon(
                    roi_name, slice_id, dict_rois_contours_sagittal,
                    1 / aspect["sagittal"])
                new_dict_polygons_coronal[roi_name][
                    slice_id] = polygons_coronal
                new_dict_polygons_sagittal[roi_name][
                    slice_id] = polygons_sagittal

            self.patient_dict_container.set("dict_polygons_axial",
                                            new_dict_polygons_axial)
            self.patient_dict_container.set("dict_polygons_coronal",
                                            new_dict_polygons_coronal)
            self.patient_dict_container.set("dict_polygons_sagittal",
                                            new_dict_polygons_sagittal)
        else:
            new_dict_polygons_axial.pop(roi_name, None)
            new_dict_polygons_coronal.pop(roi_name, None)
            new_dict_polygons_sagittal.pop(roi_name, None)

    def on_rtss_selected(self, selected_rtss):
        """
        Function to run after a rtss is selected from SelectRTSSPopUp
        """
        self.patient_dict_container.get("existing_rtss_files").clear()
        self.patient_dict_container.get("existing_rtss_files").append(
            selected_rtss)
        self.save_new_rtss(auto=True)

    def display_select_rtss_window(self):
        """
        Display a pop up window that contains all RTSSs attached to the
        selected image set.
        """
        self.select_rtss_window = SelectRTSSPopUp(
            self.patient_dict_container.get("existing_rtss_files"),
            parent=self)
        self.select_rtss_window.signal_rtss_selected.connect(
            self.on_rtss_selected)
        self.select_rtss_window.show()

    def save_new_rtss_to_fixed_image_set(self, event=None, auto=False):
        """
        Save the current RTSS stored in fixed patient dictionary to the file
        system. :param event: Not used but will be passed as an argument
        from modified_indicator_widget on mouseReleaseEvent :param auto:
        Used for auto save without user confirmation
        """
        existing_rtss_files = self.patient_dict_container.get(
            "existing_rtss_files")
        if len(existing_rtss_files) == 1:
            if isinstance(existing_rtss_files[0], Series):
                existing_rtss_directory = str(
                    Path(existing_rtss_files[0].get_files()[0]))
            else:
                # This "else" is used by iso2roi gui and structure tab tests to
                # quickly set existing_rtss_directory
                existing_rtss_directory = existing_rtss_files[0]
        elif len(existing_rtss_files) > 1:
            self.display_select_rtss_window()
            # This function will be called again when a RTSS is selected
            return
        else:
            existing_rtss_directory = None

        rtss_directory = str(Path(
            self.patient_dict_container.get("file_rtss")))

        if auto:
            confirm_save = QtWidgets.QMessageBox.Yes
        else:
            confirm_save = \
                QtWidgets.QMessageBox.information(self, "Confirmation",
                                                  "Are you sure you want to "
                                                  "save the modified RTSTRUCT "
                                                  "file? This will overwrite "
                                                  "the existing file. This is "
                                                  "not reversible.",
                                                  QtWidgets.QMessageBox.Yes,
                                                  QtWidgets.QMessageBox.No)

        if confirm_save == QtWidgets.QMessageBox.Yes:
            if existing_rtss_directory is None:
                self.patient_dict_container.get("dataset_rtss").save_as(
                    rtss_directory)
            else:
                new_rtss = self.patient_dict_container.get("dataset_rtss")
                old_rtss = pydicom.dcmread(existing_rtss_directory, force=True)
                old_roi_names = \
                    set(value["name"] for value in
                        ImageLoading.get_roi_info(old_rtss).values())
                new_roi_names = \
                    set(value["name"] for value in
                        self.patient_dict_container.get("rois").values())
                duplicated_names = old_roi_names.intersection(new_roi_names)

                # stop if there are conflicting roi names and user do not
                # wish to proceed.
                if duplicated_names and not self.display_confirm_merge(
                        duplicated_names):
                    return

                merged_rtss = merge_rtss(old_rtss, new_rtss, duplicated_names)
                merged_rtss.save_as(existing_rtss_directory)

            if not auto:
                QtWidgets.QMessageBox.about(
                    self.parentWidget(), "File saved",
                    "The RTSTRUCT file has been saved.")
            self.patient_dict_container.set("rtss_modified", False)
            # Hide the modified indicator
            self.modified_indicator_widget.setVisible(False)

    def save_new_rtss_to_moving_image_set(self, event=None):
        """
        Save the current RTSS stored in moving patient dictionary to the
        file system. ROIs modification into moving patient dict is auto
        saved :param event: Not used but will be passed as an argument from
        modified_indicator_widget on mouseReleaseEvent
        """
        if self.moving_dict_container.get("existing_file_rtss") is not None:
            existing_rtss_directory = str(
                Path(self.moving_dict_container.get("existing_file_rtss")))
        else:
            existing_rtss_directory = None
        rtss_directory = str(Path(self.moving_dict_container.get("file_rtss")))

        if existing_rtss_directory is None:
            self.moving_dict_container.get("dataset_rtss").save_as(
                rtss_directory)
        else:
            new_rtss = self.moving_dict_container.get("dataset_rtss")
            old_rtss = pydicom.dcmread(existing_rtss_directory, force=True)
            old_roi_names = \
                set(value["name"] for value in
                    ImageLoading.get_roi_info(old_rtss).values())
            new_roi_names = \
                set(value["name"] for value in
                    self.moving_dict_container.get("rois").values())
            duplicated_names = old_roi_names.intersection(new_roi_names)
            merged_rtss = merge_rtss(old_rtss, new_rtss, duplicated_names)
            merged_rtss.save_as(existing_rtss_directory)
        self.moving_dict_container.set("rtss_modified", False)

    def display_confirm_merge(self, duplicated_names):
        confirm_merge = QtWidgets.QMessageBox(parent=self)
        confirm_merge.setIcon(QtWidgets.QMessageBox.Question)
        confirm_merge.setWindowTitle("Merge RTSTRUCTs?")
        confirm_merge.setText("Conflicting ROI names found between new ROIs "
                              "and existing ROIs:\n" + str(duplicated_names) +
                              "\nAre you sure you want to merge the RTSTRUCT "
                              "files? The new ROIs will replace the existing "
                              "ROIs. ")
        button_yes = QtWidgets.QPushButton("Yes, I want to merge")
        button_no = QtWidgets.QPushButton("No, I will change the names")
        """ 
        We want the buttons 'No' & 'Yes' to be displayed in that exact 
        order. QMessageBox displays buttons in respect to their assigned 
        roles. (0 first, then 0 and so on) 'AcceptRole' is 0 and 
        'RejectRole' is 1 thus by counterintuitively assigning 'No' to 
        'AcceptRole' and 'Yes' to 'RejectRole' the buttons are 
        positioned as desired.
        """
        confirm_merge.addButton(button_no, QtWidgets.QMessageBox.AcceptRole)
        confirm_merge.addButton(button_yes, QtWidgets.QMessageBox.RejectRole)
        confirm_merge.exec_()

        if confirm_merge.clickedButton() == button_yes:
            return True
        return False
Example #18
0
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
Example #19
0
    def generate_ROI(self, contours, progress_callback):
        """
        Generates new ROIs based on contour data.
        :param contours: dictionary of contours to turn into ROIs.
        :param progress_callback: signal that receives the current
                                  progress of the loading.
        """
        # Initialise variables needed for function
        patient_dict_container = PatientDictContainer()
        dataset_rtss = patient_dict_container.get("dataset_rtss")

        # Get existing ROIs
        existing_rois = []
        rois = patient_dict_container.get("dataset_rtss")
        if rois:
            for roi in rois.StructureSetROISequence:
                existing_rois.append(roi.ROIName)

        # Loop through each SUV level
        item_count = len(contours)
        current_progress = 60
        progress_increment = round((95 - 60) / item_count)
        for item in contours:
            # Delete ROI if it already exists to recreate it
            if item in existing_rois:
                dataset_rtss = ROI.delete_roi(dataset_rtss, item)

                # Update patient dict container
                current_rois = patient_dict_container.get("rois")
                keys = []
                for key, value in current_rois.items():
                    if value["name"] == item:
                        keys.append(key)
                for key in keys:
                    del current_rois[key]
                patient_dict_container.set("rois", current_rois)

            progress_callback.emit(("Generating ROIs", current_progress))
            current_progress += progress_increment

            # Loop through each slice
            for i in range(len(contours[item])):
                slider_id = contours[item][i][0]
                dataset = patient_dict_container.dataset[slider_id]
                pixlut = patient_dict_container.get("pixluts")
                pixlut = pixlut[dataset.SOPInstanceUID]
                z_coord = dataset.SliceLocation

                # List storing lists that contain all points for a
                # contour.
                single_array = []

                # Loop through each contour
                for j in range(len(contours[item][i][1])):
                    single_array.append([])
                    # Loop through every point in the contour
                    for point in contours[item][i][1][j]:
                        # Convert pixel coordinates to RCS points
                        rcs_pixels = ROI.pixel_to_rcs(pixlut, round(point[1]),
                                                      round(point[0]))
                        # Append RCS points to the single array
                        single_array[j].append(rcs_pixels[0])
                        single_array[j].append(rcs_pixels[1])
                        single_array[j].append(z_coord)

                # Create the ROI(s)
                for array in single_array:
                    rtss = ROI.create_roi(dataset_rtss, item, [{
                        'coords': array,
                        'ds': dataset
                    }], "")

                    # Save the updated rtss
                    patient_dict_container.set("dataset_rtss", rtss)
                    patient_dict_container.set("rois",
                                               ImageLoading.get_roi_info(rtss))
def test_merge_rtss(qtbot, test_object):
    """Test merging rtss. This function creates a new rtss, then merges
    the new rtss with the old rtss and asserts that duplicated ROIs
    will be overwritten when the other being merged.

    :param test_object: test_object function, for accessing the shared
    TestStructureTab object.
    """
    patient_dict_container = PatientDictContainer()

    # Create a new rtss
    dataset = patient_dict_container.dataset[0]
    rtss_path = Path(patient_dict_container.path).joinpath('rtss.dcm')
    new_rtss = create_initial_rtss_from_ct(
        dataset, rtss_path,
        ImageLoading.get_image_uid_list(patient_dict_container.dataset))

    # Set ROIs
    rois = ImageLoading.get_roi_info(new_rtss)
    patient_dict_container.set("rois", rois)

    # Add a new ROI into the new rtss with the name of the first ROI in
    # the old rtss
    roi_name = test_object.rois.get(1)["name"]
    roi_coordinates = [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0]
    new_rtss = create_roi(new_rtss, roi_name, [{
        'coords': roi_coordinates,
        'ds': dataset
    }])

    # Add a new ROI with a new name
    roi_name = "NewTestROI"
    new_rtss = create_roi(new_rtss, roi_name, [{
        'coords': roi_coordinates,
        'ds': dataset
    }])

    # Set ROIs
    rois = ImageLoading.get_roi_info(new_rtss)
    patient_dict_container.set("rois", rois)
    patient_dict_container.set("existing_file_rtss",
                               patient_dict_container.get("file_rtss"))
    patient_dict_container.set("dataset_rtss", new_rtss)

    # Merge the old and new rtss
    structure_tab = StructureTab()
    structure_tab.show_modified_indicator()
    qtbot.addWidget(structure_tab)

    def test_message_window():
        messagebox = structure_tab.findChild(QtWidgets.QMessageBox)
        assert messagebox is not None
        yes_button = messagebox.buttons()[1]
        qtbot.mouseClick(yes_button, QtCore.Qt.LeftButton, delay=1)

    QtCore.QTimer.singleShot(1000, test_message_window)

    structure_tab.save_new_rtss_to_fixed_image_set(auto=True)

    merged_rtss = pydicom.read_file(patient_dict_container.get("file_rtss"))
    merged_rois = ImageLoading.get_roi_info(merged_rtss)
    assert (len(test_object.rois) + 1 == len(merged_rois))
Example #21
0
class ActionHandler:
    """
    This class is responsible for initializing all of the actions that
    will be used by the MainPage and its components. There exists a
    1-to-1 relationship between this class and the MainPage. This class
    has access to the main page's attributes and components, however
    this access should only be used to provide functionality to the
    actions defined below. The instance of this class can be given to
    the main page's components in order to trigger actions.
    """
    def __init__(self, main_page):
        self.__main_page = main_page
        self.patient_dict_container = PatientDictContainer()
        self.is_four_view = False
        self.has_image_registration_single = False
        self.has_image_registration_four = False

        ##############################
        # Init all actions and icons #
        ##############################

        # Open patient
        self.icon_open = QtGui.QIcon()
        self.icon_open.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/open_patient_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_open = QtGui.QAction()
        self.action_open.setIcon(self.icon_open)
        self.action_open.setText("Open new patient")
        self.action_open.setIconVisibleInMenu(True)

        # Save RTSTRUCT changes action
        self.icon_save_structure = QtGui.QIcon()
        self.icon_save_structure.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/save_all_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_save_structure = QtGui.QAction()
        self.action_save_structure.setIcon(self.icon_save_structure)
        self.action_save_structure.setText("Save RTSTRUCT changes")
        self.action_save_structure.setIconVisibleInMenu(True)
        self.action_save_structure.triggered.connect(self.save_struct_handler)

        # Save as Anonymous Action
        self.icon_save_as_anonymous = QtGui.QIcon()
        self.icon_save_as_anonymous.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/anonlock_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_save_as_anonymous = QtGui.QAction()
        self.action_save_as_anonymous.setIcon(self.icon_save_as_anonymous)
        self.action_save_as_anonymous.setText("Save as Anonymous")
        self.action_save_as_anonymous.triggered.connect(
            self.anonymization_handler)

        # Exit action
        self.action_exit = QtGui.QAction()
        self.action_exit.setText("Exit")
        self.action_exit.triggered.connect(self.action_exit_handler)

        # Zoom Out Action
        self.icon_zoom_out = QtGui.QIcon()
        self.icon_zoom_out.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/zoom_out_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_zoom_out = QtGui.QAction()
        self.action_zoom_out.setIcon(self.icon_zoom_out)
        self.action_zoom_out.setIconVisibleInMenu(True)
        self.action_zoom_out.setText("Zoom Out")
        self.action_zoom_out.triggered.connect(self.zoom_out_handler)

        # Zoom In Action
        self.icon_zoom_in = QtGui.QIcon()
        self.icon_zoom_in.addPixmap(
            QtGui.QPixmap(
                resource_path("res/images/btn-icons/zoom_in_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_zoom_in = QtGui.QAction()
        self.action_zoom_in.setIcon(self.icon_zoom_in)
        self.action_zoom_in.setIconVisibleInMenu(True)
        self.action_zoom_in.setText("Zoom In")
        self.action_zoom_in.triggered.connect(self.zoom_in_handler)

        # Transect Action
        self.icon_transect = QtGui.QIcon()
        self.icon_transect.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/transect_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_transect = QtGui.QAction()
        self.action_transect.setIcon(self.icon_transect)
        self.action_transect.setIconVisibleInMenu(True)
        self.action_transect.setText("Transect")
        self.action_transect.triggered.connect(self.transect_handler)

        # Add-On Options Action
        self.icon_add_ons = QtGui.QIcon()
        self.icon_add_ons.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/management_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_add_ons = QtGui.QAction()
        self.action_add_ons.setIcon(self.icon_add_ons)
        self.action_add_ons.setIconVisibleInMenu(True)
        self.action_add_ons.setText("Add-On Options")
        self.action_add_ons.triggered.connect(self.add_on_options_handler)

        # Switch to Single View Action
        self.icon_one_view = QtGui.QIcon()
        self.icon_one_view.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/axial_view_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_one_view = QtGui.QAction()
        self.action_one_view.setIcon(self.icon_one_view)
        self.action_one_view.setIconVisibleInMenu(True)
        self.action_one_view.setText("One View")
        self.action_one_view.triggered.connect(self.one_view_handler)

        # Switch to 4 Views Action
        self.icon_four_views = QtGui.QIcon()
        self.icon_four_views.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/four_views_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_four_views = QtGui.QAction()
        self.action_four_views.setIcon(self.icon_four_views)
        self.action_four_views.setIconVisibleInMenu(True)
        self.action_four_views.setText("Four Views")
        self.action_four_views.triggered.connect(self.four_views_handler)

        # Show cut lines
        self.icon_cut_lines = QtGui.QIcon()
        self.icon_cut_lines.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/cut_line_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_show_cut_lines = QtGui.QAction()
        self.action_show_cut_lines.setIcon(self.icon_cut_lines)
        self.action_show_cut_lines.setIconVisibleInMenu(True)
        self.action_show_cut_lines.setText("Show Cut Lines")
        self.action_show_cut_lines.triggered.connect(self.cut_lines_handler)

        # Export Pyradiomics Action
        self.action_pyradiomics_export = QtGui.QAction()
        self.action_pyradiomics_export.setText("Export Pyradiomics")
        self.action_pyradiomics_export.triggered.connect(
            self.pyradiomics_export_handler)

        # Export DVH Action
        self.action_dvh_export = QtGui.QAction()
        self.action_dvh_export.setText("Export DVH")
        self.action_dvh_export.triggered.connect(self.export_dvh_handler)

        # Create Windowing menu
        self.icon_windowing = QtGui.QIcon()
        self.icon_windowing.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/windowing_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.menu_windowing = QtWidgets.QMenu()
        self.init_windowing_menu()

        self.windowing_window = Windowing(self)
        self.windowing_window.done_signal.connect(self.update_views)

        # Create Export menu
        self.icon_export = QtGui.QIcon()
        self.icon_export.addPixmap(
            QtGui.QPixmap(
                resource_path("res/images/btn-icons/export_purple_icon.png")),
            QtGui.QIcon.Normal,
            QtGui.QIcon.On,
        )
        self.menu_export = QtWidgets.QMenu()
        self.menu_export.setTitle("Export")
        self.menu_export.addAction(self.action_pyradiomics_export)
        self.menu_export.addAction(self.action_dvh_export)

        # Image Fusion Action
        self.icon_image_fusion = QtGui.QIcon()
        self.icon_image_fusion.addPixmap(
            QtGui.QPixmap(
                resource_path(
                    "res/images/btn-icons/image_fusion_purple_icon.png")),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.action_image_fusion = QtGui.QAction()
        self.action_image_fusion.setIcon(self.icon_image_fusion)
        self.action_image_fusion.setIconVisibleInMenu(True)
        self.action_image_fusion.setText("Image Fusion")

    def init_windowing_menu(self):
        self.menu_windowing.setIcon(self.icon_windowing)
        self.menu_windowing.setTitle("Windowing")

        dict_windowing = self.patient_dict_container.get("dict_windowing")

        # Get the right order for windowing names
        names_ordered = sorted(dict_windowing.keys())
        if "Normal" in dict_windowing.keys():
            old_index = names_ordered.index("Normal")
            names_ordered.insert(0, names_ordered.pop(old_index))

        # Create actions for each windowing item
        def generate_triggered_handler(text_):
            def handler(state):
                self.windowing_handler(state, text_)

            return handler

        windowing_actions = []
        for name in names_ordered:
            text = str(name)
            action_windowing_item = QtGui.QAction(self.menu_windowing)
            action_windowing_item.triggered.connect(
                generate_triggered_handler(text))
            action_windowing_item.setText(text)
            windowing_actions.append(action_windowing_item)

        # For reasons beyond me, the actions have to be set as a child
        # of the windowing menu *and* later be added to the menu as
        # well. You can't do one or the other, otherwise the menu won't
        # populate. Feel free to try fix (or at least explain why the
        # action has to be set as the windowing menu's child twice)
        for item in windowing_actions:
            self.menu_windowing.addAction(item)

    def save_struct_handler(self):
        """
        If there are changes to the RTSTRUCT detected,
        save the changes to disk.
        """
        if self.patient_dict_container.get("rtss_modified"):
            self.__main_page.structures_tab.save_new_rtss_to_fixed_image_set()
        else:
            QtWidgets.QMessageBox.information(
                self.__main_page, "File not saved",
                "No changes to the RTSTRUCT file detected.")

    def zoom_out_handler(self):
        self.__main_page.zoom_out(self.is_four_view,
                                  self.has_image_registration_single,
                                  self.has_image_registration_four)

    def zoom_in_handler(self):
        self.__main_page.zoom_in(self.is_four_view,
                                 self.has_image_registration_single,
                                 self.has_image_registration_four)

    def windowing_handler(self, state, text):
        """
        Function triggered when a window is selected from the menu.
        :param state: Variable not used. Present to be able to use a
            lambda function.
        :param text: The name of the window selected.
        """
        ptct = PTCTDictContainer()
        mvd = MovingDictContainer()
        if ptct.is_empty() and mvd.is_empty():
            windowing_model(text, [True, False, False, False])
            self.update_views()
        else:
            self.windowing_window.set_window(text)
            self.windowing_window.show()

    def update_views(self):
        """
        function to update all dicom views
        """
        self.__main_page.update_views(update_3d_window=True)

    def anonymization_handler(self):
        """
        Function triggered when the Anonymization button is pressed from
        the menu.
        """

        save_reply = QtWidgets.QMessageBox.information(
            self.__main_page.main_window_instance, "Confirmation",
            "Are you sure you want to perform anonymization?",
            QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)

        if save_reply == QtWidgets.QMessageBox.Yes:
            raw_dvh = self.patient_dict_container.get("raw_dvh")
            hashed_path = self.__main_page.call_class.run_anonymization(
                raw_dvh)
            self.patient_dict_container.set("hashed_path", hashed_path)
            # now that the radiomics data can just get copied across...
            # maybe skip this?
            radiomics_reply = QtWidgets.QMessageBox.information(
                self.__main_page.main_window_instance, "Confirmation",
                "Anonymization complete. Would you like to perform radiomics?",
                QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
            if radiomics_reply == QtWidgets.QMessageBox.Yes:
                self.__main_page.pyradi_trigger.emit(
                    self.patient_dict_container.path,
                    self.patient_dict_container.filepaths, hashed_path)

    def transect_handler(self):
        """
        Function triggered when the Transect button is pressed from the
        menu.
        """
        if self.is_four_view:
            view = self.__main_page.dicom_axial_view.view
            slider_id = self.__main_page.dicom_axial_view.slider.value()
        else:
            view = self.__main_page.dicom_single_view.view
            slider_id = self.__main_page.dicom_single_view.slider.value()
        dt = self.patient_dict_container.dataset[slider_id]
        row_s = dt.PixelSpacing[0]
        col_s = dt.PixelSpacing[1]
        dt.convert_pixel_data()
        pixmap = self.patient_dict_container.get("pixmaps_axial")[slider_id]
        self.__main_page.call_class.run_transect(self.__main_page, view,
                                                 pixmap,
                                                 dt._pixel_array.transpose(),
                                                 row_s, col_s)

    def add_on_options_handler(self):
        self.__main_page.add_on_options_controller.show_add_on_options()

    def one_view_handler(self):
        self.is_four_view = False

        self.__main_page.dicom_view.setCurrentWidget(
            self.__main_page.dicom_single_view)
        self.__main_page.dicom_single_view.update_view()

        if hasattr(self.__main_page, 'image_fusion_view'):
            self.has_image_registration_four = False
            self.has_image_registration_single = True
            if isinstance(self.__main_page.image_fusion_single_view,
                          ImageFusionAxialView):
                self.__main_page.image_fusion_view.setCurrentWidget(
                    self.__main_page.image_fusion_single_view)
                self.__main_page.image_fusion_single_view.update_view()

    def four_views_handler(self):
        self.is_four_view = True

        self.__main_page.dicom_view.setCurrentWidget(
            self.__main_page.dicom_four_views)
        self.__main_page.dicom_axial_view.update_view()

        if hasattr(self.__main_page, 'image_fusion_view'):
            self.has_image_registration_four = True
            self.has_image_registration_single = False
            if isinstance(self.__main_page.image_fusion_view, QStackedWidget):
                self.__main_page.image_fusion_view.setCurrentWidget(
                    self.__main_page.image_fusion_four_views)
                self.__main_page.image_fusion_view_axial.update_view()

    def cut_lines_handler(self):
        self.__main_page.toggle_cut_lines()

    def export_dvh_handler(self):
        if self.patient_dict_container.has_attribute("raw_dvh"):
            self.__main_page.dvh_tab.export_csv()
        else:
            QtWidgets.QMessageBox.information(
                self.__main_page, "Unable to export DVH",
                "DVH cannot be exported as there is no DVH present.",
                QtWidgets.QMessageBox.Ok)

    def pyradiomics_export_handler(self):
        self.__main_page.pyradi_trigger.emit(
            self.patient_dict_container.path,
            self.patient_dict_container.filepaths, '')

    def action_exit_handler(self):
        QtCore.QCoreApplication.exit(0)
Example #22
0
def create_initial_model():
    """
    This function initializes all the attributes in the PatientDictContainer model required for the operation of the
    main window. This should be called before the main window's components are constructed, but after the initial
    values of the PatientDictContainer instance are set (i.e. dataset and filepaths).
    """
    ##############################
    #  LOAD PATIENT INFORMATION  #
    ##############################
    patient_dict_container = PatientDictContainer()

    dataset = patient_dict_container.dataset
    filepaths = patient_dict_container.filepaths
    patient_dict_container.set("rtss_modified", False)

    if ('WindowWidth' in dataset[0]):
        if isinstance(dataset[0].WindowWidth, pydicom.valuerep.DSfloat):
            window = int(dataset[0].WindowWidth)
        elif isinstance(dataset[0].WindowWidth, pydicom.multival.MultiValue):
            window = int(dataset[0].WindowWidth[1])
    else:
        window = int(400)

    if ('WindowCenter' in dataset[0]):
        if isinstance(dataset[0].WindowCenter, pydicom.valuerep.DSfloat):
            level = int(dataset[0].WindowCenter)
        elif isinstance(dataset[0].WindowCenter, pydicom.multival.MultiValue):
            level = int(dataset[0].WindowCenter[1])
    else:
        level = int(800)

    patient_dict_container.set("window", window)
    patient_dict_container.set("level", level)

    # Check to see if the imageWindowing.csv file exists
    if os.path.exists(resource_path('data/csv/imageWindowing.csv')):
        # If it exists, read data from file into the self.dict_windowing variable
        dict_windowing = {}
        with open(resource_path('data/csv/imageWindowing.csv'),
                  "r") as fileInput:
            next(fileInput)
            dict_windowing["Normal"] = [window, level]
            for row in fileInput:
                # Format: Organ - Scan - Window - Level
                items = [item for item in row.split(',')]
                dict_windowing[items[0]] = [int(items[2]), int(items[3])]
    else:
        # If csv does not exist, initialize dictionary with default values
        dict_windowing = {
            "Normal": [window, level],
            "Lung": [1600, -300],
            "Bone": [1400, 700],
            "Brain": [160, 950],
            "Soft Tissue": [400, 800],
            "Head and Neck": [275, 900]
        }

    patient_dict_container.set("dict_windowing", dict_windowing)

    pixel_values = convert_raw_data(dataset)
    pixmaps = get_pixmaps(pixel_values, window, level)
    patient_dict_container.set("pixmaps", pixmaps)
    patient_dict_container.set("pixel_values", pixel_values)

    basic_info = get_basic_info(dataset[0])
    patient_dict_container.set("basic_info", basic_info)

    patient_dict_container.set("dict_uid", dict_instanceUID(dataset))

    # Set RTSS attributes
    if patient_dict_container.has_modality("rtss"):
        patient_dict_container.set("file_rtss", filepaths['rtss'])
        patient_dict_container.set("dataset_rtss", dataset['rtss'])

        dicom_tree_rtss = DicomTree(filepaths['rtss'])
        patient_dict_container.set("dict_dicom_tree_rtss",
                                   dicom_tree_rtss.dict)

        patient_dict_container.set(
            "list_roi_numbers",
            ordered_list_rois(patient_dict_container.get("rois")))
        patient_dict_container.set("selected_rois", [])

        patient_dict_container.set("dict_polygons", {})

    # Set RTDOSE attributes
    if patient_dict_container.has_modality("rtdose"):
        dicom_tree_rtdose = DicomTree(filepaths['rtdose'])
        patient_dict_container.set("dict_dicom_tree_rtdose",
                                   dicom_tree_rtdose.dict)

        patient_dict_container.set("dose_pixluts", get_dose_pixluts(dataset))

        patient_dict_container.set("selected_doses", [])
        patient_dict_container.set(
            "rx_dose_in_cgray",
            1)  # This will be overwritten if an RTPLAN is present.

    # Set RTPLAN attributes
    if patient_dict_container.has_modality("rtplan"):
        # the TargetPrescriptionDose is type 3 (optional), so it may not be there
        # However, it is preferable to the sum of the beam doses
        # DoseReferenceStructureType is type 1 (value is mandatory),
        # but it can have a value of ORGAN_AT_RISK rather than TARGET
        # in which case there will *not* be a TargetPrescriptionDose
        # and even if it is TARGET, that's no guarantee that TargetPrescriptionDose
        # will be encoded and have a value
        rx_dose_in_cgray = calculate_rx_dose_in_cgray(dataset["rtplan"])
        patient_dict_container.set("rx_dose_in_cgray", rx_dose_in_cgray)

        dicom_tree_rtplan = DicomTree(filepaths['rtplan'])
        patient_dict_container.set("dict_dicom_tree_rtplan",
                                   dicom_tree_rtplan.dict)
Example #23
0
    def generate_roi(self, contours, progress_callback):
        """
        Generates new ROIs based on contour data.
        :param contours: dictionary of contours to turn into ROIs.
        :param progress_callback: signal to update loading progress
        """
        # Initialise variables needed for function
        patient_dict_container = PatientDictContainer()
        dataset_rtss = patient_dict_container.get("dataset_rtss")
        pixmaps = patient_dict_container.get("pixmaps_axial")
        slider_min = 0
        slider_max = len(pixmaps) - 1

        # Get existing ROIs
        existing_rois = []
        rois = patient_dict_container.get("dataset_rtss")
        if rois:
            for roi in rois.StructureSetROISequence:
                existing_rois.append(roi.ROIName)

        # Loop through each isodose level
        for item in contours:
            # Delete ROI if it already exists to recreate it
            if item in existing_rois:
                dataset_rtss = ROI.delete_roi(dataset_rtss, item)

                # Update patient dict container
                current_rois = patient_dict_container.get("rois")
                keys = []
                for key, value in current_rois.items():
                    if value["name"] == item:
                        keys.append(key)
                for key in keys:
                    del current_rois[key]
                patient_dict_container.set("rois", current_rois)

            # Calculate isodose ROI for each slice, skip if slice has no
            # contour data
            for i in range(slider_min, slider_max):
                if not len(contours[item][i]):
                    continue

                # Get required data for calculating ROI
                dataset = patient_dict_container.dataset[i]
                pixlut = patient_dict_container.get("pixluts")
                pixlut = pixlut[dataset.SOPInstanceUID]
                z_coord = dataset.SliceLocation
                curr_slice_uid = patient_dict_container.get("dict_uid")[i]
                dose_pixluts = patient_dict_container.get("dose_pixluts")
                dose_pixluts = dose_pixluts[curr_slice_uid]

                # Loop through each contour for each slice.
                # Convert the pixel points to RCS points, append z value
                single_array = []

                # Loop through each contour
                for j in range(len(contours[item][i])):
                    single_array.append([])
                    # Loop through every second point in the contour
                    for point in contours[item][i][j][::2]:
                        # Transform into dose pixel
                        dose_pixels = [
                            dose_pixluts[0][int(point[1])],
                            dose_pixluts[1][int(point[0])]
                        ]
                        # Transform into RCS pixel
                        rcs_pixels = ROI.pixel_to_rcs(pixlut,
                                                      round(dose_pixels[0]),
                                                      round(dose_pixels[1]))
                        # Append point coordinates to the single array
                        single_array[j].append(rcs_pixels[0])
                        single_array[j].append(rcs_pixels[1])
                        single_array[j].append(z_coord)

                # Create the ROI(s)
                for array in single_array:
                    rtss = ROI.create_roi(dataset_rtss, item, [{
                        'coords': array,
                        'ds': dataset
                    }], "DOSE_REGION")

                    # Save the updated rtss
                    patient_dict_container.set("dataset_rtss", rtss)
                    patient_dict_container.set("rois",
                                               ImageLoading.get_roi_info(rtss))

        progress_callback.emit(("Writing to RT Structure Set", 85))
Example #24
0
class DicomView(QtWidgets.QWidget):
    def __init__(self, roi_color=None, iso_color=None):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.iso_color = iso_color
        self.zoom = 1
        self.current_slice_number = None

        self.dicom_view_layout = QtWidgets.QHBoxLayout()

        # Create components
        self.slider = QtWidgets.QSlider(QtCore.Qt.Vertical)
        self.init_slider()
        self.view = QtWidgets.QGraphicsView()
        self.init_view()
        self.scene = QtWidgets.QGraphicsScene()

        # Set layout
        self.dicom_view_layout.addWidget(self.view)
        self.dicom_view_layout.addWidget(self.slider)
        self.setLayout(self.dicom_view_layout)

        # Init metadata widgets
        self.metadata_layout = QtWidgets.QVBoxLayout(self.view)
        self.label_image_id = QtWidgets.QLabel()
        self.label_image_pos = QtWidgets.QLabel()
        self.label_wl = QtWidgets.QLabel()
        self.label_image_size = QtWidgets.QLabel()
        self.label_zoom = QtWidgets.QLabel()
        self.label_patient_pos = QtWidgets.QLabel()
        self.init_metadata()

        self.update_view()

    def init_slider(self):
        """
        Create a slider for the DICOM Image View.
        """
        pixmaps = self.patient_dict_container.get("pixmaps")
        self.slider.setMinimum(0)
        self.slider.setMaximum(len(pixmaps) - 1)
        self.slider.setValue(int(len(pixmaps) / 2))
        self.slider.setTickPosition(QtWidgets.QSlider.TicksLeft)
        self.slider.setTickInterval(1)
        self.slider.valueChanged.connect(self.value_changed)

    def init_view(self):
        """
        Create a view widget for DICOM image.
        """
        self.view.setRenderHints(QtGui.QPainter.Antialiasing
                                 | QtGui.QPainter.SmoothPixmapTransform)
        background_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0),
                                        QtCore.Qt.SolidPattern)
        self.view.setBackgroundBrush(background_brush)
        self.view.setGeometry(QtCore.QRect(0, 0, 877, 517))

    def init_metadata(self):
        """
        Create and place metadata on the view widget.
        """
        # Position of the labels on the DICOM view.
        self.label_image_id.setAlignment(QtCore.Qt.AlignTop)
        self.label_image_pos.setAlignment(QtCore.Qt.AlignTop)
        self.label_wl.setAlignment(QtCore.Qt.AlignRight)
        self.label_image_size.setAlignment(QtCore.Qt.AlignBottom)
        self.label_zoom.setAlignment(QtCore.Qt.AlignBottom)
        self.label_patient_pos.setAlignment(QtCore.Qt.AlignRight)

        # Set all labels to white
        stylesheet = "QLabel { color : white; }"
        self.label_image_id.setStyleSheet(stylesheet)
        self.label_image_pos.setStyleSheet(stylesheet)
        self.label_wl.setStyleSheet(stylesheet)
        self.label_image_size.setStyleSheet(stylesheet)
        self.label_zoom.setStyleSheet(stylesheet)
        self.label_patient_pos.setStyleSheet(stylesheet)

        # The following layout was originally accomplished using a QGridLayout with QSpaceItems to anchor the labels
        # to the corners of the DICOM view. This caused a reintroduction of the tedious memory issues that were fixed
        # with the restructure. The following was rewritten to not use QSpaceItems because they, for reasons unknown,
        # caused a memory leak resulting in the entire patient dictionary not being cleared from memory correctly,
        # leaving hundreds of additional megabytes unused in memory each time a new patient was opened.

        # Create a widget to contain the two top-left labels
        top_left_widget = QtWidgets.QWidget()
        top_left = QtWidgets.QVBoxLayout(top_left_widget)
        top_left.addWidget(self.label_image_id, QtCore.Qt.AlignTop)
        top_left.addWidget(self.label_image_pos, QtCore.Qt.AlignTop)

        # Create a widget to contain the top-right label
        top_right_widget = QtWidgets.QWidget()
        top_right = QtWidgets.QVBoxLayout(top_right_widget)
        top_right.addWidget(self.label_wl, QtCore.Qt.AlignTop)

        # Create a widget to contain the two top widgets
        top_widget = QtWidgets.QWidget()
        top_widget.setFixedHeight(100)
        top = QtWidgets.QHBoxLayout(top_widget)
        top.addWidget(top_left_widget, QtCore.Qt.AlignLeft)
        top.addWidget(top_right_widget, QtCore.Qt.AlignRight)

        # Create a widget to contain the two bottom-left labels
        bottom_left_widget = QtWidgets.QWidget()
        bottom_left = QtWidgets.QVBoxLayout(bottom_left_widget)
        bottom_left.addWidget(self.label_image_size, QtCore.Qt.AlignBottom)
        bottom_left.addWidget(self.label_zoom, QtCore.Qt.AlignBottom)

        # Create a widget to contain the bottom-right label
        bottom_right_widget = QtWidgets.QWidget()
        bottom_right = QtWidgets.QVBoxLayout(bottom_right_widget)
        bottom_right.addWidget(self.label_patient_pos, QtCore.Qt.AlignBottom)

        # Create a widget to contain the two bottom widgets
        bottom_widget = QtWidgets.QWidget()
        bottom_widget.setFixedHeight(100)
        bottom = QtWidgets.QHBoxLayout(bottom_widget)
        bottom.addWidget(bottom_left_widget, QtCore.Qt.AlignLeft)
        bottom.addWidget(bottom_right_widget, QtCore.Qt.AlignRight)

        # Add the bottom and top widgets to the view
        self.metadata_layout.addWidget(top_widget, QtCore.Qt.AlignTop)
        self.metadata_layout.addStretch()
        self.metadata_layout.addWidget(bottom_widget, QtCore.Qt.AlignBottom)

    def value_changed(self):
        self.update_view()

    def update_view(self, zoom_change=False):
        """
        Update the view of the DICOM Image.
        :param zoom_change: Boolean indicating whether the user wants to change the zoom. False by default.
        """
        self.image_display()

        if zoom_change:
            self.view.setTransform(QtGui.QTransform().scale(
                self.zoom, self.zoom))

        if self.patient_dict_container.get("selected_rois"):
            self.roi_display()

        if self.patient_dict_container.get("selected_doses"):
            self.isodose_display()

        self.update_metadata()
        self.view.setScene(self.scene)

    def image_display(self):
        """
        Update the image to be displayed on the DICOM View.
        """
        pixmaps = self.patient_dict_container.get("pixmaps")
        slider_id = self.slider.value()
        image = pixmaps[slider_id]
        label = QtWidgets.QGraphicsPixmapItem(image)
        self.scene = QtWidgets.QGraphicsScene()
        self.scene.addItem(label)

    def roi_display(self):
        """
        Display ROI structures on the DICOM Image.
        """
        slider_id = self.slider.value()
        curr_slice = self.patient_dict_container.get("dict_uid")[slider_id]

        selected_rois = self.patient_dict_container.get("selected_rois")
        rois = self.patient_dict_container.get("rois")
        selected_rois_name = []
        for roi in selected_rois:
            selected_rois_name.append(rois[roi]['name'])

        for roi in selected_rois:
            roi_name = rois[roi]['name']

            if roi_name not in self.patient_dict_container.get(
                    "dict_polygons").keys():
                new_dict_polygons = self.patient_dict_container.get(
                    "dict_polygons")
                new_dict_polygons[roi_name] = {}
                dict_rois_contours = get_contour_pixel(
                    self.patient_dict_container.get("raw_contour"),
                    selected_rois_name,
                    self.patient_dict_container.get("pixluts"), curr_slice)
                polygons = self.calc_roi_polygon(roi_name, curr_slice,
                                                 dict_rois_contours)
                new_dict_polygons[roi_name][curr_slice] = polygons
                self.patient_dict_container.set("dict_polygons",
                                                new_dict_polygons)

            elif curr_slice not in self.patient_dict_container.get(
                    "dict_polygons")[roi_name].keys():
                new_dict_polygons = self.patient_dict_container.get(
                    "dict_polygons")
                dict_rois_contours = get_contour_pixel(
                    self.patient_dict_container.get("raw_contour"),
                    selected_rois_name,
                    self.patient_dict_container.get("pixluts"), curr_slice)
                polygons = self.calc_roi_polygon(roi_name, curr_slice,
                                                 dict_rois_contours)
                new_dict_polygons[roi_name][curr_slice] = polygons
                self.patient_dict_container.set("dict_polygons",
                                                new_dict_polygons)

            else:
                polygons = self.patient_dict_container.get(
                    "dict_polygons")[roi_name][curr_slice]

            color = self.patient_dict_container.get("roi_color_dict")[roi]
            with open(resource_path('src/data/line&fill_configuration'),
                      'r') as stream:
                elements = stream.readlines()
                if len(elements) > 0:
                    roi_line = int(elements[0].replace('\n', ''))
                    roi_opacity = int(elements[1].replace('\n', ''))
                    line_width = float(elements[4].replace('\n', ''))
                else:
                    roi_line = 1
                    roi_opacity = 10
                    line_width = 2.0
                stream.close()
            roi_opacity = int((roi_opacity / 100) * 255)
            color.setAlpha(roi_opacity)
            pen = self.get_qpen(color, roi_line, line_width)
            for i in range(len(polygons)):
                self.scene.addPolygon(polygons[i], pen, QtGui.QBrush(color))

    def isodose_display(self):
        """
        Display isodoses on the DICOM Image.
        """
        slider_id = self.slider.value()
        curr_slice_uid = self.patient_dict_container.get("dict_uid")[slider_id]
        z = self.patient_dict_container.dataset[
            slider_id].ImagePositionPatient[2]
        dataset_rtdose = self.patient_dict_container.dataset['rtdose']
        grid = get_dose_grid(dataset_rtdose, float(z))

        if not (grid == []):
            x, y = np.meshgrid(np.arange(grid.shape[1]),
                               np.arange(grid.shape[0]))

            # Instantiate the isodose generator for this slice
            isodosegen = cntr.Cntr(x, y, grid)

            # sort selected_doses in ascending order so that the high dose isodose washes
            # paint over the lower dose isodose washes
            for sd in sorted(
                    self.patient_dict_container.get("selected_doses")):
                dose_level = sd * self.patient_dict_container.get("rx_dose_in_cgray") / \
                             (dataset_rtdose.DoseGridScaling * 10000)
                contours = isodosegen.trace(dose_level)
                contours = contours[:len(contours) // 2]

                polygons = self.calc_dose_polygon(
                    self.patient_dict_container.get("dose_pixluts")
                    [curr_slice_uid], contours)

                brush_color = self.iso_color[sd]
                with open(resource_path('src/data/line&fill_configuration'),
                          'r') as stream:
                    elements = stream.readlines()
                    if len(elements) > 0:
                        iso_line = int(elements[2].replace('\n', ''))
                        iso_opacity = int(elements[3].replace('\n', ''))
                        line_width = float(elements[4].replace('\n', ''))
                    else:
                        iso_line = 2
                        iso_opacity = 5
                        line_width = 2.0
                    stream.close()
                iso_opacity = int((iso_opacity / 100) * 255)
                brush_color.setAlpha(iso_opacity)
                pen_color = QtGui.QColor(brush_color.red(),
                                         brush_color.green(),
                                         brush_color.blue())
                pen = self.get_qpen(pen_color, iso_line, line_width)
                for i in range(len(polygons)):
                    self.scene.addPolygon(polygons[i], pen,
                                          QtGui.QBrush(brush_color))

    def update_metadata(self):
        """
        Update metadata displayed on the DICOM Image view.
        """
        # Retrieve dictionary from the dataset of the slice
        id = self.slider.value()
        dataset = self.patient_dict_container.dataset[id]

        # Information to display
        self.current_slice_number = dataset['InstanceNumber'].value
        total_slices = len(self.patient_dict_container.get("pixmaps"))
        row_img = dataset['Rows'].value
        col_img = dataset['Columns'].value
        patient_pos = dataset['PatientPosition'].value
        window = self.patient_dict_container.get("window")
        level = self.patient_dict_container.get("level")
        slice_pos = dataset['SliceLocation'].value

        # Update labels
        self.label_image_id.setText(
            "Image: %s / %s" %
            (str(self.current_slice_number), str(total_slices)))
        self.label_image_pos.setText("Position: %s mm" % (str(slice_pos)))
        self.label_wl.setText("W/L: %s/%s" % (str(window), str(level)))
        self.label_image_size.setText("Image Size: %sx%spx" %
                                      (str(row_img), str(col_img)))
        self.label_zoom.setText("Zoom: " + "{:.2f}".format(self.zoom * 100) +
                                "%")
        self.label_patient_pos.setText("Patient Position: %s" %
                                       (str(patient_pos)))

    def calc_roi_polygon(self, curr_roi, curr_slice, dict_rois_contours):
        """
        Calculate a list of polygons to display for a given ROI and a given slice.
        :param curr_roi:
         the ROI structure
        :param curr_slice:
         the current slice
        :return: List of polygons of type QPolygonF.
        """
        # TODO Implement support for showing "holes" in contours.
        # Possible process for this is:
        # 1. Calculate the areas of each contour on the slice
        # https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates
        # 2. Compare each contour to the largest contour by area to determine if it is contained entirely within the
        # largest contour.
        # https://stackoverflow.com/questions/4833802/check-if-polygon-is-inside-a-polygon
        # 3. If the polygon is contained, use QPolygonF.subtracted(QPolygonF) to subtract the smaller "hole" polygon
        # from the largest polygon, and then remove the polygon from the list of polygons to be displayed.
        # This process should provide fast and reliable results, however it should be noted that this method may fall
        # apart in a situation where there are multiple "large" polygons, each with their own hole in it. An appropriate
        # solution to that may be to compare every contour against one another and determine which ones have holes
        # encompassed entirely by them, and then subtract each hole from the larger polygon and delete the smaller
        # holes. This second solution would definitely lead to more accurate representation of contours, but could
        # possibly be too slow to be viable.

        list_polygons = []
        pixel_list = dict_rois_contours[curr_roi][curr_slice]
        for i in range(len(pixel_list)):
            list_qpoints = []
            contour = pixel_list[i]
            for point in contour:
                curr_qpoint = QtCore.QPoint(point[0], point[1])
                list_qpoints.append(curr_qpoint)
            curr_polygon = QtGui.QPolygonF(list_qpoints)
            list_polygons.append(curr_polygon)
        return list_polygons

    def calc_dose_polygon(self, dose_pixluts, contours):
        """
        Calculate a list of polygons to display for a given isodose.
        :param dose_pixluts:
         lookup table (LUT) to get the image pixel values
        :param contours:
          trace outline of the isodose to be displayed
        :return: List of polygons of type QPolygonF.
        """
        list_polygons = []
        for contour in contours:
            list_qpoints = []
            # Slicing controls how many points considered for visualization
            # Essentially effects sharpness of edges, fewer points equals "smoother" edges
            for point in contour[::2]:
                curr_qpoint = QtCore.QPoint(dose_pixluts[0][int(point[0])],
                                            dose_pixluts[1][int(point[1])])
                list_qpoints.append(curr_qpoint)
            curr_polygon = QtGui.QPolygonF(list_qpoints)
            list_polygons.append(curr_polygon)
        return list_polygons

    def get_qpen(self, color, style=1, widthF=1):
        """
        The color and style for ROI structure and isodose display.
        :param color:
         Color of the region. QColor type.
        :param style:
         Style of the contour line. NoPen: 0  SolidLine: 1  DashLine: 2  DotLine: 3  DashDotLine: 4  DashDotDotLine: 5
        :param widthF:
         Width of the contour line.
        :return: QPen object.
        """
        pen = QtGui.QPen(color)
        pen.setStyle(style)
        pen.setWidthF(widthF)
        return pen

    def zoom_in(self):
        self.zoom *= 1.05
        self.update_view(zoom_change=True)

    def zoom_out(self):
        self.zoom /= 1.05
        self.update_view(zoom_change=True)
Example #25
0
class DVHTab(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.dvh_calculated = self.patient_dict_container.has_attribute(
            "raw_dvh")

        self.raw_dvh = None
        self.dvh_x_y = None
        self.plot = None

        self.selected_rois = self.patient_dict_container.get("selected_rois")

        self.dvh_tab_layout = QtWidgets.QVBoxLayout()

        # Construct the layout based on whether or not the DVH has already been calculated.
        if self.dvh_calculated:
            self.init_layout_dvh()
        else:
            self.init_layout_no_dvh()

        self.setLayout(self.dvh_tab_layout)

    def init_layout_dvh(self):
        self.raw_dvh = self.patient_dict_container.get("raw_dvh")
        self.dvh_x_y = self.patient_dict_container.get("dvh_x_y")

        self.plot = self.plot_dvh()
        widget_plot = FigureCanvas(self.plot)

        button_export = QtWidgets.QPushButton("Export DVH")
        button_export.clicked.connect(self.export_csv)

        self.dvh_tab_layout.setAlignment(QtCore.Qt.Alignment())
        self.dvh_tab_layout.addWidget(widget_plot)
        self.dvh_tab_layout.addWidget(button_export,
                                      alignment=QtCore.Qt.AlignRight)

    def init_layout_no_dvh(self):
        button_calc_dvh = QtWidgets.QPushButton("Calculate DVH")
        button_calc_dvh.clicked.connect(self.prompt_calc_dvh)

        self.dvh_tab_layout.setAlignment(QtCore.Qt.AlignCenter)
        self.dvh_tab_layout.addWidget(button_calc_dvh)

    def clear_layout(self):
        for i in reversed(range(self.dvh_tab_layout.count())):
            self.dvh_tab_layout.itemAt(i).widget().setParent(None)

    def plot_dvh(self):
        """
        :return: DVH plot using Matplotlib library.
        """

        # Initialisation of the plots
        fig, ax = plt.subplots()
        fig.subplots_adjust(0.1, 0.15, 1, 1)
        # Maximum value for x axis
        max_xlim = 0

        # Plot for all the ROIs selected in the left column of the window
        for roi in self.selected_rois:
            dvh = self.raw_dvh[int(roi)]

            # Plot only the ROIs whose volume is non equal to 0
            if dvh.volume != 0:
                # Bincenters, obtained from the dvh object, give the x axis values
                # (Doses originally in Gy unit)
                bincenters = self.dvh_x_y[roi]['bincenters']
                #print(self.dvh_x_y[roi])

                # Counts, obtained from the dvh object, give the y axis values
                # (values between 0 and dvh.volume)
                counts = self.dvh_x_y[roi]['counts']

                # Color of the line is the same as the color shown in the left column of the window
                color = self.patient_dict_container.get("roi_color_dict")[roi]
                color_R = color.red() / 255
                color_G = color.green() / 255
                color_B = color.blue() / 255

                plt.plot(100 * bincenters,
                         100 * counts / dvh.volume,
                         label=dvh.name,
                         color=[color_R, color_G, color_B])

                # Update the maximum value for x axis (usually different between ROIs)
                if (100 * bincenters[-1]) > max_xlim:
                    max_xlim = 100 * bincenters[-1]

                plt.xlabel('Dose [%s]' % 'cGy')
                plt.ylabel('Volume [%s]' % '%')
                if dvh.name:
                    plt.legend(loc='lower center', bbox_to_anchor=(0, 1, 5, 5))

        # Set the range values for x and y axis
        ax.set_ylim([0, 105])
        ax.set_xlim([0, max_xlim + 3])

        # Create the grids on the plot
        major_ticks_y = np.arange(0, 105, 20)
        minor_ticks_y = np.arange(0, 105, 5)
        major_ticks_x = np.arange(0, max_xlim + 250, 1000)
        minor_ticks_x = np.arange(0, max_xlim + 250, 250)
        ax.set_xticks(major_ticks_x)
        ax.set_xticks(minor_ticks_x, minor=True)
        ax.set_yticks(major_ticks_y)
        ax.set_yticks(minor_ticks_y, minor=True)
        ax.grid(which='minor', alpha=0.2)
        ax.grid(which='major', alpha=0.5)

        # Add the legend at the bottom left of the graph
        if len(self.selected_rois) != 0:
            ax.legend(loc='upper left', bbox_to_anchor=(-0.1, -0.15), ncol=4)

        plt.subplots_adjust(bottom=0.3)

        return fig

    def prompt_calc_dvh(self):
        choice = QtWidgets.QMessageBox.question(
            self, "Calculate DVHs?",
            "Would you like to calculate DVHs? This may"
            " take up to several minutes on some systems.",
            QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)

        if choice == QtWidgets.QMessageBox.Yes:
            progress_window = CalculateDVHProgressWindow(
                self,
                QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
            progress_window.signal_dvh_calculated.connect(
                self.dvh_calculation_finished)
            progress_window.exec_()

    def dvh_calculation_finished(self):
        # Clear the screen
        self.clear_layout()
        self.dvh_calculated = True
        self.init_layout_dvh()

    def update_plot(self):
        if self.dvh_calculated:
            # Get new list of selected rois that have DVHs calculated
            self.selected_rois = [
                roi for roi in self.patient_dict_container.get("selected_rois")
                if roi in self.raw_dvh.keys()
            ]

            # Clear the current layout
            self.clear_layout()

            # If the DVH has become outdated, show the user an indicator advising them such.
            if self.patient_dict_container.get("dvh_outdated"):
                self.display_outdated_indicator()

            # Re-draw the plot and add to layout
            self.init_layout_dvh()

    def export_csv(self):
        path = self.patient_dict_container.path
        basic_info = self.patient_dict_container.get("basic_info")
        if not os.path.isdir(path + '/CSV'):
            os.mkdir(path + '/CSV')
        dvh2csv(self.raw_dvh, path + "/CSV/", 'DVH_' + basic_info['id'],
                basic_info['id'])
        save_reply = QtWidgets.QMessageBox.information(
            self, "Message",
            "The DVH Data was saved successfully in your directory!",
            QtWidgets.QMessageBox.Ok)

    def display_outdated_indicator(self):
        self.modified_indicator_widget = QtWidgets.QWidget()
        self.modified_indicator_widget.setContentsMargins(8, 5, 8, 5)
        #self.modified_indicator_widget.setFixedHeight(35)
        modified_indicator_layout = QtWidgets.QHBoxLayout()
        modified_indicator_layout.setAlignment(QtCore.Qt.AlignLeft)

        modified_indicator_icon = QtWidgets.QLabel()
        modified_indicator_icon.setPixmap(
            QtGui.QPixmap(
                resource_path("src/res/images/btn-icons/alert_icon.png")))
        modified_indicator_layout.addWidget(modified_indicator_icon)

        modified_indicator_text = QtWidgets.QLabel(
            "Contours have been modified since DVH calculation. Some DVHs may "
            "now be out of date.")
        modified_indicator_text.setStyleSheet("color: red")
        modified_indicator_layout.addWidget(modified_indicator_text)

        self.modified_indicator_widget.setLayout(modified_indicator_layout)

        self.dvh_tab_layout.addWidget(self.modified_indicator_widget,
                                      QtCore.Qt.AlignTop)
Example #26
0
class DicomView(QtWidgets.QWidget):

    def __init__(self, roi_color=None, iso_color=None, cut_line_color=None):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.iso_color = iso_color
        self.roi_color = roi_color
        self.zoom = INITIAL_ONE_VIEW_ZOOM
        self.current_slice_number = None
        self.horizontal_view = None
        self.vertical_view = None
        self.cut_lines_color = cut_line_color
        self.dicom_view_layout = QtWidgets.QHBoxLayout()

        # Create components
        self.slider = QtWidgets.QSlider(QtCore.Qt.Vertical)
        self.init_slider()
        self.view = QtWidgets.QGraphicsView()
        self.init_view()
        self.scene = QtWidgets.QGraphicsScene()

        # Set layout
        self.dicom_view_layout.addWidget(self.view)
        self.dicom_view_layout.addWidget(self.slider)
        self.setLayout(self.dicom_view_layout)

    def init_slider(self):
        """
        Create a slider for the DICOM Image View.
        """
        pixmaps = self.patient_dict_container.get("pixmaps_" + self.slice_view)
        self.slider.setMinimum(0)
        self.slider.setMaximum(len(pixmaps) - 1)
        self.slider.setValue(int(len(pixmaps) / 2))
        self.slider.setTickPosition(QtWidgets.QSlider.TicksLeft)
        self.slider.setTickInterval(1)
        self.slider.valueChanged.connect(self.value_changed)

    def init_view(self):
        """
        Create a view widget for DICOM image.
        """
        self.view.setRenderHints(
            QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
        background_brush = QtGui.QBrush(
            QtGui.QColor(0, 0, 0), QtCore.Qt.SolidPattern)
        self.view.setBackgroundBrush(background_brush)

    def value_changed(self):
        self.update_view()
        if self.horizontal_view is not None and self.vertical_view is not None:
            self.horizontal_view.update_view()
            self.vertical_view.update_view()

    def update_view(self, zoom_change=False):
        """
        Update the view of the DICOM Image.
        :param zoom_change: Boolean indicating whether the user wants to change the zoom. False by default.
        """
        self.image_display()
        # Update roi colours if they are not explicitly set to None
        if self.roi_color is not None:
            self.roi_color = self.patient_dict_container.get("roi_color_dict")

        # If roi colours are set and rois are selected then update the display
        if self.roi_color and self.patient_dict_container.get("selected_rois"):
            self.roi_display()

        # If isodose colours are set and doses are selected then update the display
        if self.iso_color and self.patient_dict_container.get("selected_doses"):
            self.isodose_display()

        if zoom_change:
            self.view.setTransform(
                QtGui.QTransform().scale(self.zoom, self.zoom))

        self.view.setScene(self.scene)

    def image_display(self):
        """
        Update the image to be displayed on the DICOM View.
        """
        pixmaps = self.patient_dict_container.get("pixmaps_" + self.slice_view)
        slider_id = self.slider.value()
        image = pixmaps[slider_id]
        label = QtWidgets.QGraphicsPixmapItem(image)
        self.scene = GraphicsScene(
            label, self.horizontal_view, self.vertical_view)

    def draw_roi_polygons(self, roi_id, polygons, roi_color=None):
        """
        Draw ROI polygons on the image slice
        :param roi_id: ROI number
        :param polygons: List of ROI polygons
        :param roi_color: colors for ROIs used when displaying selected rois in
        manipulate ROI window
        """
        if roi_color is None:
            color = self.roi_color[roi_id]
        else:
            color = roi_color[roi_id]
        with open(data_path('line&fill_configuration'), 'r') as stream:
            elements = stream.readlines()
            if len(elements) > 0:
                roi_line = int(elements[0].replace('\n', ''))
                roi_opacity = int(elements[1].replace('\n', ''))
                line_width = float(elements[4].replace('\n', ''))
            else:
                roi_line = 1
                roi_opacity = 10
                line_width = 2.0
            stream.close()
        roi_opacity = int((roi_opacity / 100) * 255)
        color.setAlpha(roi_opacity)
        pen_color = QtGui.QColor(color.red(), color.green(), color.blue())
        pen = self.get_qpen(pen_color, roi_line, line_width)
        for i in range(len(polygons)):
            self.scene.addPolygon(polygons[i], pen, QtGui.QBrush(color))

    def get_qpen(self, color, style=1, widthF=1.):
        """
        The color and style for ROI structure and isodose display.
        :param color:
         Color of the region. QColor type.
        :param style:
         Style of the contour line. NoPen: 0  SolidLine: 1  DashLine: 2  DotLine: 3  DashDotLine: 4  DashDotDotLine: 5
        :param widthF:
         Width of the contour line.
        :return: QPen object.
        """
        pen = QtGui.QPen(color)
        pen.setStyle(QtCore.Qt.PenStyle(style))
        pen.setWidthF(widthF)
        return pen

    def zoom_in(self):
        self.zoom *= 1.05
        self.update_view(zoom_change=True)

    def zoom_out(self):
        self.zoom /= 1.05
        self.update_view(zoom_change=True)

    def set_views(self, horizontal_view, vertical_view):
        """
        Set the views represented by the horizontal and vertical cut lines respectively
        """
        self.horizontal_view = horizontal_view
        self.vertical_view = vertical_view
        self.update_view()

    def set_slider_value(self, value):
        """
        Set the value of the slider of this view
        """
        self.slider.setValue(value*self.slider.maximum())
        self.update_view()
Example #27
0
class DVHTab(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.dvh_calculated = self.patient_dict_container.has_attribute(
            "raw_dvh")
        self.rt_dose = self.patient_dict_container.dataset['rtdose']

        self.raw_dvh = None
        self.dvh_x_y = None
        self.plot = None

        self.selected_rois = self.patient_dict_container.get("selected_rois")

        self.dvh_tab_layout = QtWidgets.QVBoxLayout()

        try:
            # Import the DVH from RT Dose
            self.import_rtdose()
        except (AttributeError, KeyError):
            # Construct the layout based on whether or not the DVH has
            # already been calculated.
            # TODO: convert to logging
            print("DVH data not in RT Dose.")
            if self.dvh_calculated:
                self.init_layout_dvh()
            else:
                self.init_layout_no_dvh()

        self.setLayout(self.dvh_tab_layout)

    def init_layout_dvh(self):
        """
        Initialise the DVH tab's layout when DVH data exists.
        """
        self.raw_dvh = self.patient_dict_container.get("raw_dvh")
        self.dvh_x_y = self.patient_dict_container.get("dvh_x_y")

        self.plot = self.plot_dvh()
        widget_plot = FigureCanvas(self.plot)

        button_layout = QtWidgets.QHBoxLayout()

        button_export = QtWidgets.QPushButton("Export DVH to CSV")
        button_export.clicked.connect(self.export_csv)
        button_layout.addWidget(button_export)

        # Added Recalculate button
        button_calc_dvh = QtWidgets.QPushButton("Recalculate DVH")
        button_calc_dvh.clicked.connect(self.prompt_calc_dvh)
        button_layout.addWidget(button_calc_dvh)

        self.dvh_tab_layout.setAlignment(QtCore.Qt.Alignment())
        self.dvh_tab_layout.addWidget(widget_plot)
        self.dvh_tab_layout.addLayout(button_layout)

    def init_layout_no_dvh(self):
        """
        Initialise the DVH tab's layout when DVH data does not exist.
        """
        button_calc_dvh = QtWidgets.QPushButton("Calculate DVH")
        button_calc_dvh.clicked.connect(self.prompt_calc_dvh)

        self.dvh_tab_layout.setAlignment(QtCore.Qt.AlignCenter
                                         | QtCore.Qt.AlignCenter)
        self.dvh_tab_layout.addWidget(button_calc_dvh)

    def clear_layout(self):
        """
        Clear the layout of the DVH tab.
        """
        for i in reversed(range(self.dvh_tab_layout.count())):
            item = self.dvh_tab_layout.itemAt(i)
            if item.widget():
                item.widget().setParent(None)
            else:
                for j in reversed(range(item.count())):
                    item.itemAt(j).widget().setParent(None)

    def plot_dvh(self):
        """
        :return: DVH plot using Matplotlib library.
        """
        # Initialisation of the plots
        fig, ax = plt.subplots()
        fig.subplots_adjust(0.1, 0.15, 1, 1)
        # Maximum value for x axis
        max_xlim = 0

        # Plot for all the ROIs selected in the left column of the window
        for roi in self.selected_rois:
            dvh = self.raw_dvh[int(roi)]

            # Plot only the ROIs whose volume is non equal to 0
            if dvh.volume != 0:
                # Bincenters, obtained from the dvh object, give the x axis values
                # (Doses originally in Gy unit)
                bincenters = self.dvh_x_y[roi]['bincenters']
                # print(self.dvh_x_y[roi])

                # Counts, obtained from the dvh object, give the y axis values
                # (values between 0 and dvh.volume)
                counts = self.dvh_x_y[roi]['counts']

                # Color of the line is the same as the color shown in the left column of the window
                color = self.patient_dict_container.get("roi_color_dict")[roi]
                color_R = color.red() / 255
                color_G = color.green() / 255
                color_B = color.blue() / 255

                plt.plot(100 * bincenters,
                         100 * counts / dvh.volume,
                         label=dvh.name,
                         color=[color_R, color_G, color_B])

                # Update the maximum value for x axis (usually different between ROIs)
                if (100 * bincenters[-1]) > max_xlim:
                    max_xlim = 100 * bincenters[-1]

                plt.xlabel('Dose [%s]' % 'cGy')
                plt.ylabel('Volume [%s]' % '%')
                if dvh.name:
                    plt.legend(loc='lower center', bbox_to_anchor=(0, 1, 5, 5))

        # Set the range values for x and y axis
        ax.set_ylim([0, 105])
        ax.set_xlim([0, max_xlim + 3])

        # Create the grids on the plot
        major_ticks_y = np.arange(0, 105, 20)
        minor_ticks_y = np.arange(0, 105, 5)
        major_ticks_x = np.arange(0, max_xlim + 250, 1000)
        minor_ticks_x = np.arange(0, max_xlim + 250, 250)
        ax.set_xticks(major_ticks_x)
        ax.set_xticks(minor_ticks_x, minor=True)
        ax.set_yticks(major_ticks_y)
        ax.set_yticks(minor_ticks_y, minor=True)
        ax.grid(which='minor', alpha=0.2)
        ax.grid(which='major', alpha=0.5)

        # Add the legend at the bottom left of the graph
        if len(self.selected_rois) != 0:
            ax.legend(loc='upper left', bbox_to_anchor=(-0.1, -0.15), ncol=4)

        plt.subplots_adjust(bottom=0.3)

        return fig

    def prompt_calc_dvh(self):
        """
        Prompt for DVH calculation.
        """
        if platform.system() == "Linux":
            choice = \
                QtWidgets.QMessageBox.question(
                    self, "Calculate DVHs?",
                    "Would you like to (re)calculate DVHs?",
                    QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)

            if choice == QtWidgets.QMessageBox.Yes:
                progress_window = \
                    CalculateDVHProgressWindow(
                        self,
                        QtCore.Qt.WindowTitleHint |
                        QtCore.Qt.WindowCloseButtonHint)
                progress_window.signal_dvh_calculated.connect(
                    self.dvh_calculation_finished)
                self.patient_dict_container.set("dvh_outdated", False)
                progress_window.exec_()

                self.export_rtdose()
        else:
            stylesheet_path = ""

            # Select appropriate style sheet
            if platform.system() == 'Darwin':
                stylesheet_path = Path.cwd().joinpath('res', 'stylesheet.qss')
            else:
                stylesheet_path = Path.cwd().joinpath(
                    'res', 'stylesheet-win-linux.qss')

            # Create a message box and add attributes
            mb = QtWidgets.QMessageBox()
            mb.setIcon(QtWidgets.QMessageBox.Question)
            mb.setWindowTitle("Calculate DVHs?")
            mb.setText("Would you like to (re)calculate DVHs?")
            button_no = QtWidgets.QPushButton("No")
            button_yes = QtWidgets.QPushButton("Yes")
            """ We want the buttons 'No' & 'Yes' to be displayed in that exact order. QMessageBox displays buttons in
                respect to their assigned roles. (0 first, then 0 and so on) 'AcceptRole' is 0 and 'RejectRole' is 1 
                thus by counterintuitively assigning 'No' to 'AcceptRole' and 'Yes' to 'RejectRole' the buttons are 
                positioned as desired.
            """
            mb.addButton(button_no, QtWidgets.QMessageBox.AcceptRole)
            mb.addButton(button_yes, QtWidgets.QMessageBox.RejectRole)

            # Apply stylesheet to the message box and add icon to the window
            mb.setStyleSheet(open(stylesheet_path).read())
            mb.setWindowIcon(
                QtGui.QIcon(
                    resource_path(Path.cwd().joinpath('res', 'images',
                                                      'btn-icons',
                                                      'onkodicom_icon.png'))))

            mb.exec_()

            if mb.clickedButton() == button_yes:
                progress_window = CalculateDVHProgressWindow(
                    self, QtCore.Qt.WindowTitleHint
                    | QtCore.Qt.WindowCloseButtonHint)
                progress_window.signal_dvh_calculated.connect(
                    self.dvh_calculation_finished)
                self.patient_dict_container.set("dvh_outdated", False)
                progress_window.exec_()

                self.export_rtdose()

    def dvh_calculation_finished(self):
        # Clear the screen
        self.clear_layout()
        self.dvh_calculated = True
        self.init_layout_dvh()

    def update_plot(self):
        if self.dvh_calculated:
            # Get new list of selected rois that have DVHs calculated
            self.selected_rois = [
                roi for roi in self.patient_dict_container.get("selected_rois")
                if roi in self.raw_dvh.keys()
            ]

            # Clear the current layout
            self.clear_layout()

            # If the DVH has become outdated, show the user an indicator advising them such.
            if self.patient_dict_container.get("dvh_outdated"):
                self.display_outdated_indicator()

            # Re-draw the plot and add to layout
            self.init_layout_dvh()

    def export_csv(self):
        path = self.patient_dict_container.path
        basic_info = self.patient_dict_container.get("basic_info")
        if not os.path.isdir(path + '/CSV'):
            os.mkdir(path + '/CSV')
        dvh2csv(self.raw_dvh, path + "/CSV/", 'DVH_' + basic_info['id'],
                basic_info['id'])
        QtWidgets.QMessageBox.information(
            self, "Message",
            "The DVH Data was saved successfully in your directory!",
            QtWidgets.QMessageBox.Ok)

    def export_rtdose(self):
        """
        Exports DVH data into the RT Dose file in the dataset directory.
        """
        dvh2rtdose(self.raw_dvh)
        QtWidgets.QMessageBox.information(
            self, "Message",
            "The DVH Data was saved successfully in your directory!",
            QtWidgets.QMessageBox.Ok)

    def import_rtdose(self):
        """
        Import DVH data from an RT Dose.
        """
        # Get DVH data
        result = rtdose2dvh()

        # If there is DVH data
        if bool(result):
            incomplete = result["diff"]
            result.pop("diff")
            dvh_x_y = ImageLoading.converge_to_0_dvh(result)
            self.patient_dict_container.set("raw_dvh", result)
            self.patient_dict_container.set("dvh_x_y", dvh_x_y)

            # If incomplete, tell the user about this
            if incomplete:
                self.patient_dict_container.set("dvh_outdated", True)
                self.display_outdated_indicator()

            # Initialise the display
            self.dvh_calculation_finished()
        else:
            result.pop("diff")
            self.init_layout_no_dvh()

    def display_outdated_indicator(self):
        self.modified_indicator_widget = QtWidgets.QWidget()
        self.modified_indicator_widget.setContentsMargins(8, 5, 8, 5)
        # self.modified_indicator_widget.setFixedHeight(35)
        modified_indicator_layout = QtWidgets.QHBoxLayout()
        modified_indicator_layout.setAlignment(QtCore.Qt.AlignLeft
                                               | QtCore.Qt.AlignLeft)

        modified_indicator_icon = QtWidgets.QLabel()
        modified_indicator_icon.setPixmap(
            QtGui.QPixmap(
                resource_path("res/images/btn-icons/alert_icon.png")))
        modified_indicator_layout.addWidget(modified_indicator_icon)

        modified_indicator_text = QtWidgets.QLabel(
            "Contours have been modified since DVH calculation. Some DVHs may "
            "now be out of date.")
        modified_indicator_text.setStyleSheet("color: red")
        modified_indicator_layout.addWidget(modified_indicator_text)

        self.modified_indicator_widget.setLayout(modified_indicator_layout)

        self.dvh_tab_layout.addWidget(self.modified_indicator_widget,
                                      QtCore.Qt.AlignTop | QtCore.Qt.AlignTop)
Example #28
0
class UIImageFusionWindow(object):
    image_fusion_info_initialized = QtCore.Signal(object)

    def setup_ui(self, open_image_fusion_select_instance):
        """Sets up a UI"""
        if platform.system() == 'Darwin':
            self.stylesheet_path = "res/stylesheet.qss"
        else:
            self.stylesheet_path = "res/stylesheet-win-linux.qss"

        window_icon = QIcon()
        window_icon.addPixmap(QPixmap(resource_path("res/images/icon.ico")),
                              QIcon.Normal, QIcon.Off)
        open_image_fusion_select_instance.setObjectName(
            "OpenPatientWindowInstance")
        open_image_fusion_select_instance.setWindowIcon(window_icon)
        open_image_fusion_select_instance.resize(840, 530)

        # Create a vertical box for containing the other elements and layouts
        self.open_patient_window_instance_vertical_box = QVBoxLayout()
        self.open_patient_window_instance_vertical_box.setObjectName(
            "OpenPatientWindowInstanceVerticalBox")

        # Create a label to prompt the user to enter the path to the
        # directory that contains the DICOM files
        self.open_patient_directory_prompt = QLabel()
        self.open_patient_directory_prompt.setObjectName(
            "OpenPatientDirectoryPrompt")
        self.open_patient_directory_prompt.setAlignment(Qt.AlignLeft)
        self.open_patient_window_instance_vertical_box.addWidget(
            self.open_patient_directory_prompt)

        # Create a horizontal box to hold the input box for the directory 
        # and the choose button
        self.open_patient_directory_input_horizontal_box = QHBoxLayout()
        self.open_patient_directory_input_horizontal_box.setObjectName(
            "OpenPatientDirectoryInputHorizontalBox")
        # Create a textbox to contain the path to the directory that contains 
        # the DICOM files
        self.open_patient_directory_input_box = \
            UIImageFusionWindowDragAndDropEvent(self)

        self.open_patient_directory_input_box.setObjectName(
            "OpenPatientDirectoryInputBox")
        self.open_patient_directory_input_box.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding,
                        QSizePolicy.MinimumExpanding))
        self.open_patient_directory_input_box.returnPressed.connect(
            self.scan_directory_for_patient)
        self.open_patient_directory_input_horizontal_box.addWidget(
            self.open_patient_directory_input_box)

        # Create a choose button to open the file dialog
        self.open_patient_directory_choose_button = QPushButton()
        self.open_patient_directory_choose_button.setObjectName(
            "OpenPatientDirectoryChooseButton")
        self.open_patient_directory_choose_button.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding,
                        QSizePolicy.MinimumExpanding))
        self.open_patient_directory_choose_button.resize(
            self.open_patient_directory_choose_button.sizeHint().width(),
            self.open_patient_directory_input_box.height())
        self.open_patient_directory_choose_button.setCursor(
            QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.open_patient_directory_input_horizontal_box.addWidget(
            self.open_patient_directory_choose_button)
        self.open_patient_directory_choose_button.clicked.connect(
            self.choose_button_clicked)

        # Create a widget to hold the input fields
        self.open_patient_directory_input_widget = QWidget()
        self.open_patient_directory_input_horizontal_box.setStretch(0, 4)
        self.open_patient_directory_input_widget.setLayout(
            self.open_patient_directory_input_horizontal_box)
        self.open_patient_window_instance_vertical_box.addWidget(
            self.open_patient_directory_input_widget)

        # Create a horizontal box to hold the stop button and direction to 
        # the user on where to select the patient
        self.open_patient_appear_prompt_and_stop_horizontal_box = QHBoxLayout()
        self.open_patient_appear_prompt_and_stop_horizontal_box.setObjectName(
            "OpenPatientAppearPromptAndStopHorizontalBox")
        # Create a label to show direction on where the files will appear
        self.open_patient_directory_appear_prompt = QLabel()
        self.open_patient_directory_appear_prompt.setObjectName(
            "OpenPatientDirectoryAppearPrompt")
        self.open_patient_directory_appear_prompt.setAlignment(Qt.AlignLeft)
        self.open_patient_appear_prompt_and_stop_horizontal_box.addWidget(
            self.open_patient_directory_appear_prompt)
        self.open_patient_appear_prompt_and_stop_horizontal_box.addStretch(1)
        # Create a button to stop searching
        self.open_patient_window_stop_button = QPushButton()
        self.open_patient_window_stop_button.setObjectName(
            "OpenPatientWindowStopButton")
        self.open_patient_window_stop_button.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding,
                        QSizePolicy.MinimumExpanding))
        self.open_patient_window_stop_button.resize(
            self.open_patient_window_stop_button.sizeHint().width(),
            self.open_patient_window_stop_button.sizeHint().height())
        self.open_patient_window_stop_button.setCursor(
            QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.open_patient_window_stop_button.clicked.connect(
            self.stop_button_clicked)
        self.open_patient_window_stop_button.setProperty(
            "QPushButtonClass", "fail-button")
        self.open_patient_window_stop_button.setVisible(False)
        self.open_patient_appear_prompt_and_stop_horizontal_box.addWidget(
            self.open_patient_window_stop_button)
        # Create a widget to hold the layout
        self.open_patient_appear_prompt_and_stop_widget = QWidget()
        self.open_patient_appear_prompt_and_stop_widget.setLayout(
            self.open_patient_appear_prompt_and_stop_horizontal_box)
        self.open_patient_window_instance_vertical_box.addWidget(
            self.open_patient_appear_prompt_and_stop_widget)

        # Create a tree view list to list out all patients in the directory 
        # selected above
        self.open_patient_window_patients_tree = QTreeWidget()
        self.open_patient_window_patients_tree.setObjectName(
            "OpenPatientWindowPatientsTree")
        self.open_patient_window_patients_tree.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding,
                        QSizePolicy.MinimumExpanding))
        self.open_patient_window_patients_tree.resize(
            self.open_patient_window_patients_tree.sizeHint().width(),
            self.open_patient_window_patients_tree.sizeHint().height())
        self.open_patient_window_patients_tree.setHeaderHidden(False)
        self.open_patient_window_patients_tree.setHeaderLabels([""])
        self.open_patient_window_patients_tree.itemChanged.connect(
            self.tree_item_clicked)
        self.open_patient_window_instance_vertical_box.addWidget(
            self.open_patient_window_patients_tree)
        self.last_patient = None

        # Create a label to show what would happen if they select the patient
        self.open_patient_directory_result_label = QtWidgets.QLabel()
        self.open_patient_directory_result_label.setObjectName(
            "OpenPatientDirectoryResultLabel")
        self.open_patient_directory_result_label.setAlignment(Qt.AlignLeft)
        self.open_patient_window_instance_vertical_box.addWidget(
            self.open_patient_directory_result_label)

        # Create a horizontal box to hold the Cancel and Open button
        self.open_patient_window_patient_open_actions_horizontal_box = \
            QHBoxLayout()
        self.open_patient_window_patient_open_actions_horizontal_box. \
            setObjectName("OpenPatientWindowPatientOpenActionsHorizontalBox")
        self.open_patient_window_patient_open_actions_horizontal_box. \
            addStretch(1)
        # Add a button to go back/close from the application
        self.open_patient_window_close_button = QPushButton()
        self.open_patient_window_close_button.setObjectName(
            "OpenPatientWindowcloseButton")
        self.open_patient_window_close_button.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding,
                        QSizePolicy.MinimumExpanding))
        self.open_patient_window_close_button.resize(
            self.open_patient_window_stop_button.sizeHint().width(),
            self.open_patient_window_stop_button.sizeHint().height())
        self.open_patient_window_close_button.setCursor(
            QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.open_patient_window_close_button.clicked.connect(
            self.close_button_clicked)
        self.open_patient_window_close_button.setProperty(
            "QPushButtonClass", "fail-button")
        self.open_patient_window_patient_open_actions_horizontal_box. \
            addWidget(self.open_patient_window_close_button)

        # Add a button to confirm opening of the patient
        self.open_patient_window_confirm_button = QPushButton()
        self.open_patient_window_confirm_button.setObjectName(
            "OpenPatientWindowConfirmButton")
        self.open_patient_window_confirm_button.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding,
                        QSizePolicy.MinimumExpanding))
        self.open_patient_window_confirm_button.resize(
            self.open_patient_window_confirm_button.sizeHint().width(),
            self.open_patient_window_confirm_button.sizeHint().height())

        self.open_patient_window_confirm_button.setCursor(
            QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.open_patient_window_confirm_button.setDisabled(True)
        self.open_patient_window_confirm_button.clicked.connect(
            self.confirm_button_clicked)
        self.open_patient_window_confirm_button.setProperty(
            "QPushButtonClass", "success-button")
        self.open_patient_window_patient_open_actions_horizontal_box. \
            addWidget(
            self.open_patient_window_confirm_button)

        # Create a widget to house all of the actions button for open patient
        #  window
        self.open_patient_window_patient_open_actions_widget = QWidget()
        self.open_patient_window_patient_open_actions_widget.setLayout(
            self.open_patient_window_patient_open_actions_horizontal_box)
        self.open_patient_window_instance_vertical_box.addWidget(
            self.open_patient_window_patient_open_actions_widget)

        # Set the vertical box fourth element, the tree view, to stretch 
        # out as far as possible
        self.open_patient_window_instance_vertical_box.setStretch(3, 4)
        self.open_patient_window_instance_central_widget = QWidget()
        self.open_patient_window_instance_central_widget.setObjectName(
            "OpenPatientWindowInstanceCentralWidget")
        self.open_patient_window_instance_central_widget.setLayout(
            self.open_patient_window_instance_vertical_box)

        # Create threadpool for multithreading
        self.threadpool = QThreadPool()
        # print("Multithreading with maximum %d threads" % self.threadpool.
        # maxThreadCount())
        # Create interrupt event for stopping the directory search
        self.interrupt_flag = threading.Event()

        # Bind all texts into the buttons and labels
        self.retranslate_ui(open_image_fusion_select_instance)
        # Set the central widget, ready for display
        open_image_fusion_select_instance.setCentralWidget(
            self.open_patient_window_instance_central_widget)

        # Set the current stylesheet to the instance and connect it back 
        # to the caller through slot
        _stylesheet = open(resource_path(self.stylesheet_path)).read()
        open_image_fusion_select_instance.setStyleSheet(_stylesheet)

        QtCore.QMetaObject.connectSlotsByName(
            open_image_fusion_select_instance)

    def retranslate_ui(self, open_image_fusion_select_instance):
        """Translates UI"""
        _translate = QtCore.QCoreApplication.translate
        open_image_fusion_select_instance.setWindowTitle(
            _translate("OpenPatientWindowInstance",
                       "OnkoDICOM - Select Patient"))
        self.open_patient_directory_prompt.setText(_translate(
            "OpenPatientWindowInstance",
            "Choose an image to merge with:"))
        self.open_patient_directory_input_box.setPlaceholderText(
            _translate("OpenPatientWindowInstance",
                       "Enter DICOM Files Path (For example, "
                       "C:\path\\to\your\DICOM\Files)"))
        self.open_patient_directory_choose_button.setText(_translate(
            "OpenPatientWindowInstance",
            "Choose"))
        self.open_patient_directory_appear_prompt.setText(_translate(
            "OpenPatientWindowInstance",
            "Please select below the image set you wish to overlay:"))
        self.open_patient_directory_result_label. \
            setText("The selected imageset(s) above will be "
                    "co-registered with the current imageset.")
        self.open_patient_window_stop_button.setText(_translate(
            "OpenPatientWindowInstance", "Stop Search"))
        self.open_patient_window_close_button.setText(_translate(
            "OpenPatientWindowInstance", "Close"))
        self.open_patient_window_confirm_button.setText(_translate(
            "OpenPatientWindowInstance", "Confirm"))

    def update_patient(self):
        self.clear_checked_leaves()
        self.patient_dict_container = PatientDictContainer()
        self.patient = self.patient_dict_container.get("basic_info")
        self.patient_id = self.patient['id']

        dataset = self.patient_dict_container.dataset[0]
        self.patient_current_image_series_uid = \
            dataset.get("SeriesInstanceUID")

    def clear_checked_leaves(self):
        """
        Resets all leaves to their unchecked state
        """
        def recurse(parent_item: QTreeWidgetItem):
            for i in range(parent_item.childCount()):
                child = parent_item.child(i)
                grand_children = child.childCount()
                if grand_children > 0:
                    recurse(child)
                else:
                    if child.checkState(0) == Qt.Checked:
                        child.setCheckState(0, Qt.CheckState.Unchecked)
                        child.setSelected(False)

        recurse(self.open_patient_window_patients_tree.invisibleRootItem())
        self.open_patient_window_patients_tree.collapseAll()

    def close_button_clicked(self):
        """Closes the window."""
        self.close()

    def scan_directory_for_patient(self):
        # Reset tree view header and last patient
        self.open_patient_window_confirm_button.setDisabled(True)
        self.open_patient_window_patients_tree.setHeaderLabels([""])
        self.last_patient = None
        self.filepath = self.open_patient_directory_input_box.text()
        # Proceed if a folder was selected
        if self.filepath != "":
            # Update the QTreeWidget to reflect data being loaded
            # First, clear the widget of any existing data
            self.open_patient_window_patients_tree.clear()

            # Next, update the tree widget
            self.open_patient_window_patients_tree.addTopLevelItem(
                QTreeWidgetItem(["Loading selected directory..."]))

            # The choose button is disabled until the thread finishes executing
            self.open_patient_directory_choose_button.setEnabled(False)

            # Reveals the Stop Search button for the duration of the search
            self.open_patient_window_stop_button.setVisible(True)

            # The interrupt flag is then un-set if a previous search has been
            # stopped.
            self.interrupt_flag.clear()

            # Then, create a new thread that will load the selected folder
            worker = Worker(DICOMDirectorySearch.get_dicom_structure,
                            self.filepath,
                            self.interrupt_flag, progress_callback=True)
            worker.signals.result.connect(self.on_search_complete)
            worker.signals.progress.connect(self.search_progress)

            # Execute the thread
            self.threadpool.start(worker)

    def choose_button_clicked(self):
        """
        Executes when the choose button is clicked.
        Gets filepath from the user and loads all files and subdirectories.
        """
        # Get folder path from pop up dialog box
        self.filepath = QtWidgets.QFileDialog.getExistingDirectory(
            None, 'Select patient folder...', '')
        self.open_patient_directory_input_box.setText(self.filepath)
        self.scan_directory_for_patient()

    def stop_button_clicked(self):
        self.interrupt_flag.set()

    def search_progress(self, progress_update):
        """
        Current progress of the file search.
        """
        self.open_patient_window_patients_tree.clear()
        self.open_patient_window_patients_tree.addTopLevelItem(
            QTreeWidgetItem(["Loading selected directory... "
                             "(%s files searched)" % progress_update]))

    def on_search_complete(self, dicom_structure):
        """
        Executes once the directory search is complete.
        :param dicom_structure: DICOMStructure object constructed by the
        directory search.
        """
        self.open_patient_directory_choose_button.setEnabled(True)
        self.open_patient_window_stop_button.setVisible(False)
        self.open_patient_window_patients_tree.clear()

        # dicom_structure will be None if function was interrupted.
        if dicom_structure is None:
            return

        for patient_item in dicom_structure.get_tree_items_list():
            self.open_patient_window_patients_tree.addTopLevelItem(
                patient_item)
            patient_item.setExpanded(True)  # Display all studies
            # Display all image sets
            for i in range(patient_item.childCount()):
                study = patient_item.child(i)
                study.setExpanded(True)

        if len(dicom_structure.patients) == 0:
            QMessageBox.about(self, "No files found",
                              "Selected directory contains no DICOM files.")

    def tree_item_clicked(self, item, _):
        """
        Executes when a tree item is checked or unchecked.
        If a different patient is checked, uncheck the previous patient.
        Inform user about missing DICOM files.
        """
        # If patient is only selected, but not checked, set it to "focus" to
        # coincide with stylesheet. And if the selected item is an image set,
        # display its child branches.
        if item.checkState(0) == Qt.CheckState.Unchecked:
            self.open_patient_window_patients_tree.setCurrentItem(item)
        else:  # Otherwise don't "focus", then set patient as selected
            self.open_patient_window_patients_tree.setCurrentItem(None)
            item.setSelected(True)

        # Expand or collapse the tree branch if item is an image series
        # Only collapse if the selected image series is expanded but unchecked
        # Otherwise, expand its tree branch to show RT files
        is_expanded = False \
            if (item.isExpanded() is True and
                item.checkState(0) == Qt.CheckState.Unchecked) else True
        self.display_a_tree_branch(item, is_expanded)

        selected_patient = item
        # If the item is not top-level, bubble up to see which top-level item
        # this item belongs to
        if self.open_patient_window_patients_tree.invisibleRootItem(). \
                indexOfChild(item) == -1:
            while self.open_patient_window_patients_tree.invisibleRootItem(). \
                    indexOfChild(selected_patient) == -1:
                selected_patient = selected_patient.parent()

        # Uncheck previous patient if a different patient is selected
        if item.checkState(0) == Qt.CheckState.Checked and self.last_patient \
                != selected_patient:
            if self.last_patient is not None:
                last_patient_checked_items = self.get_checked_nodes(
                    self.last_patient)
                for checked_item in last_patient_checked_items:
                    checked_item.setCheckState(0, Qt.Unchecked)
            self.last_patient = selected_patient

        # Check selected items and display warning messages
        self.check_selected_items(selected_patient)

    def display_a_tree_branch(self, node, is_expanded):
        # TO DO:
        # Could Team 23 please update the defintion of this docstring as
        # well as same function presented in OpenPatientWindow.
        """
        Displays a tree branch
        Parameters:
            node : root node the tree
            is_expanded (boolean): flag for checking if a particular
            node/leaf is expanded.
        """
        node.setExpanded(is_expanded)
        if node.childCount() > 0:
            for i in range(node.childCount()):
                self.display_a_tree_branch(node.child(i), is_expanded)
        else:
            return

    def check_selected_items(self, selected_patient):
        """
        Check and display warning messages based on the existence and quantity
        of image series, RTSTRUCT, RTPLAN, RTDOSE and SR files

        Parameters:
            selected_patient (DICOMStructure): DICOM Object of patient
        """
        # Get the types of all selected leaves & Get the names of all selected
        # studies
        checked_nodes = self.get_checked_nodes(
            self.open_patient_window_patients_tree.invisibleRootItem())
        selected_series_types = [checked_node.dicom_object.get_series_type()
                                 for checked_node in checked_nodes]
        selected_series_id = [checked_node.dicom_object.series_uid
                              for checked_node in checked_nodes]

        # Total number of selected image series
        total_selected_image_series = selected_series_types.count('CT') + \
                                      selected_series_types.count('MR') + \
                                      selected_series_types.count('PT')

        # Check the existence of IMAGE, RTSTRUCT, RTPLAN and RTDOSE files
        proceed = True

        if total_selected_image_series < 1:
            header = "Cannot proceed without an image."
            proceed = False
        elif total_selected_image_series > 1:
            header = "Cannot proceed with more than 1 selected image."
            proceed = False
        elif selected_patient.dicom_object.patient_id.strip() != \
                self.patient_id:
            header = "Cannot proceed with different patient."
            proceed = False
        elif self.patient_current_image_series_uid in selected_series_id:
            header = "Cannot fuse with the same series."
            proceed = False
        elif not self.check_selected_items_referencing(checked_nodes):
            # Check that selected items properly reference each other
            header = "Selected series do not reference each other."
            proceed = False
        elif 'RTSTRUCT' not in selected_series_types and \
            self.check_existing_rtss(checked_nodes):
            header = "The associated RTSTRUCT must be selected."
            proceed = False
        elif 'RTDOSE' in selected_series_types:
            header = "Cannot fuse with a RTDOSE file."
            proceed = False
        else:
            header = ""
        self.open_patient_window_confirm_button.setDisabled(not proceed)

        # Set the tree header
        self.open_patient_window_patients_tree.setHeaderLabel(header)

    def check_selected_items_referencing(self, items):
        """
        Check if selected tree items properly reference each other.
        :param items: List of selected DICOMWidgetItems.
        :return: True if the selected items belong to the same tree branch.
        """
        # Dictionary of series of different file types
        series = {
            "IMAGE": None,
            "RTSTRUCT": None,
            "RTPLAN": None,
            "RTDOSE": None,
            "SR": None
        }

        for item in items:
            series_type = item.dicom_object.get_series_type()
            if series_type in series:
                series[series_type] = item
            else:
                series["IMAGE"] = item

        # Check if the RTSTRUCT, RTPLAN, and RTDOSE are a child item of the
        # image series
        if series["IMAGE"]:
            if series["RTSTRUCT"] and series["RTSTRUCT"].parent() != \
                    series["IMAGE"]:
                return False

            if series["RTPLAN"] and \
                    series["RTPLAN"].parent().parent() != series["IMAGE"]:
                return False

            if series["SR"] and series["SR"].parent() != series["IMAGE"]:
                return False

        return True

    def check_existing_rtss(self, items):
        """
        Check for existing rtss
        :return: bool, whether there is a rtss associated with the selected
        image series
        """
        image_series = ['CT', 'MR', 'PT']
        for item in items:
            if item.dicom_object.get_series_type() in image_series:
                for i in range(item.childCount()):
                    if item.child(i).dicom_object:
                        return True
                return False

    def get_checked_nodes(self, root):
        """
        :param root: QTreeWidgetItem as a root.
        :return: A list of all QTreeWidgetItems in the QTreeWidget that are
        checked under the root.
        """
        checked_items = []

        def recurse(parent_item: QTreeWidgetItem):
            for i in range(parent_item.childCount()):
                child = parent_item.child(i)
                if int(child.flags()) & int(Qt.ItemIsUserCheckable) and \
                        child.checkState(0) == Qt.Checked:
                    checked_items.append(child)
                grand_children = child.childCount()
                if grand_children > 0:
                    recurse(child)

        recurse(root)
        return checked_items

    def confirm_button_clicked(self):
        """
        Begins loading of the selected files.
        """
        selected_files = []
        for item in self.get_checked_nodes(
                self.open_patient_window_patients_tree.invisibleRootItem()):
            selected_files += item.dicom_object.get_files()

        self.progress_window = ImageFusionProgressWindow(self)
        self.progress_window.signal_loaded.connect(self.on_loaded)
        self.progress_window.signal_error.connect(self.on_loading_error)
        self.progress_window.start_loading(selected_files)

    def on_loaded(self, results):
        """
        Executes when the progress bar finishes loaded the selected files.
        """
        if results[0] is True:  # Will be NoneType if loading was interrupted.
            self.image_fusion_info_initialized.emit(results[1])

    def on_loading_error(self, exception):
        """
        Error handling for progress window.
        """
        if type(exception[1]) == ImageLoading.NotRTSetError:
            QMessageBox.about(self.progress_window,
                              "Unable to open selection",
                              "Selected files cannot be opened as they are not"
                              " a DICOM-RT set.")
            self.progress_window.close()
        elif type(exception[1]) == ImageLoading.NotAllowedClassError:
            QMessageBox.about(self.progress_window,
                              "Unable to open selection",
                              "Selected files cannot be opened as they contain"
                              " unsupported DICOM classes.")
            self.progress_window.close()
class UITransferROIWindow:
    def setup_ui(self, transfer_roi_window_instance,
                 signal_roi_transferred_to_fixed_container,
                 signal_roi_transferred_to_moving_container):
        self.patient_dict_container = PatientDictContainer()
        self.moving_dict_container = MovingDictContainer()
        self.fixed_image_initial_rois = self.patient_dict_container.get("rois")
        self.moving_image_initial_rois = self.moving_dict_container.get("rois")
        self.transfer_roi_window_instance = transfer_roi_window_instance
        self.signal_roi_transferred_to_fixed_container = \
            signal_roi_transferred_to_fixed_container
        self.signal_roi_transferred_to_moving_container = \
            signal_roi_transferred_to_moving_container
        self.fixed_to_moving_rois = {}
        self.moving_to_fixed_rois = {}
        self.add_suffix = True
        self.progress_window = ProgressWindow(
            self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
        self.progress_window.setFixedSize(250, 100)
        self.progress_window.signal_loaded \
            .connect(self.onTransferRoiFinished)
        self.progress_window.signal_error.connect(self.onTransferRoiError)

        self.init_layout()

    def retranslate_ui(self, transfer_roi_window_instance):
        _translate = QtCore.QCoreApplication.translate
        transfer_roi_window_instance.setWindowTitle(
            _translate("TransferRoiWindowInstance",
                       "OnkoDICOM - Transfer Region of Interest"))
        self.add_suffix_checkbox.setText(
            _translate("AddSuffixCheckBox", "Add Suffix"))
        self.patient_A_label.setText(
            _translate("PatientAROILabel", "First Image Set ROIs"))
        self.patient_B_label.setText(
            _translate("PatientBROILabel", "Second Image Set ROIs"))
        self.transfer_all_rois_to_patient_B_button.setText(
            _translate("ROITransferToBButton", "All"))
        self.transfer_all_rois_to_patient_A_button.setText(
            _translate("ROITransferToAButton", "All"))
        self.save_button.setText(_translate("SaveButton", "Save"))
        self.reset_button.setText(_translate("ResetButton", "Reset"))

    def init_layout(self):
        """
        Initialize the layout for the Transfer ROI Window.
        """
        if platform.system() == 'Darwin':
            self.stylesheet_path = "res/stylesheet.qss"
        else:
            self.stylesheet_path = "res/stylesheet-win-linux.qss"
        stylesheet = open(resource_path(self.stylesheet_path)).read()
        window_icon = QIcon()
        window_icon.addPixmap(QPixmap(resource_path("res/images/icon.ico")),
                              QIcon.Normal, QIcon.Off)
        self.transfer_roi_window_instance.setObjectName(
            "TransferRoiWindowInstance")
        self.transfer_roi_window_instance.setWindowIcon(window_icon)

        # Creating a grid layout to hold all elements
        self.transfer_roi_window_grid_layout = QGridLayout()
        self.transfer_roi_window_grid_layout.setColumnStretch(0, 1)
        self.transfer_roi_window_grid_layout.setColumnStretch(1, 1)
        self.transfer_roi_window_grid_layout.setColumnStretch(2, 1)

        self.init_patient_labels()
        self.init_transfer_arrow_buttons()
        self.init_patient_A_initial_roi_list()
        self.init_patient_B_rois_to_A_layout()
        self.init_patient_A_rois_to_B_layout()
        self.init_patient_B_initial_roi_list()
        self.init_add_suffix_checkbox()
        self.init_save_and_reset_button_layout()

        # Create a new central widget to hold the grid layout
        self.transfer_roi_window_instance_central_widget = QWidget()
        self.transfer_roi_window_instance_central_widget.setLayout(
            self.transfer_roi_window_grid_layout)
        self.retranslate_ui(self.transfer_roi_window_instance)
        self.transfer_roi_window_instance.setStyleSheet(stylesheet)
        self.transfer_roi_window_instance.setCentralWidget(
            self.transfer_roi_window_instance_central_widget)
        QtCore.QMetaObject.connectSlotsByName(
            self.transfer_roi_window_instance)

    def init_transfer_arrow_buttons(self):
        """
        Initialize the layout for arrow buttons

        """
        self.transfer_all_rois_to_patient_B_button = QPushButton()
        self.transfer_all_rois_to_patient_B_button.setObjectName(
            "ROITransferToBButton")

        transfer_all_rois_to_patient_B_icon = QtGui.QIcon()
        transfer_all_rois_to_patient_B_icon.addPixmap(
            QtGui.QPixmap(
                resource_path('res/images/btn-icons/forward_slide_icon.png')),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.transfer_all_rois_to_patient_B_button \
            .setIcon(transfer_all_rois_to_patient_B_icon)
        self.transfer_all_rois_to_patient_B_button.clicked.connect(
            self.transfer_all_rois_to_patient_B_button_clicked)
        self.transfer_roi_window_grid_layout.addWidget(
            self.transfer_all_rois_to_patient_B_button, 1, 1)

        self.transfer_all_rois_to_patient_A_button = QPushButton()
        self.transfer_all_rois_to_patient_A_button.setObjectName(
            "ROITransferToAButton")
        self.transfer_all_rois_to_patient_A_button.setMaximumWidth(100)
        transfer_all_rois_to_patient_A_icon = QtGui.QIcon()
        transfer_all_rois_to_patient_A_icon.addPixmap(
            QtGui.QPixmap(
                resource_path('res/images/btn-icons/backward_slide_icon.png')),
            QtGui.QIcon.Normal, QtGui.QIcon.On)
        self.transfer_all_rois_to_patient_A_button \
            .setIcon(transfer_all_rois_to_patient_A_icon)
        self.transfer_all_rois_to_patient_A_button.clicked.connect(
            self.transfer_all_rois_to_patient_A_button_clicked)
        self.transfer_roi_window_grid_layout.addWidget(
            self.transfer_all_rois_to_patient_A_button, 2, 1)

    def transfer_all_rois_to_patient_B_button_clicked(self):
        """
        This function is triggered when the right arrow button is clicked.
        """
        self.fixed_to_moving_rois.clear()
        self.patient_A_rois_to_B_list_widget.clear()

        for i in range(0, len(self.fixed_image_initial_rois)):
            self.patient_A_initial_roi_double_clicked(
                self.patient_A_initial_rois_list_widget.item(i))

    def transfer_all_rois_to_patient_A_button_clicked(self):
        """
        This function is triggered when the left arrow button is clicked.
        """
        self.moving_to_fixed_rois.clear()
        self.patient_B_rois_to_A_list_widget.clear()

        for i in range(0, len(self.moving_image_initial_rois)):
            self.patient_B_initial_roi_double_clicked(
                self.patient_B_initial_rois_list_widget.item(i))

    def init_add_suffix_checkbox(self):
        """
        Initialize the layout for add suffix checkbox
        """
        self.add_suffix_checkbox = QCheckBox()
        self.add_suffix_checkbox.setObjectName("AddSuffixCheckBox")
        self.add_suffix_checkbox.setChecked(self.add_suffix)
        self.add_suffix_checkbox.clicked.connect(
            self.add_suffix_checkbox_clicked)
        self.transfer_roi_window_grid_layout.addWidget(
            self.add_suffix_checkbox, 3, 0)

    def init_patient_labels(self):
        """
        Initialize the layout for two patient labels
        """
        self.patient_A_label = QLabel()
        self.patient_A_label.setObjectName("PatientAROILabel")
        self.patient_A_label.setMinimumHeight(50)
        self.patient_A_label.setAlignment(Qt.AlignCenter)
        self.patient_A_label.setStyleSheet(
            "QLabel { background-color : green; color : white; "
            "font-size: 15pt; font-weight: bold;}")

        self.patient_B_label = QLabel()
        self.patient_B_label.setObjectName("PatientBROILabel")
        self.patient_B_label.setMinimumHeight(50)
        self.patient_B_label.setAlignment(Qt.AlignCenter)
        self.patient_B_label.setStyleSheet(
            "QLabel { background-color : red; color : white; "
            "font-size: 15pt; font-weight: bold;}")

        self.transfer_roi_window_grid_layout.addWidget(self.patient_A_label, 0,
                                                       0)
        self.transfer_roi_window_grid_layout.addWidget(self.patient_B_label, 0,
                                                       2)

    def init_save_and_reset_button_layout(self):
        """
        Initialize the layout for save and reset buttons
        """
        self.reset_and_save_buttons_layout = QHBoxLayout()
        self.reset_button = QPushButton()
        self.reset_button.setObjectName("ResetButton")
        self.reset_button.clicked.connect(self.reset_clicked)
        self.save_button = QPushButton()
        self.save_button.setObjectName("SaveButton")
        self.save_button.setDisabled(True)
        self.save_button.clicked.connect(self.transfer_roi_clicked)

        self.reset_and_save_buttons_layout.setAlignment(Qt.AlignRight)
        self.reset_and_save_buttons_layout.addWidget(self.reset_button)
        self.reset_and_save_buttons_layout.addWidget(self.save_button)

        # Create a widget to hold Reset and Save buttons
        self.reset_and_save_button_central_widget = QWidget()
        self.reset_and_save_button_central_widget.setLayout(
            self.reset_and_save_buttons_layout)

        self.transfer_roi_window_grid_layout.addWidget(
            self.reset_and_save_button_central_widget, 3, 2)

    def add_suffix_checkbox_clicked(self):
        """
        This function is triggered when the add suffix checkbox is clicked
        """
        self.add_suffix = self.add_suffix_checkbox.isChecked()

    def init_patient_B_rois_to_A_layout(self):
        """
        Initialize the layout for transfer rois from B to A container
        """
        # Create scrolling area widget to contain the content.
        self.patient_B_rois_to_A_list_widget = QListWidget(self)
        self.transfer_roi_window_grid_layout \
            .addWidget(self.patient_B_rois_to_A_list_widget, 2, 0)
        self.patient_B_rois_to_A_list_widget.itemDoubleClicked.connect(
            self.patient_B_to_A_rois_double_clicked)

    def init_patient_A_rois_to_B_layout(self):
        """
        Initialize the layout for transfer rois from A to B container
        """
        self.patient_A_rois_to_B_list_widget = QListWidget(self)
        self.transfer_roi_window_grid_layout \
            .addWidget(self.patient_A_rois_to_B_list_widget, 1, 2)
        self.patient_A_rois_to_B_list_widget.itemDoubleClicked.connect(
            self.patient_A_to_B_rois_double_clicked)

    def init_patient_A_initial_roi_list(self):
        """
        Initialize the layout for patient A's roi list
        """
        self.patient_A_initial_rois_list_widget = QListWidget(self)
        self.patient_A_initial_rois_list_widget.itemDoubleClicked.connect(
            self.patient_A_initial_roi_double_clicked)
        for idx in self.fixed_image_initial_rois:
            roi_label = QListWidgetItem(
                self.fixed_image_initial_rois[idx]['name'])
            roi_label.setForeground(Qt.darkGreen)
            roi_label.setData(Qt.UserRole, self.fixed_image_initial_rois[idx])
            self.patient_A_initial_rois_list_widget.addItem(roi_label)
        self.transfer_roi_window_grid_layout.addWidget(
            self.patient_A_initial_rois_list_widget, 1, 0)

    def init_patient_B_initial_roi_list(self):
        """
        Initialize the layout for patient B's roi list
        """
        self.patient_B_initial_rois_list_widget = QListWidget(self)
        self.patient_B_initial_rois_list_widget.itemDoubleClicked.connect(
            self.patient_B_initial_roi_double_clicked)
        for idx in self.moving_image_initial_rois:
            roi_label = QListWidgetItem(
                self.moving_image_initial_rois[idx]['name'])
            roi_label.setForeground(Qt.red)
            roi_label.setData(Qt.UserRole, self.moving_image_initial_rois[idx])

            self.patient_B_initial_rois_list_widget.addItem(roi_label)
        self.transfer_roi_window_grid_layout.addWidget(
            self.patient_B_initial_rois_list_widget, 2, 2)

    def patient_A_to_B_rois_double_clicked(self, item):
        """
        This function is triggered when a roi in "A to B" list is
        double-clicked.
        """
        roi_to_remove = item.data(Qt.UserRole)
        to_delete_value = roi_to_remove['name']
        self.fixed_to_moving_rois.pop(to_delete_value)
        self.patient_A_rois_to_B_list_widget.clear()
        for key, value in self.fixed_to_moving_rois.items():
            roi_label = QListWidgetItem(value)
            roi_label.setForeground(Qt.red)
            roi_label.setData(Qt.UserRole, {'name': key})
            self.patient_A_rois_to_B_list_widget.addItem(roi_label)
        if self.transfer_list_is_empty():
            self.save_button.setDisabled(True)

    def patient_B_to_A_rois_double_clicked(self, item):
        """
        This function is triggered when a roi in "B to A" list is
        double-clicked.
        """
        roi_to_remove = item.data(Qt.UserRole)
        to_delete_value = roi_to_remove['name']
        self.moving_to_fixed_rois.pop(to_delete_value)
        self.patient_B_rois_to_A_list_widget.clear()
        for key, value in self.moving_to_fixed_rois.items():
            roi_label = QListWidgetItem(value)
            roi_label.setForeground(Qt.red)
            roi_label.setData(Qt.UserRole, {'name': key})
            self.patient_B_rois_to_A_list_widget.addItem(roi_label)
        if self.transfer_list_is_empty():
            self.save_button.setDisabled(True)

    def patient_A_initial_roi_double_clicked(self, item):
        """
        This function is triggered when a roi in patient A's roi list is
        double-clicked.
        """
        roi_to_add = item.data(Qt.UserRole)
        transferred_roi_name = roi_to_add['name']

        # If the clicked roi is already transferred, return
        if transferred_roi_name in self.fixed_to_moving_rois.keys():
            QMessageBox.about(self, "Transfer Failed",
                              "Chosen ROI has already been transferred!")
            return
        # Create a set to store all current roi names in target patient
        # including both initial rois name and added roi names so far
        patient_B_initial_roi_name_list = set()

        for item in self.fixed_to_moving_rois.values():
            patient_B_initial_roi_name_list.add(item)
        for idx in self.moving_image_initial_rois:
            patient_B_initial_roi_name_list.add(
                self.moving_image_initial_rois[idx]['name'])

        # Check if clicked roi name has duplicate
        # in patient B's initial roi names list
        if transferred_roi_name in patient_B_initial_roi_name_list:
            if self.add_suffix:
                transferred_roi_name = generate_non_duplicated_name(
                    transferred_roi_name, patient_B_initial_roi_name_list)
            else:
                QMessageBox.about(
                    self, "Transfer Failed", "Duplicated ROI name. "
                    "Please consider adding suffix.")
                return

        # Add clicked roi to transferred list
        self.fixed_to_moving_rois[roi_to_add['name']] = transferred_roi_name
        roi_label = QListWidgetItem(transferred_roi_name)
        roi_label.setForeground(Qt.red)
        roi_label.setData(Qt.UserRole, roi_to_add)
        self.patient_A_rois_to_B_list_widget.addItem(roi_label)
        self.save_button.setDisabled(False)

    def patient_B_initial_roi_double_clicked(self, item):
        """
        This function is triggered when a roi in patient B's roi list is
        double-clicked.
        """
        roi_to_add = item.data(Qt.UserRole)
        transferred_roi_name = roi_to_add['name']

        # If the clicked roi is already transferred, return
        if transferred_roi_name in self.moving_to_fixed_rois.keys():
            QMessageBox.about(self, "Transfer Failed",
                              "Chosen ROI has already been transferred!")
            return

        # Create a set to store all current roi names in target patient
        # including both initial rois name and added roi names so far
        patient_A_current_roi_name_list = set()

        for item in self.moving_to_fixed_rois.values():
            patient_A_current_roi_name_list.add(item)

        for idx in self.fixed_image_initial_rois:
            patient_A_current_roi_name_list.add(
                self.fixed_image_initial_rois[idx]['name'])

        # Check if clicked roi name has duplicate in
        # target patient's roi names list
        if transferred_roi_name in patient_A_current_roi_name_list:
            # if add suffix is ticked, iteratively try adding suffix
            # from _A to _Z, stop when no duplicate found
            if self.add_suffix:
                transferred_roi_name = generate_non_duplicated_name(
                    transferred_roi_name, patient_A_current_roi_name_list)
            else:
                QMessageBox.about(
                    self, "Transfer Failed", "Duplicated ROI name. "
                    "Please consider adding suffix.")
                return

        # Add clicked roi to transferred list
        self.moving_to_fixed_rois[roi_to_add['name']] = transferred_roi_name
        roi_label = QListWidgetItem(transferred_roi_name)
        roi_label.setForeground(Qt.red)
        roi_label.setData(Qt.UserRole, roi_to_add)
        self.patient_B_rois_to_A_list_widget.addItem(roi_label)
        self.save_button.setDisabled(False)

    def reset_clicked(self):
        """
        This function is triggered when reset button is clicked.
        """
        self.fixed_to_moving_rois.clear()
        self.moving_to_fixed_rois.clear()
        self.patient_A_rois_to_B_list_widget.clear()
        self.patient_B_rois_to_A_list_widget.clear()
        self.save_button.setDisabled(True)

    def transfer_list_is_empty(self):
        """
        This function is to check if the transfer list is empty
        """
        return len(self.fixed_to_moving_rois) == 0 \
               and len(self.moving_to_fixed_rois) == 0

    def save_clicked(self, interrupt_flag, progress_callback):
        """
        This function is triggered when the save button is clicked. It contains
        all steps in the ROI transferring process.

        :param interrupt_flag: interrupt flag to stop process
        :param progress_callback: signal that receives the current
                                  progress of the loading.
        """
        progress_callback.emit(("Converting images to sitk", 0))

        # check if interrupt flag is set
        if not check_interrupt_flag(interrupt_flag):
            return False

        rtss = self.patient_dict_container.get("dataset_rtss")

        # get sitk for the fixed image
        dicom_image = read_dicom_image_to_sitk(
            self.patient_dict_container.filepaths)

        if not check_interrupt_flag(interrupt_flag):
            return False

        # get array of roi indexes from sitk images
        rois_images_fixed = transform_point_set_from_dicom_struct(
            dicom_image,
            rtss,
            self.fixed_to_moving_rois.keys(),
            spacing_override=None,
            interrupt_flag=interrupt_flag)

        moving_rtss = self.moving_dict_container.get("dataset_rtss")

        if not check_interrupt_flag(interrupt_flag):
            return False

        # get sitk for the moving image
        moving_dicom_image = read_dicom_image_to_sitk(
            self.moving_dict_container.filepaths)

        if not check_interrupt_flag(interrupt_flag):
            return False

        # get array of roi indexes from sitk images
        progress_callback \
            .emit(("Retrieving ROIs from \nboth image sets", 20))

        # check if interrupt flag is set
        if not check_interrupt_flag(interrupt_flag):
            return False

        if moving_rtss:
            rois_images_moving = transform_point_set_from_dicom_struct(
                moving_dicom_image,
                moving_rtss,
                self.moving_to_fixed_rois.keys(),
                spacing_override=None,
                interrupt_flag=interrupt_flag)
        else:
            rois_images_moving = ([], [])

        if not check_interrupt_flag(interrupt_flag):
            return False

        tfm = self.moving_dict_container.get("tfm")

        progress_callback.emit(
            ("Transfering ROIs from moving \nto fixed image set", 40))

        # check if interrupt flag is set
        if not check_interrupt_flag(interrupt_flag):
            return False

        # transform roi from moving_dict to fixed_dict
        self.transfer_rois(self.moving_to_fixed_rois, tfm, dicom_image,
                           rois_images_moving, self.patient_dict_container)

        progress_callback.emit(
            ("Transfering ROIs from fixed \nto moving image set", 60))

        if not check_interrupt_flag(interrupt_flag):
            return False

        # transform roi from moving_dict to fixed_dict
        self.transfer_rois(self.fixed_to_moving_rois, tfm.GetInverse(),
                           moving_dicom_image, rois_images_fixed,
                           self.moving_dict_container)

        progress_callback.emit(("Saving ROIs to RTSS", 80))

        # check if interrupt flag is set
        if not check_interrupt_flag(interrupt_flag):
            return False
        progress_callback.emit(("Reloading window", 90))
        return True

    def transfer_roi_clicked(self):
        """
        telling progress window to start ROI transfer
        """
        self.progress_window.start(self.save_clicked)

    def onTransferRoiError(self, exception):
        """
        This function is triggered when there is an error in the
        ROI transferring process.

        :param exception: exception thrown
        """
        QMessageBox.about(self.progress_window, "Unable to transfer ROIs",
                          "Please check your image set and ROI data.")
        self.progress_window.close()

    def onTransferRoiFinished(self, result):
        """
        This function is triggered when ROI transferring process is finished.
        """
        # emit changed dataset to structure_modified function and
        # auto_save_roi function
        if result[0] is True:
            if len(self.fixed_to_moving_rois) > 0:
                self.signal_roi_transferred_to_moving_container.emit(
                    (self.moving_dict_container.get("dataset_rtss"), {
                        "transfer": None
                    }))
            if len(self.moving_to_fixed_rois) > 0:
                self.signal_roi_transferred_to_fixed_container.emit(
                    (self.patient_dict_container.get("dataset_rtss"), {
                        "transfer": None
                    }))
            self.progress_window.close()
            QMessageBox.about(self.transfer_roi_window_instance, "Saved",
                              "ROIs are successfully transferred!")
        else:
            QMessageBox.about(self.transfer_roi_window_instance, "Cancelled",
                              "ROIs Transfer is cancelled.")
        self.closeWindow()

    def transfer_rois(self, transfer_dict, tfm, reference_image,
                      original_roi_list, patient_dict_container):
        """
        Converting (transferring) ROIs from one image set to another and save
        the transferred rois to rtss.
        :param transfer_dict: dictionary of rois to be transfer.
        key is original roi names, value is the name after transferred.
        :param original_roi_list: tuple of sitk rois from the base image.
        :param tfm: the tfm that contains information for transferring rois
        :param reference_image: the reference (base) image
        :param patient_dict_container: container of the transfer image set.

        """
        for roi_name, new_roi_name in transfer_dict.items():
            for index, name in enumerate(original_roi_list[1]):
                if name == roi_name:
                    sitk_image = original_roi_list[0][index]
                    new_contour = apply_linear_transform(
                        input_image=sitk_image,
                        transform=tfm,
                        reference_image=reference_image,
                        is_structure=True)
                    contour = sitk.GetArrayViewFromImage(new_contour)
                    contours = np.transpose(contour.nonzero())
                    self.save_roi_to_patient_dict_container(
                        contours, new_roi_name, patient_dict_container)

    def save_roi_to_patient_dict_container(self, contours, roi_name,
                                           patient_dict_container):
        """
        Save the transferred ROI to the corresponding rtss.

        :param contours: np array of coordinates of the ROI to be saved.
        :param roi_name: name of the ROI to be saved
        :param patient_dict_container: container of the transfer image set.

        """
        pixels_coords_dict = {}
        slice_ids_dict = get_dict_slice_to_uid(patient_dict_container)
        total_slices = len(slice_ids_dict)
        for contour in contours:
            curr_slice_id = total_slices - contour[0]
            if curr_slice_id >= total_slices:
                curr_slice_id = 0
            if curr_slice_id not in pixels_coords_dict:
                pixels_coords_dict[curr_slice_id] = [
                    tuple([contour[2], contour[1]])
                ]
            else:
                pixels_coords_dict[curr_slice_id].append(
                    tuple([contour[2], contour[1]]))

        rois_to_save = {}
        for key in pixels_coords_dict.keys():
            coords = pixels_coords_dict[key]
            polygon_list = ROI.calculate_concave_hull_of_points(coords)
            if len(polygon_list) > 0:
                rois_to_save[key] = {
                    'ds': patient_dict_container.dataset[key],
                    'coords': polygon_list
                }
        roi_list = ROI.convert_hull_list_to_contours_data(
            rois_to_save, patient_dict_container)

        if len(roi_list) > 0:
            print("Saving ", roi_name)
            if isinstance(patient_dict_container, MovingDictContainer):
                new_rtss = ROI.create_roi(
                    patient_dict_container.get("dataset_rtss"),
                    roi_name,
                    roi_list,
                    rtss_owner="MOVING")
                self.moving_dict_container.set("dataset_rtss", new_rtss)
                self.moving_dict_container.set("rtss_modified", True)
            else:
                new_rtss = ROI.create_roi(
                    patient_dict_container.get("dataset_rtss"), roi_name,
                    roi_list)
                self.patient_dict_container.set("dataset_rtss", new_rtss)
                self.patient_dict_container.set("rtss_modified", True)

    def closeWindow(self):
        """
        function to close transfer roi window
        """
        self.close()
class BatchProcessISO2ROI(BatchProcess):
    """
    This class handles batch processing for the ISO2ROI process.
    Inherits from the BatchProcess class.
    """
    # Allowed classes for ISO2ROI
    allowed_classes = {
        # CT Image
        "1.2.840.10008.5.1.4.1.1.2": {
            "name": "ct",
            "sliceable": True
        },
        # RT Structure Set
        "1.2.840.10008.5.1.4.1.1.481.3": {
            "name": "rtss",
            "sliceable": False
        },
        # RT Dose
        "1.2.840.10008.5.1.4.1.1.481.2": {
            "name": "rtdose",
            "sliceable": False
        },
        # RT Plan
        "1.2.840.10008.5.1.4.1.1.481.5": {
            "name": "rtplan",
            "sliceable": False
        }
    }

    def __init__(self, progress_callback, interrupt_flag, patient_files):
        """
        Class initialiser function.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param patient_files: List of patient files.
        """
        # Call the parent class
        super(BatchProcessISO2ROI, self).__init__(progress_callback,
                                                  interrupt_flag,
                                                  patient_files)

        # Set class variables
        self.patient_dict_container = PatientDictContainer()
        self.required_classes = ('ct', 'rtdose', 'rtplan')
        self.ready = self.load_images(patient_files, self.required_classes)

    def start(self):
        """
        Goes through the steps of the ISO2ROI conversion.
        :return: True if successful, False if not.
        """
        # Stop loading
        if self.interrupt_flag.is_set():
            # TODO: convert print to logging
            print("Stopped ISO2ROI")
            self.patient_dict_container.clear()
            self.summary = "INTERRUPT"
            return False

        if not self.ready:
            self.summary = "SKIP"
            return False

        # Update progress
        self.progress_callback.emit(("Setting up...", 30))

        # Initialise
        InitialModel.create_initial_model_batch()

        # Stop loading
        if self.interrupt_flag.is_set():
            # TODO: convert print to logging
            print("Stopped ISO2ROI")
            self.patient_dict_container.clear()
            self.summary = "INTERRUPT"
            return False

        # Check if the dataset is complete
        self.progress_callback.emit(("Checking dataset...", 40))
        dataset_complete = ImageLoading.is_dataset_dicom_rt(
            self.patient_dict_container.dataset)

        # Create ISO2ROI object
        iso2roi = ISO2ROI()
        self.progress_callback.emit(("Performing ISO2ROI... ", 50))

        # Stop loading
        if self.interrupt_flag.is_set():
            # TODO: convert print to logging
            print("Stopped ISO2ROI")
            self.patient_dict_container.clear()
            self.summary = "INTERRUPT"
            return False

        if not dataset_complete:
            # Check if RT struct file is missing. If yes, create one and
            # add its data to the patient dict container. Otherwise
            # return
            if not self.patient_dict_container.get("file_rtss"):
                self.progress_callback.emit(("Generating RT Struct", 55))
                self.create_new_rtstruct(self.progress_callback)

        # Get isodose levels to turn into ROIs
        isodose_levels = \
            iso2roi.get_iso_levels(data_path('batch_isodoseRoi.csv'))

        # Stop loading
        if self.interrupt_flag.is_set():
            # TODO: convert print to logging
            print("Stopped ISO2ROI")
            self.patient_dict_container.clear()
            self.summary = "INTERRUPT"
            return False

        # Calculate boundaries
        self.progress_callback.emit(("Calculating boundaries...", 60))
        boundaries = iso2roi.calculate_isodose_boundaries(isodose_levels)

        # Return if boundaries could not be calculated
        if not boundaries:
            print("Boundaries could not be calculated.")
            self.summary = "ISO_NO_RX_DOSE"
            return False

        # Generate ROIs
        self.progress_callback.emit(("Generating ROIs...", 80))
        iso2roi.generate_roi(boundaries, self.progress_callback)

        # Save new RTSS
        self.progress_callback.emit(("Saving RT Struct...", 90))
        self.save_rtss()
        return True