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)
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
class UIImageFusionWindow(object): image_fusion_info_initialized = QtCore.Signal(object) def setup_ui(self, open_image_fusion_select_instance): """Sets up a UI""" if platform.system() == 'Darwin': self.stylesheet_path = "res/stylesheet.qss" else: self.stylesheet_path = "res/stylesheet-win-linux.qss" window_icon = QIcon() window_icon.addPixmap(QPixmap(resource_path("res/images/icon.ico")), QIcon.Normal, QIcon.Off) open_image_fusion_select_instance.setObjectName( "OpenPatientWindowInstance") open_image_fusion_select_instance.setWindowIcon(window_icon) open_image_fusion_select_instance.resize(840, 530) # Create a vertical box for containing the other elements and layouts self.open_patient_window_instance_vertical_box = QVBoxLayout() self.open_patient_window_instance_vertical_box.setObjectName( "OpenPatientWindowInstanceVerticalBox") # Create a label to prompt the user to enter the path to the # directory that contains the DICOM files self.open_patient_directory_prompt = QLabel() self.open_patient_directory_prompt.setObjectName( "OpenPatientDirectoryPrompt") self.open_patient_directory_prompt.setAlignment(Qt.AlignLeft) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_prompt) # Create a horizontal box to hold the input box for the directory # and the choose button self.open_patient_directory_input_horizontal_box = QHBoxLayout() self.open_patient_directory_input_horizontal_box.setObjectName( "OpenPatientDirectoryInputHorizontalBox") # Create a textbox to contain the path to the directory that contains # the DICOM files self.open_patient_directory_input_box = \ UIImageFusionWindowDragAndDropEvent(self) self.open_patient_directory_input_box.setObjectName( "OpenPatientDirectoryInputBox") self.open_patient_directory_input_box.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_directory_input_box.returnPressed.connect( self.scan_directory_for_patient) self.open_patient_directory_input_horizontal_box.addWidget( self.open_patient_directory_input_box) # Create a choose button to open the file dialog self.open_patient_directory_choose_button = QPushButton() self.open_patient_directory_choose_button.setObjectName( "OpenPatientDirectoryChooseButton") self.open_patient_directory_choose_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_directory_choose_button.resize( self.open_patient_directory_choose_button.sizeHint().width(), self.open_patient_directory_input_box.height()) self.open_patient_directory_choose_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_directory_input_horizontal_box.addWidget( self.open_patient_directory_choose_button) self.open_patient_directory_choose_button.clicked.connect( self.choose_button_clicked) # Create a widget to hold the input fields self.open_patient_directory_input_widget = QWidget() self.open_patient_directory_input_horizontal_box.setStretch(0, 4) self.open_patient_directory_input_widget.setLayout( self.open_patient_directory_input_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_input_widget) # Create a horizontal box to hold the stop button and direction to # the user on where to select the patient self.open_patient_appear_prompt_and_stop_horizontal_box = QHBoxLayout() self.open_patient_appear_prompt_and_stop_horizontal_box.setObjectName( "OpenPatientAppearPromptAndStopHorizontalBox") # Create a label to show direction on where the files will appear self.open_patient_directory_appear_prompt = QLabel() self.open_patient_directory_appear_prompt.setObjectName( "OpenPatientDirectoryAppearPrompt") self.open_patient_directory_appear_prompt.setAlignment(Qt.AlignLeft) self.open_patient_appear_prompt_and_stop_horizontal_box.addWidget( self.open_patient_directory_appear_prompt) self.open_patient_appear_prompt_and_stop_horizontal_box.addStretch(1) # Create a button to stop searching self.open_patient_window_stop_button = QPushButton() self.open_patient_window_stop_button.setObjectName( "OpenPatientWindowStopButton") self.open_patient_window_stop_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_stop_button.resize( self.open_patient_window_stop_button.sizeHint().width(), self.open_patient_window_stop_button.sizeHint().height()) self.open_patient_window_stop_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_stop_button.clicked.connect( self.stop_button_clicked) self.open_patient_window_stop_button.setProperty( "QPushButtonClass", "fail-button") self.open_patient_window_stop_button.setVisible(False) self.open_patient_appear_prompt_and_stop_horizontal_box.addWidget( self.open_patient_window_stop_button) # Create a widget to hold the layout self.open_patient_appear_prompt_and_stop_widget = QWidget() self.open_patient_appear_prompt_and_stop_widget.setLayout( self.open_patient_appear_prompt_and_stop_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_appear_prompt_and_stop_widget) # Create a tree view list to list out all patients in the directory # selected above self.open_patient_window_patients_tree = QTreeWidget() self.open_patient_window_patients_tree.setObjectName( "OpenPatientWindowPatientsTree") self.open_patient_window_patients_tree.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_patients_tree.resize( self.open_patient_window_patients_tree.sizeHint().width(), self.open_patient_window_patients_tree.sizeHint().height()) self.open_patient_window_patients_tree.setHeaderHidden(False) self.open_patient_window_patients_tree.setHeaderLabels([""]) self.open_patient_window_patients_tree.itemChanged.connect( self.tree_item_clicked) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_window_patients_tree) self.last_patient = None # Create a label to show what would happen if they select the patient self.open_patient_directory_result_label = QtWidgets.QLabel() self.open_patient_directory_result_label.setObjectName( "OpenPatientDirectoryResultLabel") self.open_patient_directory_result_label.setAlignment(Qt.AlignLeft) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_directory_result_label) # Create a horizontal box to hold the Cancel and Open button self.open_patient_window_patient_open_actions_horizontal_box = \ QHBoxLayout() self.open_patient_window_patient_open_actions_horizontal_box. \ setObjectName("OpenPatientWindowPatientOpenActionsHorizontalBox") self.open_patient_window_patient_open_actions_horizontal_box. \ addStretch(1) # Add a button to go back/close from the application self.open_patient_window_close_button = QPushButton() self.open_patient_window_close_button.setObjectName( "OpenPatientWindowcloseButton") self.open_patient_window_close_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_close_button.resize( self.open_patient_window_stop_button.sizeHint().width(), self.open_patient_window_stop_button.sizeHint().height()) self.open_patient_window_close_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_close_button.clicked.connect( self.close_button_clicked) self.open_patient_window_close_button.setProperty( "QPushButtonClass", "fail-button") self.open_patient_window_patient_open_actions_horizontal_box. \ addWidget(self.open_patient_window_close_button) # Add a button to confirm opening of the patient self.open_patient_window_confirm_button = QPushButton() self.open_patient_window_confirm_button.setObjectName( "OpenPatientWindowConfirmButton") self.open_patient_window_confirm_button.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) self.open_patient_window_confirm_button.resize( self.open_patient_window_confirm_button.sizeHint().width(), self.open_patient_window_confirm_button.sizeHint().height()) self.open_patient_window_confirm_button.setCursor( QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.open_patient_window_confirm_button.setDisabled(True) self.open_patient_window_confirm_button.clicked.connect( self.confirm_button_clicked) self.open_patient_window_confirm_button.setProperty( "QPushButtonClass", "success-button") self.open_patient_window_patient_open_actions_horizontal_box. \ addWidget( self.open_patient_window_confirm_button) # Create a widget to house all of the actions button for open patient # window self.open_patient_window_patient_open_actions_widget = QWidget() self.open_patient_window_patient_open_actions_widget.setLayout( self.open_patient_window_patient_open_actions_horizontal_box) self.open_patient_window_instance_vertical_box.addWidget( self.open_patient_window_patient_open_actions_widget) # Set the vertical box fourth element, the tree view, to stretch # out as far as possible self.open_patient_window_instance_vertical_box.setStretch(3, 4) self.open_patient_window_instance_central_widget = QWidget() self.open_patient_window_instance_central_widget.setObjectName( "OpenPatientWindowInstanceCentralWidget") self.open_patient_window_instance_central_widget.setLayout( self.open_patient_window_instance_vertical_box) # Create threadpool for multithreading self.threadpool = QThreadPool() # print("Multithreading with maximum %d threads" % self.threadpool. # maxThreadCount()) # Create interrupt event for stopping the directory search self.interrupt_flag = threading.Event() # Bind all texts into the buttons and labels self.retranslate_ui(open_image_fusion_select_instance) # Set the central widget, ready for display open_image_fusion_select_instance.setCentralWidget( self.open_patient_window_instance_central_widget) # Set the current stylesheet to the instance and connect it back # to the caller through slot _stylesheet = open(resource_path(self.stylesheet_path)).read() open_image_fusion_select_instance.setStyleSheet(_stylesheet) QtCore.QMetaObject.connectSlotsByName( open_image_fusion_select_instance) def retranslate_ui(self, open_image_fusion_select_instance): """Translates UI""" _translate = QtCore.QCoreApplication.translate open_image_fusion_select_instance.setWindowTitle( _translate("OpenPatientWindowInstance", "OnkoDICOM - Select Patient")) self.open_patient_directory_prompt.setText(_translate( "OpenPatientWindowInstance", "Choose an image to merge with:")) self.open_patient_directory_input_box.setPlaceholderText( _translate("OpenPatientWindowInstance", "Enter DICOM Files Path (For example, " "C:\path\\to\your\DICOM\Files)")) self.open_patient_directory_choose_button.setText(_translate( "OpenPatientWindowInstance", "Choose")) self.open_patient_directory_appear_prompt.setText(_translate( "OpenPatientWindowInstance", "Please select below the image set you wish to overlay:")) self.open_patient_directory_result_label. \ setText("The selected imageset(s) above will be " "co-registered with the current imageset.") self.open_patient_window_stop_button.setText(_translate( "OpenPatientWindowInstance", "Stop Search")) self.open_patient_window_close_button.setText(_translate( "OpenPatientWindowInstance", "Close")) self.open_patient_window_confirm_button.setText(_translate( "OpenPatientWindowInstance", "Confirm")) def update_patient(self): self.clear_checked_leaves() self.patient_dict_container = PatientDictContainer() self.patient = self.patient_dict_container.get("basic_info") self.patient_id = self.patient['id'] dataset = self.patient_dict_container.dataset[0] self.patient_current_image_series_uid = \ dataset.get("SeriesInstanceUID") def clear_checked_leaves(self): """ Resets all leaves to their unchecked state """ def recurse(parent_item: QTreeWidgetItem): for i in range(parent_item.childCount()): child = parent_item.child(i) grand_children = child.childCount() if grand_children > 0: recurse(child) else: if child.checkState(0) == Qt.Checked: child.setCheckState(0, Qt.CheckState.Unchecked) child.setSelected(False) recurse(self.open_patient_window_patients_tree.invisibleRootItem()) self.open_patient_window_patients_tree.collapseAll() def close_button_clicked(self): """Closes the window.""" self.close() def scan_directory_for_patient(self): # Reset tree view header and last patient self.open_patient_window_confirm_button.setDisabled(True) self.open_patient_window_patients_tree.setHeaderLabels([""]) self.last_patient = None self.filepath = self.open_patient_directory_input_box.text() # Proceed if a folder was selected if self.filepath != "": # Update the QTreeWidget to reflect data being loaded # First, clear the widget of any existing data self.open_patient_window_patients_tree.clear() # Next, update the tree widget self.open_patient_window_patients_tree.addTopLevelItem( QTreeWidgetItem(["Loading selected directory..."])) # The choose button is disabled until the thread finishes executing self.open_patient_directory_choose_button.setEnabled(False) # Reveals the Stop Search button for the duration of the search self.open_patient_window_stop_button.setVisible(True) # The interrupt flag is then un-set if a previous search has been # stopped. self.interrupt_flag.clear() # Then, create a new thread that will load the selected folder worker = Worker(DICOMDirectorySearch.get_dicom_structure, self.filepath, self.interrupt_flag, progress_callback=True) worker.signals.result.connect(self.on_search_complete) worker.signals.progress.connect(self.search_progress) # Execute the thread self.threadpool.start(worker) def choose_button_clicked(self): """ Executes when the choose button is clicked. Gets filepath from the user and loads all files and subdirectories. """ # Get folder path from pop up dialog box self.filepath = QtWidgets.QFileDialog.getExistingDirectory( None, 'Select patient folder...', '') self.open_patient_directory_input_box.setText(self.filepath) self.scan_directory_for_patient() def stop_button_clicked(self): self.interrupt_flag.set() def search_progress(self, progress_update): """ Current progress of the file search. """ self.open_patient_window_patients_tree.clear() self.open_patient_window_patients_tree.addTopLevelItem( QTreeWidgetItem(["Loading selected directory... " "(%s files searched)" % progress_update])) def on_search_complete(self, dicom_structure): """ Executes once the directory search is complete. :param dicom_structure: DICOMStructure object constructed by the directory search. """ self.open_patient_directory_choose_button.setEnabled(True) self.open_patient_window_stop_button.setVisible(False) self.open_patient_window_patients_tree.clear() # dicom_structure will be None if function was interrupted. if dicom_structure is None: return for patient_item in dicom_structure.get_tree_items_list(): self.open_patient_window_patients_tree.addTopLevelItem( patient_item) patient_item.setExpanded(True) # Display all studies # Display all image sets for i in range(patient_item.childCount()): study = patient_item.child(i) study.setExpanded(True) if len(dicom_structure.patients) == 0: QMessageBox.about(self, "No files found", "Selected directory contains no DICOM files.") def tree_item_clicked(self, item, _): """ Executes when a tree item is checked or unchecked. If a different patient is checked, uncheck the previous patient. Inform user about missing DICOM files. """ # If patient is only selected, but not checked, set it to "focus" to # coincide with stylesheet. And if the selected item is an image set, # display its child branches. if item.checkState(0) == Qt.CheckState.Unchecked: self.open_patient_window_patients_tree.setCurrentItem(item) else: # Otherwise don't "focus", then set patient as selected self.open_patient_window_patients_tree.setCurrentItem(None) item.setSelected(True) # Expand or collapse the tree branch if item is an image series # Only collapse if the selected image series is expanded but unchecked # Otherwise, expand its tree branch to show RT files is_expanded = False \ if (item.isExpanded() is True and item.checkState(0) == Qt.CheckState.Unchecked) else True self.display_a_tree_branch(item, is_expanded) selected_patient = item # If the item is not top-level, bubble up to see which top-level item # this item belongs to if self.open_patient_window_patients_tree.invisibleRootItem(). \ indexOfChild(item) == -1: while self.open_patient_window_patients_tree.invisibleRootItem(). \ indexOfChild(selected_patient) == -1: selected_patient = selected_patient.parent() # Uncheck previous patient if a different patient is selected if item.checkState(0) == Qt.CheckState.Checked and self.last_patient \ != selected_patient: if self.last_patient is not None: last_patient_checked_items = self.get_checked_nodes( self.last_patient) for checked_item in last_patient_checked_items: checked_item.setCheckState(0, Qt.Unchecked) self.last_patient = selected_patient # Check selected items and display warning messages self.check_selected_items(selected_patient) def display_a_tree_branch(self, node, is_expanded): # TO DO: # Could Team 23 please update the defintion of this docstring as # well as same function presented in OpenPatientWindow. """ Displays a tree branch Parameters: node : root node the tree is_expanded (boolean): flag for checking if a particular node/leaf is expanded. """ node.setExpanded(is_expanded) if node.childCount() > 0: for i in range(node.childCount()): self.display_a_tree_branch(node.child(i), is_expanded) else: return def check_selected_items(self, selected_patient): """ Check and display warning messages based on the existence and quantity of image series, RTSTRUCT, RTPLAN, RTDOSE and SR files Parameters: selected_patient (DICOMStructure): DICOM Object of patient """ # Get the types of all selected leaves & Get the names of all selected # studies checked_nodes = self.get_checked_nodes( self.open_patient_window_patients_tree.invisibleRootItem()) selected_series_types = [checked_node.dicom_object.get_series_type() for checked_node in checked_nodes] selected_series_id = [checked_node.dicom_object.series_uid for checked_node in checked_nodes] # Total number of selected image series total_selected_image_series = selected_series_types.count('CT') + \ selected_series_types.count('MR') + \ selected_series_types.count('PT') # Check the existence of IMAGE, RTSTRUCT, RTPLAN and RTDOSE files proceed = True if total_selected_image_series < 1: header = "Cannot proceed without an image." proceed = False elif total_selected_image_series > 1: header = "Cannot proceed with more than 1 selected image." proceed = False elif selected_patient.dicom_object.patient_id.strip() != \ self.patient_id: header = "Cannot proceed with different patient." proceed = False elif self.patient_current_image_series_uid in selected_series_id: header = "Cannot fuse with the same series." proceed = False elif not self.check_selected_items_referencing(checked_nodes): # Check that selected items properly reference each other header = "Selected series do not reference each other." proceed = False elif 'RTSTRUCT' not in selected_series_types and \ self.check_existing_rtss(checked_nodes): header = "The associated RTSTRUCT must be selected." proceed = False elif 'RTDOSE' in selected_series_types: header = "Cannot fuse with a RTDOSE file." proceed = False else: header = "" self.open_patient_window_confirm_button.setDisabled(not proceed) # Set the tree header self.open_patient_window_patients_tree.setHeaderLabel(header) def check_selected_items_referencing(self, items): """ Check if selected tree items properly reference each other. :param items: List of selected DICOMWidgetItems. :return: True if the selected items belong to the same tree branch. """ # Dictionary of series of different file types series = { "IMAGE": None, "RTSTRUCT": None, "RTPLAN": None, "RTDOSE": None, "SR": None } for item in items: series_type = item.dicom_object.get_series_type() if series_type in series: series[series_type] = item else: series["IMAGE"] = item # Check if the RTSTRUCT, RTPLAN, and RTDOSE are a child item of the # image series if series["IMAGE"]: if series["RTSTRUCT"] and series["RTSTRUCT"].parent() != \ series["IMAGE"]: return False if series["RTPLAN"] and \ series["RTPLAN"].parent().parent() != series["IMAGE"]: return False if series["SR"] and series["SR"].parent() != series["IMAGE"]: return False return True def check_existing_rtss(self, items): """ Check for existing rtss :return: bool, whether there is a rtss associated with the selected image series """ image_series = ['CT', 'MR', 'PT'] for item in items: if item.dicom_object.get_series_type() in image_series: for i in range(item.childCount()): if item.child(i).dicom_object: return True return False def get_checked_nodes(self, root): """ :param root: QTreeWidgetItem as a root. :return: A list of all QTreeWidgetItems in the QTreeWidget that are checked under the root. """ checked_items = [] def recurse(parent_item: QTreeWidgetItem): for i in range(parent_item.childCount()): child = parent_item.child(i) if int(child.flags()) & int(Qt.ItemIsUserCheckable) and \ child.checkState(0) == Qt.Checked: checked_items.append(child) grand_children = child.childCount() if grand_children > 0: recurse(child) recurse(root) return checked_items def confirm_button_clicked(self): """ Begins loading of the selected files. """ selected_files = [] for item in self.get_checked_nodes( self.open_patient_window_patients_tree.invisibleRootItem()): selected_files += item.dicom_object.get_files() self.progress_window = ImageFusionProgressWindow(self) self.progress_window.signal_loaded.connect(self.on_loaded) self.progress_window.signal_error.connect(self.on_loading_error) self.progress_window.start_loading(selected_files) def on_loaded(self, results): """ Executes when the progress bar finishes loaded the selected files. """ if results[0] is True: # Will be NoneType if loading was interrupted. self.image_fusion_info_initialized.emit(results[1]) def on_loading_error(self, exception): """ Error handling for progress window. """ if type(exception[1]) == ImageLoading.NotRTSetError: QMessageBox.about(self.progress_window, "Unable to open selection", "Selected files cannot be opened as they are not" " a DICOM-RT set.") self.progress_window.close() elif type(exception[1]) == ImageLoading.NotAllowedClassError: QMessageBox.about(self.progress_window, "Unable to open selection", "Selected files cannot be opened as they contain" " unsupported DICOM classes.") self.progress_window.close()
class 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}')
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()
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)
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)