# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable
import numpy as np
from guidata.dataset import (
BeginGroup,
BoolItem,
ChoiceItem,
ColorItem,
DataSet,
EndGroup,
FloatItem,
GetAttrProp,
ImageChoiceItem,
IntItem,
StringItem,
)
from qtpy import QtGui as QG
from plotpy._scaler import INTERP_AA, INTERP_LINEAR, INTERP_NEAREST
from plotpy.config import _
from plotpy.constants import LUTAlpha
from plotpy.mathutils.colormap import (
ALL_COLORMAPS,
LARGE_ICON_HEIGHT,
LARGE_ICON_ORIENTATION,
LARGE_ICON_WIDTH,
build_icon_from_cmap_name,
)
from plotpy.styles.base import ItemParameters
if TYPE_CHECKING:
from guidata.dataset import DataSet
from plotpy.items import (
BaseImageItem,
ImageFilterItem,
ImageItem,
MaskedImageItem,
MaskedXYImageItem,
RawImageItem,
RGBImageItem,
TrImageItem,
)
from plotpy.plot import BasePlot
def _create_choices(
dataset: DataSet, item: ImageChoiceItem, value: Any
) -> list[tuple[str, str, Callable[[str], QG.QIcon]]]:
"""Create the list of choices for the colormap item."""
choices: list[tuple[str, str, Callable[[str], QG.QIcon]]] = []
for cmap in ALL_COLORMAPS.values():
choices.append(
(
cmap.name,
cmap.name,
lambda name: build_icon_from_cmap_name(
name,
LARGE_ICON_WIDTH,
LARGE_ICON_HEIGHT,
LARGE_ICON_ORIENTATION,
1,
),
)
)
return choices
# TODO: Use an "enum" like LUTAlpha for the interpolation mode as well
# (and eventually for other parameters)
class BaseImageParam(DataSet):
"""Base class for image parameters."""
_multiselection = False
label = StringItem(_("Image title"), default=_("Image")).set_prop(
"display", hide=GetAttrProp("_multiselection")
)
alpha_function = ChoiceItem(
_("Alpha function"),
LUTAlpha.get_choices(),
default=LUTAlpha.NONE.value,
help=LUTAlpha.get_help(),
)
alpha = FloatItem(
_("Global alpha"), default=1.0, min=0, max=1, help=_("Global alpha value")
)
_hide_colormap = False
colormap = (
ImageChoiceItem(_("Colormap"), _create_choices, default="jet")
.set_prop("display", hide=GetAttrProp("_hide_colormap"))
.set_prop("display", size=(LARGE_ICON_WIDTH, LARGE_ICON_HEIGHT))
)
invert_colormap = BoolItem(_("Invert colormap"), default=False)
keep_lut_range = BoolItem(
_("Lock LUT range when updating data"),
default=False,
help=_(
"If enabled, the LUT range is not updated when the image data changes."
"<br>This allows to keep the same color scale for different successive "
"images. <br><br>"
"<u>Note:</u> It has no effect when a new image is added to the plot."
),
)
interpolation = ChoiceItem(
_("Interpolation"),
[
(0, _("None (nearest pixel)")),
(1, _("Linear interpolation")),
(2, _("2x2 antialiasing filter")),
(3, _("3x3 antialiasing filter")),
(5, _("5x5 antialiasing filter")),
],
default=0,
help=_("Image interpolation type"),
)
_formats = BeginGroup(_("Statistics string formatting"))
xformat = StringItem(_("X-Axis"), default=r"%.1f")
yformat = StringItem(_("Y-Axis"), default=r"%.1f")
zformat = StringItem(_("Z-Axis"), default=r"%.1f")
_end_formats = EndGroup(_("Statistics string formatting"))
def get_units(self) -> tuple[str, str, str]:
"""Get the units for the x, y and z axes.
Returns:
The units for the x, y and z axes.
"""
try:
xunit = self.xformat.split()[1]
except IndexError:
xunit = ""
try:
yunit = self.yformat.split()[1]
except IndexError:
yunit = ""
try:
zunit = self.zformat.split()[1]
except IndexError:
zunit = ""
return xunit, yunit, zunit
def update_param(self, item: BaseImageItem) -> None:
"""Update the parameters from the given image item.
Args:
item: The image item to update the parameters from.
"""
self.label = str(item.title().text())
cmap = item.get_color_map()
if cmap is not None:
self.colormap = cmap.name
self.invert_colormap = cmap.invert
interpolation = item.get_interpolation()
mode = interpolation[0]
if mode == INTERP_NEAREST:
self.interpolation = 0
elif mode == INTERP_LINEAR:
self.interpolation = 1
else:
size = interpolation[1].shape[0]
self.interpolation = size
def update_item(self, item: BaseImageItem) -> None:
"""Update the given image item from the parameters.
Args:
item: The image item to update.
"""
if isinstance(self.alpha_function, LUTAlpha):
self.alpha_function = self.alpha_function.value
plot: BasePlot = item.plot()
if plot is not None:
plot.blockSignals(True) # Avoid unwanted calls of update_param
# triggered by the setter methods below
item.setTitle(self.label)
item.set_color_map(self.colormap, self.invert_colormap)
size = self.interpolation
if size == 0:
mode = INTERP_NEAREST
elif size == 1:
mode = INTERP_LINEAR
else:
mode = INTERP_AA
item.set_interpolation(mode, size)
if plot is not None:
plot.blockSignals(False)
[docs]
class QuadGridParam(DataSet):
"""Parameters for the grid of a quad item."""
_multiselection = False
label = StringItem(_("Image title"), default=_("Image")).set_prop(
"display", hide=GetAttrProp("_multiselection")
)
alpha_function = ChoiceItem(
_("Alpha function"),
LUTAlpha.get_choices(),
default=LUTAlpha.NONE.value,
help=LUTAlpha.get_help(),
)
alpha = FloatItem(
_("Global alpha"), default=1.0, min=0, max=1, help=_("Global alpha value")
)
_hide_colormap = False
colormap = ImageChoiceItem(_("Colormap"), _create_choices, default="jet").set_prop(
"display", hide=GetAttrProp("_hide_colormap")
)
invert_colormap = BoolItem(_("Invert colormap"), default=False)
interpolation = ChoiceItem(
_("Interpolation"),
[(0, _("Quadrangle interpolation")), (1, _("Flat"))],
default=0,
help=_(
"Image interpolation type, Flat mode use fixed u,v interpolation parameters"
),
)
uflat = FloatItem(
_("Fixed U interpolation parameter"),
default=0.5,
min=0.0,
max=1.0,
help=_("For flat mode only"),
)
vflat = FloatItem(
_("Fixed V interpolation parameter"),
default=0.5,
min=0.0,
max=1.0,
help=_("For flat mode only"),
)
grid = BoolItem(_("Show grid"), default=False)
gridcolor = ColorItem(_("Grid lines color"), default="black")
[docs]
def update_param(self, item: BaseImageItem) -> None:
"""Update the parameters from the given image item.
Args:
item: The image item to update the parameters from.
"""
self.label = str(item.title().text())
cmap = item.get_color_map()
if cmap is not None:
self.colormap = cmap.name
self.invert_colormap = cmap.invert
interp, uflat, vflat = item.interpolate
self.interpolation = interp
self.uflat = uflat
self.vflat = vflat
self.grid = item.grid
[docs]
def update_item(self, item: BaseImageItem) -> None:
"""Update the given image item from the parameters.
Args:
item: The image item to update.
"""
if isinstance(self.alpha_function, LUTAlpha):
self.alpha_function = self.alpha_function.value
plot = item.plot()
if plot is not None:
plot.blockSignals(True) # Avoid unwanted calls of update_param
# triggered by the setter methods below
item.setTitle(self.label)
item.set_color_map(self.colormap, self.invert_colormap)
item.interpolate = (self.interpolation, self.uflat, self.vflat)
item.grid = self.grid
if plot is not None:
plot.blockSignals(False)
[docs]
class RawImageParam(BaseImageParam):
"""Parameters for a raw image."""
_hide_background = False
background = ColorItem(_("Background color"), default="#000000").set_prop(
"display", hide=GetAttrProp("_hide_background")
)
[docs]
def update_param(self, item: RawImageItem) -> None:
"""Update the parameters from the given image item.
Args:
item: The image item from which to update the parameters.
"""
super().update_param(item)
self.background = str(QG.QColor(item.bg_qcolor).name())
[docs]
def update_item(self, item: RawImageItem) -> None:
"""Update the given image item from the parameters.
Args:
item: The image item to update.
"""
super().update_item(item)
plot: BasePlot = item.plot()
if plot is not None:
plot.blockSignals(True) # Avoid unwanted calls of update_param
# triggered by the setter methods below
item.set_background_color(self.background)
if plot is not None:
plot.blockSignals(False)
class RawImageParam_MS(RawImageParam):
"""Parameters for a raw image (multiselection)."""
_multiselection = True
ItemParameters.register_multiselection(RawImageParam, RawImageParam_MS)
[docs]
class XYImageParam(RawImageParam):
"""Parameters for an XY image."""
pass
class XYImageParam_MS(XYImageParam):
"""Parameters for an XY image (multiselection)."""
_multiselection = True
ItemParameters.register_multiselection(XYImageParam, XYImageParam_MS)
[docs]
class ImageParam(RawImageParam):
"""Parameters for an image."""
lock_position = BoolItem(
_("Lock position"),
_("Position"),
default=True,
help=_("Locked images are not movable with the mouse"),
)
_xdata = BeginGroup(_("Image placement along X-axis"))
xmin = FloatItem(_("x|min"), default=None, check=False)
xmax = FloatItem(_("x|max"), default=None, check=False)
_end_xdata = EndGroup(_("Image placement along X-axis"))
_ydata = BeginGroup(_("Image placement along Y-axis"))
ymin = FloatItem(_("y|min"), default=None, check=False)
ymax = FloatItem(_("y|max"), default=None, check=False)
_end_ydata = EndGroup(_("Image placement along Y-axis"))
[docs]
def update_param(self, item: ImageItem) -> None:
"""Update the parameters from the given image item.
Args:
item: The image item from which to update the parameters.
"""
super().update_param(item)
self.xmin = item.xmin
self.ymin = item.ymin
self.xmax = item.xmax
self.ymax = item.ymax
[docs]
def update_item(self, item: ImageItem) -> None:
"""Update the given image item from the parameters.
Args:
item: The image item to update.
"""
super().update_item(item)
plot: BasePlot = item.plot()
if plot is not None:
plot.blockSignals(True) # Avoid unwanted calls of update_param
# triggered by the setter methods below
item.xmin = self.xmin
item.xmax = self.xmax
item.ymin = self.ymin
item.ymax = self.ymax
item.update_bounds()
item.update_border()
if plot is not None:
plot.blockSignals(False)
class ImageParam_MS(ImageParam):
"""Parameters for an image (multiselection)."""
_multiselection = True
ItemParameters.register_multiselection(ImageParam, ImageParam_MS)
[docs]
class RGBImageParam(ImageParam):
"""Parameters for an RGB image."""
_hide_background = True
_hide_colormap = True
[docs]
def update_item(self, item: RGBImageItem) -> None:
"""Update the given image item from the parameters.
Args:
item: The image item to update.
"""
super().update_item(item)
plot = item.plot()
if plot is not None:
plot.blockSignals(True) # Avoid unwanted calls of update_param
# triggered by the setter methods below
item.recompute_alpha_channel()
if plot is not None:
plot.blockSignals(False)
class RGBImageParam_MS(RGBImageParam):
"""Parameters for an RGB image (multiselection)."""
_multiselection = True
ItemParameters.register_multiselection(RGBImageParam, RGBImageParam_MS)
class MaskedImageParamMixin(DataSet):
"""Mixin for masked image parameters."""
g_mask = BeginGroup(_("Mask"))
filling_value = FloatItem(_("Filling value"), allow_none=True)
show_mask = BoolItem(_("Show image mask"), default=False)
alpha_masked = FloatItem(_("Masked area alpha"), default=0.7, min=0, max=1)
alpha_unmasked = FloatItem(_("Unmasked area alpha"), default=0.0, min=0, max=1)
_g_mask = EndGroup(_("Mask"))
def _update_item(self, item: MaskedImageItem) -> None:
"""Update the given masked image item from the parameters.
Args:
item: The masked image item to update.
"""
plot: BasePlot = item.plot()
if plot is not None:
plot.blockSignals(True) # Avoid unwanted calls of update_param
# triggered by the setter methods below
item.update_mask()
if plot is not None:
plot.blockSignals(False)
[docs]
class MaskedImageParam(ImageParam, MaskedImageParamMixin):
"""Parameters for a masked image."""
[docs]
def update_item(self, item: MaskedImageItem) -> None:
"""Update the given image item from the parameters.
Args:
item: The image item to update.
"""
super().update_item(item)
self._update_item(item)
[docs]
class MaskedXYImageParam(XYImageParam, MaskedImageParamMixin):
"""Parameters for a masked XY image."""
[docs]
def update_item(self, item: MaskedXYImageItem) -> None:
"""Update the given image item from the parameters.
Args:
item: The image item to update.
"""
super().update_item(item)
self._update_item(item)
class MaskedImageParam_MS(MaskedImageParam):
"""Parameters for a masked image (multiselection)."""
_multiselection = True
class MaskedXYImageParam_MS(MaskedXYImageParam):
"""Parameters for a masked XY image (multiselection)."""
_multiselection = True
ItemParameters.register_multiselection(MaskedImageParam, MaskedImageParam_MS)
ItemParameters.register_multiselection(MaskedXYImageParam, MaskedXYImageParam_MS)
[docs]
class ImageFilterParam(BaseImageParam):
"""Parameters for an image filter."""
label = StringItem(_("Title"), default=_("Filter"))
g1 = BeginGroup(_("Bounds"))
xmin = FloatItem(_("x|min"), default=0.0)
xmax = FloatItem(_("x|max"), default=1.0)
ymin = FloatItem(_("y|min"), default=0.0)
ymax = FloatItem(_("y|max"), default=1.0)
_g1 = EndGroup("sub-group")
use_source_cmap = BoolItem(
_("Use image colormap and level"), _("Color map"), default=True
)
[docs]
def update_param(self, item: ImageFilterItem) -> None:
"""Update the parameters from the given image filter item.
Args:
item: The image filter item from which to update the parameters.
"""
self.xmin, self.ymin, self.xmax, self.ymax = item.border_rect.get_rect()
self.use_source_cmap = item.use_source_cmap
super().update_param(item)
[docs]
def update_imagefilter(self, item: ImageFilterItem) -> None:
"""Update the given image filter item from the parameters.
Args:
item: The image filter item to update.
"""
m, M = item.get_lut_range()
set_range = False
if not self.use_source_cmap and item.use_source_cmap:
set_range = True
item.use_source_cmap = self.use_source_cmap
if set_range:
item.set_lut_range([m, M])
self.update_item(item)
item.border_rect.set_rect(self.xmin, self.ymin, self.xmax, self.ymax)
[docs]
class TrImageParam(RawImageParam):
"""Parameters for a transformed image."""
lock_position = BoolItem(
_("Lock position"),
_("Position"),
default=False,
help=_("Locked images are not movable with the mouse"),
)
_crop = BeginGroup(_("Crop")).set_prop(
"display", hide=GetAttrProp("_multiselection")
)
crop_left = IntItem(_("Left"), default=0)
crop_right = IntItem(_("Right"), default=0)
crop_top = IntItem(_("Top"), default=0)
crop_bottom = IntItem(_("Bottom"), default=0)
_end_crop = EndGroup(_("Cropping")).set_prop(
"display", hide=GetAttrProp("_multiselection")
)
_ps = BeginGroup(_("Pixel size")).set_prop(
"display", hide=GetAttrProp("_multiselection")
)
dx = FloatItem(_("Width (dx)"), default=1.0)
dy = FloatItem(_("Height (dy)"), default=1.0)
_end_ps = EndGroup(_("Pixel size")).set_prop(
"display", hide=GetAttrProp("_multiselection")
)
_pos = BeginGroup(_("Translate, rotate and flip"))
pos_x0 = FloatItem(_("x<sub>CENTER</sub>"), default=0.0).set_prop(
"display", hide=GetAttrProp("_multiselection")
)
hflip = BoolItem(_("Flip horizontally"), default=False).set_prop("display", col=1)
pos_y0 = FloatItem(_("y<sub>CENTER</sub>"), default=0.0).set_prop(
"display", hide=GetAttrProp("_multiselection")
)
vflip = BoolItem(_("Flip vertically"), default=False).set_prop("display", col=1)
pos_angle = FloatItem(_("θ (°)"), default=0.0).set_prop("display", col=0)
_end_pos = EndGroup(_("Translate, rotate and flip"))
[docs]
def update_param(self, item: TrImageItem) -> None:
"""Update the parameters from the given image item.
Args:
item: The image item from which to update the parameters.
"""
super().update_param(item)
# we don't get crop info from the image because
# its not easy to extract from the transform
# and TrImageItem keeps it's crop information
# directly in this DataSet
[docs]
def update_item(self, item: TrImageItem) -> None:
"""Update the given image item from the parameters.
Args:
item: The image item to update.
"""
RawImageParam.update_item(self, item)
plot: BasePlot = item.plot()
if plot is not None:
plot.blockSignals(True) # Avoid unwanted calls of update_param
# triggered by the setter methods below
item.set_transform(*self.get_transform())
if plot is not None:
plot.blockSignals(False)
[docs]
def set_crop(self, left: int, top: int, right: int, bottom: int) -> None:
"""Set the crop parameters.
Args:
left: The left crop.
top: The top crop.
right: The right crop.
bottom: The bottom crop.
"""
self.crop_left = left
self.crop_right = right
self.crop_top = top
self.crop_bottom = bottom
[docs]
def get_crop(self) -> tuple[int, int, int, int]:
"""Get the crop parameters.
Returns:
The crop parameters: left, top, right, bottom.
"""
return (self.crop_left, self.crop_top, self.crop_right, self.crop_bottom)
class TrImageParam_MS(TrImageParam):
"""Parameters for a transformed image (multiselection)."""
_multiselection = True
ItemParameters.register_multiselection(TrImageParam, TrImageParam_MS)