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)
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)
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)
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
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
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)
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("<<")
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()