From a4103211ef3a7010cfd31f190364977d32d266ad Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 7 Aug 2024 13:01:03 +0100 Subject: [PATCH 01/46] Create DaskImageDataStack to hold delayed array of all images in LV path --- .../gui/windows/live_viewer/model.py | 29 +++++++++++++++++-- .../gui/windows/live_viewer/presenter.py | 26 ++++++++--------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index dd0c78671f2..d23ad7cba58 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 dask.array from PyQt5.QtCore import QFileSystemWatcher, QObject, pyqtSignal, QTimer +import dask_image.imread + if TYPE_CHECKING: from os import stat_result from mantidimaging.gui.windows.live_viewer.view import LiveViewerWindowPresenter @@ -15,6 +19,18 @@ LOG = getLogger(__name__) +class DaskImageDataStack: + """ + A Dask Image Data Stack Class to hold a delayed array of all the images in the Live Viewer Path + """ + def __init__(self, image_list: list[Image_Data]): + self.delayed_stack = dask.array.concatenate([image_data.delayed_array for image_data in image_list]) + + @property + def shape(self): + return self.delayed_stack.shape + + class Image_Data: """ Image Data Class to store represent image data. @@ -45,6 +61,7 @@ def __init__(self, image_path: Path): self.image_path = image_path self.image_name = image_path.name self._stat = image_path.stat() + self.delayed_array = dask_image.imread.imread(self.image_path) @property def stat(self) -> stat_result: @@ -103,6 +120,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self._dataset_path: Path | None = None self.image_watcher: ImageWatcher | None = None self.images: list[Image_Data] = [] + self.image_stack: DaskImageDataStack @property def path(self) -> Path | None: @@ -116,7 +134,7 @@ 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)) - def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None: + def _handle_image_changed_in_list(self, image_files: list[Image_Data], dask_image_stack: DaskImageDataStack) -> None: """ Handle an image changed event. Update the image in the view. This method is called when the image_watcher detects a change @@ -125,6 +143,7 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None: :param image_files: list of image files """ self.images = image_files + self.image_stack = dask_image_stack self.presenter.update_image_list(image_files) def handle_image_modified(self, image_path: Path): @@ -160,7 +179,7 @@ class ImageWatcher(QObject): sort_images_by_modified_time(images) Sort the images by modified time. """ - image_changed = pyqtSignal(list) # Signal emitted when an image is added or removed + image_changed = pyqtSignal(list, DaskImageDataStack) # Signal emitted when an image is added or removed recent_image_changed = pyqtSignal(Path) def __init__(self, directory: Path): @@ -268,8 +287,12 @@ def _handle_directory_change(self) -> None: break images = self.sort_images_by_modified_time(images) + dask_image_stack = DaskImageDataStack(images) self.update_recent_watcher(images[-1:]) - self.image_changed.emit(images) + self.image_changed.emit(images, dask_image_stack) + + # arrsum = dask.array.sum(dask_image_stack.delayed_stack) + # print(f"{arrsum.compute()=}") @staticmethod def _is_image_file(file_name: str) -> bool: diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 34984d032b0..fe158435a47 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -82,16 +82,16 @@ def select_image(self, index: int) -> None: 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) + image_data = self.load_image(image_data_obj) except (OSError, KeyError, ValueError, TiffFileError, DeflateError) as error: - message = f"{type(error).__name__} reading image: {image_path}: {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) @@ -99,25 +99,23 @@ def display_image(self, image_path: Path) -> None: image_data = self.perform_operations(image_data) if image_data.size == 0: message = "reading image: {image_path}: Image has zero size" - logger.error("reading image: %s: Image has zero size", image_path) + logger.error("reading image: %s: Image has zero size", image_data_obj.image_path) self.view.remove_image() self.view.live_viewer.show_error(message) return - self.view.show_most_recent_image(image_data) self.view.live_viewer.show_error(None) @staticmethod - def load_image(image_path: Path) -> np.ndarray: + def load_image(image_data_obj: Image_Data) -> 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: + if image_data_obj.image_path.suffix.lower() in [".tif", ".tiff"]: + image_data = image_data_obj.delayed_array.compute()[0] + elif image_data_obj.image_path.suffix.lower() == ".fits": + with fits.open(image_data_obj.image_path.__str__()) as fit: image_data = fit[0].data return image_data @@ -126,14 +124,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: """ From 37088ad43528658aafa30a2f3bd47eb41ef91186 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 7 Aug 2024 16:43:36 +0100 Subject: [PATCH 02/46] add dask and dask-image to dependancies --- conda/meta.yaml | 2 ++ 1 file changed, 2 insertions(+) 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: From fd92b8d89855ae9e913bd5ff1a625542faba543c Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 8 Aug 2024 10:34:07 +0100 Subject: [PATCH 03/46] live viewer model unit test fixes --- .../gui/windows/live_viewer/model.py | 23 +++++++++++++++---- .../windows/live_viewer/test/model_test.py | 4 +++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index d23ad7cba58..6a064a1a5e3 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -23,8 +23,12 @@ class DaskImageDataStack: """ A Dask Image Data Stack Class to hold a delayed array of all the images in the Live Viewer Path """ + delayed_stack: dask.array.Array | None = None + def __init__(self, image_list: list[Image_Data]): - self.delayed_stack = dask.array.concatenate([image_data.delayed_array for image_data in image_list]) + if image_list: + if image_list[0].create_delayed_array: + self.delayed_stack = dask.array.concatenate([image_data.delayed_array for image_data in image_list]) @property def shape(self): @@ -47,9 +51,13 @@ class Image_Data: size of image file image_modified_time : float last modified time of image file + delayed_array: dask.array.Array + A delayed dask array of the image data """ + delayed_array: dask.array.Array + create_delayed_array: bool - def __init__(self, image_path: Path): + def __init__(self, image_path: Path, create_delayed_array: bool = True): """ Constructor for Image_Data class. @@ -61,7 +69,10 @@ def __init__(self, image_path: Path): self.image_path = image_path self.image_name = image_path.name self._stat = image_path.stat() - self.delayed_array = dask_image.imread.imread(self.image_path) + self.create_delayed_array = create_delayed_array + if self.create_delayed_array: + self.set_delayed_array() + @property def stat(self) -> stat_result: @@ -77,6 +88,9 @@ def image_modified_time_stamp(self) -> str: """Return the image modified time as a string""" return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.image_modified_time)) + def set_delayed_array(self) -> None: + self.delayed_array = dask_image.imread.imread(self.image_path) + class SubDirectory: @@ -181,6 +195,7 @@ class ImageWatcher(QObject): """ image_changed = pyqtSignal(list, DaskImageDataStack) # Signal emitted when an image is added or removed recent_image_changed = pyqtSignal(Path) + create_delayed_array: bool def __init__(self, directory: Path): """ @@ -215,7 +230,7 @@ def find_images(self, directory: Path) -> list[Image_Data]: for file_path in directory.iterdir(): if self._is_image_file(file_path.name): try: - image_obj = Image_Data(file_path) + image_obj = Image_Data(file_path, create_delayed_array=self.create_delayed_array) image_files.append(image_obj) except FileNotFoundError: continue diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 0fbad537e2d..bd521e99902 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -9,7 +9,7 @@ from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal -from mantidimaging.gui.windows.live_viewer.model import ImageWatcher +from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, Image_Data from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase @@ -26,8 +26,10 @@ def setUp(self) -> None: mocker.side_effect = [mock_dir_watcher, mock_file_watcher] self.watcher = ImageWatcher(self.top_path) + self.watcher.create_delayed_array = False self.mock_signal_image = mock.create_autospec(pyqtSignal, emit=mock.Mock()) self.watcher.image_changed = self.mock_signal_image + self.watcher.create_delayed_array = False def _make_simple_dir(self, directory: Path, t0: float = 1000): file_list = [directory / f"abc_{i:06d}.tif" for i in range(5)] From 0890fee8b0507eea98037bf9448814c022df0c2c Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 8 Aug 2024 10:58:05 +0100 Subject: [PATCH 04/46] yapf ruff fixes --- mantidimaging/gui/windows/live_viewer/model.py | 4 ++-- mantidimaging/gui/windows/live_viewer/presenter.py | 3 +-- mantidimaging/gui/windows/live_viewer/test/model_test.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 6a064a1a5e3..d7e4c4f1524 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -73,7 +73,6 @@ def __init__(self, image_path: Path, create_delayed_array: bool = True): if self.create_delayed_array: self.set_delayed_array() - @property def stat(self) -> stat_result: return self._stat @@ -148,7 +147,8 @@ 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)) - def _handle_image_changed_in_list(self, image_files: list[Image_Data], dask_image_stack: DaskImageDataStack) -> None: + def _handle_image_changed_in_list(self, image_files: list[Image_Data], + dask_image_stack: DaskImageDataStack) -> None: """ Handle an image changed event. Update the image in the view. This method is called when the image_watcher detects a change diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index fe158435a47..4911d20c4ae 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -9,7 +9,6 @@ import numpy as np from imagecodecs._deflate import DeflateError -from tifffile import tifffile, TiffFileError from astropy.io import fits from mantidimaging.gui.mvp_base import BasePresenter @@ -90,7 +89,7 @@ def display_image(self, image_data_obj: Image_Data) -> None: """ try: image_data = self.load_image(image_data_obj) - except (OSError, KeyError, ValueError, TiffFileError, DeflateError) as error: + 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() diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index bd521e99902..99c7c35932f 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -9,7 +9,7 @@ from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal -from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, Image_Data +from mantidimaging.gui.windows.live_viewer.model import ImageWatcher from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase From 85a4695cac72f5faf74fd2914f2162c905c7c552 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 8 Aug 2024 11:51:22 +0100 Subject: [PATCH 05/46] cleanup --- mantidimaging/gui/windows/live_viewer/model.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index d7e4c4f1524..436db84f778 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -230,7 +230,7 @@ def find_images(self, directory: Path) -> list[Image_Data]: for file_path in directory.iterdir(): if self._is_image_file(file_path.name): try: - image_obj = Image_Data(file_path, create_delayed_array=self.create_delayed_array) + image_obj = Image_Data(file_path) image_files.append(image_obj) except FileNotFoundError: continue @@ -306,9 +306,6 @@ def _handle_directory_change(self) -> None: self.update_recent_watcher(images[-1:]) self.image_changed.emit(images, dask_image_stack) - # arrsum = dask.array.sum(dask_image_stack.delayed_stack) - # print(f"{arrsum.compute()=}") - @staticmethod def _is_image_file(file_name: str) -> bool: """ From 85f699a1e29ad186e77fe91f24e333cd3b7c5263 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 8 Aug 2024 13:00:15 +0100 Subject: [PATCH 06/46] impliment toggle to create delayed dask array --- mantidimaging/gui/windows/live_viewer/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 436db84f778..7ce41c8cc1e 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -195,7 +195,7 @@ class ImageWatcher(QObject): """ image_changed = pyqtSignal(list, DaskImageDataStack) # Signal emitted when an image is added or removed recent_image_changed = pyqtSignal(Path) - create_delayed_array: bool + create_delayed_array: bool = True def __init__(self, directory: Path): """ @@ -230,7 +230,7 @@ def find_images(self, directory: Path) -> list[Image_Data]: for file_path in directory.iterdir(): if self._is_image_file(file_path.name): try: - image_obj = Image_Data(file_path) + image_obj = Image_Data(file_path, create_delayed_array=self.create_delayed_array) image_files.append(image_obj) except FileNotFoundError: continue From a09127b7cded4e322d8a686e4657ffcd66728208 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 8 Aug 2024 16:45:39 +0100 Subject: [PATCH 07/46] added ability to read fits files via delayed dask functions --- mantidimaging/gui/windows/live_viewer/model.py | 18 +++++++++++++++--- .../gui/windows/live_viewer/presenter.py | 7 ++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 7ce41c8cc1e..2f6c09be921 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -11,6 +11,7 @@ from PyQt5.QtCore import QFileSystemWatcher, QObject, pyqtSignal, QTimer import dask_image.imread +from astropy.io import fits if TYPE_CHECKING: from os import stat_result @@ -25,10 +26,18 @@ class DaskImageDataStack: """ delayed_stack: dask.array.Array | None = None - def __init__(self, image_list: list[Image_Data]): + def __init__(self, image_list: list[Image_Data] | None): if image_list: if image_list[0].create_delayed_array: - self.delayed_stack = dask.array.concatenate([image_data.delayed_array for image_data in image_list]) + if image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: + arrays = [image_data.delayed_array for image_data in image_list] + self.delayed_stack = dask.array.stack(dask.array.array(arrays)) + elif image_list[0].image_path.suffix.lower() in [".fits"]: + with fits.open(image_list[0].image_path.__str__()) as fit: + sample = fit[0].data + arrays = [image_data.delayed_array for image_data in image_list] + lazy_arrays =[dask.array.from_delayed(x, shape=sample.shape, dtype=sample.dtype) for x in arrays] + self.delayed_stack = dask.array.stack(lazy_arrays) @property def shape(self): @@ -88,7 +97,10 @@ def image_modified_time_stamp(self) -> str: return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.image_modified_time)) def set_delayed_array(self) -> None: - self.delayed_array = dask_image.imread.imread(self.image_path) + if self.image_path.suffix.lower() in [".tif", ".tiff"]: + self.delayed_array = dask_image.imread.imread(self.image_path)[0] + elif self.image_path.suffix.lower() == ".fits": + self.delayed_array = dask.delayed(fits.open)(self.image_path)[0].data class SubDirectory: diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 4911d20c4ae..7404f2a3dda 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -111,11 +111,8 @@ def load_image(image_data_obj: Image_Data) -> np.ndarray: Load a .Tif, .Tiff or .Fits file only if it exists and returns as an ndarray """ - if image_data_obj.image_path.suffix.lower() in [".tif", ".tiff"]: - image_data = image_data_obj.delayed_array.compute()[0] - elif image_data_obj.image_path.suffix.lower() == ".fits": - with fits.open(image_data_obj.image_path.__str__()) as fit: - image_data = fit[0].data + if image_data_obj.image_path.suffix.lower() in [".tif", ".tiff", ".fits"]: + image_data = image_data_obj.delayed_array.compute() return image_data def update_image_modified(self, image_path: Path) -> None: From 71bb53fcb10fa0e239f380723d2a579b246b2e09 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 8 Aug 2024 16:54:19 +0100 Subject: [PATCH 08/46] remove astropy import and yapf fix --- 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 2f6c09be921..9a90444c48a 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -36,7 +36,7 @@ def __init__(self, image_list: list[Image_Data] | None): with fits.open(image_list[0].image_path.__str__()) as fit: sample = fit[0].data arrays = [image_data.delayed_array for image_data in image_list] - lazy_arrays =[dask.array.from_delayed(x, shape=sample.shape, dtype=sample.dtype) for x in arrays] + lazy_arrays = [dask.array.from_delayed(x, shape=sample.shape, dtype=sample.dtype) for x in arrays] self.delayed_stack = dask.array.stack(lazy_arrays) @property diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 7404f2a3dda..0f5ce32114d 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -9,7 +9,6 @@ import numpy as np from imagecodecs._deflate import DeflateError -from astropy.io import fits from mantidimaging.gui.mvp_base import BasePresenter from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data From 09927e5d3b598fed55646facdea448bfdcdde143 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 9 Aug 2024 13:00:10 +0100 Subject: [PATCH 09/46] live viewer eyes tests fixes --- mantidimaging/eyes_tests/live_viewer_window_test.py | 6 +++--- mantidimaging/gui/windows/live_viewer/model.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mantidimaging/eyes_tests/live_viewer_window_test.py b/mantidimaging/eyes_tests/live_viewer_window_test.py index 7d9b49bea06..89855459049 100644 --- a/mantidimaging/eyes_tests/live_viewer_window_test.py +++ b/mantidimaging/eyes_tests/live_viewer_window_test.py @@ -61,7 +61,7 @@ def test_live_view_opens_without_data(self, _mock_time, _mock_image_watcher): @mock.patch("time.time", return_value=4000.0) def test_live_view_opens_with_data(self, _mock_time, _mock_image_watcher, mock_load_image): file_list = self._make_simple_dir(self.live_directory) - image_list = [Image_Data(path) for path in file_list] + image_list = [Image_Data(path, create_delayed_array=False) for path in file_list] mock_load_image.return_value = self._generate_image() self.imaging.show_live_viewer(self.live_directory) self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list) @@ -72,7 +72,7 @@ def test_live_view_opens_with_data(self, _mock_time, _mock_image_watcher, mock_l @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): file_list = self._make_simple_dir(self.live_directory) - image_list = [Image_Data(path) for path in file_list] + image_list = [Image_Data(path, create_delayed_array=False) for path in file_list] mock_load_image.side_effect = ValueError self.imaging.show_live_viewer(self.live_directory) self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list) @@ -83,7 +83,7 @@ def test_live_view_opens_with_bad_data(self, _mock_time, _mock_image_watcher, mo @mock.patch("time.time", return_value=4000.0) def test_rotate_operation_rotates_image(self, _mock_time, _mock_image_watcher, mock_load_image): file_list = self._make_simple_dir(self.live_directory) - image_list = [Image_Data(path) for path in file_list] + image_list = [Image_Data(path, create_delayed_array=False) for path in file_list] mock_load_image.return_value = self._generate_image() self.imaging.show_live_viewer(self.live_directory) self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 9a90444c48a..23153b33f36 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -145,7 +145,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self._dataset_path: Path | None = None self.image_watcher: ImageWatcher | None = None self.images: list[Image_Data] = [] - self.image_stack: DaskImageDataStack + self.image_stack: DaskImageDataStack | None @property def path(self) -> Path | None: @@ -159,8 +159,9 @@ 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)) - def _handle_image_changed_in_list(self, image_files: list[Image_Data], - dask_image_stack: DaskImageDataStack) -> None: + def _handle_image_changed_in_list(self, + image_files: list[Image_Data], + dask_image_stack: DaskImageDataStack | None = None) -> None: """ Handle an image changed event. Update the image in the view. This method is called when the image_watcher detects a change From c7079b13f8cc09aabdfd1e6a578281d0eda47ad7 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 12 Aug 2024 17:29:59 +0100 Subject: [PATCH 10/46] move all delayed array creation into DaskImageDataStack --- .../eyes_tests/live_viewer_window_test.py | 17 ++++--- .../gui/windows/live_viewer/model.py | 50 +++++++++++-------- .../gui/windows/live_viewer/presenter.py | 23 +++++---- 3 files changed, 51 insertions(+), 39 deletions(-) diff --git a/mantidimaging/eyes_tests/live_viewer_window_test.py b/mantidimaging/eyes_tests/live_viewer_window_test.py index 89855459049..c41b529deba 100644 --- a/mantidimaging/eyes_tests/live_viewer_window_test.py +++ b/mantidimaging/eyes_tests/live_viewer_window_test.py @@ -7,7 +7,7 @@ import numpy as np import os from mantidimaging.core.operations.loader import load_filter_packages -from mantidimaging.gui.windows.live_viewer.model import Image_Data +from mantidimaging.gui.windows.live_viewer.model import Image_Data, DaskImageDataStack from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase from pathlib import Path from mantidimaging.eyes_tests.base_eyes import BaseEyesTest @@ -61,10 +61,11 @@ def test_live_view_opens_without_data(self, _mock_time, _mock_image_watcher): @mock.patch("time.time", return_value=4000.0) def test_live_view_opens_with_data(self, _mock_time, _mock_image_watcher, mock_load_image): file_list = self._make_simple_dir(self.live_directory) - image_list = [Image_Data(path, create_delayed_array=False) for path in file_list] + image_list = [Image_Data(path) for path in file_list] + dask_image_stack = DaskImageDataStack(image_list, create_delayed_array=False) mock_load_image.return_value = self._generate_image() self.imaging.show_live_viewer(self.live_directory) - self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list) + self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list, dask_image_stack) self.check_target(widget=self.imaging.live_viewer) @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image') @@ -72,10 +73,11 @@ def test_live_view_opens_with_data(self, _mock_time, _mock_image_watcher, mock_l @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): file_list = self._make_simple_dir(self.live_directory) - image_list = [Image_Data(path, create_delayed_array=False) for path in file_list] + image_list = [Image_Data(path) for path in file_list] + dask_image_stack = DaskImageDataStack(image_list, create_delayed_array=False) mock_load_image.side_effect = ValueError self.imaging.show_live_viewer(self.live_directory) - self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list) + self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list, dask_image_stack) self.check_target(widget=self.imaging.live_viewer) @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image') @@ -83,9 +85,10 @@ def test_live_view_opens_with_bad_data(self, _mock_time, _mock_image_watcher, mo @mock.patch("time.time", return_value=4000.0) def test_rotate_operation_rotates_image(self, _mock_time, _mock_image_watcher, mock_load_image): file_list = self._make_simple_dir(self.live_directory) - image_list = [Image_Data(path, create_delayed_array=False) for path in file_list] + image_list = [Image_Data(path) for path in file_list] + dask_image_stack = DaskImageDataStack(image_list, create_delayed_array=False) mock_load_image.return_value = self._generate_image() self.imaging.show_live_viewer(self.live_directory) - self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list) + self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list, dask_image_stack) self.imaging.live_viewer.rotate_angles_group.actions()[1].trigger() self.check_target(widget=self.imaging.live_viewer) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 23153b33f36..675fc760cdf 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -24,18 +24,20 @@ class DaskImageDataStack: """ A Dask Image Data Stack Class to hold a delayed array of all the images in the Live Viewer Path """ - delayed_stack: dask.array.Array | None = None + delayed_stack: dask.array.Array + image_list: list[Image_Data] - def __init__(self, image_list: list[Image_Data] | None): - if image_list: - if image_list[0].create_delayed_array: + def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = True): + self.image_list = image_list + + if image_list and create_delayed_array: + arrays = self.get_delayed_arrays() + if arrays: if image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: - arrays = [image_data.delayed_array for image_data in image_list] self.delayed_stack = dask.array.stack(dask.array.array(arrays)) elif image_list[0].image_path.suffix.lower() in [".fits"]: with fits.open(image_list[0].image_path.__str__()) as fit: sample = fit[0].data - arrays = [image_data.delayed_array for image_data in image_list] lazy_arrays = [dask.array.from_delayed(x, shape=sample.shape, dtype=sample.dtype) for x in arrays] self.delayed_stack = dask.array.stack(lazy_arrays) @@ -43,6 +45,20 @@ def __init__(self, image_list: list[Image_Data] | None): def shape(self): return self.delayed_stack.shape + def get_delayed_arrays(self) -> list[dask.array.Array] | None: + if self.image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: + return [dask_image.imread.imread(image_data.image_path)[0] for image_data in self.image_list] + elif self.image_list[0].image_path.suffix.lower() == ".fits": + return [dask.delayed(fits.open)(image_data.image_path)[0].data for image_data in self.image_list] + else: + return None + + def get_delayed_image(self, index) -> dask.array.Array: + return self.delayed_stack[index] + + def get_image_data(self, index) -> Image_Data: + return self.image_list[index] + class Image_Data: """ @@ -66,7 +82,7 @@ class Image_Data: delayed_array: dask.array.Array create_delayed_array: bool - def __init__(self, image_path: Path, create_delayed_array: bool = True): + def __init__(self, image_path: Path): """ Constructor for Image_Data class. @@ -78,9 +94,6 @@ def __init__(self, image_path: Path, create_delayed_array: bool = True): self.image_path = image_path self.image_name = image_path.name self._stat = image_path.stat() - self.create_delayed_array = create_delayed_array - if self.create_delayed_array: - self.set_delayed_array() @property def stat(self) -> stat_result: @@ -96,12 +109,6 @@ def image_modified_time_stamp(self) -> str: """Return the image modified time as a string""" return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.image_modified_time)) - def set_delayed_array(self) -> None: - if self.image_path.suffix.lower() in [".tif", ".tiff"]: - self.delayed_array = dask_image.imread.imread(self.image_path)[0] - elif self.image_path.suffix.lower() == ".fits": - self.delayed_array = dask.delayed(fits.open)(self.image_path)[0].data - class SubDirectory: @@ -145,7 +152,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self._dataset_path: Path | None = None self.image_watcher: ImageWatcher | None = None self.images: list[Image_Data] = [] - self.image_stack: DaskImageDataStack | None + self.image_stack: DaskImageDataStack @property def path(self) -> Path | None: @@ -159,9 +166,8 @@ 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)) - def _handle_image_changed_in_list(self, - image_files: list[Image_Data], - dask_image_stack: DaskImageDataStack | None = None) -> None: + def _handle_image_changed_in_list(self, image_files: list[Image_Data], + dask_image_stack: DaskImageDataStack) -> None: """ Handle an image changed event. Update the image in the view. This method is called when the image_watcher detects a change @@ -243,7 +249,7 @@ def find_images(self, directory: Path) -> list[Image_Data]: for file_path in directory.iterdir(): if self._is_image_file(file_path.name): try: - image_obj = Image_Data(file_path, create_delayed_array=self.create_delayed_array) + image_obj = Image_Data(file_path) image_files.append(image_obj) except FileNotFoundError: continue @@ -315,7 +321,7 @@ def _handle_directory_change(self) -> None: break images = self.sort_images_by_modified_time(images) - dask_image_stack = DaskImageDataStack(images) + dask_image_stack = DaskImageDataStack(images, create_delayed_array=self.create_delayed_array) self.update_recent_watcher(images[-1:]) self.image_changed.emit(images, dask_image_stack) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 0f5ce32114d..97d8f1af175 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 +import dask.array from imagecodecs._deflate import DeflateError @@ -40,6 +41,8 @@ def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView): self.main_window = main_window self.model = LiveViewerWindowModel(self) self.selected_image: Image_Data | None = None + self.selected_delayed_image: dask.array.Array + self.filters = {f.filter_name: f for f in load_filter_packages()} def close(self) -> None: @@ -74,20 +77,21 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: self.view.set_load_as_dataset_enabled(True) def select_image(self, index: int) -> None: - if not self.model.images: + self.selected_image = self.model.image_stack.get_image_data(index) + self.selected_delayed_image = self.model.image_stack.get_delayed_image(index) + if not self.selected_image: return - self.selected_image = self.model.images[index] 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) + self.display_image(self.selected_image, self.selected_delayed_image) - def display_image(self, image_data_obj: Image_Data) -> None: + def display_image(self, image_data_obj: Image_Data, delayed_image: dask.array.Array) -> None: """ Display image in the view after validating contents """ try: - image_data = self.load_image(image_data_obj) + image_data = self.load_image(delayed_image) except (OSError, KeyError, ValueError, DeflateError) as error: message = f"{type(error).__name__} reading image: {image_data_obj.image_path}: {error}" logger.error(message) @@ -105,13 +109,12 @@ def display_image(self, image_data_obj: Image_Data) -> None: self.view.live_viewer.show_error(None) @staticmethod - def load_image(image_data_obj: Image_Data) -> np.ndarray: + def load_image(delayed_image: dask.array.Array) -> np.ndarray: """ Load a .Tif, .Tiff or .Fits file only if it exists and returns as an ndarray """ - if image_data_obj.image_path.suffix.lower() in [".tif", ".tiff", ".fits"]: - image_data = image_data_obj.delayed_array.compute() + image_data = delayed_image.compute() return image_data def update_image_modified(self, image_path: Path) -> None: @@ -119,14 +122,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(self.selected_image) + self.display_image(self.selected_image, self.selected_delayed_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) + self.display_image(self.selected_image, self.selected_delayed_image) def convert_image_to_imagestack(self, image_data) -> ImageStack: """ From 9be8efe58d911392ce17087e32b79aa52acda391 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 13 Aug 2024 10:12:43 +0100 Subject: [PATCH 11/46] eyes test fixes --- mantidimaging/gui/windows/live_viewer/model.py | 11 ++++++----- mantidimaging/gui/windows/live_viewer/presenter.py | 11 +++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 675fc760cdf..1f7cc480f43 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -24,8 +24,9 @@ class DaskImageDataStack: """ A Dask Image Data Stack Class to hold a delayed array of all the images in the Live Viewer Path """ - delayed_stack: dask.array.Array + delayed_stack: dask.array.Array | None = None image_list: list[Image_Data] + create_delayed_array: bool def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = True): self.image_list = image_list @@ -53,11 +54,11 @@ def get_delayed_arrays(self) -> list[dask.array.Array] | None: else: return None - def get_delayed_image(self, index) -> dask.array.Array: - return self.delayed_stack[index] + def get_delayed_image(self, index: int) -> dask.array.Array | None: + return self.delayed_stack[index] if self.delayed_stack else None - def get_image_data(self, index) -> Image_Data: - return self.image_list[index] + def get_image_data(self, index: int) -> Image_Data | None: + return self.image_list[index] if self.image_list else None class Image_Data: diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 97d8f1af175..5249ca0e51b 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -41,7 +41,7 @@ def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView): self.main_window = main_window self.model = LiveViewerWindowModel(self) self.selected_image: Image_Data | None = None - self.selected_delayed_image: dask.array.Array + self.selected_delayed_image: dask.array.Array | None self.filters = {f.filter_name: f for f in load_filter_packages()} @@ -86,7 +86,7 @@ def select_image(self, index: int) -> None: self.display_image(self.selected_image, self.selected_delayed_image) - def display_image(self, image_data_obj: Image_Data, delayed_image: dask.array.Array) -> None: + def display_image(self, image_data_obj: Image_Data, delayed_image: dask.array.Array | None) -> None: """ Display image in the view after validating contents """ @@ -109,12 +109,15 @@ def display_image(self, image_data_obj: Image_Data, delayed_image: dask.array.Ar self.view.live_viewer.show_error(None) @staticmethod - def load_image(delayed_image: dask.array.Array) -> np.ndarray: + def load_image(delayed_image: dask.array.Array | None) -> np.ndarray: """ Load a .Tif, .Tiff or .Fits file only if it exists and returns as an ndarray """ - image_data = delayed_image.compute() + if delayed_image: + image_data = delayed_image.compute() + else: + raise ValueError return image_data def update_image_modified(self, image_path: Path) -> None: From 93a146db889ade3bcb39da853a2fa579aeec9aac Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 13 Aug 2024 15:56:11 +0100 Subject: [PATCH 12/46] DaskImageDataStackTest created --- .../gui/windows/live_viewer/model.py | 5 ++-- .../windows/live_viewer/test/model_test.py | 30 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 1f7cc480f43..a93fe54cc0a 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -30,6 +30,7 @@ class DaskImageDataStack: def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = True): self.image_list = image_list + self.create_delayed_array = create_delayed_array if image_list and create_delayed_array: arrays = self.get_delayed_arrays() @@ -47,9 +48,9 @@ def shape(self): return self.delayed_stack.shape def get_delayed_arrays(self) -> list[dask.array.Array] | None: - if self.image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: + if self.image_list[0].image_path.suffix.lower() in [".tif", ".tiff"] and self.create_delayed_array: return [dask_image.imread.imread(image_data.image_path)[0] for image_data in self.image_list] - elif self.image_list[0].image_path.suffix.lower() == ".fits": + elif self.image_list[0].image_path.suffix.lower() == ".fits" and self.create_delayed_array: return [dask.delayed(fits.open)(image_data.image_path)[0].data for image_data in self.image_list] else: return None diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 99c7c35932f..ba165da3394 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -9,7 +9,7 @@ from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal -from mantidimaging.gui.windows.live_viewer.model import ImageWatcher +from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, DaskImageDataStack, Image_Data from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase @@ -29,7 +29,6 @@ def setUp(self) -> None: self.watcher.create_delayed_array = False self.mock_signal_image = mock.create_autospec(pyqtSignal, emit=mock.Mock()) self.watcher.image_changed = self.mock_signal_image - self.watcher.create_delayed_array = False def _make_simple_dir(self, directory: Path, t0: float = 1000): file_list = [directory / f"abc_{i:06d}.tif" for i in range(5)] @@ -161,3 +160,30 @@ 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 DaskImageDataStackTest(FakeFSTestCase): + + def setUp(self): + super().setUp() + self.top_path = Path("/live") + file_list = self._make_simple_dir(self.top_path) + self.image_data_list = [Image_Data(path) for path in file_list] + + def _make_simple_dir(self, directory: Path, t0: float = 1000): + file_list = [directory / f"abc_{i:06d}.tif" for i in range(5)] + if not directory.exists(): + self.fs.create_dir(directory) + os.utime(directory, (10, t0)) + n = 1 + for file in file_list: + self.fs.create_file(file) + os.utime(file, (10, t0 + n)) + n += 1 + + return file_list + + def test_WHEN_not_create_delayed_array_THEN_no_delayed_array_created(self): + self.delayed_image_stack = DaskImageDataStack(self.image_data_list, create_delayed_array=False) + delayed_array = self.delayed_image_stack.get_delayed_arrays() + self.assertIsNone(delayed_array) From ed192d1c8f6d9d665134a0f71cb822c460ffe9b8 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 13 Aug 2024 17:23:55 +0100 Subject: [PATCH 13/46] test_WHEN_create_delayed_array_THEN_delayed_array_created --- .../windows/live_viewer/test/model_test.py | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index ba165da3394..bcb59633a75 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -4,9 +4,12 @@ import os import time +import unittest from pathlib import Path from unittest import mock +from numpy.testing import assert_array_equal +import dask.array.random from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, DaskImageDataStack, Image_Data @@ -162,28 +165,26 @@ def test_WHEN_sub_directory_change_THEN_images_emitted(self, _mock_time): self._file_list_count_equal(emitted_images, file_list2) -class DaskImageDataStackTest(FakeFSTestCase): +class DaskImageDataStackTest(unittest.TestCase): - def setUp(self): + @mock.patch("mantidimaging.gui.windows.live_viewer.model.Path.stat", side_effect=None) + def setUp(self, _): super().setUp() - self.top_path = Path("/live") - file_list = self._make_simple_dir(self.top_path) + file_list = [Path(f"abc_{i:06d}.tif") for i in range(5)] self.image_data_list = [Image_Data(path) for path in file_list] - - def _make_simple_dir(self, directory: Path, t0: float = 1000): - file_list = [directory / f"abc_{i:06d}.tif" for i in range(5)] - if not directory.exists(): - self.fs.create_dir(directory) - os.utime(directory, (10, t0)) - n = 1 - for file in file_list: - self.fs.create_file(file) - os.utime(file, (10, t0 + n)) - n += 1 - - return file_list + self.fake_data_array_list = [dask.array.random.random(5) for _ in self.image_data_list] + self.fake_data_stack = dask.array.stack(self.fake_data_array_list) def test_WHEN_not_create_delayed_array_THEN_no_delayed_array_created(self): self.delayed_image_stack = DaskImageDataStack(self.image_data_list, create_delayed_array=False) - delayed_array = self.delayed_image_stack.get_delayed_arrays() - self.assertIsNone(delayed_array) + self.assertIsNone(self.delayed_image_stack.get_delayed_arrays()) + self.assertIsNone(self.delayed_image_stack.delayed_stack) + self.assertEqual(self.delayed_image_stack.image_list, self.image_data_list) + + @mock.patch("mantidimaging.gui.windows.live_viewer.model.DaskImageDataStack.get_delayed_arrays") + def test_WHEN_create_delayed_array_THEN_delayed_array_created(self, mock_delayed_arrays): + mock_delayed_arrays.return_value = self.fake_data_array_list + self.delayed_image_stack = DaskImageDataStack(self.image_data_list, create_delayed_array=True) + assert_array_equal(self.delayed_image_stack.delayed_stack, self.fake_data_stack) + assert_array_equal(self.delayed_image_stack.delayed_stack.compute(), self.fake_data_stack.compute()) + From 22a35340b54f97147a359aba1beafec82907a400 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 14 Aug 2024 10:02:40 +0100 Subject: [PATCH 14/46] create _get_fake_data() to test tif and fits files --- .../windows/live_viewer/test/model_test.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index bcb59633a75..1e917a3f935 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -167,24 +167,25 @@ def test_WHEN_sub_directory_change_THEN_images_emitted(self, _mock_time): class DaskImageDataStackTest(unittest.TestCase): - @mock.patch("mantidimaging.gui.windows.live_viewer.model.Path.stat", side_effect=None) - def setUp(self, _): - super().setUp() - file_list = [Path(f"abc_{i:06d}.tif") for i in range(5)] - self.image_data_list = [Image_Data(path) for path in file_list] - self.fake_data_array_list = [dask.array.random.random(5) for _ in self.image_data_list] - self.fake_data_stack = dask.array.stack(self.fake_data_array_list) + def _get_fake_data(self, ext: str): + file_list = [Path(f"abc_{i:06d}" + ext) for i in range(5)] + with mock.patch("mantidimaging.gui.windows.live_viewer.model.Path.stat"): + image_data_list = [Image_Data(path) for path in file_list] + fake_data_array_list = [dask.array.random.random(5) for _ in image_data_list] + fake_data_stack = dask.array.stack(fake_data_array_list) + return image_data_list, fake_data_array_list, fake_data_stack def test_WHEN_not_create_delayed_array_THEN_no_delayed_array_created(self): - self.delayed_image_stack = DaskImageDataStack(self.image_data_list, create_delayed_array=False) + image_data_list, _, _ = self._get_fake_data('.tif') + self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=False) self.assertIsNone(self.delayed_image_stack.get_delayed_arrays()) self.assertIsNone(self.delayed_image_stack.delayed_stack) - self.assertEqual(self.delayed_image_stack.image_list, self.image_data_list) + self.assertEqual(self.delayed_image_stack.image_list, image_data_list) @mock.patch("mantidimaging.gui.windows.live_viewer.model.DaskImageDataStack.get_delayed_arrays") def test_WHEN_create_delayed_array_THEN_delayed_array_created(self, mock_delayed_arrays): - mock_delayed_arrays.return_value = self.fake_data_array_list - self.delayed_image_stack = DaskImageDataStack(self.image_data_list, create_delayed_array=True) - assert_array_equal(self.delayed_image_stack.delayed_stack, self.fake_data_stack) - assert_array_equal(self.delayed_image_stack.delayed_stack.compute(), self.fake_data_stack.compute()) - + image_data_list, fake_data_array_list, fake_data_stack = self._get_fake_data(".tif") + mock_delayed_arrays.return_value = fake_data_array_list + self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=True) + assert_array_equal(self.delayed_image_stack.delayed_stack, fake_data_stack) + assert_array_equal(self.delayed_image_stack.delayed_stack.compute(), fake_data_stack.compute()) From 96ed7591e6eff57a29760bdd757f2261cfa5849a Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 14 Aug 2024 13:26:09 +0100 Subject: [PATCH 15/46] test tif files handled correctly --- mantidimaging/gui/windows/live_viewer/test/model_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 1e917a3f935..11c11f3da53 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -189,3 +189,10 @@ def test_WHEN_create_delayed_array_THEN_delayed_array_created(self, mock_delayed self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=True) assert_array_equal(self.delayed_image_stack.delayed_stack, fake_data_stack) assert_array_equal(self.delayed_image_stack.delayed_stack.compute(), fake_data_stack.compute()) + + @mock.patch("mantidimaging.gui.windows.live_viewer.model.dask_image.imread.imread") + def test_WHEN_tif_file_THEN_dask_image_imread_called(self, mock_imread): + image_data_list, _, _ = self._get_fake_data('.tif') + calls = [mock.call(image.image_path) for image in image_data_list] + self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=True) + mock_imread.assert_has_calls(calls, any_order=True) From ff47f2d5fd83840c62c9d857a46da217e4d90017 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 14 Aug 2024 15:27:47 +0100 Subject: [PATCH 16/46] testing unsupported files --- .../windows/live_viewer/test/model_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 11c11f3da53..4eec5590537 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -8,6 +8,7 @@ from pathlib import Path from unittest import mock from numpy.testing import assert_array_equal +import numpy as np import dask.array.random from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal @@ -196,3 +197,21 @@ def test_WHEN_tif_file_THEN_dask_image_imread_called(self, mock_imread): calls = [mock.call(image.image_path) for image in image_data_list] self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=True) mock_imread.assert_has_calls(calls, any_order=True) + + @mock.patch("mantidimaging.gui.windows.live_viewer.model.dask.delayed") + @mock.patch("mantidimaging.gui.windows.live_viewer.model.DaskImageDataStack.get_fits_sample") + def test_WHEN_fits_file_THEN_dask_delayed_called(self, mock_fits_sample, mock_dask_delayed): + mock_fits_sample.return_value = np.random.random(5) + image_data_list, _, _ = self._get_fake_data('.fits') + calls = [mock.call()(image.image_path) for image in image_data_list] + with mock.patch("mantidimaging.gui.windows.live_viewer.model.fits.open"): + self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=True) + mock_dask_delayed.assert_has_calls(calls, any_order=True) + + @mock.patch("mantidimaging.gui.windows.live_viewer.model.DaskImageDataStack.get_delayed_arrays") + def test_WHEN_unsupported_file_THEN_raises_error(self, mock_delayed_arrays): + image_data_list, fake_data_array_list, _ = self._get_fake_data(".jpeg") + mock_delayed_arrays.return_value = fake_data_array_list + with self.assertRaises(NotImplementedError): + self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=True) + From aeafeb99d7f7833a36662c9d225a69a1e0f1b16f Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 14 Aug 2024 15:59:01 +0100 Subject: [PATCH 17/46] testing supported files --- .../gui/windows/live_viewer/test/model_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 4eec5590537..2751dc800be 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -12,6 +12,7 @@ import dask.array.random from PyQt5.QtCore import QFileSystemWatcher, pyqtSignal +from parameterized import parameterized from mantidimaging.gui.windows.live_viewer.model import ImageWatcher, DaskImageDataStack, Image_Data from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase @@ -215,3 +216,17 @@ def test_WHEN_unsupported_file_THEN_raises_error(self, mock_delayed_arrays): with self.assertRaises(NotImplementedError): self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=True) + @parameterized.expand([".tif", ".tiff", ".fits"]) + @mock.patch("mantidimaging.gui.windows.live_viewer.model.DaskImageDataStack.get_delayed_arrays") + @mock.patch("mantidimaging.gui.windows.live_viewer.model.dask.array.from_delayed") + @mock.patch("mantidimaging.gui.windows.live_viewer.model.dask.delayed") + @mock.patch("mantidimaging.gui.windows.live_viewer.model.DaskImageDataStack.get_fits_sample") + def test_WHEN_supported_file_THEN_no_error_raised(self, file_ext, mock_fits_sample, _, mock_from_delayed, mock_delayed_arrays): + mock_fits_sample.return_value = np.random.random(5) + image_data_list, fake_data_array_list, _ = self._get_fake_data(file_ext) + mock_delayed_arrays.return_value = fake_data_array_list + mock_from_delayed.return_value = fake_data_array_list + try: + self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=True) + except NotImplementedError: + self.fail("DaskImageDataStack raised NotImplementedError unexpectedly!") From ccbe5952d4bf1ff582aefcdce34c2c88c91a5f89 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 14 Aug 2024 16:10:14 +0100 Subject: [PATCH 18/46] minor refactoring and using NotImplimentedError to check supported files --- mantidimaging/gui/windows/live_viewer/model.py | 12 +++++++++--- mantidimaging/gui/windows/live_viewer/presenter.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index a93fe54cc0a..4db9b6386e5 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -8,6 +8,7 @@ from logging import getLogger import dask.array +import numpy as np from PyQt5.QtCore import QFileSystemWatcher, QObject, pyqtSignal, QTimer import dask_image.imread @@ -38,10 +39,11 @@ def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = Tr if image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: self.delayed_stack = dask.array.stack(dask.array.array(arrays)) elif image_list[0].image_path.suffix.lower() in [".fits"]: - with fits.open(image_list[0].image_path.__str__()) as fit: - sample = fit[0].data + sample = self.get_fits_sample(image_list[0]) lazy_arrays = [dask.array.from_delayed(x, shape=sample.shape, dtype=sample.dtype) for x in arrays] self.delayed_stack = dask.array.stack(lazy_arrays) + else: + raise NotImplementedError(f"DaskImageDataStack does not support image with extension {image_list[0].image_path.suffix.lower()}") @property def shape(self): @@ -56,11 +58,15 @@ def get_delayed_arrays(self) -> list[dask.array.Array] | None: return None def get_delayed_image(self, index: int) -> dask.array.Array | None: - return self.delayed_stack[index] if self.delayed_stack else None + return self.delayed_stack[index] if self.delayed_stack is not None else None def get_image_data(self, index: int) -> Image_Data | None: return self.image_list[index] if self.image_list else None + def get_fits_sample(self, image_data: Image_Data) -> np.ndarray: + with fits.open(image_data.image_path.__str__()) as fit: + return fit[0].data + class Image_Data: """ diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 5249ca0e51b..4ff34172957 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -114,7 +114,7 @@ def load_image(delayed_image: dask.array.Array | None) -> np.ndarray: Load a .Tif, .Tiff or .Fits file only if it exists and returns as an ndarray """ - if delayed_image: + if delayed_image is not None: image_data = delayed_image.compute() else: raise ValueError From ab1aa5552a19c59256e56da417c684df9387f77d Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 14 Aug 2024 17:31:45 +0100 Subject: [PATCH 19/46] yapf and ruff fixes --- mantidimaging/gui/windows/live_viewer/model.py | 3 ++- .../gui/windows/live_viewer/test/model_test.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 4db9b6386e5..ada244bad73 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -43,7 +43,8 @@ def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = Tr lazy_arrays = [dask.array.from_delayed(x, shape=sample.shape, dtype=sample.dtype) for x in arrays] self.delayed_stack = dask.array.stack(lazy_arrays) else: - raise NotImplementedError(f"DaskImageDataStack does not support image with extension {image_list[0].image_path.suffix.lower()}") + raise NotImplementedError(f"DaskImageDataStack does not support image with extension " + f"{image_list[0].image_path.suffix.lower()}") @property def shape(self): diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 2751dc800be..88847753bac 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -169,6 +169,9 @@ def test_WHEN_sub_directory_change_THEN_images_emitted(self, _mock_time): class DaskImageDataStackTest(unittest.TestCase): + def setUp(self): + self.test_array = np.array([1, 3, 5, 12, 15]) + def _get_fake_data(self, ext: str): file_list = [Path(f"abc_{i:06d}" + ext) for i in range(5)] with mock.patch("mantidimaging.gui.windows.live_viewer.model.Path.stat"): @@ -202,7 +205,7 @@ def test_WHEN_tif_file_THEN_dask_image_imread_called(self, mock_imread): @mock.patch("mantidimaging.gui.windows.live_viewer.model.dask.delayed") @mock.patch("mantidimaging.gui.windows.live_viewer.model.DaskImageDataStack.get_fits_sample") def test_WHEN_fits_file_THEN_dask_delayed_called(self, mock_fits_sample, mock_dask_delayed): - mock_fits_sample.return_value = np.random.random(5) + mock_fits_sample.return_value = self.test_array image_data_list, _, _ = self._get_fake_data('.fits') calls = [mock.call()(image.image_path) for image in image_data_list] with mock.patch("mantidimaging.gui.windows.live_viewer.model.fits.open"): @@ -221,8 +224,9 @@ def test_WHEN_unsupported_file_THEN_raises_error(self, mock_delayed_arrays): @mock.patch("mantidimaging.gui.windows.live_viewer.model.dask.array.from_delayed") @mock.patch("mantidimaging.gui.windows.live_viewer.model.dask.delayed") @mock.patch("mantidimaging.gui.windows.live_viewer.model.DaskImageDataStack.get_fits_sample") - def test_WHEN_supported_file_THEN_no_error_raised(self, file_ext, mock_fits_sample, _, mock_from_delayed, mock_delayed_arrays): - mock_fits_sample.return_value = np.random.random(5) + def test_WHEN_supported_file_THEN_no_error_raised(self, file_ext, mock_fits_sample, _, mock_from_delayed, + mock_delayed_arrays): + mock_fits_sample.return_value = self.test_array image_data_list, fake_data_array_list, _ = self._get_fake_data(file_ext) mock_delayed_arrays.return_value = fake_data_array_list mock_from_delayed.return_value = fake_data_array_list From 47acbf33a2e0b4ff3a76e891282eef62ff5a35a3 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 15 Aug 2024 12:42:12 +0100 Subject: [PATCH 20/46] release note --- docs/release_notes/next/dev-2311-dask-live-viewer | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/release_notes/next/dev-2311-dask-live-viewer 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 From 161dd1ad3a236c2a2ae95208eed1a20bb7eab58c Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 16 Aug 2024 14:08:08 +0100 Subject: [PATCH 21/46] setting create_delayed_array to false falls back to loading into memory --- .../gui/windows/live_viewer/model.py | 15 +++++- .../gui/windows/live_viewer/presenter.py | 46 +++++++++++++------ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index ada244bad73..e3e65d9de06 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -14,6 +14,8 @@ import dask_image.imread from astropy.io import fits +from mantidimaging.core.utility import ExecutionProfiler + if TYPE_CHECKING: from os import stat_result from mantidimaging.gui.windows.live_viewer.view import LiveViewerWindowPresenter @@ -28,6 +30,7 @@ class DaskImageDataStack: delayed_stack: dask.array.Array | None = None image_list: list[Image_Data] create_delayed_array: bool + _selected_index: int def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = True): self.image_list = image_list @@ -50,6 +53,14 @@ def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = Tr def shape(self): return self.delayed_stack.shape + @property + def selected_index(self): + return self._selected_index + + @selected_index.setter + def selected_index(self, index): + self._selected_index = index + def get_delayed_arrays(self) -> list[dask.array.Array] | None: if self.image_list[0].image_path.suffix.lower() in [".tif", ".tiff"] and self.create_delayed_array: return [dask_image.imread.imread(image_data.image_path)[0] for image_data in self.image_list] @@ -58,8 +69,8 @@ def get_delayed_arrays(self) -> list[dask.array.Array] | None: else: return None - def get_delayed_image(self, index: int) -> dask.array.Array | None: - return self.delayed_stack[index] if self.delayed_stack is not None else None + def get_delayed_image(self, index: int) -> dask.array.Array: + return self.delayed_stack[index] def get_image_data(self, index: int) -> Image_Data | None: return self.image_list[index] if self.image_list else None diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 4ff34172957..8bec70eb21b 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -10,9 +10,11 @@ import dask.array from imagecodecs._deflate import DeflateError +from tifffile import tifffile +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, DaskImageDataStack from mantidimaging.core.operations.loader import load_filter_packages from mantidimaging.core.data import ImageStack @@ -33,6 +35,7 @@ class LiveViewerWindowPresenter(BasePresenter): view: LiveViewerWindowView model: LiveViewerWindowModel op_func: Callable + image_stack: DaskImageDataStack def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView): super().__init__(view) @@ -77,21 +80,25 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: self.view.set_load_as_dataset_enabled(True) def select_image(self, index: int) -> None: - self.selected_image = self.model.image_stack.get_image_data(index) - self.selected_delayed_image = self.model.image_stack.get_delayed_image(index) + self.selected_image = self.model.images[index] + self.image_stack = self.model.image_stack + self.image_stack.selected_index = 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, self.selected_delayed_image) + self.display_image(self.selected_image, self.image_stack) - def display_image(self, image_data_obj: Image_Data, delayed_image: dask.array.Array | None) -> None: + def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskImageDataStack | None) -> None: """ Display image in the view after validating contents """ try: - image_data = self.load_image(delayed_image) + if delayed_image_stack is None or not delayed_image_stack.create_delayed_array: + image_data = self.load_image_from_path(image_data_obj.image_path) + else: + image_data = self.load_image_from_delayed_stack(delayed_image_stack) except (OSError, KeyError, ValueError, DeflateError) as error: message = f"{type(error).__name__} reading image: {image_data_obj.image_path}: {error}" logger.error(message) @@ -109,30 +116,43 @@ def display_image(self, image_data_obj: Image_Data, delayed_image: dask.array.Ar self.view.live_viewer.show_error(None) @staticmethod - def load_image(delayed_image: dask.array.Array | None) -> np.ndarray: + def load_image_from_delayed_stack(delayed_image_stack: DaskImageDataStack | None) -> np.ndarray: """ - Load a .Tif, .Tiff or .Fits file only if it exists - and returns as an ndarray + Load a delayed stack from a DaskImageDataStack and compute """ - if delayed_image is not None: - image_data = delayed_image.compute() + if delayed_image_stack is not None and delayed_image_stack.delayed_stack is not None: + image_data = delayed_image_stack.get_delayed_image(delayed_image_stack.selected_index).compute() else: raise ValueError return image_data + @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 + 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(self.selected_image, self.selected_delayed_image) + self.display_image(self.selected_image, self.image_stack) 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, self.selected_delayed_image) + self.display_image(self.selected_image, self.image_stack) def convert_image_to_imagestack(self, image_data) -> ImageStack: """ From 50997108ee4ceb00ef01190fa09821b8682fa6f2 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 16 Aug 2024 14:25:39 +0100 Subject: [PATCH 22/46] ruff fix --- mantidimaging/gui/windows/live_viewer/model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index e3e65d9de06..04223763239 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -14,8 +14,6 @@ import dask_image.imread from astropy.io import fits -from mantidimaging.core.utility import ExecutionProfiler - if TYPE_CHECKING: from os import stat_result from mantidimaging.gui.windows.live_viewer.view import LiveViewerWindowPresenter From b80b90562d1450b388fbef6e4817de27c23b1fb1 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 16 Aug 2024 15:09:00 +0100 Subject: [PATCH 23/46] mypy fixes --- mantidimaging/gui/windows/live_viewer/model.py | 4 ++-- mantidimaging/gui/windows/live_viewer/presenter.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 04223763239..3e80ff35c52 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -67,8 +67,8 @@ def get_delayed_arrays(self) -> list[dask.array.Array] | None: else: return None - def get_delayed_image(self, index: int) -> dask.array.Array: - return self.delayed_stack[index] + def get_delayed_image(self, index: int) -> dask.array.Array | None: + return self.delayed_stack[index] if self.delayed_stack else None def get_image_data(self, index: int) -> Image_Data | None: return self.image_list[index] if self.image_list else None diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 8bec70eb21b..dc596237681 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -120,8 +120,10 @@ def load_image_from_delayed_stack(delayed_image_stack: DaskImageDataStack | None """ Load a delayed stack from a DaskImageDataStack and compute """ - if delayed_image_stack is not None and delayed_image_stack.delayed_stack is not None: - image_data = delayed_image_stack.get_delayed_image(delayed_image_stack.selected_index).compute() + if delayed_image_stack is not None: + delayed_image = delayed_image_stack.get_delayed_image(delayed_image_stack.selected_index) + if delayed_image is not None: + image_data = delayed_image.compute() else: raise ValueError return image_data From b0876c9997bc0e30a24d779d6e636a61f80c6cc9 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 16 Aug 2024 16:36:26 +0100 Subject: [PATCH 24/46] eyes_tests_fix --- mantidimaging/eyes_tests/live_viewer_window_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mantidimaging/eyes_tests/live_viewer_window_test.py b/mantidimaging/eyes_tests/live_viewer_window_test.py index c41b529deba..b43eb70db62 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) - @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image') + @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.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): @@ -68,7 +68,7 @@ def test_live_view_opens_with_data(self, _mock_time, _mock_image_watcher, mock_l self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list, dask_image_stack) self.check_target(widget=self.imaging.live_viewer) - @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image') + @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.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): @@ -80,7 +80,7 @@ def test_live_view_opens_with_bad_data(self, _mock_time, _mock_image_watcher, mo self.imaging.live_viewer.presenter.model._handle_image_changed_in_list(image_list, dask_image_stack) self.check_target(widget=self.imaging.live_viewer) - @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image') + @mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.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): From b01379796968f4915c8050f255ebf1dcd477d754 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 19 Aug 2024 14:14:53 +0100 Subject: [PATCH 25/46] Dask array now handles current image being deleted --- mantidimaging/gui/windows/live_viewer/model.py | 14 ++++++++++++-- mantidimaging/gui/windows/live_viewer/presenter.py | 10 +++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 3e80ff35c52..a5c3686e947 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -68,7 +68,7 @@ def get_delayed_arrays(self) -> list[dask.array.Array] | None: return None def get_delayed_image(self, index: int) -> dask.array.Array | None: - return self.delayed_stack[index] if self.delayed_stack else None + return self.delayed_stack[index] if self.delayed_stack is not None else None def get_image_data(self, index: int) -> Image_Data | None: return self.image_list[index] if self.image_list else None @@ -77,6 +77,16 @@ def get_fits_sample(self, image_data: Image_Data) -> np.ndarray: with fits.open(image_data.image_path.__str__()) as fit: return fit[0].data + def remove_image_data_by_path(self, image_path: Path) -> None: + image_paths = [image.image_path for image in self.image_list] + index_to_remove = image_paths.index(image_path) + self.image_list.pop(index_to_remove) + dask.array.delete(self.delayed_stack, index_to_remove, 0) + if index_to_remove == self.selected_index and self.selected_index > 0: + self.selected_index = self.selected_index - 1 + else: + self.selected_index = 0 + self.delayed_stack = None class Image_Data: """ @@ -198,6 +208,7 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data], self.presenter.update_image_list(image_files) def handle_image_modified(self, image_path: Path): + self.image_stack.remove_image_data_by_path(image_path) self.presenter.update_image_modified(image_path) def close(self) -> None: @@ -337,7 +348,6 @@ def _handle_directory_change(self) -> None: if len(images) > 0: break - images = self.sort_images_by_modified_time(images) dask_image_stack = DaskImageDataStack(images, create_delayed_array=self.create_delayed_array) self.update_recent_watcher(images[-1:]) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index dc596237681..7edb89b3616 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -81,14 +81,13 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: def select_image(self, index: int) -> None: self.selected_image = self.model.images[index] - self.image_stack = self.model.image_stack - self.image_stack.selected_index = index + self.model.image_stack.selected_index = 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, self.image_stack) + self.display_image(self.selected_image, self.model.image_stack) def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskImageDataStack | None) -> None: """ @@ -120,6 +119,7 @@ def load_image_from_delayed_stack(delayed_image_stack: DaskImageDataStack | None """ Load a delayed stack from a DaskImageDataStack and compute """ + delayed_image = None if delayed_image_stack is not None: delayed_image = delayed_image_stack.get_delayed_image(delayed_image_stack.selected_index) if delayed_image is not None: @@ -147,14 +147,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(self.selected_image, self.image_stack) + self.display_image(self.selected_image, self.model.image_stack) 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, self.image_stack) + self.display_image(self.selected_image, self.model.image_stack) def convert_image_to_imagestack(self, image_data) -> ImageStack: """ From 4c01e01a8560395dc2b40170a45c91f79a0d02f7 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 19 Aug 2024 14:55:52 +0100 Subject: [PATCH 26/46] yapf fix --- mantidimaging/gui/windows/live_viewer/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index a5c3686e947..d35ead31f14 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -88,6 +88,7 @@ def remove_image_data_by_path(self, image_path: Path) -> None: self.selected_index = 0 self.delayed_stack = None + class Image_Data: """ Image Data Class to store represent image data. From 7593ea5ad9641d843b2fc7d486c4134fe58e0c06 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 20 Aug 2024 21:36:13 +0100 Subject: [PATCH 27/46] Dask Stack now copes with file deletion --- .../gui/windows/live_viewer/model.py | 145 ++++++++++++++---- .../gui/windows/live_viewer/presenter.py | 32 ++-- 2 files changed, 140 insertions(+), 37 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index d35ead31f14..71984219135 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -14,6 +14,8 @@ import dask_image.imread from astropy.io import fits +from mantidimaging.core.utility import ExecutionProfiler + if TYPE_CHECKING: from os import stat_result from mantidimaging.gui.windows.live_viewer.view import LiveViewerWindowPresenter @@ -35,17 +37,7 @@ def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = Tr self.create_delayed_array = create_delayed_array if image_list and create_delayed_array: - arrays = self.get_delayed_arrays() - if arrays: - if image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: - self.delayed_stack = dask.array.stack(dask.array.array(arrays)) - elif image_list[0].image_path.suffix.lower() in [".fits"]: - sample = self.get_fits_sample(image_list[0]) - lazy_arrays = [dask.array.from_delayed(x, shape=sample.shape, dtype=sample.dtype) for x in arrays] - self.delayed_stack = dask.array.stack(lazy_arrays) - else: - raise NotImplementedError(f"DaskImageDataStack does not support image with extension " - f"{image_list[0].image_path.suffix.lower()}") + self.delayed_stack = self.create_delayed_stack_from_image_data(image_list) @property def shape(self): @@ -59,13 +51,14 @@ def selected_index(self): def selected_index(self, index): self._selected_index = index - def get_delayed_arrays(self) -> list[dask.array.Array] | None: - if self.image_list[0].image_path.suffix.lower() in [".tif", ".tiff"] and self.create_delayed_array: - return [dask_image.imread.imread(image_data.image_path)[0] for image_data in self.image_list] - elif self.image_list[0].image_path.suffix.lower() == ".fits" and self.create_delayed_array: - return [dask.delayed(fits.open)(image_data.image_path)[0].data for image_data in self.image_list] - else: - return None + def get_delayed_arrays(self, image_list: list[Image_Data]) -> list[dask.array.Array] | None: + if image_list: + if image_list[0].image_path.suffix.lower() in [".tif", ".tiff"] and self.create_delayed_array: + return [dask_image.imread.imread(image_data.image_path)[0] for image_data in image_list] + elif image_list[0].image_path.suffix.lower() == ".fits" and self.create_delayed_array: + return [dask.delayed(fits.open)(image_data.image_path)[0].data for image_data in image_list] + else: + return None def get_delayed_image(self, index: int) -> dask.array.Array | None: return self.delayed_stack[index] if self.delayed_stack is not None else None @@ -77,17 +70,80 @@ def get_fits_sample(self, image_data: Image_Data) -> np.ndarray: with fits.open(image_data.image_path.__str__()) as fit: return fit[0].data + def get_computed_image(self, index: int): + if index < 0: + return None + try: + self.get_delayed_image(index).compute() + print(f"+++++++++++++++++++++++++++++++++++ get_computed_image {index=} ++++++++++++++++++++++++++++++") + print(f"+++++++++++++++++++++++++++++++++++ get_computed_image {self.get_delayed_image(index)=} ++++++++++++++++++++++++++++++") + print( + f"+++++++++++++++++++++++++++++++++++ get_computed_image {self.get_image_data(index).image_path=} ++++++++++++++++++++++++++++++") + except dask_image.imread.pims.api.UnknownFormatError: + print(f"+++++++++++++++++++++++++++++++++++ get_computed_image FAILED {self.get_image_data(index).image_path=}++++++++++++++++++++++++++++++") + self.remove_image_data_by_index(index) + self.get_computed_image(index-1) + except AttributeError: + return None + return self.get_delayed_image(index).compute() + + def get_selected_computed_image(self): + try: + self.get_computed_image(self.selected_index) + except dask_image.imread.pims.api.UnknownFormatError: + pass + return self.get_computed_image(self.selected_index) + def remove_image_data_by_path(self, image_path: Path) -> None: image_paths = [image.image_path for image in self.image_list] index_to_remove = image_paths.index(image_path) + self.remove_image_data_by_index(index_to_remove) + + def remove_image_data_by_index(self, index_to_remove: int) -> None: self.image_list.pop(index_to_remove) - dask.array.delete(self.delayed_stack, index_to_remove, 0) + print(f"KKKKKKKKKKKKK {self.delayed_stack=} KKKKKKKKKKKKKKKKKKKK") + self.delayed_stack = dask.array.delete(self.delayed_stack, index_to_remove, 0) + print(f"VVVVVVVVVVVVVVVVVVVVV {self.delayed_stack=} VVVVVVVVVVVVVVVVVVVVVV") if index_to_remove == self.selected_index and self.selected_index > 0: self.selected_index = self.selected_index - 1 - else: - self.selected_index = 0 + if not self.image_list: self.delayed_stack = None + def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> None | dask.array.Array: + delayed_stack = None + arrays = self.get_delayed_arrays(image_list) + if arrays: + if image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: + delayed_stack = dask.array.stack(dask.array.array(arrays)) + elif image_list[0].image_path.suffix.lower() in [".fits"]: + sample = self.get_fits_sample(image_list[0]) + lazy_arrays = [dask.array.from_delayed(x, shape=sample.shape, dtype=sample.dtype) for x in arrays] + delayed_stack = dask.array.stack(lazy_arrays) + else: + raise NotImplementedError(f"DaskImageDataStack does not support image with extension " + f"{image_list[0].image_path.suffix.lower()}") + return delayed_stack + + def add_images_to_delayed_stack(self, new_image_list: list[Image_Data]) -> None: + if not new_image_list: + return + image_paths = [image.image_path for image in self.image_list] + images_to_add = [image for image in new_image_list if image.image_path not in image_paths] + print(f"============== {images_to_add=} ======================") + if self.delayed_stack is None or dask.array.isnan(self.delayed_stack.shape).any(): + self.delayed_stack = self.create_delayed_stack_from_image_data(new_image_list) + else: + if images_to_add: + self.delayed_stack = dask.array.concatenate([self.delayed_stack, self.get_delayed_arrays(images_to_add)]) + self.image_list.extend(images_to_add) + # self.delayed_stack = dask.array.stack([self.delayed_stack, self.create_delayed_stack_from_image_data(image_list)]) + # self.delayed_stack = dask.array.unique(self.delayed_stack) + + def delete_all_data(self): + self.image_list = [] + self.delayed_stack = None + self.selected_index = 0 + class Image_Data: """ @@ -105,10 +161,7 @@ class Image_Data: size of image file image_modified_time : float last modified time of image file - delayed_array: dask.array.Array - A delayed dask array of the image data """ - delayed_array: dask.array.Array create_delayed_array: bool def __init__(self, image_path: Path): @@ -180,8 +233,8 @@ 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.image_stack: DaskImageDataStack + self._images: list[Image_Data] = [] + self.image_stack: DaskImageDataStack = DaskImageDataStack([]) @property def path(self) -> Path | None: @@ -195,6 +248,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], dask_image_stack: DaskImageDataStack) -> None: """ @@ -204,13 +265,26 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data], :param image_files: list of image files """ + print(f"++++++++++++++++ _handle_image_changed_in_list +++++++++++++++++++++") + print(f"\n{[image.image_path for image in self.image_stack.image_list]=}\n") + print(f"\n{[image.image_path for image in dask_image_stack.image_list]=}\n") + print(f"\n{[image.image_path for image in image_files]=}\n") self.images = image_files self.image_stack = dask_image_stack + # if dask_image_stack.image_list: + # self.image_stack = dask_image_stack self.presenter.update_image_list(image_files) + self.presenter.update_image_stack(self.image_stack) def handle_image_modified(self, image_path: Path): + print(f"++++++++++++++++ handle_image_modified +++++++++++++++++++++") + print(f"\n{[image.image_path for image in self.image_stack.image_list]=}\n") + print(f"\n{image_path=}\n") self.image_stack.remove_image_data_by_path(image_path) + print(f"\n{[image.image_path for image in self.image_stack.image_list]=}\n") + #print(f"\n{[self.image_stack.delayed_stack.shape]=}\n") self.presenter.update_image_modified(image_path) + self.presenter.update_image_stack(self.image_stack) def close(self) -> None: """Close the model.""" @@ -245,6 +319,7 @@ class ImageWatcher(QObject): image_changed = pyqtSignal(list, DaskImageDataStack) # Signal emitted when an image is added or removed recent_image_changed = pyqtSignal(Path) create_delayed_array: bool = True + image_stack = DaskImageDataStack([], create_delayed_array=True) def __init__(self, directory: Path): """ @@ -350,9 +425,23 @@ def _handle_directory_change(self) -> None: if len(images) > 0: break images = self.sort_images_by_modified_time(images) - dask_image_stack = DaskImageDataStack(images, create_delayed_array=self.create_delayed_array) + if len(images) == 0: + self.image_stack.delete_all_data() + self.image_stack.add_images_to_delayed_stack(images) + print(f"\n ================== {len(self.image_stack.image_list)=} =========================\n") + #print(f"\n ================== {self.image_stack.delayed_stack.shape=} =========================\n") + if len(images) % 50 == 0: + with ExecutionProfiler(msg=f"create delayed array and compute mean for {len(images)} images"): + + print(f"\n ================== {self.image_stack.delayed_stack=} =========================\n") + print(f"\n ================== {self.image_stack.image_list=} =========================\n") + print(f"\n ================== {len(self.image_stack.image_list)=} =========================\n") + if self.image_stack.image_list: + arrmean = dask.array.mean(self.image_stack.delayed_stack, axis=(1, 2)) + print(arrmean.compute()) + self.update_recent_watcher(images[-1:]) - self.image_changed.emit(images, dask_image_stack) + self.image_changed.emit(images, self.image_stack) @staticmethod def _is_image_file(file_name: str) -> bool: diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 7edb89b3616..a6bb1034f72 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -6,6 +6,8 @@ from typing import TYPE_CHECKING from collections.abc import Callable from logging import getLogger + +import dask_image.imread import numpy as np import dask.array @@ -80,24 +82,35 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: self.view.set_load_as_dataset_enabled(True) def select_image(self, index: int) -> None: + print(f"OOOOOOOOOOOOO select_image: {index=}, {self.model.images=} 0000000000000000") + if not self.model.images: + self.update_image_list([]) + return self.selected_image = self.model.images[index] - self.model.image_stack.selected_index = index + self.image_stack = self.model.image_stack + self.image_stack.selected_index = 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, self.model.image_stack) + self.display_image(self.selected_image, self.image_stack) def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskImageDataStack | None) -> None: """ Display image in the view after validating contents """ + print(f"\n+++++++++++++++ display_image {delayed_image_stack=} ++++++++++++++++\n") + print(f"\n+++++++++++++++ display_image {delayed_image_stack.delayed_stack=} ++++++++++++++++\n") + #print(f"\n+++++++++++++++ display_image {delayed_image_stack.delayed_stack.shape=} ++++++++++++++++\n") try: - if delayed_image_stack is None or not delayed_image_stack.create_delayed_array: + if delayed_image_stack is None or delayed_image_stack.delayed_stack is None or not delayed_image_stack.create_delayed_array: image_data = self.load_image_from_path(image_data_obj.image_path) else: - image_data = self.load_image_from_delayed_stack(delayed_image_stack) + try: + image_data = self.load_image_from_delayed_stack(delayed_image_stack) + except (AttributeError, dask_image.imread.pims.api.UnknownFormatError): + 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) @@ -121,9 +134,7 @@ def load_image_from_delayed_stack(delayed_image_stack: DaskImageDataStack | None """ delayed_image = None if delayed_image_stack is not None: - delayed_image = delayed_image_stack.get_delayed_image(delayed_image_stack.selected_index) - if delayed_image is not None: - image_data = delayed_image.compute() + image_data = delayed_image_stack.get_selected_computed_image() else: raise ValueError return image_data @@ -147,14 +158,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(self.selected_image, self.model.image_stack) + self.display_image(self.selected_image, self.image_stack) 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, self.model.image_stack) + self.display_image(self.selected_image, self.image_stack) def convert_image_to_imagestack(self, image_data) -> ImageStack: """ @@ -180,3 +191,6 @@ 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_image_stack(self, image_stack: DaskImageDataStack): + self.image_stack = image_stack From 64f388372b77ed63040e78af739e66eac591d278 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 21 Aug 2024 22:01:52 +0100 Subject: [PATCH 28/46] DaskImageDataStack allows images to be added and removed dynamically --- .../gui/windows/live_viewer/model.py | 37 ++----------------- .../gui/windows/live_viewer/presenter.py | 8 +--- 2 files changed, 5 insertions(+), 40 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 71984219135..9a38e7be3a4 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -14,8 +14,6 @@ import dask_image.imread from astropy.io import fits -from mantidimaging.core.utility import ExecutionProfiler - if TYPE_CHECKING: from os import stat_result from mantidimaging.gui.windows.live_viewer.view import LiveViewerWindowPresenter @@ -75,14 +73,9 @@ def get_computed_image(self, index: int): return None try: self.get_delayed_image(index).compute() - print(f"+++++++++++++++++++++++++++++++++++ get_computed_image {index=} ++++++++++++++++++++++++++++++") - print(f"+++++++++++++++++++++++++++++++++++ get_computed_image {self.get_delayed_image(index)=} ++++++++++++++++++++++++++++++") - print( - f"+++++++++++++++++++++++++++++++++++ get_computed_image {self.get_image_data(index).image_path=} ++++++++++++++++++++++++++++++") except dask_image.imread.pims.api.UnknownFormatError: - print(f"+++++++++++++++++++++++++++++++++++ get_computed_image FAILED {self.get_image_data(index).image_path=}++++++++++++++++++++++++++++++") self.remove_image_data_by_index(index) - self.get_computed_image(index-1) + self.get_computed_image(index - 1) except AttributeError: return None return self.get_delayed_image(index).compute() @@ -101,9 +94,7 @@ def remove_image_data_by_path(self, image_path: Path) -> None: def remove_image_data_by_index(self, index_to_remove: int) -> None: self.image_list.pop(index_to_remove) - print(f"KKKKKKKKKKKKK {self.delayed_stack=} KKKKKKKKKKKKKKKKKKKK") self.delayed_stack = dask.array.delete(self.delayed_stack, index_to_remove, 0) - print(f"VVVVVVVVVVVVVVVVVVVVV {self.delayed_stack=} VVVVVVVVVVVVVVVVVVVVVV") if index_to_remove == self.selected_index and self.selected_index > 0: self.selected_index = self.selected_index - 1 if not self.image_list: @@ -129,15 +120,13 @@ def add_images_to_delayed_stack(self, new_image_list: list[Image_Data]) -> None: return image_paths = [image.image_path for image in self.image_list] images_to_add = [image for image in new_image_list if image.image_path not in image_paths] - print(f"============== {images_to_add=} ======================") if self.delayed_stack is None or dask.array.isnan(self.delayed_stack.shape).any(): self.delayed_stack = self.create_delayed_stack_from_image_data(new_image_list) else: if images_to_add: - self.delayed_stack = dask.array.concatenate([self.delayed_stack, self.get_delayed_arrays(images_to_add)]) + self.delayed_stack = dask.array.concatenate( + [self.delayed_stack, self.get_delayed_arrays(images_to_add)]) self.image_list.extend(images_to_add) - # self.delayed_stack = dask.array.stack([self.delayed_stack, self.create_delayed_stack_from_image_data(image_list)]) - # self.delayed_stack = dask.array.unique(self.delayed_stack) def delete_all_data(self): self.image_list = [] @@ -265,10 +254,6 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data], :param image_files: list of image files """ - print(f"++++++++++++++++ _handle_image_changed_in_list +++++++++++++++++++++") - print(f"\n{[image.image_path for image in self.image_stack.image_list]=}\n") - print(f"\n{[image.image_path for image in dask_image_stack.image_list]=}\n") - print(f"\n{[image.image_path for image in image_files]=}\n") self.images = image_files self.image_stack = dask_image_stack # if dask_image_stack.image_list: @@ -277,12 +262,7 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data], self.presenter.update_image_stack(self.image_stack) def handle_image_modified(self, image_path: Path): - print(f"++++++++++++++++ handle_image_modified +++++++++++++++++++++") - print(f"\n{[image.image_path for image in self.image_stack.image_list]=}\n") - print(f"\n{image_path=}\n") self.image_stack.remove_image_data_by_path(image_path) - print(f"\n{[image.image_path for image in self.image_stack.image_list]=}\n") - #print(f"\n{[self.image_stack.delayed_stack.shape]=}\n") self.presenter.update_image_modified(image_path) self.presenter.update_image_stack(self.image_stack) @@ -428,17 +408,6 @@ def _handle_directory_change(self) -> None: if len(images) == 0: self.image_stack.delete_all_data() self.image_stack.add_images_to_delayed_stack(images) - print(f"\n ================== {len(self.image_stack.image_list)=} =========================\n") - #print(f"\n ================== {self.image_stack.delayed_stack.shape=} =========================\n") - if len(images) % 50 == 0: - with ExecutionProfiler(msg=f"create delayed array and compute mean for {len(images)} images"): - - print(f"\n ================== {self.image_stack.delayed_stack=} =========================\n") - print(f"\n ================== {self.image_stack.image_list=} =========================\n") - print(f"\n ================== {len(self.image_stack.image_list)=} =========================\n") - if self.image_stack.image_list: - arrmean = dask.array.mean(self.image_stack.delayed_stack, axis=(1, 2)) - print(arrmean.compute()) self.update_recent_watcher(images[-1:]) self.image_changed.emit(images, self.image_stack) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index a6bb1034f72..97aac477fd8 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -82,7 +82,6 @@ def update_image_list(self, images_list: list[Image_Data]) -> None: self.view.set_load_as_dataset_enabled(True) def select_image(self, index: int) -> None: - print(f"OOOOOOOOOOOOO select_image: {index=}, {self.model.images=} 0000000000000000") if not self.model.images: self.update_image_list([]) return @@ -100,11 +99,9 @@ def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskIma """ Display image in the view after validating contents """ - print(f"\n+++++++++++++++ display_image {delayed_image_stack=} ++++++++++++++++\n") - print(f"\n+++++++++++++++ display_image {delayed_image_stack.delayed_stack=} ++++++++++++++++\n") - #print(f"\n+++++++++++++++ display_image {delayed_image_stack.delayed_stack.shape=} ++++++++++++++++\n") try: - if delayed_image_stack is None or delayed_image_stack.delayed_stack is None or not delayed_image_stack.create_delayed_array: + if (delayed_image_stack is None or delayed_image_stack.delayed_stack is None + or not delayed_image_stack.create_delayed_array): image_data = self.load_image_from_path(image_data_obj.image_path) else: try: @@ -132,7 +129,6 @@ def load_image_from_delayed_stack(delayed_image_stack: DaskImageDataStack | None """ Load a delayed stack from a DaskImageDataStack and compute """ - delayed_image = None if delayed_image_stack is not None: image_data = delayed_image_stack.get_selected_computed_image() else: From 3c18f431ec5e5ab667b90702986bd823028297e3 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 22 Aug 2024 11:57:38 +0100 Subject: [PATCH 29/46] fixes to LV Model test --- mantidimaging/gui/windows/live_viewer/model.py | 12 +++++++++--- .../gui/windows/live_viewer/test/model_test.py | 1 - 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 9a38e7be3a4..8da9210fa36 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -57,6 +57,8 @@ def get_delayed_arrays(self, image_list: list[Image_Data]) -> list[dask.array.Ar return [dask.delayed(fits.open)(image_data.image_path)[0].data for image_data in image_list] else: return None + else: + return None def get_delayed_image(self, index: int) -> dask.array.Array | None: return self.delayed_stack[index] if self.delayed_stack is not None else None @@ -72,13 +74,15 @@ def get_computed_image(self, index: int): if index < 0: return None try: - self.get_delayed_image(index).compute() + image_to_compute = self.get_delayed_image(index) + if image_to_compute: + computed_image = image_to_compute.compute() except dask_image.imread.pims.api.UnknownFormatError: self.remove_image_data_by_index(index) self.get_computed_image(index - 1) except AttributeError: return None - return self.get_delayed_image(index).compute() + return computed_image def get_selected_computed_image(self): try: @@ -407,7 +411,9 @@ def _handle_directory_change(self) -> None: images = self.sort_images_by_modified_time(images) if len(images) == 0: self.image_stack.delete_all_data() - self.image_stack.add_images_to_delayed_stack(images) + + if self.create_delayed_array: + self.image_stack.add_images_to_delayed_stack(images) self.update_recent_watcher(images[-1:]) self.image_changed.emit(images, self.image_stack) diff --git a/mantidimaging/gui/windows/live_viewer/test/model_test.py b/mantidimaging/gui/windows/live_viewer/test/model_test.py index 88847753bac..2c689d7c86f 100644 --- a/mantidimaging/gui/windows/live_viewer/test/model_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/model_test.py @@ -183,7 +183,6 @@ def _get_fake_data(self, ext: str): def test_WHEN_not_create_delayed_array_THEN_no_delayed_array_created(self): image_data_list, _, _ = self._get_fake_data('.tif') self.delayed_image_stack = DaskImageDataStack(image_data_list, create_delayed_array=False) - self.assertIsNone(self.delayed_image_stack.get_delayed_arrays()) self.assertIsNone(self.delayed_image_stack.delayed_stack) self.assertEqual(self.delayed_image_stack.image_list, image_data_list) From 23ce67c79824a4924b14e8b67054af197483b453 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 22 Aug 2024 18:00:44 +0100 Subject: [PATCH 30/46] fix get_computed_image --- 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 8da9210fa36..5ac700c8414 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -75,7 +75,7 @@ def get_computed_image(self, index: int): return None try: image_to_compute = self.get_delayed_image(index) - if image_to_compute: + if image_to_compute is not None: computed_image = image_to_compute.compute() except dask_image.imread.pims.api.UnknownFormatError: self.remove_image_data_by_index(index) From 3d4b57581d18c334f40aab5909c804c3b26bc137 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 23 Aug 2024 15:51:36 +0100 Subject: [PATCH 31/46] mean is calculated as each image is added to DaskImageStack --- mantidimaging/gui/windows/live_viewer/model.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 5ac700c8414..929aefb1493 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -29,6 +29,7 @@ class DaskImageDataStack: image_list: list[Image_Data] create_delayed_array: bool _selected_index: int + mean: list[float] = [] def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = True): self.image_list = image_list @@ -119,7 +120,7 @@ def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> f"{image_list[0].image_path.suffix.lower()}") return delayed_stack - def add_images_to_delayed_stack(self, new_image_list: list[Image_Data]) -> None: + def add_images_to_delayed_stack(self, new_image_list: list[Image_Data], param_to_calc: list[str]) -> None: if not new_image_list: return image_paths = [image.image_path for image in self.image_list] @@ -131,6 +132,11 @@ def add_images_to_delayed_stack(self, new_image_list: list[Image_Data]) -> None: self.delayed_stack = dask.array.concatenate( [self.delayed_stack, self.get_delayed_arrays(images_to_add)]) self.image_list.extend(images_to_add) + if 'mean' in param_to_calc: + self.add_last_mean() + + def add_last_mean(self) -> None: + self.mean.append(dask.array.mean(self.delayed_stack[-1]).compute()) def delete_all_data(self): self.image_list = [] @@ -413,7 +419,7 @@ def _handle_directory_change(self) -> None: self.image_stack.delete_all_data() if self.create_delayed_array: - self.image_stack.add_images_to_delayed_stack(images) + self.image_stack.add_images_to_delayed_stack(images, ['mean']) self.update_recent_watcher(images[-1:]) self.image_changed.emit(images, self.image_stack) From 1785d74f0d80eb6f3491e8c03235e7bac40a4560 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 23 Aug 2024 21:45:20 +0100 Subject: [PATCH 32/46] mypy fix --- mantidimaging/gui/windows/live_viewer/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 929aefb1493..d84fe28cd10 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -136,7 +136,8 @@ def add_images_to_delayed_stack(self, new_image_list: list[Image_Data], param_to self.add_last_mean() def add_last_mean(self) -> None: - self.mean.append(dask.array.mean(self.delayed_stack[-1]).compute()) + if self.delayed_stack is not None: + self.mean.append(dask.array.mean(self.delayed_stack[-1]).compute()) def delete_all_data(self): self.image_list = [] From 7eaf6111e4135bfe4dbe80360e8f1b65e0c7d579 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 9 Sep 2024 15:13:31 +0100 Subject: [PATCH 33/46] added split spectrum plot to LV window --- mantidimaging/gui/windows/live_viewer/model.py | 16 +++++++++++++--- .../gui/windows/live_viewer/presenter.py | 4 ++++ mantidimaging/gui/windows/live_viewer/view.py | 17 ++++++++++++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index d84fe28cd10..80f0d5a2f4e 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -29,7 +29,7 @@ class DaskImageDataStack: image_list: list[Image_Data] create_delayed_array: bool _selected_index: int - mean: list[float] = [] + mean: np.ndarray = np.array([]) def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = True): self.image_list = image_list @@ -133,11 +133,18 @@ def add_images_to_delayed_stack(self, new_image_list: list[Image_Data], param_to [self.delayed_stack, self.get_delayed_arrays(images_to_add)]) self.image_list.extend(images_to_add) if 'mean' in param_to_calc: - self.add_last_mean() + if len(self.mean) == len(self.image_list) - 1: + self.add_last_mean() + else: + self.calc_mean_fully() def add_last_mean(self) -> None: if self.delayed_stack is not None: - self.mean.append(dask.array.mean(self.delayed_stack[-1]).compute()) + self.mean = np.append(self.mean, dask.array.mean(self.delayed_stack[-1]).compute()) + + 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 delete_all_data(self): self.image_list = [] @@ -246,6 +253,7 @@ def path(self, path: Path) -> None: self.image_watcher = ImageWatcher(path) self.image_watcher.image_changed.connect(self._handle_image_changed_in_list) self.image_watcher.recent_image_changed.connect(self.handle_image_modified) + self.image_watcher.update_spectrum.connect(self.presenter.update_spectrum) self.image_watcher._handle_notified_of_directry_change(str(path)) @property @@ -308,6 +316,7 @@ class ImageWatcher(QObject): Sort the images by modified time. """ image_changed = pyqtSignal(list, DaskImageDataStack) # 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 = True image_stack = DaskImageDataStack([], create_delayed_array=True) @@ -421,6 +430,7 @@ def _handle_directory_change(self) -> None: if self.create_delayed_array: self.image_stack.add_images_to_delayed_stack(images, ['mean']) + self.update_spectrum.emit(self.image_stack.mean) self.update_recent_watcher(images[-1:]) self.image_changed.emit(images, self.image_stack) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 97aac477fd8..e56b2e6bb37 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -190,3 +190,7 @@ def load_as_dataset(self) -> None: def update_image_stack(self, image_stack: DaskImageDataStack): self.image_stack = image_stack + + def update_spectrum(self, spec_data: list): + self.view.spectrum.clearPlots() + self.view.spectrum.plot(spec_data) diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index c7d31c88058..7c38fa0e0ef 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 @@ -32,10 +34,19 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: self.main_window = main_window self.path = live_dir_path self.presenter = LiveViewerWindowPresenter(self, main_window) + self.splitter = QSplitter(Qt.Vertical) self.live_viewer = LiveViewWidget() - self.imageLayout.addWidget(self.live_viewer) + 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.splitter.addWidget(self.live_viewer) + self.splitter.addWidget(self.spectrum_plot_widget) + widget_height = self.frameGeometry().height() + self.splitter.setSizes([int(0.7 * widget_height), int(0.3 * widget_height)]) + self.filter_params: dict[str, dict] = {} self.right_click_menu = self.live_viewer.image.vb.menu operations_menu = self.right_click_menu.addMenu("Operations") From c08fb5ae60c04ba008a1d34194db2659423ddfc6 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 10 Sep 2024 17:58:31 +0100 Subject: [PATCH 34/46] live viewer spectrum plot toggles via right click menu --- .../windows/live_viewer/live_view_widget.py | 42 ++++++++++++++++++- .../gui/windows/live_viewer/model.py | 22 +++++++++- .../gui/windows/live_viewer/presenter.py | 13 +++++- mantidimaging/gui/windows/live_viewer/view.py | 21 ++++++++-- 4 files changed, 91 insertions(+), 7 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index 3a02baec8ce..e03b2805a42 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 + roi_changed = pyqtSignal() + roi_object: SpectrumROI | None = None + sensible_roi: SensibleROI def __init__(self) -> None: super().__init__() @@ -48,3 +57,34 @@ def handle_deleted(self) -> None: def show_error(self, message: str | None): self.image.show_message(message) + + def add_roi(self): + 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: + 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: float) -> None: + 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: + 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 80f0d5a2f4e..3a8594e2392 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -14,6 +14,8 @@ import dask_image.imread from astropy.io import fits +from mantidimaging.core.utility.sensible_roi import SensibleROI + if TYPE_CHECKING: from os import stat_result from mantidimaging.gui.windows.live_viewer.view import LiveViewerWindowPresenter @@ -30,6 +32,7 @@ class DaskImageDataStack: create_delayed_array: bool _selected_index: int mean: np.ndarray = np.array([]) + roi: SensibleROI | None = None def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = True): self.image_list = image_list @@ -136,16 +139,31 @@ def add_images_to_delayed_stack(self, new_image_list: list[Image_Data], param_to if len(self.mean) == len(self.image_list) - 1: self.add_last_mean() else: - self.calc_mean_fully() + 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: - self.mean = np.append(self.mean, dask.array.mean(self.delayed_stack[-1]).compute()) + if self.roi: + left, top, right, bottom = self.roi + self.mean = np.append(self.mean, dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]).compute()) + else: + self.mean = np.append(self.mean, dask.array.mean(self.delayed_stack[-1]).compute()) 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: + left, top, right, bottom = self.roi + self.mean = dask.array.mean(self.delayed_stack[:, top:bottom, left:right], axis=(1, 2)).compute() + + def set_roi(self, roi: SensibleROI): + self.roi = roi + def delete_all_data(self): self.image_list = [] self.delayed_stack = None diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index e56b2e6bb37..7851d64c310 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -114,6 +114,10 @@ def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskIma 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: + 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" @@ -191,6 +195,13 @@ def load_as_dataset(self) -> None: def update_image_stack(self, image_stack: DaskImageDataStack): self.image_stack = image_stack - def update_spectrum(self, spec_data: list): + 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): + print(f"handle_roi_moved") + 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 7c38fa0e0ef..c89453d5d4b 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -14,7 +14,7 @@ import numpy as np -from ..spectrum_viewer.spectrum_widget import SpectrumPlotWidget +from ..spectrum_viewer.spectrum_widget import SpectrumWidget, SpectrumPlotWidget if TYPE_CHECKING: from mantidimaging.gui.windows.main import MainWindowView # noqa:F401 # pragma: no cover @@ -34,18 +34,19 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: self.main_window = main_window self.path = live_dir_path self.presenter = LiveViewerWindowPresenter(self, main_window) - self.splitter = QSplitter(Qt.Vertical) self.live_viewer = LiveViewWidget() + 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([int(0.7 * widget_height), int(0.3 * widget_height)]) + self.splitter.setSizes([widget_height, 0]) self.filter_params: dict[str, dict] = {} self.right_click_menu = self.live_viewer.image.vb.menu @@ -65,6 +66,11 @@ 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) + def show(self) -> None: """Show the window""" super().show() @@ -117,3 +123,12 @@ 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(): + 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 54c185ba3a153d519aa8aea0b1078fc7a661e2b1 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 12 Sep 2024 18:09:10 +0100 Subject: [PATCH 35/46] Spectrum calculation now toggled on and off by right click menu --- .../windows/live_viewer/live_view_widget.py | 7 +++---- .../gui/windows/live_viewer/model.py | 19 ++++++++++++++----- .../gui/windows/live_viewer/presenter.py | 1 - mantidimaging/gui/windows/live_viewer/view.py | 8 +++++++- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index e03b2805a42..47264a3438b 100644 --- a/mantidimaging/gui/windows/live_viewer/live_view_widget.py +++ b/mantidimaging/gui/windows/live_viewer/live_view_widget.py @@ -25,7 +25,7 @@ class LiveViewWidget(GraphicsLayoutWidget): image: MIMiniImageView image_shape: tuple roi_changed = pyqtSignal() - roi_object: SpectrumROI | None = None + roi_object: SpectrumROI sensible_roi: SensibleROI def __init__(self) -> None: @@ -76,8 +76,8 @@ def get_roi(self) -> SensibleROI: size = CloseEnoughPoint(roi.size()) return SensibleROI.from_points(pos, size) - def set_roi_alpha(self, alpha: float) -> None: - self.roi_object.colour = self.roi_object.colour[:3] + (alpha,) + def set_roi_alpha(self, alpha: int) -> None: + 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)) @@ -87,4 +87,3 @@ def set_roi_visibility_flags(self, visible: bool) -> None: 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 3a8594e2392..6f311e056e4 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -34,12 +34,12 @@ class DaskImageDataStack: mean: np.ndarray = np.array([]) roi: SensibleROI | None = None - def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = True): + def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = False): self.image_list = image_list self.create_delayed_array = create_delayed_array if image_list and create_delayed_array: - self.delayed_stack = self.create_delayed_stack_from_image_data(image_list) + self.create_and_set_delayed_stack() @property def shape(self): @@ -144,11 +144,15 @@ def add_images_to_delayed_stack(self, new_image_list: list[Image_Data], param_to else: self.calc_mean_fully() + def update_image_list(self, new_image_list: list) -> None: + self.image_list = new_image_list + def add_last_mean(self) -> None: if self.delayed_stack is not None: if self.roi: left, top, right, bottom = self.roi - self.mean = np.append(self.mean, dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]).compute()) + self.mean = np.append(self.mean, + dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]).compute()) else: self.mean = np.append(self.mean, dask.array.mean(self.delayed_stack[-1]).compute()) @@ -169,6 +173,9 @@ def delete_all_data(self): self.delayed_stack = None self.selected_index = 0 + def create_and_set_delayed_stack(self): + self.delayed_stack = self.create_delayed_stack_from_image_data(self.image_list) + class Image_Data: """ @@ -336,8 +343,8 @@ class ImageWatcher(QObject): image_changed = pyqtSignal(list, DaskImageDataStack) # 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 = True - image_stack = DaskImageDataStack([], create_delayed_array=True) + create_delayed_array: bool = False + image_stack = DaskImageDataStack([]) def __init__(self, directory: Path): """ @@ -446,6 +453,8 @@ def _handle_directory_change(self) -> None: if len(images) == 0: self.image_stack.delete_all_data() + self.image_stack.update_image_list(images) + if self.create_delayed_array: self.image_stack.add_images_to_delayed_stack(images, ['mean']) self.update_spectrum.emit(self.image_stack.mean) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 7851d64c310..2141215982f 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -200,7 +200,6 @@ 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): - print(f"handle_roi_moved") roi = self.view.live_viewer.get_roi() self.model.image_stack.set_roi(roi) self.model.image_stack.calc_mean_fully_roi() diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index c89453d5d4b..6e0a838921b 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -14,7 +14,7 @@ import numpy as np -from ..spectrum_viewer.spectrum_widget import SpectrumWidget, SpectrumPlotWidget +from ..spectrum_viewer.spectrum_widget import SpectrumPlotWidget if TYPE_CHECKING: from mantidimaging.gui.windows.main import MainWindowView # noqa:F401 # pragma: no cover @@ -70,6 +70,7 @@ 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 def show(self) -> None: """Show the window""" @@ -129,6 +130,11 @@ def set_spectrum_visibility(self): if self.spectrum_action.isChecked(): self.live_viewer.set_roi_alpha(255) self.splitter.setSizes([int(0.7 * widget_height), int(0.3 * widget_height)]) + self.presenter.model.image_stack.create_delayed_array = True + self.presenter.model.image_stack.create_and_set_delayed_stack() + self.presenter.model.image_stack.calc_mean_fully_roi() + self.presenter.update_spectrum(self.presenter.model.image_stack.mean) else: self.live_viewer.set_roi_alpha(0) self.splitter.setSizes([widget_height, 0]) + self.presenter.model.image_stack.create_delayed_array = False From 427ff4bb65bc896376d600795afb55371e956253 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 13 Sep 2024 14:37:25 +0100 Subject: [PATCH 36/46] Live Viewer behaves correctly when spectrum is turned on with no data --- .../windows/live_viewer/live_view_widget.py | 2 +- mantidimaging/gui/windows/live_viewer/model.py | 18 ++++-------------- .../gui/windows/live_viewer/presenter.py | 4 ++++ mantidimaging/gui/windows/live_viewer/view.py | 3 ++- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index 47264a3438b..ce85df82db1 100644 --- a/mantidimaging/gui/windows/live_viewer/live_view_widget.py +++ b/mantidimaging/gui/windows/live_viewer/live_view_widget.py @@ -25,7 +25,7 @@ class LiveViewWidget(GraphicsLayoutWidget): image: MIMiniImageView image_shape: tuple roi_changed = pyqtSignal() - roi_object: SpectrumROI + roi_object: SpectrumROI | None = None sensible_roi: SensibleROI def __init__(self) -> None: diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 6f311e056e4..a2014dc7c18 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -123,18 +123,8 @@ def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> f"{image_list[0].image_path.suffix.lower()}") return delayed_stack - def add_images_to_delayed_stack(self, new_image_list: list[Image_Data], param_to_calc: list[str]) -> None: - if not new_image_list: - return - image_paths = [image.image_path for image in self.image_list] - images_to_add = [image for image in new_image_list if image.image_path not in image_paths] - if self.delayed_stack is None or dask.array.isnan(self.delayed_stack.shape).any(): - self.delayed_stack = self.create_delayed_stack_from_image_data(new_image_list) - else: - if images_to_add: - self.delayed_stack = dask.array.concatenate( - [self.delayed_stack, self.get_delayed_arrays(images_to_add)]) - self.image_list.extend(images_to_add) + def update_delayed_stack(self, param_to_calc: list[str]) -> None: + self.delayed_stack = self.create_delayed_stack_from_image_data(self.image_list) if 'mean' in param_to_calc: if len(self.mean) == len(self.image_list) - 1: self.add_last_mean() @@ -455,8 +445,8 @@ def _handle_directory_change(self) -> None: self.image_stack.update_image_list(images) - if self.create_delayed_array: - self.image_stack.add_images_to_delayed_stack(images, ['mean']) + if self.image_stack.create_delayed_array: + self.image_stack.update_delayed_stack(['mean']) self.update_spectrum.emit(self.image_stack.mean) self.update_recent_watcher(images[-1:]) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 2141215982f..9ac1081b109 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -15,6 +15,7 @@ from tifffile import tifffile from astropy.io import fits +from mantidimaging.core.utility import ExecutionProfiler from mantidimaging.gui.mvp_base import BasePresenter from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data, DaskImageDataStack from mantidimaging.core.operations.loader import load_filter_packages @@ -99,6 +100,8 @@ def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskIma """ Display image in the view after validating contents """ + # print(f"display_image: {[image.image_path for image in delayed_image_stack.image_list]=}") + # print(f"display_image: {len(delayed_image_stack.delayed_stack)=}") try: if (delayed_image_stack is None or delayed_image_stack.delayed_stack is None or not delayed_image_stack.create_delayed_array): @@ -133,6 +136,7 @@ def load_image_from_delayed_stack(delayed_image_stack: DaskImageDataStack | None """ Load a delayed stack from a DaskImageDataStack and compute """ + #with ExecutionProfiler(msg=f"load_image_from_delayed_stack() with {len(delayed_image_stack.delayed_stack)=}"): if delayed_image_stack is not None: image_data = delayed_image_stack.get_selected_computed_image() else: diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 6e0a838921b..044ebdfcaa5 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -128,7 +128,8 @@ def set_load_as_dataset_enabled(self, enabled: bool): def set_spectrum_visibility(self): widget_height = self.frameGeometry().height() if self.spectrum_action.isChecked(): - self.live_viewer.set_roi_alpha(255) + if self.live_viewer.roi_object: + self.live_viewer.set_roi_alpha(255) self.splitter.setSizes([int(0.7 * widget_height), int(0.3 * widget_height)]) self.presenter.model.image_stack.create_delayed_array = True self.presenter.model.image_stack.create_and_set_delayed_stack() From 26737ed1d68770403ab56eda5b1a9a0c1a5ef21a Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 17 Sep 2024 11:45:52 +0100 Subject: [PATCH 37/46] dask slicing is optimised --- mantidimaging/gui/windows/live_viewer/live_view_widget.py | 6 ++++++ mantidimaging/gui/windows/live_viewer/model.py | 6 +++--- mantidimaging/gui/windows/live_viewer/presenter.py | 2 -- mantidimaging/gui/windows/live_viewer/view.py | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index ce85df82db1..a6c3777792d 100644 --- a/mantidimaging/gui/windows/live_viewer/live_view_widget.py +++ b/mantidimaging/gui/windows/live_viewer/live_view_widget.py @@ -71,18 +71,24 @@ 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) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index a2014dc7c18..5c997c4a761 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -80,7 +80,8 @@ def get_computed_image(self, index: int): try: image_to_compute = self.get_delayed_image(index) if image_to_compute is not None: - computed_image = image_to_compute.compute() + image_to_compute_opt = dask.optimize(image_to_compute) + computed_image = image_to_compute_opt[0].compute() except dask_image.imread.pims.api.UnknownFormatError: self.remove_image_data_by_index(index) self.get_computed_image(index - 1) @@ -90,10 +91,9 @@ def get_computed_image(self, index: int): def get_selected_computed_image(self): try: - self.get_computed_image(self.selected_index) + return self.get_computed_image(self.selected_index) except dask_image.imread.pims.api.UnknownFormatError: pass - return self.get_computed_image(self.selected_index) def remove_image_data_by_path(self, image_path: Path) -> None: image_paths = [image.image_path for image in self.image_list] diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 9ac1081b109..95e3d2250fe 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -15,7 +15,6 @@ from tifffile import tifffile from astropy.io import fits -from mantidimaging.core.utility import ExecutionProfiler from mantidimaging.gui.mvp_base import BasePresenter from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data, DaskImageDataStack from mantidimaging.core.operations.loader import load_filter_packages @@ -136,7 +135,6 @@ def load_image_from_delayed_stack(delayed_image_stack: DaskImageDataStack | None """ Load a delayed stack from a DaskImageDataStack and compute """ - #with ExecutionProfiler(msg=f"load_image_from_delayed_stack() with {len(delayed_image_stack.delayed_stack)=}"): if delayed_image_stack is not None: image_data = delayed_image_stack.get_selected_computed_image() else: diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 044ebdfcaa5..311f1c8c01c 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -128,7 +128,7 @@ def set_load_as_dataset_enabled(self, enabled: bool): def set_spectrum_visibility(self): widget_height = self.frameGeometry().height() if self.spectrum_action.isChecked(): - if self.live_viewer.roi_object: + if self.live_viewer.roi_object: self.live_viewer.set_roi_alpha(255) self.splitter.setSizes([int(0.7 * widget_height), int(0.3 * widget_height)]) self.presenter.model.image_stack.create_delayed_array = True From 056f3049c7f2ae875a5bc07c4a0e417222ed58c9 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 17 Sep 2024 18:24:11 +0100 Subject: [PATCH 38/46] Using z slider is more fluid with Dask --- mantidimaging/gui/windows/live_viewer/live_view_widget.py | 4 +++- mantidimaging/gui/windows/live_viewer/model.py | 7 +++++-- mantidimaging/gui/windows/live_viewer/presenter.py | 7 +++++-- mantidimaging/gui/windows/live_viewer/view.py | 8 ++++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/live_view_widget.py b/mantidimaging/gui/windows/live_viewer/live_view_widget.py index a6c3777792d..c7baafd1eec 100644 --- a/mantidimaging/gui/windows/live_viewer/live_view_widget.py +++ b/mantidimaging/gui/windows/live_viewer/live_view_widget.py @@ -23,7 +23,7 @@ class LiveViewWidget(GraphicsLayoutWidget): @param parent: The parent widget """ image: MIMiniImageView - image_shape: tuple + image_shape: tuple = (-1, -1) roi_changed = pyqtSignal() roi_object: SpectrumROI | None = None sensible_roi: SensibleROI @@ -59,6 +59,8 @@ 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) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 5c997c4a761..e084177d8a3 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -123,7 +123,9 @@ def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> f"{image_list[0].image_path.suffix.lower()}") return delayed_stack - def update_delayed_stack(self, param_to_calc: list[str]) -> None: + def update_delayed_stack(self, param_to_calc=None) -> None: + if param_to_calc is None: + param_to_calc = [] self.delayed_stack = self.create_delayed_stack_from_image_data(self.image_list) if 'mean' in param_to_calc: if len(self.mean) == len(self.image_list) - 1: @@ -446,7 +448,8 @@ def _handle_directory_change(self) -> None: self.image_stack.update_image_list(images) if self.image_stack.create_delayed_array: - self.image_stack.update_delayed_stack(['mean']) + #self.image_stack.update_delayed_stack(['mean']) + self.image_stack.update_delayed_stack() self.update_spectrum.emit(self.image_stack.mean) self.update_recent_watcher(images[-1:]) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 95e3d2250fe..832e0396afe 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -117,9 +117,9 @@ def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskIma 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: + 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()) + 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" @@ -127,6 +127,9 @@ def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskIma self.view.remove_image() self.view.live_viewer.show_error(message) return + if np.any(np.isnan(self.model.image_stack.mean)): + self.model.image_stack.calc_mean_fully_roi() + self.update_spectrum(self.model.image_stack.mean) 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 311f1c8c01c..1c41dd447d4 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -71,6 +71,8 @@ def __init__(self, main_window: MainWindowView, live_dir_path: Path) -> None: 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""" @@ -128,10 +130,12 @@ def set_load_as_dataset_enabled(self, enabled: bool): def set_spectrum_visibility(self): widget_height = self.frameGeometry().height() if self.spectrum_action.isChecked(): - if self.live_viewer.roi_object: - self.live_viewer.set_roi_alpha(255) + 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)]) self.presenter.model.image_stack.create_delayed_array = True + self.presenter.model.image_stack.set_roi(self.live_viewer.get_roi()) self.presenter.model.image_stack.create_and_set_delayed_stack() self.presenter.model.image_stack.calc_mean_fully_roi() self.presenter.update_spectrum(self.presenter.model.image_stack.mean) From beacb41ef45b224d6bc06d31c1bfb1b3d41a0391 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 18 Sep 2024 16:43:41 +0100 Subject: [PATCH 39/46] dask mean calculation optimised --- mantidimaging/gui/windows/live_viewer/model.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index e084177d8a3..fae984468f4 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -127,6 +127,7 @@ def update_delayed_stack(self, param_to_calc=None) -> None: if param_to_calc is None: param_to_calc = [] self.delayed_stack = self.create_delayed_stack_from_image_data(self.image_list) + #self.delayed_stack.visualize(filename=f'dask-update_delayed_stack-{self._selected_index}', format='png') if 'mean' in param_to_calc: if len(self.mean) == len(self.image_list) - 1: self.add_last_mean() @@ -143,10 +144,12 @@ def add_last_mean(self) -> None: if self.delayed_stack is not None: if self.roi: left, top, right, bottom = self.roi - self.mean = np.append(self.mean, - dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]).compute()) + #mean_visual = dask.optimize(dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]))[0].visualize(filename=f'dask-mean-add-task-opt-{self._selected_index}.png', format='png') + mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]))[0].compute() + self.mean = np.append(self.mean, mean_to_add) else: - self.mean = np.append(self.mean, dask.array.mean(self.delayed_stack[-1]).compute()) + mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1]))[0].compute() + self.mean = np.append(self.mean, mean_to_add) def calc_mean_fully(self) -> None: if self.delayed_stack is not None: @@ -155,6 +158,7 @@ def calc_mean_fully(self) -> None: def calc_mean_fully_roi(self): if self.delayed_stack is not None: left, top, right, bottom = self.roi + #mean_visual = dask.array.mean(self.delayed_stack[:, top:bottom, left:right], axis=(1, 2)).visualize(filename=f'dask-mean-full-task-{self._selected_index}.png', format='png') self.mean = dask.array.mean(self.delayed_stack[:, top:bottom, left:right], axis=(1, 2)).compute() def set_roi(self, roi: SensibleROI): @@ -448,8 +452,8 @@ def _handle_directory_change(self) -> None: self.image_stack.update_image_list(images) if self.image_stack.create_delayed_array: - #self.image_stack.update_delayed_stack(['mean']) - self.image_stack.update_delayed_stack() + self.image_stack.update_delayed_stack(['mean']) + #self.image_stack.update_delayed_stack() self.update_spectrum.emit(self.image_stack.mean) self.update_recent_watcher(images[-1:]) From 7ec86f0f2d72b6572dc011bf6a58f495b4a09f93 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Thu, 19 Sep 2024 12:12:59 +0100 Subject: [PATCH 40/46] refactor and optimise how DaskImageDataStack updates its image lis --- .../gui/windows/live_viewer/model.py | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index fae984468f4..4e2c60c4823 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -33,6 +33,7 @@ class DaskImageDataStack: _selected_index: int mean: np.ndarray = np.array([]) roi: SensibleROI | None = None + param_to_calc: list[str] = [] def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = False): self.image_list = image_list @@ -123,12 +124,19 @@ def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> f"{image_list[0].image_path.suffix.lower()}") return delayed_stack - def update_delayed_stack(self, param_to_calc=None) -> None: - if param_to_calc is None: - param_to_calc = [] - self.delayed_stack = self.create_delayed_stack_from_image_data(self.image_list) - #self.delayed_stack.visualize(filename=f'dask-update_delayed_stack-{self._selected_index}', format='png') - if 'mean' in param_to_calc: + def update_delayed_stack(self, new_image_list) -> None: + if self.delayed_stack is None: + self.delayed_stack = self.create_delayed_stack_from_image_data(new_image_list) + else: + new_images = [ + image for image in new_image_list + if image.image_path not in [image.image_path for image in self.image_list] + ] + #new_images = [image for image in new_image_list if image not in self.image_list] + self.delayed_stack = dask.optimize( + dask.array.concatenate([self.delayed_stack, + self.create_delayed_stack_from_image_data(new_images)]))[0] + if 'mean' in self.param_to_calc: if len(self.mean) == len(self.image_list) - 1: self.add_last_mean() else: @@ -137,15 +145,17 @@ def update_delayed_stack(self, param_to_calc=None) -> None: else: self.calc_mean_fully() - def update_image_list(self, new_image_list: list) -> None: + def update_image_list(self, new_image_list: list, update_stack: bool = True) -> None: + if update_stack and self.create_delayed_array: + self.update_delayed_stack(new_image_list) self.image_list = new_image_list def add_last_mean(self) -> None: if self.delayed_stack is not None: if self.roi: left, top, right, bottom = self.roi - #mean_visual = dask.optimize(dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]))[0].visualize(filename=f'dask-mean-add-task-opt-{self._selected_index}.png', format='png') - mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]))[0].compute() + mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1, top:bottom, + left:right]))[0].compute() self.mean = np.append(self.mean, mean_to_add) else: mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1]))[0].compute() @@ -158,7 +168,6 @@ def calc_mean_fully(self) -> None: def calc_mean_fully_roi(self): if self.delayed_stack is not None: left, top, right, bottom = self.roi - #mean_visual = dask.array.mean(self.delayed_stack[:, top:bottom, left:right], axis=(1, 2)).visualize(filename=f'dask-mean-full-task-{self._selected_index}.png', format='png') self.mean = dask.array.mean(self.delayed_stack[:, top:bottom, left:right], axis=(1, 2)).compute() def set_roi(self, roi: SensibleROI): @@ -172,6 +181,9 @@ def delete_all_data(self): def create_and_set_delayed_stack(self): self.delayed_stack = self.create_delayed_stack_from_image_data(self.image_list) + def add_param_to_calc(self, param_name: str): + self.param_to_calc.append(param_name) + class Image_Data: """ @@ -367,6 +379,8 @@ def __init__(self, directory: Path): self.sub_directories: dict[Path, SubDirectory] = {} self.add_sub_directory(SubDirectory(self.directory)) + self.image_stack.add_param_to_calc('mean') + def find_images(self, directory: Path) -> list[Image_Data]: """ Find all the images in the directory. @@ -451,9 +465,7 @@ def _handle_directory_change(self) -> None: self.image_stack.update_image_list(images) - if self.image_stack.create_delayed_array: - self.image_stack.update_delayed_stack(['mean']) - #self.image_stack.update_delayed_stack() + if 'mean' in self.image_stack.param_to_calc: self.update_spectrum.emit(self.image_stack.mean) self.update_recent_watcher(images[-1:]) From 7b9c47d3de0487e617b0096ded74adb92dd99b89 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 23 Sep 2024 16:58:56 +0100 Subject: [PATCH 41/46] change how image_list updates and some benchmarking --- .../gui/windows/live_viewer/model.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 4e2c60c4823..6bd6aa24674 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -14,6 +14,7 @@ import dask_image.imread from astropy.io import fits +from mantidimaging.core.utility import ExecutionProfiler from mantidimaging.core.utility.sensible_roi import SensibleROI if TYPE_CHECKING: @@ -29,6 +30,7 @@ class DaskImageDataStack: """ delayed_stack: dask.array.Array | None = None image_list: list[Image_Data] + image_paths: set[str] = set() create_delayed_array: bool _selected_index: int mean: np.ndarray = np.array([]) @@ -112,6 +114,7 @@ def remove_image_data_by_index(self, index_to_remove: int) -> None: def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> None | dask.array.Array: delayed_stack = None arrays = self.get_delayed_arrays(image_list) + arrays_vis = arrays.visualize() if arrays: if image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: delayed_stack = dask.array.stack(dask.array.array(arrays)) @@ -130,9 +133,8 @@ def update_delayed_stack(self, new_image_list) -> None: else: new_images = [ image for image in new_image_list - if image.image_path not in [image.image_path for image in self.image_list] + if image.image_path not in self.image_paths ] - #new_images = [image for image in new_image_list if image not in self.image_list] self.delayed_stack = dask.optimize( dask.array.concatenate([self.delayed_stack, self.create_delayed_stack_from_image_data(new_images)]))[0] @@ -149,6 +151,11 @@ def update_image_list(self, new_image_list: list, update_stack: bool = True) -> if update_stack and self.create_delayed_array: self.update_delayed_stack(new_image_list) self.image_list = new_image_list + self.update_image_paths(new_image_list) + + def update_image_paths(self, new_image_list: list): + for image in new_image_list: + self.image_paths.add(image.image_path) def add_last_mean(self) -> None: if self.delayed_stack is not None: @@ -463,7 +470,13 @@ def _handle_directory_change(self) -> None: if len(images) == 0: self.image_stack.delete_all_data() - self.image_stack.update_image_list(images) + if len(images) % 50 == 0: + print("\n") + with ExecutionProfiler(msg=f"self.image_stack.update_image_list(images): {len(images)=}"): + self.image_stack.update_image_list(images) + print("\n") + else: + self.image_stack.update_image_list(images) if 'mean' in self.image_stack.param_to_calc: self.update_spectrum.emit(self.image_stack.mean) From 708344bcb202756b06c293c47c5f9d78c8c2ddaf Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 27 Sep 2024 17:05:28 +0100 Subject: [PATCH 42/46] small optimization of mean calc and mypy fix --- mantidimaging/gui/windows/live_viewer/model.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 6bd6aa24674..b8e794c36df 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -114,7 +114,6 @@ def remove_image_data_by_index(self, index_to_remove: int) -> None: def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> None | dask.array.Array: delayed_stack = None arrays = self.get_delayed_arrays(image_list) - arrays_vis = arrays.visualize() if arrays: if image_list[0].image_path.suffix.lower() in [".tif", ".tiff"]: delayed_stack = dask.array.stack(dask.array.array(arrays)) @@ -131,10 +130,7 @@ def update_delayed_stack(self, new_image_list) -> None: if self.delayed_stack is None: self.delayed_stack = self.create_delayed_stack_from_image_data(new_image_list) else: - new_images = [ - image for image in new_image_list - if image.image_path not in self.image_paths - ] + new_images = [image for image in new_image_list if image.image_path not in self.image_paths] self.delayed_stack = dask.optimize( dask.array.concatenate([self.delayed_stack, self.create_delayed_stack_from_image_data(new_images)]))[0] @@ -175,7 +171,8 @@ def calc_mean_fully(self) -> None: def calc_mean_fully_roi(self): if self.delayed_stack is not None: left, top, right, bottom = self.roi - self.mean = dask.array.mean(self.delayed_stack[:, top:bottom, left:right], axis=(1, 2)).compute() + self.mean = dask.optimize(dask.array.mean(self.delayed_stack[:, top:bottom, left:right], + axis=(1, 2)))[0].compute() def set_roi(self, roi: SensibleROI): self.roi = roi From 94888a2f6f7271939e2cec2796950dcf3e688a25 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 27 Sep 2024 17:42:52 +0100 Subject: [PATCH 43/46] release note --- .../next/feature-2327-live-viewer-spectrum-experimental | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/release_notes/next/feature-2327-live-viewer-spectrum-experimental diff --git a/docs/release_notes/next/feature-2327-live-viewer-spectrum-experimental b/docs/release_notes/next/feature-2327-live-viewer-spectrum-experimental new file mode 100644 index 00000000000..7b752aed5da --- /dev/null +++ b/docs/release_notes/next/feature-2327-live-viewer-spectrum-experimental @@ -0,0 +1 @@ +#2327: The mean spectrum of live data in the Live Spectrum can be plotted via a right-click menu, using Dask. This feature is experimental. \ No newline at end of file From a14efd5c418c1d195e196b082b5971ec5fb5e48e Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 22 Nov 2024 15:49:41 +0000 Subject: [PATCH 44/46] 100 most recent images are stored in LRU cache --- .../gui/windows/live_viewer/model.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index b8e794c36df..5a06ec43efd 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 functools import lru_cache from typing import TYPE_CHECKING from pathlib import Path from logging import getLogger @@ -77,6 +78,7 @@ def get_fits_sample(self, image_data: Image_Data) -> np.ndarray: with fits.open(image_data.image_path.__str__()) as fit: return fit[0].data + @lru_cache(maxsize=100) # noqa: B019 def get_computed_image(self, index: int): if index < 0: return None @@ -169,10 +171,26 @@ def calc_mean_fully(self) -> 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: + if self.delayed_stack is not None and self.image_list: left, top, right, bottom = self.roi - self.mean = dask.optimize(dask.array.mean(self.delayed_stack[:, top:bottom, left:right], - axis=(1, 2)))[0].compute() + current_cache_size = self.get_computed_image.cache_info()[3] + self.mean = np.zeros(len(self.image_list)) + np.put(self.mean, range(-current_cache_size, 0), self.calc_mean_cached_images(left, top, right, bottom)) + if len(self.image_list) > current_cache_size: + dask_mean = dask.optimize( + dask.array.mean(self.delayed_stack[0:current_cache_size, top:bottom, left:right], + axis=(1, 2)))[0].compute() + np.put(self.mean, range(-len(self.image_list), -current_cache_size), dask_mean) + + 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 set_roi(self, roi: SensibleROI): self.roi = roi From c85406a84670029b88df06c12066b33f06a4639c Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 26 Nov 2024 11:16:04 +0000 Subject: [PATCH 45/46] Images stored in LRUCache and new means append correctly (with debug prints) --- .../gui/windows/live_viewer/model.py | 61 ++++++++++++++----- .../gui/windows/live_viewer/presenter.py | 15 ++++- mantidimaging/gui/windows/live_viewer/view.py | 3 + 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 5a06ec43efd..40b5c2bc867 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -37,6 +37,7 @@ class DaskImageDataStack: mean: np.ndarray = np.array([]) roi: SensibleROI | None = None param_to_calc: list[str] = [] + max_cache_size: int = 2 def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = False): self.image_list = image_list @@ -78,7 +79,7 @@ def get_fits_sample(self, image_data: Image_Data) -> np.ndarray: with fits.open(image_data.image_path.__str__()) as fit: return fit[0].data - @lru_cache(maxsize=100) # noqa: B019 + @lru_cache(maxsize=max_cache_size) # noqa: B019 def get_computed_image(self, index: int): if index < 0: return None @@ -129,6 +130,8 @@ def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> return delayed_stack def update_delayed_stack(self, new_image_list) -> None: + print(f"\nupdate_delayed_stack:\n {len(self.mean)=} ================== {len(self.image_list)=}\n") + print(f"{new_image_list=}\n") if self.delayed_stack is None: self.delayed_stack = self.create_delayed_stack_from_image_data(new_image_list) else: @@ -136,6 +139,19 @@ def update_delayed_stack(self, new_image_list) -> None: self.delayed_stack = dask.optimize( dask.array.concatenate([self.delayed_stack, self.create_delayed_stack_from_image_data(new_images)]))[0] + + def update_image_list(self, new_image_list: list, update_stack: bool = True) -> None: + print(f"\n ========================= update_image_list =============================\n") + if update_stack and self.create_delayed_array: + self.update_delayed_stack(new_image_list) + self.image_list = new_image_list + self.update_image_paths(new_image_list) + print(f"@@@@@@@@@@@@@@@update_image_list 1 : {len(self.mean)=} ================== {len(self.image_list)=}\n") + #self.update_param_calculations() + print(f"@@@@@@@@@@@@@@@update_image_list 2 : {len(self.mean)=} ================== {len(self.image_list)=}\n") + + def update_param_calculations(self) -> None: + print(f"update_param_calculations: {len(self.mean)=} ================== {len(self.image_list)=}\n") if 'mean' in self.param_to_calc: if len(self.mean) == len(self.image_list) - 1: self.add_last_mean() @@ -145,26 +161,27 @@ def update_delayed_stack(self, new_image_list) -> None: else: self.calc_mean_fully() - def update_image_list(self, new_image_list: list, update_stack: bool = True) -> None: - if update_stack and self.create_delayed_array: - self.update_delayed_stack(new_image_list) - self.image_list = new_image_list - self.update_image_paths(new_image_list) - def update_image_paths(self, new_image_list: list): for image in new_image_list: self.image_paths.add(image.image_path) def add_last_mean(self) -> None: + print("ADD LAST MEAN") if self.delayed_stack is not None: if self.roi: left, top, right, bottom = self.roi + print(f"{(left, top, right, bottom)=}") + print(f"{self.delayed_stack=}") + print(f"{self.delayed_stack.compute()=}") + print(f"{self.delayed_stack[-1, top:bottom, left:right].compute()=}") mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1, top:bottom, left:right]))[0].compute() - self.mean = np.append(self.mean, mean_to_add) else: mean_to_add = dask.optimize(dask.array.mean(self.delayed_stack[-1]))[0].compute() - self.mean = np.append(self.mean, mean_to_add) + print(f"{mean_to_add=}") + self.mean = np.append(self.mean, mean_to_add) + print(f"{self.mean=}") + #self.calc_mean_buffer() def calc_mean_fully(self) -> None: if self.delayed_stack is not None: @@ -174,13 +191,16 @@ 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 = self.get_computed_image.cache_info()[3] - self.mean = np.zeros(len(self.image_list)) + print(f"calc_mean_fully_roi ================== \n{len(self.mean)=}\n{len(self.image_list)=}\n{range(-current_cache_size, 0)=}\n{self.calc_mean_cached_images(left, top, right, bottom)}\n") + self.mean = np.full(len(self.image_list), np.nan) + print(f"calc_mean_fully_roi 1: {len(self.mean)=} ================== {len(self.image_list)=}\n{self.mean=}") np.put(self.mean, range(-current_cache_size, 0), self.calc_mean_cached_images(left, top, right, bottom)) - if len(self.image_list) > current_cache_size: - dask_mean = dask.optimize( - dask.array.mean(self.delayed_stack[0:current_cache_size, top:bottom, left:right], - axis=(1, 2)))[0].compute() - np.put(self.mean, range(-len(self.image_list), -current_cache_size), dask_mean) + print(f"calc_mean_fully_roi 2: {len(self.mean)=} ================== {len(self.image_list)=}\n{self.mean=}") + # if len(self.image_list) > current_cache_size and False: + # dask_mean = dask.optimize( + # dask.array.mean(self.delayed_stack[0:current_cache_size, top:bottom, left:right], + # axis=(1, 2)))[0].compute() + # np.put(self.mean, range(-len(self.image_list), -current_cache_size), dask_mean) def calc_mean_cached_images(self, left, top, right, bottom): current_cache_size = self.get_computed_image.cache_info()[3] @@ -188,10 +208,15 @@ def calc_mean_cached_images(self, left, top, right, bottom): self.get_computed_image(index) for index in range(self.selected_index - current_cache_size + 1, self.selected_index + 1, 1) ] + print(f"calc_mean_cached_images: {cache_stack=}") 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)) + print(f"{nanInds=}") + def set_roi(self, roi: SensibleROI): self.roi = roi @@ -321,6 +346,7 @@ def images(self, images): def _handle_image_changed_in_list(self, image_files: list[Image_Data], dask_image_stack: DaskImageDataStack) -> None: + print("++++++++++++++++++++++++++++ _handle_image_changed_in_list ++++++++++++++++++++++++++++++++++++++++++") """ Handle an image changed event. Update the image in the view. This method is called when the image_watcher detects a change @@ -332,8 +358,11 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data], self.image_stack = dask_image_stack # if dask_image_stack.image_list: # self.image_stack = dask_image_stack + print("++++++++++++++++++++++++++++ _handle_image_changed_in_list 1 ++++++++++++++++++++++++++++++++++++++++++") self.presenter.update_image_list(image_files) + print("++++++++++++++++++++++++++++ _handle_image_changed_in_list 2 ++++++++++++++++++++++++++++++++++++++++++") self.presenter.update_image_stack(self.image_stack) + print("++++++++++++++++++++++++++++ _handle_image_changed_in_list 3 ++++++++++++++++++++++++++++++++++++++++++") def handle_image_modified(self, image_path: Path): self.image_stack.remove_image_data_by_path(image_path) @@ -494,6 +523,8 @@ def _handle_directory_change(self) -> None: self.image_stack.update_image_list(images) if 'mean' in self.image_stack.param_to_calc: + print("<<<<<<<<<<<<< UPDATE SPECTRUM EMITTED!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<") + print(f"{self.image_stack.mean=}") self.update_spectrum.emit(self.image_stack.mean) self.update_recent_watcher(images[-1:]) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 832e0396afe..7dad15979b9 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -74,12 +74,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: @@ -119,18 +126,20 @@ def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskIma 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() + print(f"{self.view.live_viewer.get_roi()=}") self.model.image_stack.set_roi(self.view.live_viewer.get_roi()) image_data = self.perform_operations(image_data) + self.model.image_stack.update_param_calculations() if image_data.size == 0: message = "reading image: {image_path}: Image has zero size" 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.image_stack.calc_mean_fully_roi() - self.update_spectrum(self.model.image_stack.mean) + # if np.any(np.isnan(self.model.image_stack.mean)): + # self.model.image_stack.calc_mean_fully_roi() self.view.show_most_recent_image(image_data) + self.update_spectrum(self.model.image_stack.mean) self.view.live_viewer.show_error(None) @staticmethod diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 1c41dd447d4..42a5af7ba55 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 67a58c0f5851744d5a6e7af48ac90cccc83aaf5d Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Wed, 27 Nov 2024 11:47:19 +0000 Subject: [PATCH 46/46] Mean buffer loading and prints removed --- .../gui/windows/live_viewer/model.py | 46 ++++++------------- .../gui/windows/live_viewer/presenter.py | 10 ---- mantidimaging/gui/windows/live_viewer/view.py | 3 -- 3 files changed, 14 insertions(+), 45 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 40b5c2bc867..2596a1741c5 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -37,7 +37,8 @@ class DaskImageDataStack: mean: np.ndarray = np.array([]) roi: SensibleROI | None = None param_to_calc: list[str] = [] - max_cache_size: int = 2 + max_cache_size: int = 100 + buffer_size: int = 10 def __init__(self, image_list: list[Image_Data], create_delayed_array: bool = False): self.image_list = image_list @@ -130,8 +131,6 @@ def create_delayed_stack_from_image_data(self, image_list: list[Image_Data]) -> return delayed_stack def update_delayed_stack(self, new_image_list) -> None: - print(f"\nupdate_delayed_stack:\n {len(self.mean)=} ================== {len(self.image_list)=}\n") - print(f"{new_image_list=}\n") if self.delayed_stack is None: self.delayed_stack = self.create_delayed_stack_from_image_data(new_image_list) else: @@ -141,17 +140,12 @@ def update_delayed_stack(self, new_image_list) -> None: self.create_delayed_stack_from_image_data(new_images)]))[0] def update_image_list(self, new_image_list: list, update_stack: bool = True) -> None: - print(f"\n ========================= update_image_list =============================\n") if update_stack and self.create_delayed_array: self.update_delayed_stack(new_image_list) self.image_list = new_image_list self.update_image_paths(new_image_list) - print(f"@@@@@@@@@@@@@@@update_image_list 1 : {len(self.mean)=} ================== {len(self.image_list)=}\n") - #self.update_param_calculations() - print(f"@@@@@@@@@@@@@@@update_image_list 2 : {len(self.mean)=} ================== {len(self.image_list)=}\n") def update_param_calculations(self) -> None: - print(f"update_param_calculations: {len(self.mean)=} ================== {len(self.image_list)=}\n") if 'mean' in self.param_to_calc: if len(self.mean) == len(self.image_list) - 1: self.add_last_mean() @@ -166,22 +160,15 @@ def update_image_paths(self, new_image_list: list): self.image_paths.add(image.image_path) def add_last_mean(self) -> None: - print("ADD LAST MEAN") if self.delayed_stack is not None: if self.roi: left, top, right, bottom = self.roi - print(f"{(left, top, right, bottom)=}") - print(f"{self.delayed_stack=}") - print(f"{self.delayed_stack.compute()=}") - print(f"{self.delayed_stack[-1, top:bottom, left:right].compute()=}") 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() - print(f"{mean_to_add=}") self.mean = np.append(self.mean, mean_to_add) - print(f"{self.mean=}") - #self.calc_mean_buffer() + self.calc_mean_buffer() def calc_mean_fully(self) -> None: if self.delayed_stack is not None: @@ -191,16 +178,8 @@ 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 = self.get_computed_image.cache_info()[3] - print(f"calc_mean_fully_roi ================== \n{len(self.mean)=}\n{len(self.image_list)=}\n{range(-current_cache_size, 0)=}\n{self.calc_mean_cached_images(left, top, right, bottom)}\n") self.mean = np.full(len(self.image_list), np.nan) - print(f"calc_mean_fully_roi 1: {len(self.mean)=} ================== {len(self.image_list)=}\n{self.mean=}") np.put(self.mean, range(-current_cache_size, 0), self.calc_mean_cached_images(left, top, right, bottom)) - print(f"calc_mean_fully_roi 2: {len(self.mean)=} ================== {len(self.image_list)=}\n{self.mean=}") - # if len(self.image_list) > current_cache_size and False: - # dask_mean = dask.optimize( - # dask.array.mean(self.delayed_stack[0:current_cache_size, top:bottom, left:right], - # axis=(1, 2)))[0].compute() - # np.put(self.mean, range(-len(self.image_list), -current_cache_size), dask_mean) def calc_mean_cached_images(self, left, top, right, bottom): current_cache_size = self.get_computed_image.cache_info()[3] @@ -208,14 +187,23 @@ def calc_mean_cached_images(self, left, top, right, bottom): self.get_computed_image(index) for index in range(self.selected_index - current_cache_size + 1, self.selected_index + 1, 1) ] - print(f"calc_mean_cached_images: {cache_stack=}") 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)) - print(f"{nanInds=}") + 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 @@ -346,7 +334,6 @@ def images(self, images): def _handle_image_changed_in_list(self, image_files: list[Image_Data], dask_image_stack: DaskImageDataStack) -> None: - print("++++++++++++++++++++++++++++ _handle_image_changed_in_list ++++++++++++++++++++++++++++++++++++++++++") """ Handle an image changed event. Update the image in the view. This method is called when the image_watcher detects a change @@ -358,11 +345,8 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data], self.image_stack = dask_image_stack # if dask_image_stack.image_list: # self.image_stack = dask_image_stack - print("++++++++++++++++++++++++++++ _handle_image_changed_in_list 1 ++++++++++++++++++++++++++++++++++++++++++") self.presenter.update_image_list(image_files) - print("++++++++++++++++++++++++++++ _handle_image_changed_in_list 2 ++++++++++++++++++++++++++++++++++++++++++") self.presenter.update_image_stack(self.image_stack) - print("++++++++++++++++++++++++++++ _handle_image_changed_in_list 3 ++++++++++++++++++++++++++++++++++++++++++") def handle_image_modified(self, image_path: Path): self.image_stack.remove_image_data_by_path(image_path) @@ -523,8 +507,6 @@ def _handle_directory_change(self) -> None: self.image_stack.update_image_list(images) if 'mean' in self.image_stack.param_to_calc: - print("<<<<<<<<<<<<< UPDATE SPECTRUM EMITTED!!! <<<<<<<<<<<<<<<<<<<<<<<<<<<") - print(f"{self.image_stack.mean=}") self.update_spectrum.emit(self.image_stack.mean) self.update_recent_watcher(images[-1:]) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 7dad15979b9..bf1fa0b8614 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -74,19 +74,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: @@ -106,8 +99,6 @@ def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskIma """ Display image in the view after validating contents """ - # print(f"display_image: {[image.image_path for image in delayed_image_stack.image_list]=}") - # print(f"display_image: {len(delayed_image_stack.delayed_stack)=}") try: if (delayed_image_stack is None or delayed_image_stack.delayed_stack is None or not delayed_image_stack.create_delayed_array): @@ -126,7 +117,6 @@ def display_image(self, image_data_obj: Image_Data, delayed_image_stack: DaskIma 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() - print(f"{self.view.live_viewer.get_roi()=}") self.model.image_stack.set_roi(self.view.live_viewer.get_roi()) image_data = self.perform_operations(image_data) self.model.image_stack.update_param_calculations() diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index 42a5af7ba55..1c41dd447d4 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"""