def generate_ROI(self, contours, progress_callback): """ Generates new ROIs based on contour data. :param contours: dictionary of contours to turn into ROIs. :param progress_callback: signal that receives the current progress of the loading. """ # Initialise variables needed for function patient_dict_container = PatientDictContainer() dataset_rtss = patient_dict_container.get("dataset_rtss") # Get existing ROIs existing_rois = [] rois = patient_dict_container.get("dataset_rtss") if rois: for roi in rois.StructureSetROISequence: existing_rois.append(roi.ROIName) # Loop through each SUV level item_count = len(contours) current_progress = 60 progress_increment = round((95 - 60) / item_count) for item in contours: # Delete ROI if it already exists to recreate it if item in existing_rois: dataset_rtss = ROI.delete_roi(dataset_rtss, item) # Update patient dict container current_rois = patient_dict_container.get("rois") keys = [] for key, value in current_rois.items(): if value["name"] == item: keys.append(key) for key in keys: del current_rois[key] patient_dict_container.set("rois", current_rois) progress_callback.emit(("Generating ROIs", current_progress)) current_progress += progress_increment # Loop through each slice for i in range(len(contours[item])): slider_id = contours[item][i][0] dataset = patient_dict_container.dataset[slider_id] pixlut = patient_dict_container.get("pixluts") pixlut = pixlut[dataset.SOPInstanceUID] z_coord = dataset.SliceLocation # List storing lists that contain all points for a # contour. single_array = [] # Loop through each contour for j in range(len(contours[item][i][1])): single_array.append([]) # Loop through every point in the contour for point in contours[item][i][1][j]: # Convert pixel coordinates to RCS points rcs_pixels = ROI.pixel_to_rcs(pixlut, round(point[1]), round(point[0])) # Append RCS points to the single array single_array[j].append(rcs_pixels[0]) single_array[j].append(rcs_pixels[1]) single_array[j].append(z_coord) # Create the ROI(s) for array in single_array: rtss = ROI.create_roi(dataset_rtss, item, [{ 'coords': array, 'ds': dataset }], "") # Save the updated rtss patient_dict_container.set("dataset_rtss", rtss) patient_dict_container.set("rois", ImageLoading.get_roi_info(rtss))
def generate_roi(self, contours, progress_callback): """ Generates new ROIs based on contour data. :param contours: dictionary of contours to turn into ROIs. :param progress_callback: signal to update loading progress """ # Initialise variables needed for function patient_dict_container = PatientDictContainer() dataset_rtss = patient_dict_container.get("dataset_rtss") pixmaps = patient_dict_container.get("pixmaps_axial") slider_min = 0 slider_max = len(pixmaps) - 1 # Get existing ROIs existing_rois = [] rois = patient_dict_container.get("dataset_rtss") if rois: for roi in rois.StructureSetROISequence: existing_rois.append(roi.ROIName) # Loop through each isodose level for item in contours: # Delete ROI if it already exists to recreate it if item in existing_rois: dataset_rtss = ROI.delete_roi(dataset_rtss, item) # Update patient dict container current_rois = patient_dict_container.get("rois") keys = [] for key, value in current_rois.items(): if value["name"] == item: keys.append(key) for key in keys: del current_rois[key] patient_dict_container.set("rois", current_rois) # Calculate isodose ROI for each slice, skip if slice has no # contour data for i in range(slider_min, slider_max): if not len(contours[item][i]): continue # Get required data for calculating ROI dataset = patient_dict_container.dataset[i] pixlut = patient_dict_container.get("pixluts") pixlut = pixlut[dataset.SOPInstanceUID] z_coord = dataset.SliceLocation curr_slice_uid = patient_dict_container.get("dict_uid")[i] dose_pixluts = patient_dict_container.get("dose_pixluts") dose_pixluts = dose_pixluts[curr_slice_uid] # Loop through each contour for each slice. # Convert the pixel points to RCS points, append z value single_array = [] # Loop through each contour for j in range(len(contours[item][i])): single_array.append([]) # Loop through every second point in the contour for point in contours[item][i][j][::2]: # Transform into dose pixel dose_pixels = [ dose_pixluts[0][int(point[1])], dose_pixluts[1][int(point[0])] ] # Transform into RCS pixel rcs_pixels = ROI.pixel_to_rcs(pixlut, round(dose_pixels[0]), round(dose_pixels[1])) # Append point coordinates to the single array single_array[j].append(rcs_pixels[0]) single_array[j].append(rcs_pixels[1]) single_array[j].append(z_coord) # Create the ROI(s) for array in single_array: rtss = ROI.create_roi(dataset_rtss, item, [{ 'coords': array, 'ds': dataset }], "DOSE_REGION") # Save the updated rtss patient_dict_container.set("dataset_rtss", rtss) patient_dict_container.set("rois", ImageLoading.get_roi_info(rtss)) progress_callback.emit(("Writing to RT Structure Set", 85))
class ActionHandler: """ This class is responsible for initializing all of the actions that will be used by the MainPage and its components. There exists a 1-to-1 relationship between this class and the MainPage. This class has access to the main page's attributes and components, however this access should only be used to provide functionality to the actions defined below. The instance of this class can be given to the main page's components in order to trigger actions. """ def __init__(self, main_page): self.__main_page = main_page self.patient_dict_container = PatientDictContainer() self.is_four_view = False self.has_image_registration_single = False self.has_image_registration_four = False ############################## # Init all actions and icons # ############################## # Open patient self.icon_open = QtGui.QIcon() self.icon_open.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/open_patient_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_open = QtGui.QAction() self.action_open.setIcon(self.icon_open) self.action_open.setText("Open new patient") self.action_open.setIconVisibleInMenu(True) # Save RTSTRUCT changes action self.icon_save_structure = QtGui.QIcon() self.icon_save_structure.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/save_all_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_save_structure = QtGui.QAction() self.action_save_structure.setIcon(self.icon_save_structure) self.action_save_structure.setText("Save RTSTRUCT changes") self.action_save_structure.setIconVisibleInMenu(True) self.action_save_structure.triggered.connect(self.save_struct_handler) # Save as Anonymous Action self.icon_save_as_anonymous = QtGui.QIcon() self.icon_save_as_anonymous.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/anonlock_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_save_as_anonymous = QtGui.QAction() self.action_save_as_anonymous.setIcon(self.icon_save_as_anonymous) self.action_save_as_anonymous.setText("Save as Anonymous") self.action_save_as_anonymous.triggered.connect( self.anonymization_handler) # Exit action self.action_exit = QtGui.QAction() self.action_exit.setText("Exit") self.action_exit.triggered.connect(self.action_exit_handler) # Zoom Out Action self.icon_zoom_out = QtGui.QIcon() self.icon_zoom_out.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/zoom_out_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_zoom_out = QtGui.QAction() self.action_zoom_out.setIcon(self.icon_zoom_out) self.action_zoom_out.setIconVisibleInMenu(True) self.action_zoom_out.setText("Zoom Out") self.action_zoom_out.triggered.connect(self.zoom_out_handler) # Zoom In Action self.icon_zoom_in = QtGui.QIcon() self.icon_zoom_in.addPixmap( QtGui.QPixmap( resource_path("res/images/btn-icons/zoom_in_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_zoom_in = QtGui.QAction() self.action_zoom_in.setIcon(self.icon_zoom_in) self.action_zoom_in.setIconVisibleInMenu(True) self.action_zoom_in.setText("Zoom In") self.action_zoom_in.triggered.connect(self.zoom_in_handler) # Transect Action self.icon_transect = QtGui.QIcon() self.icon_transect.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/transect_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_transect = QtGui.QAction() self.action_transect.setIcon(self.icon_transect) self.action_transect.setIconVisibleInMenu(True) self.action_transect.setText("Transect") self.action_transect.triggered.connect(self.transect_handler) # Add-On Options Action self.icon_add_ons = QtGui.QIcon() self.icon_add_ons.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/management_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_add_ons = QtGui.QAction() self.action_add_ons.setIcon(self.icon_add_ons) self.action_add_ons.setIconVisibleInMenu(True) self.action_add_ons.setText("Add-On Options") self.action_add_ons.triggered.connect(self.add_on_options_handler) # Switch to Single View Action self.icon_one_view = QtGui.QIcon() self.icon_one_view.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/axial_view_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_one_view = QtGui.QAction() self.action_one_view.setIcon(self.icon_one_view) self.action_one_view.setIconVisibleInMenu(True) self.action_one_view.setText("One View") self.action_one_view.triggered.connect(self.one_view_handler) # Switch to 4 Views Action self.icon_four_views = QtGui.QIcon() self.icon_four_views.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/four_views_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_four_views = QtGui.QAction() self.action_four_views.setIcon(self.icon_four_views) self.action_four_views.setIconVisibleInMenu(True) self.action_four_views.setText("Four Views") self.action_four_views.triggered.connect(self.four_views_handler) # Show cut lines self.icon_cut_lines = QtGui.QIcon() self.icon_cut_lines.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/cut_line_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_show_cut_lines = QtGui.QAction() self.action_show_cut_lines.setIcon(self.icon_cut_lines) self.action_show_cut_lines.setIconVisibleInMenu(True) self.action_show_cut_lines.setText("Show Cut Lines") self.action_show_cut_lines.triggered.connect(self.cut_lines_handler) # Export Pyradiomics Action self.action_pyradiomics_export = QtGui.QAction() self.action_pyradiomics_export.setText("Export Pyradiomics") self.action_pyradiomics_export.triggered.connect( self.pyradiomics_export_handler) # Export DVH Action self.action_dvh_export = QtGui.QAction() self.action_dvh_export.setText("Export DVH") self.action_dvh_export.triggered.connect(self.export_dvh_handler) # Create Windowing menu self.icon_windowing = QtGui.QIcon() self.icon_windowing.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/windowing_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.menu_windowing = QtWidgets.QMenu() self.init_windowing_menu() self.windowing_window = Windowing(self) self.windowing_window.done_signal.connect(self.update_views) # Create Export menu self.icon_export = QtGui.QIcon() self.icon_export.addPixmap( QtGui.QPixmap( resource_path("res/images/btn-icons/export_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On, ) self.menu_export = QtWidgets.QMenu() self.menu_export.setTitle("Export") self.menu_export.addAction(self.action_pyradiomics_export) self.menu_export.addAction(self.action_dvh_export) # Image Fusion Action self.icon_image_fusion = QtGui.QIcon() self.icon_image_fusion.addPixmap( QtGui.QPixmap( resource_path( "res/images/btn-icons/image_fusion_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_image_fusion = QtGui.QAction() self.action_image_fusion.setIcon(self.icon_image_fusion) self.action_image_fusion.setIconVisibleInMenu(True) self.action_image_fusion.setText("Image Fusion") def init_windowing_menu(self): self.menu_windowing.setIcon(self.icon_windowing) self.menu_windowing.setTitle("Windowing") dict_windowing = self.patient_dict_container.get("dict_windowing") # Get the right order for windowing names names_ordered = sorted(dict_windowing.keys()) if "Normal" in dict_windowing.keys(): old_index = names_ordered.index("Normal") names_ordered.insert(0, names_ordered.pop(old_index)) # Create actions for each windowing item def generate_triggered_handler(text_): def handler(state): self.windowing_handler(state, text_) return handler windowing_actions = [] for name in names_ordered: text = str(name) action_windowing_item = QtGui.QAction(self.menu_windowing) action_windowing_item.triggered.connect( generate_triggered_handler(text)) action_windowing_item.setText(text) windowing_actions.append(action_windowing_item) # For reasons beyond me, the actions have to be set as a child # of the windowing menu *and* later be added to the menu as # well. You can't do one or the other, otherwise the menu won't # populate. Feel free to try fix (or at least explain why the # action has to be set as the windowing menu's child twice) for item in windowing_actions: self.menu_windowing.addAction(item) def save_struct_handler(self): """ If there are changes to the RTSTRUCT detected, save the changes to disk. """ if self.patient_dict_container.get("rtss_modified"): self.__main_page.structures_tab.save_new_rtss_to_fixed_image_set() else: QtWidgets.QMessageBox.information( self.__main_page, "File not saved", "No changes to the RTSTRUCT file detected.") def zoom_out_handler(self): self.__main_page.zoom_out(self.is_four_view, self.has_image_registration_single, self.has_image_registration_four) def zoom_in_handler(self): self.__main_page.zoom_in(self.is_four_view, self.has_image_registration_single, self.has_image_registration_four) def windowing_handler(self, state, text): """ Function triggered when a window is selected from the menu. :param state: Variable not used. Present to be able to use a lambda function. :param text: The name of the window selected. """ ptct = PTCTDictContainer() mvd = MovingDictContainer() if ptct.is_empty() and mvd.is_empty(): windowing_model(text, [True, False, False, False]) self.update_views() else: self.windowing_window.set_window(text) self.windowing_window.show() def update_views(self): """ function to update all dicom views """ self.__main_page.update_views(update_3d_window=True) def anonymization_handler(self): """ Function triggered when the Anonymization button is pressed from the menu. """ save_reply = QtWidgets.QMessageBox.information( self.__main_page.main_window_instance, "Confirmation", "Are you sure you want to perform anonymization?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if save_reply == QtWidgets.QMessageBox.Yes: raw_dvh = self.patient_dict_container.get("raw_dvh") hashed_path = self.__main_page.call_class.run_anonymization( raw_dvh) self.patient_dict_container.set("hashed_path", hashed_path) # now that the radiomics data can just get copied across... # maybe skip this? radiomics_reply = QtWidgets.QMessageBox.information( self.__main_page.main_window_instance, "Confirmation", "Anonymization complete. Would you like to perform radiomics?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if radiomics_reply == QtWidgets.QMessageBox.Yes: self.__main_page.pyradi_trigger.emit( self.patient_dict_container.path, self.patient_dict_container.filepaths, hashed_path) def transect_handler(self): """ Function triggered when the Transect button is pressed from the menu. """ if self.is_four_view: view = self.__main_page.dicom_axial_view.view slider_id = self.__main_page.dicom_axial_view.slider.value() else: view = self.__main_page.dicom_single_view.view slider_id = self.__main_page.dicom_single_view.slider.value() dt = self.patient_dict_container.dataset[slider_id] row_s = dt.PixelSpacing[0] col_s = dt.PixelSpacing[1] dt.convert_pixel_data() pixmap = self.patient_dict_container.get("pixmaps_axial")[slider_id] self.__main_page.call_class.run_transect(self.__main_page, view, pixmap, dt._pixel_array.transpose(), row_s, col_s) def add_on_options_handler(self): self.__main_page.add_on_options_controller.show_add_on_options() def one_view_handler(self): self.is_four_view = False self.__main_page.dicom_view.setCurrentWidget( self.__main_page.dicom_single_view) self.__main_page.dicom_single_view.update_view() if hasattr(self.__main_page, 'image_fusion_view'): self.has_image_registration_four = False self.has_image_registration_single = True if isinstance(self.__main_page.image_fusion_single_view, ImageFusionAxialView): self.__main_page.image_fusion_view.setCurrentWidget( self.__main_page.image_fusion_single_view) self.__main_page.image_fusion_single_view.update_view() def four_views_handler(self): self.is_four_view = True self.__main_page.dicom_view.setCurrentWidget( self.__main_page.dicom_four_views) self.__main_page.dicom_axial_view.update_view() if hasattr(self.__main_page, 'image_fusion_view'): self.has_image_registration_four = True self.has_image_registration_single = False if isinstance(self.__main_page.image_fusion_view, QStackedWidget): self.__main_page.image_fusion_view.setCurrentWidget( self.__main_page.image_fusion_four_views) self.__main_page.image_fusion_view_axial.update_view() def cut_lines_handler(self): self.__main_page.toggle_cut_lines() def export_dvh_handler(self): if self.patient_dict_container.has_attribute("raw_dvh"): self.__main_page.dvh_tab.export_csv() else: QtWidgets.QMessageBox.information( self.__main_page, "Unable to export DVH", "DVH cannot be exported as there is no DVH present.", QtWidgets.QMessageBox.Ok) def pyradiomics_export_handler(self): self.__main_page.pyradi_trigger.emit( self.patient_dict_container.path, self.patient_dict_container.filepaths, '') def action_exit_handler(self): QtCore.QCoreApplication.exit(0)
def test_merge_rtss(qtbot, test_object): """Test merging rtss. This function creates a new rtss, then merges the new rtss with the old rtss and asserts that duplicated ROIs will be overwritten when the other being merged. :param test_object: test_object function, for accessing the shared TestStructureTab object. """ patient_dict_container = PatientDictContainer() # Create a new rtss dataset = patient_dict_container.dataset[0] rtss_path = Path(patient_dict_container.path).joinpath('rtss.dcm') new_rtss = create_initial_rtss_from_ct( dataset, rtss_path, ImageLoading.get_image_uid_list(patient_dict_container.dataset)) # Set ROIs rois = ImageLoading.get_roi_info(new_rtss) patient_dict_container.set("rois", rois) # Add a new ROI into the new rtss with the name of the first ROI in # the old rtss roi_name = test_object.rois.get(1)["name"] roi_coordinates = [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0] new_rtss = create_roi(new_rtss, roi_name, [{ 'coords': roi_coordinates, 'ds': dataset }]) # Add a new ROI with a new name roi_name = "NewTestROI" new_rtss = create_roi(new_rtss, roi_name, [{ 'coords': roi_coordinates, 'ds': dataset }]) # Set ROIs rois = ImageLoading.get_roi_info(new_rtss) patient_dict_container.set("rois", rois) patient_dict_container.set("existing_file_rtss", patient_dict_container.get("file_rtss")) patient_dict_container.set("dataset_rtss", new_rtss) # Merge the old and new rtss structure_tab = StructureTab() structure_tab.show_modified_indicator() qtbot.addWidget(structure_tab) def test_message_window(): messagebox = structure_tab.findChild(QtWidgets.QMessageBox) assert messagebox is not None yes_button = messagebox.buttons()[1] qtbot.mouseClick(yes_button, QtCore.Qt.LeftButton, delay=1) QtCore.QTimer.singleShot(1000, test_message_window) structure_tab.save_new_rtss_to_fixed_image_set(auto=True) merged_rtss = pydicom.read_file(patient_dict_container.get("file_rtss")) merged_rois = ImageLoading.get_roi_info(merged_rtss) assert (len(test_object.rois) + 1 == len(merged_rois))
def load(self, interrupt_flag, progress_callback): """ :param interrupt_flag: A threading.Event() object that tells the function to stop loading. :param progress_callback: A signal that receives the current progress of the loading. :return: PatientDictContainer object containing all values related to the loaded DICOM files. """ progress_callback.emit(("Creating datasets...", 0)) try: path = os.path.dirname(os.path.commonprefix(self.selected_files)) # Gets the common root folder. read_data_dict, file_names_dict = ImageLoading.get_datasets(self.selected_files) except ImageLoading.NotAllowedClassError: raise ImageLoading.NotAllowedClassError # Populate the initial values in the PatientDictContainer singleton. patient_dict_container = PatientDictContainer() patient_dict_container.clear() patient_dict_container.set_initial_values(path, read_data_dict, file_names_dict) # As there is no way to interrupt a QRunnable, this method must check after every step whether or not the # interrupt flag has been set, in which case it will interrupt this method after the currently processing # function has finished running. It's not very pretty, and the thread will still run some functions for, in some # cases, up to a couple seconds after the close button on the Progress Window has been clicked, however it's # the best solution I could come up with. If you have a cleaner alternative, please make your contribution. if interrupt_flag.is_set(): print("stopped") return False if 'rtss' in file_names_dict and 'rtdose' in file_names_dict: self.parent_window.signal_advise_calc_dvh.connect(self.update_calc_dvh) self.signal_request_calc_dvh.emit() while not self.advised_calc_dvh: pass if 'rtss' in file_names_dict: dataset_rtss = dcmread(file_names_dict['rtss']) progress_callback.emit(("Getting ROI info...", 10)) rois = ImageLoading.get_roi_info(dataset_rtss) if interrupt_flag.is_set(): # Stop loading. print("stopped") return False progress_callback.emit(("Getting contour data...", 30)) dict_raw_contour_data, dict_numpoints = ImageLoading.get_raw_contour_data(dataset_rtss) # Determine which ROIs are one slice thick dict_thickness = ImageLoading.get_thickness_dict(dataset_rtss, read_data_dict) if interrupt_flag.is_set(): # Stop loading. print("stopped") return False progress_callback.emit(("Getting pixel LUTs...", 50)) dict_pixluts = ImageLoading.get_pixluts(read_data_dict) if interrupt_flag.is_set(): # Stop loading. print("stopped") return False # Add RTSS values to PatientDictContainer patient_dict_container.set("rois", rois) patient_dict_container.set("raw_contour", dict_raw_contour_data) patient_dict_container.set("num_points", dict_numpoints) patient_dict_container.set("pixluts", dict_pixluts) if 'rtdose' in file_names_dict and self.calc_dvh: dataset_rtdose = dcmread(file_names_dict['rtdose']) # Spawn-based platforms (i.e Windows and MacOS) have a large overhead when creating a new process, which # ends up making multiprocessing on these platforms more expensive than linear calculation. As such, # multiprocessing is only available on Linux until a better solution is found. fork_safe_platforms = ['Linux'] if platform.system() in fork_safe_platforms: progress_callback.emit(("Calculating DVHs...", 60)) raw_dvh = ImageLoading.multi_calc_dvh(dataset_rtss, dataset_rtdose, rois, dict_thickness) else: progress_callback.emit(("Calculating DVHs... (This may take a while)", 60)) raw_dvh = ImageLoading.calc_dvhs(dataset_rtss, dataset_rtdose, rois, dict_thickness, interrupt_flag) if interrupt_flag.is_set(): # Stop loading. print("stopped") return False progress_callback.emit(("Converging to zero...", 80)) dvh_x_y = ImageLoading.converge_to_0_dvh(raw_dvh) if interrupt_flag.is_set(): # Stop loading. print("stopped") return False # Add DVH values to PatientDictContainer patient_dict_container.set("raw_dvh", raw_dvh) patient_dict_container.set("dvh_x_y", dvh_x_y) patient_dict_container.set("dvh_outdated", False) return True else: return True return True
class IsodoseTab(QtWidgets.QWidget): request_update_isodoses = QtCore.pyqtSignal() def __init__(self): QtWidgets.QWidget.__init__(self) self.patient_dict_container = PatientDictContainer() self.rx_dose_in_cgray = self.patient_dict_container.get("rx_dose_in_cgray") self.color_dict = self.init_color_isod() self.color_squares = self.init_color_squares() self.checkboxes = self.init_checkboxes() self.isodose_tab_layout = QtWidgets.QVBoxLayout() self.isodose_tab_layout.setAlignment(QtCore.Qt.AlignTop) self.isodose_tab_layout.setSpacing(0) self.init_layout() self.setLayout(self.isodose_tab_layout) def init_layout(self): for i in range(0, len(self.checkboxes)): widget_isodose = QtWidgets.QWidget() layout_isodose = QtWidgets.QHBoxLayout(widget_isodose) layout_isodose.setAlignment(QtCore.Qt.AlignLeft) layout_isodose.addWidget(self.color_squares[i]) layout_isodose.addWidget(self.checkboxes[i]) self.isodose_tab_layout.addWidget(widget_isodose) def init_color_isod(self): """ Create a list containing the colors for each isodose. :return: Dictionary where the key is the percentage of isodose and the value a QColor object. """ roi_color = { 107: QtGui.QColor(131, 0, 0), 105: QtGui.QColor(185, 0, 0), 100: QtGui.QColor(255, 46, 0), 95: QtGui.QColor(255, 161, 0), 90: QtGui.QColor(253, 255, 0), 80: QtGui.QColor(0, 255, 0), 70: QtGui.QColor(0, 143, 0), 60: QtGui.QColor(0, 255, 255), 30: QtGui.QColor(33, 0, 255), 10: QtGui.QColor(11, 0, 134) } return roi_color def init_color_squares(self): """ Create a color square. """ list_of_squares = [] for key, color in self.color_dict.items(): list_of_squares.append(self.draw_color_square(color)) return list_of_squares def init_checkboxes(self): """ Initialize the checkbox objects. """ list_of_checkboxes = [] # Values of Isodoses list_of_doses = [] for percentage in isodose_percentages: dose = int(self.rx_dose_in_cgray * (percentage / 100)) list_of_doses.append(dose) # Checkboxes first_iteration = True for i in range(10): if first_iteration: checkbox = QtWidgets.QCheckBox("%s %% / %s cGy [Max]" % (str(isodose_percentages[i]), str(list_of_doses[i]))) first_iteration = False else: checkbox = QtWidgets.QCheckBox("%s %% / %s cGy" % (str(isodose_percentages[i]), str(list_of_doses[i]))) checkbox.clicked.connect(lambda state, text=isodose_percentages[i]: self.checked_dose(state, text)) checkbox.setStyleSheet("font: 10pt \"Laksaman\";") list_of_checkboxes.append(checkbox) return list_of_checkboxes # Function triggered when a dose level selected # Updates the list of selected isodoses and dicom view def checked_dose(self, state, isod_value): """ Function triggered when the checkbox of a structure is checked / unchecked. Update the list of selected structures. Update the DICOM view. :param state: True if the checkbox is checked, False otherwise. :param isod_value: Percentage of isodose. """ selected_doses = self.patient_dict_container.get("selected_doses") if state: # Add the dose to the list of selected doses selected_doses.append(isod_value) else: # Remove dose from list of previously selected doses selected_doses.remove(isod_value) self.patient_dict_container.set("selected_doses", selected_doses) # Update the dicom view self.request_update_isodoses.emit() def draw_color_square(self, color): """ Create a color square. :param color: QColor object :return: Color square widget. """ color_square_label = QtWidgets.QLabel() color_square_pix = QtGui.QPixmap(15, 15) color_square_pix.fill(color) color_square_label.setPixmap(color_square_pix) return color_square_label
def create_initial_model_batch(): """ This function initializes all the attributes in the PatientDictContainer required for the operation of batch processing. It is a modified version of create_initial_model. This function only sets RTSS values in the PatientDictContainer if an RTSS exists. If one does not exist it will only be created if needed, whereas the original create_initial_model assumes that one is always created. This function also does not set SR attributes in the PatientDictContainer, as SRs are only needed for SR2CSV functions, which do not require the use of the PatientDictContainer. """ ############################## # LOAD PATIENT INFORMATION # ############################## patient_dict_container = PatientDictContainer() dataset = patient_dict_container.dataset filepaths = patient_dict_container.filepaths patient_dict_container.set("rtss_modified", False) if 'WindowWidth' in dataset[0]: if isinstance(dataset[0].WindowWidth, pydicom.valuerep.DSfloat): window = int(dataset[0].WindowWidth) elif isinstance(dataset[0].WindowWidth, pydicom.multival.MultiValue): window = int(dataset[0].WindowWidth[1]) else: window = int(400) if 'WindowCenter' in dataset[0]: if isinstance(dataset[0].WindowCenter, pydicom.valuerep.DSfloat): level = int(dataset[0].WindowCenter) elif isinstance(dataset[0].WindowCenter, pydicom.multival.MultiValue): level = int(dataset[0].WindowCenter[1]) else: level = int(800) patient_dict_container.set("window", window) patient_dict_container.set("level", level) # Check to see if the imageWindowing.csv file exists if os.path.exists(data_path('imageWindowing.csv')): # If it exists, read data from file into the self.dict_windowing # variable dict_windowing = {} with open(data_path('imageWindowing.csv'), "r") \ as fileInput: next(fileInput) dict_windowing["Normal"] = [window, level] for row in fileInput: # Format: Organ - Scan - Window - Level items = [item for item in row.split(',')] dict_windowing[items[0]] = [int(items[2]), int(items[3])] else: # If csv does not exist, initialize dictionary with default values dict_windowing = { "Normal": [window, level], "Lung": [1600, -300], "Bone": [1400, 700], "Brain": [160, 950], "Soft Tissue": [400, 800], "Head and Neck": [275, 900] } patient_dict_container.set("dict_windowing", dict_windowing) pixel_values = convert_raw_data(dataset) # Calculate the ratio between x axis and y axis of 3 views pixmap_aspect = {} pixel_spacing = dataset[0].PixelSpacing slice_thickness = dataset[0].SliceThickness pixmap_aspect["axial"] = pixel_spacing[1] / pixel_spacing[0] pixmap_aspect["sagittal"] = pixel_spacing[1] / slice_thickness pixmap_aspect["coronal"] = slice_thickness / pixel_spacing[0] pixmaps_axial, pixmaps_coronal, pixmaps_sagittal = \ get_pixmaps(pixel_values, window, level, pixmap_aspect) patient_dict_container.set("pixmaps_axial", pixmaps_axial) patient_dict_container.set("pixmaps_coronal", pixmaps_coronal) patient_dict_container.set("pixmaps_sagittal", pixmaps_sagittal) patient_dict_container.set("pixel_values", pixel_values) patient_dict_container.set("pixmap_aspect", pixmap_aspect) basic_info = get_basic_info(dataset[0]) patient_dict_container.set("basic_info", basic_info) patient_dict_container.set("dict_uid", dict_instance_uid(dataset)) # Set RTSS attributes if patient_dict_container.has_modality("rtss"): patient_dict_container.set("file_rtss", filepaths['rtss']) patient_dict_container.set("dataset_rtss", dataset['rtss']) dict_raw_contour_data, dict_numpoints = \ ImageLoading.get_raw_contour_data(dataset['rtss']) patient_dict_container.set("raw_contour", dict_raw_contour_data) dicom_tree_rtss = DicomTree(filepaths['rtss']) patient_dict_container.set("dict_dicom_tree_rtss", dicom_tree_rtss.dict) patient_dict_container.set( "list_roi_numbers", ordered_list_rois(patient_dict_container.get("rois"))) patient_dict_container.set("selected_rois", []) patient_dict_container.set("dict_polygons_axial", {}) patient_dict_container.set("dict_polygons_sagittal", {}) patient_dict_container.set("dict_polygons_coronal", {}) # Set RTDOSE attributes if patient_dict_container.has_modality("rtdose"): dicom_tree_rtdose = DicomTree(filepaths['rtdose']) patient_dict_container.set("dict_dicom_tree_rtdose", dicom_tree_rtdose.dict) patient_dict_container.set("dose_pixluts", get_dose_pixluts(dataset)) patient_dict_container.set("selected_doses", []) # overwritten if RTPLAN is present. patient_dict_container.set("rx_dose_in_cgray", 1) # Set RTPLAN attributes if patient_dict_container.has_modality("rtplan"): # the TargetPrescriptionDose is type 3 (optional), so it may not be # there However, it is preferable to the sum of the beam doses # DoseReferenceStructureType is type 1 (value is mandatory), but it # can have a value of ORGAN_AT_RISK rather than TARGET in which case # there will *not* be a TargetPrescriptionDose and even if it is # TARGET, that's no guarantee that TargetPrescriptionDose will be # encoded and have a value rx_dose_in_cgray = calculate_rx_dose_in_cgray(dataset["rtplan"]) patient_dict_container.set("rx_dose_in_cgray", rx_dose_in_cgray) dicom_tree_rtplan = DicomTree(filepaths['rtplan']) patient_dict_container.set("dict_dicom_tree_rtplan", dicom_tree_rtplan.dict)
def load_images(cls, patient_files, required_classes): """ Loads required datasets for the selected patient. :param patient_files: dictionary of classes and patient files. :param required_classes: list of classes required for the selected/current process. :return: True if all required datasets found, false otherwise. """ files = [] found_classes = set() # Loop through each item in patient_files for key, value in patient_files.items(): # If the item is an allowed class if key in cls.allowed_classes: for i in range(len(value)): # Add item's files to the files list files.extend(value[i].get_files()) # Get the modality name modality_name = cls.allowed_classes.get(key).get('name') # If the modality name is not found_classes, add it if modality_name not in found_classes \ and modality_name in required_classes: found_classes.add(modality_name) # Get the difference between required classes and found classes class_diff = set(required_classes).difference(found_classes) # If the dataset is missing required files, pass on it if len(class_diff) > 0: print("Skipping dataset. Missing required file(s) {}".format( class_diff)) return False # Try to get the datasets from the selected files try: # Convert paths to a common file system representation for i, file in enumerate(files): files[i] = Path(file).as_posix() read_data_dict, file_names_dict = cls.get_datasets(files) path = os.path.dirname( os.path.commonprefix(list(file_names_dict.values()))) # Otherwise raise an exception (OnkoDICOM does not support the # selected file type) except ImageLoading.NotAllowedClassError: raise ImageLoading.NotAllowedClassError # Populate the initial values in the PatientDictContainer patient_dict_container = PatientDictContainer() patient_dict_container.clear() patient_dict_container.set_initial_values(path, read_data_dict, file_names_dict) # If an RT Struct is included, set relevant values in the # PatientDictContainer if 'rtss' in file_names_dict: dataset_rtss = dcmread(file_names_dict['rtss']) rois = ImageLoading.get_roi_info(dataset_rtss) dict_raw_contour_data, dict_numpoints = \ ImageLoading.get_raw_contour_data(dataset_rtss) dict_pixluts = ImageLoading.get_pixluts(read_data_dict) # Add RT Struct values to PatientDictContainer patient_dict_container.set("rois", rois) patient_dict_container.set("raw_contour", dict_raw_contour_data) patient_dict_container.set("num_points", dict_numpoints) patient_dict_container.set("pixluts", dict_pixluts) return True
class DVHTab(QtWidgets.QWidget): def __init__(self): QtWidgets.QWidget.__init__(self) self.patient_dict_container = PatientDictContainer() self.dvh_calculated = self.patient_dict_container.has_attribute( "raw_dvh") self.rt_dose = self.patient_dict_container.dataset['rtdose'] self.raw_dvh = None self.dvh_x_y = None self.plot = None self.selected_rois = self.patient_dict_container.get("selected_rois") self.dvh_tab_layout = QtWidgets.QVBoxLayout() try: # Import the DVH from RT Dose self.import_rtdose() except (AttributeError, KeyError): # Construct the layout based on whether or not the DVH has # already been calculated. # TODO: convert to logging print("DVH data not in RT Dose.") if self.dvh_calculated: self.init_layout_dvh() else: self.init_layout_no_dvh() self.setLayout(self.dvh_tab_layout) def init_layout_dvh(self): """ Initialise the DVH tab's layout when DVH data exists. """ self.raw_dvh = self.patient_dict_container.get("raw_dvh") self.dvh_x_y = self.patient_dict_container.get("dvh_x_y") self.plot = self.plot_dvh() widget_plot = FigureCanvas(self.plot) button_layout = QtWidgets.QHBoxLayout() button_export = QtWidgets.QPushButton("Export DVH to CSV") button_export.clicked.connect(self.export_csv) button_layout.addWidget(button_export) # Added Recalculate button button_calc_dvh = QtWidgets.QPushButton("Recalculate DVH") button_calc_dvh.clicked.connect(self.prompt_calc_dvh) button_layout.addWidget(button_calc_dvh) self.dvh_tab_layout.setAlignment(QtCore.Qt.Alignment()) self.dvh_tab_layout.addWidget(widget_plot) self.dvh_tab_layout.addLayout(button_layout) def init_layout_no_dvh(self): """ Initialise the DVH tab's layout when DVH data does not exist. """ button_calc_dvh = QtWidgets.QPushButton("Calculate DVH") button_calc_dvh.clicked.connect(self.prompt_calc_dvh) self.dvh_tab_layout.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignCenter) self.dvh_tab_layout.addWidget(button_calc_dvh) def clear_layout(self): """ Clear the layout of the DVH tab. """ for i in reversed(range(self.dvh_tab_layout.count())): item = self.dvh_tab_layout.itemAt(i) if item.widget(): item.widget().setParent(None) else: for j in reversed(range(item.count())): item.itemAt(j).widget().setParent(None) def plot_dvh(self): """ :return: DVH plot using Matplotlib library. """ # Initialisation of the plots fig, ax = plt.subplots() fig.subplots_adjust(0.1, 0.15, 1, 1) # Maximum value for x axis max_xlim = 0 # Plot for all the ROIs selected in the left column of the window for roi in self.selected_rois: dvh = self.raw_dvh[int(roi)] # Plot only the ROIs whose volume is non equal to 0 if dvh.volume != 0: # Bincenters, obtained from the dvh object, give the x axis values # (Doses originally in Gy unit) bincenters = self.dvh_x_y[roi]['bincenters'] # print(self.dvh_x_y[roi]) # Counts, obtained from the dvh object, give the y axis values # (values between 0 and dvh.volume) counts = self.dvh_x_y[roi]['counts'] # Color of the line is the same as the color shown in the left column of the window color = self.patient_dict_container.get("roi_color_dict")[roi] color_R = color.red() / 255 color_G = color.green() / 255 color_B = color.blue() / 255 plt.plot(100 * bincenters, 100 * counts / dvh.volume, label=dvh.name, color=[color_R, color_G, color_B]) # Update the maximum value for x axis (usually different between ROIs) if (100 * bincenters[-1]) > max_xlim: max_xlim = 100 * bincenters[-1] plt.xlabel('Dose [%s]' % 'cGy') plt.ylabel('Volume [%s]' % '%') if dvh.name: plt.legend(loc='lower center', bbox_to_anchor=(0, 1, 5, 5)) # Set the range values for x and y axis ax.set_ylim([0, 105]) ax.set_xlim([0, max_xlim + 3]) # Create the grids on the plot major_ticks_y = np.arange(0, 105, 20) minor_ticks_y = np.arange(0, 105, 5) major_ticks_x = np.arange(0, max_xlim + 250, 1000) minor_ticks_x = np.arange(0, max_xlim + 250, 250) ax.set_xticks(major_ticks_x) ax.set_xticks(minor_ticks_x, minor=True) ax.set_yticks(major_ticks_y) ax.set_yticks(minor_ticks_y, minor=True) ax.grid(which='minor', alpha=0.2) ax.grid(which='major', alpha=0.5) # Add the legend at the bottom left of the graph if len(self.selected_rois) != 0: ax.legend(loc='upper left', bbox_to_anchor=(-0.1, -0.15), ncol=4) plt.subplots_adjust(bottom=0.3) return fig def prompt_calc_dvh(self): """ Prompt for DVH calculation. """ if platform.system() == "Linux": choice = \ QtWidgets.QMessageBox.question( self, "Calculate DVHs?", "Would you like to (re)calculate DVHs?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if choice == QtWidgets.QMessageBox.Yes: progress_window = \ CalculateDVHProgressWindow( self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) progress_window.signal_dvh_calculated.connect( self.dvh_calculation_finished) self.patient_dict_container.set("dvh_outdated", False) progress_window.exec_() self.export_rtdose() else: stylesheet_path = "" # Select appropriate style sheet if platform.system() == 'Darwin': stylesheet_path = Path.cwd().joinpath('res', 'stylesheet.qss') else: stylesheet_path = Path.cwd().joinpath( 'res', 'stylesheet-win-linux.qss') # Create a message box and add attributes mb = QtWidgets.QMessageBox() mb.setIcon(QtWidgets.QMessageBox.Question) mb.setWindowTitle("Calculate DVHs?") mb.setText("Would you like to (re)calculate DVHs?") button_no = QtWidgets.QPushButton("No") button_yes = QtWidgets.QPushButton("Yes") """ We want the buttons 'No' & 'Yes' to be displayed in that exact order. QMessageBox displays buttons in respect to their assigned roles. (0 first, then 0 and so on) 'AcceptRole' is 0 and 'RejectRole' is 1 thus by counterintuitively assigning 'No' to 'AcceptRole' and 'Yes' to 'RejectRole' the buttons are positioned as desired. """ mb.addButton(button_no, QtWidgets.QMessageBox.AcceptRole) mb.addButton(button_yes, QtWidgets.QMessageBox.RejectRole) # Apply stylesheet to the message box and add icon to the window mb.setStyleSheet(open(stylesheet_path).read()) mb.setWindowIcon( QtGui.QIcon( resource_path(Path.cwd().joinpath('res', 'images', 'btn-icons', 'onkodicom_icon.png')))) mb.exec_() if mb.clickedButton() == button_yes: progress_window = CalculateDVHProgressWindow( self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) progress_window.signal_dvh_calculated.connect( self.dvh_calculation_finished) self.patient_dict_container.set("dvh_outdated", False) progress_window.exec_() self.export_rtdose() def dvh_calculation_finished(self): # Clear the screen self.clear_layout() self.dvh_calculated = True self.init_layout_dvh() def update_plot(self): if self.dvh_calculated: # Get new list of selected rois that have DVHs calculated self.selected_rois = [ roi for roi in self.patient_dict_container.get("selected_rois") if roi in self.raw_dvh.keys() ] # Clear the current layout self.clear_layout() # If the DVH has become outdated, show the user an indicator advising them such. if self.patient_dict_container.get("dvh_outdated"): self.display_outdated_indicator() # Re-draw the plot and add to layout self.init_layout_dvh() def export_csv(self): path = self.patient_dict_container.path basic_info = self.patient_dict_container.get("basic_info") if not os.path.isdir(path + '/CSV'): os.mkdir(path + '/CSV') dvh2csv(self.raw_dvh, path + "/CSV/", 'DVH_' + basic_info['id'], basic_info['id']) QtWidgets.QMessageBox.information( self, "Message", "The DVH Data was saved successfully in your directory!", QtWidgets.QMessageBox.Ok) def export_rtdose(self): """ Exports DVH data into the RT Dose file in the dataset directory. """ dvh2rtdose(self.raw_dvh) QtWidgets.QMessageBox.information( self, "Message", "The DVH Data was saved successfully in your directory!", QtWidgets.QMessageBox.Ok) def import_rtdose(self): """ Import DVH data from an RT Dose. """ # Get DVH data result = rtdose2dvh() # If there is DVH data if bool(result): incomplete = result["diff"] result.pop("diff") dvh_x_y = ImageLoading.converge_to_0_dvh(result) self.patient_dict_container.set("raw_dvh", result) self.patient_dict_container.set("dvh_x_y", dvh_x_y) # If incomplete, tell the user about this if incomplete: self.patient_dict_container.set("dvh_outdated", True) self.display_outdated_indicator() # Initialise the display self.dvh_calculation_finished() else: result.pop("diff") self.init_layout_no_dvh() def display_outdated_indicator(self): self.modified_indicator_widget = QtWidgets.QWidget() self.modified_indicator_widget.setContentsMargins(8, 5, 8, 5) # self.modified_indicator_widget.setFixedHeight(35) modified_indicator_layout = QtWidgets.QHBoxLayout() modified_indicator_layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignLeft) modified_indicator_icon = QtWidgets.QLabel() modified_indicator_icon.setPixmap( QtGui.QPixmap( resource_path("res/images/btn-icons/alert_icon.png"))) modified_indicator_layout.addWidget(modified_indicator_icon) modified_indicator_text = QtWidgets.QLabel( "Contours have been modified since DVH calculation. Some DVHs may " "now be out of date.") modified_indicator_text.setStyleSheet("color: red") modified_indicator_layout.addWidget(modified_indicator_text) self.modified_indicator_widget.setLayout(modified_indicator_layout) self.dvh_tab_layout.addWidget(self.modified_indicator_widget, QtCore.Qt.AlignTop | QtCore.Qt.AlignTop)
class StructureTab(QtWidgets.QWidget): request_update_structures = QtCore.pyqtSignal() def __init__(self): QtWidgets.QWidget.__init__(self) self.patient_dict_container = PatientDictContainer() self.rois = self.patient_dict_container.get("rois") self.color_dict = self.init_color_roi() self.patient_dict_container.set("roi_color_dict", self.color_dict) self.structure_tab_layout = QtWidgets.QVBoxLayout() self.roi_delete_handler = ROIDelOption(self.structure_modified) self.roi_draw_handler = ROIDrawOption(self.structure_modified) # Create scrolling area widget to contain the content. self.scroll_area = QtWidgets.QScrollArea() self.scroll_area.setWidgetResizable(True) self.scroll_area_content = QtWidgets.QWidget(self.scroll_area) self.scroll_area.ensureWidgetVisible(self.scroll_area_content) # Create layout for checkboxes and colour squares self.layout_content = QtWidgets.QVBoxLayout(self.scroll_area_content) self.layout_content.setContentsMargins(0, 0, 0, 0) self.layout_content.setSpacing(0) self.layout_content.setAlignment(QtCore.Qt.AlignTop) # Create list of standard organ and volume names self.standard_organ_names = [] self.standard_volume_names = [] self.init_standard_names() # Create StructureWidget objects self.update_content() # Create ROI manipulation buttons self.button_roi_draw = QtWidgets.QPushButton() self.button_roi_delete = QtWidgets.QPushButton() self.roi_buttons = QtWidgets.QWidget() self.init_roi_buttons() # Set layout self.structure_tab_layout.addWidget(self.scroll_area) self.structure_tab_layout.addWidget(self.roi_buttons) self.setLayout(self.structure_tab_layout) def init_color_roi(self): """ Create a dictionary containing the colors for each structure. :return: Dictionary where the key is the ROI number and the value a QColor object. """ roi_color = dict() roi_contour_info = self.patient_dict_container.get( "dict_dicom_tree_rtss")['ROI Contour Sequence'] if len(roi_contour_info) > 0: for item, roi_dict in roi_contour_info.items(): # Note: the keys of roiContourInfo are "item 0", "item 1", etc. # As all the ROI structures are identified by the ROI numbers in the whole code, # we get the ROI number 'roi_id' by using the member 'list_roi_numbers' id = item.split()[1] roi_id = self.patient_dict_container.get("list_roi_numbers")[ int(id)] if 'ROI Display Color' in roi_contour_info[item]: RGB_list = roi_contour_info[item]['ROI Display Color'][0] red = RGB_list[0] green = RGB_list[1] blue = RGB_list[2] else: seed(1) red = randint(0, 255) green = randint(0, 255) blue = randint(0, 255) roi_color[roi_id] = QtGui.QColor(red, green, blue) return roi_color def init_standard_names(self): """ Create two lists containing standard organ and standard volume names as set by the Add-On options. """ with open(resource_path('src/data/csv/organName.csv'), 'r') as f: self.standard_organ_names = [] csv_input = csv.reader(f) header = next(f) # Ignore the "header" of the column for row in csv_input: self.standard_organ_names.append(row[0]) with open(resource_path('src/data/csv/volumeName.csv'), 'r') as f: self.standard_volume_names = [] csv_input = csv.reader(f) header = next(f) # Ignore the "header" of the column for row in csv_input: self.standard_volume_names.append(row[1]) def init_roi_buttons(self): icon_roi_delete = QtGui.QIcon() icon_roi_delete.addPixmap( QtGui.QPixmap( resource_path('src/res/images/btn-icons/delete_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) icon_roi_draw = QtGui.QIcon() icon_roi_draw.addPixmap( QtGui.QPixmap( resource_path('src/res/images/btn-icons/draw_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) #self.button_roi_delete.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.button_roi_delete.setIcon(icon_roi_delete) self.button_roi_delete.setText("Delete ROI") self.button_roi_delete.clicked.connect(self.roi_delete_clicked) #self.button_roi_draw.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) self.button_roi_draw.setIcon(icon_roi_draw) self.button_roi_draw.setText("Draw ROI") self.button_roi_draw.clicked.connect(self.roi_draw_clicked) layout_roi_buttons = QtWidgets.QHBoxLayout(self.roi_buttons) layout_roi_buttons.setContentsMargins(0, 0, 0, 0) layout_roi_buttons.addWidget(self.button_roi_draw) layout_roi_buttons.addWidget(self.button_roi_delete) def update_content(self): """ Add the contents (color square and checkbox) in the scrolling area widget. """ # Clear the children for i in reversed(range(self.layout_content.count())): self.layout_content.itemAt(i).widget().setParent(None) row = 0 for roi_id, roi_dict in self.rois.items(): # Creates a widget representing each ROI structure = StructureWidget(roi_id, self.color_dict[roi_id], roi_dict['name'], self) structure.structure_renamed.connect(self.structure_modified) self.layout_content.addWidget(structure) row += 1 self.scroll_area.setStyleSheet( "QScrollArea {background-color: #ffffff; border-style: none;}") self.scroll_area_content.setStyleSheet( "QWidget {background-color: #ffffff; border-style: none;}") self.scroll_area.setWidget(self.scroll_area_content) def roi_delete_clicked(self): self.roi_delete_handler.show_roi_delete_options() def roi_draw_clicked(self): self.roi_draw_handler.show_roi_draw_options() def structure_modified(self, changes): """ Executes when a structure is renamed/deleted. Displays indicator that structure has changed. changes is a tuple of (new_dataset, description_of_changes) description_of_changes follows the format {"type_of_change": value_of_change}. Examples: {"rename": ["TOOTH", "TEETH"]} represents that the TOOTH structure has been renamed to TEETH. {"delete": ["TEETH", "MAXILLA"]} represents that the TEETH and MAXILLA structures have been deleted. {"draw": "AORTA"} represents that a new structure AORTA has been drawn. """ new_dataset = changes[0] change_description = changes[1] # If this is the first time the RTSS has been modified, create a modified indicator giving the user the option # to save their new file. if self.patient_dict_container.get("rtss_modified") is False: self.show_modified_indicator() # If this is the first change made to the RTSS file, update the dataset with the new one so that OnkoDICOM # starts working off this dataset rather than the original RTSS file. self.patient_dict_container.set("rtss_modified", True) self.patient_dict_container.set("dataset_rtss", new_dataset) # Refresh ROIs in main page self.patient_dict_container.set("rois", ImageLoading.get_roi_info(new_dataset)) self.rois = self.patient_dict_container.get("rois") contour_data = ImageLoading.get_raw_contour_data(new_dataset) self.patient_dict_container.set("raw_contour", contour_data[0]) self.patient_dict_container.set("num_points", contour_data[1]) pixluts = ImageLoading.get_pixluts(self.patient_dict_container.dataset) self.patient_dict_container.set("pixluts", pixluts) self.patient_dict_container.set( "list_roi_numbers", ordered_list_rois(self.patient_dict_container.get("rois"))) self.patient_dict_container.set("selected_rois", []) self.patient_dict_container.set("dict_polygons", {}) if "draw" in change_description: dicom_tree_rtss = DicomTree(None) dicom_tree_rtss.dataset = new_dataset dicom_tree_rtss.dict = dicom_tree_rtss.dataset_to_dict( dicom_tree_rtss.dataset) self.patient_dict_container.set("dict_dicom_tree_rtss", dicom_tree_rtss.dict) self.color_dict = self.init_color_roi() self.patient_dict_container.set("roi_color_dict", self.color_dict) if self.patient_dict_container.has_attribute("raw_dvh"): # DVH will be outdated once changes to it are made, and recalculation will be required. self.patient_dict_container.set("dvh_outdated", True) if self.patient_dict_container.has_modality("raw_dvh"): # Rename structures in DVH list if "rename" in changes[1]: new_raw_dvh = self.patient_dict_container.get("raw_dvh") for key, dvh in new_raw_dvh.items(): if dvh.name == change_description["rename"][0]: dvh.name = change_description["rename"][1] break self.patient_dict_container.set("raw_dvh", new_raw_dvh) # Remove structures from DVH list - the only visible effect of this section is the exported DVH csv if "delete" in changes[1]: list_of_deleted = [] new_raw_dvh = self.patient_dict_container.get("raw_dvh") for key, dvh in new_raw_dvh.items(): if dvh.name in change_description["delete"]: list_of_deleted.append(key) for key in list_of_deleted: new_raw_dvh.pop(key) self.patient_dict_container.set("raw_dvh", new_raw_dvh) # Refresh ROIs in DVH tab and DICOM View self.request_update_structures.emit() # Refresh structure tab self.update_content() def show_modified_indicator(self): self.modified_indicator_widget = QtWidgets.QWidget() self.modified_indicator_widget.setContentsMargins(8, 5, 8, 5) modified_indicator_layout = QtWidgets.QHBoxLayout() modified_indicator_layout.setAlignment(QtCore.Qt.AlignLeft) modified_indicator_icon = QtWidgets.QLabel() modified_indicator_icon.setPixmap( QtGui.QPixmap( resource_path("src/res/images/btn-icons/alert_icon.png"))) modified_indicator_layout.addWidget(modified_indicator_icon) modified_indicator_text = QtWidgets.QLabel( "Structures have been modified") modified_indicator_text.setStyleSheet("color: red") modified_indicator_layout.addWidget(modified_indicator_text) self.modified_indicator_widget.setLayout(modified_indicator_layout) self.modified_indicator_widget.mouseReleaseEvent = self.save_new_rtss # When the widget is clicked, save the rtss # Temporarily remove the ROI modify buttons, add this indicator, then add them back again. # This ensure that the modifier appears above the ROI modify buttons. self.structure_tab_layout.removeWidget(self.roi_buttons) self.structure_tab_layout.addWidget(self.modified_indicator_widget) self.structure_tab_layout.addWidget(self.roi_buttons) def structure_checked(self, state, roi_id): """ Function triggered when the checkbox of a structure is checked / unchecked. Update the list of selected structures. Update the plot of the DVH and the DICOM view. :param state: True if the checkbox is checked, False otherwise. :param roi_id: ROI number """ selected_rois = self.patient_dict_container.get("selected_rois") if state: selected_rois.append(roi_id) else: selected_rois.remove(roi_id) self.patient_dict_container.set("selected_rois", selected_rois) self.request_update_structures.emit() def save_new_rtss(self, event=None): rtss_directory = str(Path( self.patient_dict_container.get("file_rtss"))) confirm_save = QtWidgets.QMessageBox.information( self, "Confirmation", "Are you sure you want to save the modified RTSTRUCT file? This will " "overwrite the existing file. This is not reversible.", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if confirm_save == QtWidgets.QMessageBox.Yes: self.patient_dict_container.get("dataset_rtss").save_as( rtss_directory) QtWidgets.QMessageBox.about(self.parentWidget(), "File saved", "The RTSTRUCT file has been saved.") self.patient_dict_container.set("rtss_modified", False) self.modified_indicator_widget.setParent(None)
def create_new_rtstruct(cls, progress_callback): """ Generates a new RTSS and edits the patient dict container. Used for batch processing. """ # Get common directory patient_dict_container = PatientDictContainer() file_path = patient_dict_container.filepaths.values() file_path = Path(os.path.commonpath(file_path)) # Get new RT Struct file path file_path = str(file_path.joinpath("rtss.dcm")) # Create RT Struct file progress_callback.emit(("Generating RT Structure Set", 60)) ct_uid_list = ImageLoading.get_image_uid_list( patient_dict_container.dataset) ds = ROI.create_initial_rtss_from_ct(patient_dict_container.dataset[0], file_path, ct_uid_list) ds.save_as(file_path) # Add RT Struct file path to patient dict container patient_dict_container.filepaths['rtss'] = file_path filepaths = patient_dict_container.filepaths # Add RT Struct dataset to patient dict container patient_dict_container.dataset['rtss'] = ds dataset = patient_dict_container.dataset # Set some patient dict container attributes patient_dict_container.set("file_rtss", filepaths['rtss']) patient_dict_container.set("dataset_rtss", dataset['rtss']) dicom_tree_rtss = DicomTree(filepaths['rtss']) patient_dict_container.set("dict_dicom_tree_rtss", dicom_tree_rtss.dict) dict_pixluts = ImageLoading.get_pixluts(patient_dict_container.dataset) patient_dict_container.set("pixluts", dict_pixluts) rois = ImageLoading.get_roi_info(ds) patient_dict_container.set("rois", rois) patient_dict_container.set("selected_rois", []) patient_dict_container.set("dict_polygons_axial", {}) patient_dict_container.set("rtss_modified", True)
class ActionHandler: """ This class is responsible for initializing all of the actions that will be used by the MainPage and its components. There exists a 1-to-1 relationship between this class and the MainPage. This class has access to the main page's attributes and components, however this access should only be used to provide functionality to the actions defined below. The instance of this class can be given to the main page's components in order to trigger actions. """ def __init__(self, main_page): self.__main_page = main_page self.patient_dict_container = PatientDictContainer() ############################## # Init all actions and icons # ############################## # Open patient self.icon_open = QtGui.QIcon() self.icon_open.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/open_patient_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_open = QtWidgets.QAction() self.action_open.setIcon(self.icon_open) self.action_open.setText("Open new patient") self.action_open.setIconVisibleInMenu(True) # Save RTSTRUCT changes action self.icon_save_structure = QtGui.QIcon() self.icon_save_structure.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/save_all_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_save_structure = QtWidgets.QAction() self.action_save_structure.setIcon(self.icon_save_structure) self.action_save_structure.setText("Save RTSTRUCT changes") self.action_save_structure.setIconVisibleInMenu(True) self.action_save_structure.triggered.connect(self.save_struct_handler) # Save as Anonymous Action self.icon_save_as_anonymous = QtGui.QIcon() self.icon_save_as_anonymous.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/anonlock_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_save_as_anonymous = QtWidgets.QAction() self.action_save_as_anonymous.setIcon(self.icon_save_as_anonymous) self.action_save_as_anonymous.setText("Save as Anonymous") self.action_save_as_anonymous.triggered.connect( self.anonymization_handler) # Exit action self.action_exit = QtWidgets.QAction() self.action_exit.setText("Exit") self.action_exit.triggered.connect(self.action_exit_handler) # Zoom Out Action self.icon_zoom_out = QtGui.QIcon() self.icon_zoom_out.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/zoom_out_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_zoom_out = QtWidgets.QAction() self.action_zoom_out.setIcon(self.icon_zoom_out) self.action_zoom_out.setIconVisibleInMenu(True) self.action_zoom_out.setText("Zoom Out") self.action_zoom_out.triggered.connect( self.__main_page.dicom_view.zoom_out) # Zoom In Action self.icon_zoom_in = QtGui.QIcon() self.icon_zoom_in.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/zoom_in_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_zoom_in = QtWidgets.QAction() self.action_zoom_in.setIcon(self.icon_zoom_in) self.action_zoom_in.setIconVisibleInMenu(True) self.action_zoom_in.setText("Zoom In") self.action_zoom_in.triggered.connect( self.__main_page.dicom_view.zoom_in) # Transect Action self.icon_transect = QtGui.QIcon() self.icon_transect.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/transect_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_transect = QtWidgets.QAction() self.action_transect.setIcon(self.icon_transect) self.action_transect.setIconVisibleInMenu(True) self.action_transect.setText("Transect") self.action_transect.triggered.connect(self.transect_handler) # Add-On Options Action self.icon_add_ons = QtGui.QIcon() self.icon_add_ons.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/management_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.action_add_ons = QtWidgets.QAction() self.action_add_ons.setIcon(self.icon_add_ons) self.action_add_ons.setIconVisibleInMenu(True) self.action_add_ons.setText("Add-On Options") self.action_add_ons.triggered.connect(self.add_on_options_handler) # Export Clinical Data Action self.action_clinical_data_export = QtWidgets.QAction() self.action_clinical_data_export.setText("Export Clinical Data") # TODO self.action_clinical_data_export.triggered.connect(clinical data check) # Export Pyradiomics Action self.action_pyradiomics_export = QtWidgets.QAction() self.action_pyradiomics_export.setText("Export Pyradiomics") self.action_pyradiomics_export.triggered.connect( self.pyradiomics_export_handler) # Export DVH Action self.action_dvh_export = QtWidgets.QAction() self.action_dvh_export.setText("Export DVH") self.action_dvh_export.triggered.connect(self.export_dvh_handler) # Create Windowing menu self.icon_windowing = QtGui.QIcon() self.icon_windowing.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/windowing_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On) self.menu_windowing = QtWidgets.QMenu() self.init_windowing_menu() # Create Export menu self.icon_export = QtGui.QIcon() self.icon_export.addPixmap( QtGui.QPixmap( resource_path( "src/res/images/btn-icons/export_purple_icon.png")), QtGui.QIcon.Normal, QtGui.QIcon.On, ) self.menu_export = QtWidgets.QMenu() self.menu_export.setTitle("Export") self.menu_export.addAction(self.action_clinical_data_export) self.menu_export.addAction(self.action_pyradiomics_export) self.menu_export.addAction(self.action_dvh_export) def init_windowing_menu(self): self.menu_windowing.setIcon(self.icon_windowing) self.menu_windowing.setTitle("Windowing") dict_windowing = self.patient_dict_container.get("dict_windowing") # Get the right order for windowing names names_ordered = sorted(dict_windowing.keys()) if "Normal" in dict_windowing.keys(): old_index = names_ordered.index("Normal") names_ordered.insert(0, names_ordered.pop(old_index)) # Create actions for each windowing item windowing_actions = [] for name in names_ordered: text = str(name) action_windowing_item = QtWidgets.QAction(self.menu_windowing) action_windowing_item.triggered.connect( lambda state, text=name: self.windowing_handler(state, text)) action_windowing_item.setText(text) windowing_actions.append(action_windowing_item) # For reasons beyond me, the actions have to be set as a child of the windowing menu *and* later be added to # the menu as well. You can't do one or the other, otherwise the menu won't populate. # Feel free to try fix (or at least explain why the action has to be set as the windowing menu's child twice) for item in windowing_actions: self.menu_windowing.addAction(item) def save_struct_handler(self): """ If there are changes to the RTSTRUCT detected, save the changes to disk. """ if self.patient_dict_container.get("rtss_modified"): self.__main_page.structures_tab.save_new_rtss() else: QtWidgets.QMessageBox.information( self.__main_page, "File not saved", "No changes to the RTSTRUCT file detected.") def windowing_handler(self, state, text): """ Function triggered when a window is selected from the menu. :param state: Variable not used. Present to be able to use a lambda function. :param text: The name of the window selected. """ # Get the values for window and level from the dict windowing_limits = self.patient_dict_container.get( "dict_windowing")[text] # Set window and level to the new values window = windowing_limits[0] level = windowing_limits[1] # Update the dictionary of pixmaps with the update window and level values pixel_values = self.patient_dict_container.get("pixel_values") pixmaps = get_pixmaps(pixel_values, window, level) self.patient_dict_container.set("window", window) self.patient_dict_container.set("level", level) self.patient_dict_container.set("pixmaps", pixmaps) self.__main_page.update_views() def anonymization_handler(self): """ Function triggered when the Anonymization button is pressed from the menu. """ save_reply = QtWidgets.QMessageBox.information( self.__main_page.main_window_instance, "Confirmation", "Are you sure you want to perform anonymization?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if save_reply == QtWidgets.QMessageBox.Yes: raw_dvh = self.patient_dict_container.get("raw_dvh") hashed_path = self.__main_page.call_class.runAnonymization(raw_dvh) self.patient_dict_container.set("hashed_path", hashed_path) # now that the radiomics data can just get copied across... maybe skip this? radiomics_reply = QtWidgets.QMessageBox.information( self.__main_page.main_window_instance, "Confirmation", "Anonymization complete. Would you like to perform radiomics?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if radiomics_reply == QtWidgets.QMessageBox.Yes: self.__main_page.pyradi_trigger.emit( self.patient_dict_container.path, self.patient_dict_container.filepaths, hashed_path) def transect_handler(self): """ Function triggered when the Transect button is pressed from the menu. """ id = self.__main_page.dicom_view.slider.value() dt = self.patient_dict_container.dataset[id] rowS = dt.PixelSpacing[0] colS = dt.PixelSpacing[1] dt.convert_pixel_data() pixmap = self.patient_dict_container.get("pixmaps")[id] self.__main_page.call_class.runTransect( self.__main_page, self.__main_page.dicom_view.view, pixmap, dt._pixel_array.transpose(), rowS, colS) def add_on_options_handler(self): self.__main_page.add_on_options_controller.show_add_on_options() def export_dvh_handler(self): if self.patient_dict_container.has_attribute("raw_dvh"): self.__main_page.dvh_tab.export_csv() else: QtWidgets.QMessageBox.information( self.__main_page, "Unable to export DVH", "DVH cannot be exported as there is no DVH present.", QtWidgets.QMessageBox.Ok) def pyradiomics_export_handler(self): self.__main_page.pyradi_trigger.emit( self.patient_dict_container.path, self.patient_dict_container.filepaths, '') def action_exit_handler(self): QtCore.QCoreApplication.exit(0)
def load_temp_rtss(self, path, progress_callback, interrupt_flag): """ Generate a temporary rtss and load its data into PatientDictContainer :param path: str. The common root folder of all DICOM files. :param progress_callback: A signal that receives the current progress of the loading. :param interrupt_flag: A threading.Event() object that tells the function to stop loading. """ progress_callback.emit(("Generating temporary rtss...", 20)) patient_dict_container = PatientDictContainer() rtss_path = Path(path).joinpath('rtss.dcm') uid_list = ImageLoading.get_image_uid_list( patient_dict_container.dataset) rtss = create_initial_rtss_from_ct(patient_dict_container.dataset[0], rtss_path, uid_list) if interrupt_flag.is_set(): # Stop loading. print("stopped") return False progress_callback.emit(("Loading temporary rtss...", 50)) # Set ROIs rois = ImageLoading.get_roi_info(rtss) patient_dict_container.set("rois", rois) # Set pixluts dict_pixluts = ImageLoading.get_pixluts(patient_dict_container.dataset) patient_dict_container.set("pixluts", dict_pixluts) # Add RT Struct file path and dataset to patient dict container patient_dict_container.filepaths['rtss'] = rtss_path patient_dict_container.dataset['rtss'] = rtss # Set some patient dict container attributes patient_dict_container.set("file_rtss", rtss_path) patient_dict_container.set("dataset_rtss", rtss) ordered_dict = DicomTree(None).dataset_to_dict(rtss) patient_dict_container.set("dict_dicom_tree_rtss", ordered_dict) patient_dict_container.set("selected_rois", [])
def windowing_model(text, init): """ Function triggered when a window is selected from the menu. :param text: The name of the window selected. :param init: list of bool to determine which views are chosen """ patient_dict_container = PatientDictContainer() moving_dict_container = MovingDictContainer() pt_ct_dict_container = PTCTDictContainer() # Get the values for window and level from the dict windowing_limits = patient_dict_container.get("dict_windowing")[text] # Set window and level to the new values window = windowing_limits[0] level = windowing_limits[1] # Update the dictionary of pixmaps with the update window and # level values if init[0]: pixel_values = patient_dict_container.get("pixel_values") pixmap_aspect = patient_dict_container.get("pixmap_aspect") pixmaps_axial, pixmaps_coronal, pixmaps_sagittal = \ get_pixmaps(pixel_values, window, level, pixmap_aspect) patient_dict_container.set("pixmaps_axial", pixmaps_axial) patient_dict_container.set("pixmaps_coronal", pixmaps_coronal) patient_dict_container.set("pixmaps_sagittal", pixmaps_sagittal) patient_dict_container.set("window", window) patient_dict_container.set("level", level) # Update CT if init[2]: ct_pixel_values = pt_ct_dict_container.get("ct_pixel_values") ct_pixmap_aspect = pt_ct_dict_container.get("ct_pixmap_aspect") ct_pixmaps_axial, ct_pixmaps_coronal, ct_pixmaps_sagittal = \ get_pixmaps(ct_pixel_values, window, level, ct_pixmap_aspect, fusion=True) pt_ct_dict_container.set("ct_pixmaps_axial", ct_pixmaps_axial) pt_ct_dict_container.set("ct_pixmaps_coronal", ct_pixmaps_coronal) pt_ct_dict_container.set("ct_pixmaps_sagittal", ct_pixmaps_sagittal) pt_ct_dict_container.set("ct_window", window) pt_ct_dict_container.set("ct_level", level) # Update PT if init[1]: pt_pixel_values = pt_ct_dict_container.get("pt_pixel_values") pt_pixmap_aspect = pt_ct_dict_container.get("pt_pixmap_aspect") pt_pixmaps_axial, pt_pixmaps_coronal, pt_pixmaps_sagittal = \ get_pixmaps(pt_pixel_values, window, level, pt_pixmap_aspect, fusion=True, color="Heat") pt_ct_dict_container.set("pt_pixmaps_axial", pt_pixmaps_axial) pt_ct_dict_container.set("pt_pixmaps_coronal", pt_pixmaps_coronal) pt_ct_dict_container.set("pt_pixmaps_sagittal", pt_pixmaps_sagittal) pt_ct_dict_container.set("pt_window", window) pt_ct_dict_container.set("pt_level", level) # Update Fusion if init[3]: fusion_axial, fusion_coronal, fusion_sagittal, tfm = \ get_fused_window(level, window) patient_dict_container.set("color_axial", fusion_axial) patient_dict_container.set("color_coronal", fusion_coronal) patient_dict_container.set("color_sagittal", fusion_sagittal) moving_dict_container.set("tfm", tfm)
class StructureTab(QtWidgets.QWidget): request_update_structures = QtCore.Signal() def __init__(self, moving=False): QtWidgets.QWidget.__init__(self) self.patient_dict_container = PatientDictContainer() self.moving_dict_container = MovingDictContainer() self.rois = self.patient_dict_container.get("rois") self.color_dict = self.init_color_roi(self.patient_dict_container) self.patient_dict_container.set("roi_color_dict", self.color_dict) self.structure_tab_layout = QtWidgets.QVBoxLayout() self.roi_delete_handler = ROIDelOption( self.fixed_container_structure_modified) self.roi_draw_handler = ROIDrawOption( self.fixed_container_structure_modified) self.roi_manipulate_handler = ROIManipulateOption( self.fixed_container_structure_modified) # Create scrolling area widget to contain the content. self.scroll_area = QtWidgets.QScrollArea() self.scroll_area.setWidgetResizable(True) self.scroll_area_content = QtWidgets.QWidget(self.scroll_area) self.scroll_area.ensureWidgetVisible(self.scroll_area_content) # Create layout for checkboxes and colour squares self.layout_content = QtWidgets.QVBoxLayout(self.scroll_area_content) self.layout_content.setContentsMargins(0, 0, 0, 0) self.layout_content.setSpacing(0) self.layout_content.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignTop) # Create list of standard organ and volume names self.standard_organ_names = [] self.standard_volume_names = [] self.init_standard_names() # Create StructureWidget objects self.update_content() # Create a modified indicator self.modified_indicator_widget = QtWidgets.QWidget() self.modified_indicator_widget.setContentsMargins(8, 5, 8, 5) modified_indicator_layout = QtWidgets.QHBoxLayout() modified_indicator_layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignLeft) modified_indicator_icon = QtWidgets.QLabel() modified_indicator_icon.setPixmap( QtGui.QPixmap( resource_path("res/images/btn-icons/alert_icon.png"))) modified_indicator_layout.addWidget(modified_indicator_icon) modified_indicator_text = QtWidgets.QLabel( "Structures have been modified") modified_indicator_text.setStyleSheet("color: red") modified_indicator_layout.addWidget(modified_indicator_text) self.modified_indicator_widget.setLayout(modified_indicator_layout) self.modified_indicator_widget.mouseReleaseEvent = self.\ save_new_rtss_to_fixed_image_set self.modified_indicator_widget.setVisible(False) # Create ROI manipulation buttons self.button_roi_manipulate = QtWidgets.QPushButton() self.button_roi_draw = QtWidgets.QPushButton() self.button_roi_delete = QtWidgets.QPushButton() self.roi_buttons = QtWidgets.QWidget() self.init_roi_buttons() # Set layout self.structure_tab_layout.addWidget(self.scroll_area) self.structure_tab_layout.addWidget(self.modified_indicator_widget) self.structure_tab_layout.addWidget(self.roi_buttons) self.setLayout(self.structure_tab_layout) def init_color_roi(self, dict_container): """ Create a dictionary containing the colors for each structure. :param: either PatientDictContainer or MovingDictContainer :return: Dictionary where the key is the ROI number and the value a QColor object. """ roi_color = dict() roi_contour_info = dict_container.get( "dict_dicom_tree_rtss")['ROI Contour Sequence'] if len(roi_contour_info) > 0: for item, roi_dict in roi_contour_info.items(): # Note: the keys of roiContourInfo are "item 0", "item 1", # etc. As all the ROI structures are identified by the ROI # numbers in the whole code, we get the ROI number 'roi_id' # by using the member 'list_roi_numbers' id = item.split()[1] roi_id = dict_container.get("list_roi_numbers")[int(id)] if 'ROI Display Color' in roi_contour_info[item]: RGB_list = roi_contour_info[item]['ROI Display Color'][0] red = RGB_list[0] green = RGB_list[1] blue = RGB_list[2] else: seed(1) red = randint(0, 255) green = randint(0, 255) blue = randint(0, 255) roi_color[roi_id] = QtGui.QColor(red, green, blue) return roi_color def init_standard_names(self): """ Create two lists containing standard organ and standard volume names as set by the Add-On options. """ with open(data_path('organName.csv'), 'r') as f: self.standard_organ_names = [] csv_input = csv.reader(f) header = next(f) # Ignore the "header" of the column for row in csv_input: self.standard_organ_names.append(row[0]) with open(data_path('volumeName.csv'), 'r') as f: self.standard_volume_names = [] csv_input = csv.reader(f) header = next(f) # Ignore the "header" of the column for row in csv_input: self.standard_volume_names.append(row[1]) def init_roi_buttons(self): icon_roi_delete = QtGui.QIcon() icon_roi_delete.addPixmap( QtGui.QPixmap( resource_path('res/images/btn-icons/delete_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) icon_roi_draw = QtGui.QIcon() icon_roi_draw.addPixmap( QtGui.QPixmap(resource_path('res/images/btn-icons/draw_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) icon_roi_manipulate = QtGui.QIcon() icon_roi_manipulate.addPixmap( QtGui.QPixmap( resource_path('res/images/btn-icons/manipulate_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) self.button_roi_delete.setIcon(icon_roi_delete) self.button_roi_delete.setText("Delete ROI") self.button_roi_delete.clicked.connect(self.roi_delete_clicked) self.button_roi_draw.setIcon(icon_roi_draw) self.button_roi_draw.setText("Draw ROI") self.button_roi_draw.clicked.connect(self.roi_draw_clicked) self.button_roi_manipulate.setIcon(icon_roi_manipulate) self.button_roi_manipulate.setText("Manipulate ROI") self.button_roi_manipulate.clicked.connect(self.roi_manipulate_clicked) layout_roi_buttons = QtWidgets.QVBoxLayout(self.roi_buttons) layout_roi_buttons.setContentsMargins(0, 0, 0, 0) layout_roi_buttons.addWidget(self.button_roi_draw) layout_roi_buttons.addWidget(self.button_roi_manipulate) layout_roi_buttons.addWidget(self.button_roi_delete) def update_ui(self, moving=False): """ Update the UI of Structure Tab when a new patient is opened """ self.patient_dict_container = PatientDictContainer() self.rois = self.patient_dict_container.get("rois") self.color_dict = self.init_color_roi(self.patient_dict_container) self.patient_dict_container.set("roi_color_dict", self.color_dict) if hasattr(self, "modified_indicator_widget"): self.modified_indicator_widget.setParent(None) self.update_content() def update_content(self): """ Add the contents (color square and checkbox) in the scrolling area widget. """ # Clear the children for i in reversed(range(self.layout_content.count())): self.layout_content.itemAt(i).widget().setParent(None) row = 0 for roi_id, roi_dict in self.rois.items(): # Creates a widget representing each ROI color = self.color_dict[roi_id] color.setAlpha(255) structure = StructureWidget(roi_id, color, roi_dict['name'], self) if roi_id in self.patient_dict_container.get("selected_rois"): structure.checkbox.setChecked(Qt.Checked) structure.structure_renamed.\ connect(self.fixed_container_structure_modified) self.layout_content.addWidget(structure) row += 1 self.scroll_area.setStyleSheet( "QScrollArea {background-color: #ffffff; border-style: none;}") self.scroll_area_content.setStyleSheet( "QWidget {background-color: #ffffff; border-style: none;}") self.scroll_area.setWidget(self.scroll_area_content) def roi_delete_clicked(self): self.roi_delete_handler.show_roi_delete_options() def roi_draw_clicked(self): self.roi_draw_handler.show_roi_draw_options() def roi_manipulate_clicked(self): """ Open ROI Manipulate Window """ self.roi_manipulate_handler.show_roi_manipulate_options( self.color_dict) def moving_container_structure_modified(self, changes): """ Executes when a structure of moving container is modified. Changes is a tuple of (new_dataset, description_of_changes) description_of_changes follows the format {"type_of_change": value_of_change}. Examples: {"rename": ["TOOTH", "TEETH"]} represents that the TOOTH structure has been renamed to TEETH. {"delete": ["TEETH", "MAXILLA"]} represents that the TEETH and MAXILLA structures have been deleted. {"draw": "AORTA"} represents that a new structure AORTA has been drawn. Note: Use {"draw": None} after multiple ROIs are generated (E.g., from ISO2ROI functionality), and use {"transfer":None} for ROI Transfer instead of calling this function multiple times. This will trigger auto save. """ new_dataset = changes[0] change_description = changes[1] # If this is the first change made to the RTSS file, update the # dataset with the new one so that OnkoDICOM starts working off this # dataset rather than the original RTSS file. self.moving_dict_container.set("rtss_modified", True) self.moving_dict_container.set("dataset_rtss", new_dataset) # Refresh ROIs in main page self.moving_dict_container.set("rois", ImageLoading.get_roi_info(new_dataset)) self.rois = self.moving_dict_container.get("rois") contour_data = ImageLoading.get_raw_contour_data(new_dataset) self.moving_dict_container.set("raw_contour", contour_data[0]) self.moving_dict_container.set("num_points", contour_data[1]) pixluts = ImageLoading.get_pixluts(self.moving_dict_container.dataset) self.moving_dict_container.set("pixluts", pixluts) self.moving_dict_container.set( "list_roi_numbers", ordered_list_rois(self.moving_dict_container.get("rois"))) self.moving_dict_container.set("selected_rois", []) self.moving_dict_container.set("dict_polygons_axial", {}) self.moving_dict_container.set("dict_polygons_sagittal", {}) self.moving_dict_container.set("dict_polygons_coronal", {}) if "draw" in change_description or "transfer" in change_description: dicom_tree_rtss = DicomTree(None) dicom_tree_rtss.dataset = new_dataset dicom_tree_rtss.dict = dicom_tree_rtss.dataset_to_dict( dicom_tree_rtss.dataset) self.moving_dict_container.set("dict_dicom_tree_rtss", dicom_tree_rtss.dict) self.color_dict = self.init_color_roi(self.moving_dict_container) self.moving_dict_container.set("roi_color_dict", self.color_dict) if self.moving_dict_container.has_attribute("raw_dvh"): # DVH will be outdated once changes to it are made, and # recalculation will be required. self.moving_dict_container.set("dvh_outdated", True) if self.moving_dict_container.has_modality("raw_dvh"): # Rename structures in DVH list if "rename" in change_description: new_raw_dvh = self.moving_dict_container.get("raw_dvh") for key, dvh in new_raw_dvh.items(): if dvh.name == change_description["rename"][0]: dvh.name = change_description["rename"][1] break self.moving_dict_container.set("raw_dvh", new_raw_dvh) # Remove structures from DVH list - the only visible effect of # this section is the exported DVH csv if "delete" in change_description: list_of_deleted = [] new_raw_dvh = self.moving_dict_container.get("raw_dvh") for key, dvh in new_raw_dvh.items(): if dvh.name in change_description["delete"]: list_of_deleted.append(key) for key in list_of_deleted: new_raw_dvh.pop(key) self.moving_dict_container.set("raw_dvh", new_raw_dvh) if "transfer" in change_description \ and change_description["transfer"] is None: self.save_new_rtss_to_moving_image_set() def fixed_container_structure_modified(self, changes): """ Executes when a structure of fixed patient container is modified Displays indicator that structure has changed. Changes is a tuple of (new_dataset, description_of_changes) description_of_changes follows the format {"type_of_change": value_of_change}. Examples: {"rename": ["TOOTH", "TEETH"]} represents that the TOOTH structure has been renamed to TEETH. {"delete": ["TEETH", "MAXILLA"]} represents that the TEETH and MAXILLA structures have been deleted. {"draw": "AORTA"} represents that a new structure AORTA has been drawn. Note: Use {"draw": None} after multiple ROIs are generated (E.g., from ISO2ROI functionality), and use {"transfer":None} for ROI Transfer instead of calling this function multiple times. This will trigger auto save. """ new_dataset = changes[0] change_description = changes[1] # Only show the modified indicator if description_of_changes is # not {"draw": None}, as this description means that the RTSS # is autosaved, and therefore there is no need to tell the user # that the RTSS has been modified if not("draw" in change_description and change_description["draw"] is None) and \ not ("transfer" in change_description): self.show_modified_indicator() # If this is the first change made to the RTSS file, update the # dataset with the new one so that OnkoDICOM starts working off this # dataset rather than the original RTSS file. self.patient_dict_container.set("rtss_modified", True) self.patient_dict_container.set("dataset_rtss", new_dataset) # Refresh ROIs in main page self.patient_dict_container.set("rois", ImageLoading.get_roi_info(new_dataset)) self.rois = self.patient_dict_container.get("rois") contour_data = ImageLoading.get_raw_contour_data(new_dataset) self.patient_dict_container.set("raw_contour", contour_data[0]) self.patient_dict_container.set("num_points", contour_data[1]) pixluts = ImageLoading.get_pixluts(self.patient_dict_container.dataset) self.patient_dict_container.set("pixluts", pixluts) self.patient_dict_container.set( "list_roi_numbers", ordered_list_rois(self.patient_dict_container.get("rois"))) self.patient_dict_container.set("selected_rois", []) self.patient_dict_container.set("dict_polygons_axial", {}) self.patient_dict_container.set("dict_polygons_sagittal", {}) self.patient_dict_container.set("dict_polygons_coronal", {}) if "draw" in change_description or "transfer" in change_description: dicom_tree_rtss = DicomTree(None) dicom_tree_rtss.dataset = new_dataset dicom_tree_rtss.dict = dicom_tree_rtss.dataset_to_dict( dicom_tree_rtss.dataset) self.patient_dict_container.set("dict_dicom_tree_rtss", dicom_tree_rtss.dict) self.color_dict = self.init_color_roi(self.patient_dict_container) self.patient_dict_container.set("roi_color_dict", self.color_dict) if self.patient_dict_container.has_attribute("raw_dvh"): # DVH will be outdated once changes to it are made, and # recalculation will be required. self.patient_dict_container.set("dvh_outdated", True) if self.patient_dict_container.has_attribute("raw_dvh"): # Rename structures in DVH list if "rename" in change_description: new_raw_dvh = self.patient_dict_container.get("raw_dvh") for key, dvh in new_raw_dvh.items(): if dvh.name == change_description["rename"][0]: dvh.name = change_description["rename"][1] break self.patient_dict_container.set("raw_dvh", new_raw_dvh) dvh2rtdose(new_raw_dvh) # Remove structures from DVH list - the only visible effect of # this section is the exported DVH csv if "delete" in change_description: list_of_deleted = [] new_raw_dvh = self.patient_dict_container.get("raw_dvh") for key, dvh in new_raw_dvh.items(): if dvh.name in change_description["delete"]: list_of_deleted.append(key) for key in list_of_deleted: new_raw_dvh.pop(key) self.patient_dict_container.set("raw_dvh", new_raw_dvh) dvh2rtdose(new_raw_dvh) # Refresh ROIs in DVH tab and DICOM View self.request_update_structures.emit() # Refresh structure tab self.update_content() if "draw" in change_description and change_description["draw"] is None: self.save_new_rtss_to_fixed_image_set(auto=True) elif "transfer" in change_description \ and change_description["transfer"] is None: self.save_new_rtss_to_fixed_image_set(auto=True) def show_modified_indicator(self): self.modified_indicator_widget.setVisible(True) def structure_checked(self, state, roi_id): """ Function triggered when the checkbox of a structure is checked / unchecked. Update the list of selected structures. Update the plot of the DVH and the DICOM view. :param state: True if the checkbox is checked, False otherwise. :param roi_id: ROI number """ selected_rois = self.patient_dict_container.get("selected_rois") if state: selected_rois.append(roi_id) else: selected_rois.remove(roi_id) self.patient_dict_container.set("selected_rois", selected_rois) self.update_dict_polygons(state, roi_id) self.request_update_structures.emit() def update_dict_polygons(self, state, roi_id): """ Update the polygon dictionaries (axial, coronal, sagittal) used to display the ROIs. :param state: True if the ROI is selected, False otherwise :param roi_id: ROI number """ rois = self.patient_dict_container.get("rois") new_dict_polygons_axial = self.patient_dict_container.get( "dict_polygons_axial") new_dict_polygons_coronal = self.patient_dict_container.get( "dict_polygons_coronal") new_dict_polygons_sagittal = self.patient_dict_container.get( "dict_polygons_sagittal") aspect = self.patient_dict_container.get("pixmap_aspect") roi_name = rois[roi_id]['name'] if state: new_dict_polygons_axial[roi_name] = {} new_dict_polygons_coronal[roi_name] = {} new_dict_polygons_sagittal[roi_name] = {} dict_rois_contours_axial = get_roi_contour_pixel( self.patient_dict_container.get("raw_contour"), [roi_name], self.patient_dict_container.get("pixluts")) dict_rois_contours_coronal, dict_rois_contours_sagittal = \ transform_rois_contours( dict_rois_contours_axial) for slice_id in self.patient_dict_container.get( "dict_uid").values(): polygons = calc_roi_polygon(roi_name, slice_id, dict_rois_contours_axial) new_dict_polygons_axial[roi_name][slice_id] = polygons for slice_id in range( 0, len(self.patient_dict_container.get("pixmaps_coronal"))): polygons_coronal = calc_roi_polygon( roi_name, slice_id, dict_rois_contours_coronal, aspect["coronal"]) polygons_sagittal = calc_roi_polygon( roi_name, slice_id, dict_rois_contours_sagittal, 1 / aspect["sagittal"]) new_dict_polygons_coronal[roi_name][ slice_id] = polygons_coronal new_dict_polygons_sagittal[roi_name][ slice_id] = polygons_sagittal self.patient_dict_container.set("dict_polygons_axial", new_dict_polygons_axial) self.patient_dict_container.set("dict_polygons_coronal", new_dict_polygons_coronal) self.patient_dict_container.set("dict_polygons_sagittal", new_dict_polygons_sagittal) else: new_dict_polygons_axial.pop(roi_name, None) new_dict_polygons_coronal.pop(roi_name, None) new_dict_polygons_sagittal.pop(roi_name, None) def on_rtss_selected(self, selected_rtss): """ Function to run after a rtss is selected from SelectRTSSPopUp """ self.patient_dict_container.get("existing_rtss_files").clear() self.patient_dict_container.get("existing_rtss_files").append( selected_rtss) self.save_new_rtss(auto=True) def display_select_rtss_window(self): """ Display a pop up window that contains all RTSSs attached to the selected image set. """ self.select_rtss_window = SelectRTSSPopUp( self.patient_dict_container.get("existing_rtss_files"), parent=self) self.select_rtss_window.signal_rtss_selected.connect( self.on_rtss_selected) self.select_rtss_window.show() def save_new_rtss_to_fixed_image_set(self, event=None, auto=False): """ Save the current RTSS stored in fixed patient dictionary to the file system. :param event: Not used but will be passed as an argument from modified_indicator_widget on mouseReleaseEvent :param auto: Used for auto save without user confirmation """ existing_rtss_files = self.patient_dict_container.get( "existing_rtss_files") if len(existing_rtss_files) == 1: if isinstance(existing_rtss_files[0], Series): existing_rtss_directory = str( Path(existing_rtss_files[0].get_files()[0])) else: # This "else" is used by iso2roi gui and structure tab tests to # quickly set existing_rtss_directory existing_rtss_directory = existing_rtss_files[0] elif len(existing_rtss_files) > 1: self.display_select_rtss_window() # This function will be called again when a RTSS is selected return else: existing_rtss_directory = None rtss_directory = str(Path( self.patient_dict_container.get("file_rtss"))) if auto: confirm_save = QtWidgets.QMessageBox.Yes else: confirm_save = \ QtWidgets.QMessageBox.information(self, "Confirmation", "Are you sure you want to " "save the modified RTSTRUCT " "file? This will overwrite " "the existing file. This is " "not reversible.", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if confirm_save == QtWidgets.QMessageBox.Yes: if existing_rtss_directory is None: self.patient_dict_container.get("dataset_rtss").save_as( rtss_directory) else: new_rtss = self.patient_dict_container.get("dataset_rtss") old_rtss = pydicom.dcmread(existing_rtss_directory, force=True) old_roi_names = \ set(value["name"] for value in ImageLoading.get_roi_info(old_rtss).values()) new_roi_names = \ set(value["name"] for value in self.patient_dict_container.get("rois").values()) duplicated_names = old_roi_names.intersection(new_roi_names) # stop if there are conflicting roi names and user do not # wish to proceed. if duplicated_names and not self.display_confirm_merge( duplicated_names): return merged_rtss = merge_rtss(old_rtss, new_rtss, duplicated_names) merged_rtss.save_as(existing_rtss_directory) if not auto: QtWidgets.QMessageBox.about( self.parentWidget(), "File saved", "The RTSTRUCT file has been saved.") self.patient_dict_container.set("rtss_modified", False) # Hide the modified indicator self.modified_indicator_widget.setVisible(False) def save_new_rtss_to_moving_image_set(self, event=None): """ Save the current RTSS stored in moving patient dictionary to the file system. ROIs modification into moving patient dict is auto saved :param event: Not used but will be passed as an argument from modified_indicator_widget on mouseReleaseEvent """ if self.moving_dict_container.get("existing_file_rtss") is not None: existing_rtss_directory = str( Path(self.moving_dict_container.get("existing_file_rtss"))) else: existing_rtss_directory = None rtss_directory = str(Path(self.moving_dict_container.get("file_rtss"))) if existing_rtss_directory is None: self.moving_dict_container.get("dataset_rtss").save_as( rtss_directory) else: new_rtss = self.moving_dict_container.get("dataset_rtss") old_rtss = pydicom.dcmread(existing_rtss_directory, force=True) old_roi_names = \ set(value["name"] for value in ImageLoading.get_roi_info(old_rtss).values()) new_roi_names = \ set(value["name"] for value in self.moving_dict_container.get("rois").values()) duplicated_names = old_roi_names.intersection(new_roi_names) merged_rtss = merge_rtss(old_rtss, new_rtss, duplicated_names) merged_rtss.save_as(existing_rtss_directory) self.moving_dict_container.set("rtss_modified", False) def display_confirm_merge(self, duplicated_names): confirm_merge = QtWidgets.QMessageBox(parent=self) confirm_merge.setIcon(QtWidgets.QMessageBox.Question) confirm_merge.setWindowTitle("Merge RTSTRUCTs?") confirm_merge.setText("Conflicting ROI names found between new ROIs " "and existing ROIs:\n" + str(duplicated_names) + "\nAre you sure you want to merge the RTSTRUCT " "files? The new ROIs will replace the existing " "ROIs. ") button_yes = QtWidgets.QPushButton("Yes, I want to merge") button_no = QtWidgets.QPushButton("No, I will change the names") """ We want the buttons 'No' & 'Yes' to be displayed in that exact order. QMessageBox displays buttons in respect to their assigned roles. (0 first, then 0 and so on) 'AcceptRole' is 0 and 'RejectRole' is 1 thus by counterintuitively assigning 'No' to 'AcceptRole' and 'Yes' to 'RejectRole' the buttons are positioned as desired. """ confirm_merge.addButton(button_no, QtWidgets.QMessageBox.AcceptRole) confirm_merge.addButton(button_yes, QtWidgets.QMessageBox.RejectRole) confirm_merge.exec_() if confirm_merge.clickedButton() == button_yes: return True return False
def create_initial_model(): """ This function initializes all the attributes in the PatientDictContainer model required for the operation of the main window. This should be called before the main window's components are constructed, but after the initial values of the PatientDictContainer instance are set (i.e. dataset and filepaths). """ ############################## # LOAD PATIENT INFORMATION # ############################## patient_dict_container = PatientDictContainer() dataset = patient_dict_container.dataset filepaths = patient_dict_container.filepaths patient_dict_container.set("rtss_modified", False) if ('WindowWidth' in dataset[0]): if isinstance(dataset[0].WindowWidth, pydicom.valuerep.DSfloat): window = int(dataset[0].WindowWidth) elif isinstance(dataset[0].WindowWidth, pydicom.multival.MultiValue): window = int(dataset[0].WindowWidth[1]) else: window = int(400) if ('WindowCenter' in dataset[0]): if isinstance(dataset[0].WindowCenter, pydicom.valuerep.DSfloat): level = int(dataset[0].WindowCenter) elif isinstance(dataset[0].WindowCenter, pydicom.multival.MultiValue): level = int(dataset[0].WindowCenter[1]) else: level = int(800) patient_dict_container.set("window", window) patient_dict_container.set("level", level) # Check to see if the imageWindowing.csv file exists if os.path.exists(resource_path('data/csv/imageWindowing.csv')): # If it exists, read data from file into the self.dict_windowing variable dict_windowing = {} with open(resource_path('data/csv/imageWindowing.csv'), "r") as fileInput: next(fileInput) dict_windowing["Normal"] = [window, level] for row in fileInput: # Format: Organ - Scan - Window - Level items = [item for item in row.split(',')] dict_windowing[items[0]] = [int(items[2]), int(items[3])] else: # If csv does not exist, initialize dictionary with default values dict_windowing = { "Normal": [window, level], "Lung": [1600, -300], "Bone": [1400, 700], "Brain": [160, 950], "Soft Tissue": [400, 800], "Head and Neck": [275, 900] } patient_dict_container.set("dict_windowing", dict_windowing) pixel_values = convert_raw_data(dataset) pixmaps = get_pixmaps(pixel_values, window, level) patient_dict_container.set("pixmaps", pixmaps) patient_dict_container.set("pixel_values", pixel_values) basic_info = get_basic_info(dataset[0]) patient_dict_container.set("basic_info", basic_info) patient_dict_container.set("dict_uid", dict_instanceUID(dataset)) # Set RTSS attributes if patient_dict_container.has_modality("rtss"): patient_dict_container.set("file_rtss", filepaths['rtss']) patient_dict_container.set("dataset_rtss", dataset['rtss']) dicom_tree_rtss = DicomTree(filepaths['rtss']) patient_dict_container.set("dict_dicom_tree_rtss", dicom_tree_rtss.dict) patient_dict_container.set( "list_roi_numbers", ordered_list_rois(patient_dict_container.get("rois"))) patient_dict_container.set("selected_rois", []) patient_dict_container.set("dict_polygons", {}) # Set RTDOSE attributes if patient_dict_container.has_modality("rtdose"): dicom_tree_rtdose = DicomTree(filepaths['rtdose']) patient_dict_container.set("dict_dicom_tree_rtdose", dicom_tree_rtdose.dict) patient_dict_container.set("dose_pixluts", get_dose_pixluts(dataset)) patient_dict_container.set("selected_doses", []) patient_dict_container.set( "rx_dose_in_cgray", 1) # This will be overwritten if an RTPLAN is present. # Set RTPLAN attributes if patient_dict_container.has_modality("rtplan"): # the TargetPrescriptionDose is type 3 (optional), so it may not be there # However, it is preferable to the sum of the beam doses # DoseReferenceStructureType is type 1 (value is mandatory), # but it can have a value of ORGAN_AT_RISK rather than TARGET # in which case there will *not* be a TargetPrescriptionDose # and even if it is TARGET, that's no guarantee that TargetPrescriptionDose # will be encoded and have a value rx_dose_in_cgray = calculate_rx_dose_in_cgray(dataset["rtplan"]) patient_dict_container.set("rx_dose_in_cgray", rx_dose_in_cgray) dicom_tree_rtplan = DicomTree(filepaths['rtplan']) patient_dict_container.set("dict_dicom_tree_rtplan", dicom_tree_rtplan.dict)
class IsodoseTab(QtWidgets.QWidget): request_update_isodoses = QtCore.Signal() request_update_ui = QtCore.Signal(tuple) def __init__(self): QtWidgets.QWidget.__init__(self) self.patient_dict_container = PatientDictContainer() self.rx_dose_in_cgray = self.patient_dict_container.get( "rx_dose_in_cgray") self.color_dict = self.init_color_isod() self.color_squares = self.init_color_squares() self.checkboxes = self.init_checkboxes() # Create and initialise ISO2ROI button and layout self.iso2roi_button = QtWidgets.QPushButton() self.iso2roi_button.setText("Convert Isodoses to ROIs") self.iso2roi_button.clicked.connect(self.iso2roi_button_clicked) self.iso2roi_layout = QtWidgets.QHBoxLayout() self.iso2roi_layout.setContentsMargins(0, 0, 0, 0) self.iso2roi_layout.addWidget(self.iso2roi_button) self.isodose_tab_layout = QtWidgets.QVBoxLayout() self.isodose_tab_layout.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignTop) self.isodose_tab_layout.setSpacing(0) self.init_layout() self.iso2roi = ISO2ROI() # Add button to tab self.isodose_tab_layout.addStretch() self.isodose_tab_layout.addLayout(self.iso2roi_layout) self.setLayout(self.isodose_tab_layout) self.progress_window = ProgressWindow( self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) self.progress_window.signal_loaded.connect(self.on_loaded_iso2roi) def init_layout(self): for i in range(0, len(self.checkboxes)): widget_isodose = QtWidgets.QWidget() layout_isodose = QtWidgets.QHBoxLayout(widget_isodose) layout_isodose.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignLeft) layout_isodose.addWidget(self.color_squares[i]) layout_isodose.addWidget(self.checkboxes[i]) self.isodose_tab_layout.addWidget(widget_isodose) def init_color_isod(self): """ Create a list containing the colors for each isodose. :return: Dictionary where the key is the percentage of isodose and the value a QColor object. """ roi_color = { 107: QtGui.QColor(131, 0, 0), 105: QtGui.QColor(185, 0, 0), 100: QtGui.QColor(255, 46, 0), 95: QtGui.QColor(255, 161, 0), 90: QtGui.QColor(253, 255, 0), 80: QtGui.QColor(0, 255, 0), 70: QtGui.QColor(0, 143, 0), 60: QtGui.QColor(0, 255, 255), 30: QtGui.QColor(33, 0, 255), 10: QtGui.QColor(11, 0, 134) } return roi_color def init_color_squares(self): """ Create a color square. """ list_of_squares = [] for key, color in self.color_dict.items(): list_of_squares.append(self.draw_color_square(color)) return list_of_squares def init_checkboxes(self): """ Initialize the checkbox objects. """ list_of_checkboxes = [] # Values of Isodoses list_of_doses = [] for percentage in isodose_percentages: dose = int(self.rx_dose_in_cgray * (percentage / 100)) list_of_doses.append(dose) # Checkboxes def generate_clicked_handler(text): def handler(state): self.checked_dose(state, text) return handler first_iteration = True for i in range(10): if first_iteration: checkbox = QtWidgets.QCheckBox( "%s %% / %s cGy [Max]" % (str(isodose_percentages[i]), str(list_of_doses[i]))) first_iteration = False else: checkbox = QtWidgets.QCheckBox( "%s %% / %s cGy" % (str(isodose_percentages[i]), str(list_of_doses[i]))) checkbox.clicked.connect( generate_clicked_handler(isodose_percentages[i])) checkbox.setStyleSheet("font: 10pt \"Laksaman\";") list_of_checkboxes.append(checkbox) return list_of_checkboxes # Function triggered when a dose level selected # Updates the list of selected isodoses and dicom view def checked_dose(self, state, isod_value): """ Function triggered when the checkbox of a structure is checked / unchecked. Update the list of selected structures. Update the DICOM view. :param state: True if the checkbox is checked, False otherwise. :param isod_value: Percentage of isodose. """ selected_doses = self.patient_dict_container.get("selected_doses") if state: # Add the dose to the list of selected doses selected_doses.append(isod_value) else: # Remove dose from list of previously selected doses selected_doses.remove(isod_value) self.patient_dict_container.set("selected_doses", selected_doses) # Update the dicom view self.request_update_isodoses.emit() def draw_color_square(self, color): """ Create a color square. :param color: QColor object :return: Color square widget. """ color_square_label = QtWidgets.QLabel() color_square_pix = QtGui.QPixmap(15, 15) color_square_pix.fill(color) color_square_label.setPixmap(color_square_pix) return color_square_label def iso2roi_button_clicked(self): """ Clicked action handler for the ISO2ROI button. Opens a progress window and Initiates the ISO2ROI conversion process. """ self.progress_window.start(self.iso2roi.start_conversion) def on_loaded_iso2roi(self): """ Called when progress bar has finished. Closes the progress window and refreshes the main screen. """ self.request_update_ui.emit( (self.patient_dict_container.get('dataset_rtss'), { "draw": None })) self.progress_window.close()
class DicomView(QtWidgets.QWidget): def __init__(self, roi_color=None, iso_color=None): QtWidgets.QWidget.__init__(self) self.patient_dict_container = PatientDictContainer() self.iso_color = iso_color self.zoom = 1 self.current_slice_number = None self.dicom_view_layout = QtWidgets.QHBoxLayout() # Create components self.slider = QtWidgets.QSlider(QtCore.Qt.Vertical) self.init_slider() self.view = QtWidgets.QGraphicsView() self.init_view() self.scene = QtWidgets.QGraphicsScene() # Set layout self.dicom_view_layout.addWidget(self.view) self.dicom_view_layout.addWidget(self.slider) self.setLayout(self.dicom_view_layout) # Init metadata widgets self.metadata_layout = QtWidgets.QVBoxLayout(self.view) self.label_image_id = QtWidgets.QLabel() self.label_image_pos = QtWidgets.QLabel() self.label_wl = QtWidgets.QLabel() self.label_image_size = QtWidgets.QLabel() self.label_zoom = QtWidgets.QLabel() self.label_patient_pos = QtWidgets.QLabel() self.init_metadata() self.update_view() def init_slider(self): """ Create a slider for the DICOM Image View. """ pixmaps = self.patient_dict_container.get("pixmaps") self.slider.setMinimum(0) self.slider.setMaximum(len(pixmaps) - 1) self.slider.setValue(int(len(pixmaps) / 2)) self.slider.setTickPosition(QtWidgets.QSlider.TicksLeft) self.slider.setTickInterval(1) self.slider.valueChanged.connect(self.value_changed) def init_view(self): """ Create a view widget for DICOM image. """ self.view.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform) background_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0), QtCore.Qt.SolidPattern) self.view.setBackgroundBrush(background_brush) self.view.setGeometry(QtCore.QRect(0, 0, 877, 517)) def init_metadata(self): """ Create and place metadata on the view widget. """ # Position of the labels on the DICOM view. self.label_image_id.setAlignment(QtCore.Qt.AlignTop) self.label_image_pos.setAlignment(QtCore.Qt.AlignTop) self.label_wl.setAlignment(QtCore.Qt.AlignRight) self.label_image_size.setAlignment(QtCore.Qt.AlignBottom) self.label_zoom.setAlignment(QtCore.Qt.AlignBottom) self.label_patient_pos.setAlignment(QtCore.Qt.AlignRight) # Set all labels to white stylesheet = "QLabel { color : white; }" self.label_image_id.setStyleSheet(stylesheet) self.label_image_pos.setStyleSheet(stylesheet) self.label_wl.setStyleSheet(stylesheet) self.label_image_size.setStyleSheet(stylesheet) self.label_zoom.setStyleSheet(stylesheet) self.label_patient_pos.setStyleSheet(stylesheet) # The following layout was originally accomplished using a QGridLayout with QSpaceItems to anchor the labels # to the corners of the DICOM view. This caused a reintroduction of the tedious memory issues that were fixed # with the restructure. The following was rewritten to not use QSpaceItems because they, for reasons unknown, # caused a memory leak resulting in the entire patient dictionary not being cleared from memory correctly, # leaving hundreds of additional megabytes unused in memory each time a new patient was opened. # Create a widget to contain the two top-left labels top_left_widget = QtWidgets.QWidget() top_left = QtWidgets.QVBoxLayout(top_left_widget) top_left.addWidget(self.label_image_id, QtCore.Qt.AlignTop) top_left.addWidget(self.label_image_pos, QtCore.Qt.AlignTop) # Create a widget to contain the top-right label top_right_widget = QtWidgets.QWidget() top_right = QtWidgets.QVBoxLayout(top_right_widget) top_right.addWidget(self.label_wl, QtCore.Qt.AlignTop) # Create a widget to contain the two top widgets top_widget = QtWidgets.QWidget() top_widget.setFixedHeight(100) top = QtWidgets.QHBoxLayout(top_widget) top.addWidget(top_left_widget, QtCore.Qt.AlignLeft) top.addWidget(top_right_widget, QtCore.Qt.AlignRight) # Create a widget to contain the two bottom-left labels bottom_left_widget = QtWidgets.QWidget() bottom_left = QtWidgets.QVBoxLayout(bottom_left_widget) bottom_left.addWidget(self.label_image_size, QtCore.Qt.AlignBottom) bottom_left.addWidget(self.label_zoom, QtCore.Qt.AlignBottom) # Create a widget to contain the bottom-right label bottom_right_widget = QtWidgets.QWidget() bottom_right = QtWidgets.QVBoxLayout(bottom_right_widget) bottom_right.addWidget(self.label_patient_pos, QtCore.Qt.AlignBottom) # Create a widget to contain the two bottom widgets bottom_widget = QtWidgets.QWidget() bottom_widget.setFixedHeight(100) bottom = QtWidgets.QHBoxLayout(bottom_widget) bottom.addWidget(bottom_left_widget, QtCore.Qt.AlignLeft) bottom.addWidget(bottom_right_widget, QtCore.Qt.AlignRight) # Add the bottom and top widgets to the view self.metadata_layout.addWidget(top_widget, QtCore.Qt.AlignTop) self.metadata_layout.addStretch() self.metadata_layout.addWidget(bottom_widget, QtCore.Qt.AlignBottom) def value_changed(self): self.update_view() def update_view(self, zoom_change=False): """ Update the view of the DICOM Image. :param zoom_change: Boolean indicating whether the user wants to change the zoom. False by default. """ self.image_display() if zoom_change: self.view.setTransform(QtGui.QTransform().scale( self.zoom, self.zoom)) if self.patient_dict_container.get("selected_rois"): self.roi_display() if self.patient_dict_container.get("selected_doses"): self.isodose_display() self.update_metadata() self.view.setScene(self.scene) def image_display(self): """ Update the image to be displayed on the DICOM View. """ pixmaps = self.patient_dict_container.get("pixmaps") slider_id = self.slider.value() image = pixmaps[slider_id] label = QtWidgets.QGraphicsPixmapItem(image) self.scene = QtWidgets.QGraphicsScene() self.scene.addItem(label) def roi_display(self): """ Display ROI structures on the DICOM Image. """ slider_id = self.slider.value() curr_slice = self.patient_dict_container.get("dict_uid")[slider_id] selected_rois = self.patient_dict_container.get("selected_rois") rois = self.patient_dict_container.get("rois") selected_rois_name = [] for roi in selected_rois: selected_rois_name.append(rois[roi]['name']) for roi in selected_rois: roi_name = rois[roi]['name'] if roi_name not in self.patient_dict_container.get( "dict_polygons").keys(): new_dict_polygons = self.patient_dict_container.get( "dict_polygons") new_dict_polygons[roi_name] = {} dict_rois_contours = get_contour_pixel( self.patient_dict_container.get("raw_contour"), selected_rois_name, self.patient_dict_container.get("pixluts"), curr_slice) polygons = self.calc_roi_polygon(roi_name, curr_slice, dict_rois_contours) new_dict_polygons[roi_name][curr_slice] = polygons self.patient_dict_container.set("dict_polygons", new_dict_polygons) elif curr_slice not in self.patient_dict_container.get( "dict_polygons")[roi_name].keys(): new_dict_polygons = self.patient_dict_container.get( "dict_polygons") dict_rois_contours = get_contour_pixel( self.patient_dict_container.get("raw_contour"), selected_rois_name, self.patient_dict_container.get("pixluts"), curr_slice) polygons = self.calc_roi_polygon(roi_name, curr_slice, dict_rois_contours) new_dict_polygons[roi_name][curr_slice] = polygons self.patient_dict_container.set("dict_polygons", new_dict_polygons) else: polygons = self.patient_dict_container.get( "dict_polygons")[roi_name][curr_slice] color = self.patient_dict_container.get("roi_color_dict")[roi] with open(resource_path('src/data/line&fill_configuration'), 'r') as stream: elements = stream.readlines() if len(elements) > 0: roi_line = int(elements[0].replace('\n', '')) roi_opacity = int(elements[1].replace('\n', '')) line_width = float(elements[4].replace('\n', '')) else: roi_line = 1 roi_opacity = 10 line_width = 2.0 stream.close() roi_opacity = int((roi_opacity / 100) * 255) color.setAlpha(roi_opacity) pen = self.get_qpen(color, roi_line, line_width) for i in range(len(polygons)): self.scene.addPolygon(polygons[i], pen, QtGui.QBrush(color)) def isodose_display(self): """ Display isodoses on the DICOM Image. """ slider_id = self.slider.value() curr_slice_uid = self.patient_dict_container.get("dict_uid")[slider_id] z = self.patient_dict_container.dataset[ slider_id].ImagePositionPatient[2] dataset_rtdose = self.patient_dict_container.dataset['rtdose'] grid = get_dose_grid(dataset_rtdose, float(z)) if not (grid == []): x, y = np.meshgrid(np.arange(grid.shape[1]), np.arange(grid.shape[0])) # Instantiate the isodose generator for this slice isodosegen = cntr.Cntr(x, y, grid) # sort selected_doses in ascending order so that the high dose isodose washes # paint over the lower dose isodose washes for sd in sorted( self.patient_dict_container.get("selected_doses")): dose_level = sd * self.patient_dict_container.get("rx_dose_in_cgray") / \ (dataset_rtdose.DoseGridScaling * 10000) contours = isodosegen.trace(dose_level) contours = contours[:len(contours) // 2] polygons = self.calc_dose_polygon( self.patient_dict_container.get("dose_pixluts") [curr_slice_uid], contours) brush_color = self.iso_color[sd] with open(resource_path('src/data/line&fill_configuration'), 'r') as stream: elements = stream.readlines() if len(elements) > 0: iso_line = int(elements[2].replace('\n', '')) iso_opacity = int(elements[3].replace('\n', '')) line_width = float(elements[4].replace('\n', '')) else: iso_line = 2 iso_opacity = 5 line_width = 2.0 stream.close() iso_opacity = int((iso_opacity / 100) * 255) brush_color.setAlpha(iso_opacity) pen_color = QtGui.QColor(brush_color.red(), brush_color.green(), brush_color.blue()) pen = self.get_qpen(pen_color, iso_line, line_width) for i in range(len(polygons)): self.scene.addPolygon(polygons[i], pen, QtGui.QBrush(brush_color)) def update_metadata(self): """ Update metadata displayed on the DICOM Image view. """ # Retrieve dictionary from the dataset of the slice id = self.slider.value() dataset = self.patient_dict_container.dataset[id] # Information to display self.current_slice_number = dataset['InstanceNumber'].value total_slices = len(self.patient_dict_container.get("pixmaps")) row_img = dataset['Rows'].value col_img = dataset['Columns'].value patient_pos = dataset['PatientPosition'].value window = self.patient_dict_container.get("window") level = self.patient_dict_container.get("level") slice_pos = dataset['SliceLocation'].value # Update labels self.label_image_id.setText( "Image: %s / %s" % (str(self.current_slice_number), str(total_slices))) self.label_image_pos.setText("Position: %s mm" % (str(slice_pos))) self.label_wl.setText("W/L: %s/%s" % (str(window), str(level))) self.label_image_size.setText("Image Size: %sx%spx" % (str(row_img), str(col_img))) self.label_zoom.setText("Zoom: " + "{:.2f}".format(self.zoom * 100) + "%") self.label_patient_pos.setText("Patient Position: %s" % (str(patient_pos))) def calc_roi_polygon(self, curr_roi, curr_slice, dict_rois_contours): """ Calculate a list of polygons to display for a given ROI and a given slice. :param curr_roi: the ROI structure :param curr_slice: the current slice :return: List of polygons of type QPolygonF. """ # TODO Implement support for showing "holes" in contours. # Possible process for this is: # 1. Calculate the areas of each contour on the slice # https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates # 2. Compare each contour to the largest contour by area to determine if it is contained entirely within the # largest contour. # https://stackoverflow.com/questions/4833802/check-if-polygon-is-inside-a-polygon # 3. If the polygon is contained, use QPolygonF.subtracted(QPolygonF) to subtract the smaller "hole" polygon # from the largest polygon, and then remove the polygon from the list of polygons to be displayed. # This process should provide fast and reliable results, however it should be noted that this method may fall # apart in a situation where there are multiple "large" polygons, each with their own hole in it. An appropriate # solution to that may be to compare every contour against one another and determine which ones have holes # encompassed entirely by them, and then subtract each hole from the larger polygon and delete the smaller # holes. This second solution would definitely lead to more accurate representation of contours, but could # possibly be too slow to be viable. list_polygons = [] pixel_list = dict_rois_contours[curr_roi][curr_slice] for i in range(len(pixel_list)): list_qpoints = [] contour = pixel_list[i] for point in contour: curr_qpoint = QtCore.QPoint(point[0], point[1]) list_qpoints.append(curr_qpoint) curr_polygon = QtGui.QPolygonF(list_qpoints) list_polygons.append(curr_polygon) return list_polygons def calc_dose_polygon(self, dose_pixluts, contours): """ Calculate a list of polygons to display for a given isodose. :param dose_pixluts: lookup table (LUT) to get the image pixel values :param contours: trace outline of the isodose to be displayed :return: List of polygons of type QPolygonF. """ list_polygons = [] for contour in contours: list_qpoints = [] # Slicing controls how many points considered for visualization # Essentially effects sharpness of edges, fewer points equals "smoother" edges for point in contour[::2]: curr_qpoint = QtCore.QPoint(dose_pixluts[0][int(point[0])], dose_pixluts[1][int(point[1])]) list_qpoints.append(curr_qpoint) curr_polygon = QtGui.QPolygonF(list_qpoints) list_polygons.append(curr_polygon) return list_polygons def get_qpen(self, color, style=1, widthF=1): """ The color and style for ROI structure and isodose display. :param color: Color of the region. QColor type. :param style: Style of the contour line. NoPen: 0 SolidLine: 1 DashLine: 2 DotLine: 3 DashDotLine: 4 DashDotDotLine: 5 :param widthF: Width of the contour line. :return: QPen object. """ pen = QtGui.QPen(color) pen.setStyle(style) pen.setWidthF(widthF) return pen def zoom_in(self): self.zoom *= 1.05 self.update_view(zoom_change=True) def zoom_out(self): self.zoom /= 1.05 self.update_view(zoom_change=True)
class UITransferROIWindow: def setup_ui(self, transfer_roi_window_instance, signal_roi_transferred_to_fixed_container, signal_roi_transferred_to_moving_container): self.patient_dict_container = PatientDictContainer() self.moving_dict_container = MovingDictContainer() self.fixed_image_initial_rois = self.patient_dict_container.get("rois") self.moving_image_initial_rois = self.moving_dict_container.get("rois") self.transfer_roi_window_instance = transfer_roi_window_instance self.signal_roi_transferred_to_fixed_container = \ signal_roi_transferred_to_fixed_container self.signal_roi_transferred_to_moving_container = \ signal_roi_transferred_to_moving_container self.fixed_to_moving_rois = {} self.moving_to_fixed_rois = {} self.add_suffix = True self.progress_window = ProgressWindow( self, QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) self.progress_window.setFixedSize(250, 100) self.progress_window.signal_loaded \ .connect(self.onTransferRoiFinished) self.progress_window.signal_error.connect(self.onTransferRoiError) self.init_layout() def retranslate_ui(self, transfer_roi_window_instance): _translate = QtCore.QCoreApplication.translate transfer_roi_window_instance.setWindowTitle( _translate("TransferRoiWindowInstance", "OnkoDICOM - Transfer Region of Interest")) self.add_suffix_checkbox.setText( _translate("AddSuffixCheckBox", "Add Suffix")) self.patient_A_label.setText( _translate("PatientAROILabel", "First Image Set ROIs")) self.patient_B_label.setText( _translate("PatientBROILabel", "Second Image Set ROIs")) self.transfer_all_rois_to_patient_B_button.setText( _translate("ROITransferToBButton", "All")) self.transfer_all_rois_to_patient_A_button.setText( _translate("ROITransferToAButton", "All")) self.save_button.setText(_translate("SaveButton", "Save")) self.reset_button.setText(_translate("ResetButton", "Reset")) def init_layout(self): """ Initialize the layout for the Transfer ROI Window. """ if platform.system() == 'Darwin': self.stylesheet_path = "res/stylesheet.qss" else: self.stylesheet_path = "res/stylesheet-win-linux.qss" stylesheet = open(resource_path(self.stylesheet_path)).read() window_icon = QIcon() window_icon.addPixmap(QPixmap(resource_path("res/images/icon.ico")), QIcon.Normal, QIcon.Off) self.transfer_roi_window_instance.setObjectName( "TransferRoiWindowInstance") self.transfer_roi_window_instance.setWindowIcon(window_icon) # Creating a grid layout to hold all elements self.transfer_roi_window_grid_layout = QGridLayout() self.transfer_roi_window_grid_layout.setColumnStretch(0, 1) self.transfer_roi_window_grid_layout.setColumnStretch(1, 1) self.transfer_roi_window_grid_layout.setColumnStretch(2, 1) self.init_patient_labels() self.init_transfer_arrow_buttons() self.init_patient_A_initial_roi_list() self.init_patient_B_rois_to_A_layout() self.init_patient_A_rois_to_B_layout() self.init_patient_B_initial_roi_list() self.init_add_suffix_checkbox() self.init_save_and_reset_button_layout() # Create a new central widget to hold the grid layout self.transfer_roi_window_instance_central_widget = QWidget() self.transfer_roi_window_instance_central_widget.setLayout( self.transfer_roi_window_grid_layout) self.retranslate_ui(self.transfer_roi_window_instance) self.transfer_roi_window_instance.setStyleSheet(stylesheet) self.transfer_roi_window_instance.setCentralWidget( self.transfer_roi_window_instance_central_widget) QtCore.QMetaObject.connectSlotsByName( self.transfer_roi_window_instance) def init_transfer_arrow_buttons(self): """ Initialize the layout for arrow buttons """ self.transfer_all_rois_to_patient_B_button = QPushButton() self.transfer_all_rois_to_patient_B_button.setObjectName( "ROITransferToBButton") transfer_all_rois_to_patient_B_icon = QtGui.QIcon() transfer_all_rois_to_patient_B_icon.addPixmap( QtGui.QPixmap( resource_path('res/images/btn-icons/forward_slide_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) self.transfer_all_rois_to_patient_B_button \ .setIcon(transfer_all_rois_to_patient_B_icon) self.transfer_all_rois_to_patient_B_button.clicked.connect( self.transfer_all_rois_to_patient_B_button_clicked) self.transfer_roi_window_grid_layout.addWidget( self.transfer_all_rois_to_patient_B_button, 1, 1) self.transfer_all_rois_to_patient_A_button = QPushButton() self.transfer_all_rois_to_patient_A_button.setObjectName( "ROITransferToAButton") self.transfer_all_rois_to_patient_A_button.setMaximumWidth(100) transfer_all_rois_to_patient_A_icon = QtGui.QIcon() transfer_all_rois_to_patient_A_icon.addPixmap( QtGui.QPixmap( resource_path('res/images/btn-icons/backward_slide_icon.png')), QtGui.QIcon.Normal, QtGui.QIcon.On) self.transfer_all_rois_to_patient_A_button \ .setIcon(transfer_all_rois_to_patient_A_icon) self.transfer_all_rois_to_patient_A_button.clicked.connect( self.transfer_all_rois_to_patient_A_button_clicked) self.transfer_roi_window_grid_layout.addWidget( self.transfer_all_rois_to_patient_A_button, 2, 1) def transfer_all_rois_to_patient_B_button_clicked(self): """ This function is triggered when the right arrow button is clicked. """ self.fixed_to_moving_rois.clear() self.patient_A_rois_to_B_list_widget.clear() for i in range(0, len(self.fixed_image_initial_rois)): self.patient_A_initial_roi_double_clicked( self.patient_A_initial_rois_list_widget.item(i)) def transfer_all_rois_to_patient_A_button_clicked(self): """ This function is triggered when the left arrow button is clicked. """ self.moving_to_fixed_rois.clear() self.patient_B_rois_to_A_list_widget.clear() for i in range(0, len(self.moving_image_initial_rois)): self.patient_B_initial_roi_double_clicked( self.patient_B_initial_rois_list_widget.item(i)) def init_add_suffix_checkbox(self): """ Initialize the layout for add suffix checkbox """ self.add_suffix_checkbox = QCheckBox() self.add_suffix_checkbox.setObjectName("AddSuffixCheckBox") self.add_suffix_checkbox.setChecked(self.add_suffix) self.add_suffix_checkbox.clicked.connect( self.add_suffix_checkbox_clicked) self.transfer_roi_window_grid_layout.addWidget( self.add_suffix_checkbox, 3, 0) def init_patient_labels(self): """ Initialize the layout for two patient labels """ self.patient_A_label = QLabel() self.patient_A_label.setObjectName("PatientAROILabel") self.patient_A_label.setMinimumHeight(50) self.patient_A_label.setAlignment(Qt.AlignCenter) self.patient_A_label.setStyleSheet( "QLabel { background-color : green; color : white; " "font-size: 15pt; font-weight: bold;}") self.patient_B_label = QLabel() self.patient_B_label.setObjectName("PatientBROILabel") self.patient_B_label.setMinimumHeight(50) self.patient_B_label.setAlignment(Qt.AlignCenter) self.patient_B_label.setStyleSheet( "QLabel { background-color : red; color : white; " "font-size: 15pt; font-weight: bold;}") self.transfer_roi_window_grid_layout.addWidget(self.patient_A_label, 0, 0) self.transfer_roi_window_grid_layout.addWidget(self.patient_B_label, 0, 2) def init_save_and_reset_button_layout(self): """ Initialize the layout for save and reset buttons """ self.reset_and_save_buttons_layout = QHBoxLayout() self.reset_button = QPushButton() self.reset_button.setObjectName("ResetButton") self.reset_button.clicked.connect(self.reset_clicked) self.save_button = QPushButton() self.save_button.setObjectName("SaveButton") self.save_button.setDisabled(True) self.save_button.clicked.connect(self.transfer_roi_clicked) self.reset_and_save_buttons_layout.setAlignment(Qt.AlignRight) self.reset_and_save_buttons_layout.addWidget(self.reset_button) self.reset_and_save_buttons_layout.addWidget(self.save_button) # Create a widget to hold Reset and Save buttons self.reset_and_save_button_central_widget = QWidget() self.reset_and_save_button_central_widget.setLayout( self.reset_and_save_buttons_layout) self.transfer_roi_window_grid_layout.addWidget( self.reset_and_save_button_central_widget, 3, 2) def add_suffix_checkbox_clicked(self): """ This function is triggered when the add suffix checkbox is clicked """ self.add_suffix = self.add_suffix_checkbox.isChecked() def init_patient_B_rois_to_A_layout(self): """ Initialize the layout for transfer rois from B to A container """ # Create scrolling area widget to contain the content. self.patient_B_rois_to_A_list_widget = QListWidget(self) self.transfer_roi_window_grid_layout \ .addWidget(self.patient_B_rois_to_A_list_widget, 2, 0) self.patient_B_rois_to_A_list_widget.itemDoubleClicked.connect( self.patient_B_to_A_rois_double_clicked) def init_patient_A_rois_to_B_layout(self): """ Initialize the layout for transfer rois from A to B container """ self.patient_A_rois_to_B_list_widget = QListWidget(self) self.transfer_roi_window_grid_layout \ .addWidget(self.patient_A_rois_to_B_list_widget, 1, 2) self.patient_A_rois_to_B_list_widget.itemDoubleClicked.connect( self.patient_A_to_B_rois_double_clicked) def init_patient_A_initial_roi_list(self): """ Initialize the layout for patient A's roi list """ self.patient_A_initial_rois_list_widget = QListWidget(self) self.patient_A_initial_rois_list_widget.itemDoubleClicked.connect( self.patient_A_initial_roi_double_clicked) for idx in self.fixed_image_initial_rois: roi_label = QListWidgetItem( self.fixed_image_initial_rois[idx]['name']) roi_label.setForeground(Qt.darkGreen) roi_label.setData(Qt.UserRole, self.fixed_image_initial_rois[idx]) self.patient_A_initial_rois_list_widget.addItem(roi_label) self.transfer_roi_window_grid_layout.addWidget( self.patient_A_initial_rois_list_widget, 1, 0) def init_patient_B_initial_roi_list(self): """ Initialize the layout for patient B's roi list """ self.patient_B_initial_rois_list_widget = QListWidget(self) self.patient_B_initial_rois_list_widget.itemDoubleClicked.connect( self.patient_B_initial_roi_double_clicked) for idx in self.moving_image_initial_rois: roi_label = QListWidgetItem( self.moving_image_initial_rois[idx]['name']) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, self.moving_image_initial_rois[idx]) self.patient_B_initial_rois_list_widget.addItem(roi_label) self.transfer_roi_window_grid_layout.addWidget( self.patient_B_initial_rois_list_widget, 2, 2) def patient_A_to_B_rois_double_clicked(self, item): """ This function is triggered when a roi in "A to B" list is double-clicked. """ roi_to_remove = item.data(Qt.UserRole) to_delete_value = roi_to_remove['name'] self.fixed_to_moving_rois.pop(to_delete_value) self.patient_A_rois_to_B_list_widget.clear() for key, value in self.fixed_to_moving_rois.items(): roi_label = QListWidgetItem(value) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, {'name': key}) self.patient_A_rois_to_B_list_widget.addItem(roi_label) if self.transfer_list_is_empty(): self.save_button.setDisabled(True) def patient_B_to_A_rois_double_clicked(self, item): """ This function is triggered when a roi in "B to A" list is double-clicked. """ roi_to_remove = item.data(Qt.UserRole) to_delete_value = roi_to_remove['name'] self.moving_to_fixed_rois.pop(to_delete_value) self.patient_B_rois_to_A_list_widget.clear() for key, value in self.moving_to_fixed_rois.items(): roi_label = QListWidgetItem(value) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, {'name': key}) self.patient_B_rois_to_A_list_widget.addItem(roi_label) if self.transfer_list_is_empty(): self.save_button.setDisabled(True) def patient_A_initial_roi_double_clicked(self, item): """ This function is triggered when a roi in patient A's roi list is double-clicked. """ roi_to_add = item.data(Qt.UserRole) transferred_roi_name = roi_to_add['name'] # If the clicked roi is already transferred, return if transferred_roi_name in self.fixed_to_moving_rois.keys(): QMessageBox.about(self, "Transfer Failed", "Chosen ROI has already been transferred!") return # Create a set to store all current roi names in target patient # including both initial rois name and added roi names so far patient_B_initial_roi_name_list = set() for item in self.fixed_to_moving_rois.values(): patient_B_initial_roi_name_list.add(item) for idx in self.moving_image_initial_rois: patient_B_initial_roi_name_list.add( self.moving_image_initial_rois[idx]['name']) # Check if clicked roi name has duplicate # in patient B's initial roi names list if transferred_roi_name in patient_B_initial_roi_name_list: if self.add_suffix: transferred_roi_name = generate_non_duplicated_name( transferred_roi_name, patient_B_initial_roi_name_list) else: QMessageBox.about( self, "Transfer Failed", "Duplicated ROI name. " "Please consider adding suffix.") return # Add clicked roi to transferred list self.fixed_to_moving_rois[roi_to_add['name']] = transferred_roi_name roi_label = QListWidgetItem(transferred_roi_name) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, roi_to_add) self.patient_A_rois_to_B_list_widget.addItem(roi_label) self.save_button.setDisabled(False) def patient_B_initial_roi_double_clicked(self, item): """ This function is triggered when a roi in patient B's roi list is double-clicked. """ roi_to_add = item.data(Qt.UserRole) transferred_roi_name = roi_to_add['name'] # If the clicked roi is already transferred, return if transferred_roi_name in self.moving_to_fixed_rois.keys(): QMessageBox.about(self, "Transfer Failed", "Chosen ROI has already been transferred!") return # Create a set to store all current roi names in target patient # including both initial rois name and added roi names so far patient_A_current_roi_name_list = set() for item in self.moving_to_fixed_rois.values(): patient_A_current_roi_name_list.add(item) for idx in self.fixed_image_initial_rois: patient_A_current_roi_name_list.add( self.fixed_image_initial_rois[idx]['name']) # Check if clicked roi name has duplicate in # target patient's roi names list if transferred_roi_name in patient_A_current_roi_name_list: # if add suffix is ticked, iteratively try adding suffix # from _A to _Z, stop when no duplicate found if self.add_suffix: transferred_roi_name = generate_non_duplicated_name( transferred_roi_name, patient_A_current_roi_name_list) else: QMessageBox.about( self, "Transfer Failed", "Duplicated ROI name. " "Please consider adding suffix.") return # Add clicked roi to transferred list self.moving_to_fixed_rois[roi_to_add['name']] = transferred_roi_name roi_label = QListWidgetItem(transferred_roi_name) roi_label.setForeground(Qt.red) roi_label.setData(Qt.UserRole, roi_to_add) self.patient_B_rois_to_A_list_widget.addItem(roi_label) self.save_button.setDisabled(False) def reset_clicked(self): """ This function is triggered when reset button is clicked. """ self.fixed_to_moving_rois.clear() self.moving_to_fixed_rois.clear() self.patient_A_rois_to_B_list_widget.clear() self.patient_B_rois_to_A_list_widget.clear() self.save_button.setDisabled(True) def transfer_list_is_empty(self): """ This function is to check if the transfer list is empty """ return len(self.fixed_to_moving_rois) == 0 \ and len(self.moving_to_fixed_rois) == 0 def save_clicked(self, interrupt_flag, progress_callback): """ This function is triggered when the save button is clicked. It contains all steps in the ROI transferring process. :param interrupt_flag: interrupt flag to stop process :param progress_callback: signal that receives the current progress of the loading. """ progress_callback.emit(("Converting images to sitk", 0)) # check if interrupt flag is set if not check_interrupt_flag(interrupt_flag): return False rtss = self.patient_dict_container.get("dataset_rtss") # get sitk for the fixed image dicom_image = read_dicom_image_to_sitk( self.patient_dict_container.filepaths) if not check_interrupt_flag(interrupt_flag): return False # get array of roi indexes from sitk images rois_images_fixed = transform_point_set_from_dicom_struct( dicom_image, rtss, self.fixed_to_moving_rois.keys(), spacing_override=None, interrupt_flag=interrupt_flag) moving_rtss = self.moving_dict_container.get("dataset_rtss") if not check_interrupt_flag(interrupt_flag): return False # get sitk for the moving image moving_dicom_image = read_dicom_image_to_sitk( self.moving_dict_container.filepaths) if not check_interrupt_flag(interrupt_flag): return False # get array of roi indexes from sitk images progress_callback \ .emit(("Retrieving ROIs from \nboth image sets", 20)) # check if interrupt flag is set if not check_interrupt_flag(interrupt_flag): return False if moving_rtss: rois_images_moving = transform_point_set_from_dicom_struct( moving_dicom_image, moving_rtss, self.moving_to_fixed_rois.keys(), spacing_override=None, interrupt_flag=interrupt_flag) else: rois_images_moving = ([], []) if not check_interrupt_flag(interrupt_flag): return False tfm = self.moving_dict_container.get("tfm") progress_callback.emit( ("Transfering ROIs from moving \nto fixed image set", 40)) # check if interrupt flag is set if not check_interrupt_flag(interrupt_flag): return False # transform roi from moving_dict to fixed_dict self.transfer_rois(self.moving_to_fixed_rois, tfm, dicom_image, rois_images_moving, self.patient_dict_container) progress_callback.emit( ("Transfering ROIs from fixed \nto moving image set", 60)) if not check_interrupt_flag(interrupt_flag): return False # transform roi from moving_dict to fixed_dict self.transfer_rois(self.fixed_to_moving_rois, tfm.GetInverse(), moving_dicom_image, rois_images_fixed, self.moving_dict_container) progress_callback.emit(("Saving ROIs to RTSS", 80)) # check if interrupt flag is set if not check_interrupt_flag(interrupt_flag): return False progress_callback.emit(("Reloading window", 90)) return True def transfer_roi_clicked(self): """ telling progress window to start ROI transfer """ self.progress_window.start(self.save_clicked) def onTransferRoiError(self, exception): """ This function is triggered when there is an error in the ROI transferring process. :param exception: exception thrown """ QMessageBox.about(self.progress_window, "Unable to transfer ROIs", "Please check your image set and ROI data.") self.progress_window.close() def onTransferRoiFinished(self, result): """ This function is triggered when ROI transferring process is finished. """ # emit changed dataset to structure_modified function and # auto_save_roi function if result[0] is True: if len(self.fixed_to_moving_rois) > 0: self.signal_roi_transferred_to_moving_container.emit( (self.moving_dict_container.get("dataset_rtss"), { "transfer": None })) if len(self.moving_to_fixed_rois) > 0: self.signal_roi_transferred_to_fixed_container.emit( (self.patient_dict_container.get("dataset_rtss"), { "transfer": None })) self.progress_window.close() QMessageBox.about(self.transfer_roi_window_instance, "Saved", "ROIs are successfully transferred!") else: QMessageBox.about(self.transfer_roi_window_instance, "Cancelled", "ROIs Transfer is cancelled.") self.closeWindow() def transfer_rois(self, transfer_dict, tfm, reference_image, original_roi_list, patient_dict_container): """ Converting (transferring) ROIs from one image set to another and save the transferred rois to rtss. :param transfer_dict: dictionary of rois to be transfer. key is original roi names, value is the name after transferred. :param original_roi_list: tuple of sitk rois from the base image. :param tfm: the tfm that contains information for transferring rois :param reference_image: the reference (base) image :param patient_dict_container: container of the transfer image set. """ for roi_name, new_roi_name in transfer_dict.items(): for index, name in enumerate(original_roi_list[1]): if name == roi_name: sitk_image = original_roi_list[0][index] new_contour = apply_linear_transform( input_image=sitk_image, transform=tfm, reference_image=reference_image, is_structure=True) contour = sitk.GetArrayViewFromImage(new_contour) contours = np.transpose(contour.nonzero()) self.save_roi_to_patient_dict_container( contours, new_roi_name, patient_dict_container) def save_roi_to_patient_dict_container(self, contours, roi_name, patient_dict_container): """ Save the transferred ROI to the corresponding rtss. :param contours: np array of coordinates of the ROI to be saved. :param roi_name: name of the ROI to be saved :param patient_dict_container: container of the transfer image set. """ pixels_coords_dict = {} slice_ids_dict = get_dict_slice_to_uid(patient_dict_container) total_slices = len(slice_ids_dict) for contour in contours: curr_slice_id = total_slices - contour[0] if curr_slice_id >= total_slices: curr_slice_id = 0 if curr_slice_id not in pixels_coords_dict: pixels_coords_dict[curr_slice_id] = [ tuple([contour[2], contour[1]]) ] else: pixels_coords_dict[curr_slice_id].append( tuple([contour[2], contour[1]])) rois_to_save = {} for key in pixels_coords_dict.keys(): coords = pixels_coords_dict[key] polygon_list = ROI.calculate_concave_hull_of_points(coords) if len(polygon_list) > 0: rois_to_save[key] = { 'ds': patient_dict_container.dataset[key], 'coords': polygon_list } roi_list = ROI.convert_hull_list_to_contours_data( rois_to_save, patient_dict_container) if len(roi_list) > 0: print("Saving ", roi_name) if isinstance(patient_dict_container, MovingDictContainer): new_rtss = ROI.create_roi( patient_dict_container.get("dataset_rtss"), roi_name, roi_list, rtss_owner="MOVING") self.moving_dict_container.set("dataset_rtss", new_rtss) self.moving_dict_container.set("rtss_modified", True) else: new_rtss = ROI.create_roi( patient_dict_container.get("dataset_rtss"), roi_name, roi_list) self.patient_dict_container.set("dataset_rtss", new_rtss) self.patient_dict_container.set("rtss_modified", True) def closeWindow(self): """ function to close transfer roi window """ self.close()
def create_initial_model(): """ This function initializes all the attributes in the PatientDictContainer model required for the operation of the main window. This should be called before the main window's components are constructed, but after the initial values of the PatientDictContainer instance are set (i.e. dataset and filepaths). """ ############################## # LOAD PATIENT INFORMATION # ############################## patient_dict_container = PatientDictContainer() dataset = patient_dict_container.dataset filepaths = patient_dict_container.filepaths patient_dict_container.set("rtss_modified", False) # Determine if dataset is CT for aditional rescaling is_ct = False if dataset[0].Modality == "CT": is_ct = True if 'WindowWidth' in dataset[0]: if isinstance(dataset[0].WindowWidth, pydicom.valuerep.DSfloat): window = int(dataset[0].WindowWidth) elif isinstance(dataset[0].WindowWidth, pydicom.multival.MultiValue): window = int(dataset[0].WindowWidth[1]) else: window = int(400) if 'WindowCenter' in dataset[0]: if isinstance(dataset[0].WindowCenter, pydicom.valuerep.DSfloat): level = int(dataset[0].WindowCenter) - window / 2 elif isinstance(dataset[0].WindowCenter, pydicom.multival.MultiValue): level = int(dataset[0].WindowCenter[1]) - window / 2 if is_ct: level += CT_RESCALE_INTERCEPT else: level = int(800) patient_dict_container.set("window", window) patient_dict_container.set("level", level) # Check to see if the imageWindowing.csv file exists if os.path.exists(data_path('imageWindowing.csv')): # If it exists, read data from file into the self.dict_windowing # variable dict_windowing = {} with open(data_path('imageWindowing.csv'), "r") \ as fileInput: next(fileInput) dict_windowing["Normal"] = [window, level] for row in fileInput: # Format: Organ - Scan - Window - Level items = [item for item in row.split(',')] dict_windowing[items[0]] = [int(items[2]), int(items[3])] else: # If csv does not exist, initialize dictionary with default values dict_windowing = { "Normal": [window, level], "Lung": [1600, -300], "Bone": [1400, 700], "Brain": [160, 950], "Soft Tissue": [400, 800], "Head and Neck": [275, 900] } patient_dict_container.set("dict_windowing", dict_windowing) if not patient_dict_container.has_attribute("scaled"): patient_dict_container.set("scaled", True) pixel_values = convert_raw_data(dataset, False, is_ct) else: pixel_values = convert_raw_data(dataset, True) # Calculate the ratio between x axis and y axis of 3 views pixmap_aspect = {} pixel_spacing = dataset[0].PixelSpacing slice_thickness = dataset[0].SliceThickness pixmap_aspect["axial"] = pixel_spacing[1] / pixel_spacing[0] pixmap_aspect["sagittal"] = pixel_spacing[1] / slice_thickness pixmap_aspect["coronal"] = slice_thickness / pixel_spacing[0] pixmaps_axial, pixmaps_coronal, pixmaps_sagittal = \ get_pixmaps(pixel_values, window, level, pixmap_aspect) patient_dict_container.set("pixmaps_axial", pixmaps_axial) patient_dict_container.set("pixmaps_coronal", pixmaps_coronal) patient_dict_container.set("pixmaps_sagittal", pixmaps_sagittal) patient_dict_container.set("pixel_values", pixel_values) patient_dict_container.set("pixmap_aspect", pixmap_aspect) basic_info = get_basic_info(dataset[0]) patient_dict_container.set("basic_info", basic_info) patient_dict_container.set("dict_uid", dict_instance_uid(dataset)) # Set RTSS attributes patient_dict_container.set("file_rtss", filepaths['rtss']) patient_dict_container.set("dataset_rtss", dataset['rtss']) dict_raw_contour_data, dict_numpoints = \ ImageLoading.get_raw_contour_data(dataset['rtss']) patient_dict_container.set("raw_contour", dict_raw_contour_data) # dict_dicom_tree_rtss will be set in advance if the program # generates a new rtss through the execution of # ROI.create_initial_rtss_from_ct(...) if patient_dict_container.get("dict_dicom_tree_rtss") is None: dicom_tree_rtss = DicomTree(filepaths['rtss']) patient_dict_container.set("dict_dicom_tree_rtss", dicom_tree_rtss.dict) patient_dict_container.set( "list_roi_numbers", ordered_list_rois(patient_dict_container.get("rois"))) patient_dict_container.set("selected_rois", []) patient_dict_container.set("dict_polygons_axial", {}) patient_dict_container.set("dict_polygons_sagittal", {}) patient_dict_container.set("dict_polygons_coronal", {}) # Set RTDOSE attributes if patient_dict_container.has_modality("rtdose"): dicom_tree_rtdose = DicomTree(filepaths['rtdose']) patient_dict_container.set("dict_dicom_tree_rtdose", dicom_tree_rtdose.dict) patient_dict_container.set("dose_pixluts", get_dose_pixluts(dataset)) patient_dict_container.set("selected_doses", []) # overwritten if RTPLAN is present. patient_dict_container.set("rx_dose_in_cgray", 1) # Set RTPLAN attributes if patient_dict_container.has_modality("rtplan"): # the TargetPrescriptionDose is type 3 (optional), so it may not be # there However, it is preferable to the sum of the beam doses # DoseReferenceStructureType is type 1 (value is mandatory), but it # can have a value of ORGAN_AT_RISK rather than TARGET in which case # there will *not* be a TargetPrescriptionDose and even if it is # TARGET, that's no guarantee that TargetPrescriptionDose will be # encoded and have a value rx_dose_in_cgray = calculate_rx_dose_in_cgray(dataset["rtplan"]) patient_dict_container.set("rx_dose_in_cgray", rx_dose_in_cgray) dicom_tree_rtplan = DicomTree(filepaths['rtplan']) patient_dict_container.set("dict_dicom_tree_rtplan", dicom_tree_rtplan.dict) # Set SR attributes if patient_dict_container.has_modality("sr-cd"): dicom_tree_sr_clinical_data = DicomTree(filepaths['sr-cd']) patient_dict_container.set("dict_dicom_tree_sr_cd", dicom_tree_sr_clinical_data.dict) if patient_dict_container.has_modality("sr-rad"): dicom_tree_sr_pyrad = DicomTree(filepaths['sr-rad']) patient_dict_container.set("dict_dicom_tree_sr_pyrad", dicom_tree_sr_pyrad.dict)