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 StackVisualiserView(QDockWidget): # Signal that signifies when the ROI is updated. Used to update previews in Filter views roi_updated = pyqtSignal(SensibleROI) image_view: MIImageView presenter: StackVisualiserPresenter layout: QVBoxLayout 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) @property def name(self): return self.windowTitle() @name.setter def name(self, name: str): self.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 @property def id(self): return self.presenter.images.id def closeEvent(self, event): self.setFloating(False) self.hide() super().closeEvent(event) 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