# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
.. Image panel (see parent package :mod:`datalab.gui.panel`)
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from __future__ import annotations
from typing import TYPE_CHECKING, Type
from weakref import ReferenceType, ref
from plotpy.interfaces import IVoiImageItemType
from plotpy.tools import (
AnnotatedCircleTool,
AnnotatedEllipseTool,
AnnotatedPointTool,
AnnotatedRectangleTool,
AnnotatedSegmentTool,
LabelTool,
)
from sigima.io.image import ImageIORegistry
from sigima.objects import ImageDatatypes, ImageObj, ImageROI, NewImageParam
from datalab.config import Conf, _
from datalab.gui import roieditor
from datalab.gui.actionhandler import ImageActionHandler
from datalab.gui.newobject import create_image_gui
from datalab.gui.panel.base import BaseDataPanel
from datalab.gui.plothandler import ImagePlotHandler
from datalab.gui.processor.image import ImageProcessor
from datalab.objectmodel import get_uuid
if TYPE_CHECKING:
from plotpy.plot import BasePlot
from qtpy import QtWidgets as QW
from datalab.gui.docks import DockablePlotWidget
[docs]
class ImagePanel(BaseDataPanel[ImageObj, ImageROI, roieditor.ImageROIEditor]):
"""Object handling the item list, the selected item properties and plot,
specialized for Image objects"""
PANEL_STR = _("Image Panel")
PANEL_STR_ID = "image"
PARAMCLASS = ImageObj
MINDIALOGSIZE = (800, 800)
# The following tools are used to create annotations on images. The annotation
# items are created using PlotPy's default settings. Those appearance settings
# may be modified in the configuration (see `datalab.config`).
ANNOTATION_TOOLS = (
AnnotatedCircleTool,
AnnotatedSegmentTool,
AnnotatedRectangleTool,
AnnotatedPointTool,
AnnotatedEllipseTool,
LabelTool,
)
IO_REGISTRY = ImageIORegistry
H5_PREFIX = "DataLab_Ima"
# pylint: disable=duplicate-code
[docs]
@staticmethod
def get_roi_class() -> Type[ImageROI]:
"""Return ROI class"""
return ImageROI
[docs]
@staticmethod
def get_roieditor_class() -> Type[roieditor.ImageROIEditor]:
"""Return ROI editor class"""
return roieditor.ImageROIEditor
def __init__(
self,
parent: QW.QWidget,
dockableplotwidget: DockablePlotWidget,
panel_toolbar: QW.QToolBar,
) -> None:
super().__init__(parent)
self._contrast_sync_in_progress = False
self._contrast_editors: dict[
str, list[ReferenceType[roieditor.ImageROIEditor]]
] = {}
self.plothandler = ImagePlotHandler(self, dockableplotwidget.plotwidget)
self.processor = ImageProcessor(self, dockableplotwidget.plotwidget)
view_toolbar = dockableplotwidget.toolbar
self.acthandler = ImageActionHandler(self, panel_toolbar, view_toolbar)
[docs]
def register_contrast_editor(
self, obj: ImageObj, editor: roieditor.ImageROIEditor
) -> None:
"""Register an image ROI editor for contrast synchronization."""
obj_uuid = get_uuid(obj)
editors = self._contrast_editors.setdefault(obj_uuid, [])
for editor_ref in list(editors):
current_editor = editor_ref()
if current_editor is None:
editors.remove(editor_ref)
elif current_editor is editor:
return
editors.append(ref(editor))
item = self.plothandler.get(obj_uuid)
if item is not None:
zmin, zmax = item.get_lut_range()
editor.apply_shared_contrast(zmin, zmax)
def _update_contrast_panel_range(self, zmin: float, zmax: float) -> None:
"""Update contrast panel range without re-emitting LUT signals."""
contrast = self.plothandler.plotwidget.manager.get_contrast_panel()
if contrast is None:
return
contrast.histogram.range.set_range(zmin, zmax, dosignal=False)
contrast.histogram.replot()
def _update_object_contrast_state(
self, obj: ImageObj, zmin: float, zmax: float, update_panel: bool = False
) -> None:
"""Update object and current panel state for a contrast change."""
obj.zscalemin, obj.zscalemax = zmin, zmax
if obj is self.objview.get_current_object():
self.objprop.update_properties_from(obj)
if update_panel:
self._update_contrast_panel_range(zmin, zmax)
[docs]
def apply_shared_contrast(
self,
obj: ImageObj,
zmin: float,
zmax: float,
source: roieditor.ImageROIEditor | None = None,
) -> None:
"""Apply a contrast change coming from another view."""
del source # unused: kept for API symmetry with _notify_contrast_editors
self._update_object_contrast_state(obj, zmin, zmax, update_panel=True)
item = self.plothandler.get(get_uuid(obj))
if item is None:
return
if item.get_lut_range() == (zmin, zmax):
return
self._contrast_sync_in_progress = True
try:
item.set_lut_range((zmin, zmax))
plot = self.plothandler.plot
plot.update_colormap_axis(item)
plot.notify_colormap_changed()
finally:
self._contrast_sync_in_progress = False
def _notify_contrast_editors(
self,
obj: ImageObj,
zmin: float,
zmax: float,
source: roieditor.ImageROIEditor | None = None,
) -> None:
"""Propagate a contrast change to all ROI editors of an image."""
obj_uuid = get_uuid(obj)
editors = self._contrast_editors.get(obj_uuid)
if not editors:
return
alive_editors: list[ReferenceType[roieditor.ImageROIEditor]] = []
for editor_ref in editors:
editor = editor_ref()
if editor is None:
continue
alive_editors.append(editor_ref)
if editor is not source:
editor.apply_shared_contrast(zmin, zmax)
if alive_editors:
self._contrast_editors[obj_uuid] = alive_editors
else:
self._contrast_editors.pop(obj_uuid, None)
def _get_lut_changed_objects(
self, plot: BasePlot
) -> list[tuple[ImageObj, float, float]]:
"""Return image objects affected by a LUT change on the plot."""
changed_objects: list[tuple[ImageObj, float, float]] = []
items = plot.get_selected_items(item_type=IVoiImageItemType)
if not items:
active_item = plot.get_last_active_item(IVoiImageItemType)
items = [] if active_item is None else [active_item]
for item in items:
obj = self.plothandler.get_obj_from_item(item)
if not isinstance(obj, ImageObj):
continue
zmin, zmax = item.get_lut_range()
changed_objects.append((obj, zmin, zmax))
return changed_objects
# ------Refreshing GUI--------------------------------------------------------------
[docs]
def plot_lut_changed(self, plot: BasePlot) -> None:
"""The LUT of the plot has changed: updating image objects accordingly
Args:
plot: Plot object
"""
for obj, zmin, zmax in self._get_lut_changed_objects(plot):
self._update_object_contrast_state(obj, zmin, zmax, update_panel=True)
if not self._contrast_sync_in_progress:
self._notify_contrast_editors(obj, zmin, zmax)
# ------Creating, adding, removing objects------------------------------------------
[docs]
def get_newparam_from_current(
self, newparam: NewImageParam | None = None, title: str | None = None
) -> NewImageParam | None:
"""Get new object parameters from the current object.
Args:
newparam (guidata.dataset.DataSet): new object parameters.
If None, create a new one.
title: new object title. If None, use the current object title, or the
default title.
Returns:
New object parameters
"""
curobj: ImageObj = self.objview.get_current_object()
if newparam is None:
newparam = NewImageParam()
if title is not None:
newparam.title = title
if curobj is not None and Conf.proc.use_image_dims.get(True):
# Use current image dimensions for new image:
newparam.height, newparam.width = curobj.data.shape
newparam.dtype = ImageDatatypes.from_numpy_dtype(curobj.data.dtype)
return newparam
[docs]
def new_object(
self,
param: NewImageParam | None = None,
edit: bool = False,
add_to_panel: bool = True,
) -> ImageObj | None:
"""Create a new object (image).
Args:
param (guidata.dataset.DataSet): new object parameters
edit (bool): Open a dialog box to edit parameters (default: False).
When False, the object is created with default parameters and creation
parameters are stored in metadata for interactive editing.
add_to_panel (bool): Add the object to the panel (default: True)
Returns:
New object
"""
if not self.mainwindow.confirm_memory_state():
return None
param = self.get_newparam_from_current(param)
image = create_image_gui(param, edit=edit, parent=self.parentWidget())
if image is None:
return None
if add_to_panel:
self.add_object(image)
return image
[docs]
def toggle_show_contrast(self, state: bool) -> None:
"""Toggle show contrast option"""
Conf.view.show_contrast.set(state)
self.refresh_plot("selected", True, False)