Source code for mantidimaging.gui.windows.spectrum_viewer.view

# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations

from pathlib import Path
from functools import partial
from typing import TYPE_CHECKING

from PyQt5 import QtWidgets
from pyqtgraph import mkPen
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (QCheckBox, QVBoxLayout, QFileDialog, QLabel, QGroupBox, QActionGroup, QAction)
from PyQt5.QtCore import QModelIndex
from logging import getLogger

from mantidimaging.core.utility import finder
from mantidimaging.gui.mvp_base import BaseMainWindowView
from mantidimaging.gui.widgets.dataset_selector import DatasetSelectorWidgetView
from .model import ROI_RITS, allowed_modes
from .presenter import SpectrumViewerWindowPresenter, ExportMode
from .spectrum_widget import SpectrumWidget
from mantidimaging.gui.widgets.spectrum_widgets.tof_properties import ExperimentSetupFormWidget
from mantidimaging.gui.widgets.spectrum_widgets.roi_selection_widget import ROISelectionWidget
from mantidimaging.gui.widgets.spectrum_widgets.fitting_display_widget import FittingDisplayWidget
from mantidimaging.gui.widgets.spectrum_widgets.fitting_param_form_widget import FittingParamFormWidget
from mantidimaging.gui.widgets.spectrum_widgets.export_settings_widget import FitExportFormWidget
from mantidimaging.gui.widgets.spectrum_widgets.export_data_table_widget import ExportDataTableWidget

import numpy as np

from ...widgets.spectrum_widgets.fitting_selection_widget import FitSelectionWidget

if TYPE_CHECKING:
    from mantidimaging.gui.windows.main import MainWindowView  # noqa:F401  # pragma: no cover
    from mantidimaging.gui.widgets.spectrum_widgets.roi_form_widget import ROIFormWidget, ROITableWidget
    from mantidimaging.core.fitting.fitting_functions import FittingRegion
    from uuid import UUID

LOG = getLogger(__name__)


[docs] class SpectrumViewerWindowView(BaseMainWindowView): sampleStackSelector: DatasetSelectorWidgetView normaliseStackSelector: DatasetSelectorWidgetView normaliseCheckBox: QCheckBox normalise_ShutterCount_CheckBox: QCheckBox imageLayout: QVBoxLayout fittingLayout: QVBoxLayout exportLayout: QVBoxLayout normaliseErrorIcon: QLabel shuttercountErrorIcon: QLabel normalise_error_issue: str = "" shuttercount_error_issue: str = "" spectrum_widget: SpectrumWidget experimentSetupGroupBox: QGroupBox experimentSetupFormWidget: ExperimentSetupFormWidget roi_form: ROIFormWidget def __init__(self, main_window: MainWindowView): super().__init__(None, 'gui/ui/spectrum_viewer.ui') self.main_window = main_window icon_path = finder.ROOT_PATH + "/gui/ui/images/exclamation-triangle-red.png" self.normalise_error_icon_pixmap = QPixmap(icon_path) self.presenter = SpectrumViewerWindowPresenter(self, main_window) self.spectrum_widget = SpectrumWidget(main_window) self.spectrum = self.spectrum_widget.spectrum_plot_widget self.imageLayout.addWidget(self.spectrum_widget) self.spectrum.range_changed.connect(self.presenter.handle_range_slide_moved) self.roiSelectionWidget = ROISelectionWidget(self) self.fittingFormLayout.layout().addWidget(self.roiSelectionWidget) self.fitSelectionWidget = FitSelectionWidget(self) self.fittingFormLayout.layout().addWidget(self.fitSelectionWidget) self.fitSelectionWidget.selectionChanged.connect(self.presenter.update_fitting_function) self.fittingDisplayWidget = FittingDisplayWidget() self.fittingDisplayWidget.unit_changed.connect(self.presenter.handle_tof_unit_change_via_menu) self.fittingLayout.addWidget(self.fittingDisplayWidget) self.roiSelectionWidget.selectionChanged.connect(self.handle_fitting_roi_changed) self.scalable_roi_widget = FittingParamFormWidget(self.presenter) self.fittingFormLayout.layout().addWidget(self.scalable_roi_widget) self.exportSettingsWidget = FitExportFormWidget() self.exportFormLayout.layout().addWidget(self.exportSettingsWidget) self.exportSettingsWidget.exportButton.clicked.connect(self.presenter.handle_export_table) self.exportSettingsWidget.fitAllButton.clicked.connect(self.presenter.fit_all_regions) self.spectrum_widget.roi_clicked.connect(self.presenter.handle_roi_clicked) self.spectrum_widget.roi_changing.connect(self.presenter.handle_notify_roi_moved) self.spectrum_widget.roiColorChangeRequested.connect(self.presenter.change_roi_colour) self.spectrum_right_click_menu = self.spectrum.spectrum_viewbox.menu self.units_menu = self.spectrum_right_click_menu.addMenu("Units") self.tof_mode_select_group = QActionGroup(self) for mode in allowed_modes.keys(): action = QAction(mode, self.tof_mode_select_group) action.setCheckable(True) action.setObjectName(mode) self.units_menu.addAction(action) action.triggered.connect(partial(self.presenter.handle_tof_unit_change_via_menu, mode)) if mode == "Wavelength": action.setChecked(True) if self.presenter.model.tof_data.size == 0: self.tof_mode_select_group.setEnabled(False) self.current_dataset_id: UUID | None = None self.sampleStackSelector.stack_selected_uuid.connect(self.presenter.handle_sample_change) self.sampleStackSelector.stack_selected_uuid.connect(self.presenter.handle_button_enabled) self.normaliseStackSelector.stack_selected_uuid.connect(self.presenter.handle_normalise_stack_change) self.normaliseStackSelector.stack_selected_uuid.connect(self.presenter.handle_button_enabled) self.normaliseCheckBox.stateChanged.connect(self.normaliseStackSelector.setEnabled) self.normaliseCheckBox.stateChanged.connect(self.presenter.handle_enable_normalised) self.normaliseCheckBox.stateChanged.connect(self.presenter.handle_button_enabled) self.normalise_ShutterCount_CheckBox.stateChanged.connect(self.presenter.set_shuttercount_error) self.normalise_ShutterCount_CheckBox.stateChanged.connect(self.presenter.handle_button_enabled) self.roi_form.exportTabs.currentChanged.connect(self.presenter.handle_export_tab_change) # ROI action buttons self.roi_form.addBtn.clicked.connect(self.set_new_roi) self.roi_form.removeBtn.clicked.connect(self.remove_roi) self.roi_form.exportButton.clicked.connect(self.presenter.handle_export_csv) self.roi_form.exportButtonRITS.clicked.connect(self.presenter.handle_rits_export) self.exportDataTableWidget = ExportDataTableWidget() self.exportLayout.addWidget(self.exportDataTableWidget) self.roi_form.table_view.clicked.connect(self.handle_table_click) self.roi_form.roi_properties_widget.roi_changed.connect(self.presenter.do_adjust_roi) self.spectrum_widget.roi_changed.connect(self.set_roi_properties) self.experimentSetupFormWidget = ExperimentSetupFormWidget(self.experimentSetupGroupBox) self.experimentSetupFormWidget.flight_path = 56.4 self.experimentSetupFormWidget.connect_value_changed(self.presenter.handle_experiment_setup_properties_change) self.roi_form.table_view.selection_changed.connect(self.set_roi_properties) self.roi_form.table_view.name_changed.connect(self.presenter.handle_roi_name_change) self.roi_form.table_view.visibility_changed.connect(self.on_visibility_change) self.formTabs.currentChanged.connect(self.handle_change_tab)
[docs] def show(self) -> None: super().show() self.activateWindow() self.initial_setup()
[docs] def cleanup(self) -> None: self.sampleStackSelector.unsubscribe_from_main_window() self.normaliseStackSelector.unsubscribe_from_main_window() self.main_window.spectrum_viewer = None
[docs] def initial_setup(self) -> None: QtWidgets.qApp.processEvents() self._configure_dropdown(self.sampleStackSelector) self._configure_dropdown(self.normaliseStackSelector) QtWidgets.qApp.processEvents() self.sampleStackSelector.select_eligible_stack() self.try_to_select_relevant_normalise_stack("Flat") self.presenter.handle_tof_unit_change() self.set_roi_properties() self.presenter.initial_sample_change = False self.presenter.initial_roi_calc() self.presenter.setup_fitting_model()
[docs] def handle_change_tab(self, tab_index: int): self.imageTabs.setCurrentIndex(tab_index) self.presenter.update_unit_labels_and_menus() LOG.debug("Tab changed: index=%d", tab_index)
[docs] def sync_unit_menus(self, unit_name: str) -> None: """Sync the checked unit in both the image and fitting tab unit menus.""" for group in (self.tof_mode_select_group, self.fittingDisplayWidget.tof_mode_select_group): for action in group.actions(): action.setChecked(action.text() == unit_name)
[docs] 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.set_roi_visibility_flags(ROI_RITS, visible=False) if self.table_view.row_count() == 0: self.disable_roi_properties() else: self.set_roi_properties() for row in range(self.table_view.row_count()): roi_name = self.table_view.get_roi_name_by_row(row) roi_visible = self.table_view.get_roi_visibility_by_row(row) self.set_roi_visibility_flags(roi_name, visible=roi_visible) if roi_visible: self.presenter.redraw_spectrum(roi_name) elif self.presenter.export_mode == ExportMode.IMAGE_MODE: for row in range(self.table_view.row_count()): roi_name = self.table_view.get_roi_name_by_row(row) self.set_roi_visibility_flags(roi_name, visible=False) self.set_roi_visibility_flags(ROI_RITS, visible=True) self.presenter.redraw_spectrum(ROI_RITS) self.set_roi_properties()
[docs] def get_fitting_region(self) -> FittingRegion: return self.fittingDisplayWidget.get_selected_fit_region()
[docs] def set_fitting_region(self, region: tuple[float, float]) -> None: self.fittingDisplayWidget.set_selected_fit_region(region)
def _configure_dropdown(self, selector: DatasetSelectorWidgetView) -> None: selector.presenter.show_stacks = True selector.subscribe_to_main_window(self.main_window)
[docs] def try_to_select_relevant_normalise_stack(self, name: str) -> None: self.normaliseStackSelector.try_to_select_relevant_stack(name)
[docs] def get_normalise_stack(self) -> UUID | None: return self.normaliseStackSelector.current()
[docs] def get_csv_filename(self) -> Path | None: path = QFileDialog.getSaveFileName(self, "Save CSV file", "", "CSV file (*.csv)")[0] if path: return Path(path) else: return None
[docs] def update_fitting_plot(self, roi_name: str, spectrum_data: np.ndarray) -> None: """Updates the spectrum plot in the Fitting Window with a yellow line.""" self.fittingSpectrumPlot.spectrum.clear() if spectrum_data is not None and len(spectrum_data) > 0: LOG.info("Fitting plot updated: ROI=%s, points=%d", roi_name, len(spectrum_data)) yellow_pen = mkPen(color=(255, 255, 0), width=2) self.fittingSpectrumPlot.spectrum.plot(self.presenter.model.tof_data, spectrum_data, pen=yellow_pen, name=roi_name)
[docs] def get_rits_export_directory(self) -> Path | None: """ Get the path to save the RITS file too """ path = QFileDialog.getExistingDirectory(self, "Select Directory", "", QFileDialog.ShowDirsOnly) if path: return Path(path) else: return None
[docs] def get_rits_export_filename(self) -> Path | None: """ Get the path to save the RITS file too """ path = QFileDialog.getSaveFileName(self, "Save DAT file", "", "DAT file (*.dat)")[0] if path: return Path(path) else: return None
[docs] def update_export_table(self, roi_name: str, params: dict[str, float], status: str = "Ready") -> None: self.exportDataTableWidget.update_roi_data(roi_name, params, status)
[docs] def set_image(self, image_data: np.ndarray, autoLevels: bool = True) -> None: self.spectrum_widget.image.setImage(image_data, autoLevels=autoLevels)
[docs] def set_spectrum(self, name: str, spectrum_data: np.ndarray) -> None: """ Try to set the spectrum data for a given ROI assuming the roi may not exist in the spectrum widget yet depending on when method is called """ self.spectrum_widget.spectrum_data_dict[name] = spectrum_data self.show_visible_spectrums()
[docs] def clear(self) -> None: self.spectrum_widget.spectrum_data_dict = {} self.spectrum_widget.image.setImage(np.zeros((1, 1))) self.spectrum_widget.spectrum.clearPlots()
[docs] def auto_range_image(self) -> None: self.spectrum_widget.image.vb.autoRange()
[docs] def set_normalise_error(self, norm_issue: str) -> None: self.normalise_error_issue = norm_issue self.display_normalise_error()
[docs] def display_normalise_error(self) -> None: if self.normalise_error_issue and self.normalisation_enabled(): self.normaliseErrorIcon.setPixmap(self.normalise_error_icon_pixmap) self.normaliseErrorIcon.setToolTip(self.normalise_error_issue) else: self.normaliseErrorIcon.setPixmap(QPixmap()) self.normaliseErrorIcon.setToolTip("")
[docs] def normalisation_enabled(self) -> bool: return self.normaliseCheckBox.isChecked()
[docs] def set_shuttercount_error(self, shuttercount_issue: str) -> None: self.shuttercount_error_issue = shuttercount_issue self.display_shuttercount_error()
[docs] def handle_shuttercount_change(self) -> None: self.presenter.set_shuttercount_error(self.normalise_ShutterCount_CheckBox.isChecked()) self.normalise_ShutterCount_CheckBox.setEnabled(self.shuttercount_error_issue == "")
[docs] def display_shuttercount_error(self) -> None: if (self.shuttercount_error_issue and self.normalisation_enabled() and self.shuttercount_norm_enabled() and self.normalise_error_issue == ""): self.shuttercountErrorIcon.setPixmap(self.normalise_error_icon_pixmap) self.shuttercountErrorIcon.setToolTip(self.shuttercount_error_issue) else: self.shuttercountErrorIcon.setPixmap(QPixmap()) self.shuttercountErrorIcon.setToolTip("")
[docs] def update_roi_dropdown(self) -> None: """ Updates the ROI dropdown menu with the available ROIs. """ roi_names = self.presenter.get_roi_names() self.roiSelectionWidget.update_roi_list(roi_names) self.exportSettingsWidget.set_roi_names(roi_names) LOG.debug("ROI dropdown updated in view")
[docs] def handle_fitting_roi_changed(self) -> None: self.show_visible_spectrums() self.presenter.update_roi_on_fitting_thumbnail()
[docs] def shuttercount_norm_enabled(self) -> bool: return self.normalise_ShutterCount_CheckBox.isChecked()
[docs] def set_new_roi(self) -> None: """ Set a new ROI on the image """ self.presenter.do_add_roi() self.roi_form.roi_properties_widget.enable_roi_spinboxes(True) self.set_roi_properties() LOG.debug("New ROI creation triggered by user")
[docs] def handle_table_click(self, index: QModelIndex) -> None: if index.isValid() and index.column() == 1: roi_name = self.table_view.get_roi_name_by_row(index.row()) self.set_spectum_roi_color(roi_name)
[docs] def set_spectum_roi_color(self, roi_name: str) -> None: spectrum_roi = self.spectrum_widget.roi_dict[roi_name] spectrum_roi.change_color_action.trigger()
[docs] def set_roi_visibility_flags(self, roi_name: str, visible: bool) -> None: """ Set the visibility for the selected ROI and update the spectrum to reflect the change. A check is made on the spectrum to see if it exists as it may not have been created yet. @param visible: Whether the ROI is visible. """ self.spectrum_widget.set_roi_visibility_flags(roi_name, visible=visible) self.show_visible_spectrums()
[docs] def show_visible_spectrums(self) -> None: self.spectrum_widget.spectrum.clearPlots() for roi_name, spectrum_data in self.spectrum_widget.spectrum_data_dict.items(): if roi_name not in self.spectrum_widget.roi_dict: continue if not self.spectrum_widget.roi_dict[roi_name].isVisible(): continue self.spectrum_widget.spectrum.plot(self.presenter.model.tof_data, spectrum_data, name=roi_name, pen=self.spectrum_widget.roi_dict[roi_name].colour) if current_roi_name := self.roiSelectionWidget.current_roi_name: self.fittingDisplayWidget.update_plot(self.presenter.model.tof_data, self.presenter.fitting_spectrum, current_roi_name)
[docs] def add_roi_table_row(self, name: str, colour: tuple[int, int, int, int]) -> None: """ Add a new row to the ROI table @param name: The name of the ROI @param colour: The colour of the ROI """ self.table_view.add_row(name, colour, self.presenter.get_roi_names()) self.roi_form.removeBtn.setEnabled(True)
[docs] def remove_roi(self) -> None: """ Clear the selected ROI in the table view """ roi_name = self.table_view.get_roi_name_by_row(self.table_view.selected_row) LOG.debug("ROI removed by user: %s", roi_name) self.table_view.remove_row(self.table_view.selected_row) self.presenter.do_remove_roi(roi_name) self.spectrum_widget.spectrum_data_dict.pop(roi_name) self.set_spectrum(roi_name, np.empty(0)) if self.table_view.roi_table_model.rowCount() == 0: self.roi_form.removeBtn.setEnabled(False) self.disable_roi_properties() else: self.set_roi_properties()
@property def transmission_error_mode(self) -> str: return self.roi_form.transmission_error_mode_combobox.currentText() @property def image_output_mode(self) -> str: return self.roi_form.image_output_mode @property def bin_size(self) -> int: return self.roi_form.bin_size_spinBox.value() @property def bin_step(self) -> int: return self.roi_form.bin_step_spinBox.value() @property def tof_units_mode(self) -> str: return self.tof_mode_select_group.checkedAction().text() @tof_units_mode.setter def tof_units_mode(self, value: str) -> None: for action in self.tof_mode_select_group.actions(): if action.text() == value: action.setChecked(True) break @property def table_view(self) -> ROITableWidget: return self.roi_form.table_view
[docs] def set_roi_properties(self) -> None: if self.presenter.export_mode == ExportMode.IMAGE_MODE: roi_name = ROI_RITS else: roi_name = self.table_view.get_roi_name_by_row(self.table_view.selected_row) if roi_name not in self.presenter.view.spectrum_widget.roi_dict: return current_roi = self.presenter.view.spectrum_widget.get_roi(roi_name) self.roi_form.roi_properties_widget.set_roi_name(roi_name) self.roi_form.roi_properties_widget.set_roi_values(current_roi) self.roi_form.roi_properties_widget.enable_roi_spinboxes(True)
[docs] def disable_roi_properties(self) -> None: self.roi_form.roi_properties_widget.set_roi_name("None selected") self.roi_form.roi_properties_widget.enable_roi_spinboxes(False)
[docs] def setup_roi_properties_spinboxes(self) -> None: assert self.spectrum_widget.image.image_data is not None self.roi_form.roi_properties_widget.set_roi_limits(self.spectrum_widget.image.image_data.shape)