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 BatchProcessingController:
    """
    This class is the controller for batch processing. It starts and
    ends processes, and controls the progress window.
    """
    def __init__(self):
        """
        Class initialiser function.
        """
        self.batch_path = ""
        self.dvh_output_path = ""
        self.pyrad_output_path = ""
        self.clinical_data_input_path = ""
        self.clinical_data_output_path = ""
        self.processes = []
        self.dicom_structure = None
        self.suv2roi_weights = None
        self.name_cleaning_options = None
        self.patient_files_loaded = False
        self.progress_window = ProgressWindow(None)
        self.timestamp = ""
        self.batch_summary = [{}, ""]

        # Threadpool for file loading
        self.threadpool = QThreadPool()
        self.interrupt_flag = threading.Event()

    def set_file_paths(self, file_paths):
        """
        Sets all the required paths
        :param file_paths: dict of directories
        """
        self.batch_path = file_paths.get('batch_path')
        self.dvh_output_path = file_paths.get('dvh_output_path')
        self.pyrad_output_path = file_paths.get('pyrad_output_path')
        self.clinical_data_input_path = \
            file_paths.get('clinical_data_input_path')
        self.clinical_data_output_path = \
            file_paths.get('clinical_data_output_path')

    def set_processes(self, processes):
        """
        Sets the selected processes
        :param processes: list of selected processes
        """
        self.processes = processes

    def set_suv2roi_weights(self, suv2roi_weights):
        """
        Function used to set suv2roi_weights.
        :param suv2roi_weights: Dictionary of patient IDs and patient weight
                                in grams.
        """
        self.suv2roi_weights = suv2roi_weights

    def start_processing(self):
        """
        Starts the batch process.
        """
        # Create new instance of ProgressWindow
        self.progress_window = ProgressWindow(None)

        # Connect callbacks
        self.progress_window.signal_error.connect(self.error_processing)
        self.progress_window.signal_loaded.connect(self.completed_processing)

        # Start performing processes on patient files
        self.progress_window.start(self.perform_processes)

    def load_patient_files(self, path, progress_callback,
                           search_complete_callback):
        """
        Load the patient files from directory.
        """
        # Set the interrup flag
        self.interrupt_flag.set()

        # Release the current thread, and create new threadpool
        self.threadpool.releaseThread()
        self.threadpool = QThreadPool()

        # Clear the interrupt flag
        self.interrupt_flag.clear()

        # Create new worker
        worker = Worker(DICOMDirectorySearch.get_dicom_structure,
                        path,
                        self.interrupt_flag,
                        progress_callback=True)

        # Connect callbacks
        worker.signals.result.connect(search_complete_callback)
        worker.signals.progress.connect(progress_callback)

        # Start the worker
        self.threadpool.start(worker)

    def set_dicom_structure(self, dicom_structure):
        """
        Function used to set dicom_structure
        :param dicom_structure: DICOMStructure
        """
        self.dicom_structure = dicom_structure

    def set_name_cleaning_options(self, options):
        """
        Set name cleaning options for batch ROI name cleaning.
        :param options: Dictionary of datasets, ROIs, and options for
                        cleaning the ROIs.
        """
        self.name_cleaning_options = options

    @staticmethod
    def get_patient_files(patient):
        """
        Get patient files.
        :param patient: patient data.
        :return: cur_patient_files, dictionary of classes and series'.
        """
        # Get files in patient
        cur_patient_files = {}
        for study in patient.studies.values():
            for series_type in study.series.values():
                for series in series_type.values():

                    image = list(series.images.values())[0]
                    class_id = image.class_id

                    if class_id not in cur_patient_files:
                        cur_patient_files[class_id] = []

                    cur_patient_files[class_id].append(series)

        return cur_patient_files

    def perform_processes(self, interrupt_flag, progress_callback=None):
        """
        Performs each selected process to each selected patient.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        """
        # Clear batch summary
        self.batch_summary = [{}, ""]

        # Dictionary of process names and functions
        self.process_functions = {
            "iso2roi": self.batch_iso2roi_handler,
            "suv2roi": self.batch_suv2roi_handler,
            "dvh2csv": self.batch_dvh2csv_handler,
            "pyrad2csv": self.batch_pyrad2csv_handler,
            "pyrad2pyrad-sr": self.batch_pyrad2pyradsr_handler,
            "csv2clinicaldata-sr": self.batch_csv2clinicaldatasr_handler,
            "clinicaldata-sr2csv": self.batch_clinicaldatasr2csv_handler,
            "roiname2fmaid": self.batch_roiname2fmaid_handler,
        }

        patient_count = len(self.dicom_structure.patients)
        cur_patient_num = 0
        self.timestamp = self.create_timestamp()

        # Loop through each patient
        for patient in self.dicom_structure.patients.values():
            # Stop loading
            if interrupt_flag.is_set():
                # TODO: convert print to logging
                print("Stopped Batch Processing")
                PatientDictContainer().clear()
                return False

            cur_patient_num += 1

            progress_callback.emit(
                ("Loading patient ({}/{}) .. ".format(cur_patient_num,
                                                      patient_count), 20))

            # Perform processes on patient
            for process in self.processes:
                if process == 'roinamecleaning':
                    continue
                self.process_functions[process](interrupt_flag,
                                                progress_callback, patient)

        # Perform batch ROI Name Cleaning on all patients
        if 'roinamecleaning' in self.processes:
            if self.name_cleaning_options:
                # Start the process
                process = \
                    BatchProcessROINameCleaning(progress_callback,
                                                interrupt_flag,
                                                self.name_cleaning_options)
                process.start()

                # Append process summary
                self.batch_summary[1] = process.summary
                progress_callback.emit(("Completed ROI Name Cleaning", 100))

        PatientDictContainer().clear()

    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)

    def batch_iso2roi_handler(self, interrupt_flag, progress_callback,
                              patient):
        """
        Handles creating, starting, and processing the results of batch
        ISO2ROI.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param patient: The patient to perform this process on.
        """
        # Get current patient files
        cur_patient_files = \
            BatchProcessingController.get_patient_files(patient)

        # Create and start process
        process = BatchProcessISO2ROI(progress_callback, interrupt_flag,
                                      cur_patient_files)
        success = process.start()

        # Add rtss to patient in case it is needed in future
        # processes
        if success:
            if PatientDictContainer().get("rtss_modified"):
                self.update_rtss(patient)
            reason = "SUCCESS"
        else:
            reason = process.summary

        # Append process summary
        if patient not in self.batch_summary[0].keys():
            self.batch_summary[0][patient] = {}
        self.batch_summary[0][patient]["iso2roi"] = reason
        progress_callback.emit(("Completed ISO2ROI", 100))

    def batch_suv2roi_handler(self, interrupt_flag, progress_callback,
                              patient):
        """
        Handles creating, starting, and processing the results of batch
        SUV2ROI.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param patient: The patient to perform this process on.
        """
        # Get patient files
        cur_patient_files = \
            BatchProcessingController.get_patient_files(patient)

        # Get patient weight
        if patient.patient_id in self.suv2roi_weights.keys():
            if self.suv2roi_weights[patient.patient_id] is None:
                patient_weight = None
            else:
                patient_weight = \
                    self.suv2roi_weights[patient.patient_id] * 1000
        else:
            patient_weight = None

        process = BatchProcessSUV2ROI(progress_callback, interrupt_flag,
                                      cur_patient_files, patient_weight)
        success = process.start()

        # Add rtss to patient in case it is needed in future
        # processes
        if success:
            if PatientDictContainer().get("rtss_modified"):
                self.update_rtss(patient)
            reason = "SUCCESS"
        else:
            reason = process.summary

        # Append process summary
        if patient not in self.batch_summary[0].keys():
            self.batch_summary[0][patient] = {}
        self.batch_summary[0][patient]["suv2roi"] = reason
        progress_callback.emit(("Completed SUV2ROI", 100))

    def batch_dvh2csv_handler(self, interrupt_flag, progress_callback,
                              patient):
        """
        Handles creating, starting, and processing the results of batch
        DVH2CSV.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param patient: The patient to perform this process on.
        """
        # Get current patient files
        cur_patient_files = \
            BatchProcessingController.get_patient_files(patient)

        # Create and start process
        process = BatchProcessDVH2CSV(progress_callback, interrupt_flag,
                                      cur_patient_files, self.dvh_output_path)
        process.set_filename('DVHs_' + self.timestamp + '.csv')
        success = process.start()

        # Set process summary
        if success:
            reason = "SUCCESS"
        else:
            reason = process.summary

        # Append process summary
        if patient not in self.batch_summary[0].keys():
            self.batch_summary[0][patient] = {}
        self.batch_summary[0][patient]['dvh2csv'] = reason
        progress_callback.emit(("Completed DVH2CSV", 100))

    def batch_pyrad2csv_handler(self, interrupt_flag, progress_callback,
                                patient):
        """
        Handles creating, starting, and processing the results of batch
        Pyrad2CSV.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param patient: The patient to perform this process on.
        """
        # Get current files
        cur_patient_files = \
            BatchProcessingController.get_patient_files(patient)
        process = BatchProcessPyRad2CSV(progress_callback, interrupt_flag,
                                        cur_patient_files,
                                        self.pyrad_output_path)
        process.set_filename('PyRadiomics_' + self.timestamp + '.csv')
        success = process.start()

        # Set summary message
        if success:
            reason = "SUCCESS"
        else:
            reason = process.summary

        # Append process summary
        if patient not in self.batch_summary[0].keys():
            self.batch_summary[0][patient] = {}
        self.batch_summary[0][patient]['pyrad2csv'] = reason
        progress_callback.emit(("Completed PyRad2CSV", 100))

    def batch_pyrad2pyradsr_handler(self, interrupt_flag, progress_callback,
                                    patient):
        """
        Handles creating, starting, and processing the results of batch
        PyRad2PyRad-SR.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param patient: The patient to perform this process on.
        """
        # Get current files
        cur_patient_files = \
            BatchProcessingController.get_patient_files(patient)
        process = BatchProcessPyRad2PyRadSR(progress_callback, interrupt_flag,
                                            cur_patient_files)
        success = process.start()

        # Set summary message
        if success:
            reason = "SUCCESS"
        else:
            reason = process.summary

        # Append process summary
        if patient not in self.batch_summary[0].keys():
            self.batch_summary[0][patient] = {}
        self.batch_summary[0][patient]['pyrad2pyradSR'] = reason
        progress_callback.emit(("Completed PyRad2PyRad-SR", 100))

    def batch_csv2clinicaldatasr_handler(self, interrupt_flag,
                                         progress_callback, patient):
        """
        Handles creating, starting, and processing the results of batch
        CSV2ClinicalData-SR.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param patient: The patient to perform this process on.
        """
        # Get current files
        cur_patient_files = \
            BatchProcessingController.get_patient_files(patient)
        process = \
            BatchProcessCSV2ClinicalDataSR(progress_callback, interrupt_flag,
                                           cur_patient_files,
                                           self.clinical_data_input_path)
        success = process.start()

        # Update summary
        if success:
            reason = "SUCCESS"
        else:
            reason = process.summary

        if patient not in self.batch_summary[0].keys():
            self.batch_summary[0][patient] = {}
        self.batch_summary[0][patient]["csv2clinicaldatasr"] = reason
        progress_callback.emit(("Completed CSV2ClinicalData-SR", 100))

    def batch_clinicaldatasr2csv_handler(self, interrupt_flag,
                                         progress_callback, patient):
        """
        Handles creating, starting, and processing the results of batch
        ClinicalData-SR2CSV.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param patient: The patient to perform this process on.
        """
        cur_patient_files = \
            BatchProcessingController.get_patient_files(patient)
        process = \
            BatchProcessClinicalDataSR2CSV(progress_callback, interrupt_flag,
                                           cur_patient_files,
                                           self.clinical_data_output_path)
        success = process.start()

        # Update summary
        if success:
            reason = "SUCCESS"
        else:
            reason = process.summary

        if patient not in self.batch_summary[0].keys():
            self.batch_summary[0][patient] = {}
        self.batch_summary[0][patient]["clinicaldatasr2csv"] = reason
        progress_callback.emit(("Completed ClinicalData-SR2CSV", 100))

    def batch_roiname2fmaid_handler(self, interrupt_flag, progress_callback,
                                    patient):
        """
        Handles creating, starting, and processing the results of batch
        ROIName2FMA-ID.
        :param interrupt_flag: A threading.Event() object that tells the
                               function to stop loading.
        :param progress_callback: A signal that receives the current
                                  progress of the loading.
        :param patient: The patient to perform this process on.
        """
        # Get patient files and start process
        cur_patient_files = \
            BatchProcessingController.get_patient_files(patient)
        process = \
            BatchProcessROIName2FMAID(progress_callback,
                                      interrupt_flag,
                                      cur_patient_files)
        process.start()

        # Append process summary
        reason = process.summary
        if patient not in self.batch_summary[0].keys():
            self.batch_summary[0][patient] = {}
        self.batch_summary[0][patient]["roiname2fmaid"] = reason
        progress_callback.emit(("Completed ROI Name to FMA ID", 100))

    def completed_processing(self):
        """
        Runs when batch processing has been completed.
        """
        self.progress_window.update_progress(("Processing complete!", 100))
        self.progress_window.close()

        # Create window to store summary info
        batch_summary_window = BatchSummaryWindow()
        batch_summary_window.set_summary_text(self.batch_summary)
        batch_summary_window.exec_()

    def error_processing(self):
        """
        Runs when there is an error during batch processing.
        """
        print("Error performing batch processing.")
        self.progress_window.close()
        return

    @classmethod
    def create_timestamp(cls):
        """
        Create a unique timestamp as a string.
        returns string
        """
        cur_time = datetime.datetime.now()
        year = cur_time.year
        month = cur_time.month
        day = cur_time.day
        hour = cur_time.hour
        min = cur_time.minute
        sec = cur_time.second

        time_stamp = str(year) + str(month) + str(day) + str(hour) + \
                     str(min) + str(sec)

        return time_stamp
Exemple #3
0
class UIOpenPatientWindow(object):
    patient_info_initialized = QtCore.Signal(object)

    def setup_ui(self, open_patient_window_instance):
        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_patient_window_instance.setObjectName("OpenPatientWindowInstance")
        open_patient_window_instance.setWindowIcon(window_icon)
        open_patient_window_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 = UIOpenPatientWindowDragAndDropEvent(
            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)  # Button doesn't show until a search commences
        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_changed)
        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/exit from the application
        self.open_patient_window_exit_button = QPushButton()
        self.open_patient_window_exit_button.setObjectName(
            "OpenPatientWindowExitButton")
        self.open_patient_window_exit_button.setSizePolicy(
            QSizePolicy(QSizePolicy.MinimumExpanding,
                        QSizePolicy.MinimumExpanding))
        self.open_patient_window_exit_button.resize(
            self.open_patient_window_stop_button.sizeHint().width(),
            self.open_patient_window_stop_button.sizeHint().height())
        self.open_patient_window_exit_button.setCursor(
            QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.open_patient_window_exit_button.clicked.connect(
            self.exit_button_clicked)
        self.open_patient_window_exit_button.setProperty(
            "QPushButtonClass", "fail-button")
        self.open_patient_window_patient_open_actions_horizontal_box.addWidget(
            self.open_patient_window_exit_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)  # Stretch the treeview out as far as possible
        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_patient_window_instance)
        # Set the central widget, ready for display
        open_patient_window_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_patient_window_instance.setStyleSheet(_stylesheet)

        QtCore.QMetaObject.connectSlotsByName(open_patient_window_instance)

    def retranslate_ui(self, open_patient_window_instance):
        _translate = QtCore.QCoreApplication.translate
        open_patient_window_instance.setWindowTitle(
            _translate("OpenPatientWindowInstance",
                       "OnkoDICOM - Select Patient"))
        self.open_patient_directory_prompt.setText(
            _translate(
                "OpenPatientWindowInstance",
                "Choose the path of the folder containing DICOM files to load Patient's details:"
            ))
        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",
                "Patient File directory shown below once file path chosen. Please select the file(s) you want to open:"
            ))
        self.open_patient_directory_result_label.setText(
            "The selected directory(s) above will be opened in the OnkoDICOM program."
        )
        self.open_patient_window_stop_button.setText(
            _translate("OpenPatientWindowInstance", "Stop Search"))
        self.open_patient_window_exit_button.setText(
            _translate("OpenPatientWindowInstance", "Exit"))
        self.open_patient_window_confirm_button.setText(
            _translate("OpenPatientWindowInstance", "Confirm"))

    def exit_button_clicked(self):
        QCoreApplication.exit(0)

    def scan_directory_for_patient(self):
        # Reset tree view header and last patient
        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()

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

        for patient_item in dicom_structure.get_tree_items_list():
            self.open_patient_window_patients_tree.addTopLevelItem(
                patient_item)

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

    def tree_item_changed(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.
        """
        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:
                self.last_patient.setCheckState(0, Qt.CheckState.Unchecked)
                self.last_patient.setSelected(False)
            self.last_patient = selected_patient

        # Get the types of all selected leaves
        self.selected_series_types = set()
        for checked_item in self.get_checked_leaves():
            series_type = checked_item.dicom_object.get_series_type()
            if type(series_type) == str:
                self.selected_series_types.add(series_type)
            else:
                self.selected_series_types.update(series_type)

        # Check the existence of IMAGE, RTSTRUCT and RTDOSE files
        if len(list({'CT', 'MR', 'PT'} & self.selected_series_types)) == 0:
            header = "Cannot proceed without an image file."
            self.open_patient_window_confirm_button.setDisabled(True)
        elif 'RTSTRUCT' not in self.selected_series_types:
            header = "DVH and Radiomics calculations are not available without a RTSTRUCT file."
        elif 'RTDOSE' not in self.selected_series_types:
            header = "DVH calculations are not available without a RTDOSE file."
        else:
            header = ""
        self.open_patient_window_patients_tree.setHeaderLabel(header)

        if len(list({'CT', 'MR', 'PT'} & self.selected_series_types)) != 0:
            self.open_patient_window_confirm_button.setDisabled(False)

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

        self.progress_window = ProgressWindow(
            self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
        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)
        self.progress_window.exec_()

    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.patient_info_initialized.emit(
                results[1])  # Emits the progress window.

    def on_loading_error(self, error_code):
        """
        Error handling for progress window.
        """
        if error_code == 0:
            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 error_code == 1:
            QMessageBox.about(
                self.progress_window, "Unable to open selection",
                "Selected files cannot be opened as they contain unsupported DICOM classes."
            )
            self.progress_window.close()

    def get_checked_leaves(self):
        """
        :return: A list of all QTreeWidgetItems in the QTreeWidget that are both leaves and checked.
        """
        checked_items = []

        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:
                        checked_items.append(child)

        recurse(self.open_patient_window_patients_tree.invisibleRootItem())
        return checked_items
Exemple #4
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()
class UIOpenPatientWindow(object):
    patient_info_initialized = QtCore.pyqtSignal(tuple)

    def setup_ui(self, main_window):
        stylesheet = open("src/res/stylesheet.qss").read()

        main_window.setObjectName("MainWindow")
        main_window.setStyleSheet(stylesheet)
        main_window.setFixedSize(700, 550)
        #main_window.setFixedSize(750, 570) # Window size change
        main_window.setWindowTitle("OnkoDICOM")
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("src/res/images/icon.ico"),
                       QtGui.QIcon.Normal, QtGui.QIcon.Off)
        main_window.setWindowIcon(icon)
        main_window.setAutoFillBackground(False)

        # 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()

        self.central_widget = QtWidgets.QWidget(main_window)
        self.central_widget.setObjectName("centralwidget")

        # self.grid_layout = QtWidgets.QGridLayout(self.central_widget)
        # self.grid_layout.setObjectName("gridLayout")

        # Frame
        self.frame = QtWidgets.QFrame(self.central_widget)
        self.frame.setGeometry(QtCore.QRect(40, 10, 611, 101))
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame.setObjectName("frame")

        self.path_text_browser = QtWidgets.QLineEdit(
            self.frame)  # changed to text edit instead of browser
        self.path_text_browser.setPlaceholderText("Your Path:")
        self.path_text_browser.setGeometry(QtCore.QRect(6, 40, 500, 26))
        size_policy = QtWidgets.QSizePolicy(
            QtWidgets.QSizePolicy.MinimumExpanding,
            QtWidgets.QSizePolicy.Ignored)
        size_policy.setHorizontalStretch(0)
        size_policy.setVerticalStretch(0)
        size_policy.setHeightForWidth(
            self.path_text_browser.sizePolicy().hasHeightForWidth())
        self.path_text_browser.setSizePolicy(size_policy)
        self.path_text_browser.setObjectName("pathTextBrowser")
        # self.grid_layout.addWidget(self.path_text_browser, 1, 1, 1, 1)

        self.choose_button = QtWidgets.QPushButton(self.frame)
        self.choose_button.setObjectName("chooseButton")
        self.choose_button.setGeometry(QtCore.QRect(521, 40, 90, 26))
        self.choose_button.setText("Choose")
        self.choose_button.setCursor(
            QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.choose_button.clicked.connect(self.choose_button_clicked)
        # self.grid_layout.addWidget(self.choose_button, 1, 2, 1, 1)

        self.choose_label = QtWidgets.QLabel(self.frame)
        self.choose_label.setGeometry(QtCore.QRect(6, 10, 571, 16))
        self.choose_label.setObjectName("choose_label")
        self.choose_label.setText(
            "<html><head/><body><p><span style=\" font-size:10pt;\">Choose the file path of a "
            "folder containing DICOM files to create the Patient file "
            "directory:</span></p></body></html>")
        # self.grid_layout.addWidget(self.choose_label, 0, 1, 1, 3)

        self.patient_file_label = QtWidgets.QLabel(self.frame)
        self.patient_file_label.setGeometry(QtCore.QRect(6, 76, 571, 16))
        self.patient_file_label.setObjectName("patient_file_label")
        self.patient_file_label.setText(
            "<html><head/><body><p><span style=\" font-size:10pt;\">Patient File "
            "directory shown below once file path chosen. Please select the file(s) you "
            "want to open:</span></p></body></html>")
        # self.grid_layout.addWidget(self.patient_file_label, 6, 1, 1, 3)

        #self.path_label = QtWidgets.QLabel(self.frame)
        #self.path_label.setGeometry(QtCore.QRect(10, 40, 47, 13))
        #self.path_label.setObjectName("path_label")
        #self.path_label.setText("<html><head/><body><p><span style=\" font-size:10pt;\">Path:</span></p></body></html>")
        #self.grid_layout.addWidget(self.path_label, 1, 0, 1, 1)

        # Frame 2
        self.frame_2 = QtWidgets.QFrame(self.central_widget)
        self.frame_2.setGeometry(QtCore.QRect(44, 130, 611, 411))
        self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame_2.setObjectName("frame_2")

        self.cancel_button = QtWidgets.QPushButton(self.frame_2)
        self.cancel_button.setObjectName("cancelButton")
        self.cancel_button.setGeometry(QtCore.QRect(414, 365, 90, 26))
        self.cancel_button.setText("Exit")
        self.cancel_button.setCursor(
            QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.cancel_button.clicked.connect(
            self.cancel_button_clicked)  # Signal Closing Application
        #self.grid_layout.addWidget(self.cancel_button, 9, 3, 1, 1)

        self.confirm_Button = QtWidgets.QPushButton(self.frame_2)
        self.confirm_Button.setObjectName("confirmButton")
        self.confirm_Button.setText("Confirm")
        self.confirm_Button.setCursor(
            QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.confirm_Button.setGeometry(QtCore.QRect(520, 365, 90, 26))
        self.confirm_Button.clicked.connect(self.confirm_button_clicked)
        #self.grid_layout.addWidget(self.confirm_Button, 9, 4, 1, 1)

        self.stop_button = QtWidgets.QPushButton(self.frame_2)
        self.stop_button.setObjectName("stopButton")
        self.stop_button.setText("Stop Search")
        self.stop_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
        self.stop_button.setGeometry(QtCore.QRect(6, 365, 120, 26))
        self.stop_button.clicked.connect(self.stop_button_clicked)
        self.stop_button.setVisible(
            False)  # Button doesn't show until a search commences

        self.selected_directory_label = QtWidgets.QLabel(self.frame_2)
        self.selected_directory_label.setObjectName("selected_directory_label")
        self.selected_directory_label.setGeometry(QtCore.QRect(
            6, 330, 541, 16))
        self.selected_directory_label.setText(
            "<html><head/><body><p><span style=\" font-size:10pt;\">The selected "
            "directory(s) above will be opened in the OnkoDICOM "
            "program.</span></p></body></html>")
        #self.grid_layout.addWidget(self.selected_directory_label, 9, 1, 1, 2)

        self.tree_widget = QtWidgets.QTreeWidget(self.frame_2)
        self.tree_widget.setGeometry(QtCore.QRect(0, 0, 611, 321))
        self.tree_widget.setHeaderHidden(True)
        self.tree_widget.setHeaderLabels([""])
        #self.grid_layout.addWidget(self.tree_widget, 7, 1, 1, 1)

        main_window.setCentralWidget(self.central_widget)
        self.status_bar = QtWidgets.QStatusBar(main_window)
        self.status_bar.setObjectName("statusbar")
        self.status_bar.setSizeGripEnabled(
            False)  # Remove expanding window option
        main_window.setStatusBar(self.status_bar)

        QtCore.QMetaObject.connectSlotsByName(main_window)

    def cancel_button_clicked(self):
        QCoreApplication.exit(0)

    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.path_text_browser.setText(self.filepath)

        # 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.tree_widget.clear()

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

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

            # Reveals the Stop Search button for the duration of the search
            self.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)
            worker.signals.result.connect(self.on_search_complete)
            worker.signals.progress.connect(self.search_progress)

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

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

    def search_progress(self, progress_update):
        """
        Current progress of the file search.
        """
        self.tree_widget.clear()
        self.tree_widget.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.choose_button.setEnabled(True)
        self.stop_button.setVisible(False)
        self.tree_widget.clear()

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

        for patient_item in dicom_structure.get_tree_items_list():
            self.tree_widget.addTopLevelItem(patient_item)

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

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

        if len(selected_files) > 0:
            self.progress_window = ProgressWindow(
                self,
                QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint)
            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)
            self.progress_window.exec_()
        else:
            QMessageBox.about(self, "Unable to open selection",
                              "No files selected.")

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

    def on_loading_error(self, error_code):
        """
        Error handling for progress window.
        """
        if error_code == 0:
            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 error_code == 1:
            QMessageBox.about(
                self.progress_window, "Unable to open selection",
                "Selected files cannot be opened as they contain unsupported DICOM classes."
            )
            self.progress_window.close()

    def get_checked_leaves(self):
        """
        :return: A list of all QTreeWidgetItems in the QTreeWidget that are both leaves and checked.
        """
        checked_items = []

        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:
                        checked_items.append(child)

        recurse(self.tree_widget.invisibleRootItem())
        return checked_items
Exemple #6
0
class UIMainWindow:
    """
    The central class responsible for initializing most of the values stored
    in the PatientDictContainer model and defining the visual layout of the
    main window of OnkoDICOM. No class has access to the attributes
    belonging to this class, except for the class's ActionHandler, which is
    used to trigger actions within the main window. Components of this class
    (i.e. QWidget child classes such as StructureTab, DicomView, DicomTree,
    etc.) should not be able to reference this class, and rather should
    exist independently and only be able to communicate with the
    PatientDictContainer model. If a component needs to communicate with
    another component, that should be accomplished by emitting signals
    within that components, and having the slots for those signals within
    this class (as demonstrated by the update_views() method of this class).
    If a class needs to trigger one of the actions defined in the
    ActionHandler, then the instance of the ActionHandler itself can safely
    be passed into the class.
    """
    pyradi_trigger = QtCore.Signal(str, dict, str)

    # Connect to GUIController
    image_fusion_main_window = QtCore.Signal()

    def setup_ui(self, main_window_instance):
        self.main_window_instance = main_window_instance
        self.call_class = MainPageCallClass()
        self.add_on_options_controller = AddOptions(self)

        ##########################################
        #  IMPLEMENTATION OF THE MAIN PAGE VIEW  #
        ##########################################
        if platform.system() == 'Darwin':
            self.stylesheet_path = "res/stylesheet.qss"
        else:
            self.stylesheet_path = "res/stylesheet-win-linux.qss"
        self.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.main_window_instance.setMinimumSize(1080, 700)
        self.main_window_instance.setObjectName("MainOnkoDicomWindowInstance")
        self.main_window_instance.setWindowIcon(window_icon)
        self.main_window_instance.setStyleSheet(self.stylesheet)

        self.setup_central_widget()
        self.setup_actions()

        # Create SUV2ROI object and connect signals
        self.suv2roi = SUV2ROI()
        self.suv2roi_progress_window = \
            ProgressWindow(self.main_window_instance,
                           QtCore.Qt.WindowTitleHint |
                           QtCore.Qt.WindowCloseButtonHint)
        self.suv2roi_progress_window.signal_loaded.connect(
            self.on_loaded_suv2roi)

    def setup_actions(self):
        if hasattr(self, 'toolbar'):
            self.main_window_instance.removeToolBar(self.toolbar)
        self.action_handler = ActionHandler(self)
        self.menubar = MenuBar(self.action_handler)
        self.main_window_instance.setMenuBar(self.menubar)
        self.toolbar = Toolbar(self.action_handler)
        self.main_window_instance.addToolBar(
            QtCore.Qt.TopToolBarArea, self.toolbar)
        self.main_window_instance.setWindowTitle("OnkoDICOM")

    def setup_central_widget(self):
        patient_dict_container = PatientDictContainer()
        self.central_widget = QtWidgets.QWidget()
        self.central_widget_layout = QVBoxLayout()

        self.patient_bar = PatientBar()

        splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal)

        # Left panel contains stuctures tab, isodoses tab,
        # and structure information
        self.left_panel = QtWidgets.QTabWidget()
        self.left_panel.setMinimumWidth(300)
        self.left_panel.setMaximumWidth(500)

        # Add structures tab to left panel
        if not hasattr(self, 'structures_tab'):
            self.structures_tab = StructureTab()
            self.structures_tab.request_update_structures.connect(
                self.update_views)
        else:
            self.structures_tab.update_ui()
        self.left_panel.addTab(self.structures_tab, "Structures")

        if patient_dict_container.has_modality("rtdose"):
            self.isodoses_tab = IsodoseTab()
            self.isodoses_tab.request_update_isodoses.connect(
                self.update_views)
            self.isodoses_tab.request_update_ui.connect(
                self.structures_tab.fixed_container_structure_modified)
            self.left_panel.addTab(self.isodoses_tab, "Isodoses")
        elif hasattr(self, 'isodoses_tab'):
            del self.isodoses_tab

        # Right panel contains the different tabs of DICOM view, DVH,
        # clinical data, DICOM tree
        self.right_panel = QtWidgets.QTabWidget()

        # Create a Dicom View containing single-slice and 3-slice views
        self.dicom_view = DicomStackedWidget(self.format_data)

        roi_color_dict = self.structures_tab.color_dict if hasattr(
            self, 'structures_tab') else None
        iso_color_dict = self.isodoses_tab.color_dict if hasattr(
            self, 'isodoses_tab') else None
        self.dicom_single_view = DicomAxialView(
            roi_color=roi_color_dict, iso_color=iso_color_dict)
        self.dicom_axial_view = DicomAxialView(
            is_four_view=True, roi_color=roi_color_dict, iso_color=iso_color_dict,
            metadata_formatted=True, cut_line_color=QtGui.QColor(255, 0, 0))
        self.dicom_sagittal_view = DicomSagittalView(
            roi_color=roi_color_dict, iso_color=iso_color_dict,
            cut_line_color=QtGui.QColor(0, 255, 0))
        self.dicom_coronal_view = DicomCoronalView(
            roi_color=roi_color_dict, iso_color=iso_color_dict,
            cut_line_color=QtGui.QColor(0, 0, 255))
        self.three_dimension_view = DicomView3D()

        # Rescale the size of the scenes inside the 3-slice views
        self.dicom_axial_view.zoom = INITIAL_FOUR_VIEW_ZOOM
        self.dicom_sagittal_view.zoom = INITIAL_FOUR_VIEW_ZOOM
        self.dicom_coronal_view.zoom = INITIAL_FOUR_VIEW_ZOOM
        self.dicom_axial_view.update_view(zoom_change=True)
        self.dicom_sagittal_view.update_view(zoom_change=True)
        self.dicom_coronal_view.update_view(zoom_change=True)

        self.dicom_four_views = QWidget()
        self.dicom_four_views_layout = QGridLayout()
        for i in range(2):
            self.dicom_four_views_layout.setColumnStretch(i, 1)
            self.dicom_four_views_layout.setRowStretch(i, 1)
        self.dicom_four_views_layout.addWidget(self.dicom_axial_view, 0, 0)
        self.dicom_four_views_layout.addWidget(self.dicom_sagittal_view, 0, 1)
        self.dicom_four_views_layout.addWidget(self.dicom_coronal_view, 1, 0)
        self.dicom_four_views_layout.addWidget(self.three_dimension_view, 1, 1)
        self.dicom_four_views.setLayout(self.dicom_four_views_layout)

        self.dicom_view.addWidget(self.dicom_four_views)
        self.dicom_view.addWidget(self.dicom_single_view)
        self.dicom_view.setCurrentWidget(self.dicom_single_view)

        # Add DICOM View to right panel as a tab
        self.right_panel.addTab(self.dicom_view, "DICOM View")

        # Add PETVT View to right panel as a tab
        self.pet_ct_tab = PetCtView()
        self.right_panel.addTab(self.pet_ct_tab, "PET/CT View")

        # Add DVH tab to right panel as a tab
        if patient_dict_container.has_modality("rtdose"):
            self.dvh_tab = DVHTab()
            self.right_panel.addTab(self.dvh_tab, "DVH")
        elif hasattr(self, 'dvh_tab'):
            del self.dvh_tab

        # Add DICOM Tree View tab
        self.dicom_tree = DicomTreeView()
        self.right_panel.addTab(self.dicom_tree, "DICOM Tree")

        # Connect SUV2ROI signal to handler function
        self.dicom_single_view.suv2roi_signal.connect(self.perform_suv2roi)

        # Add clinical data tab
        self.call_class.display_clinical_data(self.right_panel)

        splitter.addWidget(self.left_panel)
        splitter.addWidget(self.right_panel)

        # Create footer
        self.footer = QtWidgets.QWidget()
        self.create_footer()

        # Set layout
        self.central_widget_layout.addWidget(self.patient_bar)
        self.central_widget_layout.addWidget(splitter)
        self.central_widget_layout.addWidget(self.footer)

        self.central_widget.setLayout(self.central_widget_layout)
        self.main_window_instance.setCentralWidget(self.central_widget)

    def create_footer(self):
        self.footer.setFixedHeight(15)
        layout_footer = QtWidgets.QHBoxLayout(self.footer)
        layout_footer.setContentsMargins(0, 0, 0, 0)

        label_footer = QtWidgets.QLabel("@OnkoDICOM2021")
        label_footer.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignRight)

        layout_footer.addWidget(label_footer)

    def update_views(self, update_3d_window=False):
        """
        This function is a slot for signals to request the updating of the
        DICOM View and DVH tabs in order to reflect changes made by other
        components of the main window (for example, when a structure in the
        structures tab is selected, this method needs to be called in order
        for the DICOM view window to be updated to show the new region of
        interest.

        :param update_3d_window: a boolean to mark if 3d model
        needs to be updated
        """

        self.dicom_single_view.update_view()
        self.dicom_axial_view.update_view()
        self.dicom_coronal_view.update_view()
        self.dicom_sagittal_view.update_view()

        if update_3d_window:
            self.three_dimension_view.update_view()

        if hasattr(self, 'dvh_tab'):
            self.dvh_tab.update_plot()

        if hasattr(self, 'pet_ct_tab'):
            if self.pet_ct_tab.initialised:
                self.pet_ct_tab.update_view()

        if hasattr(self, 'image_fusion_view'):
            if self.image_fusion_view_axial is not None:
                self.image_fusion_single_view.update_view()
                self.image_fusion_view_axial.update_view()
                self.image_fusion_view_coronal.update_view()
                self.image_fusion_view_sagittal.update_view()

    def toggle_cut_lines(self):
        if self.dicom_axial_view.horizontal_view is None or \
                self.dicom_axial_view.vertical_view is None or \
                self.dicom_coronal_view.horizontal_view is None or \
                self.dicom_coronal_view.vertical_view is None or \
                self.dicom_sagittal_view.horizontal_view is None or \
                self.dicom_sagittal_view.vertical_view is None:
            self.dicom_axial_view.set_views(self.dicom_coronal_view,
                                            self.dicom_sagittal_view)
            self.dicom_coronal_view.set_views(self.dicom_axial_view,
                                              self.dicom_sagittal_view)
            self.dicom_sagittal_view.set_views(self.dicom_axial_view,
                                               self.dicom_coronal_view)
        else:
            self.dicom_axial_view.set_views(None, None)
            self.dicom_coronal_view.set_views(None, None)
            self.dicom_sagittal_view.set_views(None, None)

        if hasattr(self, 'image_fusion_view'):
            if self.image_fusion_view is not None:
                if self.image_fusion_view_axial.horizontal_view is None or \
                        self.image_fusion_view_axial.vertical_view is None or \
                        self.image_fusion_view_coronal.horizontal_view is None \
                        or self.image_fusion_view_coronal.vertical_view is None \
                        or \
                        self.image_fusion_view_sagittal.horizontal_view is None \
                        or \
                        self.image_fusion_view_sagittal.vertical_view is None:
                    self.image_fusion_view_axial.set_views(
                        self.image_fusion_view_coronal,
                        self.image_fusion_view_sagittal)
                    self.image_fusion_view_coronal.set_views(
                        self.image_fusion_view_axial,
                        self.image_fusion_view_sagittal)

                    self.image_fusion_view_sagittal.set_views(
                        self.image_fusion_view_axial,
                        self.image_fusion_view_coronal)
                else:
                    self.image_fusion_view_axial.set_views(None, None)
                    self.image_fusion_view_coronal.set_views(None, None)
                    self.image_fusion_view_sagittal.set_views(None, None)

    def zoom_in(self, is_four_view, image_reg_single, image_reg_four):
        """
        This function calls the zooming in function on the four view's views
        or the single view depending on what view is showing on screen.
        is_four_view: Whether the four view is showing
        """
        if is_four_view:
            self.dicom_axial_view.zoom_in()
            self.dicom_coronal_view.zoom_in()
            self.dicom_sagittal_view.zoom_in()
        else:
            self.dicom_single_view.zoom_in()

        if image_reg_single:
            self.image_fusion_single_view.zoom_in()

        if image_reg_four:
            self.image_fusion_view_axial.zoom_in()
            self.image_fusion_view_coronal.zoom_in()
            self.image_fusion_view_sagittal.zoom_in()

        if self.pet_ct_tab.initialised:
            self.pet_ct_tab.zoom_in()

    def zoom_out(self, is_four_view, image_reg_single, image_reg_four):
        """
        This function calls the zooming out function on the four view's
        views or the single view depending on what view is showing on screen.
        is_four_view: Whether the four view is showing
        """
        if is_four_view:
            self.dicom_axial_view.zoom_out()
            self.dicom_coronal_view.zoom_out()
            self.dicom_sagittal_view.zoom_out()
        else:
            self.dicom_single_view.zoom_out()

        if image_reg_single:
            self.image_fusion_single_view.zoom_out()

        if image_reg_four:
            self.image_fusion_view_axial.zoom_out()
            self.image_fusion_view_coronal.zoom_out()
            self.image_fusion_view_sagittal.zoom_out()

        if self.pet_ct_tab.initialised:
            self.pet_ct_tab.zoom_out()

    def format_data(self, size):
        """
        This function is used to update the meta data's font size and margin
        based on the height and width of the viewports.
        size: The size of the DicomStackedWidget
        """
        self.dicom_axial_view.format_metadata(size)

    def create_image_fusion_tab(self):
        """
        This function is used to create the tab for image fusion.
        Function checks if the moving dict container contains rtss to
        load rtss. Views are created and stacked into three window view.
        """
        # Set a flag for Zooming
        self.action_handler.has_image_registration_four = True

        # Instance of Moving Model
        moving_dict_container = MovingDictContainer()

        if moving_dict_container.has_modality("rtss"):
            if len(self.structures_tab.rois.items()) == 0:
                self.structures_tab.update_ui(moving=True)
            # else:
            # TODO: Display both ROIs in the same tab

        self.image_fusion_single_view \
            = ImageFusionAxialView()

        self.image_fusion_view = QStackedWidget()
        self.image_fusion_view_axial = ImageFusionAxialView(
            metadata_formatted=False,
            cut_line_color=QtGui.QColor(255, 0, 0))
        self.image_fusion_view_sagittal = ImageFusionSagittalView(
            cut_line_color=QtGui.QColor(0, 255, 0))
        self.image_fusion_view_coronal = ImageFusionCoronalView(
            cut_line_color=QtGui.QColor(0, 0, 255))
        self.image_fusion_roi_transfer_option_view = ROITransferOptionView(
            self.structures_tab.fixed_container_structure_modified,
            self.structures_tab.moving_container_structure_modified)

        # Rescale the size of the scenes inside the 3-slice views
        self.image_fusion_view_axial.zoom = INITIAL_FOUR_VIEW_ZOOM
        self.image_fusion_view_sagittal.zoom = INITIAL_FOUR_VIEW_ZOOM
        self.image_fusion_view_coronal.zoom = INITIAL_FOUR_VIEW_ZOOM
        self.image_fusion_view_axial.update_view(zoom_change=True)
        self.image_fusion_view_sagittal.update_view(zoom_change=True)
        self.image_fusion_view_coronal.update_view(zoom_change=True)

        self.image_fusion_four_views = QWidget()
        self.image_fusion_four_views_layout = QGridLayout()
        for i in range(2):
            self.image_fusion_four_views_layout.setColumnStretch(i, 1)
            self.image_fusion_four_views_layout.setRowStretch(i, 1)
        self.image_fusion_four_views_layout.addWidget(
            self.image_fusion_view_axial, 0, 0)
        self.image_fusion_four_views_layout.addWidget(
            self.image_fusion_view_sagittal, 0, 1)
        self.image_fusion_four_views_layout.addWidget(
            self.image_fusion_view_coronal, 1, 0)

        self.image_fusion_four_views_layout.addWidget(
            self.image_fusion_roi_transfer_option_view, 1, 1
        )
        self.image_fusion_four_views.setLayout(
            self.image_fusion_four_views_layout)

        self.image_fusion_view.addWidget(self.image_fusion_four_views)
        self.image_fusion_view.addWidget(self.image_fusion_single_view)
        self.image_fusion_view.setCurrentWidget(self.image_fusion_four_views)

        # Add Image Fusion Tab
        self.right_panel.addTab(self.image_fusion_view, "Image Fusion")
        self.right_panel.setCurrentWidget(self.image_fusion_view)

        # Update the Add On Option GUI
        self.add_on_options_controller.update_ui()

    def perform_suv2roi(self):
        """
        Performs the SUV2ROI process.
        """
        # Get patient weight - needs to run first as GUI cannot run in
        # threads, like the ProgressBar
        patient_dict_container = PatientDictContainer()
        dataset = patient_dict_container.dataset[0]
        self.suv2roi.get_patient_weight(dataset)
        if self.suv2roi.patient_weight is None:
            return

        # Start the SUV2ROI process
        self.suv2roi_progress_window.start(self.suv2roi.start_conversion)

    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()