Skip to content

Commit

Permalink
Image scroller for live view (#1917)
Browse files Browse the repository at this point in the history
  • Loading branch information
JackEAllen authored Sep 6, 2023
2 parents c5e3498 + 0c05d17 commit d9f590e
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#1916 : Image scroller for live view
3 changes: 3 additions & 0 deletions mantidimaging/gui/widgets/zslider/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (C) 2023 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations
57 changes: 57 additions & 0 deletions mantidimaging/gui/widgets/zslider/zslider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (C) 2023 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations

from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import QGraphicsSceneMouseEvent
from pyqtgraph import PlotItem, InfiniteLine


class ZSlider(PlotItem):
"""
A plot item to draw a z-axis slider mimicking the slider in PyQtGraph's ImageView
This gives us flexibility to choose what happens when the user move through the z-axis. It can be combined with
the one or more :py:class:`~mantidimaging.gui.widgets.mi_mini_image_view.view.MIMiniImageView`'s in a
GraphicsLayoutWidget. It is used in the Operations window to choose the slice to preview a filter with,
and in the Live Viewer scroll through images.
Emits a :code:`valueChanged` signal when the user moves the slider
"""

z_line: InfiniteLine
valueChanged = pyqtSignal(int)

def __init__(self) -> None:
super().__init__()
self.setFixedHeight(40)
self.hideAxis("left")
self.setXRange(0, 1)
self.setMouseEnabled(x=False, y=False)
self.hideButtons()

self.z_line = InfiniteLine(0, movable=True)
self.z_line.setPen((255, 255, 0, 200))
self.addItem(self.z_line)

self.z_line.sigPositionChanged.connect(self.value_changed)

def set_range(self, min: int, max: int) -> None:
self.z_line.setValue(min)
self.setXRange(min, max)
self.z_line.setBounds([min, max])

def set_value(self, value: int) -> None:
self.z_line.setValue(value)

def value_changed(self) -> None:
self.valueChanged.emit(int(self.z_line.value()))

def mousePressEvent(self, ev: 'QGraphicsSceneMouseEvent') -> None:
"""
Adjusts built in behaviour to allow user to click anywhere on the line to jump there.
"""
if ev.button() == Qt.MouseButton.LeftButton:
x = round(self.vb.mapSceneToView(ev.scenePos()).x())
self.set_value(x)
super().mousePressEvent(ev)
5 changes: 5 additions & 0 deletions mantidimaging/gui/windows/live_viewer/live_view_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import TYPE_CHECKING
from pyqtgraph import GraphicsLayoutWidget
from mantidimaging.gui.widgets.mi_mini_image_view.view import MIMiniImageView
from mantidimaging.gui.widgets.zslider.zslider import ZSlider

if TYPE_CHECKING:
import numpy as np
Expand All @@ -22,6 +23,10 @@ def __init__(self) -> None:

self.image = MIMiniImageView(name="Projection")
self.addItem(self.image, 0, 0)
self.nextRow()

self.z_slider = ZSlider()
self.addItem(self.z_slider)

def show_image(self, image: np.ndarray) -> None:
"""
Expand Down
65 changes: 25 additions & 40 deletions mantidimaging/gui/windows/live_viewer/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class LiveViewerWindowModel:
presenter for the spectrum viewer window
path : Path
path to dataset
images : list
list of images in directory
"""

def __init__(self, presenter: 'LiveViewerWindowPresenter'):
Expand All @@ -87,6 +89,7 @@ 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] = []

@property
def path(self) -> Path | None:
Expand All @@ -98,7 +101,6 @@ 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.find_images()
self.image_watcher.get_images()

def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None:
"""
Expand All @@ -108,11 +110,8 @@ def _handle_image_changed_in_list(self, image_files: list[Image_Data]) -> None:
:param image_files: list of image files
"""
if not image_files:
self.presenter.handle_deleted()
self.presenter.update_image([])
else:
self.presenter.update_image(image_files)
self.images = image_files
self.presenter.update_image_list(image_files)


class ImageWatcher(QObject):
Expand All @@ -127,21 +126,15 @@ class ImageWatcher(QObject):
path to directory to watch
watcher : QFileSystemWatcher
file system watcher to watch directory
images : list
list of images in directory
image_changed : pyqtSignal
signal emitted when an image is added or removed
Methods
-------
find_images()
Find all the images in the directory and emit the
image_changed signal for the last modified image.
Find all the images in the directory
sort_images_by_modified_time(images)
Sort the images by modified time.
find_last_modified_image()
Find the last modified image in the directory and
emit the image_changed signal.
"""
image_changed = pyqtSignal(list) # Signal emitted when an image is added or removed

Expand All @@ -160,16 +153,25 @@ def __init__(self, directory: Path):
self.watcher = QFileSystemWatcher()
self.watcher.directoryChanged.connect(self._handle_directory_change)
self.watcher.addPath(str(self.directory))
self.images: list[Image_Data] = []

def find_images(self) -> None:
def find_images(self) -> list[Image_Data]:
"""
Find all the images in the directory and emit the
image_changed signal for the last modified image.
Find all the images in the directory.
"""
self.images = self._get_image_files()
image_files = []
for file_path in Path(self.directory).iterdir():
if self._is_image_file(file_path.name):
try:
image_obj = Image_Data(file_path)
if image_obj.image_size > 45:
image_files.append(image_obj)
except FileNotFoundError:
continue

return image_files

def sort_images_by_modified_time(self, images: list[Image_Data]) -> list[Image_Data]:
@staticmethod
def sort_images_by_modified_time(images: list[Image_Data]) -> list[Image_Data]:
"""
Sort the images by modified time.
Expand All @@ -178,35 +180,18 @@ def sort_images_by_modified_time(self, images: list[Image_Data]) -> list[Image_D
"""
return sorted(images, key=lambda x: x.image_modified_time)

def get_images(self) -> list[Image_Data]:
"""Return the sorted images"""
return self.images

def _handle_directory_change(self, directory: str) -> None:
"""
Handle a directory change event. Update the list of images
to reflect directory changes and emit the image_changed signal
for the last modified image.
with the sorted image list.
:param directory: directory that has changed
"""
try:
self.find_images()
self.image_changed.emit(self.images)
except FileNotFoundError:
self.image_changed.emit([])

def _get_image_files(self) -> list[Image_Data]:
image_files = []
for file_path in Path(self.directory).iterdir():
if self._is_image_file(file_path.name):
try:
image_obj = Image_Data(file_path)
if image_obj.image_size > 45:
image_files.append(image_obj)
except FileNotFoundError:
continue
return self.sort_images_by_modified_time(image_files)
images = self.find_images()
images = self.sort_images_by_modified_time(images)
self.image_changed.emit(images)

@staticmethod
def _is_image_file(file_name: str) -> bool:
Expand Down
22 changes: 16 additions & 6 deletions mantidimaging/gui/windows/live_viewer/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,29 @@ def handle_deleted(self) -> None:
"""Handle the deletion of the image."""
self.view.remove_image()
self.clear_label()
self.view.live_viewer.z_slider.set_range(0, 1)

def update_image(self, images_list: list[Image_Data]) -> None:
def update_image_list(self, images_list: list[Image_Data]) -> None:
"""Update the image in the view."""
if not images_list:
self.view.remove_image()
self.handle_deleted()
return

self.view.live_viewer.z_slider.set_range(0, len(images_list) - 1)
self.view.set_image_index(len(images_list) - 1)

def select_image(self, index: int) -> None:
if not self.model.images:
return
latest_image = images_list[-1]
selected_image = self.model.images[index]
self.view.label_active_filename.setText(selected_image.image_name)

try:
with tifffile.TiffFile(latest_image.image_path) as tif:
with tifffile.TiffFile(selected_image.image_path) as tif:
image_data = tif.asarray()
except (IOError, KeyError, ValueError, DeflateError) as error:
logger.error("%s reading image: %s: %s", type(error).__name__, latest_image.image_path, error)
logger.error("%s reading image: %s: %s", type(error).__name__, selected_image.image_path, error)
self.view.remove_image()
return

self.view.show_most_recent_image(image_data)
self.view.label_active_filename.setText(latest_image.image_name)
5 changes: 5 additions & 0 deletions mantidimaging/gui/windows/live_viewer/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def __init__(self, main_window: 'MainWindowView', live_dir_path: Path) -> None:
# reposition to the right of the main window to be visible when launched from cli
self.move(self.main_window.x() + self.main_window.width(), self.main_window.y())

self.live_viewer.z_slider.valueChanged.connect(self.presenter.select_image)

def show_most_recent_image(self, image: np.ndarray) -> None:
"""
Show the most recently modified image in the image view.
Expand All @@ -50,3 +52,6 @@ def watch_directory(self) -> None:
def remove_image(self) -> None:
"""Remove the image from the view."""
self.live_viewer.handle_deleted()

def set_image_index(self, index: int) -> None:
self.live_viewer.z_slider.set_value(index)
45 changes: 3 additions & 42 deletions mantidimaging/gui/windows/operations/filter_previews.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@
from __future__ import annotations

from logging import getLogger
from typing import TYPE_CHECKING

import numpy as np
from PyQt5.QtCore import Qt, QPoint, QRect, pyqtSignal
from PyQt5.QtCore import QPoint, QRect
from PyQt5.QtGui import QGuiApplication, QResizeEvent
from pyqtgraph import ColorMap, GraphicsLayoutWidget, ImageItem, LegendItem, PlotItem, InfiniteLine
from pyqtgraph import ColorMap, GraphicsLayoutWidget, ImageItem, LegendItem, PlotItem
from pyqtgraph.graphicsItems.GraphicsLayout import GraphicsLayout

from mantidimaging.core.utility.histogram import set_histogram_log_scale
from mantidimaging.gui.widgets.mi_mini_image_view.view import MIMiniImageView

if TYPE_CHECKING:
from PyQt5.QtWidgets import QGraphicsSceneMouseEvent
from mantidimaging.gui.widgets.zslider.zslider import ZSlider

LOG = getLogger(__name__)

Expand All @@ -32,42 +29,6 @@ def _data_valid_for_histogram(data) -> bool:
return data is not None and any(d is not None for d in data)


class ZSlider(PlotItem):
z_line: InfiniteLine
valueChanged = pyqtSignal(int)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFixedHeight(40)
self.hideAxis("left")
self.setXRange(0, 1)
self.setMouseEnabled(x=False, y=False)
self.hideButtons()

self.z_line = InfiniteLine(0, movable=True)
self.z_line.setPen((255, 255, 0, 200))
self.addItem(self.z_line)

self.z_line.sigPositionChanged.connect(self.value_changed)

def set_range(self, min: int, max: int):
self.z_line.setValue(min)
self.setXRange(min, max)
self.z_line.setBounds([min, max])

def set_value(self, value: int):
self.z_line.setValue(value)

def value_changed(self):
self.valueChanged.emit(int(self.z_line.value()))

def mousePressEvent(self, ev: 'QGraphicsSceneMouseEvent'):
if ev.button() == Qt.MouseButton.LeftButton:
x = round(self.vb.mapSceneToView(ev.scenePos()).x())
self.set_value(x)
super().mousePressEvent(ev)


class FilterPreviews(GraphicsLayoutWidget):
histogram: PlotItem
z_slider: ZSlider
Expand Down

0 comments on commit d9f590e

Please sign in to comment.