Skip to content

Commit 6f6185d

Browse files
committed
Replace internal contour2d by scikit-images's find_contours
Attemp to fix #17
1 parent 4a222d3 commit 6f6185d

File tree

10 files changed

+65
-444
lines changed

10 files changed

+65
-444
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ pylint_report.txt
4343
docs/build/
4444

4545
# Generated cython c file
46-
src/contour2d.c
4746
src/histogram2d.c
4847
src/mandelbrot.c
4948

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
max-line-length=88
55

66
[TYPECHECK]
7-
ignored-modules=qtpy.QtWidgets,qtpy.QtCore,qtpy.QtGui,qtpy.QtSvg,qtpy.QtPrintSupport,qtpy.QtDesigner,plotpy._scaler,plotpy.mandelbrot,plotpy.contour2d,plotpy.histogram2d,PyQt5.QtWidgets
7+
ignored-modules=qtpy.QtWidgets,qtpy.QtCore,qtpy.QtGui,qtpy.QtSvg,qtpy.QtPrintSupport,qtpy.QtDesigner,plotpy._scaler,plotpy.mandelbrot,plotpy.histogram2d,PyQt5.QtWidgets
88

99
[MESSAGES CONTROL]
1010
disable=wrong-import-order

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Changelog #
22

3+
## Version 2.4.2 ##
4+
5+
In this release, test coverage is 79%.
6+
7+
🛠️ Bug fixes:
8+
9+
* [Issue #17](https://github.com/PlotPyStack/PlotPy/issues/17):
10+
* Debian's Python team has reported that the contour unit test was failing on `arm64`
11+
architecture
12+
* This is the opportunity to replace the `contour2d` Cython extension by scikit-image's
13+
`find_contours` function, thus avoiding to reinvent the wheel by relying on a more
14+
robust and tested implementation
15+
* The `contour2d` Cython extension is removed from the source code
16+
* The contour related features remain the same, but the implementation is now based on
17+
scikit-image's `find_contours` function
18+
* The scikit-image dependency is added to the package requirements
19+
320
## Version 2.4.1 ##
421

522
In this release, test coverage is 79%.

plotpy/items/contour.py

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
import numpy as np
2727
from guidata.configtools import get_icon
2828
from guidata.utils.misc import assert_interfaces_valid
29+
from skimage import measure
2930

3031
from plotpy.config import _
31-
from plotpy.contour2d import contour_2d_grid, contour_2d_ortho
3232
from plotpy.items.shape.polygon import PolygonShape
3333
from plotpy.styles import ShapeParam
3434

@@ -73,64 +73,33 @@ def compute_contours(
7373
raise TypeError("Input z must be a 2D array.")
7474
elif z.shape[0] < 2 or z.shape[1] < 2:
7575
raise TypeError("Input z must be at least a 2x2 array.")
76-
else:
77-
Ny, Nx = z.shape
78-
79-
if X is None:
80-
X = np.arange(Nx)
81-
if Y is None:
82-
Y = np.arange(Ny)
83-
84-
x = np.asarray(X, dtype=np.float64)
85-
y = np.asarray(Y, dtype=np.float64)
86-
87-
if x.ndim != y.ndim:
88-
raise TypeError("Number of dimensions of x and y should match.")
89-
if x.ndim == 1:
90-
(nx,) = x.shape
91-
(ny,) = y.shape
92-
if nx != Nx:
93-
raise TypeError("Length of x must be number of columns in z.")
94-
if ny != Ny:
95-
raise TypeError("Length of y must be number of rows in z.")
96-
elif x.ndim == 2:
97-
if x.shape != z.shape:
98-
raise TypeError(
99-
"Shape of x does not match that of z: found "
100-
"{0} instead of {1}.".format(x.shape, z.shape)
101-
)
102-
if y.shape != z.shape:
103-
raise TypeError(
104-
"Shape of y does not match that of z: found "
105-
"{0} instead of {1}.".format(y.shape, z.shape)
106-
)
107-
else:
108-
raise TypeError("Inputs x and y must be 1D or 2D.")
10976

11077
if isinstance(levels, np.ndarray):
11178
levels = np.asarray(levels, dtype=np.float64)
11279
else:
11380
levels = np.asarray([levels], dtype=np.float64)
11481

115-
if x.ndim == 2:
116-
func = contour_2d_grid
82+
if X is None:
83+
delta_x, x_origin = 1.0, 0.0
84+
else:
85+
delta_x, x_origin = X[0, 1] - X[0, 0], X[0, 0]
86+
if Y is None:
87+
delta_y, y_origin = 1.0, 0.0
11788
else:
118-
func = contour_2d_ortho
119-
120-
lines = []
121-
points, offsets = func(z, x, y, levels)
122-
start = 0
123-
v = 0
124-
for v, index in offsets:
125-
if index - start >= 2:
126-
cline = ContourLine.create(vertices=points[start:index], level=levels[v])
127-
lines.append(cline)
128-
start = index
129-
last_points = points[start:]
130-
if len(last_points) >= 2:
131-
cline = ContourLine.create(vertices=last_points, level=levels[v])
132-
lines.append(cline)
133-
return lines
89+
delta_y, y_origin = Y[1, 0] - Y[0, 0], Y[0, 0]
90+
91+
# Find contours in the binary image for each level
92+
clines = []
93+
for level in levels:
94+
for contour in measure.find_contours(Z, level):
95+
contour = contour.squeeze()
96+
if len(contour) > 1: # Avoid single points
97+
line = np.zeros_like(contour, dtype=np.float32)
98+
line[:, 0] = contour[:, 1] * delta_x + x_origin
99+
line[:, 1] = contour[:, 0] * delta_y + y_origin
100+
cline = ContourLine.create(vertices=line, level=level)
101+
clines.append(cline)
102+
return clines
134103

135104

136105
class ContourItem(PolygonShape):
@@ -179,11 +148,12 @@ def create_contour_items(
179148
A list of :py:class:`.ContourItem` instances.
180149
"""
181150
items = []
182-
lines = compute_contours(Z, levels, X, Y)
183-
for line in lines:
151+
152+
contours = compute_contours(Z, levels, X, Y)
153+
for cline in contours:
184154
param = ShapeParam("Contour", icon="contour.png")
185-
item = ContourItem(points=line.vertices, shapeparam=param)
155+
item = ContourItem(points=cline.vertices, shapeparam=param)
186156
item.set_style("plot", "shape/contour")
187-
item.setTitle(_("Contour") + f"[Z={line.level}]")
157+
item.setTitle(_("Contour") + f"[Z={cline.level}]")
188158
items.append(item)
189159
return items

plotpy/tests/data.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,14 @@ def gen_2d_gaussian(size, dtype, x0=0, y0=0, mu=0.0, sigma=2.0, amp=None):
164164
amp = np.iinfo(dtype).max * 0.5
165165
t = (np.sqrt((x - x0) ** 2 + (y - y0) ** 2) - mu) ** 2
166166
return np.array(amp * np.exp(-t / (2.0 * sigma**2)), dtype=dtype)
167+
168+
169+
def gen_xyz_data():
170+
"""Create a X, Y, Z data set for contour detection features"""
171+
delta = 0.025
172+
x, y = np.arange(-3.0, 3.0, delta), np.arange(-2.0, 2.0, delta)
173+
X, Y = np.meshgrid(x, y)
174+
Z1 = np.exp(-(X**2) - Y**2)
175+
Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
176+
Z = (Z1 - Z2) * 2
177+
return X, Y, Z

plotpy/tests/items/test_image_contour.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,15 @@
1212

1313
from plotpy.builder import make
1414
from plotpy.tests import vistools as ptv
15+
from plotpy.tests.data import gen_xyz_data
1516

1617

1718
def test_contour():
1819
"""Contour plotting test"""
1920
with qt_app_context(exec_loop=True):
20-
# compute the image
21-
delta = 0.025
22-
x, y = np.arange(-3.0, 3.0, delta), np.arange(-2.0, 2.0, delta)
23-
X, Y = np.meshgrid(x, y)
24-
Z1 = np.exp(-(X**2) - Y**2)
25-
Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2)
26-
Z = (Z1 - Z2) * 2
27-
28-
# show the image
21+
_x, _y, z = gen_xyz_data()
2922
_win = ptv.show_items(
30-
[make.image(Z)] + make.contours(Z, np.arange(-2, 2, 0.5)),
23+
[make.image(z)] + make.contours(z, np.arange(-2, 2, 0.5)),
3124
wintitle=test_contour.__doc__,
3225
curve_antialiasing=False,
3326
lock_aspect_ratio=True,

plotpy/tests/unit/test_contour.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import numpy as np
44

55
from plotpy.items.contour import compute_contours
6+
from plotpy.tests.data import gen_xyz_data
67

78

89
class ContourTest(unittest.TestCase):
@@ -14,18 +15,12 @@ class ContourTest(unittest.TestCase):
1415
"""
1516

1617
def setUp(self):
17-
delta = 0.025
18-
self.x = np.arange(-3.0, 3.0, delta)
19-
self.y = np.arange(-2.0, 2.0, delta)
20-
self.X, self.Y = np.meshgrid(self.x, self.y)
21-
Z1 = np.exp(-self.X**2 - self.Y**2)
22-
Z2 = np.exp(-((self.X - 1) ** 2) - (self.Y - 1) ** 2)
23-
self.Z = (Z1 - Z2) * 2
18+
self.X, self.Y, self.Z = gen_xyz_data()
2419

2520
def test_contour_level_1(self):
2621
"""Test that contour() returns one closed line when level is 1.0"""
2722
# Ortho coord
28-
lines = compute_contours(self.Z, 1.0, self.x, self.y)
23+
lines = compute_contours(self.Z, 1.0, self.X, self.Y)
2924
assert len(lines) == 1
3025
assert np.all(np.equal(lines[0].vertices[0], lines[0].vertices[-1]))
3126
assert lines[0].level == 1.0
@@ -40,21 +35,21 @@ def test_contour_level_diag_0(self):
4035
"""Test that contour() returns opened lines (diagonal) when level is 0.0"""
4136
lines = compute_contours(self.Z, 0.0, self.X, self.Y)
4237
assert len(lines) == 1
43-
assert np.all(lines[0].vertices[0] != lines[0].vertices[-1])
38+
assert np.any(lines[0].vertices[0] != lines[0].vertices[-1])
4439
assert lines[0].level == 0.0
4540

4641
def test_contour_level_opened_neg_0_5(self):
4742
"""Test that contour() returns opened lines when level is -0.5"""
4843
lines = compute_contours(self.Z, -0.5, self.X, self.Y)
4944
assert len(lines) == 1
50-
assert np.all(lines[0].vertices[0] != lines[0].vertices[-1])
45+
assert np.any(lines[0].vertices[0] != lines[0].vertices[-1])
5146
assert lines[0].level == -0.5
5247

5348
def test_contour_abs_level_1(self):
5449
"""Test that contour() returns two closed lines when level is 1.0
5550
and data are absolute.
5651
"""
57-
lines = compute_contours(np.abs(self.Z), 1.0, self.x, self.y)
52+
lines = compute_contours(np.abs(self.Z), 1.0, self.X, self.Y)
5853
assert len(lines) == 2
5954
for line in lines:
6055
assert np.all(np.equal(line.vertices[0], line.vertices[-1]))

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dependencies = [
4848
"PythonQwt>=0.12.1",
4949
"NumPy>=1.17",
5050
"SciPy>=1.3",
51+
"scikit-image >= 0.18",
5152
"Pillow",
5253
"tifffile",
5354
]

setup.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,6 @@ def compile_cython_extensions():
7171
extra_compile_args=CFLAGS_CYTHON,
7272
define_macros=MACROS_CYTHON,
7373
),
74-
Extension(
75-
name=f"{LIBNAME}.contour2d",
76-
sources=[osp.join(SRCPATH, "contour2d.c")],
77-
include_dirs=INCLUDE_DIRS,
78-
extra_compile_args=CFLAGS_CYTHON,
79-
define_macros=MACROS_CYTHON,
80-
),
8174
Extension(
8275
name=f"{LIBNAME}._scaler",
8376
sources=[osp.join(SRCPATH, "scaler.cpp"), osp.join(SRCPATH, "pcolor.cpp")],

0 commit comments

Comments
 (0)