scikit-image
290 строк · 10.9 Кб
1"""Benchmarks for `skimage.morphology`.
2
3See "Writing benchmarks" in the asv docs for more information.
4"""
5
6import numpy as np
7from numpy.lib import NumpyVersion as Version
8import scipy.ndimage
9
10import skimage
11from skimage import color, data, morphology, util
12
13
14class Skeletonize3d:
15def setup(self, *args):
16try:
17# use a separate skeletonize_3d function on older scikit-image
18if Version(skimage.__version__) < Version('0.16.0'):
19self.skeletonize = morphology.skeletonize_3d
20else:
21self.skeletonize = morphology.skeletonize
22except AttributeError:
23raise NotImplementedError("3d skeletonize unavailable")
24
25# we stack the horse data 5 times to get an example volume
26self.image = np.stack(5 * [util.invert(data.horse())])
27
28def time_skeletonize(self):
29self.skeletonize(self.image)
30
31def peakmem_reference(self, *args):
32"""Provide reference for memory measurement with empty benchmark.
33
34Peakmem benchmarks measure the maximum amount of RAM used by a
35function. However, this maximum also includes the memory used
36during the setup routine (as of asv 0.2.1; see [1]_).
37Measuring an empty peakmem function might allow us to disambiguate
38between the memory used by setup and the memory used by target (see
39other ``peakmem_`` functions below).
40
41References
42----------
43.. [1]: https://asv.readthedocs.io/en/stable/writing_benchmarks.html#peak-memory
44"""
45pass
46
47def peakmem_skeletonize(self):
48self.skeletonize(self.image)
49
50
51# For binary morphology all functions ultimately are based on a single erosion
52# function in the scipy.ndimage C code, so only benchmark binary_erosion here.
53
54
55class BinaryMorphology2D:
56# skip rectangle as roughly equivalent to square
57param_names = ["shape", "footprint", "radius", "decomposition"]
58params = [
59((512, 512),),
60("square", "diamond", "octagon", "disk", "ellipse", "star"),
61(1, 3, 5, 15, 25, 40),
62(None, "sequence", "separable", "crosses"),
63]
64
65def setup(self, shape, footprint, radius, decomposition):
66rng = np.random.default_rng(123)
67# Make an image that is mostly True, with random isolated False areas
68# (so it will not become fully False for any of the footprints).
69self.image = rng.standard_normal(shape) < 3.5
70fp_func = getattr(morphology, footprint)
71allow_sequence = ("rectangle", "square", "diamond", "octagon", "disk")
72allow_separable = ("rectangle", "square")
73allow_crosses = ("disk", "ellipse")
74allow_decomp = tuple(
75set(allow_sequence) | set(allow_separable) | set(allow_crosses)
76)
77footprint_kwargs = {}
78if decomposition == "sequence" and footprint not in allow_sequence:
79raise NotImplementedError("decomposition unimplemented")
80elif decomposition == "separable" and footprint not in allow_separable:
81raise NotImplementedError("separable decomposition unavailable")
82elif decomposition == "crosses" and footprint not in allow_crosses:
83raise NotImplementedError("separable decomposition unavailable")
84if footprint in allow_decomp:
85footprint_kwargs["decomposition"] = decomposition
86if footprint in ["rectangle", "square"]:
87size = 2 * radius + 1
88self.footprint = fp_func(size, **footprint_kwargs)
89elif footprint in ["diamond", "disk"]:
90self.footprint = fp_func(radius, **footprint_kwargs)
91elif footprint == "star":
92# set a so bounding box size is approximately 2*radius + 1
93# size will be 2*a + 1 + 2*floor(a / 2)
94a = max((2 * radius) // 3, 1)
95self.footprint = fp_func(a, **footprint_kwargs)
96elif footprint == "octagon":
97# overall size is m + 2 * n
98# so choose m = n so that overall size is ~ 2*radius + 1
99m = n = max((2 * radius) // 3, 1)
100self.footprint = fp_func(m, n, **footprint_kwargs)
101elif footprint == "ellipse":
102if radius > 1:
103# make somewhat elliptical
104self.footprint = fp_func(radius - 1, radius + 1, **footprint_kwargs)
105else:
106self.footprint = fp_func(radius, radius, **footprint_kwargs)
107
108def time_erosion(self, shape, footprint, radius, *args):
109morphology.binary_erosion(self.image, self.footprint)
110
111
112class BinaryMorphology3D:
113# skip rectangle as roughly equivalent to square
114param_names = ["shape", "footprint", "radius", "decomposition"]
115params = [
116((128, 128, 128),),
117("ball", "cube", "octahedron"),
118(1, 3, 5, 10),
119(None, "sequence", "separable"),
120]
121
122def setup(self, shape, footprint, radius, decomposition):
123rng = np.random.default_rng(123)
124# make an image that is mostly True, with a few isolated False areas
125self.image = rng.standard_normal(shape) > -3
126fp_func = getattr(morphology, footprint)
127allow_decomp = ("cube", "octahedron", "ball")
128allow_separable = ("cube",)
129if decomposition == "separable" and footprint != "cube":
130raise NotImplementedError("separable unavailable")
131footprint_kwargs = {}
132if decomposition is not None and footprint not in allow_decomp:
133raise NotImplementedError("decomposition unimplemented")
134elif decomposition == "separable" and footprint not in allow_separable:
135raise NotImplementedError("separable decomposition unavailable")
136if footprint in allow_decomp:
137footprint_kwargs["decomposition"] = decomposition
138if footprint == "cube":
139size = 2 * radius + 1
140self.footprint = fp_func(size, **footprint_kwargs)
141elif footprint in ["ball", "octahedron"]:
142self.footprint = fp_func(radius, **footprint_kwargs)
143
144def time_erosion(self, shape, footprint, radius, *args):
145morphology.binary_erosion(self.image, self.footprint)
146
147
148class IsotropicMorphology2D:
149# skip rectangle as roughly equivalent to square
150param_names = ["shape", "radius"]
151params = [
152((512, 512),),
153(1, 3, 5, 15, 25, 40),
154]
155
156def setup(self, shape, radius):
157rng = np.random.default_rng(123)
158# Make an image that is mostly True, with random isolated False areas
159# (so it will not become fully False for any of the footprints).
160self.image = rng.standard_normal(shape) < 3.5
161
162def time_erosion(self, shape, radius, *args):
163morphology.isotropic_erosion(self.image, radius)
164
165
166# Repeat the same footprint tests for grayscale morphology
167# just need to call morphology.erosion instead of morphology.binary_erosion
168
169
170class GrayMorphology2D(BinaryMorphology2D):
171def time_erosion(self, shape, footprint, radius, *args):
172morphology.erosion(self.image, self.footprint)
173
174
175class GrayMorphology3D(BinaryMorphology3D):
176def time_erosion(self, shape, footprint, radius, *args):
177morphology.erosion(self.image, self.footprint)
178
179
180class GrayReconstruction:
181# skip rectangle as roughly equivalent to square
182param_names = ["shape", "dtype"]
183params = [
184((10, 10), (64, 64), (1200, 1200), (96, 96, 96)),
185(np.uint8, np.float32, np.float64),
186]
187
188def setup(self, shape, dtype):
189rng = np.random.default_rng(123)
190# make an image that is mostly True, with a few isolated False areas
191rvals = rng.integers(1, 255, size=shape).astype(dtype=dtype)
192
193roi1 = tuple(slice(s // 4, s // 2) for s in rvals.shape)
194roi2 = tuple(slice(s // 2 + 1, (3 * s) // 4) for s in rvals.shape)
195seed = np.full(rvals.shape, 1, dtype=dtype)
196seed[roi1] = rvals[roi1]
197seed[roi2] = rvals[roi2]
198
199# create a mask with a couple of square regions set to seed maximum
200mask = np.full(seed.shape, 1, dtype=dtype)
201mask[roi1] = 255
202mask[roi2] = 255
203
204self.seed = seed
205self.mask = mask
206
207def time_reconstruction(self, shape, dtype):
208morphology.reconstruction(self.seed, self.mask)
209
210def peakmem_reference(self, *args):
211"""Provide reference for memory measurement with empty benchmark.
212
213Peakmem benchmarks measure the maximum amount of RAM used by a
214function. However, this maximum also includes the memory used
215during the setup routine (as of asv 0.2.1; see [1]_).
216Measuring an empty peakmem function might allow us to disambiguate
217between the memory used by setup and the memory used by target (see
218other ``peakmem_`` functions below).
219
220References
221----------
222.. [1]: https://asv.readthedocs.io/en/stable/writing_benchmarks.html#peak-memory # noqa
223"""
224pass
225
226def peakmem_reconstruction(self, shape, dtype):
227morphology.reconstruction(self.seed, self.mask)
228
229
230class LocalMaxima:
231param_names = ["connectivity", "allow_borders"]
232params = [(1, 2), (False, True)]
233
234def setup(self, *args):
235# Natural image with small extrema
236self.image = data.moon()
237
238def time_2d(self, connectivity, allow_borders):
239morphology.local_maxima(
240self.image, connectivity=connectivity, allow_borders=allow_borders
241)
242
243def peakmem_reference(self, *args):
244"""Provide reference for memory measurement with empty benchmark.
245
246.. [1] https://asv.readthedocs.io/en/stable/writing_benchmarks.html#peak-memory
247"""
248pass
249
250def peakmem_2d(self, connectivity, allow_borders):
251morphology.local_maxima(
252self.image, connectivity=connectivity, allow_borders=allow_borders
253)
254
255
256class RemoveObjectsByDistance:
257
258param_names = ["min_distance"]
259params = [5, 100]
260
261def setup(self, *args):
262image = data.hubble_deep_field()
263image = color.rgb2gray(image)
264objects = image > 0.18 # Chosen with threshold_li
265self.labels, _ = scipy.ndimage.label(objects)
266
267def time_remove_near_objects(self, min_distance):
268morphology.remove_objects_by_distance(self.labels, min_distance=min_distance)
269
270def peakmem_reference(self, *args):
271"""Provide reference for memory measurement with empty benchmark.
272
273Peakmem benchmarks measure the maximum amount of RAM used by a
274function. However, this maximum also includes the memory used
275during the setup routine (as of asv 0.2.1; see [1]_).
276Measuring an empty peakmem function might allow us to disambiguate
277between the memory used by setup and the memory used by target (see
278other ``peakmem_`` functions below).
279
280References
281----------
282.. [1]: https://asv.readthedocs.io/en/stable/writing_benchmarks.html#peak-memory
283"""
284pass
285
286def peakmem_remove_near_objects(self, min_distance):
287morphology.remove_objects_by_distance(
288self.labels,
289min_distance=min_distance,
290)
291