Source code for plotpy.items.image.base

# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see plotpy/LICENSE for details)

from __future__ import annotations

import os
import os.path as osp
from typing import TYPE_CHECKING, Any

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 qtpy import QtGui as QG
from qwt import QwtLinearScaleEngine, QwtPlotItem

from plotpy import io
from plotpy._scaler import (
    INTERP_AA,
    INTERP_LINEAR,
    INTERP_NEAREST,
    _histogram,
    _scale_rect,
)
from plotpy.config import _
from plotpy.constants import LUT_MAX, LUT_SIZE, LUTAlpha
from plotpy.coords import pixelround
from plotpy.interfaces import (
    IBaseImageItem,
    IBasePlotItem,
    IColormapImageItemType,
    ICSImageItemType,
    IExportROIImageItemType,
    IHistDataSource,
    IImageItemType,
    ISerializableType,
    ITrackableItemType,
    IVoiImageItemType,
)

# do not import rectangleshape from plotpy.items directly
from plotpy.items.shape.rectangle import RectangleShape
from plotpy.lutrange import lut_range_threshold
from plotpy.mathutils.arrayfuncs import get_nan_range
from plotpy.mathutils.colormap import FULLRANGE, get_cmap
from plotpy.styles.image import RawImageParam

if TYPE_CHECKING:
    import guidata.io
    import qwt.color_map
    import qwt.scale_map
    from qtpy.QtCore import QPointF, QRectF
    from qtpy.QtGui import QColor, QPainter

    from plotpy.interfaces import IItemType
    from plotpy.plot import BasePlot
    from plotpy.styles.base import ItemParameters
    from plotpy.widgets.colormap.widget import EditableColormap


[docs] class BaseImageItem(QwtPlotItem): """Base class for image items Args: data: Image data param: Image parameters """ __implements__ = ( IBasePlotItem, IBaseImageItem, IHistDataSource, IVoiImageItemType, ICSImageItemType, IExportROIImageItemType, ) _can_select = True _can_resize = False _can_move = False _can_rotate = False _readonly = False _private = False _icon_name = "image.png" def __init__( self, data: np.ndarray | None = None, param: Any | None = None ) -> None: super().__init__() self.bg_qcolor = QG.QColor() self.bounds = QC.QRectF() # BaseImageItem needs: # param.background # param.alpha_function # param.alpha # param.colormap if param is None: param = self.get_default_param() self.param = param self.selected = False self.data: np.ndarray | None = None self.min = 0.0 self.max = 1.0 self.cmap_table = None self.cmap = None self.colormap_axis = None self._image: QG.QImage | None = None self._offscreen = np.array((1, 1), np.uint32) # Linear interpolation is the default interpolation algorithm: # it's almost as fast as 'nearest pixel' method but far smoother self.interpolate = None self.set_interpolation(INTERP_LINEAR) x1, y1 = self.bounds.left(), self.bounds.top() x2, y2 = self.bounds.right(), self.bounds.bottom() self.border_rect = RectangleShape(x1, y1, x2, y2) self.border_rect.set_style("plot", "shape/imageborder") # A, B, Background, Colormap self.lut = (1.0, 0.0, None, np.zeros((LUT_SIZE,), np.uint32)) self.set_lut_range((0.0, 255.0)) self.setItemAttribute(QwtPlotItem.AutoScale) self.setItemAttribute(QwtPlotItem.Legend, True) self._filename = None # The file this image comes from self.histogram_cache = None # Z-axis logarithmic scale support self._log_data: np.ndarray | None = None self._lin_lut_range: tuple[float, float] | None = None self._is_zaxis_log = False if data is not None: self.set_data(data) self.param.update_item(self) # ---- Public API ----------------------------------------------------------
[docs] def get_default_param(self) -> Any: """Return instance of the default image param DataSet""" raise NotImplementedError
[docs] def set_filename(self, fname: str) -> None: """Set the filename of the image Args: fname: Filename """ self._filename = fname
[docs] def get_filename(self) -> str | None: """Get the filename of the image Returns: Filename """ fname = self._filename if fname is not None and not osp.isfile(fname): other_try = osp.join(os.getcwd(), osp.basename(fname)) if osp.isfile(other_try): self.set_filename(other_try) fname = other_try return fname
[docs] def get_filter(self, filterobj, filterparam): """Provides a filter object over this image's content""" raise NotImplementedError
[docs] def get_pixel_coordinates(self, xplot: float, yplot: float) -> tuple[float, float]: """Get pixel coordinates from plot coordinates Args: xplot: X plot coordinate yplot: Y plot coordinate Returns: Pixel coordinates """ return xplot, yplot
[docs] def get_plot_coordinates(self, xpixel: float, ypixel: float) -> tuple[float, float]: """Get plot coordinates from pixel coordinates Args: xpixel: X pixel coordinate ypixel: Y pixel coordinate Returns: Plot coordinates """ return xpixel, ypixel
[docs] def get_closest_indexes( self, x: float, y: float, corner: str | None = None ) -> tuple[int, int]: """Get closest image pixel indexes to the given coordinates Args: x: X coordinate y: Y coordinate corner: None (not a corner), 'TL' (top-left corner), 'BR' (bottom-right corner) Returns: Closest image pixel indexes """ x, y = self.get_pixel_coordinates(x, y) i_max = self.data.shape[1] - 1 j_max = self.data.shape[0] - 1 if corner == "BR": i_max += 1 j_max += 1 i = max([0, min([i_max, int(pixelround(x, corner))])]) j = max([0, min([j_max, int(pixelround(y, corner))])]) return i, j
[docs] def get_closest_index_rect( self, x0: float, y0: float, x1: float, y1: float, avoid_empty: bool = True ) -> tuple[int, int, int, int]: """Get closest image rectangular pixel area index bounds, optionally avoid returning an empty rectangular area (return at least 1x1 pixel area) Args: x0: X coordinate of first point y0: Y coordinate of first point x1: X coordinate of second point y1: Y coordinate of second point avoid_empty: True to avoid returning an empty rectangular area. Defaults to True. Returns: Closest image rectangular pixel area index bounds compatible with direct array slicing (e.g., data[iy0:iy1, ix0:ix1]) .. note:: Avoid returning empty rectangular area (return 1x1 pixel area instead). Handle reversed/not-reversed Y-axis orientation. Indices are compatible with direct array slicing and match get_data behavior. """ ix0, iy0 = self.get_closest_indexes(x0, y0, corner="TL") # Use TL to avoid double +1 addition ix1, iy1 = self.get_closest_indexes(x1, y1, corner="TL") if ix0 > ix1: ix1, ix0 = ix0, ix1 if iy0 > iy1: iy1, iy0 = iy0, iy1 # Add +1 to bottom-right indices for slice compatibility (matches get_data) ix1 = min(ix1 + 1, self.data.shape[1]) iy1 = min(iy1 + 1, self.data.shape[0]) if ix0 == ix1 and avoid_empty: ix1 = min(ix1 + 1, self.data.shape[1]) if iy0 == iy1 and avoid_empty: iy1 = min(iy1 + 1, self.data.shape[0]) return ix0, iy0, ix1, iy1
[docs] def align_rectangular_shape(self, shape: RectangleShape) -> None: """Align rectangular shape to image pixels Args: shape: Shape to align """ ix0, iy0, ix1, iy1 = self.get_closest_index_rect(*shape.get_rect()) x0, y0 = self.get_plot_coordinates(ix0, iy0) x1, y1 = self.get_plot_coordinates(ix1, iy1) shape.set_rect(x0, y0, x1, y1)
[docs] def get_closest_pixel_indexes(self, x: float, y: float) -> tuple[int, int]: """Get closest pixel indexes Args: x: X coordinate y: Y coordinate Returns: Closest pixel indexes .. note:: Instead of returning indexes of an image pixel like the method 'get_closest_indexes', this method returns the indexes of the closest pixel which is not necessarily on the image itself (i.e. indexes may be outside image index bounds: negative or superior than the image dimension) .. note:: This is *not* the same as retrieving the canvas pixel coordinates (which depends on the zoom level) """ x, y = self.get_pixel_coordinates(x, y) i = int(pixelround(x)) j = int(pixelround(y)) return i, j
[docs] def get_x_values(self, i0: int, i1: int) -> np.ndarray: """Get X values from pixel indexes Args: i0: First index i1: Second index Returns: X values corresponding to the given pixel indexes """ return np.arange(i0, i1)
[docs] def get_y_values(self, j0: int, j1: int) -> np.ndarray: """Get Y values from pixel indexes Args: j0: First index j1: Second index Returns: Y values corresponding to the given pixel indexes """ return np.arange(j0, j1)
[docs] def get_r_values(self, i0, i1, j0, j1, flag_circle=False): """Get radial values from pixel indexes Args: i0: First index i1: Second index j0: Third index j1: Fourth index flag_circle: Flag circle (Default value = False) Returns: Radial values corresponding to the given pixel indexes """ return self.get_x_values(i0, i1)
def _recompute_log_data(self) -> None: """Refresh the cached log10 data from the current ``self.data``. Used both when toggling the Z-axis log scale on and when the underlying data is replaced (e.g. via :meth:`set_data`) while the log scale is already active. """ self._log_data = np.array(np.log10(self.data.clip(1)), dtype=np.float64)
[docs] def set_data( self, data: np.ndarray, lut_range: tuple[float, float] | None = None ) -> None: """Set image data Args: data: 2D NumPy array lut_range: LUT range -- tuple (levelmin, levelmax) (Default value = None) """ self.data = data self.histogram_cache = None self.update_bounds() self.update_border() # Refresh the cached log10 data when log scale is active, otherwise the # display would keep using the previous (now stale) log data. if self.get_zaxis_log_state(): self._recompute_log_data() if not self.param.keep_lut_range: if lut_range is not None: _min, _max = lut_range elif self.get_zaxis_log_state(): _min, _max = get_nan_range(self._log_data) else: _min, _max = get_nan_range(data) self.set_lut_range((_min, _max))
[docs] def get_data( self, x0: float, y0: float, x1: float | None = None, y1: float | None = None ) -> float | tuple[np.ndarray, np.ndarray, np.ndarray]: """Get image data Args: x0: X coordinate of first point y0: Y coordinate of first point x1: X coordinate of second point (Default value = None) y1: Y coordinate of second point (Default value = None) Returns: Image level, or image levels in rectangular area (x0,y0,x1,y1) as a tuple (x, y, data) of arrays """ i0, j0 = self.get_closest_indexes(x0, y0) if x1 is None or y1 is None: return self.data[j0, i0] else: i1, j1 = self.get_closest_indexes(x1, y1) i1 += 1 j1 += 1 return ( self.get_x_values(i0, i1), self.get_y_values(j0, j1), self.data[j0:j1, i0:i1], )
[docs] def get_closest_coordinates(self, x: float, y: float) -> tuple[float, float]: """ Get the closest coordinates to the given point Args: x: X coordinate y: Y coordinate Returns: tuple[float, float]: Closest coordinates """ return self.get_closest_indexes(x, y)
[docs] def get_coordinates_label(self, x: float, y: float) -> str: """ Get the coordinates label for the given coordinates Args: x: X coordinate y: Y coordinate Returns: str: Coordinates label """ title = self.title().text() z = self.get_data(x, y) zstr = "--" if z is np.ma.masked else f"{z:g}" return f"{title}:<br>x = {int(x):d}<br>y = {int(y):d}<br>z = {zstr}"
[docs] def set_background_color(self, qcolor: QColor | str) -> None: """Set background color Args: qcolor: Background color """ # mask = np.uint32(255*self.param.alpha+0.5).clip(0,255) << 24 self.bg_qcolor = qcolor a, b, _bg, cmap = self.lut if qcolor is None: self.lut = (a, b, None, cmap) else: self.lut = (a, b, np.uint32(QG.QColor(qcolor).rgb() & 0xFFFFFF), cmap)
[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 name_or_table is self.cmap_table: # This avoids rebuilding the LUT all the time return if isinstance(name_or_table, str): table = get_cmap(name_or_table) else: table = name_or_table if invert is not None: table.invert = invert self.cmap_table = table self.cmap = table.colorTable(FULLRANGE) cmap_a = self.lut[3] alpha = self.param.alpha alpha_function = self.param.alpha_function for i in range(LUT_SIZE): if alpha_function == LUTAlpha.NONE.value: pix_alpha = 1.0 elif alpha_function == LUTAlpha.CONSTANT.value: pix_alpha = alpha else: x = i / float(LUT_SIZE - 1) if alpha_function == LUTAlpha.LINEAR.value: # Linear alpha function pix_alpha = alpha * x elif alpha_function == LUTAlpha.SIGMOID.value: # Sigmoid alpha function pix_alpha = alpha / (1 + np.exp(-10 * x)) elif alpha_function == LUTAlpha.TANH.value: # Hyperbolic tangent alpha function pix_alpha = alpha * np.tanh(5 * x) elif alpha_function == LUTAlpha.STEP.value: # Fully transparent lowest value and `alpha` transparent elsewhere pix_alpha = alpha if x > 0 else 0 else: raise ValueError(f"Invalid alpha function {alpha_function}") # pylint: disable=too-many-function-args alpha_channel = max(min(np.uint32(255 * pix_alpha + 0.5), 255), 0) << 24 cmap_a[i] = ( np.uint32((table.rgb(FULLRANGE, i / LUT_MAX)) & 0xFFFFFF) | alpha_channel ) plot = self.plot() if plot: plot.update_colormap_axis(self)
[docs] def get_color_map(self) -> EditableColormap | None: """Get colormap Returns: Colormap """ return self.cmap_table
[docs] def set_interpolation(self, interp_mode: int, size: int | None = None) -> None: """Set interpolation mode Args: interp_mode: INTERP_NEAREST, INTERP_LINEAR, INTERP_AA size: (for anti-aliasing only) AA matrix size (Default value = None) """ if interp_mode in (INTERP_NEAREST, INTERP_LINEAR): self.interpolate = (interp_mode,) if interp_mode == INTERP_AA: aa = np.ones((size, size), self.data.dtype) self.interpolate = (interp_mode, aa)
[docs] def get_interpolation(self) -> tuple[int] | tuple[int, np.ndarray]: """Get interpolation mode""" return self.interpolate
[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)) """ self.min, self.max = lut_range _a, _b, bg, cmap = self.lut if self.max == self.min: self.lut = (LUT_MAX, self.min, bg, cmap) else: fmin, fmax = float(self.min), float(self.max) # avoid overflows self.lut = ( LUT_MAX / (fmax - fmin), -LUT_MAX * fmin / (fmax - fmin), bg, cmap, )
[docs] def get_lut_range(self) -> tuple[float, float]: """Get the current active lut range Returns: tuple[float, float]: Lut range, tuple(min, max) """ return self.min, self.max
[docs] def set_lut_threshold(self, threshold: float) -> None: """Set lut threshold value, eliminating the given percent of the histogram Args: threshold: Lut threshold value """ self.set_lut_range(lut_range_threshold(self, 256, threshold))
[docs] def get_lut_range_full(self) -> tuple[float, float]: """Return full dynamic range Returns: tuple[float, float]: Lut range, tuple(min, max) """ return get_nan_range(self.data)
# ---- Z-axis logarithmic scale --------------------------------------------
[docs] def get_zaxis_log_state(self) -> bool: """Return True if Z-axis is in logarithmic scale""" return self._is_zaxis_log
[docs] def set_zaxis_log_state(self, state: bool) -> None: """Set Z-axis logarithmic scale state Args: state: True to enable logarithmic scale, False otherwise """ self._is_zaxis_log = state plot = self.plot() if state: self._lin_lut_range = self.get_lut_range() if self._log_data is None: self._recompute_log_data() self.set_lut_range(get_nan_range(self._log_data)) dtype = self._log_data.dtype else: self._log_data = None self.set_lut_range(self._lin_lut_range) dtype = self.data.dtype if self.interpolate[0] == INTERP_AA: self.interpolate = (INTERP_AA, self.interpolate[1].astype(dtype)) if plot is not None: plot.update_colormap_axis(self)
[docs] def get_lut_range_max(self) -> tuple[float, float]: """Get maximum range for this dataset Returns: tuple[float, float]: Lut range, tuple(min, max) """ kind = self.data.dtype.kind if kind in np.typecodes["AllFloat"]: info = np.finfo(self.data.dtype) else: info = np.iinfo(self.data.dtype) return info.min, info.max
[docs] def update_bounds(self) -> None: """Update image bounds to fit image shape""" if self.data is None: return self.bounds = QC.QRectF(0, 0, self.data.shape[1], self.data.shape[0])
[docs] def update_border(self) -> None: """Update image border rectangle to fit image shape""" bounds = self.boundingRect().getCoords() self.border_rect.set_rect(*bounds)
[docs] def draw_border( self, painter: QPainter, xMap: qwt.scale_map.QwtScaleMap, yMap: qwt.scale_map.QwtScaleMap, canvasRect: QRectF, ) -> None: """Draw image border rectangle Args: painter: Painter xMap: X axis scale map yMap: Y axis scale map canvasRect: Canvas rectangle """ self.border_rect.draw(painter, xMap, yMap, canvasRect)
[docs] def warn_if_non_linear_scale(self, painter: QPainter, canvasRect: QRectF) -> bool: """Warn if non-linear scale Args: painter: Painter canvasRect: Canvas rectangle Returns: True if non-linear scale """ plot: BasePlot = self.plot() if not isinstance( plot.axisScaleEngine(plot.X_BOTTOM), QwtLinearScaleEngine ) or not isinstance(plot.axisScaleEngine(plot.Y_LEFT), QwtLinearScaleEngine): painter.setPen(QC.Qt.red) painter.drawText( canvasRect, QC.Qt.AlignCenter, _("Non-linear scales are not supported for image items"), ) return True return False
[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 """ if self.warn_if_non_linear_scale(painter, canvasRect): return dest = _scale_rect( self.data, src_rect, self._offscreen, dst_rect, self.lut, self.interpolate ) qrect = QC.QRectF(QC.QPointF(dest[0], dest[1]), QC.QPointF(dest[2], dest[3])) painter.drawImage(qrect, self._image, qrect)
[docs] def export_roi( self, src_rect: tuple[float, float, float, float], dst_rect: tuple[float, float, float, float], dst_image: np.ndarray, apply_lut: bool = False, apply_interpolation: bool = False, original_resolution: bool = False, force_interp_mode: str | None = None, force_interp_size: int | None = None, ) -> None: """ Export a rectangular area of the image to another image Args: src_rect: Source rectangle dst_rect: Destination rectangle dst_image: Destination image apply_lut: Apply lut (Default value = False) apply_interpolation: Apply interpolation (Default value = False) original_resolution: Original resolution (Default value = False) force_interp_mode: Force interpolation mode (Default value = None) force_interp_size: Force interpolation size (Default value = None) """ if apply_lut: a, b, _bg, _cmap = self.lut else: a, b = 1.0, 0.0 interp = self.interpolate if apply_interpolation else (INTERP_NEAREST,) _scale_rect(self.data, src_rect, dst_image, dst_rect, (a, b, None), interp)
# ---- QwtPlotItem API -----------------------------------------------------
[docs] def draw( self, painter: QPainter, xMap: qwt.scale_map.QwtScaleMap, yMap: qwt.scale_map.QwtScaleMap, canvasRect: QRectF, ) -> None: """Draw the item Args: painter: Painter xMap: X axis scale map yMap: Y axis scale map canvasRect: Canvas rectangle """ x1, y1, x2, y2 = canvasRect.getCoords() i1, i2 = xMap.invTransform(x1), xMap.invTransform(x2) j1, j2 = yMap.invTransform(y1), yMap.invTransform(y2) xl, yt, xr, yb = self.boundingRect().getCoords() dest = ( xMap.transform(xl), yMap.transform(yt), xMap.transform(xr) + 1, yMap.transform(yb) + 1, ) W = int(canvasRect.right()) H = int(canvasRect.bottom()) if self._offscreen.shape != (H, W): self._offscreen = np.empty((H, W), np.uint32) self._image = QG.QImage(self._offscreen, W, H, QG.QImage.Format_ARGB32) self._image.ndarray = self._offscreen self.notify_new_offscreen() self.draw_image(painter, canvasRect, (i1, j1, i2, j2), dest, xMap, yMap) self.draw_border(painter, xMap, yMap, canvasRect)
[docs] def boundingRect(self) -> QRectF: """Return the bounding rectangle of the data Returns: Bounding rectangle of the data """ return self.bounds
[docs] def notify_new_offscreen(self) -> None: """Notify that the offscreen image has changed""" # callback for those derived classes who need it pass
[docs] def setVisible(self, enable: bool) -> None: """Set item visibility Args: enable: True if item is visible, False otherwise """ if not enable: self.unselect() # when hiding item, unselect it if enable: self.border_rect.show() else: self.border_rect.hide() QwtPlotItem.setVisible(self, enable)
# ---- IBasePlotItem 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, ICSImageItemType, IExportROIImageItemType, )
[docs] def set_readonly(self, state: bool) -> None: """Set object readonly state Args: state: True if object is readonly, False otherwise """ self._readonly = state
[docs] def is_readonly(self) -> bool: """Return object readonly state Returns: bool: True if object is readonly, False otherwise """ return self._readonly
[docs] def set_private(self, state: bool) -> None: """Set object as private Args: state: True if object is private, False otherwise """ self._private = state
[docs] def is_private(self) -> bool: """Return True if object is private Returns: bool: True if object is private, False otherwise """ return self._private
[docs] def select(self) -> None: """ Select the object and eventually change its appearance to highlight the fact that it's selected """ self.selected = True self.border_rect.select()
[docs] def unselect(self) -> None: """ Unselect the object and eventually restore its original appearance to highlight the fact that it's not selected anymore """ self.selected = False self.border_rect.unselect()
[docs] def is_empty(self) -> bool: """Return True if the item is empty Returns: True if the item is empty, False otherwise """ return self.data is None or self.data.size == 0
[docs] def get_icon_name(self) -> str: """Return the icon name Returns: Icon name """ return self._icon_name
[docs] def set_icon_name(self, icon_name: str) -> None: """Set the icon name Args: icon_name: Icon name """ self._icon_name = icon_name
[docs] def set_selectable(self, state: bool) -> None: """Set item selectable state Args: state: True if item is selectable, False otherwise """ self._can_select = state
[docs] def set_resizable(self, state: bool) -> None: """Set item resizable state (or any action triggered when moving an handle, e.g. rotation) Args: state: True if item is resizable, False otherwise """ self._can_resize = state
[docs] def set_movable(self, state: bool) -> None: """Set item movable state Args: state: True if item is movable, False otherwise """ self._can_move = state
[docs] def set_rotatable(self, state: bool) -> None: """Set item rotatable state Args: state: True if item is rotatable, False otherwise """ self._can_rotate = state
[docs] def can_select(self) -> bool: """ Returns True if this item can be selected Returns: bool: True if item can be selected, False otherwise """ return self._can_select
[docs] def can_resize(self) -> bool: """ Returns True if this item can be resized Returns: bool: True if item can be resized, False otherwise """ return self._can_resize and not getattr(self.param, "lock_position", False)
[docs] def can_move(self) -> bool: """ Returns True if this item can be moved Returns: bool: True if item can be moved, False otherwise """ return self._can_move and not getattr(self.param, "lock_position", False)
[docs] def can_rotate(self) -> bool: """ Returns True if this item can be rotated Returns: bool: True if item can be rotated, False otherwise """ return self._can_rotate and not getattr(self.param, "lock_position", False)
[docs] def hit_test(self, pos: QPointF) -> tuple[float, float, bool, None]: """Return a tuple (distance, attach point, inside, other_object) Args: pos: Position Returns: tuple: Tuple with four elements: (distance, attach point, inside, other_object). Description of the returned values: * distance: distance in pixels (canvas coordinates) to the closest attach point * attach point: handle of the attach point * inside: True if the mouse button has been clicked inside the object * other_object: if not None, reference of the object which will be considered as hit instead of self """ plot = self.plot() ax = self.xAxis() ay = self.yAxis() return self.border_rect.poly_hit_test(plot, ax, ay, pos)
[docs] def update_item_parameters(self) -> None: """Update item parameters (dataset) from object properties""" pass
[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 """ itemparams.add("ShapeParam", self, self.border_rect.shapeparam)
[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 """ self.border_rect.set_item_parameters(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 """ pass
[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 """ pass
[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 """ pass
[docs] def resize_with_selection(self, zoom_dx, zoom_dy): """ Resize the shape together with other selected items zoom_dx, zoom_dy : zoom factor for dx and dy """ pass
[docs] def rotate_with_selection(self, angle): """ Rotate the shape together with other selected items angle : rotation angle """ pass
# ---- IBaseImageItem API --------------------------------------------------
[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 False
[docs] def get_histogram( self, nbins: int, drange: tuple[float, float] | None = None ) -> tuple[np.ndarray, np.ndarray]: """ Return a tuple (hist, bins) where hist is a list of histogram values Args: nbins: number of bins drange: lower and upper range of the bins. If not provided, range is simply (data.min(), data.max()). Values outside the range are ignored. Returns: Tuple (hist, bins) """ if self.data is None: return [0], [0, 1] if self.histogram_cache is None or nbins != self.histogram_cache[0].shape[0]: if True: # Note: np.histogram does not accept data with NaN res = np.histogram( self.data[~np.isnan(self.data)], bins=nbins, range=drange ) else: # TODO: _histogram is faster, but caching is buggy in this version _min, _max = get_nan_range(self.data) if self.data.dtype in (np.float64, np.float32): bins = np.unique( np.array( np.linspace(_min, _max, nbins + 1), dtype=self.data.dtype ) ) else: bins = np.arange(_min, _max + 2, dtype=self.data.dtype) res2 = np.zeros((bins.size + 1,), np.uint32) _histogram(self.data.flatten(), bins, res2) res = res2[1:-1], bins self.histogram_cache = res else: res = self.histogram_cache return res
def __process_cross_section(self, ydata, apply_lut): if apply_lut: a, b, _bg, _cmap = self.lut return (ydata * a + b).clip(0, LUT_MAX) else: return ydata
[docs] def get_xsection(self, y0: float | int, apply_lut: bool = False) -> np.ndarray: """Return cross section along x-axis at y=y0 Args: y0: Y0 apply_lut: Apply lut (Default value = False) Returns: Cross section along x-axis at y=y0 """ _ix, iy = self.get_closest_indexes(0, y0) return ( self.get_x_values(0, self.data.shape[1]), self.__process_cross_section(self.data[iy, :], apply_lut), )
[docs] def get_ysection(self, x0: float | int, apply_lut: bool = False) -> np.ndarray: """Return cross section along y-axis at x=x0 Args: x0: X0 apply_lut: Apply lut (Default value = False) Returns: Cross section along y-axis at x=x0 """ ix, _iy = self.get_closest_indexes(x0, 0) return ( self.get_y_values(0, self.data.shape[0]), self.__process_cross_section(self.data[:, ix], apply_lut), )
[docs] def get_average_xsection( self, x0: float, y0: float, x1: float, y1: float, apply_lut: bool = False ) -> np.ndarray: """Return average cross section along x-axis for the given rectangle Args: x0: X0 of top left corner y0: Y0 of top left corner x1: X1 of bottom right corner y1: Y1 of bottom right corner apply_lut: Apply lut (Default value = False) Returns: Average cross section along x-axis """ ix0, iy0, ix1, iy1 = self.get_closest_index_rect( x0, y0, x1, y1, avoid_empty=False ) ydata = self.data[iy0:iy1, ix0:ix1] if ydata.size == 0: return np.array([]), np.array([]) ydata = ydata.mean(axis=0) return ( self.get_x_values(ix0, ix1), self.__process_cross_section(ydata, apply_lut), )
[docs] def get_average_ysection( self, x0: float, y0: float, x1: float, y1: float, apply_lut: bool = False ) -> np.ndarray: """Return average cross section along y-axis Args: x0: X0 of top left corner y0: Y0 of top left corner x1: X1 of bottom right corner y1: Y1 of bottom right corner apply_lut: Apply lut (Default value = False) Returns: Average cross section along y-axis """ ix0, iy0, ix1, iy1 = self.get_closest_index_rect( x0, y0, x1, y1, avoid_empty=False ) ydata = self.data[iy0:iy1, ix0:ix1] if ydata.size == 0: return np.array([]), np.array([]) ydata = ydata.mean(axis=1) return ( self.get_y_values(iy0, iy1), self.__process_cross_section(ydata, apply_lut), )
assert_interfaces_valid(BaseImageItem) # ============================================================================== # Raw Image item (image item without scale) # ==============================================================================
[docs] class RawImageItem(BaseImageItem): """ Construct a simple image item * data: 2D NumPy array * param: image parameters (:py:class:`.styles.RawImageParam` instance) """ __implements__ = ( IBasePlotItem, IBaseImageItem, IHistDataSource, IVoiImageItemType, ISerializableType, ) # ---- BaseImageItem API ---------------------------------------------------
[docs] def get_default_param(self) -> RawImageParam: """Return instance of the default image param DataSet Returns: RawImageParam: Default image param DataSet """ return RawImageParam(_("Image"))
# ---- Serialization methods ----------------------------------------------- def __reduce__(self) -> tuple: """Return state information for pickling""" fname = self.get_filename() if fname is None: fn_or_data = self.data else: fn_or_data = fname state = self.param, self.get_lut_range(), fn_or_data, self.z() res = (self.__class__, (), state) return res def __setstate__(self, state: tuple) -> None: """Restore state information for pickling""" param, lut_range, fn_or_data, z = state self.param = param if isinstance(fn_or_data, str): self.set_filename(fn_or_data) self.load_data() elif fn_or_data is not None: # should happen only with previous API self.set_data(fn_or_data) self.set_lut_range(lut_range) self.setZ(z) self.param.update_item(self)
[docs] def serialize( self, writer: guidata.io.HDF5Writer | guidata.io.INIWriter | guidata.io.JSONWriter, ) -> None: """Serialize object to HDF5 writer Args: writer: HDF5, INI or JSON writer """ fname = self.get_filename() load_from_fname = fname is not None data = None if load_from_fname else self.data writer.write(load_from_fname, group_name="load_from_fname") writer.write(fname, group_name="fname") writer.write(data, group_name="Zdata") writer.write(self.get_lut_range(), group_name="lut_range") writer.write(self.z(), group_name="z") self.param.update_param(self) writer.write(self.param, group_name="imageparam")
[docs] def deserialize( self, reader: guidata.io.HDF5Reader | guidata.io.INIReader | guidata.io.JSONReader, ) -> None: """Deserialize object from HDF5 reader Args: reader: HDF5, INI or JSON reader """ lut_range = reader.read(group_name="lut_range") if reader.read(group_name="load_from_fname"): name = reader.read(group_name="fname", func=reader.read_unicode) self.set_filename(name) self.load_data() else: data = reader.read(group_name="Zdata", func=reader.read_array) self.set_data(data) self.set_lut_range(lut_range) self.setZ(reader.read("z")) self.param = self.get_default_param() reader.read("imageparam", instance=self.param) self.param.update_item(self)
# ---- Public API ----------------------------------------------------------
[docs] def load_data(self, lut_range: list[float, float] | None = None) -> None: """Load data from item filename attribute and eventually apply specified lut_range Args: lut_range: Lut range (Default value = None) """ data = io.imread(self.get_filename(), to_grayscale=True) self.set_data(data, lut_range=lut_range)
# ---- IBasePlotItem 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, ICSImageItemType, ISerializableType, IExportROIImageItemType, )
[docs] def update_item_parameters(self) -> None: """Update item parameters (dataset) from object properties""" self.param.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("ImageParam", self, self.param)
[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.param, itemparams.get("ImageParam"), visible_only=True) self.param.update_item(self) BaseImageItem.set_item_parameters(self, itemparams)
# ---- IBaseImageItem API --------------------------------------------------
[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
assert_interfaces_valid(RawImageItem)