-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathcontour.py
More file actions
159 lines (129 loc) · 5.21 KB
/
contour.py
File metadata and controls
159 lines (129 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# -*- 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.configtools import get_icon
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
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"))
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
class ContourItem(PolygonShape):
"""Contour shape"""
_readonly = True
_can_select = True
_can_resize = False
_can_rotate = False
_can_move = False
def __init__(self, points=None, shapeparam=None):
super().__init__(points, closed=True, shapeparam=shapeparam)
self.setIcon(get_icon("contour.png"))
assert_interfaces_valid(ContourItem)
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