class ProgramInstanceWidget(QWidget):
    def __init__(self, programInstance: ProgramInstance, uniqueRun):
        super().__init__()

        AppGlobals.Instance().onChipModified.connect(self.UpdateParameterItems)

        self.editingParameterVisibility = False

        self.programInstance = programInstance
        self.uniqueRun = uniqueRun

        self._programNameWidget = QLabel()

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.runButton = QPushButton("Run")
        self.runButton.clicked.connect(self.RunProgram)

        self._stopButton = QPushButton("Stop")
        self._stopButton.clicked.connect(self.StopProgram)

        self.parameterItems: List[ProgramParameterItem] = []

        self._parametersLayout = QVBoxLayout()

        layout.addWidget(self._programNameWidget)
        layout.addLayout(self._parametersLayout)
        layout.addWidget(self.runButton)
        layout.addWidget(self._stopButton)
        timer = QTimer(self)
        timer.timeout.connect(self.UpdateInstanceView)
        timer.start(30)

        self.UpdateInstanceView()
        self.UpdateParameterItems()

    def UpdateParameterItems(self):
        [item.deleteLater() for item in self.parameterItems]
        self.parameterItems = []

        for parameter in self.programInstance.program.parameters:
            newItem = ProgramParameterItem(parameter, self.programInstance)
            self._parametersLayout.addWidget(newItem)
            self.parameterItems.append(newItem)

    def UpdateInstanceView(self):
        self._programNameWidget.setText(self.programInstance.program.name)
        if self.uniqueRun:
            self.runButton.setVisible(
                not AppGlobals.ProgramRunner().IsRunning(self.programInstance))
            self._stopButton.setVisible(AppGlobals.ProgramRunner().IsRunning(
                self.programInstance))
        else:
            self.runButton.setVisible(True)
            self._stopButton.setVisible(False)

        for item in self.parameterItems:
            item.UpdateFields()

    def UpdateParameterVisibility(self):
        for item in self.parameterItems:
            if self.editingParameterVisibility or self.programInstance.parameterVisibility[
                    item.parameter]:
                item.setVisible(True)
            else:
                item.setVisible(False)
            item.visibilityToggle.setVisible(self.editingParameterVisibility)

        self.adjustSize()

    def RunProgram(self):
        if self.uniqueRun:
            AppGlobals.ProgramRunner().Run(self.programInstance, None)
        else:
            AppGlobals.ProgramRunner().Run(self.programInstance.Clone(), None)

    def StopProgram(self):
        AppGlobals.ProgramRunner().Stop(self.programInstance)
Exemple #2
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 #3
0
class UIImageFusionWindow(object):
    image_fusion_info_initialized = QtCore.Signal(object)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        QtCore.QMetaObject.connectSlotsByName(
            open_image_fusion_select_instance)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return True

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

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

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

        recurse(root)
        return checked_items

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

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

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

    def on_loading_error(self, exception):
        """
        Error handling for progress window.
        """
        if type(exception[1]) == ImageLoading.NotRTSetError:
            QMessageBox.about(self.progress_window,
                              "Unable to open selection",
                              "Selected files cannot be opened as they are not"
                              " a DICOM-RT set.")
            self.progress_window.close()
        elif type(exception[1]) == ImageLoading.NotAllowedClassError:
            QMessageBox.about(self.progress_window,
                              "Unable to open selection",
                              "Selected files cannot be opened as they contain"
                              " unsupported DICOM classes.")
            self.progress_window.close()
Exemple #4
0
class MainWindow(QMainWindow):
    """Main application window"""
    def __init__(self) -> None:
        QMainWindow.__init__(self)
        self.setSizePolicy(
            QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum))
        self.setMaximumSize(QSize(1920, 1080))
        self.setStyleSheet("padding: 0px; margin: 0px;")
        self.setIconSize(QSize(32, 32))
        self.setWindowTitle("BossyBot 2000 - Image Tagger")
        self.setWindowIcon(self.load_icon(icon))

        self.menubar = QMenuBar(self)
        self.menubar.setSizePolicy(EXP_MAX)
        self.menubar.setMaximumSize(QSize(INFINITE, 30))
        self.menu_file = QMenu('File', self.menubar)
        self.menu_options = QMenu('Options', self.menubar)
        self.menu_help = QMenu('Help', self.menubar)
        self.menubar.addAction(self.menu_file.menuAction())
        self.menubar.addAction(self.menu_options.menuAction())
        self.menubar.addAction(self.menu_help.menuAction())
        self.open = QAction('Open', self)
        self.menu_file.addAction(self.open)
        self.open.triggered.connect(self.open_file)
        self.exit_button = QAction('Exit', self)
        self.exit_button.triggered.connect(lambda: sys.exit(0),
                                           Qt.QueuedConnection)
        self.menu_file.addAction(self.exit_button)
        self.setMenuBar(self.menubar)

        self.previous_button = QAction(self.load_icon(previous), '<<', self)
        self.next_button = QAction(self.load_icon(next_icon), '>>', self)
        self.rotate_left_button = QAction(self.load_icon(left), '', self)
        self.rotate_right_button = QAction(self.load_icon(right), '', self)
        self.play_button = QAction(self.load_icon(play), '', self)
        self.play_button.setCheckable(True)
        self.delete_button = QAction(self.load_icon(delete), '', self)
        self.reload_button = QAction(self.load_icon(reload), '', self)
        self.mirror_button = QAction('Mirror', self)
        self.actual_size_button = QAction('Actual Size', self)
        self.browser_button = QAction('Browser', self)
        self.browser_button.setCheckable(True)
        self.browser_button.setChecked(True)
        self.crop_button = QAction('Crop', self)
        self.crop_button.setCheckable(True)

        self.toolbuttons = {
            self.rotate_left_button: {
                'shortcut':
                ',',
                'connect':
                lambda: self.pixmap.setRotation(self.pixmap.rotation() - 90)
            },
            self.rotate_right_button: {
                'shortcut':
                '.',
                'connect':
                lambda: self.pixmap.setRotation(self.pixmap.rotation() + 90)
            },
            self.delete_button: {
                'shortcut': 'Del',
                'connect': self.delete
            },
            self.previous_button: {
                'shortcut': 'Left',
                'connect': self.previous
            },
            self.play_button: {
                'shortcut': 'Space',
                'connect': self.play
            },
            self.next_button: {
                'shortcut': 'Right',
                'connect': self.next
            },
            self.reload_button: {
                'shortcut': 'F5',
                'connect': self.reload
            }
        }

        self.toolbar = QToolBar(self)
        self.toolbar.setSizePolicy(EXP_MAX)
        self.toolbar.setMaximumSize(QSize(INFINITE, 27))
        for _ in (self.browser_button, self.crop_button, self.mirror_button,
                  self.actual_size_button):
            self.toolbar.addAction(_)
        self.addToolBar(Qt.TopToolBarArea, self.toolbar)

        for button in self.toolbuttons:
            button.setShortcut(self.toolbuttons[button]['shortcut'])
            button.triggered.connect(self.toolbuttons[button]['connect'])
            self.toolbar.addAction(button)

        self.centralwidget = QWidget(self)
        self.centralwidget.setSizePolicy(EXP_EXP)
        self.setCentralWidget(self.centralwidget)
        self.grid = QGridLayout(self.centralwidget)

        self.media = QGraphicsScene(self)
        self.media.setItemIndexMethod(QGraphicsScene.NoIndex)
        self.media.setBackgroundBrush(QBrush(Qt.black))
        self.view = MyView(self.media, self)
        self.view.setSizePolicy(EXP_EXP)
        self.media.setSceneRect(0, 0, self.view.width(), self.view.height())
        self.grid.addWidget(self.view, 0, 0, 1, 1)

        self.frame = QFrame(self.centralwidget)
        self.frame.setSizePolicy(
            QSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding))
        self.frame.setMinimumSize(QSize(325, 500))
        self.frame.setStyleSheet(
            "QFrame { border: 4px inset #222; border-radius: 10; }")

        self.layout_widget = QWidget(self.frame)
        self.layout_widget.setGeometry(QRect(0, 400, 321, 91))
        self.layout_widget.setContentsMargins(15, 15, 15, 15)

        self.grid2 = QGridLayout(self.layout_widget)
        self.grid2.setContentsMargins(0, 0, 0, 0)

        self.save_button = QPushButton('Yes (Save)', self.layout_widget)
        self.save_button.setSizePolicy(FIX_FIX)
        self.save_button.setMaximumSize(QSize(120, 26))
        self.save_button.setVisible(False)
        self.grid2.addWidget(self.save_button, 1, 0, 1, 1)

        self.no_save_button = QPushButton('No (Reload)', self.layout_widget)
        self.no_save_button.setSizePolicy(FIX_FIX)
        self.no_save_button.setMaximumSize(QSize(120, 26))
        self.no_save_button.setVisible(False)
        self.grid2.addWidget(self.no_save_button, 1, 1, 1, 1)

        self.label = QLabel("Current image modified, save it?",
                            self.layout_widget)
        self.label.setSizePolicy(FIX_FIX)
        self.label.setMaximumSize(QSize(325, 60))
        self.label.setVisible(False)
        self.label.setAlignment(Qt.AlignCenter)
        self.grid2.addWidget(self.label, 0, 0, 1, 2)

        self.layout_widget = QWidget(self.frame)
        self.layout_widget.setGeometry(QRect(0, 0, 321, 213))

        self.ass = QRadioButton('Ass', self.layout_widget)
        self.ass_exposed = QRadioButton('Ass (exposed)', self.layout_widget)
        self.ass_reset = QRadioButton(self.frame)
        self.ass_group = QButtonGroup(self)

        self.breasts = QRadioButton('Breasts', self.layout_widget)
        self.breasts_exposed = QRadioButton('Breasts (exposed)',
                                            self.layout_widget)
        self.breasts_reset = QRadioButton(self.frame)
        self.breasts_group = QButtonGroup(self)

        self.pussy = QRadioButton('Pussy', self.layout_widget)
        self.pussy_exposed = QRadioButton('Pussy (exposed)',
                                          self.layout_widget)
        self.pussy_reset = QRadioButton(self.frame)
        self.pussy_group = QButtonGroup(self)

        self.fully_clothed = QRadioButton('Fully Clothed', self.layout_widget)
        self.fully_nude = QRadioButton('Fully Nude', self.layout_widget)
        self.nudity_reset = QRadioButton(self.frame)
        self.nudity = QButtonGroup(self)

        self.smiling = QRadioButton('Smiling', self.layout_widget)
        self.glaring = QRadioButton('Glaring', self.layout_widget)
        self.expression_reset = QRadioButton(self.frame)
        self.expression = QButtonGroup(self)

        self.grid3 = QGridLayout(self.layout_widget)
        self.grid3.setVerticalSpacing(15)
        self.grid3.setContentsMargins(0, 15, 0, 0)

        self.radios = {
            self.ass: {
                'this': 'ass',
                'that': 'ass_exposed',
                'group': self.ass_group,
                'reset': self.ass_reset,
                'grid': (0, 0, 1, 1)
            },
            self.ass_exposed: {
                'this': 'ass_exposed',
                'that': 'ass',
                'group': self.ass_group,
                'reset': self.ass_reset,
                'grid': (0, 1, 1, 1)
            },
            self.breasts: {
                'this': 'breasts',
                'that': 'breasts_exposed',
                'group': self.breasts_group,
                'reset': self.breasts_reset,
                'grid': (1, 0, 1, 1)
            },
            self.breasts_exposed: {
                'this': 'breasts_exposed',
                'that': 'breasts',
                'group': self.breasts_group,
                'reset': self.breasts_reset,
                'grid': (1, 1, 1, 1)
            },
            self.pussy: {
                'this': 'pussy',
                'that': 'pussy_exposed',
                'group': self.pussy_group,
                'reset': self.pussy_reset,
                'grid': (2, 0, 1, 1)
            },
            self.pussy_exposed: {
                'this': 'pussy_exposed',
                'that': 'pussy',
                'group': self.pussy_group,
                'reset': self.pussy_reset,
                'grid': (2, 1, 1, 1)
            },
            self.fully_clothed: {
                'this': 'fully_clothed',
                'that': 'fully_nude',
                'group': self.nudity,
                'reset': self.nudity_reset,
                'grid': (3, 0, 1, 1)
            },
            self.fully_nude: {
                'this': 'fully_nude',
                'that': 'fully_clothed',
                'group': self.nudity,
                'reset': self.nudity_reset,
                'grid': (3, 1, 1, 1)
            },
            self.smiling: {
                'this': 'smiling',
                'that': 'glaring',
                'group': self.expression,
                'reset': self.expression_reset,
                'grid': (4, 0, 1, 1)
            },
            self.glaring: {
                'this': 'glaring',
                'that': 'smiling',
                'group': self.expression,
                'reset': self.expression_reset,
                'grid': (4, 1, 1, 1)
            },
        }

        for radio in self.radios:
            radio.setSizePolicy(FIX_FIX)
            radio.setMaximumSize(QSize(150, 22))
            self.radios[radio]['reset'].setGeometry(QRect(0, 0, 0, 0))
            self.grid3.addWidget(radio, *self.radios[radio]['grid'])
            if self.radios[radio]['group'] != self.nudity:
                radio.toggled.connect(
                    lambda x=_, y=radio: self.annotate(self.radios[y]['this']))
            self.radios[radio]['group'].addButton(radio)
            self.radios[radio]['group'].addButton(self.radios[radio]['reset'])

        self.save_tags_button = QPushButton('Save Tags', self.layout_widget)
        self.save_tags_button.setSizePolicy(FIX_FIX)
        self.save_tags_button.setMaximumSize(QSize(120, 26))
        self.grid3.addWidget(self.save_tags_button, 5, 1, 1, 1)

        self.grid.addWidget(self.frame, 0, 1, 1, 1)

        self.browse_bar = QLabel(self.centralwidget)
        self.browse_bar.setSizePolicy(EXP_FIX)
        self.browse_bar.setMinimumSize(QSize(0, 100))
        self.browse_bar.setMaximumSize(QSize(INFINITE, 100))
        self.browse_bar.setStyleSheet("background: #000;")
        self.browse_bar.setAlignment(Qt.AlignCenter)
        self.h_box2 = QHBoxLayout(self.browse_bar)
        self.h_box2.setContentsMargins(4, 0, 0, 0)

        self.grid.addWidget(self.browse_bar, 1, 0, 1, 2)

        hiders = [
            self.no_save_button.clicked, self.save_button.clicked,
            self.reload_button.triggered
        ]
        for hider in hiders:
            hider.connect(self.save_button.hide)
            hider.connect(self.no_save_button.hide)
            hider.connect(self.label.hide)
        showers = [
            self.mirror_button.triggered, self.rotate_right_button.triggered,
            self.rotate_left_button.triggered
        ]
        for shower in showers:
            shower.connect(self.save_button.show)
            shower.connect(self.no_save_button.show)
            shower.connect(self.label.show)

        self.no_save_button.clicked.connect(self.reload)
        self.browser_button.toggled.connect(self.browse_bar.setVisible)

        self.play_button.toggled.connect(lambda: self.frame.setVisible(
            (True, False)[self.frame.isVisible()]))
        self.reload_button.triggered.connect(self.reload)
        self.mirror_button.triggered.connect(lambda: self.pixmap.setScale(-1))
        self.save_button.clicked.connect(self.save_image)
        self.play_button.toggled.connect(
            lambda: self.browser_button.setChecked(
                (True, False)[self.browse_bar.isVisible()]))
        self.crop_button.toggled.connect(self.view.reset)
        self.actual_size_button.triggered.connect(self.actual_size)
        self.browser_button.triggered.connect(self.browser)
        self.save_tags_button.clicked.connect(self.save_tags)
        self.view.got_rect.connect(self.set_rect)

        self.crop_rect = QRect(QPoint(0, 0), QSize(0, 0))
        self.dir_now = os.getcwd()
        self.files = []
        self.index = 0
        self.refresh_files()
        self.pixmap_is_scaled = False
        self.pixmap = QGraphicsPixmapItem()
        self.active_tag = ''
        self.reset_browser = False
        self.txt = PngInfo()

    def set_rect(self, rect: tuple[QPointF, QPointF]):
        """Converts the crop rectangle to a QRect after a crop action"""
        self.crop_rect = QRect(rect[0].toPoint(), rect[1].toPoint())

    def keyPressEvent(self, event: QKeyEvent):  # pylint: disable=invalid-name;
        """Keyboard event handler."""
        if event.key() == Qt.Key_Escape and self.play_button.isChecked():
            self.play_button.toggle()
            self.browser_button.setChecked((True, False)[self.reset_browser])
        elif (event.key() in [16777220, 16777221]
              and self.view.g_rect.rect().width() > 0):
            self.view.got_rect.emit((self.view.g_rect.rect().topLeft(),
                                     self.view.g_rect.rect().bottomRight()))
            if self.view.g_rect.pen().color() == Qt.red:
                new_pix = self.pixmap.pixmap().copy(self.crop_rect)
                if self.pixmap_is_scaled:
                    new_pix = new_pix.transformed(
                        self.view.transform().inverted()[0],
                        Qt.SmoothTransformation)
                self.update_pixmap(new_pix)
            elif self.view.g_rect.pen().color() == Qt.magenta:
                self.annotate_rect()
                self.view.annotation = False
            for _ in (self.label, self.save_button, self.no_save_button):
                _.show()
            self.view.reset()

    def play(self):
        """Starts a slideshow."""
        if self.play_button.isChecked():
            if self.browser_button.isChecked():
                self.reset_browser = True
            else:
                self.reset_browser = False
            QTimer.singleShot(3000, self.play)
            self.next()

    def _yield_radio(self):
        """Saves code connecting signals from all the radio buttons."""
        yield from self.radios.keys().__str__()

    def load_icon(self, icon_file):
        """Loads an icon from Base64 encoded strings in icons.py."""
        pix = QPixmap()
        pix.loadFromData(icon_file)
        return QIcon(pix)

    def open_file(self, file: str) -> None:
        """
        Open an image file and display it.

        :param file: The filename of the image to open
        """
        if not os.path.isfile(file):
            file = QFileDialog(self, self.dir_now,
                               self.dir_now).getOpenFileName()[0]
            self.dir_now = os.path.dirname(file)
            self.refresh_files()
        for i, index_file in enumerate(self.files):
            if file.split('/')[-1] == index_file:
                self.index = i
        self.view.setTransform(QTransform())
        self.update_pixmap(QPixmap(file))
        self.browser()
        self.load_tags()

    def refresh_files(self) -> list[str]:
        """Updates the file list when the directory is changed.
        Returns a list of image files available in the current directory."""
        files = os.listdir(self.dir_now)
        self.files = [
            file for file in sorted(files, key=lambda x: x.lower())
            if file.endswith((".png", ".jpg", ".gif", ".bmp", ".jpeg"))
        ]

    def next(self) -> None:
        """Opens the next image in the file list."""
        self.index = (self.index + 1) % len(self.files)
        self.reload()

    def previous(self) -> None:
        """Opens the previous image in the file list."""
        self.index = (self.index + (len(self.files) - 1)) % len(self.files)
        self.reload()

    def save_image(self) -> None:
        """
        Save the modified image file.  If the current pixmap has been
        scaled, we need to load a non-scaled pixmap from the original file and
        re-apply the transformations that have been performed to prevent it
        from being saved as the scaled-down image.
        """
        if self.pixmap_is_scaled:
            rotation = self.pixmap.rotation()
            mirror = self.pixmap.scale() < 0
            pix = QPixmap(self.files[self.index])
            pix = pix.transformed(QTransform().rotate(rotation))
            if mirror:
                pix = pix.transformed(QTransform().scale(-1, 1))
            pix.save(self.files[self.index], quality=-1)
        else:
            self.pixmap.pixmap().save(self.files[self.index], quality=-1)
        self.save_tags()

    def delete(self) -> None:
        """Deletes the current image from the file system."""
        with suppress(OSError):
            os.remove(f"{self.dir_now}/{self.files.pop(self.index)}")
        self.refresh_files()

    def reload(self) -> None:
        """Reloads the current pixmap; used to update the screen when the
        current file is changed."""
        self.open_file(f"{self.dir_now}/{self.files[self.index]}")

    def annotate(self, tag):
        """Starts an annotate action"""
        self.txt = PngInfo()
        self.view.annotation = True
        self.active_tag = tag
        self.view.reset()

    def wheelEvent(self, event: QWheelEvent) -> None:  # pylint: disable=invalid-name
        """With Ctrl depressed, zoom the current image, otherwise fire the
        next/previous functions."""
        modifiers = QApplication.keyboardModifiers()
        if event.angleDelta().y() == 120 and modifiers == Qt.ControlModifier:
            self.view.scale(0.75, 0.75)
        elif event.angleDelta().y() == 120:
            self.previous()
        elif event.angleDelta().y(
        ) == -120 and modifiers == Qt.ControlModifier:
            self.view.scale(1.25, 1.25)
        elif event.angleDelta().y() == -120:
            self.next()

    def actual_size(self) -> None:
        """Display the current image at its actual size, rather than scaled to
        fit the viewport."""
        self.update_pixmap(QPixmap(self.files[self.index]), False)
        self.view.setDragMode(QGraphicsView.ScrollHandDrag)

    def mousePressEvent(self, event: QMouseEvent) -> None:  # pylint: disable=invalid-name
        """Event handler for mouse button presses."""
        if event.button() == Qt.MouseButton.ForwardButton:
            self.next()
        elif event.button() == Qt.MouseButton.BackButton:
            self.previous()

    def update_pixmap(self, new: QPixmap, scaled: bool = True) -> None:
        """
        Updates the currently displayed image.

        :param new: The new `QPixmap` to be displayed.
        :param scaled: If False, don't scale the image to fit the viewport.
        """
        self.pixmap_is_scaled = scaled
        self.media.clear()
        self.pixmap = self.media.addPixmap(new)
        self.pixmap.setTransformOriginPoint(
            self.pixmap.boundingRect().width() / 2,
            self.pixmap.boundingRect().height() / 2)
        if scaled and (new.size().width() > self.view.width()
                       or new.size().height() > self.view.height()):
            self.view.fitInView(self.pixmap, Qt.KeepAspectRatio)
        self.media.setSceneRect(self.pixmap.boundingRect())

    def annotate_rect(self):
        """Creates image coordinate annotation data."""
        self.txt.add_itxt(
            f'{str(self.active_tag)}-rect',
            f'{str(self.crop_rect.x())}, {str(self.crop_rect.y())}, {str(self.crop_rect.width())}, {str(self.crop_rect.height())}'
        )

    def browser(self):
        """Slot function to initialize image thumbnails for the
        'browse mode.'"""
        while self.h_box2.itemAt(0):
            self.h_box2.takeAt(0).widget().deleteLater()
        index = (self.index + (len(self.files) - 2)) % len(self.files)
        for i, file in enumerate(self.files):
            file = self.dir_now + '/' + self.files[index]
            label = ClickableLabel(self, file)
            self.h_box2.addWidget(label)
            pix = QPixmap(file)
            if (pix.size().width() > self.browse_bar.width() / 5
                    or pix.size().height() > 100):
                pix = pix.scaled(self.browse_bar.width() / 5, 100,
                                 Qt.KeepAspectRatio)
            label.setPixmap(pix)
            index = (index + 1) % len(self.files)
            if i == 4:
                break

    def save_tags(self):
        """Save tags for currently loaded image into its iTxt data."""
        file = self.files[self.index]
        img = Image.open(file)
        img.load()
        for key, value, in img.text.items():
            self.txt.add_itxt(key, value)
        for key in self.radios:
            if key.isChecked():
                self.txt.add_itxt(self.radios[key]['this'], 'True')
                self.txt.add_itxt(self.radios[key]['that'], 'False')
        img.save(file, pnginfo=self.txt)

    def load_tags(self):
        """Load tags from iTxt data."""
        for radio in self.radios:
            if radio.isChecked():
                self.radios[radio]['reset'].setChecked(True)
        filename = self.files[self.index]
        fqp = filename
        img = Image.open(fqp)
        img.load()
        with suppress(AttributeError):
            for key, value in img.text.items():
                if value == 'True':
                    for radio in self.radios:
                        if key == self.radios[radio]['this']:
                            radio.setChecked(True)
                            self.view.annotation = False
                            self.active_tag = ''
                            self.view.reset()
            for key, value in img.text.items():
                if key.endswith('-rect'):
                    btn = [
                        radio for radio in self.radios
                        if self.radios[radio]['this'] == key.split('-')[0]
                    ]
                    print(key, value)
                    if btn[0].isChecked():
                        coords = [int(coord) for coord in value.split(', ')]
                        rect = QGraphicsRectItem(*coords)
                        rect.setPen(QPen(Qt.magenta, 1, Qt.SolidLine))
                        rect.setBrush(QBrush(Qt.magenta, Qt.Dense4Pattern))
                        self.view.scene().addItem(rect)
                        text = self.view.scene().addText(
                            key.split('-')[0],
                            QFont('monospace', 20, 400, False))
                        text.font().setPointSize(text.font().pointSize() * 2)
                        text.update()
                        text.setX(rect.rect().x() + 10)
                        text.setY(rect.rect().y() + 10)
                        print(f'set {key}')
Exemple #5
0
class DicomView3D(QtWidgets.QWidget):
    """
    This class is responsible for displaying the 3D construction
    of DICOM image slices
    """
    def __init__(self):
        """
        Initialize layout
        """
        QtWidgets.QWidget.__init__(self)
        self.is_rendered = False
        self.patient_dict_container = PatientDictContainer()
        # Create the layout
        self.dicom_view_layout = QtWidgets.QHBoxLayout()

        # Create start interaction button
        self.start_interaction_button = QPushButton()
        self.start_interaction_button.setText("Start 3D Interaction")
        self.start_interaction_button.clicked.connect(self.start_interaction)

        # Set layout
        self.dicom_view_layout.addWidget(self.start_interaction_button)
        self.setLayout(self.dicom_view_layout)

    def initialize_vtk_widget(self):
        """
        Initialize vtk widget for displaying 3D volume on PySide6
        """

        # Create the renderer, the render window, and the interactor.
        # The renderer draws into the render window,
        # The interactor enables mouse and keyboard-based
        # interaction with the scene.
        self.vtk_widget = QVTKRenderWindowInteractor(self)
        self.renderer = vtkRenderer()
        self.iren = self.vtk_widget.GetRenderWindow().GetInteractor()
        self.vtk_widget.GetRenderWindow().AddRenderer(self.renderer)
        self.vtk_widget.GetRenderWindow().FullScreenOff()

    def convert_pixel_values_to_vtk_3d_array(self):
        """
        Scale pixel_values based on W/L and
        convert it to a vtk 3D array
        """

        three_dimension_np_array = np.array(
            self.patient_dict_container.additional_data["pixel_values"])
        three_dimension_np_array = three_dimension_np_array.astype(np.int16)
        three_dimension_np_array = (three_dimension_np_array -
                                    (self.patient_dict_container.get("level"))) / \
            self.patient_dict_container.get("window") * 255
        three_dimension_np_array[three_dimension_np_array < 0] = 0
        three_dimension_np_array[three_dimension_np_array > 255] = 255
        three_dimension_np_array = three_dimension_np_array.astype(np.int8)
        self.depth_array = numpy_support.numpy_to_vtk(
            three_dimension_np_array.ravel(order="F"),
            deep=True,
            array_type=VTK_INT)
        self.shape = three_dimension_np_array.shape

    def update_volume_by_window_level(self):
        """
        Update volume input data when window level is changed
        """

        # Convert pixel_values in patient_dict_container into a 3D numpy array
        self.convert_pixel_values_to_vtk_3d_array()

        # Convert 3d pixel array into vtkImageData to display as vtkVolume
        self.imdata.GetPointData().SetScalars(self.depth_array)
        self.volume_mapper.SetInputData(self.imdata)
        self.volume.SetMapper(self.volume_mapper)

        # Add the volume to the renderer
        self.renderer.ResetCamera()
        self.renderer.RemoveVolume(self.volume)
        self.renderer.AddVolume(self.volume)

    def populate_volume_data(self):
        """
        Populate volume data
        """

        # Convert pixel_values in patient_dict_container into a 3D numpy array
        self.convert_pixel_values_to_vtk_3d_array()

        # Convert 3d pixel array into vtkImageData to display as vtkVolume
        self.imdata = vtkImageData()
        self.imdata.SetDimensions(self.shape)
        self.imdata.GetPointData().SetScalars(self.depth_array)

        self.volume_mapper = vtkFixedPointVolumeRayCastMapper()
        self.volume_mapper.SetBlendModeToComposite()
        self.volume_mapper.SetInputData(self.imdata)

        # The vtkLODProp3D controls the position and orientation
        # of the volume in world coordinates.
        self.volume = vtkVolume()
        self.volume.SetMapper(self.volume_mapper)
        self.volume.SetProperty(self.volume_property)
        self.volume.SetScale(
            self.patient_dict_container.get("pixmap_aspect")["axial"],
            self.patient_dict_container.get("pixmap_aspect")["sagittal"],
            self.patient_dict_container.get("pixmap_aspect")["sagittal"])

        # Add the volume to the renderer
        self.renderer.ResetCamera()
        self.renderer.RemoveVolume(self.volume)
        self.renderer.AddVolume(self.volume)

    def initialize_camera(self):
        """
        Initialize the camera
        """

        # Set up an initial view of the volume. The focal point will be the
        # center of the volume, and the zoom is 0.5
        self.camera = self.renderer.GetActiveCamera()
        self.camera.SetFocalPoint(self.volume.GetCenter())
        self.camera.Zoom(0.5)

    def initialize_volume_color(self):
        """
        Initialize volume color
        """

        # The colorTransferFunction maps voxel intensities to colors.

        self.volume_color = vtkColorTransferFunction()
        self.volume_color.AddRGBPoint(-127, 1.0, 1.0, 1.0)
        self.volume_color.AddRGBPoint(0, 0, 0, 0)
        self.volume_color.AddRGBPoint(128, 1.0, 1.0, 1.0)
        # The opacityTransferFunction is used to control the opacity
        # of different tissue types.
        self.volume_scalar_opacity = vtkPiecewiseFunction()
        self.volume_scalar_opacity.AddPoint(-127, 1)
        self.volume_scalar_opacity.AddPoint(0, 0)
        self.volume_scalar_opacity.AddPoint(128, 1)

        # The gradient opacity function is used to decrease the
        # opacity in the "flat" regions of the volume while
        # maintaining the opacity at the boundaries between tissue
        # types. The gradient is measured as the amount by which
        # the intensity changes over unit distance. For most
        # medical data, the unit distance is 1mm.
        self.volume_gradient_opacity = vtkPiecewiseFunction()
        self.volume_gradient_opacity.AddPoint(-127, 1)
        self.volume_gradient_opacity.AddPoint(0, 0)
        self.volume_gradient_opacity.AddPoint(128, 1)
        # The VolumeProperty attaches the color and opacity
        # functions to the volume, and sets other volume properties.
        # The interpolation should be set to linear
        # to do a high-quality rendering.
        self.volume_property = vtkVolumeProperty()
        self.volume_property.SetColor(self.volume_color)
        self.volume_property.SetScalarOpacity(self.volume_scalar_opacity)
        self.volume_property.SetGradientOpacity(self.volume_gradient_opacity)
        self.volume_property.SetInterpolationTypeToNearest()

        # To decrease the impact of shading, increase the Ambient and
        # decrease the Diffuse and Specular.
        # To increase the impact of shading, decrease the Ambient and
        # increase the Diffuse and Specular.
        self.volume_property.ShadeOn()
        self.volume_property.SetAmbient(0.3)
        self.volume_property.SetDiffuse(0.6)
        self.volume_property.SetSpecular(0.5)

    def update_view(self):
        """
        Update volume when there is change in window level
        """
        if self.is_rendered:
            self.update_volume_by_window_level()
            self.volume.Update()

    def start_interaction(self):
        """
        Start displaying and interacting with 3D image
        """

        # Initialize vtk widget
        self.initialize_vtk_widget()
        # Populate image data to vtkVolume for 3D rendering
        self.initialize_volume_color()
        self.populate_volume_data()
        self.initialize_camera()
        # Render vtk widget
        self.start_interaction_button.setVisible(False)
        self.dicom_view_layout.removeWidget(self.start_interaction_button)
        self.dicom_view_layout.addWidget(self.vtk_widget)

        # Start interaction
        self.iren.Initialize()
        self.iren.Start()
        self.vtk_widget.focusWidget()

        self.is_rendered = True

    def closeEvent(self, QCloseEvent):
        """
        Clean up function when the window is closed
        :param QCloseEvent: event fired when a QWidget is closed
        """
        super().closeEvent(QCloseEvent)
        # Clean up renderer
        if hasattr(self, 'renderer') and self.renderer:
            self.renderer.RemoveAllViewProps()
            render_window = self.iren.GetRenderWindow()
            render_window.Finalize()
        # Stop interaction
        if hasattr(self, 'iren') and self.iren:
            self.iren.TerminateApp()

        # Close the 3d widget
        if hasattr(self, 'vtk_widget') and self.vtk_widget:
            self.vtk_widget.Finalize()
            self.vtk_widget.close()
Exemple #6
0
class MainWidget(QWidget):
    data = None
    current_question = None
    current_correct_answer = None
    buttons_choices = None
    question_mode = None
    answer_mode = None
    correct_questions = 0
    total_questions = 0

    def __init__(self):
        super().__init__()

        # Create Widgets
        self.question = QLabel(Text="Press Start",
                               Alignment=QtCore.Qt.AlignCenter,
                               Font=QtGui.QFont("", 30))

        self.label_correct = QLabel(Alignment=QtCore.Qt.AlignCenter)

        self.button_quit = QPushButton("Quit", Visible=False)
        self.button_start = QPushButton("Start")
        lcd_height = 40
        self.lcd_total = QLCDNumber(SegmentStyle=QLCDNumber.Flat, FixedHeight=lcd_height)
        self.lcd_score = QLCDNumber(SegmentStyle=QLCDNumber.Flat, FixedHeight=lcd_height)

        self.stack = QStackedWidget()
        self.page1 = Page1Widget()
        self.page2 = Page2Widget()
        self.page3 = Page3Widget()
        self.stack.addWidget(self.page1)
        self.stack.addWidget(self.page2)
        self.stack.addWidget(self.page3)

        # Make Layout
        self.layout = QGridLayout(self)
        self.layout.addWidget(self.question, 2, 0, 1, 2)
        self.layout.addWidget(self.label_correct, 1, 0, 1, 2)
        self.layout.addWidget(self.lcd_total, 0, 0)
        self.layout.addWidget(self.lcd_score, 0, 1)
        self.layout.addWidget(self.stack, 3, 0, 1, 2)
        self.layout.addWidget(self.button_quit, 4, 0)
        self.layout.addWidget(self.button_start, 4, 1)

        # Connect Callbacks
        self.button_quit.clicked.connect(self.on_quit_pressed)
        self.button_start.clicked.connect(self.on_start_pressed)
        self.page2.ans_buttons[0].clicked.connect(self.on_ans_button0_clicked)
        self.page2.ans_buttons[1].clicked.connect(self.on_ans_button1_clicked)
        self.page2.ans_buttons[2].clicked.connect(self.on_ans_button2_clicked)
        self.page3.line_edit.returnPressed.connect(self.on_answer_given)

    def update_questions_from_selection(self):
        selected_groups = []
        for i in range(self.page1.list_groups.count()):
            item = self.page1.list_groups.item(i)
            if item.checkState():
                selected_groups.append(item.text())
        if len(selected_groups) == 0:
            self.data = data
        else:
            self.data = data.loc[data['Group'].isin(selected_groups), :]

    def on_quit_pressed(self):
        self.button_start.setVisible(True)
        self.button_quit.setVisible(False)
        self.question.clear()
        self.stack.setCurrentIndex(0)

    def on_start_pressed(self):
        self.update_questions_from_selection()
        self.button_start.setVisible(False)
        self.button_quit.setVisible(True)
        self.total_questions = -1
        self.correct_questions = 0
        self.stack.setCurrentIndex(int(self.page1.spinbox_level.text()))
        self.question_mode = MODES[self.page1.dropdown_quest.currentIndex()]
        self.answer_mode = MODES[self.page1.dropdown_ans.currentIndex()]
        self.update_question()

    def update_score(self):
        self.lcd_total.display(self.total_questions)
        self.lcd_score.display(self.correct_questions)

    def get_also_string(self):
        also_mode = self.page1.dropdown_also.currentText()
        if also_mode == "----":
            return ""
        return self.current_question[also_mode]

    def update_question(self):
        self.total_questions += 1
        self.update_score()
        picked_questions = self.data.sample(frac=1).iloc[:3]
        self.current_question = picked_questions.iloc[0]
        self.current_correct_answer = self.current_question[self.answer_mode]
        also_string = "   " + self.get_also_string()
        self.question.setText(self.current_question[self.question_mode] + also_string)

        self.buttons_choices = list(picked_questions[self.answer_mode])
        random.shuffle(self.buttons_choices)
        for button, answer in zip(self.page2.ans_buttons, self.buttons_choices):
            button.setText(answer)

    def correct_answer(self):
        self.correct_questions += 1
        self.label_correct.setText("Correct!")
        self.label_correct.setStyleSheet("QLabel { background-color : green;}")

    def wrong_answer(self):
        wrong_string = (f"Wrong: {self.current_question[self.question_mode]} "
                        f"= {self.current_correct_answer}")
        self.label_correct.setText(wrong_string)
        self.label_correct.setStyleSheet("QLabel { background-color : red;}")

    def on_answer_given(self):
        given_answer = self.page3.line_edit.text()
        if given_answer == self.current_correct_answer:
            self.correct_answer()
        else:
            self.wrong_answer()
        self.question.setText(self.page3.line_edit.text())
        self.page3.line_edit.clear()
        self.update_question()

    def after_button_clicked(self, ans_id):
        if self.buttons_choices[ans_id] == self.current_correct_answer:
            self.correct_answer()
        else:
            self.wrong_answer()
        self.update_question()

    def on_ans_button0_clicked(self):
        self.after_button_clicked(0)

    def on_ans_button1_clicked(self):
        self.after_button_clicked(1)

    def on_ans_button2_clicked(self):
        self.after_button_clicked(2)
Exemple #7
0
class PetCtView(QtWidgets.QWidget):
    load_pt_ct_signal = QtCore.Signal()

    def __init__(self):
        """
        Initialises the PET/CT View with just the start button
        """
        # Initialise Widget
        QtWidgets.QWidget.__init__(self)
        self.initialised = False
        self.pet_ct_view_layout = QtWidgets.QVBoxLayout()

        # Initialise Button
        self.load_pet_ct_button = QPushButton()
        self.load_pet_ct_button.setText("Start PT/CT")
        self.load_pet_ct_button.clicked.connect(self.go_to_patient)

        # Create variables to be initialised later
        self.pt_ct_dict_container = None
        self.iso_color = None
        self.zoom = None
        self.current_slice_number = None
        self.slice_view = None
        self.overlay_view = None
        self.display_metadata = None
        self.format_metadata = None
        self.dicom_view_layout = None
        self.radio_button_layout = None
        self.slider_layout = None
        self.slider = None
        self.alpha_slider = None
        self.view = None
        self.pt_label = None
        self.ct_label = None
        self.scene = None
        self.axial_button = None
        self.coronal_button = None
        self.sagittal_button = None

        # Add button to widget and add widget to layout
        self.pet_ct_view_layout.addWidget(self.load_pet_ct_button)
        self.setLayout(self.pet_ct_view_layout)

    def go_to_patient(self):
        """
        Triggers when the start button is pressed
        """
        self.load_pet_ct_button.setEnabled(False)
        self.load_pt_ct_signal.emit() # Opens the OpenPTCTPatientWindow
        self.load_pet_ct_button.setEnabled(True)

    def load_pet_ct(self, roi_color=None, iso_color=None, slice_view="axial",
                    format_metadata=True):
        """
        Loads the PET/CT GUI after data has been added to PTCTDictContainer
        """
        self.pt_ct_dict_container = PTCTDictContainer()
        self.iso_color = iso_color
        self.zoom = 1
        self.current_slice_number = None
        self.slice_view = slice_view
        self.overlay_view = slice_view

        self.display_metadata = False
        self.format_metadata = format_metadata

        self.dicom_view_layout = QtWidgets.QHBoxLayout()
        self.radio_button_layout = QtWidgets.QHBoxLayout()
        self.slider_layout = QtWidgets.QHBoxLayout()
        # self.radio_button_layout.setAlignment(QtCore.Qt.AlignCenter)

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

        # Alpha slider
        self.alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        self.init_alpha_slider()

        # Slider labels
        self.ct_label = QtWidgets.QLabel("CT")
        self.pt_label = QtWidgets.QLabel("PET")

        self.init_view()
        self.scene = QtWidgets.QGraphicsScene()
        # radio buttons
        self.coronal_button = QRadioButton("Coronal")
        self.coronal_button.setChecked(False)
        self.coronal_button.toggled.connect(self.update_axis)
        self.axial_button = QRadioButton("Axial")
        self.axial_button.setChecked(True)
        self.axial_button.toggled.connect(self.update_axis)
        self.sagittal_button = QRadioButton("Sagittal")
        self.sagittal_button.setChecked(False)
        self.sagittal_button.toggled.connect(self.update_axis)

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

        self.slider_layout.addWidget(self.ct_label)
        self.slider_layout.addWidget(self.alpha_slider)
        self.slider_layout.addWidget(self.pt_label)

        self.load_pet_ct_button.setVisible(False)
        self.pet_ct_view_layout.removeWidget(self.load_pet_ct_button)

        self.pet_ct_view_layout.addLayout(self.dicom_view_layout)
        self.pet_ct_view_layout.addLayout(self.slider_layout)
        self.pet_ct_view_layout.addLayout(self.radio_button_layout,
                                          QtCore.Qt.AlignBottom
                                          | QtCore.Qt.AlignCenter)

        self.radio_button_layout.addWidget(self.coronal_button)
        self.radio_button_layout.addWidget(self.axial_button)
        self.radio_button_layout.addWidget(self.sagittal_button)

        self.setLayout(self.pet_ct_view_layout)
        self.update_view()

        self.initialised = True

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

    def init_alpha_slider(self):
        """
        Creates the alpha slider for opacity between images
        """
        self.alpha_slider.setMinimum(0)
        self.alpha_slider.setMaximum(100)
        self.alpha_slider.setValue(50)
        self.alpha_slider.setTickPosition(QtWidgets.QSlider.TicksLeft)
        self.alpha_slider.setTickInterval(1)
        self.alpha_slider.valueChanged.connect(self.value_changed)

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

    def update_axis(self):
        """
        Triggers when a radio button is pressed
        """
        toggled = self.sender()
        if toggled.isChecked():
            self.slice_view = toggled.text().lower()
            pixmaps = self.pt_ct_dict_container.get(
                "ct_pixmaps_" + self.slice_view)
            self.slider.setMaximum(len(pixmaps) - 1)
            self.slider.setValue(int(len(pixmaps) / 2))

        self.update_view()

    def value_changed(self):
        """
        Triggers when a value is changed on PET/CT
        """
        self.update_view()

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

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

        self.view.setScene(self.scene)

    def image_display(self):
        """
        Update the ct_image to be displayed on the DICOM View.
        """
        # Lead CT
        ct_pixmaps = self.pt_ct_dict_container.get(
            "ct_pixmaps_" + self.slice_view)
        slider_id = self.slider.value()
        ct_image = ct_pixmaps[slider_id].toImage()

        # Load PT
        pt_pixmaps = self.pt_ct_dict_container.get(
            "pt_pixmaps_" + self.slice_view)
        m = float(len(pt_pixmaps)) / len(ct_pixmaps)
        pt_image = pt_pixmaps[int(m * slider_id)].toImage()

        # Get alpha
        alpha = float(self.alpha_slider.value() / 100)

        # Merge Images
        painter = QPainter()
        painter.begin(ct_image)
        painter.setOpacity(alpha)
        painter.drawImage(0, 0, pt_image)
        painter.end()

        # Load merged images
        merged_pixmap = QtGui.QPixmap.fromImage(ct_image)
        label = QtWidgets.QGraphicsPixmapItem(merged_pixmap)
        self.scene = QtWidgets.QGraphicsScene()
        self.scene.addItem(label)

    def zoom_in(self):
        """
        Zooms in on PET/CT
        """
        self.zoom *= 1.05
        self.update_view(zoom_change=True)

    def zoom_out(self):
        """
        Zooms out on PET/CT
        """
        self.zoom /= 1.05
        self.update_view(zoom_change=True)