diff --git a/docs/release_notes/next/feature-2421-live_viewer_spectrum_cache b/docs/release_notes/next/feature-2421-live_viewer_spectrum_cache new file mode 100644 index 00000000000..5b8d73ccc91 --- /dev/null +++ b/docs/release_notes/next/feature-2421-live_viewer_spectrum_cache @@ -0,0 +1 @@ +#2421: The Spectrum can be seen in the Live Viewer. An ImageCache is used to improve performance of loading images. \ No newline at end of file diff --git a/mantidimaging/eyes_tests/live_viewer_window_test.py b/mantidimaging/eyes_tests/live_viewer_window_test.py index 81a21eaf0a4..c1e1f46b3a7 100644 --- a/mantidimaging/eyes_tests/live_viewer_window_test.py +++ b/mantidimaging/eyes_tests/live_viewer_window_test.py @@ -56,7 +56,7 @@ def test_live_view_opens_without_data(self, _mock_time, _mock_image_watcher): self.imaging.show_live_viewer(self.live_directory) self.check_target(widget=self.imaging.live_viewer_list[-1]) - @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image') + @mock.patch('mantidimaging.gui.windows.live_viewer.model.load_image_from_path') @mock.patch('mantidimaging.gui.windows.live_viewer.model.ImageWatcher') @mock.patch("time.time", return_value=4000.0) def test_live_view_opens_with_data(self, _mock_time, _mock_image_watcher, mock_load_image): @@ -67,7 +67,7 @@ def test_live_view_opens_with_data(self, _mock_time, _mock_image_watcher, mock_l self.imaging.live_viewer_list[-1].presenter.model._handle_image_changed_in_list(image_list) self.check_target(widget=self.imaging.live_viewer_list[-1]) - @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image') + @mock.patch('mantidimaging.gui.windows.live_viewer.model.load_image_from_path') @mock.patch('mantidimaging.gui.windows.live_viewer.model.ImageWatcher') @mock.patch("time.time", return_value=4000.0) def test_live_view_opens_with_bad_data(self, _mock_time, _mock_image_watcher, mock_load_image): @@ -78,7 +78,7 @@ def test_live_view_opens_with_bad_data(self, _mock_time, _mock_image_watcher, mo self.imaging.live_viewer_list[-1].presenter.model._handle_image_changed_in_list(image_list) self.check_target(widget=self.imaging.live_viewer_list[-1]) - @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image') + @mock.patch('mantidimaging.gui.windows.live_viewer.model.load_image_from_path') @mock.patch('mantidimaging.gui.windows.live_viewer.model.ImageWatcher') @mock.patch("time.time", return_value=4000.0) def test_rotate_operation_rotates_image(self, _mock_time, _mock_image_watcher, mock_load_image): diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index 3a02baec8ce..afadadf43c8 100644 --- a/mantidimaging/gui/windows/live_viewer/live_view_widget.py +++ b/mantidimaging/gui/windows/live_viewer/live_view_widget.py @@ -2,10 +2,15 @@ # SPDX - License - Identifier: GPL-3.0-or-later from __future__ import annotations from typing import TYPE_CHECKING -from pyqtgraph import GraphicsLayoutWidget +from PyQt5.QtCore import pyqtSignal +from pyqtgraph import GraphicsLayoutWidget, mkPen + +from mantidimaging.core.utility.close_enough_point import CloseEnoughPoint +from mantidimaging.core.utility.sensible_roi import SensibleROI from mantidimaging.gui.widgets.mi_mini_image_view.view import MIMiniImageView from mantidimaging.gui.widgets.zslider.zslider import ZSlider +from mantidimaging.gui.windows.spectrum_viewer.spectrum_widget import SpectrumROI if TYPE_CHECKING: import numpy as np @@ -18,6 +23,10 @@ class LiveViewWidget(GraphicsLayoutWidget): @param parent: The parent widget """ image: MIMiniImageView + image_shape: tuple = (-1, -1) + roi_changed = pyqtSignal() + roi_changed_start = pyqtSignal(int) + roi_object: SpectrumROI | None = None def __init__(self) -> None: super().__init__() @@ -38,6 +47,7 @@ def show_image(self, image: np.ndarray) -> None: Show the image in the image view. @param image: The image to show """ + self.image_shape = image.shape self.image.setImage(image) def handle_deleted(self) -> None: @@ -48,3 +58,34 @@ def handle_deleted(self) -> None: def show_error(self, message: str | None): self.image.show_message(message) + + def add_roi(self): + if self.image_shape == (-1, -1): + return + height, width = self.image_shape + roi = SensibleROI.from_list([0, 0, width, height]) + self.roi_object = SpectrumROI('roi', roi, rotatable=False, scaleSnap=True, translateSnap=True) + self.roi_object.colour = (255, 194, 10, 255) + self.roi_object.hoverPen = mkPen(self.roi_object.colour, width=3) + self.roi_object.roi.sigRegionChangeFinished.connect(self.roi_changed.emit) + self.roi_object.roi.sigRegionChangeStarted.connect(self.roi_changed_start.emit) + self.image.vb.addItem(self.roi_object.roi) + + def set_image_shape(self, shape: tuple) -> None: + self.image_shape = shape + + def get_roi(self) -> SensibleROI | None: + if not self.roi_object: + return None + roi = self.roi_object.roi + pos = CloseEnoughPoint(roi.pos()) + size = CloseEnoughPoint(roi.size()) + return SensibleROI.from_points(pos, size) + + def set_roi_visibility_flags(self, visible: bool) -> None: + if not self.roi_object: + return + handles = self.roi_object.getHandles() + for handle in handles: + handle.setVisible(visible) + self.roi_object.setVisible(visible) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index dd0c78671f2..fe66dca74ba 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -3,11 +3,19 @@ from __future__ import annotations import time +from operator import attrgetter from typing import TYPE_CHECKING from pathlib import Path from logging import getLogger + +import numpy as np from PyQt5.QtCore import QFileSystemWatcher, QObject, pyqtSignal, QTimer +from tifffile import tifffile +from astropy.io import fits + +from mantidimaging.core.utility.sensible_roi import SensibleROI + if TYPE_CHECKING: from os import stat_result from mantidimaging.gui.windows.live_viewer.view import LiveViewerWindowPresenter @@ -15,6 +23,59 @@ LOG = getLogger(__name__) +def load_image_from_path(image_path: Path) -> np.ndarray: + """ + Load a .Tif, .Tiff or .Fits file only if it exists + and returns as an ndarray + """ + if image_path.suffix.lower() in [".tif", ".tiff"]: + with tifffile.TiffFile(image_path) as tif: + image_data = tif.asarray() + elif image_path.suffix.lower() == ".fits": + with fits.open(image_path.__str__()) as fit: + image_data = fit[0].data + return image_data + + +class ImageCache: + """ + An ImageCache class to be used as a decorator on image read functions to store recent images in memory + """ + cache_dict: dict[Image_Data, np.ndarray] + max_cache_size: int | None = None + + def __init__(self, max_cache_size=None): + self.max_cache_size = max_cache_size + self.cache_dict = {} + + def _add_to_cache(self, image: Image_Data, image_array: np.ndarray) -> None: + if image not in self.cache_dict.keys(): + if self.max_cache_size is not None: + if self.max_cache_size <= len(self.cache_dict): + self._remove_oldest_image() + self.cache_dict[image] = image_array + + def _get_oldest_image(self) -> Image_Data: + time_ordered_cache = min(self.cache_dict.keys(), key=attrgetter('image_modified_time')) + return time_ordered_cache + + def _remove_oldest_image(self) -> None: + del self.cache_dict[self._get_oldest_image()] + + def load_image(self, image: Image_Data) -> np.ndarray: + if image in self.cache_dict.keys(): + return self.cache_dict[image] + else: + try: + image_array = load_image_from_path(image.image_path) + except ValueError as error: + message = f"{type(error).__name__} reading image: {image.image_path}: {error}" + LOG.error(message) + raise ValueError from error + self._add_to_cache(image, image_array) + return image_array + + class Image_Data: """ Image Data Class to store represent image data. @@ -32,6 +93,8 @@ class Image_Data: image_modified_time : float last modified time of image file """ + image_path: Path + image_name: str def __init__(self, image_path: Path): """ @@ -45,16 +108,12 @@ def __init__(self, image_path: Path): self.image_path = image_path self.image_name = image_path.name self._stat = image_path.stat() + self.image_modified_time = self._stat.st_mtime @property def stat(self) -> stat_result: return self._stat - @property - def image_modified_time(self) -> float: - """Return the image modified time""" - return self._stat.st_mtime - @property def image_modified_time_stamp(self) -> str: """Return the image modified time as a string""" @@ -103,6 +162,11 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self._dataset_path: Path | None = None self.image_watcher: ImageWatcher | None = None self.images: list[Image_Data] = [] + self.mean: np.ndarray = np.empty(0) + self.mean_paths: set[Path] = set() + self.roi: SensibleROI | None = None + self.image_cache = ImageCache(max_cache_size=100) + self.calc_mean_all_chunks_thread = None @property def path(self) -> Path | None: @@ -125,9 +189,11 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None: :param image_files: list of image files """ self.images = image_files + # if dask_image_stack.image_list: + # self.image_stack = dask_image_stack self.presenter.update_image_list(image_files) - def handle_image_modified(self, image_path: Path): + def handle_image_modified(self, image_path: Path) -> None: self.presenter.update_image_modified(image_path) def close(self) -> None: @@ -137,6 +203,48 @@ def close(self) -> None: self.image_watcher = None self.presenter = None # type: ignore # Model instance to be destroyed -type can be inconsistent + def add_mean(self, image_data_obj: Image_Data, image_array: np.ndarray | None) -> None: + if image_array is None: + mean_to_add = np.nan + elif self.roi is not None: + left, top, right, bottom = self.roi + mean_to_add = np.mean(image_array[top:bottom, left:right]) + else: + mean_to_add = np.mean(image_array) + self.mean_paths.add(image_data_obj.image_path) + self.mean = np.append(self.mean, mean_to_add) + + def clear_mean_partial(self) -> None: + self.mean_paths.clear() + self.mean = np.full(len(self.images), np.nan) + + def clear_mean(self) -> None: + self.mean_paths.clear() + self.mean = np.delete(self.mean, np.arange(self.mean.size)) + + def calc_mean_fully(self) -> None: + if self.images is not None: + for image in self.images: + self.add_mean(image, self.image_cache.load_image(image)) + + def calc_mean_chunk(self, chunk_size: int = 10000) -> None: + if self.images is not None: + nanInds = np.argwhere(np.isnan(self.mean)) + if self.roi: + left, top, right, bottom = self.roi + else: + left, top, right, bottom = (0, 0, -1, -1) + if nanInds.size > 0: + for ind in nanInds[-1:-chunk_size:-1]: + buffer_data = self.image_cache.load_image(self.images[ind[0]]) + if buffer_data is not None: + buffer_mean = np.mean(buffer_data[top:bottom, left:right]) + np.put(self.mean, ind, buffer_mean) + + def calc_mean_all_chunks(self, chunk_size: int = 10000) -> None: + while np.isnan(self.mean).any(): + self.calc_mean_chunk(chunk_size) + class ImageWatcher(QObject): """ @@ -161,6 +269,7 @@ class ImageWatcher(QObject): Sort the images by modified time. """ image_changed = pyqtSignal(list) # Signal emitted when an image is added or removed + update_spectrum = pyqtSignal(np.ndarray) # Signal emitted to update the Live Viewer Spectrum recent_image_changed = pyqtSignal(Path) def __init__(self, directory: Path): @@ -266,7 +375,6 @@ def _handle_directory_change(self) -> None: if len(images) > 0: break - images = self.sort_images_by_modified_time(images) self.update_recent_watcher(images[-1:]) self.image_changed.emit(images) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index b26391c58b9..8a00ab2a6f2 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -9,8 +9,6 @@ import numpy as np from imagecodecs._deflate import DeflateError -from tifffile import tifffile, TiffFileError -from astropy.io import fits from mantidimaging.gui.mvp_base import BasePresenter from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data @@ -34,6 +32,7 @@ class LiveViewerWindowPresenter(BasePresenter): view: LiveViewerWindowView model: LiveViewerWindowModel op_func: Callable + roi_moving: bool = False def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView): super().__init__(view) @@ -72,6 +71,18 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: self.handle_deleted() self.view.set_load_as_dataset_enabled(False) else: + if not self.view.live_viewer.roi_object and self.view.spectrum_action.isChecked(): + self.view.live_viewer.add_roi() + self.model.roi = self.view.live_viewer.get_roi() + self.model.images = images_list + if images_list[-1].image_path not in self.model.mean_paths: + try: + image_data = self.model.image_cache.load_image(images_list[-1]) + self.model.add_mean(images_list[-1], image_data) + except (OSError, KeyError, ValueError, DeflateError) as error: + message = f"{type(error).__name__} reading image: {images_list[-1].image_path}: {error}" + logger.error(message) + self.update_spectrum(self.model.mean) self.view.set_image_range((0, len(images_list) - 1)) self.view.set_image_index(len(images_list) - 1) self.view.set_load_as_dataset_enabled(True) @@ -80,19 +91,21 @@ def select_image(self, index: int) -> None: if not self.model.images: return self.selected_image = self.model.images[index] + if not self.selected_image: + return image_timestamp = self.selected_image.image_modified_time_stamp self.view.label_active_filename.setText(f"{self.selected_image.image_name} - {image_timestamp}") - self.display_image(self.selected_image.image_path) + self.display_image(self.selected_image) - def display_image(self, image_path: Path) -> None: + def display_image(self, image_data_obj: Image_Data) -> None: """ Display image in the view after validating contents """ try: - image_data = self.load_image(image_path) - except (OSError, KeyError, ValueError, TiffFileError, DeflateError) as error: - message = f"{type(error).__name__} reading image: {image_path}: {error}" + image_data = self.model.image_cache.load_image(image_data_obj) + except (OSError, KeyError, ValueError, DeflateError) as error: + message = f"{type(error).__name__} reading image: {image_data_obj.image_path}: {error}" logger.error(message) self.view.remove_image() self.view.live_viewer.show_error(message) @@ -100,41 +113,26 @@ def display_image(self, image_path: Path) -> None: image_data = self.perform_operations(image_data) if image_data.size == 0: message = "reading image: {image_path}: Image has zero size" - logger.error("reading image: %s: Image has zero size", image_path) + logger.error("reading image: %s: Image has zero size", image_data_obj.image_path) self.view.remove_image() self.view.live_viewer.show_error(message) return - self.view.show_most_recent_image(image_data) self.view.live_viewer.show_error(None) - @staticmethod - def load_image(image_path: Path) -> np.ndarray: - """ - Load a .Tif, .Tiff or .Fits file only if it exists - and returns as an ndarray - """ - if image_path.suffix.lower() in [".tif", ".tiff"]: - with tifffile.TiffFile(image_path) as tif: - image_data = tif.asarray() - elif image_path.suffix.lower() == ".fits": - with fits.open(image_path.__str__()) as fit: - image_data = fit[0].data - return image_data - def update_image_modified(self, image_path: Path) -> None: """ Update the displayed image when the file is modified """ if self.selected_image and image_path == self.selected_image.image_path: - self.display_image(image_path) + self.display_image(self.selected_image) def update_image_operation(self) -> None: """ Reload the current image if an operation has been performed on the current image """ if self.selected_image is not None: - self.display_image(self.selected_image.image_path) + self.display_image(self.selected_image) def convert_image_to_imagestack(self, image_data) -> ImageStack: """ @@ -160,3 +158,27 @@ def load_as_dataset(self) -> None: if self.model.images: image_dir = self.model.images[0].image_path.parent self.main_window.show_image_load_dialog_with_path(str(image_dir)) + + def update_spectrum(self, spec_data: list | np.ndarray): + self.view.spectrum.clearPlots() + self.view.spectrum.plot(spec_data) + + def handle_roi_moved(self, force_new_spectrums: bool = False): + roi = self.view.live_viewer.get_roi() + self.model.roi = roi + self.model.clear_mean_partial() + self.run_mean_chunk_calc() + self.roi_moving = False + + def run_mean_chunk_calc(self): + self.model.calc_mean_all_chunks() + self.update_spectrum_with_mean() + + def handle_roi_moved_start(self): + self.roi_moving = True + self.model.clear_mean_partial() + self.update_spectrum(self.model.mean) + + def update_spectrum_with_mean(self): + self.view.spectrum.clearPlots() + self.view.spectrum.plot(self.model.mean) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 0fbad537e2d..486838ce0a4 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -3,13 +3,17 @@ from __future__ import annotations import os +import random import time +import unittest + from pathlib import Path from unittest import mock +import numpy as np from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal -from mantidimaging.gui.windows.live_viewer.model import ImageWatcher +from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, ImageCache, Image_Data from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase @@ -159,3 +163,56 @@ def test_WHEN_sub_directory_change_THEN_images_emitted(self, _mock_time): emitted_images = self._get_recent_emitted_files() self._file_list_count_equal(emitted_images, file_list2) + + +class ImageCacheTest(unittest.TestCase): + + def setUp(self) -> None: + super().setUp() + self.image_data_list = [] + self.image_array_mock_list = [np.random.default_rng().random(5)] * 5 + for i in range(5): + self.image_data_list.append(mock.create_autospec(Image_Data)) + self.image_data_list[i] = mock.create_autospec(Image_Data) + self.image_data_list[i].image_path = Path(f"abc_{i}.tif") + self.image_data_list[i].image_modified_time = random.uniform(1000, 10000) + self.image_cache = ImageCache() + + def test_WHEN_image_added_to_cache_THEN_image_is_in_cache(self): + image_data = self.image_data_list[0] + image_array_mock = self.image_array_mock_list[0] + self.image_cache._add_to_cache(image_data, image_array_mock) + np.testing.assert_array_equal(self.image_cache.cache_dict[image_data], image_array_mock) + self.assertEqual( + list(self.image_cache.cache_dict.keys())[0].image_modified_time, image_data.image_modified_time) + + def test_WHEN_remove_oldest_image_got_THEN_oldest_image_removed(self): + for i in range(len(self.image_data_list)): + self.image_cache._add_to_cache(self.image_data_list[i], self.image_array_mock_list[i]) + min_index = np.argmin([image.image_modified_time for image in self.image_data_list]) + self.assertEqual(self.image_cache._get_oldest_image(), self.image_data_list[min_index]) + self.image_cache._remove_oldest_image() + self.assertNotIn(self.image_data_list[min_index], self.image_cache.cache_dict) + + @mock.patch("mantidimaging.gui.windows.live_viewer.model.load_image_from_path") + def test_WHEN_image_not_in_cache_when_loaded_THEN_image_added_to_cache(self, load_image_from_path_mock): + self.image_cache = ImageCache() + self.image_cache.load_image(self.image_data_list[0]) + load_image_from_path_mock.assert_called_once() + self.assertIn(self.image_data_list[0], self.image_cache.cache_dict) + + @mock.patch("mantidimaging.gui.windows.live_viewer.model.load_image_from_path") + def test_WHEN_image_in_cache_when_loaded_then_image_taken_from_cache(self, load_image_from_path_mock): + self.image_cache = ImageCache() + self.image_cache._add_to_cache(self.image_data_list[0], self.image_array_mock_list[0]) + image_array = self.image_cache.load_image(self.image_data_list[0]) + load_image_from_path_mock.assert_not_called() + np.testing.assert_array_equal(image_array, self.image_cache.cache_dict[self.image_data_list[0]]) + + def test_WHEN_cache_full_THEN_loading_image_removes_oldest_image(self): + self.image_cache = ImageCache(max_cache_size=2) + self.image_cache._remove_oldest_image = mock.Mock() + self.image_cache._add_to_cache(self.image_data_list[0], self.image_array_mock_list[0]) + self.image_cache._add_to_cache(self.image_data_list[1], self.image_array_mock_list[1]) + self.image_cache._add_to_cache(self.image_data_list[2], self.image_array_mock_list[2]) + self.image_cache._remove_oldest_image.assert_called_once() diff --git a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py index 63f6b6a4d18..6fc17edffad 100644 --- a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py @@ -45,6 +45,14 @@ def test_load_as_dataset_empty_dir(self): ([mock.Mock()], True), ]) def test_load_as_dataset_enabled_when_images(self, image_list, action_enabled): + self.model.set_roi = mock.Mock() + self.model.mean_paths = set() + self.model.mean = [] + self.model.image_cache = mock.Mock() + self.model.add_mean = mock.Mock() + self.presenter.roi_moving = True + self.view.live_viewer = mock.Mock() + self.view.spectrum = mock.Mock() with mock.patch.object(self.presenter, "handle_deleted"): self.presenter.update_image_list(image_list) diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index e9aa5836a7e..fa217568353 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -4,8 +4,8 @@ from pathlib import Path from typing import TYPE_CHECKING -from PyQt5.QtCore import QSignalBlocker -from PyQt5.QtWidgets import QVBoxLayout +from PyQt5.QtCore import QSignalBlocker, Qt +from PyQt5.QtWidgets import QVBoxLayout, QSplitter from PyQt5.Qt import QAction, QActionGroup from mantidimaging.gui.mvp_base import BaseMainWindowView @@ -14,6 +14,8 @@ import numpy as np +from ..spectrum_viewer.spectrum_widget import SpectrumPlotWidget + if TYPE_CHECKING: from mantidimaging.gui.windows.main import MainWindowView # noqa:F401 # pragma: no cover @@ -33,9 +35,20 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: self.setWindowTitle(f"Mantid Imaging - Live Viewer - {str(self.path)}") self.presenter = LiveViewerWindowPresenter(self, main_window) self.live_viewer = LiveViewWidget() - self.imageLayout.addWidget(self.live_viewer) + self.splitter = QSplitter(Qt.Vertical) + self.imageLayout.addWidget(self.splitter) self.live_viewer.z_slider.valueChanged.connect(self.presenter.select_image) + self.spectrum_plot_widget = SpectrumPlotWidget() + self.spectrum = self.spectrum_plot_widget.spectrum + self.live_viewer.roi_changed.connect(self.presenter.handle_roi_moved) + self.live_viewer.roi_changed_start.connect(self.presenter.handle_roi_moved_start) + + self.splitter.addWidget(self.live_viewer) + self.splitter.addWidget(self.spectrum_plot_widget) + widget_height = self.frameGeometry().height() + self.splitter.setSizes([widget_height, 0]) + self.filter_params: dict[str, dict] = {} self.right_click_menu = self.live_viewer.image.vb.menu operations_menu = self.right_click_menu.addMenu("Operations") @@ -54,6 +67,12 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: self.load_as_dataset_action = self.right_click_menu.addAction("Load as dataset") self.load_as_dataset_action.triggered.connect(self.presenter.load_as_dataset) + self.spectrum_action = QAction("Calculate Spectrum", self) + self.spectrum_action.setCheckable(True) + operations_menu.addAction(self.spectrum_action) + self.spectrum_action.triggered.connect(self.set_spectrum_visibility) + self.live_viewer.set_roi_visibility_flags(False) + def show(self) -> None: """Show the window""" super().show() @@ -106,3 +125,17 @@ def set_image_rotation_angle(self) -> None: def set_load_as_dataset_enabled(self, enabled: bool): self.load_as_dataset_action.setEnabled(enabled) + + def set_spectrum_visibility(self): + widget_height = self.frameGeometry().height() + if self.spectrum_action.isChecked(): + if not self.live_viewer.roi_object: + self.live_viewer.add_roi() + self.live_viewer.set_roi_visibility_flags(True) + self.splitter.setSizes([int(0.7 * widget_height), int(0.3 * widget_height)]) + self.presenter.model.roi = self.live_viewer.get_roi() + self.presenter.model.calc_mean_fully() + self.presenter.update_spectrum(self.presenter.model.mean) + else: + self.live_viewer.set_roi_visibility_flags(False) + self.splitter.setSizes([widget_height, 0])