Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions mantidimaging/gui/test/gui_system_liveviewer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def test_open_close_intensity_profile(self):
self.live_viewer_window.intensity_action.trigger()
QTest.qWait(SHORT_DELAY)
QTest.qWait(SHOW_DELAY)
wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean).any(), max_retry=600)
self.assertFalse(np.isnan(self.live_viewer_window.presenter.model.mean).any())
wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean_nan_mask).any(), max_retry=600)
self.assertFalse(np.isnan(self.live_viewer_window.presenter.model.mean_nan_mask).any())
self.assertNotEqual(self.live_viewer_window.splitter.sizes()[1], 0)
self.assertTrue(self.live_viewer_window.intensity_profile.isVisible())
self.live_viewer_window.intensity_action.trigger()
Expand All @@ -53,14 +53,14 @@ def test_roi_resized(self):
QTest.qWait(SHORT_DELAY * 4)
self.live_viewer_window.intensity_action.trigger()
QTest.qWait(SHORT_DELAY)
wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean).any(), max_retry=600)
old_mean = self.live_viewer_window.presenter.model.mean
wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean_nan_mask).any(), max_retry=600)
old_mean = self.live_viewer_window.presenter.model.mean_nan_mask
roi = self.live_viewer_window.live_viewer.roi_object
handle_index = 0
new_position = (10, 20)
roi.movePoint(handle_index, new_position)
self.live_viewer_window.presenter.model.clear_mean_partial()
wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean).any(), max_retry=600)
wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean_nan_mask).any(), max_retry=600)
QTest.qWait(SHORT_DELAY)
assert_raises(AssertionError, np.testing.assert_array_equal, old_mean,
self.live_viewer_window.presenter.model.mean)
self.live_viewer_window.presenter.model.mean_nan_mask)
34 changes: 12 additions & 22 deletions mantidimaging/gui/windows/live_viewer/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter):
self._dataset_path: Path | None = None
self.image_watcher: ImageWatcher | None = None
self.images: list[Image_Data] = []
self.mean: np.ndarray = np.empty(0)
self.mean_readable: np.ndarray = np.empty(0)
self.mean_paths: dict[Path, tuple[float, int]] = {}
self.mean_nan_mask: np.ma.MaskedArray = np.ma.empty(0)
self.roi: SensibleROI | None = None
self.image_cache = ImageCache(max_cache_size=100)
self.mean_calc_finished = False
Expand Down Expand Up @@ -191,32 +189,25 @@ def close(self) -> None:
self.image_watcher = None
self.presenter = None # type: ignore # Model instance to be destroyed -type can be inconsistent

def add_mean(self, image_path: Path, image_array: np.ndarray | None) -> None:
def add_mean(self, image_array: np.ndarray | None) -> None:
if image_array is None:
mean_to_add = np.nan
mean_readable = 0
elif self.roi is not None:
left, top, right, bottom = self.roi
mean_to_add = np.mean(image_array[top:bottom, left:right])
mean_readable = 1
else:
mean_to_add = np.mean(image_array)
mean_readable = 1
self.update_mean_dict(image_path, mean_to_add, mean_readable)
self.update_mean(mean_to_add)

def update_mean_dict(self, image_path: Path, mean_to_add: float, mean_readable: int) -> None:
self.mean_paths[image_path] = (mean_to_add, mean_readable)
self.mean = np.array(list(self.mean_paths.values()))[:, 0]
self.mean_readable = np.array(list(self.mean_paths.values()))[:, 1]
def update_mean(self, mean_to_add: float) -> None:
self.mean_nan_mask = np.ma.append(self.mean_nan_mask, np.array(mean_to_add))

def clear_mean_partial(self) -> None:
self.mean_paths = dict.fromkeys(self.mean_paths, (np.nan, 1))
self.mean = np.full(len(self.images), np.nan)
self.mean_readable = np.full(len(self.images), 1)
self.mean_nan_mask = np.ma.asarray(np.full(len(self.images), np.nan))

def calc_mean_chunk(self, chunk_size: int) -> None:
def calc_mean_chunk(self, chunk_size: int = 100) -> None:
if self.images is not None:
nanInds = np.argwhere(np.isnan(self.mean))
nanInds = np.argwhere(np.isnan(self.mean_nan_mask))
if self.roi:
left, top, right, bottom = self.roi
else:
Expand All @@ -226,13 +217,12 @@ def calc_mean_chunk(self, chunk_size: int) -> None:
try:
buffer_data = self.image_cache.load_image(self.images[ind[0]])
buffer_mean = np.mean(buffer_data[top:bottom, left:right])
mean_readable = 1
except ImageLoadFailError:
mean_readable = 0
buffer_mean = np.nan
self.mean_paths[self.images[ind[0]].image_path] = (buffer_mean, mean_readable)
np.put(self.mean, ind, buffer_mean)
np.put(self.mean_readable, ind, mean_readable)
if np.isnan(buffer_mean):
self.mean_nan_mask[ind[0]] = np.ma.masked
else:
np.put(self.mean_nan_mask, ind, buffer_mean)


class ImageWatcher(QObject):
Expand Down
66 changes: 28 additions & 38 deletions mantidimaging/gui/windows/live_viewer/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
from collections.abc import Callable
from logging import getLogger
import numpy as np
from PyQt5.QtCore import pyqtSignal, QObject, QThread, QTimer
from PyQt5.QtCore import QTimer
from astropy.io import fits
from astropy.utils.exceptions import AstropyUserWarning

from imagecodecs._deflate import DeflateError
from tifffile import tifffile
from tifffile.tifffile import TiffFileError

from mantidimaging.gui.dialogs.async_task import TaskWorkerThread
from mantidimaging.gui.mvp_base import BasePresenter
from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data
from mantidimaging.core.operations.loader import load_filter_packages
Expand All @@ -28,18 +29,7 @@
logger = getLogger(__name__)

IMAGE_lIST_UPDATE_TIME = 100


class Worker(QObject):
finished = pyqtSignal()

def __init__(self, presenter: LiveViewerWindowPresenter):
super().__init__()
self.presenter = presenter

def run(self):
self.presenter.model.calc_mean_chunk(100)
self.finished.emit()
CHUNK_SIZE = 10


class LiveViewerWindowPresenter(BasePresenter):
Expand All @@ -52,9 +42,9 @@ class LiveViewerWindowPresenter(BasePresenter):
view: LiveViewerWindowView
model: LiveViewerWindowModel
op_func: Callable
thread: QThread
worker: Worker
old_image_list_paths: list[Path] = []
try_next_mean_chunk_count: int = 0
try_next_mean_chunk_max_retry: int = 100

def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView):
super().__init__(view)
Expand Down Expand Up @@ -113,11 +103,11 @@ def update_image_list(self) -> None:
images_list_paths = [image.image_path for image in images_list]
if self.old_image_list_paths == images_list_paths[:-1]:
self.try_add_mean(images_list[-1])
self.update_intensity(self.model.mean)
self.update_intensity(self.model.mean_nan_mask)
self.old_image_list_paths = images_list_paths
else:
self.model.clear_mean_partial()
self.run_mean_chunk_calc()
self.handle_roi_moved()
self.view.set_image_range((0, len(images_list) - 1))
self.view.set_image_index(len(images_list) - 1)
self.view.set_load_as_dataset_enabled(True)
Expand All @@ -128,11 +118,11 @@ def notify_update_image_list(self) -> None:
def try_add_mean(self, image: Image_Data) -> None:
try:
image_data = self.model.image_cache.load_image(image)
self.model.add_mean(image.image_path, image_data)
self.model.add_mean(image_data)
self.update_intensity_with_mean()
except ImageLoadFailError as error:
logger.error(error.message)
self.model.add_mean(image.image_path, None)
self.model.add_mean(None)

def select_image(self, index: int) -> None:
if not self.model.images:
Expand Down Expand Up @@ -241,43 +231,43 @@ def handle_roi_moved(self) -> None:
roi = self.view.live_viewer.get_roi()
if roi != self.model.roi:
self.model.clear_mean_partial()
self.try_next_mean_chunk_count = 0
self.model.roi = roi
self.set_roi_enabled(False)
self.run_mean_chunk_calc()

def run_mean_chunk_calc(self) -> None:
self.thread = QThread()
self.worker = Worker(self)
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.finished.connect(self.thread_cleanup)
self.thread = TaskWorkerThread()
self.thread.kwargs = {"chunk_size": CHUNK_SIZE}
self.thread.task_function = self.model.calc_mean_chunk

self.thread.finished.connect(lambda: self.thread_cleanup(self.thread))
self.thread.start()

def thread_cleanup(self) -> None:
def thread_cleanup(self, thread: TaskWorkerThread) -> None:
if thread.error is not None:
logger.error("Error during background processing: %s", thread.error)
raise thread.error
self.update_intensity_with_mean()
self.set_roi_enabled(True)
if np.isnan(self.model.mean).any() and self.model.mean_readable.all():
self.try_next_mean_chunk()
if not np.isnan(self.model.mean * self.model.mean_readable).any():
if np.isnan(self.model.mean_nan_mask).any():
self.try_next_mean_chunk()

def handle_notify_roi_moved(self) -> None:
self.model.clear_mean_partial()

if not self.handle_roi_change_timer.isActive():
self.handle_roi_change_timer.start(10)

def update_intensity_with_mean(self) -> None:
self.view.intensity_profile.clearPlots()
self.view.intensity_profile.plot(self.model.mean)
self.view.intensity_profile.plot(self.model.mean_nan_mask)

def set_roi_enabled(self, enable: bool):
if self.view.live_viewer.roi_object is not None:
self.view.live_viewer.roi_object.blockSignals(not enable)

def try_next_mean_chunk(self) -> None:
if np.isnan(self.model.mean).any():
if not self.handle_roi_change_timer.isActive():
self.handle_roi_change_timer.start(10)
self.try_next_mean_chunk_count += 1
if self.try_next_mean_chunk_count > (len(self.model.images) / CHUNK_SIZE) + self.try_next_mean_chunk_max_retry:
logger.warning("Too many retries for mean calculation. Aborting.")
return
if not self.handle_roi_change_timer.isActive():
self.handle_roi_change_timer.start(10)
26 changes: 23 additions & 3 deletions mantidimaging/gui/windows/live_viewer/test/presenter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def setUp(self):
self.main_window = mock.create_autospec(MainWindowView, instance=True)
self.model = mock.create_autospec(LiveViewerWindowModel, instance=True)
self.model.image_cache = mock.create_autospec(ImageCache, instance=True)
self.model.mean_nan_mask = mock.create_autospec(np.ma.MaskedArray, instance=True)

with mock.patch("mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowModel") as mock_model:
mock_model.return_value = self.model
Expand Down Expand Up @@ -70,15 +71,34 @@ def test_WHEN_handle_notify_roi_moved_THEN_timer_started(self):
self.presenter.handle_roi_change_timer.start.assert_called_once()

def test_WHEN_nans_in_mean_THEN_handle_roi_change_timer_start(self):
self.model.mean = np.array([1, 2, 3, np.nan])
self.model.mean_nan_mask = np.ma.asarray([1, 2, 3, np.nan])
self.presenter.handle_roi_change_timer = mock.Mock()
self.presenter.handle_roi_change_timer.isActive.return_value = False
self.model.images = np.empty(4)
self.presenter.try_next_mean_chunk()
self.presenter.handle_roi_change_timer.start.assert_called_once_with(10)

def test_WHEN_no_nans_in_mean_THEN_handle_roi_change_timer_not_started(self):
self.model.mean = np.array([1, 2, 3, 4])
self.model.mean_nan_mask = np.ma.asarray([1, 2, 3, 4])
self.presenter.handle_roi_change_timer = mock.Mock()
self.presenter.handle_roi_change_timer.isActive.return_value = False
self.presenter.try_next_mean_chunk()
self.model.images = np.empty(4)
mock_thread = mock.Mock()
mock_thread.error = None
self.presenter.set_roi_enabled = mock.Mock()
self.presenter.update_intensity_with_mean = mock.Mock()
self.presenter.thread_cleanup(mock_thread)
self.presenter.handle_roi_change_timer.start.assert_not_called()

def test_WHEN_thread_error_raised_THEN_error_caught_and_logged(self):
self.model.mean_nan_mask = np.ma.asarray([1, 2, 3, 4])
self.presenter.handle_roi_change_timer = mock.Mock()
self.presenter.handle_roi_change_timer.isActive.return_value = False
self.model.images = np.empty(4)
mock_thread = mock.Mock()
mock_thread.error = ValueError()
self.presenter.set_roi_enabled = mock.Mock()
self.presenter.update_intensity_with_mean = mock.Mock()
with self.assertRaises(ValueError):
self.presenter.thread_cleanup(mock_thread)
self.presenter.handle_roi_change_timer.start.assert_not_called()
2 changes: 1 addition & 1 deletion mantidimaging/gui/windows/live_viewer/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def set_intensity_visibility(self) -> None:
self.presenter.model.clear_mean_partial()
if self.presenter.model.images:
self.presenter.handle_roi_moved()
self.presenter.update_intensity(self.presenter.model.mean)
self.presenter.update_intensity(self.presenter.model.mean_nan_mask)
else:
self.live_viewer.set_roi_visibility_flags(False)
self.splitter.setSizes([widget_height, 0])
Loading