# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see plotpy/LICENSE for details)
"""
Contours
========
This module provides functions and classes to create contour curves.
.. autoclass:: ContourLine
:members:
.. autofunction:: compute_contours
.. autoclass:: ContourItem
:members:
.. autofunction:: create_contour_items
"""
from __future__ import annotations
import guidata.dataset as gds
import numpy as np
from guidata.utils.misc import assert_interfaces_valid
from skimage import measure
from plotpy.config import _
from plotpy.items.shape.polygon import PolygonShape
from plotpy.styles import ShapeParam
[docs]
class ContourLine(gds.DataSet):
"""A contour line"""
vertices = gds.FloatArrayItem(_("Vertices"), help=_("Vertices of the line"))
level = gds.FloatItem(_("Level"), help=_("Level of the line"))
[docs]
def compute_contours(
Z: np.ndarray,
levels: float | np.ndarray,
X: np.ndarray | None = None,
Y: np.ndarray | None = None,
) -> list[ContourLine]:
"""Create contour curves
Args:
Z: The height values over which the contour is drawn.
levels : Determines the number and positions of the contour lines/regions.
If a float, draw contour lines at this specified levels
If array-like, draw contour lines at the specified levels.
The values must be in increasing order.
X: The coordinates of the values in *Z*.
*X* must be 2-D with the same shape as *Z* (e.g. created via
``numpy.meshgrid``), or it must both be 1-D such that ``len(X) == M``
is the number of columns in *Z*.
If none, they are assumed to be integer indices, i.e. ``X = range(M)``.
Y: The coordinates of the values in *Z*.
*Y* must be 2-D with the same shape as *Z* (e.g. created via
``numpy.meshgrid``), or it must both be 1-D such that ``len(Y) == N``
is the number of rows in *Z*.
If none, they are assumed to be integer indices, i.e. ``Y = range(N)``.
Returns:
A list of :py:class:`ContourLine` instances.
"""
z = np.asarray(Z, dtype=np.float64)
if z.ndim != 2:
raise TypeError("Input z must be a 2D array.")
elif z.shape[0] < 2 or z.shape[1] < 2:
raise TypeError("Input z must be at least a 2x2 array.")
if isinstance(levels, np.ndarray):
levels = np.asarray(levels, dtype=np.float64)
else:
levels = np.asarray([levels], dtype=np.float64)
if X is None:
delta_x, x_origin = 1.0, 0.0
else:
delta_x, x_origin = X[0, 1] - X[0, 0], X[0, 0]
if Y is None:
delta_y, y_origin = 1.0, 0.0
else:
delta_y, y_origin = Y[1, 0] - Y[0, 0], Y[0, 0]
# Find contours in the binary image for each level
clines = []
for level in levels:
for contour in measure.find_contours(Z, level):
contour = contour.squeeze()
if len(contour) > 1: # Avoid single points
line = np.zeros_like(contour, dtype=np.float32)
line[:, 0] = contour[:, 1] * delta_x + x_origin
line[:, 1] = contour[:, 0] * delta_y + y_origin
cline = ContourLine.create(vertices=line, level=level)
clines.append(cline)
return clines
[docs]
class ContourItem(PolygonShape):
"""Contour shape"""
_readonly = True
_can_select = True
_can_resize = False
_can_rotate = False
_can_move = False
_icon_name = "contour.png"
def __init__(self, points=None, shapeparam=None):
super().__init__(points, closed=True, shapeparam=shapeparam)
assert_interfaces_valid(ContourItem)
[docs]
def create_contour_items(
Z: np.ndarray,
levels: float | np.ndarray,
X: np.ndarray | None = None,
Y: np.ndarray | None = None,
) -> list[ContourItem]:
"""Create contour items
Args:
Z: The height values over which the contour is drawn.
levels : Determines the number and positions of the contour lines/regions.
If a float, draw contour lines at this specified levels
If array-like, draw contour lines at the specified levels.
The values must be in increasing order.
X: The coordinates of the values in *Z*.
*X* must be 2-D with the same shape as *Z* (e.g. created via
``numpy.meshgrid``), or it must both be 1-D such that ``len(X) == M``
is the number of columns in *Z*.
If none, they are assumed to be integer indices, i.e. ``X = range(M)``.
Y: The coordinates of the values in *Z*.
*Y* must be 2-D with the same shape as *Z* (e.g. created via
``numpy.meshgrid``), or it must both be 1-D such that ``len(Y) == N``
is the number of rows in *Z*.
If none, they are assumed to be integer indices, i.e. ``Y = range(N)``.
Returns:
A list of :py:class:`.ContourItem` instances.
"""
items = []
contours = compute_contours(Z, levels, X, Y)
for cline in contours:
param = ShapeParam("Contour", icon="contour.png")
item = ContourItem(points=cline.vertices, shapeparam=param)
item.set_style("plot", "shape/contour")
item.setTitle(_("Contour") + f"[Z={cline.level}]")
items.append(item)
return items