diff --git a/docs/release_notes/next/feature-2027-Spectrum-ROI-Adjustable b/docs/release_notes/next/feature-2027-Spectrum-ROI-Adjustable new file mode 100644 index 00000000000..1c6d6156b91 --- /dev/null +++ b/docs/release_notes/next/feature-2027-Spectrum-ROI-Adjustable @@ -0,0 +1 @@ +#2027: The Spectrum ROI details display in an adjustable table via spinboxes \ No newline at end of file diff --git a/mantidimaging/gui/ui/spectrum_viewer.ui b/mantidimaging/gui/ui/spectrum_viewer.ui index b00fe2be2cc..52be844f773 100644 --- a/mantidimaging/gui/ui/spectrum_viewer.ui +++ b/mantidimaging/gui/ui/spectrum_viewer.ui @@ -6,8 +6,8 @@ 0 0 - 921 - 739 + 905 + 752 @@ -351,17 +351,54 @@ - + + + + 0 + 0 + + + + + 16777215 + 200 + + + + ROI Properties + + + + + + + 0 + 0 + + + + + 16777215 + 200 + + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + + + + + + Qt::Vertical - - QSizePolicy::MinimumExpanding - 20 - 20 + 40 diff --git a/mantidimaging/gui/windows/spectrum_viewer/presenter.py b/mantidimaging/gui/windows/spectrum_viewer/presenter.py index d1aaa910c14..2b77a9aea23 100644 --- a/mantidimaging/gui/windows/spectrum_viewer/presenter.py +++ b/mantidimaging/gui/windows/spectrum_viewer/presenter.py @@ -80,6 +80,7 @@ def handle_sample_change(self, uuid: Optional['UUID']) -> None: self.add_rits_roi() self.view.set_normalise_error(self.model.normalise_issue()) self.show_new_sample() + self.view.on_visibility_change() def handle_normalise_stack_change(self, normalise_uuid: Optional['UUID']) -> None: if normalise_uuid == self.current_norm_stack_uuid: @@ -118,6 +119,8 @@ def show_new_sample(self) -> None: self.view.set_image(self.model.get_averaged_image()) self.view.spectrum_widget.spectrum_plot_widget.add_range(*self.model.tof_range) self.view.auto_range_image() + if self.view.get_roi_properties_spinboxes(): + self.view.set_roi_properties() def handle_range_slide_moved(self, tof_range) -> None: self.model.tof_range = tof_range @@ -133,6 +136,10 @@ def handle_roi_moved(self, force_new_spectrums: bool = False) -> None: self.model.set_roi(name, roi) self.view.set_spectrum(name, self.model.get_spectrum(name, self.spectrum_mode)) + def handle_roi_clicked(self, roi) -> None: + self.view.current_roi = roi.name + self.view.set_roi_properties() + def redraw_spectrum(self, name: str) -> None: """ Redraw the spectrum with the given name diff --git a/mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py b/mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py index 338d4cf9036..d9f3e8902ca 100644 --- a/mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py +++ b/mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py @@ -40,6 +40,7 @@ def __init__(self, name: str, sensible_roi: SensibleROI, *args, **kwargs): self.addScaleHandle([0, 0], [1, 1]) self.addScaleHandle([0, 1], [1, 0]) self._selected_row = None + self.roi.setAcceptedMouseButtons(Qt.MouseButton.LeftButton) self.menu = QMenu() change_color_action = QAction("Change ROI Colour", self) @@ -82,6 +83,10 @@ def colour(self, colour: tuple[int, int, int, int]) -> None: def selected_row(self) -> Optional[int]: return self._selected_row + def adjust_spec_roi(self, roi: SensibleROI) -> None: + self.setPos((roi.left, roi.top)) + self.setSize((roi.width, roi.height)) + class SpectrumWidget(QWidget): """ @@ -91,6 +96,13 @@ class SpectrumWidget(QWidget): """ image: MIMiniImageView spectrum: PlotItem + + range_control: LinearRegionItem + roi_dict: dict[Optional[str], ROI] + last_clicked_roi: str + + range_changed = pyqtSignal(object) + roi_clicked = pyqtSignal(object) roi_changed = pyqtSignal() roiColorChangeRequested = pyqtSignal(str, tuple) @@ -160,8 +172,9 @@ def set_roi_visibility_flags(self, name: str, visible: bool) -> None: for handle in handles: handle.setVisible(visible) self.roi_dict[name].setVisible(visible) - self.roi_dict[name].setAcceptedMouseButtons(Qt.NoButton) + self.roi_dict[name].setAcceptedMouseButtons(Qt.MouseButton.LeftButton) self.roi_dict[name].sigRegionChanged.connect(self.roi_changed.emit) + self.roi_dict[name].sigClicked.connect(self.roi_clicked.emit) def set_roi_alpha(self, name: str, alpha: float) -> None: """ @@ -190,9 +203,18 @@ def add_roi(self, roi: SensibleROI, name: str) -> None: self.roi_dict[name] = roi_object.roi self.max_roi_size = roi_object.size() self.roi_dict[name].sigRegionChanged.connect(self.roi_changed.emit) + self.roi_dict[name].sigClicked.connect(self.roi_clicked.emit) self.image.vb.addItem(self.roi_dict[name]) self.roi_dict[name].hoverPen = mkPen(self.roi_dict[name].colour, width=3) + def adjust_roi(self, new_roi: SensibleROI, roi_name: str): + """ + Adjust the existing ROI with the given name. + @param new_roi: The new SpectrumROI to replace the existing SpectrumROI + @param roi_name: The name of the existing ROI. + """ + self.roi_dict[roi_name].adjust_spec_roi(new_roi) + def get_roi(self, roi_name: str) -> SensibleROI: """ Get the ROI with the given name. If no name is given, the default ROI is returned. diff --git a/mantidimaging/gui/windows/spectrum_viewer/view.py b/mantidimaging/gui/windows/spectrum_viewer/view.py index 300269aed3a..2d875c3b47a 100644 --- a/mantidimaging/gui/windows/spectrum_viewer/view.py +++ b/mantidimaging/gui/windows/spectrum_viewer/view.py @@ -7,7 +7,8 @@ from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QFileDialog, QPushButton, QLabel, QAbstractItemView, QHeaderView, \ - QTabWidget, QComboBox, QSpinBox + QTabWidget, QComboBox, QSpinBox, QTableWidget, QTableWidgetItem, QGroupBox +from PyQt5.QtCore import QSignalBlocker, Qt from mantidimaging.core.utility import finder from mantidimaging.gui.mvp_base import BaseMainWindowView @@ -17,6 +18,7 @@ from mantidimaging.gui.widgets import RemovableRowTableView from .spectrum_widget import SpectrumWidget from mantidimaging.gui.windows.spectrum_viewer.roi_table_model import TableModel +from mantidimaging.core.utility.sensible_roi import SensibleROI import numpy as np @@ -42,6 +44,11 @@ class SpectrumViewerWindowView(BaseMainWindowView): bin_size_spinBox: QSpinBox bin_step_spinBox: QSpinBox + roiPropertiesTableWidget: QTableWidget + roiPropertiesGroupBox: QGroupBox + + last_clicked_roi: str + spectrum_widget: SpectrumWidget def __init__(self, main_window: 'MainWindowView'): @@ -55,6 +62,8 @@ def __init__(self, main_window: 'MainWindowView'): self.selected_row: int = 0 self.current_roi: str = "" self.selected_row_data: Optional[list] = None + self.roiPropertiesSpinBoxes: dict[str, QSpinBox] = {} + self.roiPropertiesLabels: dict[str, QLabel] = {} self.presenter = SpectrumViewerWindowPresenter(self, main_window) @@ -64,6 +73,8 @@ def __init__(self, main_window: 'MainWindowView'): self.imageLayout.addWidget(self.spectrum_widget) self.spectrum.range_changed.connect(self.presenter.handle_range_slide_moved) + + self.spectrum_widget.roi_clicked.connect(self.presenter.handle_roi_clicked) self.spectrum_widget.roi_changed.connect(self.presenter.handle_roi_moved) self.spectrum_widget.roiColorChangeRequested.connect(self.presenter.change_roi_colour) @@ -98,7 +109,53 @@ def __init__(self, main_window: 'MainWindowView'): self.tableView.setSelectionMode(QAbstractItemView.SingleSelection) self.tableView.setAlternatingRowColors(True) + # Roi Prop table + self.roi_table_properties = ["Top", "Bottom", "Left", "Right"] + self.roi_table_properties_secondary = ["Width", "Height"] + self.roiPropertiesTableWidget.setColumnCount(3) + self.roiPropertiesTableWidget.setRowCount(3) + self.roiPropertiesTableWidget.setColumnWidth(0, 80) + self.roiPropertiesTableWidget.setColumnWidth(1, 50) + self.roiPropertiesTableWidget.setColumnWidth(2, 50) + + for prop in self.roi_table_properties: + spin_box = QSpinBox() + if prop == "Top" or prop == "Bottom": + spin_box.setMaximum(self.spectrum_widget.image.image_data.shape[0]) + if prop == "Left" or prop == "Right": + spin_box.setMaximum(self.spectrum_widget.image.image_data.shape[1]) + spin_box.valueChanged.connect(self.adjust_roi) + self.roiPropertiesSpinBoxes[prop] = spin_box + for prop in self.roi_table_properties_secondary: + label = QLabel() + self.roiPropertiesLabels[prop] = label + + self.roiPropertiesTableWidget.horizontalHeader().hide() + self.roiPropertiesTableWidget.verticalHeader().hide() + self.roiPropertiesTableWidget.setShowGrid(False) + + roiPropertiesTableText = ["x1, x2", "y1, y2", "Size"] + self.roiPropertiesTableTextDict = {} + for text in roiPropertiesTableText: + item = QTableWidgetItem(text) + item.setFlags(Qt.ItemIsSelectable) + self.roiPropertiesTableTextDict[text] = item + + self.roiPropertiesTableWidget.setItem(0, 0, self.roiPropertiesTableTextDict["x1, x2"]) + self.roiPropertiesTableWidget.setCellWidget(0, 1, self.roiPropertiesSpinBoxes["Left"]) + self.roiPropertiesTableWidget.setCellWidget(0, 2, self.roiPropertiesSpinBoxes["Right"]) + self.roiPropertiesTableWidget.setItem(1, 0, self.roiPropertiesTableTextDict["y1, y2"]) + self.roiPropertiesTableWidget.setCellWidget(1, 1, self.roiPropertiesSpinBoxes["Top"]) + self.roiPropertiesTableWidget.setCellWidget(1, 2, self.roiPropertiesSpinBoxes["Bottom"]) + self.roiPropertiesTableWidget.setItem(2, 0, self.roiPropertiesTableTextDict["Size"]) + self.roiPropertiesTableWidget.setCellWidget(2, 1, self.roiPropertiesLabels["Width"]) + self.roiPropertiesTableWidget.setCellWidget(2, 2, self.roiPropertiesLabels["Height"]) + + self.spectrum_widget.roi_changed.connect(self.set_roi_properties) + _ = self.roi_table_model # Initialise model + self.current_roi = self.last_clicked_roi = self.roi_table_model.roi_names()[0] + self.set_roi_properties() def on_row_change(item, _) -> None: """ @@ -110,6 +167,7 @@ def on_row_change(item, _) -> None: selected_row_data = self.roi_table_model.row_data(item.row()) self.selected_row = item.row() self.current_roi = selected_row_data[0] + self.set_roi_properties() self.tableView.selectionModel().currentRowChanged.connect(on_row_change) @@ -156,6 +214,11 @@ def on_visibility_change(self) -> None: When the visibility of an ROI is changed, update the visibility of the ROI in the spectrum widget """ if self.presenter.export_mode == ExportMode.ROI_MODE: + self.current_roi = self.last_clicked_roi + if self.roi_table_model.rowCount() == 0: + self.disable_roi_properties() + if not self.roi_table_model.rowCount() == 0: + self.set_roi_properties() for roi_name, _, roi_visible in self.roi_table_model: if roi_visible is False: self.set_roi_alpha(0, roi_name) @@ -169,6 +232,12 @@ def on_visibility_change(self) -> None: if self.presenter.export_mode == ExportMode.IMAGE_MODE: self.set_roi_alpha(255, ROI_RITS) self.presenter.redraw_spectrum(ROI_RITS) + if self.current_roi != ROI_RITS: + self.last_clicked_roi = self.current_roi + self.current_roi = ROI_RITS + for _, spinbox in self.roiPropertiesSpinBoxes.items(): + spinbox.setEnabled(True) + self.set_roi_properties() else: self.set_roi_alpha(0, ROI_RITS) @@ -266,6 +335,10 @@ def set_new_roi(self) -> None: Set a new ROI on the image """ self.presenter.do_add_roi() + for _, spinbox in self.roiPropertiesSpinBoxes.items(): + if not spinbox.isEnabled(): + spinbox.setEnabled(True) + self.set_roi_properties() def update_roi_color_in_table(self, roi_name: str, new_color: tuple): """ @@ -339,6 +412,7 @@ def remove_roi(self) -> None: if self.roi_table_model.rowCount() == 0: self.removeBtn.setEnabled(False) + self.disable_roi_properties() def clear_all_rois(self) -> None: """ @@ -348,6 +422,7 @@ def clear_all_rois(self) -> None: self.spectrum_widget.spectrum_data_dict = {} self.spectrum_widget.spectrum.clearPlots() self.removeBtn.setEnabled(False) + self.disable_roi_properties() @property def transmission_error_mode(self) -> str: @@ -371,3 +446,49 @@ def set_binning_visibility(self) -> None: self.bin_size_spinBox.setHidden(hide_binning) self.bin_step_label.setHidden(hide_binning) self.bin_step_spinBox.setHidden(hide_binning) + + def set_roi_properties(self) -> None: + if self.presenter.export_mode == ExportMode.IMAGE_MODE: + self.current_roi = ROI_RITS + if self.current_roi not in self.presenter.model.get_list_of_roi_names() or not self.roiPropertiesSpinBoxes: + return + else: + current_roi = self.presenter.model.get_roi(self.current_roi) + self.roiPropertiesGroupBox.setTitle(f"Roi Properties: {self.current_roi}") + roi_iter_order = ["Left", "Top", "Right", "Bottom"] + for row, pos in enumerate(current_roi): + with QSignalBlocker(self.roiPropertiesSpinBoxes[roi_iter_order[row]]): + self.roiPropertiesSpinBoxes[roi_iter_order[row]].setValue(pos) + self.set_roi_spinbox_ranges() + self.presenter.redraw_spectrum(self.current_roi) + self.roiPropertiesLabels["Width"].setText(str(current_roi.width)) + self.roiPropertiesLabels["Height"].setText(str(current_roi.height)) + for spinbox in self.roiPropertiesSpinBoxes.values(): + spinbox.setEnabled(True) + + def adjust_roi(self) -> None: + roi_iter_order = ["Left", "Top", "Right", "Bottom"] + new_points = [self.roiPropertiesSpinBoxes[prop].value() for prop in roi_iter_order] + new_roi = SensibleROI().from_list(new_points) + self.presenter.model.set_roi(self.current_roi, new_roi) + self.spectrum_widget.adjust_roi(new_roi, self.current_roi) + + def set_roi_spinbox_ranges(self): + self.roiPropertiesSpinBoxes["Left"].setMaximum(self.roiPropertiesSpinBoxes["Right"].value() - 1) + self.roiPropertiesSpinBoxes["Right"].setMinimum(self.roiPropertiesSpinBoxes["Left"].value() + 1) + self.roiPropertiesSpinBoxes["Top"].setMaximum(self.roiPropertiesSpinBoxes["Bottom"].value() - 1) + self.roiPropertiesSpinBoxes["Bottom"].setMinimum(self.roiPropertiesSpinBoxes["Top"].value() + 1) + + def disable_roi_properties(self): + self.roiPropertiesGroupBox.setTitle("Roi Properties: None selected") + self.last_clicked_roi = "roi" + for _, spinbox in self.roiPropertiesSpinBoxes.items(): + with QSignalBlocker(spinbox): + spinbox.setMinimum(0) + spinbox.setValue(0) + spinbox.setDisabled(True) + for _, label in self.roiPropertiesLabels.items(): + label.setText("0") + + def get_roi_properties_spinboxes(self): + return self.roiPropertiesSpinBoxes