From 974dff8cf412fe83fd7d69e8d0be735e2eb9ce8e Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 7 Aug 2024 13:01:03 +0100 Subject: [PATCH 01/29] Live Viewer Spectrum added --- conda/meta.yaml | 2 + .../next/dev-2311-dask-live-viewer | 1 + .../eyes_tests/live_viewer_window_test.py | 2 +- .../windows/live_viewer/live_view_widget.py | 49 ++++++++++++++++++- .../gui/windows/live_viewer/model.py | 1 - mantidimaging/gui/windows/live_viewer/view.py | 37 ++++++++++++-- 6 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 docs/release_notes/next/dev-2311-dask-live-viewer diff --git a/conda/meta.yaml b/conda/meta.yaml index f129f9df21a..3a9668a813e 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -41,6 +41,8 @@ requirements: - qt-material=2.14 - darkdetect=0.8.0 - qt-gtk-platformtheme # [linux] + - dask + - dask-image build: diff --git a/docs/release_notes/next/dev-2311-dask-live-viewer b/docs/release_notes/next/dev-2311-dask-live-viewer new file mode 100644 index 00000000000..807bbad4db2 --- /dev/null +++ b/docs/release_notes/next/dev-2311-dask-live-viewer @@ -0,0 +1 @@ +#2311: The Live Viewer now uses Dask to load in images and create a delayed datastack for operations \ 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..90460062b7d 100644 --- a/mantidimaging/eyes_tests/live_viewer_window_test.py +++ b/mantidimaging/eyes_tests/live_viewer_window_test.py @@ -88,4 +88,4 @@ def test_rotate_operation_rotates_image(self, _mock_time, _mock_image_watcher, m self.imaging.show_live_viewer(self.live_directory) self.imaging.live_viewer_list[-1].presenter.model._handle_image_changed_in_list(image_list) self.imaging.live_viewer_list[-1].rotate_angles_group.actions()[1].trigger() - self.check_target(widget=self.imaging.live_viewer_list[-1]) + self.check_target(widget=self.imaging.live_viewer_list[-1]) \ No newline at end of file diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index 3a02baec8ce..c7baafd1eec 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_object: SpectrumROI | None = None + sensible_roi: SensibleROI def __init__(self) -> None: super().__init__() @@ -48,3 +57,41 @@ 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.image.vb.addItem(self.roi_object.roi) + + def set_image_shape(self, shape: tuple) -> None: + self.image_shape = shape + + def get_roi(self) -> SensibleROI: + if not self.roi_object: + return SensibleROI() + roi = self.roi_object.roi + pos = CloseEnoughPoint(roi.pos()) + size = CloseEnoughPoint(roi.size()) + return SensibleROI.from_points(pos, size) + + def set_roi_alpha(self, alpha: int) -> None: + if not self.roi_object: + return + self.roi_object.colour = self.roi_object.colour[:3] + (alpha, ) + self.roi_object.setPen(self.roi_object.colour) + self.roi_object.hoverPen = mkPen(self.roi_object.colour, width=3) + self.set_roi_visibility_flags(bool(alpha)) + + 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..2414d5372a7 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -266,7 +266,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/view.py b/mantidimaging/gui/windows/live_viewer/view.py index e9aa5836a7e..94f44761acd 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,19 @@ 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.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 +66,14 @@ 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.presenter.model.image_stack.create_delayed_array = False + self.live_viewer.set_roi_alpha(self.spectrum_action.isChecked() * 255) + self.live_viewer.set_roi_visibility_flags(False) + def show(self) -> None: """Show the window""" super().show() @@ -106,3 +126,14 @@ 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_alpha(255) + self.splitter.setSizes([int(0.7 * widget_height), int(0.3 * widget_height)]) + else: + self.live_viewer.set_roi_alpha(0) + self.splitter.setSizes([widget_height, 0]) From e3eefb3e26f5e20b1ac5e00dbc9b8ba82a0b1b47 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 26 Nov 2024 11:16:04 +0000 Subject: [PATCH 02/29] Images stored in LRUCache and new means append correctly (with debug prints) --- mantidimaging/gui/windows/live_viewer/presenter.py | 8 +++++++- mantidimaging/gui/windows/live_viewer/view.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index b26391c58b9..1d561f74b22 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -69,12 +69,19 @@ def handle_deleted(self) -> None: def update_image_list(self, images_list: list[Image_Data]) -> None: """Update the image in the view.""" if not images_list: + print("++++++++++++++++++++++++++++ presenter.update_image_list() 1 +++++++++++++++++++++++++++++++++") self.handle_deleted() + print("++++++++++++++++++++++++++++ presenter.update_image_list() 2 +++++++++++++++++++++++++++++++++") self.view.set_load_as_dataset_enabled(False) + print("++++++++++++++++++++++++++++ presenter.update_image_list() 3 +++++++++++++++++++++++++++++++++") else: + print("++++++++++++++++++++++++++++ presenter.update_image_list() 4 +++++++++++++++++++++++++++++++++") self.view.set_image_range((0, len(images_list) - 1)) + print("++++++++++++++++++++++++++++ presenter.update_image_list() 5 +++++++++++++++++++++++++++++++++") self.view.set_image_index(len(images_list) - 1) + print("++++++++++++++++++++++++++++ presenter.update_image_list() 6 +++++++++++++++++++++++++++++++++") self.view.set_load_as_dataset_enabled(True) + print("++++++++++++++++++++++++++++ presenter.update_image_list() 7 +++++++++++++++++++++++++++++++++") def select_image(self, index: int) -> None: if not self.model.images: @@ -104,7 +111,6 @@ def display_image(self, image_path: Path) -> None: 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) diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 94f44761acd..4b30b048566 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -103,8 +103,11 @@ def set_image_range(self, index_range: tuple[int, int]) -> None: def set_image_index(self, index: int) -> None: """Set the position on the z-slider, triggering valueChanged signal once""" with QSignalBlocker(self.live_viewer.z_slider): + print("++++++++++++++++++++++++++++ view.set_image_index() 1 +++++++++++++++++++++++++++++++++") self.live_viewer.z_slider.set_value(index) + print("++++++++++++++++++++++++++++ view.set_image_index() 2 +++++++++++++++++++++++++++++++++") self.live_viewer.z_slider.valueChanged.emit(index) + print("++++++++++++++++++++++++++++ view.set_image_index() 3 +++++++++++++++++++++++++++++++++") def closeEvent(self, e) -> None: """Close the window and remove it from the main window list""" From a9e5c164385ad74c122ffdb37e0e95a02ff37341 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 27 Nov 2024 11:47:19 +0000 Subject: [PATCH 03/29] Mean buffer loading and prints removed --- mantidimaging/gui/windows/live_viewer/presenter.py | 11 ++++------- mantidimaging/gui/windows/live_viewer/view.py | 3 --- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 1d561f74b22..04fae1ad6c5 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -69,19 +69,12 @@ def handle_deleted(self) -> None: def update_image_list(self, images_list: list[Image_Data]) -> None: """Update the image in the view.""" if not images_list: - print("++++++++++++++++++++++++++++ presenter.update_image_list() 1 +++++++++++++++++++++++++++++++++") self.handle_deleted() - print("++++++++++++++++++++++++++++ presenter.update_image_list() 2 +++++++++++++++++++++++++++++++++") self.view.set_load_as_dataset_enabled(False) - print("++++++++++++++++++++++++++++ presenter.update_image_list() 3 +++++++++++++++++++++++++++++++++") else: - print("++++++++++++++++++++++++++++ presenter.update_image_list() 4 +++++++++++++++++++++++++++++++++") self.view.set_image_range((0, len(images_list) - 1)) - print("++++++++++++++++++++++++++++ presenter.update_image_list() 5 +++++++++++++++++++++++++++++++++") self.view.set_image_index(len(images_list) - 1) - print("++++++++++++++++++++++++++++ presenter.update_image_list() 6 +++++++++++++++++++++++++++++++++") self.view.set_load_as_dataset_enabled(True) - print("++++++++++++++++++++++++++++ presenter.update_image_list() 7 +++++++++++++++++++++++++++++++++") def select_image(self, index: int) -> None: if not self.model.images: @@ -104,6 +97,10 @@ def display_image(self, image_path: Path) -> None: self.view.remove_image() self.view.live_viewer.show_error(message) return + self.view.live_viewer.set_image_shape(image_data.shape) + if not self.view.live_viewer.roi_object and self.view.spectrum_action.isChecked(): + self.view.live_viewer.add_roi() + self.model.image_stack.set_roi(self.view.live_viewer.get_roi()) image_data = self.perform_operations(image_data) if image_data.size == 0: message = "reading image: {image_path}: Image has zero size" diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 4b30b048566..94f44761acd 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -103,11 +103,8 @@ def set_image_range(self, index_range: tuple[int, int]) -> None: def set_image_index(self, index: int) -> None: """Set the position on the z-slider, triggering valueChanged signal once""" with QSignalBlocker(self.live_viewer.z_slider): - print("++++++++++++++++++++++++++++ view.set_image_index() 1 +++++++++++++++++++++++++++++++++") self.live_viewer.z_slider.set_value(index) - print("++++++++++++++++++++++++++++ view.set_image_index() 2 +++++++++++++++++++++++++++++++++") self.live_viewer.z_slider.valueChanged.emit(index) - print("++++++++++++++++++++++++++++ view.set_image_index() 3 +++++++++++++++++++++++++++++++++") def closeEvent(self, e) -> None: """Close the window and remove it from the main window list""" From ef20e6cc27bcd733265125774293fb01cb22a9fd Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 29 Nov 2024 16:32:19 +0000 Subject: [PATCH 04/29] mean calculated with incoming data and displayed, base ImageCache class created --- .../gui/windows/live_viewer/model.py | 127 +++++++++++++++++- .../gui/windows/live_viewer/presenter.py | 38 ++++-- mantidimaging/gui/windows/live_viewer/view.py | 3 +- 3 files changed, 154 insertions(+), 14 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 2414d5372a7..349d936c5e3 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -6,8 +6,12 @@ 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 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 +19,102 @@ LOG = getLogger(__name__) +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_list: list[Image_Data] + image_paths: set[str] = set() + mean: np.ndarray = np.array([]) + roi: SensibleROI | None = None + param_to_calc: list[str] = [] + max_cache_size: int = 100 + buffer_size: int = 10 + + def __init__(self, func): + self.func = func + + def __call__(self, *args, **kwargs): + #print(f"Arguments: {args}, Keyword Arguments: {kwargs}") + result = self.func(*args, **kwargs) + self.add_to_cache(args, result) + return result + + def add_to_cache(self, args, image_array: np.ndarray): + if args not in self.cache_dict.keys(): + self.cache_dict[args] = image_array + + def remove_from_cache(self, image: Image_Data): + if image.image_path in self.cache_dict.keys(): + del self.cache_dict[image.image_path] + + # def update_param_calculations(self) -> None: + # if 'mean' in self.param_to_calc: + # if len(self.mean) == len(self.image_list) - 1: + # self.add_last_mean() + # else: + # if self.roi: + # self.calc_mean_fully_roi() + # else: + # self.calc_mean_fully() + # + # def add_last_mean(self) -> None: + # if self.delayed_stack is not None: + # if self.roi: + # left, top, right, bottom = self.roi + # mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1, top:bottom, + # left:right]))[0].compute() + # else: + # mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1]))[0].compute() + # self.mean = np.append(self.mean, mean_to_add) + # self.calc_mean_buffer() + + # def calc_mean_fully(self) -> None: + # if self.delayed_stack is not None: + # self.mean = dask.array.mean(self.delayed_stack, axis=(1, 2)).compute() + # + # def calc_mean_fully_roi(self): + # if self.delayed_stack is not None and self.image_list: + # left, top, right, bottom = self.roi + # current_cache_size = len(self.) + # self.mean = np.full(len(self.image_list), np.nan) + # np.put(self.mean, range(-current_cache_size, 0), self.calc_mean_cached_images(left, top, right, bottom)) + # + # def calc_mean_cached_images(self, left, top, right, bottom): + # current_cache_size = self.get_computed_image.cache_info()[3] + # cache_stack = [ + # self.get_computed_image(index) + # for index in range(self.selected_index - current_cache_size + 1, self.selected_index + 1, 1) + # ] + # cache_stack_array = np.stack(cache_stack) + # cache_stack_mean = np.mean(cache_stack_array[:, top:bottom, left:right], axis=(1, 2)) + # return cache_stack_mean + # + # def calc_mean_buffer(self): + # nanInds = np.argwhere(np.isnan(self.mean)) + # left, top, right, bottom = self.roi + # if nanInds.size > 0: + # print(f"{self.mean=}") + # if nanInds.size < self.buffer_size: + # buffer_start = 0 + # else: + # buffer_start = nanInds.size - self.buffer_size + # dask_mean = dask.optimize( + # dask.array.mean(self.delayed_stack[buffer_start:nanInds.size, top:bottom, left:right], + # axis=(1, 2)))[0].compute() + # np.put(self.mean, range(buffer_start, nanInds.size), dask_mean) + + def set_roi(self, roi: SensibleROI): + self.roi = roi + + def delete_all_data(self): + pass + + def add_param_to_calc(self, param_name: str): + self.param_to_calc.append(param_name) + + class Image_Data: """ Image Data Class to store represent image data. @@ -102,7 +202,10 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self.presenter = presenter self._dataset_path: Path | None = None self.image_watcher: ImageWatcher | None = None - self.images: list[Image_Data] = [] + self._images: list[Image_Data] = [] + self.mean: np.array = np.array([]) + self.mean_dict: dict[Path, float] = {} + self.roi: SensibleROI | None = None @property def path(self) -> Path | None: @@ -116,6 +219,14 @@ def path(self, path: Path) -> None: self.image_watcher.recent_image_changed.connect(self.handle_image_modified) self.image_watcher._handle_notified_of_directry_change(str(path)) + @property + def images(self): + return self._images if self._images is not None else None + + @images.setter + def images(self, images): + self._images = images + def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None: """ Handle an image changed event. Update the image in the view. @@ -125,6 +236,8 @@ 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): @@ -137,6 +250,16 @@ 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.array) -> None: + if self.roi: + 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_dict[image_data_obj.image_path] = mean_to_add + self.mean = list(self.mean_dict.values()) + + class ImageWatcher(QObject): """ @@ -161,7 +284,9 @@ 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) + create_delayed_array: bool = False def __init__(self, directory: Path): """ diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 04fae1ad6c5..f00b7d3af8d 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -13,7 +13,7 @@ from astropy.io import fits from mantidimaging.gui.mvp_base import BasePresenter -from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data +from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data, ImageCache from mantidimaging.core.operations.loader import load_filter_packages from mantidimaging.core.data import ImageStack @@ -80,19 +80,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.load_image_from_path(image_data_obj.image_path) + 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,19 +102,22 @@ def display_image(self, image_path: Path) -> None: self.view.live_viewer.set_image_shape(image_data.shape) if not self.view.live_viewer.roi_object and self.view.spectrum_action.isChecked(): self.view.live_viewer.add_roi() - self.model.image_stack.set_roi(self.view.live_viewer.get_roi()) 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 + #if np.any(np.isnan(self.model.image_stack.mean)): + self.model.add_mean(image_data_obj, image_data) self.view.show_most_recent_image(image_data) + self.update_spectrum(self.model.mean) self.view.live_viewer.show_error(None) @staticmethod - def load_image(image_path: Path) -> np.ndarray: + @ImageCache + 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 @@ -130,14 +135,14 @@ 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: """ @@ -163,3 +168,14 @@ 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.image_stack.set_roi(roi) + # self.model.image_stack.calc_mean_fully_roi() + # self.update_spectrum(self.model.image_stack.mean) diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 94f44761acd..3e5219ad712 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -41,7 +41,7 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: 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.connect(self.presenter.handle_roi_moved) self.splitter.addWidget(self.live_viewer) self.splitter.addWidget(self.spectrum_plot_widget) @@ -70,7 +70,6 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: self.spectrum_action.setCheckable(True) operations_menu.addAction(self.spectrum_action) self.spectrum_action.triggered.connect(self.set_spectrum_visibility) - self.presenter.model.image_stack.create_delayed_array = False self.live_viewer.set_roi_alpha(self.spectrum_action.isChecked() * 255) self.live_viewer.set_roi_visibility_flags(False) From 9d40078c18ccd002ca474c7d1425e8454aa00b6f Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 2 Dec 2024 12:37:31 +0000 Subject: [PATCH 05/29] LV Model add and stores mean in dict, replots when ROI is moved --- .../gui/windows/live_viewer/model.py | 29 +++++++++++++++-- .../gui/windows/live_viewer/presenter.py | 32 +++++++------------ mantidimaging/gui/windows/live_viewer/view.py | 5 ++- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 349d936c5e3..bbadb1a284f 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -10,6 +10,10 @@ 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 import ExecutionProfiler from mantidimaging.core.utility.sensible_roi import SensibleROI if TYPE_CHECKING: @@ -105,9 +109,6 @@ def remove_from_cache(self, image: Image_Data): # axis=(1, 2)))[0].compute() # np.put(self.mean, range(buffer_start, nanInds.size), dask_mean) - def set_roi(self, roi: SensibleROI): - self.roi = roi - def delete_all_data(self): pass @@ -227,6 +228,9 @@ def images(self): def images(self, images): self._images = images + def set_roi(self, roi: SensibleROI): + self.roi = roi + def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None: """ Handle an image changed event. Update the image in the view. @@ -259,7 +263,26 @@ def add_mean(self, image_data_obj: Image_Data, image_array: np.array) -> None: self.mean_dict[image_data_obj.image_path] = mean_to_add self.mean = list(self.mean_dict.values()) + def clear_mean(self): + self.mean_dict.clear() + + def calc_mean_fully(self) -> None: + for image in self.images: + self.add_mean(image, self.load_image_from_path(image.image_path)) + @staticmethod + 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 ImageWatcher(QObject): """ diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index f00b7d3af8d..f4f26aa3505 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -68,10 +68,12 @@ def handle_deleted(self) -> None: def update_image_list(self, images_list: list[Image_Data]) -> None: """Update the image in the view.""" + # TODO: Might be a good idea to update and store the image list in the model so it can be cycled through if not images_list: self.handle_deleted() self.view.set_load_as_dataset_enabled(False) else: + self.model.images = images_list 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) @@ -92,7 +94,7 @@ def display_image(self, image_data_obj: Image_Data) -> None: Display image in the view after validating contents """ try: - image_data = self.load_image_from_path(image_data_obj.image_path) + image_data = self.model.load_image_from_path(image_data_obj.image_path) except (OSError, KeyError, ValueError, DeflateError) as error: message = f"{type(error).__name__} reading image: {image_data_obj.image_path}: {error}" logger.error(message) @@ -110,26 +112,12 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.live_viewer.show_error(message) return #if np.any(np.isnan(self.model.image_stack.mean)): + self.model.set_roi(self.view.live_viewer.get_roi()) self.model.add_mean(image_data_obj, image_data) self.view.show_most_recent_image(image_data) self.update_spectrum(self.model.mean) self.view.live_viewer.show_error(None) - @staticmethod - @ImageCache - 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 - def update_image_modified(self, image_path: Path) -> None: """ Update the displayed image when the file is modified @@ -174,8 +162,10 @@ 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.image_stack.set_roi(roi) - # self.model.image_stack.calc_mean_fully_roi() - # self.update_spectrum(self.model.image_stack.mean) + def handle_roi_moved(self, force_new_spectrums: bool = False): + # TODO: should we make all these functions go in the model? + roi = self.view.live_viewer.get_roi() + self.model.set_roi(roi) + self.model.clear_mean() + self.model.calc_mean_fully() + self.update_spectrum(self.model.mean) diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 3e5219ad712..37519c262c6 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -41,7 +41,7 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: 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.connect(self.presenter.handle_roi_moved) self.splitter.addWidget(self.live_viewer) self.splitter.addWidget(self.spectrum_plot_widget) @@ -133,6 +133,9 @@ def set_spectrum_visibility(self): self.live_viewer.add_roi() self.live_viewer.set_roi_alpha(255) self.splitter.setSizes([int(0.7 * widget_height), int(0.3 * widget_height)]) + self.presenter.model.set_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_alpha(0) self.splitter.setSizes([widget_height, 0]) From 70d72ca637afa7b461338ca0c1ef21847455a5ac Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 2 Dec 2024 13:17:52 +0000 Subject: [PATCH 06/29] ruff, mypy, and unit test fixes --- mantidimaging/eyes_tests/live_viewer_window_test.py | 2 +- mantidimaging/gui/windows/live_viewer/model.py | 7 ++++--- mantidimaging/gui/windows/live_viewer/presenter.py | 5 +---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mantidimaging/eyes_tests/live_viewer_window_test.py b/mantidimaging/eyes_tests/live_viewer_window_test.py index 90460062b7d..81a21eaf0a4 100644 --- a/mantidimaging/eyes_tests/live_viewer_window_test.py +++ b/mantidimaging/eyes_tests/live_viewer_window_test.py @@ -88,4 +88,4 @@ def test_rotate_operation_rotates_image(self, _mock_time, _mock_image_watcher, m self.imaging.show_live_viewer(self.live_directory) self.imaging.live_viewer_list[-1].presenter.model._handle_image_changed_in_list(image_list) self.imaging.live_viewer_list[-1].rotate_angles_group.actions()[1].trigger() - self.check_target(widget=self.imaging.live_viewer_list[-1]) \ No newline at end of file + self.check_target(widget=self.imaging.live_viewer_list[-1]) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index bbadb1a284f..e6d1487da44 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -13,7 +13,7 @@ from tifffile import tifffile from astropy.io import fits -from mantidimaging.core.utility import ExecutionProfiler +#from mantidimaging.core.utility import ExecutionProfiler from mantidimaging.core.utility.sensible_roi import SensibleROI if TYPE_CHECKING: @@ -204,7 +204,7 @@ 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.array = np.array([]) + self.mean: list[float] = [] self.mean_dict: dict[Path, float] = {} self.roi: SensibleROI | None = None @@ -254,7 +254,7 @@ 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.array) -> None: + def add_mean(self, image_data_obj: Image_Data, image_array: np.ndarray) -> None: if self.roi: left, top, right, bottom = self.roi mean_to_add = np.mean(image_array[top:bottom, left:right]) @@ -284,6 +284,7 @@ def load_image_from_path(image_path: Path) -> np.ndarray: image_data = fit[0].data return image_data + class ImageWatcher(QObject): """ A class to watch a directory for new images. diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index f4f26aa3505..e8ad1865f52 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -9,11 +9,9 @@ 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, ImageCache +from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data from mantidimaging.core.operations.loader import load_filter_packages from mantidimaging.core.data import ImageStack @@ -157,7 +155,6 @@ def load_as_dataset(self) -> None: 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) From 036de894a08c819a67e52fe2156f5ab77366bc23 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 2 Dec 2024 16:04:38 +0000 Subject: [PATCH 07/29] New mean array plotted when ROI is starting to move --- .../gui/windows/live_viewer/live_view_widget.py | 2 ++ mantidimaging/gui/windows/live_viewer/model.py | 10 ++++++++-- mantidimaging/gui/windows/live_viewer/presenter.py | 5 ++++- mantidimaging/gui/windows/live_viewer/view.py | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index c7baafd1eec..ac3dcf939d4 100644 --- a/mantidimaging/gui/windows/live_viewer/live_view_widget.py +++ b/mantidimaging/gui/windows/live_viewer/live_view_widget.py @@ -25,6 +25,7 @@ class LiveViewWidget(GraphicsLayoutWidget): image: MIMiniImageView image_shape: tuple = (-1, -1) roi_changed = pyqtSignal() + roi_changed_start = pyqtSignal(int) roi_object: SpectrumROI | None = None sensible_roi: SensibleROI @@ -67,6 +68,7 @@ def add_roi(self): 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: diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index e6d1487da44..1f1b143fe06 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -204,7 +204,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self._dataset_path: Path | None = None self.image_watcher: ImageWatcher | None = None self._images: list[Image_Data] = [] - self.mean: list[float] = [] + self.mean: np.ndarray = np.empty(0) self.mean_dict: dict[Path, float] = {} self.roi: SensibleROI | None = None @@ -261,10 +261,16 @@ def add_mean(self, image_data_obj: Image_Data, image_array: np.ndarray) -> None: else: mean_to_add = np.mean(image_array) self.mean_dict[image_data_obj.image_path] = mean_to_add - self.mean = list(self.mean_dict.values()) + #self.mean = np.array(list(self.mean_dict.values())) + self.mean = np.append(self.mean, mean_to_add) + + def clear_mean_partial(self): + self.mean_dict.clear() + self.mean = np.full(len(self.images), np.nan) def clear_mean(self): self.mean_dict.clear() + self.mean = np.delete(self.mean, np.arange(self.mean.size)) def calc_mean_fully(self) -> None: for image in self.images: diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index e8ad1865f52..cb9db041a33 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -160,9 +160,12 @@ def update_spectrum(self, spec_data: list | np.ndarray): self.view.spectrum.plot(spec_data) def handle_roi_moved(self, force_new_spectrums: bool = False): - # TODO: should we make all these functions go in the model? roi = self.view.live_viewer.get_roi() self.model.set_roi(roi) self.model.clear_mean() self.model.calc_mean_fully() self.update_spectrum(self.model.mean) + + def handle_roi_moved_start(self): + self.model.clear_mean_partial() + self.update_spectrum(self.model.mean) diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 37519c262c6..25767185b03 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -42,6 +42,7 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: 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) From 5980d1ef90c8351035da7eefaaeda20dd5948336 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 3 Dec 2024 11:10:40 +0000 Subject: [PATCH 08/29] check if mean for given image path has been added to prevent duplication --- .../gui/windows/live_viewer/model.py | 66 +++++++++++-------- .../gui/windows/live_viewer/presenter.py | 9 +-- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 1f1b143fe06..315a6206632 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -23,6 +23,20 @@ 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 @@ -36,23 +50,35 @@ class ImageCache: max_cache_size: int = 100 buffer_size: int = 10 - def __init__(self, func): - self.func = func - - def __call__(self, *args, **kwargs): - #print(f"Arguments: {args}, Keyword Arguments: {kwargs}") - result = self.func(*args, **kwargs) - self.add_to_cache(args, result) - return result + def __init__(self): + pass - def add_to_cache(self, args, image_array: np.ndarray): - if args not in self.cache_dict.keys(): - self.cache_dict[args] = image_array + def add_to_cache(self, image: Image_Data, image_array: np.ndarray): + if image.image_path not in self.cache_dict.keys(): + self.cache_dict[image.image_path] = (image_array, image.image_modified_time) def remove_from_cache(self, image: Image_Data): if image.image_path in self.cache_dict.keys(): del self.cache_dict[image.image_path] + def load_image(self, image: Image_Data) -> np.ndarray: + if image.image_path in self.cache_dict.keys(): + return self.cache_dict[image.image_path][0] + else: + image_array = load_image_from_path(image.image_path) + self.add_to_cache(image, image_array) + return image_array + + def get_cache(self): + return self.cache_dict + + def get_cached_image_paths(self): + return list(self.cache_dict.keys()) + + def get_cached_image_arrays(self): + return list(self.cache_dict.values())[::, 0] + + # def update_param_calculations(self) -> None: # if 'mean' in self.param_to_calc: # if len(self.mean) == len(self.image_list) - 1: @@ -207,6 +233,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self.mean: np.ndarray = np.empty(0) self.mean_dict: dict[Path, float] = {} self.roi: SensibleROI | None = None + self.image_cache = ImageCache() @property def path(self) -> Path | None: @@ -261,7 +288,6 @@ def add_mean(self, image_data_obj: Image_Data, image_array: np.ndarray) -> None: else: mean_to_add = np.mean(image_array) self.mean_dict[image_data_obj.image_path] = mean_to_add - #self.mean = np.array(list(self.mean_dict.values())) self.mean = np.append(self.mean, mean_to_add) def clear_mean_partial(self): @@ -274,21 +300,7 @@ def clear_mean(self): def calc_mean_fully(self) -> None: for image in self.images: - self.add_mean(image, self.load_image_from_path(image.image_path)) - - @staticmethod - 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 + self.add_mean(image, self.image_cache.load_image(image)) class ImageWatcher(QObject): diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index cb9db041a33..0bc651406d5 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -11,7 +11,8 @@ from imagecodecs._deflate import DeflateError from mantidimaging.gui.mvp_base import BasePresenter -from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data +from mantidimaging.gui.windows.live_viewer.model import (LiveViewerWindowModel, Image_Data, load_image_from_path, + ImageCache) from mantidimaging.core.operations.loader import load_filter_packages from mantidimaging.core.data import ImageStack @@ -92,7 +93,7 @@ def display_image(self, image_data_obj: Image_Data) -> None: Display image in the view after validating contents """ try: - image_data = self.model.load_image_from_path(image_data_obj.image_path) + 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) @@ -109,9 +110,9 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.remove_image() self.view.live_viewer.show_error(message) return - #if np.any(np.isnan(self.model.image_stack.mean)): self.model.set_roi(self.view.live_viewer.get_roi()) - self.model.add_mean(image_data_obj, image_data) + if image_data_obj.image_path not in self.model.mean_dict.keys(): + self.model.add_mean(image_data_obj, image_data) self.view.show_most_recent_image(image_data) self.update_spectrum(self.model.mean) self.view.live_viewer.show_error(None) From 7fdde54a4edb7f5536f2b012d4c17e078226b562 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 3 Dec 2024 14:36:09 +0000 Subject: [PATCH 09/29] mean of cached imaged calculated with ROI is moved around --- .../gui/windows/live_viewer/model.py | 89 ++++++++----------- .../gui/windows/live_viewer/presenter.py | 6 +- 2 files changed, 42 insertions(+), 53 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 315a6206632..d57644005c8 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -44,23 +44,29 @@ class ImageCache: cache_dict: dict = {} image_list: list[Image_Data] image_paths: set[str] = set() - mean: np.ndarray = np.array([]) - roi: SensibleROI | None = None - param_to_calc: list[str] = [] - max_cache_size: int = 100 - buffer_size: int = 10 + max_cache_size: int | None = None + buffer_size: int | None = None - def __init__(self): - pass + def __init__(self, max_cache_size=None, buffer_size=None): + self.max_cache_size = max_cache_size + self.buffer_size = buffer_size def add_to_cache(self, image: Image_Data, image_array: np.ndarray): if image.image_path not in self.cache_dict.keys(): - self.cache_dict[image.image_path] = (image_array, image.image_modified_time) + 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_path] = [image_array, image.image_modified_time] def remove_from_cache(self, image: Image_Data): if image.image_path in self.cache_dict.keys(): del self.cache_dict[image.image_path] + def remove_oldest_image(self): + ordered_times = sorted(self.get_cached_image_modified_times()) + oldest_image_path = [path for path in self.cache_dict if self.cache_dict[path][1] == ordered_times[0]][0] + del self.cache_dict[oldest_image_path] + def load_image(self, image: Image_Data) -> np.ndarray: if image.image_path in self.cache_dict.keys(): return self.cache_dict[image.image_path][0] @@ -72,55 +78,20 @@ def load_image(self, image: Image_Data) -> np.ndarray: def get_cache(self): return self.cache_dict + def get_current_cache_size(self): + return len(self.cache_dict) + def get_cached_image_paths(self): return list(self.cache_dict.keys()) def get_cached_image_arrays(self): - return list(self.cache_dict.values())[::, 0] + print(f"{[info[0] for info in list(self.cache_dict.values())]=}") + return np.stack([info[0] for info in list(self.cache_dict.values())]) + + def get_cached_image_modified_times(self): + return [info[1] for info in list(self.cache_dict.values())] - # def update_param_calculations(self) -> None: - # if 'mean' in self.param_to_calc: - # if len(self.mean) == len(self.image_list) - 1: - # self.add_last_mean() - # else: - # if self.roi: - # self.calc_mean_fully_roi() - # else: - # self.calc_mean_fully() - # - # def add_last_mean(self) -> None: - # if self.delayed_stack is not None: - # if self.roi: - # left, top, right, bottom = self.roi - # mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1, top:bottom, - # left:right]))[0].compute() - # else: - # mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1]))[0].compute() - # self.mean = np.append(self.mean, mean_to_add) - # self.calc_mean_buffer() - - # def calc_mean_fully(self) -> None: - # if self.delayed_stack is not None: - # self.mean = dask.array.mean(self.delayed_stack, axis=(1, 2)).compute() - # - # def calc_mean_fully_roi(self): - # if self.delayed_stack is not None and self.image_list: - # left, top, right, bottom = self.roi - # current_cache_size = len(self.) - # self.mean = np.full(len(self.image_list), np.nan) - # np.put(self.mean, range(-current_cache_size, 0), self.calc_mean_cached_images(left, top, right, bottom)) - # - # def calc_mean_cached_images(self, left, top, right, bottom): - # current_cache_size = self.get_computed_image.cache_info()[3] - # cache_stack = [ - # self.get_computed_image(index) - # for index in range(self.selected_index - current_cache_size + 1, self.selected_index + 1, 1) - # ] - # cache_stack_array = np.stack(cache_stack) - # cache_stack_mean = np.mean(cache_stack_array[:, top:bottom, left:right], axis=(1, 2)) - # return cache_stack_mean - # # def calc_mean_buffer(self): # nanInds = np.argwhere(np.isnan(self.mean)) # left, top, right, bottom = self.roi @@ -234,6 +205,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self.mean_dict: dict[Path, float] = {} self.roi: SensibleROI | None = None self.image_cache = ImageCache() + self.mean_cached: np.ndarray = np.empty(0) @property def path(self) -> Path | None: @@ -302,6 +274,21 @@ def calc_mean_fully(self) -> None: for image in self.images: self.add_mean(image, self.image_cache.load_image(image)) + def calc_mean_cache(self) -> None: + if self.roi: + left, top, right, bottom = self.roi + self.mean_cached = np.mean(self.image_cache.get_cached_image_arrays()[:, top:bottom, left:right], axis=(1, 2)) + else: + self.mean_cached = np.mean(self.image_cache.get_cached_image_arrays(), axis=(1, 2)) + + def update_mean_with_cached_images(self) -> None: + np.put(self.mean, range(-self.image_cache.get_current_cache_size(), 0), self.mean_cached) + + def clear_and_update_mean_cache(self) -> None: + self.mean = np.full(len(self.images), np.nan) + self.calc_mean_cache() + self.update_mean_with_cached_images() + class ImageWatcher(QObject): """ diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 0bc651406d5..43ca056d317 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -67,7 +67,6 @@ def handle_deleted(self) -> None: def update_image_list(self, images_list: list[Image_Data]) -> None: """Update the image in the view.""" - # TODO: Might be a good idea to update and store the image list in the model so it can be cycled through if not images_list: self.handle_deleted() self.view.set_load_as_dataset_enabled(False) @@ -113,6 +112,8 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.model.set_roi(self.view.live_viewer.get_roi()) if image_data_obj.image_path not in self.model.mean_dict.keys(): self.model.add_mean(image_data_obj, image_data) + self.model.calc_mean_cache() + self.model.update_mean_with_cached_images() self.view.show_most_recent_image(image_data) self.update_spectrum(self.model.mean) self.view.live_viewer.show_error(None) @@ -168,5 +169,6 @@ def handle_roi_moved(self, force_new_spectrums: bool = False): self.update_spectrum(self.model.mean) def handle_roi_moved_start(self): - self.model.clear_mean_partial() + self.model.clear_mean() + self.model.clear_and_update_mean_cache() self.update_spectrum(self.model.mean) From f85222950dfb8d0f50e567eb01c481b85409b31a Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 3 Dec 2024 16:35:59 +0000 Subject: [PATCH 10/29] When ROI is moved, mean of cache is shown immediately, then images are buffered into the spectrum --- conda/meta.yaml | 2 - .../gui/windows/live_viewer/model.py | 52 +++++++++---------- .../gui/windows/live_viewer/presenter.py | 3 +- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/conda/meta.yaml b/conda/meta.yaml index 3a9668a813e..f129f9df21a 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -41,8 +41,6 @@ requirements: - qt-material=2.14 - darkdetect=0.8.0 - qt-gtk-platformtheme # [linux] - - dask - - dask-image build: diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index d57644005c8..d4cd48daa72 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -45,9 +45,9 @@ class ImageCache: image_list: list[Image_Data] image_paths: set[str] = set() max_cache_size: int | None = None - buffer_size: int | None = None + buffer_size: int = 10 - def __init__(self, max_cache_size=None, buffer_size=None): + def __init__(self, max_cache_size=None, buffer_size=10): self.max_cache_size = max_cache_size self.buffer_size = buffer_size @@ -62,10 +62,13 @@ def remove_from_cache(self, image: Image_Data): if image.image_path in self.cache_dict.keys(): del self.cache_dict[image.image_path] - def remove_oldest_image(self): + def get_oldest_image(self): ordered_times = sorted(self.get_cached_image_modified_times()) oldest_image_path = [path for path in self.cache_dict if self.cache_dict[path][1] == ordered_times[0]][0] - del self.cache_dict[oldest_image_path] + return oldest_image_path + + def remove_oldest_image(self): + del self.cache_dict[self.get_oldest_image()] def load_image(self, image: Image_Data) -> np.ndarray: if image.image_path in self.cache_dict.keys(): @@ -85,34 +88,12 @@ def get_cached_image_paths(self): return list(self.cache_dict.keys()) def get_cached_image_arrays(self): - print(f"{[info[0] for info in list(self.cache_dict.values())]=}") return np.stack([info[0] for info in list(self.cache_dict.values())]) def get_cached_image_modified_times(self): return [info[1] for info in list(self.cache_dict.values())] - # def calc_mean_buffer(self): - # nanInds = np.argwhere(np.isnan(self.mean)) - # left, top, right, bottom = self.roi - # if nanInds.size > 0: - # print(f"{self.mean=}") - # if nanInds.size < self.buffer_size: - # buffer_start = 0 - # else: - # buffer_start = nanInds.size - self.buffer_size - # dask_mean = dask.optimize( - # dask.array.mean(self.delayed_stack[buffer_start:nanInds.size, top:bottom, left:right], - # axis=(1, 2)))[0].compute() - # np.put(self.mean, range(buffer_start, nanInds.size), dask_mean) - - def delete_all_data(self): - pass - - def add_param_to_calc(self, param_name: str): - self.param_to_calc.append(param_name) - - class Image_Data: """ Image Data Class to store represent image data. @@ -204,7 +185,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self.mean: np.ndarray = np.empty(0) self.mean_dict: dict[Path, float] = {} self.roi: SensibleROI | None = None - self.image_cache = ImageCache() + self.image_cache = ImageCache(max_cache_size=10) self.mean_cached: np.ndarray = np.empty(0) @property @@ -289,6 +270,23 @@ def clear_and_update_mean_cache(self) -> None: self.calc_mean_cache() self.update_mean_with_cached_images() + def calc_mean_buffer(self): + 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: + oldest_image_modified_time = self.image_cache.cache_dict[self.image_cache.get_oldest_image()][1] + all_modified_times = [image.image_modified_time for image in self.images] + norm_mod_times = [mod_time - oldest_image_modified_time for mod_time in all_modified_times] + oldest_image_index = norm_mod_times.index(0.0) + for ind in range(len(nanInds) - 1, len(nanInds) - 1 - self.image_cache.buffer_size, -1): + if ind < 0: + break + buffer_mean = np.mean(load_image_from_path(self.images[ind].image_path)[top:bottom, left:right]) + np.put(self.mean, ind, buffer_mean) + class ImageWatcher(QObject): """ diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 43ca056d317..ab51cdecddc 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -114,6 +114,7 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.model.add_mean(image_data_obj, image_data) self.model.calc_mean_cache() self.model.update_mean_with_cached_images() + self.model.calc_mean_buffer() self.view.show_most_recent_image(image_data) self.update_spectrum(self.model.mean) self.view.live_viewer.show_error(None) @@ -165,7 +166,7 @@ def handle_roi_moved(self, force_new_spectrums: bool = False): roi = self.view.live_viewer.get_roi() self.model.set_roi(roi) self.model.clear_mean() - self.model.calc_mean_fully() + self.model.clear_and_update_mean_cache() self.update_spectrum(self.model.mean) def handle_roi_moved_start(self): From 49f4757c196f73b3f77357cf69d063f1d3524660 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 4 Dec 2024 13:27:15 +0000 Subject: [PATCH 11/29] Store Image_Data obj in cache and added TODOs --- .../gui/windows/live_viewer/model.py | 32 +++++++++---------- .../gui/windows/live_viewer/presenter.py | 6 ++-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index d4cd48daa72..06e4fb59142 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -3,6 +3,7 @@ from __future__ import annotations import time +from operator import attrgetter, itemgetter from typing import TYPE_CHECKING from pathlib import Path from logging import getLogger @@ -13,7 +14,6 @@ from tifffile import tifffile from astropy.io import fits -#from mantidimaging.core.utility import ExecutionProfiler from mantidimaging.core.utility.sensible_roi import SensibleROI if TYPE_CHECKING: @@ -41,10 +41,9 @@ 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_list: list[Image_Data] - image_paths: set[str] = set() + cache_dict: dict[Image_Data: [np.ndarray, float]] = {} max_cache_size: int | None = None + # TODO: shouldnt need buffer_size buffer_size: int = 10 def __init__(self, max_cache_size=None, buffer_size=10): @@ -52,27 +51,27 @@ def __init__(self, max_cache_size=None, buffer_size=10): self.buffer_size = buffer_size def add_to_cache(self, image: Image_Data, image_array: np.ndarray): - if image.image_path not in self.cache_dict.keys(): + 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_path] = [image_array, image.image_modified_time] + self.cache_dict[image] = [image_array, image.image_modified_time] def remove_from_cache(self, image: Image_Data): if image.image_path in self.cache_dict.keys(): del self.cache_dict[image.image_path] def get_oldest_image(self): - ordered_times = sorted(self.get_cached_image_modified_times()) - oldest_image_path = [path for path in self.cache_dict if self.cache_dict[path][1] == ordered_times[0]][0] - return oldest_image_path + time_ordered_cache = sorted(self.cache_dict.items(), key=lambda item: item[1][-1]) + print(f"{time_ordered_cache[0][0]=}") + return time_ordered_cache[0][0] def remove_oldest_image(self): del self.cache_dict[self.get_oldest_image()] def load_image(self, image: Image_Data) -> np.ndarray: - if image.image_path in self.cache_dict.keys(): - return self.cache_dict[image.image_path][0] + if image in self.cache_dict.keys(): + return self.cache_dict[image][0] else: image_array = load_image_from_path(image.image_path) self.add_to_cache(image, image_array) @@ -111,6 +110,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): """ @@ -255,10 +256,13 @@ def calc_mean_fully(self) -> None: for image in self.images: self.add_mean(image, self.image_cache.load_image(image)) + #TODO: The mean calcs shouldnt know or look at anything to do with the cache, the cache should be transparent + def calc_mean_cache(self) -> None: if self.roi: left, top, right, bottom = self.roi - self.mean_cached = np.mean(self.image_cache.get_cached_image_arrays()[:, top:bottom, left:right], axis=(1, 2)) + self.mean_cached = np.mean(self.image_cache.get_cached_image_arrays()[:, top:bottom, left:right], + axis=(1, 2)) else: self.mean_cached = np.mean(self.image_cache.get_cached_image_arrays(), axis=(1, 2)) @@ -277,10 +281,6 @@ def calc_mean_buffer(self): else: left, top, right, bottom = (0, 0, -1, -1) if nanInds.size > 0: - oldest_image_modified_time = self.image_cache.cache_dict[self.image_cache.get_oldest_image()][1] - all_modified_times = [image.image_modified_time for image in self.images] - norm_mod_times = [mod_time - oldest_image_modified_time for mod_time in all_modified_times] - oldest_image_index = norm_mod_times.index(0.0) for ind in range(len(nanInds) - 1, len(nanInds) - 1 - self.image_cache.buffer_size, -1): if ind < 0: break diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index ab51cdecddc..6a282635abb 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -11,8 +11,7 @@ from imagecodecs._deflate import DeflateError from mantidimaging.gui.mvp_base import BasePresenter -from mantidimaging.gui.windows.live_viewer.model import (LiveViewerWindowModel, Image_Data, load_image_from_path, - ImageCache) +from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data from mantidimaging.core.operations.loader import load_filter_packages from mantidimaging.core.data import ImageStack @@ -67,6 +66,8 @@ def handle_deleted(self) -> None: def update_image_list(self, images_list: list[Image_Data]) -> None: """Update the image in the view.""" + # TODO: put add_mean in here and check that if images have been deleted, these are compared against the mean_dict + # TODO: throw away and recalc the mean_dict if needed if not images_list: self.handle_deleted() self.view.set_load_as_dataset_enabled(False) @@ -110,6 +111,7 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.live_viewer.show_error(message) return self.model.set_roi(self.view.live_viewer.get_roi()) + #TODO: move all the add mean stuff into self.update_image_list() if image_data_obj.image_path not in self.model.mean_dict.keys(): self.model.add_mean(image_data_obj, image_data) self.model.calc_mean_cache() From c47d8c7ebeaa20ed4a0598276ff8eace68a9c22b Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 4 Dec 2024 15:32:16 +0000 Subject: [PATCH 12/29] Model no longer directly accesses image_cache, mean added in presenter.update_image_list --- .../gui/windows/live_viewer/model.py | 25 +++---------------- .../gui/windows/live_viewer/presenter.py | 25 ++++++++++--------- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 06e4fb59142..57931bf58bb 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -63,7 +63,6 @@ def remove_from_cache(self, image: Image_Data): def get_oldest_image(self): time_ordered_cache = sorted(self.cache_dict.items(), key=lambda item: item[1][-1]) - print(f"{time_ordered_cache[0][0]=}") return time_ordered_cache[0][0] def remove_oldest_image(self): @@ -256,35 +255,17 @@ def calc_mean_fully(self) -> None: for image in self.images: self.add_mean(image, self.image_cache.load_image(image)) - #TODO: The mean calcs shouldnt know or look at anything to do with the cache, the cache should be transparent - - def calc_mean_cache(self) -> None: - if self.roi: - left, top, right, bottom = self.roi - self.mean_cached = np.mean(self.image_cache.get_cached_image_arrays()[:, top:bottom, left:right], - axis=(1, 2)) - else: - self.mean_cached = np.mean(self.image_cache.get_cached_image_arrays(), axis=(1, 2)) - - def update_mean_with_cached_images(self) -> None: - np.put(self.mean, range(-self.image_cache.get_current_cache_size(), 0), self.mean_cached) - - def clear_and_update_mean_cache(self) -> None: - self.mean = np.full(len(self.images), np.nan) - self.calc_mean_cache() - self.update_mean_with_cached_images() - - def calc_mean_buffer(self): + def calc_mean_chunk(self, chunk_size: int) -> 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 range(len(nanInds) - 1, len(nanInds) - 1 - self.image_cache.buffer_size, -1): + for ind in range(len(nanInds) - 1, len(nanInds) - 1 - chunk_size, -1): if ind < 0: break - buffer_mean = np.mean(load_image_from_path(self.images[ind].image_path)[top:bottom, left:right]) + buffer_mean = np.mean(self.image_cache.load_image(self.images[ind])[top:bottom, left:right]) np.put(self.mean, ind, buffer_mean) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 6a282635abb..7400be811e9 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -32,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,7 +73,14 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: self.handle_deleted() self.view.set_load_as_dataset_enabled(False) else: + self.model.set_roi(self.view.live_viewer.get_roi()) self.model.images = images_list + if images_list[-1].image_path not in self.model.mean_dict.keys(): + image_data = self.model.image_cache.load_image(images_list[-1]) + self.model.add_mean(images_list[-1], image_data) + if not self.roi_moving: + self.model.calc_mean_chunk(50) + 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) @@ -110,15 +118,7 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.remove_image() self.view.live_viewer.show_error(message) return - self.model.set_roi(self.view.live_viewer.get_roi()) - #TODO: move all the add mean stuff into self.update_image_list() - if image_data_obj.image_path not in self.model.mean_dict.keys(): - self.model.add_mean(image_data_obj, image_data) - self.model.calc_mean_cache() - self.model.update_mean_with_cached_images() - self.model.calc_mean_buffer() self.view.show_most_recent_image(image_data) - self.update_spectrum(self.model.mean) self.view.live_viewer.show_error(None) def update_image_modified(self, image_path: Path) -> None: @@ -167,11 +167,12 @@ def update_spectrum(self, spec_data: list | np.ndarray): def handle_roi_moved(self, force_new_spectrums: bool = False): roi = self.view.live_viewer.get_roi() self.model.set_roi(roi) - self.model.clear_mean() - self.model.clear_and_update_mean_cache() + self.model.clear_mean_partial() + self.model.calc_mean_chunk(100) self.update_spectrum(self.model.mean) + self.roi_moving = False def handle_roi_moved_start(self): - self.model.clear_mean() - self.model.clear_and_update_mean_cache() + self.roi_moving = True + self.model.clear_mean_partial() self.update_spectrum(self.model.mean) From 60852b52c5749063e409b91b7a14726347661bd5 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 4 Dec 2024 17:21:26 +0000 Subject: [PATCH 13/29] unit test fixes and test_WHEN_image_added_to_cache_THEN_image_is_in_cache test --- .../gui/windows/live_viewer/model.py | 15 +++++-------- .../gui/windows/live_viewer/presenter.py | 5 +++-- .../windows/live_viewer/test/model_test.py | 22 ++++++++++++++++++- .../live_viewer/test/presenter_test.py | 8 +++++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 57931bf58bb..2aacb01a185 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -3,7 +3,6 @@ from __future__ import annotations import time -from operator import attrgetter, itemgetter from typing import TYPE_CHECKING from pathlib import Path from logging import getLogger @@ -41,7 +40,8 @@ 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, float]] = {} + #cache_dict: dict[Image_Data, list[np.ndarray, float]] + cache_dict: dict[Image_Data, tuple[np.ndarray, float]] max_cache_size: int | None = None # TODO: shouldnt need buffer_size buffer_size: int = 10 @@ -49,17 +49,18 @@ class ImageCache: def __init__(self, max_cache_size=None, buffer_size=10): self.max_cache_size = max_cache_size self.buffer_size = buffer_size + self.cache_dict = {} def add_to_cache(self, image: Image_Data, image_array: np.ndarray): 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, image.image_modified_time] + self.cache_dict[image] = (image_array, image.image_modified_time) def remove_from_cache(self, image: Image_Data): if image.image_path in self.cache_dict.keys(): - del self.cache_dict[image.image_path] + del self.cache_dict[image] def get_oldest_image(self): time_ordered_cache = sorted(self.cache_dict.items(), key=lambda item: item[1][-1]) @@ -124,16 +125,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""" diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 7400be811e9..58b3a5b538a 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -67,8 +67,9 @@ def handle_deleted(self) -> None: def update_image_list(self, images_list: list[Image_Data]) -> None: """Update the image in the view.""" - # TODO: put add_mean in here and check that if images have been deleted, these are compared against the mean_dict - # TODO: throw away and recalc the mean_dict if needed + # TODO: put add_mean in here and check that if images have been deleted, + # these are compared against the mean_dict + # throw away and recalc the mean_dict if needed if not images_list: self.handle_deleted() self.view.set_load_as_dataset_enabled(False) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 0fbad537e2d..6ebbf52df3c 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -4,12 +4,15 @@ import os 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 +162,20 @@ 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 = mock.create_autospec(Image_Data) + self.image_data.image_path = Path("abc_1.tif") + self.image_data.image_modified_time = 12345312.023 + self.image_array_mock = np.array(range(0, 5)) + + def test_WHEN_image_added_to_cache_THEN_image_is_in_cache(self): + image_cache = ImageCache() + image_cache.add_to_cache(self.image_data, self.image_array_mock) + np.testing.assert_array_equal(image_cache.cache_dict[self.image_data][0], self.image_array_mock) + print(f"{image_cache.cache_dict=}") + self.assertEqual(image_cache.cache_dict[self.image_data][1], 12345312.023) diff --git a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py index 63f6b6a4d18..86fcd63e934 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_dict = {} + 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) From 2e16d20233db0335a3086ca9feaf0ad0ba54e4db Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 4 Dec 2024 17:29:16 +0000 Subject: [PATCH 14/29] remove buffer_size from ImageCache --- mantidimaging/gui/windows/live_viewer/model.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 2aacb01a185..13510e68765 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -40,15 +40,11 @@ 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, list[np.ndarray, float]] cache_dict: dict[Image_Data, tuple[np.ndarray, float]] max_cache_size: int | None = None - # TODO: shouldnt need buffer_size - buffer_size: int = 10 - def __init__(self, max_cache_size=None, buffer_size=10): + def __init__(self, max_cache_size=None): self.max_cache_size = max_cache_size - self.buffer_size = buffer_size self.cache_dict = {} def add_to_cache(self, image: Image_Data, image_array: np.ndarray): From 6e5ccc1d2b2b75dffd07c19fcd219fc22867320c Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 5 Dec 2024 12:29:56 +0000 Subject: [PATCH 15/29] Made ImageCacheTest more randomised and added test_WHEN_image_removed_from_cache_THEN_image_is_not_in_cache and test_WHEN_oldest_image_got_THEN_get_oldest_image --- .../gui/windows/live_viewer/model.py | 2 +- .../windows/live_viewer/test/model_test.py | 37 ++++++++++++++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 13510e68765..e0f1467464e 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -55,7 +55,7 @@ def add_to_cache(self, image: Image_Data, image_array: np.ndarray): self.cache_dict[image] = (image_array, image.image_modified_time) def remove_from_cache(self, image: Image_Data): - if image.image_path in self.cache_dict.keys(): + if image in self.cache_dict: del self.cache_dict[image] def get_oldest_image(self): diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 6ebbf52df3c..d92e66667a4 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import random import time import unittest @@ -168,14 +169,32 @@ class ImageCacheTest(unittest.TestCase): def setUp(self) -> None: super().setUp() - self.image_data = mock.create_autospec(Image_Data) - self.image_data.image_path = Path("abc_1.tif") - self.image_data.image_modified_time = 12345312.023 - self.image_array_mock = np.array(range(0, 5)) + self.image_data_list = [None]*5 + self.image_array_mock_list = [np.random.rand(5)]*5 + for i in range(len(self.image_data_list)): + 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_cache = ImageCache() - image_cache.add_to_cache(self.image_data, self.image_array_mock) - np.testing.assert_array_equal(image_cache.cache_dict[self.image_data][0], self.image_array_mock) - print(f"{image_cache.cache_dict=}") - self.assertEqual(image_cache.cache_dict[self.image_data][1], 12345312.023) + 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][0], image_array_mock) + self.assertEqual(self.image_cache.cache_dict[image_data][1], image_data.image_modified_time) + + def test_WHEN_image_removed_from_cache_THEN_image_is_not_in_cache(self): + image_data = self.image_data_list[0] + image_array_mock = self.image_array_mock_list[0] + self.image_cache = ImageCache() + self.image_cache.add_to_cache(image_data, image_array_mock) + self.image_cache.remove_from_cache(image_data) + self.assertNotIn(image_data, self.image_cache.cache_dict) + + def test_WHEN_oldest_image_got_THEN_get_oldest_image(self): + self.image_cache = ImageCache() + 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]) From d1445b2e17f12c70c9a3a967a4e61878659691f4 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 5 Dec 2024 12:36:19 +0000 Subject: [PATCH 16/29] removed unneeded get methods in ImageCache --- mantidimaging/gui/windows/live_viewer/model.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index e0f1467464e..bd95c706bcf 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -73,21 +73,6 @@ def load_image(self, image: Image_Data) -> np.ndarray: self.add_to_cache(image, image_array) return image_array - def get_cache(self): - return self.cache_dict - - def get_current_cache_size(self): - return len(self.cache_dict) - - def get_cached_image_paths(self): - return list(self.cache_dict.keys()) - - def get_cached_image_arrays(self): - return np.stack([info[0] for info in list(self.cache_dict.values())]) - - def get_cached_image_modified_times(self): - return [info[1] for info in list(self.cache_dict.values())] - class Image_Data: """ From 261fa1ea8dceafb1a9c5345316bb7ae57a685ada Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 5 Dec 2024 13:45:05 +0000 Subject: [PATCH 17/29] added unit tests test_WHEN_image_not_in_cache_when_loaded_THEN_image_added_to_cache, test_WHEN_image_in_cache_when_loaded_then_image_taken_from_cache, test_WHEN_cache_full_THEN_loading_image_removes_oldest_image --- .../windows/live_viewer/test/model_test.py | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index d92e66667a4..879d2a377a7 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -13,7 +13,7 @@ import numpy as np from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal -from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, ImageCache, Image_Data +from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, ImageCache, Image_Data, load_image_from_path from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase @@ -192,9 +192,34 @@ def test_WHEN_image_removed_from_cache_THEN_image_is_not_in_cache(self): self.image_cache.remove_from_cache(image_data) self.assertNotIn(image_data, self.image_cache.cache_dict) - def test_WHEN_oldest_image_got_THEN_get_oldest_image(self): - self.image_cache = ImageCache() + 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]][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() + From 7ee3b5d88fd518fffefdc4d7de5cbf7b46fb59e5 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 5 Dec 2024 15:07:53 +0000 Subject: [PATCH 18/29] yapf ruff fixes --- .../gui/windows/live_viewer/test/model_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 879d2a377a7..f481c2ae342 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -13,7 +13,7 @@ import numpy as np from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal -from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, ImageCache, Image_Data, load_image_from_path +from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, ImageCache, Image_Data from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase @@ -169,9 +169,10 @@ class ImageCacheTest(unittest.TestCase): def setUp(self) -> None: super().setUp() - self.image_data_list = [None]*5 - self.image_array_mock_list = [np.random.rand(5)]*5 - for i in range(len(self.image_data_list)): + 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) @@ -222,4 +223,3 @@ def test_WHEN_cache_full_THEN_loading_image_removes_oldest_image(self): 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() - From c19a3369d0088fa197eb0991f6448b00d8a7b701 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 6 Dec 2024 09:49:31 +0000 Subject: [PATCH 19/29] pyright fixes --- .../gui/windows/live_viewer/model.py | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index bd95c706bcf..0962a696b4c 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -202,7 +202,7 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None: # 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: @@ -221,30 +221,32 @@ def add_mean(self, image_data_obj: Image_Data, image_array: np.ndarray) -> None: self.mean_dict[image_data_obj.image_path] = mean_to_add self.mean = np.append(self.mean, mean_to_add) - def clear_mean_partial(self): + def clear_mean_partial(self) -> None: self.mean_dict.clear() self.mean = np.full(len(self.images), np.nan) - def clear_mean(self): + def clear_mean(self) -> None: self.mean_dict.clear() self.mean = np.delete(self.mean, np.arange(self.mean.size)) def calc_mean_fully(self) -> None: - for image in self.images: - self.add_mean(image, self.image_cache.load_image(image)) + 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) -> 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 range(len(nanInds) - 1, len(nanInds) - 1 - chunk_size, -1): - if ind < 0: - break - buffer_mean = np.mean(self.image_cache.load_image(self.images[ind])[top:bottom, left:right]) - np.put(self.mean, ind, buffer_mean) + 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 range(len(nanInds) - 1, len(nanInds) - 1 - chunk_size, -1): + if ind < 0: + break + buffer_mean = np.mean(self.image_cache.load_image(self.images[ind])[top:bottom, left:right]) + np.put(self.mean, ind, buffer_mean) class ImageWatcher(QObject): From 0bc8dfc151e4fdae66040a82139a710e4bf755a8 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 6 Dec 2024 14:05:10 +0000 Subject: [PATCH 20/29] spectrum is updated asyncronously via a Thread (with warnings) --- mantidimaging/gui/windows/live_viewer/model.py | 13 ++++++++++++- mantidimaging/gui/windows/live_viewer/presenter.py | 11 ++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 0962a696b4c..23fdc1cdf92 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING from pathlib import Path from logging import getLogger +from threading import Thread import numpy as np from PyQt5.QtCore import QFileSystemWatcher, QObject, pyqtSignal, QTimer @@ -165,6 +166,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self.roi: SensibleROI | None = None self.image_cache = ImageCache(max_cache_size=10) self.mean_cached: np.ndarray = np.empty(0) + self.calc_mean_all_chunks_thread = None @property def path(self) -> Path | None: @@ -213,7 +215,7 @@ def close(self) -> 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: - if self.roi: + if self.roi and (self.roi.left, self.roi.top, self.roi.right, self.roi.bottom) != (0, 0, 0, 0): left, top, right, bottom = self.roi mean_to_add = np.mean(image_array[top:bottom, left:right]) else: @@ -248,6 +250,15 @@ def calc_mean_chunk(self, chunk_size: int) -> None: buffer_mean = np.mean(self.image_cache.load_image(self.images[ind])[top:bottom, left:right]) np.put(self.mean, ind, buffer_mean) + def create_new_calc_mean_all_chunks_thread(self, chunk_size: int) -> None: + self.calc_mean_all_chunks_thread = Thread(target=self.calc_mean_all_chunks, args=[chunk_size]) + + def calc_mean_all_chunks(self, chunk_size: int) -> None: + while np.isnan(self.mean).any(): + self.calc_mean_chunk(chunk_size) + self.presenter.update_spectrum(self.mean) + + class ImageWatcher(QObject): """ diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 58b3a5b538a..60e25f69015 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -67,9 +67,6 @@ def handle_deleted(self) -> None: def update_image_list(self, images_list: list[Image_Data]) -> None: """Update the image in the view.""" - # TODO: put add_mean in here and check that if images have been deleted, - # these are compared against the mean_dict - # throw away and recalc the mean_dict if needed if not images_list: self.handle_deleted() self.view.set_load_as_dataset_enabled(False) @@ -79,8 +76,6 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: if images_list[-1].image_path not in self.model.mean_dict.keys(): image_data = self.model.image_cache.load_image(images_list[-1]) self.model.add_mean(images_list[-1], image_data) - if not self.roi_moving: - self.model.calc_mean_chunk(50) 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) @@ -169,8 +164,10 @@ def handle_roi_moved(self, force_new_spectrums: bool = False): roi = self.view.live_viewer.get_roi() self.model.set_roi(roi) self.model.clear_mean_partial() - self.model.calc_mean_chunk(100) - self.update_spectrum(self.model.mean) + if self.model.calc_mean_all_chunks_thread is not None: + self.model.calc_mean_all_chunks_thread.join() + self.model.create_new_calc_mean_all_chunks_thread(100) + self.model.calc_mean_all_chunks_thread.start() self.roi_moving = False def handle_roi_moved_start(self): From b99ab420ec778155ee4dfcbc93290b4dc318385c Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 10 Dec 2024 14:57:40 +0000 Subject: [PATCH 21/29] Mean is calculated in separate thread, spectrum updated in Main thread --- .../gui/windows/live_viewer/model.py | 6 ---- .../gui/windows/live_viewer/presenter.py | 33 ++++++++++++++++--- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 23fdc1cdf92..84f369f703d 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING from pathlib import Path from logging import getLogger -from threading import Thread import numpy as np from PyQt5.QtCore import QFileSystemWatcher, QObject, pyqtSignal, QTimer @@ -250,14 +249,9 @@ def calc_mean_chunk(self, chunk_size: int) -> None: buffer_mean = np.mean(self.image_cache.load_image(self.images[ind])[top:bottom, left:right]) np.put(self.mean, ind, buffer_mean) - def create_new_calc_mean_all_chunks_thread(self, chunk_size: int) -> None: - self.calc_mean_all_chunks_thread = Thread(target=self.calc_mean_all_chunks, args=[chunk_size]) - def calc_mean_all_chunks(self, chunk_size: int) -> None: while np.isnan(self.mean).any(): self.calc_mean_chunk(chunk_size) - self.presenter.update_spectrum(self.mean) - class ImageWatcher(QObject): diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 60e25f69015..4ee0ab48790 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -7,6 +7,7 @@ from collections.abc import Callable from logging import getLogger import numpy as np +from PyQt5.QtCore import pyqtSignal, QObject, QThread from imagecodecs._deflate import DeflateError @@ -22,6 +23,18 @@ logger = getLogger(__name__) +class Worker(QObject): + finished = pyqtSignal() + + def __init__(self, presenter: LiveViewerWindowPresenter): + super().__init__() + self.presenter = presenter + + def run(self): + self.presenter.model.calc_mean_all_chunks(100) + self.finished.emit() + + class LiveViewerWindowPresenter(BasePresenter): """ The presenter for the Live Viewer window. @@ -164,13 +177,25 @@ def handle_roi_moved(self, force_new_spectrums: bool = False): roi = self.view.live_viewer.get_roi() self.model.set_roi(roi) self.model.clear_mean_partial() - if self.model.calc_mean_all_chunks_thread is not None: - self.model.calc_mean_all_chunks_thread.join() - self.model.create_new_calc_mean_all_chunks_thread(100) - self.model.calc_mean_all_chunks_thread.start() + self.run_mean_chunk_calc() self.roi_moving = False + def run_mean_chunk_calc(self): + self.thread = QThread() + self.worker = Worker(self) + self.worker.moveToThread(self.thread) + self.thread.started.connect(self.worker.run) + self.worker.finished.connect(self.update_spectrum_with_mean) + self.worker.finished.connect(self.thread.quit) + self.worker.finished.connect(self.worker.deleteLater) + self.thread.finished.connect(self.thread.deleteLater) + self.thread.start() + 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) From 994054d3fa1334c55d9b9cb54c4cc93100a8df77 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 11 Dec 2024 16:08:43 +0000 Subject: [PATCH 22/29] cleanup and suggested changes --- .../eyes_tests/live_viewer_window_test.py | 6 ++-- .../windows/live_viewer/live_view_widget.py | 11 ++----- .../gui/windows/live_viewer/model.py | 29 +++++-------------- .../gui/windows/live_viewer/presenter.py | 15 ++++++---- .../live_viewer/test/presenter_test.py | 2 +- mantidimaging/gui/windows/live_viewer/view.py | 8 ++--- 6 files changed, 27 insertions(+), 44 deletions(-) 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 ac3dcf939d4..fc76fc4b6f4 100644 --- a/mantidimaging/gui/windows/live_viewer/live_view_widget.py +++ b/mantidimaging/gui/windows/live_viewer/live_view_widget.py @@ -27,7 +27,6 @@ class LiveViewWidget(GraphicsLayoutWidget): roi_changed = pyqtSignal() roi_changed_start = pyqtSignal(int) roi_object: SpectrumROI | None = None - sensible_roi: SensibleROI def __init__(self) -> None: super().__init__() @@ -48,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: @@ -77,19 +77,12 @@ def set_image_shape(self, shape: tuple) -> None: def get_roi(self) -> SensibleROI: if not self.roi_object: return SensibleROI() + print(f"{self.roi_object.roi=}") roi = self.roi_object.roi pos = CloseEnoughPoint(roi.pos()) size = CloseEnoughPoint(roi.size()) return SensibleROI.from_points(pos, size) - def set_roi_alpha(self, alpha: int) -> None: - if not self.roi_object: - return - self.roi_object.colour = self.roi_object.colour[:3] + (alpha, ) - self.roi_object.setPen(self.roi_object.colour) - self.roi_object.hoverPen = mkPen(self.roi_object.colour, width=3) - self.set_roi_visibility_flags(bool(alpha)) - def set_roi_visibility_flags(self, visible: bool) -> None: if not self.roi_object: return diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 84f369f703d..e1fb276d2fb 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -159,12 +159,11 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self.presenter = presenter self._dataset_path: Path | None = None self.image_watcher: ImageWatcher | None = None - self._images: list[Image_Data] = [] + self.images: list[Image_Data] = [] self.mean: np.ndarray = np.empty(0) - self.mean_dict: dict[Path, float] = {} + self.mean_paths: set[Path] = set() self.roi: SensibleROI | None = None self.image_cache = ImageCache(max_cache_size=10) - self.mean_cached: np.ndarray = np.empty(0) self.calc_mean_all_chunks_thread = None @property @@ -179,17 +178,6 @@ def path(self, path: Path) -> None: self.image_watcher.recent_image_changed.connect(self.handle_image_modified) self.image_watcher._handle_notified_of_directry_change(str(path)) - @property - def images(self): - return self._images if self._images is not None else None - - @images.setter - def images(self, images): - self._images = images - - def set_roi(self, roi: SensibleROI): - self.roi = roi - def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None: """ Handle an image changed event. Update the image in the view. @@ -219,15 +207,15 @@ def add_mean(self, image_data_obj: Image_Data, image_array: np.ndarray) -> None: mean_to_add = np.mean(image_array[top:bottom, left:right]) else: mean_to_add = np.mean(image_array) - self.mean_dict[image_data_obj.image_path] = mean_to_add + 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_dict.clear() + self.mean_paths.clear() self.mean = np.full(len(self.images), np.nan) def clear_mean(self) -> None: - self.mean_dict.clear() + self.mean_paths.clear() self.mean = np.delete(self.mean, np.arange(self.mean.size)) def calc_mean_fully(self) -> None: @@ -243,10 +231,10 @@ def calc_mean_chunk(self, chunk_size: int) -> None: else: left, top, right, bottom = (0, 0, -1, -1) if nanInds.size > 0: - for ind in range(len(nanInds) - 1, len(nanInds) - 1 - chunk_size, -1): - if ind < 0: + for ind in nanInds[-1:-chunk_size:-1]: + if ind[0] < 0: break - buffer_mean = np.mean(self.image_cache.load_image(self.images[ind])[top:bottom, left:right]) + buffer_mean = np.mean(self.image_cache.load_image(self.images[ind[0]])[top:bottom, left:right]) np.put(self.mean, ind, buffer_mean) def calc_mean_all_chunks(self, chunk_size: int) -> None: @@ -279,7 +267,6 @@ class ImageWatcher(QObject): 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) - create_delayed_array: bool = False def __init__(self, directory: Path): """ diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 4ee0ab48790..fc65b2c9146 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -84,9 +84,11 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: self.handle_deleted() self.view.set_load_as_dataset_enabled(False) else: - self.model.set_roi(self.view.live_viewer.get_roi()) + 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_dict.keys(): + if images_list[-1].image_path not in self.model.mean_paths: image_data = self.model.image_cache.load_image(images_list[-1]) self.model.add_mean(images_list[-1], image_data) self.update_spectrum(self.model.mean) @@ -117,9 +119,7 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.remove_image() self.view.live_viewer.show_error(message) return - self.view.live_viewer.set_image_shape(image_data.shape) - if not self.view.live_viewer.roi_object and self.view.spectrum_action.isChecked(): - self.view.live_viewer.add_roi() + # self.view.live_viewer.set_image_shape(image_data.shape) image_data = self.perform_operations(image_data) if image_data.size == 0: message = "reading image: {image_path}: Image has zero size" @@ -128,6 +128,9 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.live_viewer.show_error(message) return self.view.show_most_recent_image(image_data) + # 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.view.live_viewer.show_error(None) def update_image_modified(self, image_path: Path) -> None: @@ -175,7 +178,7 @@ def update_spectrum(self, spec_data: list | np.ndarray): def handle_roi_moved(self, force_new_spectrums: bool = False): roi = self.view.live_viewer.get_roi() - self.model.set_roi(roi) + self.model.roi = roi self.model.clear_mean_partial() self.run_mean_chunk_calc() self.roi_moving = False diff --git a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py index 86fcd63e934..6fc17edffad 100644 --- a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py @@ -46,7 +46,7 @@ def test_load_as_dataset_empty_dir(self): ]) def test_load_as_dataset_enabled_when_images(self, image_list, action_enabled): self.model.set_roi = mock.Mock() - self.model.mean_dict = {} + self.model.mean_paths = set() self.model.mean = [] self.model.image_cache = mock.Mock() self.model.add_mean = mock.Mock() diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 25767185b03..52c0b4d2220 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -71,7 +71,6 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: 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_alpha(self.spectrum_action.isChecked() * 255) self.live_viewer.set_roi_visibility_flags(False) def show(self) -> None: @@ -132,11 +131,12 @@ def set_spectrum_visibility(self): if self.spectrum_action.isChecked(): if not self.live_viewer.roi_object: self.live_viewer.add_roi() - self.live_viewer.set_roi_alpha(255) + self.live_viewer.set_roi_visibility_flags(True) self.splitter.setSizes([int(0.7 * widget_height), int(0.3 * widget_height)]) - self.presenter.model.set_roi(self.live_viewer.get_roi()) + self.presenter.model.roi = self.live_viewer.get_roi() + print(f"{self.presenter.model.roi.right=}") self.presenter.model.calc_mean_fully() self.presenter.update_spectrum(self.presenter.model.mean) else: - self.live_viewer.set_roi_alpha(0) + self.live_viewer.set_roi_visibility_flags(False) self.splitter.setSizes([widget_height, 0]) From aed415e774601b7097bac7b1586d02263bf8aa23 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 12 Dec 2024 10:34:00 +0000 Subject: [PATCH 23/29] eyes test fix and load_image error handling --- .../gui/windows/live_viewer/model.py | 21 +++++++++++++------ .../gui/windows/live_viewer/presenter.py | 8 +++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index e1fb276d2fb..e5d3713f7ca 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -65,11 +65,16 @@ def get_oldest_image(self): def remove_oldest_image(self): del self.cache_dict[self.get_oldest_image()] - def load_image(self, image: Image_Data) -> np.ndarray: + def load_image(self, image: Image_Data) -> np.ndarray | None: if image in self.cache_dict.keys(): return self.cache_dict[image][0] else: - image_array = load_image_from_path(image.image_path) + 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 @@ -201,8 +206,10 @@ 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: - if self.roi and (self.roi.left, self.roi.top, self.roi.right, self.roi.bottom) != (0, 0, 0, 0): + 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 and (self.roi.left, self.roi.top, self.roi.right, self.roi.bottom) != (0, 0, 0, 0): left, top, right, bottom = self.roi mean_to_add = np.mean(image_array[top:bottom, left:right]) else: @@ -234,8 +241,10 @@ def calc_mean_chunk(self, chunk_size: int) -> None: for ind in nanInds[-1:-chunk_size:-1]: if ind[0] < 0: break - buffer_mean = np.mean(self.image_cache.load_image(self.images[ind[0]])[top:bottom, left:right]) - np.put(self.mean, ind, buffer_mean) + 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) -> None: while np.isnan(self.mean).any(): diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index fc65b2c9146..9e63885eea8 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -89,8 +89,12 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: 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: - image_data = self.model.image_cache.load_image(images_list[-1]) - self.model.add_mean(images_list[-1], image_data) + 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) From 5b5ada4a174ed6d7ebdbcd9b5b264ec1283834ff Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 12 Dec 2024 12:25:39 +0000 Subject: [PATCH 24/29] Live Viewer get_roi() can now return None --- mantidimaging/gui/windows/live_viewer/live_view_widget.py | 5 ++--- mantidimaging/gui/windows/live_viewer/model.py | 2 +- mantidimaging/gui/windows/live_viewer/view.py | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index fc76fc4b6f4..afadadf43c8 100644 --- a/mantidimaging/gui/windows/live_viewer/live_view_widget.py +++ b/mantidimaging/gui/windows/live_viewer/live_view_widget.py @@ -74,10 +74,9 @@ def add_roi(self): def set_image_shape(self, shape: tuple) -> None: self.image_shape = shape - def get_roi(self) -> SensibleROI: + def get_roi(self) -> SensibleROI | None: if not self.roi_object: - return SensibleROI() - print(f"{self.roi_object.roi=}") + return None roi = self.roi_object.roi pos = CloseEnoughPoint(roi.pos()) size = CloseEnoughPoint(roi.size()) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index e5d3713f7ca..0549af47578 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -209,7 +209,7 @@ def close(self) -> None: 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 and (self.roi.left, self.roi.top, self.roi.right, self.roi.bottom) != (0, 0, 0, 0): + elif self.roi is not None: left, top, right, bottom = self.roi mean_to_add = np.mean(image_array[top:bottom, left:right]) else: diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 52c0b4d2220..fa217568353 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -134,7 +134,6 @@ def set_spectrum_visibility(self): 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() - print(f"{self.presenter.model.roi.right=}") self.presenter.model.calc_mean_fully() self.presenter.update_spectrum(self.presenter.model.mean) else: From 6460ce14f7a0894f5d9b0b2e977fa1e347bebdf1 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 12 Dec 2024 14:47:22 +0000 Subject: [PATCH 25/29] release note --- docs/release_notes/next/dev-2311-dask-live-viewer | 1 - docs/release_notes/next/feature-2421-live_viewer_spectrum_cache | 0 2 files changed, 1 deletion(-) delete mode 100644 docs/release_notes/next/dev-2311-dask-live-viewer create mode 100644 docs/release_notes/next/feature-2421-live_viewer_spectrum_cache diff --git a/docs/release_notes/next/dev-2311-dask-live-viewer b/docs/release_notes/next/dev-2311-dask-live-viewer deleted file mode 100644 index 807bbad4db2..00000000000 --- a/docs/release_notes/next/dev-2311-dask-live-viewer +++ /dev/null @@ -1 +0,0 @@ -#2311: The Live Viewer now uses Dask to load in images and create a delayed datastack for operations \ No newline at end of file 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..e69de29bb2d From df14e069b3fd7354b4ee21e8f9148f9182e49ccf Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 12 Dec 2024 17:43:40 +0000 Subject: [PATCH 26/29] further changes and cleanup --- .../feature-2421-live_viewer_spectrum_cache | 1 + .../gui/windows/live_viewer/model.py | 29 +++++++-------- .../windows/live_viewer/test/model_test.py | 35 ++++++++----------- 3 files changed, 27 insertions(+), 38 deletions(-) diff --git a/docs/release_notes/next/feature-2421-live_viewer_spectrum_cache b/docs/release_notes/next/feature-2421-live_viewer_spectrum_cache index e69de29bb2d..5b8d73ccc91 100644 --- a/docs/release_notes/next/feature-2421-live_viewer_spectrum_cache +++ 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/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 0549af47578..de3b1946327 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -3,6 +3,7 @@ from __future__ import annotations import time +from operator import attrgetter from typing import TYPE_CHECKING from pathlib import Path from logging import getLogger @@ -40,34 +41,30 @@ 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, tuple[np.ndarray, float]] + 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): + 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, image.image_modified_time) + self._remove_oldest_image() + self.cache_dict[image] = image_array - def remove_from_cache(self, image: Image_Data): - if image in self.cache_dict: - del self.cache_dict[image] + 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 get_oldest_image(self): - time_ordered_cache = sorted(self.cache_dict.items(), key=lambda item: item[1][-1]) - return time_ordered_cache[0][0] - - def remove_oldest_image(self): - del self.cache_dict[self.get_oldest_image()] + def _remove_oldest_image(self) -> None: + del self.cache_dict[self._get_oldest_image()] def load_image(self, image: Image_Data) -> np.ndarray | None: if image in self.cache_dict.keys(): - return self.cache_dict[image][0] + return self.cache_dict[image] else: try: image_array = load_image_from_path(image.image_path) @@ -75,7 +72,7 @@ def load_image(self, image: Image_Data) -> np.ndarray | None: 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) + self._add_to_cache(image, image_array) return image_array @@ -239,8 +236,6 @@ def calc_mean_chunk(self, chunk_size: int) -> None: left, top, right, bottom = (0, 0, -1, -1) if nanInds.size > 0: for ind in nanInds[-1:-chunk_size:-1]: - if ind[0] < 0: - break 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]) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index f481c2ae342..486838ce0a4 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -181,24 +181,17 @@ def setUp(self) -> None: 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][0], image_array_mock) - self.assertEqual(self.image_cache.cache_dict[image_data][1], image_data.image_modified_time) - - def test_WHEN_image_removed_from_cache_THEN_image_is_not_in_cache(self): - image_data = self.image_data_list[0] - image_array_mock = self.image_array_mock_list[0] - self.image_cache = ImageCache() - self.image_cache.add_to_cache(image_data, image_array_mock) - self.image_cache.remove_from_cache(image_data) - self.assertNotIn(image_data, self.image_cache.cache_dict) + 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]) + 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.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") @@ -211,15 +204,15 @@ def test_WHEN_image_not_in_cache_when_loaded_THEN_image_added_to_cache(self, loa @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]) + 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]][0]) + 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() + 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() From 95942e63489ce85f9d5de8fd50fdad9a47c6d1f7 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 13 Dec 2024 12:00:49 +0000 Subject: [PATCH 27/29] non-threaded version --- .../gui/windows/live_viewer/model.py | 4 +-- .../gui/windows/live_viewer/presenter.py | 27 ++----------------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index de3b1946327..9efa76058c6 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -227,7 +227,7 @@ def calc_mean_fully(self) -> None: for image in self.images: self.add_mean(image, self.image_cache.load_image(image)) - def calc_mean_chunk(self, chunk_size: int) -> None: + 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: @@ -241,7 +241,7 @@ def calc_mean_chunk(self, chunk_size: int) -> 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) -> None: + def calc_mean_all_chunks(self, chunk_size: int = 10000) -> None: while np.isnan(self.mean).any(): self.calc_mean_chunk(chunk_size) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 9e63885eea8..fc0e5b3f6fc 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -23,18 +23,6 @@ logger = getLogger(__name__) -class Worker(QObject): - finished = pyqtSignal() - - def __init__(self, presenter: LiveViewerWindowPresenter): - super().__init__() - self.presenter = presenter - - def run(self): - self.presenter.model.calc_mean_all_chunks(100) - self.finished.emit() - - class LiveViewerWindowPresenter(BasePresenter): """ The presenter for the Live Viewer window. @@ -123,7 +111,6 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.remove_image() self.view.live_viewer.show_error(message) return - # self.view.live_viewer.set_image_shape(image_data.shape) image_data = self.perform_operations(image_data) if image_data.size == 0: message = "reading image: {image_path}: Image has zero size" @@ -132,9 +119,6 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.live_viewer.show_error(message) return self.view.show_most_recent_image(image_data) - # 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.view.live_viewer.show_error(None) def update_image_modified(self, image_path: Path) -> None: @@ -188,15 +172,8 @@ def handle_roi_moved(self, force_new_spectrums: bool = False): self.roi_moving = False def run_mean_chunk_calc(self): - self.thread = QThread() - self.worker = Worker(self) - self.worker.moveToThread(self.thread) - self.thread.started.connect(self.worker.run) - self.worker.finished.connect(self.update_spectrum_with_mean) - self.worker.finished.connect(self.thread.quit) - self.worker.finished.connect(self.worker.deleteLater) - self.thread.finished.connect(self.thread.deleteLater) - self.thread.start() + self.model.calc_mean_all_chunks() + self.update_spectrum_with_mean() def handle_roi_moved_start(self): self.roi_moving = True From 5bbc39e75b02b29cca5caa98ae34172cd2736ad0 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 13 Dec 2024 14:02:26 +0000 Subject: [PATCH 28/29] type annotation fix and removed unneeded imports --- mantidimaging/gui/windows/live_viewer/model.py | 2 +- mantidimaging/gui/windows/live_viewer/presenter.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 9efa76058c6..4b056d8a8c4 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -62,7 +62,7 @@ def _get_oldest_image(self) -> Image_Data: def _remove_oldest_image(self) -> None: del self.cache_dict[self._get_oldest_image()] - def load_image(self, image: Image_Data) -> np.ndarray | None: + def load_image(self, image: Image_Data) -> np.ndarray: if image in self.cache_dict.keys(): return self.cache_dict[image] else: diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index fc0e5b3f6fc..8a00ab2a6f2 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -7,7 +7,6 @@ from collections.abc import Callable from logging import getLogger import numpy as np -from PyQt5.QtCore import pyqtSignal, QObject, QThread from imagecodecs._deflate import DeflateError From c0e587586d863e40f7759cf925e4a07280789f3e Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 13 Dec 2024 14:36:52 +0000 Subject: [PATCH 29/29] increase size of cache dict to 100 --- mantidimaging/gui/windows/live_viewer/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 4b056d8a8c4..fe66dca74ba 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -165,7 +165,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): 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=10) + self.image_cache = ImageCache(max_cache_size=100) self.calc_mean_all_chunks_thread = None @property