# -*- coding: utf-8 -*-
from __future__ import annotations
import sys
from collections.abc import Callable
from typing import TYPE_CHECKING
import numpy as np
from guidata.dataset import update_dataset
from guidata.utils.misc import assert_interfaces_valid
from qtpy import QtCore as QC
from plotpy.coords import canvas_to_axes
from plotpy.interfaces import (
IBaseImageItem,
IBasePlotItem,
IColormapImageItemType,
IImageItemType,
ITrackableItemType,
IVoiImageItemType,
)
from plotpy.items.image.base import BaseImageItem
try:
from plotpy._scaler import _scale_xy
except ImportError:
print(
("Module 'plotpy.items.image.filter': missing C extension"),
file=sys.stderr,
)
print(
("try running :python setup.py build_ext --inplace -c mingw32"),
file=sys.stderr,
)
raise
if TYPE_CHECKING:
import qwt.color_map
import qwt.scale_map
from qtpy.QtCore import QPointF, QRectF
from qtpy.QtGui import QPainter
from plotpy.interfaces import IItemType
from plotpy.items import RawImageItem, XYImageItem
from plotpy.styles import ImageFilterParam, ItemParameters
from plotpy.widgets.colormap.widget import EditableColormap
# ==============================================================================
# Image with custom X, Y axes
# ==============================================================================
def to_bins(x):
"""Convert point center to point bounds
Args:
x: 1D array of point centers
Returns:
1D array of bin edges (length = len(x) + 1)
Note:
When x has only 1 element, we assume a bin width of 1.0 centered on the point.
"""
bx = np.zeros((x.shape[0] + 1,), float)
if x.shape[0] == 1:
# Single point: assume bin width of 1.0 centered on the point
bx[0] = x[0] - 0.5
bx[1] = x[0] + 0.5
else:
bx[1:-1] = (x[:-1] + x[1:]) / 2
bx[0] = x[0] - (x[1] - x[0]) / 2
bx[-1] = x[-1] + (x[-1] - x[-2]) / 2
return bx
# TODO: Implement get_filter methods for image items other than XYImageItem!
[docs]
class ImageFilterItem(BaseImageItem):
"""
Construct a rectangular area image filter item
* image: :py:class:`.RawImageItem` instance
* filter: function (x, y, data) --> data
* param: image filter parameters
(:py:class:`.ImageFilterParam` instance)
"""
__implements__ = (IBasePlotItem, IBaseImageItem)
_can_select = True
_can_resize = True
_can_move = True
_icon_name = "funct.png"
def __init__(
self, image: RawImageItem | None, filter: Callable, param: ImageFilterParam
) -> None:
self.use_source_cmap = None
# BaseImageItem constructor will try to set this item's color map
# using the method 'set_color_map'
self.image: RawImageItem | None = None
super().__init__(param=param)
self.border_rect.set_style("plot", "shape/imagefilter")
self.image = image
self.filter = filter
self.imagefilterparam = param
self.imagefilterparam.update_imagefilter(self)
# ---- Public API -----------------------------------------------------------
[docs]
def set_image(self, image: RawImageItem | None) -> None:
"""
Set the image item on which the filter will be applied
* image: :py:class:`.RawImageItem` instance
"""
self.image = image
[docs]
def set_filter(self, filter: Callable) -> None:
"""
Set the filter function
* filter: function (x, y, data) --> data
"""
self.filter = filter
# ---- QwtPlotItem API ------------------------------------------------------
[docs]
def boundingRect(self) -> QC.QRectF:
"""Return the bounding rectangle of the shape
Returns:
Bounding rectangle of the shape
"""
x0, y0, x1, y1 = self.border_rect.get_rect()
return QC.QRectF(x0, y0, x1 - x0, y1 - y0)
# ---- IBasePlotItem API ----------------------------------------------------
[docs]
def update_item_parameters(self) -> None:
"""Update item parameters (dataset) from object properties"""
BaseImageItem.update_item_parameters(self)
self.imagefilterparam.update_param(self)
[docs]
def get_item_parameters(self, itemparams: ItemParameters) -> None:
"""
Appends datasets to the list of DataSets describing the parameters
used to customize apearance of this item
Args:
itemparams: Item parameters
"""
BaseImageItem.get_item_parameters(self, itemparams)
self.update_item_parameters()
itemparams.add("ImageFilterParam", self, self.imagefilterparam)
[docs]
def set_item_parameters(self, itemparams: ItemParameters) -> None:
"""
Change the appearance of this item according
to the parameter set provided
Args:
itemparams: Item parameters
"""
update_dataset(
self.imagefilterparam, itemparams.get("ImageFilterParam"), visible_only=True
)
self.imagefilterparam.update_imagefilter(self)
BaseImageItem.set_item_parameters(self, itemparams)
[docs]
def move_local_point_to(self, handle: int, pos: QPointF, ctrl: bool = None) -> None:
"""Move a handle as returned by hit_test to the new position
Args:
handle: Handle
pos: Position
ctrl: True if <Ctrl> button is being pressed, False otherwise
"""
npos = canvas_to_axes(self, pos)
self.border_rect.move_point_to(handle, npos, ctrl)
if self.plot():
self.plot().SIG_ITEM_HANDLE_MOVED.emit(self)
[docs]
def move_local_shape(self, old_pos: QPointF, new_pos: QPointF) -> None:
"""Translate the shape such that old_pos becomes new_pos in canvas coordinates
Args:
old_pos: Old position
new_pos: New position
"""
old_pt = canvas_to_axes(self, old_pos)
new_pt = canvas_to_axes(self, new_pos)
self.border_rect.move_shape(old_pt, new_pt)
if self.plot():
self.plot().SIG_ITEM_MOVED.emit(self, *(old_pt + new_pt))
[docs]
def move_with_selection(self, delta_x: float, delta_y: float) -> None:
"""Translate the item together with other selected items
Args:
delta_x: Translation in plot coordinates along x-axis
delta_y: Translation in plot coordinates along y-axis
"""
self.border_rect.move_with_selection(delta_x, delta_y)
[docs]
def set_color_map(
self, name_or_table: str | EditableColormap, invert: bool | None = None
) -> None:
"""Set colormap
Args:
name_or_table: Colormap name or colormap
invert: True to invert colormap, False otherwise (Default value = None,
i.e. do not change the default behavior)
"""
if self.use_source_cmap:
if self.image is not None:
self.image.set_color_map(name_or_table, invert)
else:
BaseImageItem.set_color_map(self, name_or_table, invert)
[docs]
def get_color_map(self) -> qwt.color_map.QwtLinearColorMap:
"""Get colormap"""
if self.use_source_cmap:
return self.image.get_color_map()
else:
return BaseImageItem.get_color_map(self)
[docs]
def get_lut_range(self) -> tuple[float, float]:
"""Get the current active lut range
Returns:
tuple[float, float]: Lut range, tuple(min, max)
"""
if self.use_source_cmap:
return self.image.get_lut_range()
else:
return BaseImageItem.get_lut_range(self)
[docs]
def set_lut_range(self, lut_range: tuple[float, float]) -> None:
"""
Set the current active lut range
Args:
lut_range: Lut range, tuple(min, max)
Example:
>>> item.set_lut_range((0.0, 1.0))
"""
if self.use_source_cmap:
self.image.set_lut_range(lut_range)
else:
BaseImageItem.set_lut_range(self, lut_range)
# ---- IBaseImageItem API ---------------------------------------------------
[docs]
def types(self) -> tuple[type[IItemType], ...]:
"""Returns a group or category for this item.
This should be a tuple of class objects inheriting from IItemType
Returns:
tuple: Tuple of class objects inheriting from IItemType
"""
return (
IImageItemType,
IVoiImageItemType,
IColormapImageItemType,
ITrackableItemType,
)
[docs]
def can_sethistogram(self) -> bool:
"""
Returns True if this item can be associated with a levels histogram
Returns:
bool: True if item can be associated with a levels histogram,
False otherwise
"""
return True
[docs]
class XYImageFilterItem(ImageFilterItem):
"""
Construct a rectangular area image filter item
* image: :py:class:`.XYImageItem` instance
* filter: function (x, y, data) --> data
* param: image filter parameters
(:py:class:`.ImageFilterParam` instance)
"""
def __init__(
self, image: XYImageItem, filter: Callable, param: ImageFilterParam
) -> None:
self.image: XYImageItem | None = None
super().__init__(image, filter, param)
[docs]
def set_image(self, image):
"""
Set the image item on which the filter will be applied
* image: :py:class:`.XYImageItem` instance
"""
ImageFilterItem.set_image(self, image)
[docs]
def draw_image(
self,
painter: QPainter,
canvasRect: QRectF,
src_rect: tuple[float, float, float, float],
dst_rect: tuple[float, float, float, float],
xMap: qwt.scale_map.QwtScaleMap,
yMap: qwt.scale_map.QwtScaleMap,
) -> None:
"""Draw image
Args:
painter: Painter
canvasRect: Canvas rectangle
src_rect: Source rectangle
dst_rect: Destination rectangle
xMap: X axis scale map
yMap: Y axis scale map
"""
bounds = self.boundingRect()
filt_qrect = bounds & self.image.boundingRect()
x0, y0, x1, y1 = filt_qrect.getCoords()
i0, i1 = int(xMap.transform(x0)), int(xMap.transform(x1))
j0, j1 = int(yMap.transform(y0)), int(yMap.transform(y1))
dstRect = QC.QRect(i0, j0, i1 - i0, j1 - j0)
if not dstRect.intersects(canvasRect.toAlignedRect()):
return
x, y, data = self.image.get_data(x0, y0, x1, y1)
new_data = self.filter(x, y, data)
self.data = new_data
if self.use_source_cmap:
lut = self.image.lut
else:
lut = self.lut
W = canvasRect.width()
H = canvasRect.height()
if W <= 1 or H <= 1:
return
# x, y = x - self.image.x[0], y - self.image.y[0]
dest = _scale_xy(
new_data,
(x, y, src_rect),
self._offscreen,
dstRect.getCoords(),
lut,
self.interpolate,
)
qrect = QC.QRectF(QC.QPointF(dest[0], dest[1]), QC.QPointF(dest[2], dest[3]))
painter.drawImage(qrect, self._image, qrect)
assert_interfaces_valid(ImageFilterItem)