示例#1
0
    def __init__(self, parent: 'MainWindowView', dock: QDockWidget,
                 images: Images):
        # enforce not showing a single image
        assert images.data.ndim == 3, \
            "Data does NOT have 3 dimensions! Dimensions found: {0}".format(images.data.ndim)

        # We set the main window as the parent, the effect is the same as
        # having no parent, the window will be inside the QDockWidget. If the
        # dock is set as a parent the window will be an independent floating
        # window
        super(StackVisualiserView, self).__init__(parent, None)
        self.central_widget = QWidget(self)
        self.layout = QVBoxLayout(self)
        self.central_widget.setLayout(self.layout)
        self.setCentralWidget(self.central_widget)
        self.parent_create_stack = self.parent().create_new_stack
        self._main_window = parent

        # capture the QDockWidget reference so that we can access the Qt widget
        # and change things like the title
        self.dock = dock
        # Swap out the dock close event with our own provided close event. This
        # is needed to manually delete the data reference, otherwise it is left
        # hanging in the presenter
        setattr(dock, 'closeEvent', self.closeEvent)

        self.presenter = StackVisualiserPresenter(self, images)

        self._actions = [
            ("Show history and metadata", self.show_image_metadata),
            ("Duplicate whole data",
             lambda: self.presenter.notify(SVNotification.DUPE_STACK)),
            ("Duplicate current ROI of data",
             lambda: self.presenter.notify(SVNotification.DUPE_STACK_ROI)),
            ("Mark as projections/sinograms", self.mark_as_sinograms),
            ("", None),
            ("Toggle show averaged image",
             lambda: self.presenter.notify(SVNotification.TOGGLE_IMAGE_MODE)),
            ("Create sinograms from stack",
             lambda: self.presenter.notify(SVNotification.SWAP_AXES)),
            ("Set ROI", self.set_roi),
            ("Copy ROI to clipboard", self.copy_roi_to_clipboard), ("", None),
            ("Change window name", self.change_window_name_clicked),
            ("Goto projection", self.goto_projection),
            ("Goto angle", self.goto_angle)
        ]
        self._context_actions = self.build_context_menu()

        self.image_view = MIImageView(self)
        self.image_view.imageItem.menu = self._context_actions
        self.actionCloseStack = QAction("Close window", self)
        self.actionCloseStack.triggered.connect(self.close)
        self.actionCloseStack.setShortcut("Ctrl+W")
        self.dock.addAction(self.actionCloseStack)
        self.image_view.setImage(self.presenter.images.data)
        self.image_view.roi_changed_callback = self.roi_changed_callback
        self.layout.addWidget(self.image_view)
示例#2
0
    def __init__(self, parent: 'MainWindowView', images: Images):
        # enforce not showing a single image
        assert images.data.ndim == 3, \
            "Data does NOT have 3 dimensions! Dimensions found: {0}".format(images.data.ndim)

        # We set the main window as the parent, the effect is the same as
        # having no parent, the window will be inside the QDockWidget. If the
        # dock is set as a parent the window will be an independent floating
        # window
        super().__init__(images.name, parent)

        self.central_widget = QWidget(self)
        self.layout = QVBoxLayout()
        self.central_widget.setLayout(self.layout)
        self.setWidget(self.central_widget)
        self.parent_create_stack = self.parent().create_new_stack
        self._main_window = parent

        self.presenter = StackVisualiserPresenter(self, images)

        self._actions = [
            ("Show history and metadata", self.show_image_metadata),
            ("Duplicate whole data",
             lambda: self.presenter.notify(SVNotification.DUPE_STACK)),
            ("Duplicate current ROI of data",
             lambda: self.presenter.notify(SVNotification.DUPE_STACK_ROI)),
            ("Mark as projections/sinograms", self.mark_as_sinograms),
            ("", None),
            ("Toggle averaged image",
             lambda: self.presenter.notify(SVNotification.TOGGLE_IMAGE_MODE)),
            ("Create sinograms from stack",
             lambda: self.presenter.notify(SVNotification.SWAP_AXES)),
            ("Set ROI", self.set_roi),
            ("Copy ROI to clipboard", self.copy_roi_to_clipboard), ("", None),
            ("Change window name", self.change_window_name_clicked),
            ("Goto projection", self.goto_projection),
            ("Goto angle", self.goto_angle)
        ]
        self._context_actions = self.build_context_menu()

        self.image_view = MIImageView(self)
        self.image_view.imageItem.menu = self._context_actions
        self.actionCloseStack = QAction("Close window", self)
        self.actionCloseStack.triggered.connect(self.close)
        self.actionCloseStack.setShortcut("Ctrl+W")

        nan_check_menu = [
            ("Crop Coordinates", lambda: self._main_window.presenter.
             show_operation("Crop Coordinates")),
            ("NaN Removal",
             lambda: self._main_window.presenter.show_operation("NaN Removal"))
        ]
        self.image_view.enable_nan_check(actions=nan_check_menu)
        self.addAction(self.actionCloseStack)
        self.image_view.setImage(self.presenter.images.data)
        self.image_view.roi_changed_callback = self.roi_changed_callback
        self.layout.addWidget(self.image_view)
示例#3
0
 def _setup_stack_for_view(self, stack: MIImageView, data: np.ndarray):
     stack.setContentsMargins(4, 4, 4, 4)
     stack.setImage(data)
     stack.ui.menuBtn.hide()
     stack.ui.roiBtn.hide()
     stack.button_stack_right.hide()
     stack.button_stack_left.hide()
     details_size_policy = QSizePolicy(QSizePolicy.MinimumExpanding,
                                       QSizePolicy.Preferred)
     details_size_policy.setHorizontalStretch(1)
     stack.details.setSizePolicy(details_size_policy)
     self.roiButton.clicked.connect(stack.roiClicked)
示例#4
0
class StackVisualiserView(BaseMainWindowView):
    # Signal that signifies when the ROI is updated. Used to update previews in Filter views
    roi_updated = pyqtSignal(SensibleROI)

    image_view: MIImageView
    presenter: StackVisualiserPresenter
    dock: QDockWidget
    layout: QVBoxLayout

    def __init__(self, parent: 'MainWindowView', dock: QDockWidget,
                 images: Images):
        # enforce not showing a single image
        assert images.data.ndim == 3, \
            "Data does NOT have 3 dimensions! Dimensions found: {0}".format(images.data.ndim)

        # We set the main window as the parent, the effect is the same as
        # having no parent, the window will be inside the QDockWidget. If the
        # dock is set as a parent the window will be an independent floating
        # window
        super(StackVisualiserView, self).__init__(parent, None)
        self.central_widget = QWidget(self)
        self.layout = QVBoxLayout(self)
        self.central_widget.setLayout(self.layout)
        self.setCentralWidget(self.central_widget)
        self.parent_create_stack = self.parent().create_new_stack
        self._main_window = parent

        # capture the QDockWidget reference so that we can access the Qt widget
        # and change things like the title
        self.dock = dock
        # Swap out the dock close event with our own provided close event. This
        # is needed to manually delete the data reference, otherwise it is left
        # hanging in the presenter
        setattr(dock, 'closeEvent', self.closeEvent)

        self.presenter = StackVisualiserPresenter(self, images)

        self._actions = [
            ("Show history and metadata", self.show_image_metadata),
            ("Duplicate whole data",
             lambda: self.presenter.notify(SVNotification.DUPE_STACK)),
            ("Duplicate current ROI of data",
             lambda: self.presenter.notify(SVNotification.DUPE_STACK_ROI)),
            ("Mark as projections/sinograms", self.mark_as_sinograms),
            ("", None),
            ("Toggle show averaged image",
             lambda: self.presenter.notify(SVNotification.TOGGLE_IMAGE_MODE)),
            ("Create sinograms from stack",
             lambda: self.presenter.notify(SVNotification.SWAP_AXES)),
            ("Set ROI", self.set_roi),
            ("Copy ROI to clipboard", self.copy_roi_to_clipboard), ("", None),
            ("Change window name", self.change_window_name_clicked),
            ("Goto projection", self.goto_projection),
            ("Goto angle", self.goto_angle)
        ]
        self._context_actions = self.build_context_menu()

        self.image_view = MIImageView(self)
        self.image_view.imageItem.menu = self._context_actions
        self.actionCloseStack = QAction("Close window", self)
        self.actionCloseStack.triggered.connect(self.close)
        self.actionCloseStack.setShortcut("Ctrl+W")
        self.dock.addAction(self.actionCloseStack)
        self.image_view.setImage(self.presenter.images.data)
        self.image_view.roi_changed_callback = self.roi_changed_callback
        self.layout.addWidget(self.image_view)

    @property
    def name(self):
        return self.dock.windowTitle()

    @name.setter
    def name(self, name: str):
        self.dock.setWindowTitle(name)

    @property
    def current_roi(self) -> SensibleROI:
        return SensibleROI.from_points(*self.image_view.get_roi())

    @property
    def image(self):
        return self.image_view.imageItem

    @image.setter
    def image(self, to_display):
        self.image_view.setImage(to_display)

    @property
    def main_window(self) -> 'MainWindowView':
        return self._main_window

    @property
    def context_actions(self):
        return self._context_actions

    @property
    def actions(self):
        return self._actions

    def closeEvent(self, event):
        window: 'MainWindowView' = self.window()
        stacks_with_proj180 = window.get_all_stack_visualisers_with_180deg_proj(
        )
        for stack in stacks_with_proj180:
            if stack.presenter.images.proj180deg is self.presenter.images:
                if not self.ask_confirmation(
                        "Caution: If you close this then the 180 degree projection will "
                        "not be available for COR correlation, and the middle of the image stack will be used."
                ):
                    event.ignore()
                    return
                else:
                    stack.presenter.images.clear_proj180deg()

        with operation_in_progress("Closing image view",
                                   "Freeing image memory"):
            self.dock.setFloating(False)
            self.hide()
            self.image_view.close()

            # this removes all references to the data, allowing it to be GC'ed
            # otherwise there is a hanging reference
            self.presenter.delete_data()
            window.remove_stack(self)
            self.deleteLater()
            # refers to the QDockWidget within which the stack is contained
            self.dock.deleteLater()

    def roi_changed_callback(self, roi: SensibleROI):
        self.roi_updated.emit(roi)

    def build_context_menu(self) -> QMenu:
        menu = QMenu(self)
        populate_menu(menu, self.actions)
        return menu

    def goto_projection(self):
        projection_to_goto, accepted = QInputDialog.getInt(
            self,
            "Enter Projection",
            "Projection",
            0,  # Default value
            0,  # Min projection value
            self.presenter.get_num_images(),  # Max possible value
        )
        if accepted:
            self.image_view.set_selected_image(projection_to_goto)

    def goto_angle(self):
        projection_to_goto, accepted = QInputDialog.getDouble(
            self,
            "Enter Angle",
            "Angle in Degrees",
            0,  # Default value
            0,  # Min projection value
            2147483647,  # Max possible value
            4,  # Digits/decimals
        )
        if accepted:
            self.image_view.set_selected_image(
                self.presenter.find_image_from_angle(projection_to_goto))

    def set_roi(self):
        roi, accepted = QInputDialog.getText(
            self,
            "Manual ROI",
            "Enter ROI in order left, top, right, bottom, with commas in-between each number",
            text="0, 0, 50, 50")
        if accepted:
            roi = [int(r.strip()) for r in roi.split(",")]
            self.image_view.roi.setPos((roi[0], roi[1]), update=False)
            self.image_view.roi.setSize((roi[2] - roi[0], roi[3] - roi[1]))
            self.image_view.roi.show()
            self.image_view.roiChanged()

    def copy_roi_to_clipboard(self):
        pos, size = self.image_view.get_roi()
        QGuiApplication.clipboard().setText(
            f"{pos.x}, {pos.y}, {pos.x + size.x}, {pos.y + size.y}")

    def change_window_name_clicked(self):
        input_window = QInputDialog()
        new_window_name, ok = input_window.getText(self,
                                                   "Change window name",
                                                   "Name:",
                                                   text=self.name)
        if ok:
            if new_window_name not in self.main_window.stack_names:
                self.main_window.rename_stack(self.name, new_window_name)
            else:
                error = QMessageBox(self)
                error.setWindowTitle("Stack name conflict")
                error.setText(
                    f"There is already a window named {new_window_name}")
                error.exec()

    def show_image_metadata(self):
        dialog = MetadataDialog(self, self.presenter.images)
        dialog.show()

    def show_op_history_copy_dialog(self):
        dialog = OpHistoryCopyDialogView(self, self.presenter.images,
                                         self.main_window)
        dialog.show()

    def mark_as_sinograms(self):
        # 1 is position of sinograms, 0 is projections
        current = 1 if self.presenter.images._is_sinograms else 0
        item, accepted = QInputDialog.getItem(
            self, "Select if projections or sinograms", "Images are:",
            ["projections", "sinograms"], current)
        if accepted:
            self.presenter.images._is_sinograms = False if item == "projections" else True

    def ask_confirmation(self, msg: str):
        response = QMessageBox.question(self, "Confirm action", msg,
                                        QMessageBox.Ok
                                        | QMessageBox.Cancel)  # type:ignore
        return response == QMessageBox.Ok
示例#5
0
    def __init__(self, original_stack: Images, new_stack: Images,
                 presenter: Union['StackComparePresenter',
                                  'StackChoicePresenter'],
                 parent: Optional[QMainWindow]):
        super().__init__(parent, "gui/ui/stack_choice_window.ui")

        self.presenter = presenter

        self.setWindowTitle("Choose the stack you want to keep")
        self.setWindowModality(Qt.WindowModality.ApplicationModal)

        # Create stacks and place them in the choice window
        self.original_stack = MIImageView(detailsSpanAllCols=True)
        self.original_stack.name = "Original Stack"
        self.original_stack.enable_nan_check(True)

        self.new_stack = MIImageView(detailsSpanAllCols=True)
        self.new_stack.name = "New Stack"
        self.new_stack.enable_nan_check(True)

        self._setup_stack_for_view(self.original_stack, original_stack.data)
        self._setup_stack_for_view(self.new_stack, new_stack.data)

        self.topVerticalOriginal.addWidget(self.original_stack)
        self.topVerticalNew.addWidget(self.new_stack)

        self.shifting_through_images = False
        self.original_stack.sigTimeChanged.connect(
            self._sync_timelines_for_new_stack_with_old_stack)
        self.new_stack.sigTimeChanged.connect(
            self._sync_timelines_for_old_stack_with_new_stack)

        # Hook nav buttons into original stack (new stack not needed as the timelines are synced)
        self.leftButton.pressed.connect(
            self.original_stack.button_stack_left.pressed)
        self.leftButton.released.connect(
            self.original_stack.button_stack_left.released)
        self.rightButton.pressed.connect(
            self.original_stack.button_stack_right.pressed)
        self.rightButton.released.connect(
            self.original_stack.button_stack_right.released)

        # Hook the choice buttons
        self.originalDataButton.clicked.connect(
            lambda: self.presenter.notify(Notification.CHOOSE_ORIGINAL))
        self.newDataButton.clicked.connect(
            lambda: self.presenter.notify(Notification.CHOOSE_NEW_DATA))

        # Hooks the lock histograms checkbox
        self.lockHistograms.clicked.connect(
            lambda: self.presenter.notify(Notification.TOGGLE_LOCK_HISTOGRAMS))

        # Hook ROI button into both stacks
        self.roiButton.clicked.connect(self._toggle_roi)

        # Hook the two plot ROIs together so that any changes are synced
        self.original_stack.roi.sigRegionChanged.connect(
            self._sync_roi_plot_for_new_stack_with_old_stack)
        self.new_stack.roi.sigRegionChanged.connect(
            self._sync_roi_plot_for_old_stack_with_new_stack)

        self._sync_both_image_axis()
        self._ensure_range_is_the_same()

        self.choice_made = False
        self.roi_shown = False
示例#6
0
class StackChoiceView(BaseMainWindowView):
    originalDataButton: QPushButton
    newDataButton: QPushButton
    lockHistograms: QCheckBox

    def __init__(self, original_stack: Images, new_stack: Images,
                 presenter: Union['StackComparePresenter',
                                  'StackChoicePresenter'],
                 parent: Optional[QMainWindow]):
        super().__init__(parent, "gui/ui/stack_choice_window.ui")

        self.presenter = presenter

        self.setWindowTitle("Choose the stack you want to keep")
        self.setWindowModality(Qt.WindowModality.ApplicationModal)

        # Create stacks and place them in the choice window
        self.original_stack = MIImageView(detailsSpanAllCols=True)
        self.original_stack.name = "Original Stack"
        self.original_stack.enable_nan_check(True)

        self.new_stack = MIImageView(detailsSpanAllCols=True)
        self.new_stack.name = "New Stack"
        self.new_stack.enable_nan_check(True)

        self._setup_stack_for_view(self.original_stack, original_stack.data)
        self._setup_stack_for_view(self.new_stack, new_stack.data)

        self.topVerticalOriginal.addWidget(self.original_stack)
        self.topVerticalNew.addWidget(self.new_stack)

        self.shifting_through_images = False
        self.original_stack.sigTimeChanged.connect(
            self._sync_timelines_for_new_stack_with_old_stack)
        self.new_stack.sigTimeChanged.connect(
            self._sync_timelines_for_old_stack_with_new_stack)

        # Hook nav buttons into original stack (new stack not needed as the timelines are synced)
        self.leftButton.pressed.connect(
            self.original_stack.button_stack_left.pressed)
        self.leftButton.released.connect(
            self.original_stack.button_stack_left.released)
        self.rightButton.pressed.connect(
            self.original_stack.button_stack_right.pressed)
        self.rightButton.released.connect(
            self.original_stack.button_stack_right.released)

        # Hook the choice buttons
        self.originalDataButton.clicked.connect(
            lambda: self.presenter.notify(Notification.CHOOSE_ORIGINAL))
        self.newDataButton.clicked.connect(
            lambda: self.presenter.notify(Notification.CHOOSE_NEW_DATA))

        # Hooks the lock histograms checkbox
        self.lockHistograms.clicked.connect(
            lambda: self.presenter.notify(Notification.TOGGLE_LOCK_HISTOGRAMS))

        # Hook ROI button into both stacks
        self.roiButton.clicked.connect(self._toggle_roi)

        # Hook the two plot ROIs together so that any changes are synced
        self.original_stack.roi.sigRegionChanged.connect(
            self._sync_roi_plot_for_new_stack_with_old_stack)
        self.new_stack.roi.sigRegionChanged.connect(
            self._sync_roi_plot_for_old_stack_with_new_stack)

        self._sync_both_image_axis()
        self._ensure_range_is_the_same()

        self.choice_made = False
        self.roi_shown = False

    def _ensure_range_is_the_same(self):
        new_range = self.new_stack.ui.histogram.getLevels()
        original_range = self.original_stack.ui.histogram.getLevels()

        new_max_y = max(new_range[0], new_range[1])
        new_min_y = min(new_range[0], new_range[1])
        original_max_y = max(original_range[0], original_range[1])
        original_min_y = min(original_range[0], original_range[1])
        y_range_min = min(new_min_y, original_min_y)
        y_range_max = max(new_max_y, original_max_y)

        self.new_stack.ui.histogram.vb.setRange(yRange=(y_range_min,
                                                        y_range_max))
        self.original_stack.ui.histogram.vb.setRange(yRange=(y_range_min,
                                                             y_range_max))

    def _toggle_roi(self):
        if self.roi_shown:
            self.roi_shown = False
            self.original_stack.ui.roiBtn.setChecked(False)
            self.new_stack.ui.roiBtn.setChecked(False)
            self.original_stack.roiClicked()
            self.new_stack.roiClicked()
        else:
            self.roi_shown = True
            self.original_stack.ui.roiBtn.setChecked(True)
            self.new_stack.ui.roiBtn.setChecked(True)
            self.original_stack.roiClicked()
            self.new_stack.roiClicked()

    def _setup_stack_for_view(self, stack: MIImageView, data: np.ndarray):
        stack.setContentsMargins(4, 4, 4, 4)
        stack.setImage(data)
        stack.ui.menuBtn.hide()
        stack.ui.roiBtn.hide()
        stack.button_stack_right.hide()
        stack.button_stack_left.hide()
        details_size_policy = QSizePolicy(QSizePolicy.MinimumExpanding,
                                          QSizePolicy.Preferred)
        details_size_policy.setHorizontalStretch(1)
        stack.details.setSizePolicy(details_size_policy)
        self.roiButton.clicked.connect(stack.roiClicked)

    def _sync_roi_plot_for_new_stack_with_old_stack(self):
        self.new_stack.roi.sigRegionChanged.disconnect(
            self._sync_roi_plot_for_old_stack_with_new_stack)
        self.new_stack.roi.setPos(self.original_stack.roi.pos())
        self.new_stack.roi.setSize(self.original_stack.roi.size())
        self.new_stack.roi.sigRegionChanged.connect(
            self._sync_roi_plot_for_old_stack_with_new_stack)

    def _sync_roi_plot_for_old_stack_with_new_stack(self):
        self.original_stack.roi.sigRegionChanged.disconnect(
            self._sync_roi_plot_for_new_stack_with_old_stack)
        self.original_stack.roi.setPos(self.new_stack.roi.pos())
        self.original_stack.roi.setSize(self.new_stack.roi.size())
        self.original_stack.roi.sigRegionChanged.connect(
            self._sync_roi_plot_for_new_stack_with_old_stack)

    def _sync_timelines_for_new_stack_with_old_stack(self, index, _):
        self.new_stack.sigTimeChanged.disconnect(
            self._sync_timelines_for_old_stack_with_new_stack)
        self.new_stack.setCurrentIndex(index)
        self.new_stack.sigTimeChanged.connect(
            self._sync_timelines_for_old_stack_with_new_stack)

    def _sync_timelines_for_old_stack_with_new_stack(self, index, _):
        self.original_stack.sigTimeChanged.disconnect(
            self._sync_timelines_for_new_stack_with_old_stack)
        self.original_stack.setCurrentIndex(index)
        self.original_stack.sigTimeChanged.connect(
            self._sync_timelines_for_new_stack_with_old_stack)

    def _sync_both_image_axis(self):
        self.original_stack.view.linkView(ViewBox.XAxis, self.new_stack.view)
        self.original_stack.view.linkView(ViewBox.YAxis, self.new_stack.view)

    def closeEvent(self, e):
        # Confirm exit is actually wanted as it will lead to data loss
        if not self.choice_made:
            response = QMessageBox.warning(
                self, "Data Loss! Are you sure?",
                "You will lose the original stack if you close this window! Are you sure?",
                QMessageBox.Ok | QMessageBox.Cancel)
            if response == QMessageBox.Ok:
                self.presenter.notify(Notification.CHOOSE_NEW_DATA)
            else:
                e.ignore()
                return

        self.original_stack.close()
        self.new_stack.close()

    def _set_from_old_to_new(self):
        """
        Signal triggered when the histograms are locked and the contrast values changed.
        """
        levels: Tuple[float,
                      float] = self.original_stack.ui.histogram.getLevels()
        self.new_stack.ui.histogram.setLevels(*levels)

    def _set_from_new_to_old(self):
        """
        Signal triggered when the histograms are locked and the contrast values changed.
        """
        levels: Tuple[float, float] = self.new_stack.ui.histogram.getLevels()
        self.original_stack.ui.histogram.setLevels(*levels)

    def connect_histogram_changes(self):
        self._set_from_old_to_new()

        self.original_stack.ui.histogram.sigLevelsChanged.connect(
            self._set_from_old_to_new)
        self.new_stack.ui.histogram.sigLevelsChanged.connect(
            self._set_from_new_to_old)

        self.new_stack.ui.histogram.vb.linkView(
            ViewBox.YAxis, self.original_stack.ui.histogram.vb)
        self.new_stack.ui.histogram.vb.linkView(
            ViewBox.XAxis, self.original_stack.ui.histogram.vb)

    def disconnect_histogram_changes(self):
        self.original_stack.ui.histogram.sigLevelsChanged.disconnect(
            self._set_from_old_to_new)
        self.new_stack.ui.histogram.sigLevelsChanged.disconnect(
            self._set_from_new_to_old)

        self.new_stack.ui.histogram.vb.linkView(ViewBox.YAxis, None)
        self.new_stack.ui.histogram.vb.linkView(ViewBox.XAxis, None)
示例#7
0
class FiltersWindowView(BaseMainWindowView):
    auto_update_triggered = pyqtSignal()
    filter_applied = pyqtSignal()

    splitter: QSplitter
    collapseToggleButton: QPushButton

    linkImages: QCheckBox
    invertDifference: QCheckBox
    overlayDifference: QCheckBox
    lockScaleCheckBox: QCheckBox
    lockZoomCheckBox: QCheckBox

    previewsLayout: QVBoxLayout
    previews: FilterPreviews
    stackSelector: StackSelectorWidgetView

    notification_icon: QLabel
    notification_text: QLabel

    presenter: FiltersWindowPresenter

    applyButton: QPushButton
    applyToAllButton: QPushButton
    filterSelector: QComboBox

    def __init__(self, main_window: 'MainWindowView'):
        super().__init__(main_window, 'gui/ui/filters_window.ui')

        self.main_window = main_window
        self.presenter = FiltersWindowPresenter(self, main_window)
        self.roi_view = None
        self.roi_view_averaged = False
        self.splitter.setSizes([200, 9999])
        self.splitter.setStretchFactor(0, 1)

        # Populate list of operations and handle filter selection
        self.filterSelector.addItems(self.presenter.model.filter_names)
        self.filterSelector.currentTextChanged.connect(
            self.handle_filter_selection)
        self.filterSelector.currentTextChanged.connect(
            self._update_apply_all_button)

        # Handle stack selection
        self.stackSelector.stack_selected_uuid.connect(
            self.presenter.set_stack_uuid)
        self.stackSelector.stack_selected_uuid.connect(
            self.auto_update_triggered.emit)

        # Handle apply filter
        self.applyButton.clicked.connect(
            lambda: self.presenter.notify(PresNotification.APPLY_FILTER))
        self.applyToAllButton.clicked.connect(lambda: self.presenter.notify(
            PresNotification.APPLY_FILTER_TO_ALL))

        self.previews = FilterPreviews(self)
        self.previews.setSizePolicy(QSizePolicy.Expanding,
                                    QSizePolicy.Expanding)
        self.previewsLayout.addWidget(self.previews)
        self.clear_previews()

        self.linkImages.stateChanged.connect(self.link_images_changed)
        # set here to trigger the changed event
        self.linkImages.setChecked(True)
        self.invertDifference.stateChanged.connect(
            lambda: self.presenter.notify(PresNotification.UPDATE_PREVIEWS))
        self.overlayDifference.stateChanged.connect(
            lambda: self.presenter.notify(PresNotification.UPDATE_PREVIEWS))

        # Handle preview index selection
        self.previewImageIndex.valueChanged[int].connect(
            self.presenter.set_preview_image_index)

        # Preview update triggers
        self.auto_update_triggered.connect(self.on_auto_update_triggered)
        self.updatePreviewButton.clicked.connect(
            lambda: self.presenter.notify(PresNotification.UPDATE_PREVIEWS))

        self.stackSelector.subscribe_to_main_window(main_window)
        self.stackSelector.select_eligible_stack()

        # Handle help button pressed
        self.filterHelpButton.pressed.connect(self.open_help_webpage)
        self.collapseToggleButton.pressed.connect(self.toggle_filters_section)

        self.handle_filter_selection("")

    def closeEvent(self, e):
        if self.presenter.filter_is_running:
            e.ignore()
        else:
            super().closeEvent(e)

    def cleanup(self):
        self.stackSelector.unsubscribe_from_main_window()
        if self.roi_view is not None:
            self.roi_view.close()
            self.roi_view = None
        self.auto_update_triggered.disconnect()
        self.main_window.filters = None
        self.presenter = None

    def show(self):
        super().show()
        self.auto_update_triggered.emit()

    def handle_filter_selection(self, filter_name: str):
        """
        Handle selection of a filter from the drop down list.
        """
        # If a divider select the one below the divider.
        if filter_name == self.presenter.divider:
            self.filterSelector.setCurrentIndex(
                self.filterSelector.currentIndex() + 1)
            # Changing the selection triggers a second run through of this method that results in unwanted popups
            # Terminating the original call here ensures that the presenter is only notified once
            return

        # Remove all existing items from the properties layout
        delete_all_widgets_from_layout(self.filterPropertiesLayout)

        # Do registration of new filter
        self.presenter.notify(PresNotification.REGISTER_ACTIVE_FILTER)

        # Update preview on filter selection (on the off chance the default
        # options are valid)
        self.auto_update_triggered.emit()

    def on_auto_update_triggered(self):
        """
        Called when the signal indicating the filter, filter properties or data
        has changed such that the previews are now out of date.
        """
        # Disable the preview image box widget as it can misbehave if making a preview takes too long
        self.previewImageIndex.setEnabled(False)

        self.clear_notification_dialog()
        if self.previewAutoUpdate.isChecked() and self.isVisible():
            self.presenter.notify(PresNotification.UPDATE_PREVIEWS)

        # Enable the spinbox widget once the preview has been created
        self.previewImageIndex.setEnabled(True)

    def clear_previews(self, clear_before: bool = True):
        self.previews.clear_items(clear_before=clear_before)

    def link_images_changed(self):
        if self.linkImages.isChecked():
            self.previews.link_all_views()
        else:
            self.previews.unlink_all_views()

    @property
    def preview_image_before(self) -> ImageItem:
        return self.previews.imageview_before

    @property
    def preview_image_after(self) -> ImageItem:
        return self.previews.imageview_after

    @property
    def preview_image_difference(self) -> ImageItem:
        return self.previews.imageview_difference

    def show_error_dialog(self, msg=""):
        self.notification_text.show()
        self.notification_icon.setPixmap(QApplication.style().standardPixmap(
            QStyle.SP_MessageBoxCritical))
        self.notification_text.setText(str(msg))

    def clear_notification_dialog(self):
        self.notification_icon.clear()
        self.notification_text.clear()
        self.notification_text.hide()

    def show_operation_completed(self, operation_name):
        self.notification_text.show()
        self.notification_icon.setPixmap(QApplication.style().standardPixmap(
            QStyle.SP_DialogYesButton))
        self.notification_text.setText(
            f"{operation_name} completed successfully!")

    def show_operation_cancelled(self, operation_name):
        self.notification_text.show()
        self.notification_icon.setPixmap(QApplication.style().standardPixmap(
            QStyle.SP_DialogYesButton))
        self.notification_text.setText(
            f"{operation_name} cancelled, original data restored")

    def open_help_webpage(self):
        filter_name = self.filterSelector.currentText()

        try:
            open_user_operation_docs(filter_name)
        except RuntimeError as err:
            self.show_error_dialog(str(err))

    def ask_confirmation(self, msg: str):
        response = QMessageBox.question(self, "Confirm action", msg,
                                        QMessageBox.Ok
                                        | QMessageBox.Cancel)  # type:ignore
        return response == QMessageBox.Ok

    def _update_apply_all_button(self, filter_name):
        list_of_apply_single_stack = ["ROI Normalisation", "Flat-fielding"]
        if filter_name in list_of_apply_single_stack:
            self.applyToAllButton.setEnabled(False)
        else:
            self.applyToAllButton.setEnabled(True)

    def roi_visualiser(self, roi_field):
        # Start the stack visualiser and ensure that it uses the ROI from here in the rest of this
        try:
            images = self.presenter.stack.presenter.get_image(
                self.presenter.model.preview_image_idx)
        except Exception:
            # Happens if nothing has been loaded, so do nothing as nothing can't be visualised
            return

        window = QMainWindow(self)
        window.setWindowTitle("Select ROI")
        window.setMinimumHeight(600)
        window.setMinimumWidth(600)
        self.roi_view = MIImageView(window)
        window.setCentralWidget(self.roi_view)
        self.roi_view.setWindowTitle("Select ROI for operation")

        def set_averaged_image():
            averaged_images = np.sum(
                self.presenter.stack.presenter.images.data, axis=0)
            self.roi_view.setImage(
                averaged_images.reshape(
                    (1, averaged_images.shape[0], averaged_images.shape[1])))
            self.roi_view_averaged = True

        def toggle_average_images(images_):
            if self.roi_view_averaged:
                self.roi_view.setImage(images_.data)
                self.roi_view_averaged = False
            else:
                set_averaged_image()
            self.roi_view.roi.show()
            self.roi_view.ui.roiPlot.hide()

        # Add context menu bits:
        menu = QMenu(self.roi_view)
        toggle_show_averaged_image = QAction("Toggle show averaged image",
                                             menu)
        toggle_show_averaged_image.triggered.connect(
            lambda: toggle_average_images(images))
        menu.addAction(toggle_show_averaged_image)
        menu.addSeparator()
        self.roi_view.imageItem.menu = menu

        set_averaged_image()

        def roi_changed_callback(callback):
            roi_field.setText(callback.to_list_string())
            roi_field.editingFinished.emit()

        self.roi_view.roi_changed_callback = lambda callback: roi_changed_callback(
            callback)

        # prep the MIImageView to display in this context
        self.roi_view.ui.roiBtn.hide()
        self.roi_view.ui.histogram.hide()
        self.roi_view.ui.menuBtn.hide()
        self.roi_view.ui.roiPlot.hide()
        self.roi_view.roi.show()
        self.roi_view.ui.gridLayout.setRowStretch(1, 5)
        self.roi_view.ui.gridLayout.setRowStretch(0, 95)
        self.roi_view.button_stack_right.hide()
        self.roi_view.button_stack_left.hide()
        button = QPushButton("OK", window)
        button.clicked.connect(lambda: window.close())
        self.roi_view.ui.gridLayout.addWidget(button)

        window.show()

    def toggle_filters_section(self):
        if self.collapseToggleButton.text() == "<<":
            self.splitter.setSizes([0, 9999])
            self.collapseToggleButton.setText(">>")
        else:
            self.splitter.setSizes([200, 9999])
            self.collapseToggleButton.setText("<<")
示例#8
0
    def roi_visualiser(self, roi_field):
        # Start the stack visualiser and ensure that it uses the ROI from here in the rest of this
        try:
            images = self.presenter.stack.presenter.get_image(
                self.presenter.model.preview_image_idx)
        except Exception:
            # Happens if nothing has been loaded, so do nothing as nothing can't be visualised
            return

        window = QMainWindow(self)
        window.setWindowTitle("Select ROI")
        window.setMinimumHeight(600)
        window.setMinimumWidth(600)
        self.roi_view = MIImageView(window)
        window.setCentralWidget(self.roi_view)
        self.roi_view.setWindowTitle("Select ROI for operation")

        def set_averaged_image():
            averaged_images = np.sum(
                self.presenter.stack.presenter.images.data, axis=0)
            self.roi_view.setImage(
                averaged_images.reshape(
                    (1, averaged_images.shape[0], averaged_images.shape[1])))
            self.roi_view_averaged = True

        def toggle_average_images(images_):
            if self.roi_view_averaged:
                self.roi_view.setImage(images_.data)
                self.roi_view_averaged = False
            else:
                set_averaged_image()
            self.roi_view.roi.show()
            self.roi_view.ui.roiPlot.hide()

        # Add context menu bits:
        menu = QMenu(self.roi_view)
        toggle_show_averaged_image = QAction("Toggle show averaged image",
                                             menu)
        toggle_show_averaged_image.triggered.connect(
            lambda: toggle_average_images(images))
        menu.addAction(toggle_show_averaged_image)
        menu.addSeparator()
        self.roi_view.imageItem.menu = menu

        set_averaged_image()

        def roi_changed_callback(callback):
            roi_field.setText(callback.to_list_string())
            roi_field.editingFinished.emit()

        self.roi_view.roi_changed_callback = lambda callback: roi_changed_callback(
            callback)

        # prep the MIImageView to display in this context
        self.roi_view.ui.roiBtn.hide()
        self.roi_view.ui.histogram.hide()
        self.roi_view.ui.menuBtn.hide()
        self.roi_view.ui.roiPlot.hide()
        self.roi_view.roi.show()
        self.roi_view.ui.gridLayout.setRowStretch(1, 5)
        self.roi_view.ui.gridLayout.setRowStretch(0, 95)
        self.roi_view.button_stack_right.hide()
        self.roi_view.button_stack_left.hide()
        button = QPushButton("OK", window)
        button.clicked.connect(lambda: window.close())
        self.roi_view.ui.gridLayout.addWidget(button)

        window.show()