コード例 #1
0
ファイル: ActionHandler.py プロジェクト: celeron533/OnkoDICOM
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)
コード例 #2
0
ファイル: StructureTab.py プロジェクト: celeron533/OnkoDICOM
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
コード例 #3
0
ファイル: DVHTab.py プロジェクト: sjswerdloff/OnkoDICOM
class DVHTab(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.dvh_calculated = self.patient_dict_container.has_attribute(
            "raw_dvh")

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

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

        self.dvh_tab_layout = QtWidgets.QVBoxLayout()

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

        self.setLayout(self.dvh_tab_layout)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        plt.subplots_adjust(bottom=0.3)

        return fig

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

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

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

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

            # Clear the current layout
            self.clear_layout()

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

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

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

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

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

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

        self.modified_indicator_widget.setLayout(modified_indicator_layout)

        self.dvh_tab_layout.addWidget(self.modified_indicator_widget,
                                      QtCore.Qt.AlignTop)
コード例 #4
0
class DVHTab(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.patient_dict_container = PatientDictContainer()
        self.dvh_calculated = self.patient_dict_container.has_attribute(
            "raw_dvh")
        self.rt_dose = self.patient_dict_container.dataset['rtdose']

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

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

        self.dvh_tab_layout = QtWidgets.QVBoxLayout()

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

        self.setLayout(self.dvh_tab_layout)

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

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

        button_layout = QtWidgets.QHBoxLayout()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        plt.subplots_adjust(bottom=0.3)

        return fig

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

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

                self.export_rtdose()
        else:
            stylesheet_path = ""

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

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

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

            mb.exec_()

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

                self.export_rtdose()

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

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

            # Clear the current layout
            self.clear_layout()

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

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

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

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

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

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

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

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

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

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

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

        self.modified_indicator_widget.setLayout(modified_indicator_layout)

        self.dvh_tab_layout.addWidget(self.modified_indicator_widget,
                                      QtCore.Qt.AlignTop | QtCore.Qt.AlignTop)
コード例 #5
0
ファイル: StructureTab.py プロジェクト: sjswerdloff/OnkoDICOM
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)
コード例 #6
0
class ActionHandler:
    """
    This class is responsible for initializing all of the actions that will be used by the MainPage and its components.
    There exists a 1-to-1 relationship between this class and the MainPage. This class has access to the main page's
    attributes and components, however this access should only be used to provide functionality to the actions defined
    below. The instance of this class can be given to the main page's components in order to trigger actions.
    """
    def __init__(self, main_page):
        self.__main_page = main_page
        self.patient_dict_container = PatientDictContainer()

        ##############################
        # 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)
コード例 #7
0
ファイル: InitialModel.py プロジェクト: celeron533/OnkoDICOM
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)