Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live Viewer Intensity Plot recalculates when images modified #2491

Merged
merged 4 commits into from
Feb 20, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#2490: The Live Viewer Intensity plot now updates correctly when images are modified, i.e. deleted or multiple added files
7 changes: 6 additions & 1 deletion mantidimaging/eyes_tests/live_viewer_window_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
from unittest import mock
import numpy as np
import os

from PyQt5.QtTest import QTest

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.presenter import ImageLoadFailError
from mantidimaging.gui.windows.live_viewer.presenter import ImageLoadFailError, IMAGE_lIST_UPDATE_TIME
from mantidimaging.test_helpers.unit_test_helper import FakeFSTestCase
from pathlib import Path
from mantidimaging.eyes_tests.base_eyes import BaseEyesTest
Expand Down Expand Up @@ -66,6 +69,7 @@ def test_live_view_opens_with_data(self, _mock_time, _mock_image_watcher, mock_l
mock_load_image.return_value = self._generate_image()
self.imaging.show_live_viewer(self.live_directory)
self.imaging.live_viewer_list[-1].presenter.model._handle_image_changed_in_list(image_list)
QTest.qWait(int(IMAGE_lIST_UPDATE_TIME * 1.5))
self.check_target(widget=self.imaging.live_viewer_list[-1])

@mock.patch('mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowPresenter.load_image_from_path')
Expand All @@ -88,5 +92,6 @@ def test_rotate_operation_rotates_image(self, _mock_time, _mock_image_watcher, m
mock_load_image.return_value = self._generate_image()
self.imaging.show_live_viewer(self.live_directory)
self.imaging.live_viewer_list[-1].presenter.model._handle_image_changed_in_list(image_list)
QTest.qWait(int(IMAGE_lIST_UPDATE_TIME * 1.5))
self.imaging.live_viewer_list[-1].rotate_angles_group.actions()[1].trigger()
self.check_target(widget=self.imaging.live_viewer_list[-1])
2 changes: 2 additions & 0 deletions mantidimaging/gui/test/gui_system_liveviewer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def tearDown(self) -> None:

def test_open_close_intensity_profile(self):
self.assertEqual(self.live_viewer_window.splitter.sizes()[1], 0)
QTest.qWait(SHORT_DELAY * 4)
self.live_viewer_window.intensity_action.trigger()
QTest.qWait(SHORT_DELAY)
QTest.qWait(SHOW_DELAY)
Expand All @@ -49,6 +50,7 @@ def test_open_close_intensity_profile(self):
QTest.qWait(SHOW_DELAY)

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)
Expand Down
14 changes: 8 additions & 6 deletions mantidimaging/gui/windows/live_viewer/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,10 @@ 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.presenter.update_image_list(self.images)
self.presenter.notify_update_image_list()

def handle_image_modified(self, image_path: Path) -> None:
self.presenter.update_image_modified(image_path)
self.presenter.update_image_list(self.images)

def close(self) -> None:
"""Close the model."""
Expand All @@ -192,7 +191,7 @@ def close(self) -> None:
self.image_watcher = None
self.presenter = None # type: ignore # Model instance to be destroyed -type can be inconsistent

def add_mean(self, image_data_obj: Image_Data, image_array: np.ndarray | None) -> None:
def add_mean(self, image_path: Path, image_array: np.ndarray | None) -> None:
if image_array is None:
mean_to_add = np.nan
mean_readable = 0
Expand All @@ -203,9 +202,12 @@ def add_mean(self, image_data_obj: Image_Data, image_array: np.ndarray | None) -
else:
mean_to_add = np.mean(image_array)
mean_readable = 1
self.mean_paths[image_data_obj.image_path] = (mean_to_add, mean_readable)
self.mean = np.array(list(dict(sorted(self.mean_paths.items())).values()))[:, 0]
self.mean_readable = np.array(list(dict(sorted(self.mean_paths.items())).values()))[:, 1]
self.update_mean_dict(image_path, mean_to_add, mean_readable)

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 clear_mean_partial(self) -> None:
self.mean_paths = dict.fromkeys(self.mean_paths, (np.nan, 1))
Expand Down
42 changes: 33 additions & 9 deletions mantidimaging/gui/windows/live_viewer/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

logger = getLogger(__name__)

IMAGE_lIST_UPDATE_TIME = 100


class Worker(QObject):
finished = pyqtSignal()
Expand All @@ -52,6 +54,7 @@ class LiveViewerWindowPresenter(BasePresenter):
op_func: Callable
thread: QThread
worker: Worker
old_image_list_paths: list[Path] = []

def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView):
super().__init__(view)
Expand All @@ -66,6 +69,10 @@ def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView):
self.handle_roi_change_timer.setSingleShot(True)
self.handle_roi_change_timer.timeout.connect(self.handle_roi_moved)

self.update_image_list_timer = QTimer()
self.update_image_list_timer.setSingleShot(True)
self.update_image_list_timer.timeout.connect(self.update_image_list)

self.model.image_cache.use_loading_function(self.load_image_from_path)

def close(self) -> None:
Expand All @@ -90,28 +97,43 @@ def handle_deleted(self) -> None:
self.view.live_viewer.z_slider.set_range(0, 1)
self.view.live_viewer.show_error(None)

def update_image_list(self, images_list: list[Image_Data]) -> None:
def update_image_list(self) -> None:
"""Update the image in the view."""
images_list = self.model.images
if not images_list:
self.handle_deleted()
self.view.set_load_as_dataset_enabled(False)
self.model.clear_mean_partial()
self.update_intensity_with_mean()
else:
if self.view.intensity_action.isChecked():
if not self.view.live_viewer.roi_object:
self.view.live_viewer.add_roi()
self.model.roi = self.view.live_viewer.get_roi()
self.model.images = images_list
try:
image_data = self.model.image_cache.load_image(images_list[-1])
self.model.add_mean(images_list[-1], image_data)
except ImageLoadFailError as error:
logger.error(error.message)
self.model.add_mean(images_list[-1], None)
self.update_intensity(self.model.mean)
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.old_image_list_paths = images_list_paths
else:
self.model.clear_mean_partial()
self.run_mean_chunk_calc()
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)

def notify_update_image_list(self) -> None:
self.update_image_list_timer.start(IMAGE_lIST_UPDATE_TIME)

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.update_intensity_with_mean()
except ImageLoadFailError as error:
logger.error(error.message)
self.model.add_mean(image.image_path, None)

def select_image(self, index: int) -> None:
if not self.model.images:
return
Expand Down Expand Up @@ -176,6 +198,8 @@ def update_image_modified(self, image_path: Path) -> None:
"""
if self.selected_image and image_path == self.selected_image.image_path:
self.display_image(self.selected_image)
self.try_add_mean(self.selected_image)
self.update_intensity_with_mean()

def update_image_operation(self) -> None:
"""
Expand Down
3 changes: 2 additions & 1 deletion mantidimaging/gui/windows/live_viewer/test/presenter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def test_load_as_dataset_enabled_when_images(self, image_list, action_enabled):
self.model.mean = []
self.model.image_cache = mock.Mock()
self.model.add_mean = mock.Mock()
self.model.images = image_list
self.presenter.roi_moving = True
self.view.live_viewer = mock.Mock()
self.view.intensity_profile = mock.Mock()
Expand All @@ -60,7 +61,7 @@ def test_load_as_dataset_enabled_when_images(self, image_list, action_enabled):
self.view.intensity_action.isChecked = mock.Mock()
self.view.intensity_action.isChecked.return_value = False
with mock.patch.object(self.presenter, "handle_deleted"):
self.presenter.update_image_list(image_list)
self.presenter.update_image_list()

self.view.set_load_as_dataset_enabled.assert_called_once_with(action_enabled)

Expand Down
Loading