示例#1
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
示例#2
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)