imops 0.8.8__cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. _build_utils.py +113 -0
  2. imops/__init__.py +10 -0
  3. imops/__version__.py +1 -0
  4. imops/_configs.py +29 -0
  5. imops/backend.py +95 -0
  6. imops/box.py +74 -0
  7. imops/cpp/cpp_modules.cpython-312-x86_64-linux-gnu.so +0 -0
  8. imops/cpp/interp2d/delaunator/delaunator-header-only.hpp +33 -0
  9. imops/cpp/interp2d/delaunator/delaunator.cpp +645 -0
  10. imops/cpp/interp2d/delaunator/delaunator.hpp +170 -0
  11. imops/cpp/interp2d/interpolator.h +52 -0
  12. imops/cpp/interp2d/triangulator.h +198 -0
  13. imops/cpp/interp2d/utils.h +63 -0
  14. imops/cpp/main.cpp +13 -0
  15. imops/crop.py +120 -0
  16. imops/interp1d.py +207 -0
  17. imops/interp2d.py +120 -0
  18. imops/measure.py +228 -0
  19. imops/morphology.py +525 -0
  20. imops/numeric.py +384 -0
  21. imops/pad.py +253 -0
  22. imops/py.typed +0 -0
  23. imops/radon.py +247 -0
  24. imops/src/__init__.py +0 -0
  25. imops/src/_backprojection.c +27339 -0
  26. imops/src/_backprojection.cpython-312-x86_64-linux-gnu.so +0 -0
  27. imops/src/_fast_backprojection.c +27374 -0
  28. imops/src/_fast_backprojection.cpython-312-x86_64-linux-gnu.so +0 -0
  29. imops/src/_fast_measure.c +33845 -0
  30. imops/src/_fast_measure.cpython-312-x86_64-linux-gnu.so +0 -0
  31. imops/src/_fast_morphology.c +26124 -0
  32. imops/src/_fast_morphology.cpython-312-x86_64-linux-gnu.so +0 -0
  33. imops/src/_fast_numeric.c +48686 -0
  34. imops/src/_fast_numeric.cpython-312-x86_64-linux-gnu.so +0 -0
  35. imops/src/_fast_radon.c +30749 -0
  36. imops/src/_fast_radon.cpython-312-x86_64-linux-gnu.so +0 -0
  37. imops/src/_fast_zoom.c +57238 -0
  38. imops/src/_fast_zoom.cpython-312-x86_64-linux-gnu.so +0 -0
  39. imops/src/_measure.c +33810 -0
  40. imops/src/_measure.cpython-312-x86_64-linux-gnu.so +0 -0
  41. imops/src/_morphology.c +26089 -0
  42. imops/src/_morphology.cpython-312-x86_64-linux-gnu.so +0 -0
  43. imops/src/_numba_zoom.py +503 -0
  44. imops/src/_numeric.c +48651 -0
  45. imops/src/_numeric.cpython-312-x86_64-linux-gnu.so +0 -0
  46. imops/src/_radon.c +30714 -0
  47. imops/src/_radon.cpython-312-x86_64-linux-gnu.so +0 -0
  48. imops/src/_zoom.c +57203 -0
  49. imops/src/_zoom.cpython-312-x86_64-linux-gnu.so +0 -0
  50. imops/testing.py +57 -0
  51. imops/utils.py +205 -0
  52. imops/zoom.py +297 -0
  53. imops-0.8.8.dist-info/LICENSE +21 -0
  54. imops-0.8.8.dist-info/METADATA +218 -0
  55. imops-0.8.8.dist-info/RECORD +58 -0
  56. imops-0.8.8.dist-info/WHEEL +6 -0
  57. imops-0.8.8.dist-info/top_level.txt +2 -0
  58. imops.libs/libgomp-a34b3233.so.1.0.0 +0 -0
imops/morphology.py ADDED
@@ -0,0 +1,525 @@
1
+ from typing import Callable, Tuple, Union
2
+ from warnings import warn
3
+
4
+ import numpy as np
5
+ from edt import edt
6
+ from scipy.ndimage import distance_transform_edt as scipy_distance_transform_edt, generate_binary_structure
7
+ from scipy.ndimage._nd_image import euclidean_feature_transform
8
+
9
+
10
+ try:
11
+ from scipy.ndimage._morphology import _ni_support
12
+ except ImportError:
13
+ from scipy.ndimage.morphology import _ni_support
14
+
15
+ from skimage.morphology import (
16
+ binary_closing as scipy_binary_closing,
17
+ binary_dilation as scipy_binary_dilation,
18
+ binary_erosion as scipy_binary_erosion,
19
+ binary_opening as scipy_binary_opening,
20
+ )
21
+
22
+ from .backend import BackendLike, Cython, Scipy, resolve_backend
23
+ from .box import add_margin, box_to_shape, mask_to_box, shape_to_box
24
+ from .crop import crop_to_box
25
+ from .pad import restore_crop
26
+ from .src._fast_morphology import (
27
+ _binary_dilation as cython_fast_binary_dilation,
28
+ _binary_erosion as cython_fast_binary_erosion,
29
+ )
30
+ from .src._morphology import _binary_dilation as cython_binary_dilation, _binary_erosion as cython_binary_erosion
31
+ from .utils import morphology_composition_args, normalize_num_threads
32
+
33
+
34
+ def morphology_op_wrapper(
35
+ op_name: str, backend2src_op: Callable[[np.ndarray, np.ndarray, int], np.ndarray]
36
+ ) -> Callable:
37
+ def wrapped(
38
+ image: np.ndarray,
39
+ footprint: np.ndarray = None,
40
+ output: np.ndarray = None,
41
+ boxed: bool = False,
42
+ num_threads: int = -1,
43
+ backend: BackendLike = None,
44
+ ) -> np.ndarray:
45
+ backend = resolve_backend(backend, warn_stacklevel=4)
46
+ if backend.name not in {x.name for x in backend2src_op.keys()}:
47
+ raise ValueError(f'Unsupported backend "{backend.name}".')
48
+
49
+ ndim = image.ndim
50
+ num_threads = normalize_num_threads(num_threads, backend, warn_stacklevel=4)
51
+
52
+ if footprint is None:
53
+ footprint = generate_binary_structure(ndim, 1)
54
+ elif not footprint.size:
55
+ raise RuntimeError('Footprint must not be empty.')
56
+
57
+ if output is None:
58
+ output = np.empty_like(image, dtype=bool)
59
+ elif boxed:
60
+ raise ValueError('`boxed==True` is incompatible with provided `output`')
61
+ elif output.shape != image.shape:
62
+ raise ValueError('Input image and output image shapes must be the same.')
63
+ elif output.dtype != bool:
64
+ raise ValueError(f'Output image must have `bool` dtype, got {output.dtype}.')
65
+ elif not output.data.c_contiguous:
66
+ # TODO: Implement morphology for `output` of arbitrary layout
67
+ raise ValueError('`output` must be a C-contiguous array.')
68
+
69
+ src_op = backend2src_op[backend]
70
+
71
+ if backend.name == 'Scipy':
72
+ if boxed:
73
+ raise ValueError('`boxed==True` is incompatible with "Scipy" backend.')
74
+ src_op(image, footprint, out=output)
75
+
76
+ return output
77
+
78
+ if ndim > 3:
79
+ warn(
80
+ f"Fast {' '.join(op_name.split('_'))} is only supported for ndim<=3. "
81
+ "Falling back to scipy's implementation.",
82
+ stacklevel=3,
83
+ )
84
+ backend2src_op[Scipy()](image, footprint, out=output)
85
+
86
+ return output
87
+
88
+ if footprint.ndim != image.ndim:
89
+ raise ValueError('Input image and footprint number of dimensions must be the same.')
90
+
91
+ if not image.any():
92
+ warn(f'{op_name} is applied to the fully False mask (mask.any() == False).', stacklevel=3) # noqa
93
+ output.fill(False)
94
+
95
+ return output
96
+
97
+ if image.all():
98
+ warn(f'{op_name} is applied to the fully True mask (mask.all() == True).', stacklevel=3) # noqa
99
+ output.fill(True)
100
+
101
+ return output
102
+
103
+ n_dummy = 3 - ndim
104
+
105
+ if n_dummy:
106
+ image = image[(None,) * n_dummy]
107
+ output = output[(None,) * n_dummy]
108
+ footprint = footprint[(None,) * n_dummy]
109
+
110
+ src_op_args = (image.astype(bool, copy=False), footprint.astype(bool, copy=False), output, num_threads)
111
+ output = boxed_morphology(src_op, op_name)(*src_op_args) if boxed else src_op(*src_op_args)
112
+
113
+ if n_dummy:
114
+ output = output[(0,) * n_dummy]
115
+
116
+ return output
117
+
118
+ return wrapped
119
+
120
+
121
+ def boxed_morphology(func, op_name) -> Callable:
122
+ # TODO: for consistency support exotic footprints which alter border pixels in Scikit-Image different from the
123
+ # current implementation, e.g. footrint [[1, 1], [1, 0]] sets border pixel to 1 for `binary_erosion`
124
+ def wrapped(
125
+ image: np.ndarray,
126
+ footprint: np.ndarray,
127
+ output: np.ndarray,
128
+ num_threads: int,
129
+ ) -> np.ndarray:
130
+ box_delta = np.asarray(footprint.shape) // 2
131
+
132
+ image_box = shape_to_box(image.shape)
133
+ tight_box = mask_to_box(image)
134
+ supp_box = add_margin(tight_box, 2 * box_delta)
135
+
136
+ # TODO: generalize to "anisotropic" images
137
+ # TODO: make separate class for `Box` and implement comparison operators?
138
+ if (supp_box[0] < image_box[0]).any() or (image_box[1] < supp_box[1]).any():
139
+ return func(image, footprint, output, num_threads)
140
+
141
+ final_crop_box = add_margin(tight_box, box_delta)
142
+
143
+ supp_image = crop_to_box(image, supp_box)
144
+ supp_output = np.empty_like(supp_image, dtype=bool)
145
+
146
+ cropped = crop_to_box(
147
+ func(supp_image, footprint, supp_output, num_threads),
148
+ add_margin(shape_to_box(box_to_shape(supp_box)), -box_delta), # crop border values of supp_box
149
+ )
150
+
151
+ output = restore_crop(cropped, final_crop_box, image.shape, False)
152
+
153
+ return output
154
+
155
+ return wrapped
156
+
157
+
158
+ _binary_dilation = morphology_op_wrapper(
159
+ 'binary_dilation',
160
+ {
161
+ Scipy(): scipy_binary_dilation,
162
+ Cython(fast=False): cython_binary_dilation,
163
+ Cython(fast=True): cython_fast_binary_dilation,
164
+ },
165
+ )
166
+
167
+
168
+ def binary_dilation(
169
+ image: np.ndarray,
170
+ footprint: np.ndarray = None,
171
+ output: np.ndarray = None,
172
+ boxed: bool = False,
173
+ num_threads: int = -1,
174
+ backend: BackendLike = None,
175
+ ) -> np.ndarray:
176
+ """
177
+ Fast parallelizable binary morphological dilation of an image
178
+
179
+ Parameters
180
+ ----------
181
+ image: np.ndarray
182
+ input image
183
+ footprint: np.ndarray
184
+ the neighborhood expressed as a n-D array of 1's and 0's. If None, use a cross-shaped footprint (connectivity=1)
185
+ output: np.ndarray
186
+ array of the same shape as input, into which the output is placed (must be C-contiguous). By default, a new
187
+ array is created
188
+ boxed: bool
189
+ if True, dilation is performed on cropped image which may speed up computation depedning on how localized True
190
+ pixels are. This may induce differences with Scikit-Image implementation at border pixels if footprint is
191
+ exotic (has even shape or center pixel is False)
192
+ num_threads: int
193
+ the number of threads to use for computation. Default = the cpu count. If negative value passed
194
+ cpu count + num_threads + 1 threads will be used
195
+ backend: BackendLike
196
+ which backend to use. `cython` and `scipy` are available, `cython` is used by default
197
+
198
+ Returns
199
+ -------
200
+ dilated: np.ndarray
201
+ the result of morphological dilation
202
+
203
+ Examples
204
+ --------
205
+ ```python
206
+ dilated = binary_dilation(x)
207
+ ```
208
+ """
209
+ return _binary_dilation(image, footprint, output, boxed, num_threads, backend)
210
+
211
+
212
+ _binary_erosion = morphology_op_wrapper(
213
+ 'binary_erosion',
214
+ {
215
+ Scipy(): scipy_binary_erosion,
216
+ Cython(fast=False): cython_binary_erosion,
217
+ Cython(fast=True): cython_fast_binary_erosion,
218
+ },
219
+ )
220
+
221
+
222
+ def binary_erosion(
223
+ image: np.ndarray,
224
+ footprint: np.ndarray = None,
225
+ output: np.ndarray = None,
226
+ boxed: bool = False,
227
+ num_threads: int = -1,
228
+ backend: BackendLike = None,
229
+ ) -> np.ndarray:
230
+ """
231
+ Fast parallelizable binary morphological erosion of an image
232
+
233
+ Parameters
234
+ ----------
235
+ image: np.ndarray
236
+ input image
237
+ footprint: np.ndarray
238
+ the neighborhood expressed as a n-D array of 1's and 0's. If None, use a cross-shaped footprint (connectivity=1)
239
+ output: np.ndarray
240
+ array of the same shape as input, into which the output is placed (must be C-contiguous). By default, a new
241
+ array is created
242
+ boxed: bool
243
+ if True, erosion is performed on cropped image which may speed up computation depedning on how localized True
244
+ pixels are. This may induce differences with Scikit-Image implementation at border pixels if footprint is
245
+ exotic (has even shape or center pixel is False)
246
+ num_threads: int
247
+ the number of threads to use for computation. Default = the cpu count. If negative value passed
248
+ cpu count + num_threads + 1 threads will be used
249
+ backend: BackendLike
250
+ which backend to use. `cython` and `scipy` are available, `cython` is used by default
251
+
252
+ Returns
253
+ -------
254
+ eroded: np.ndarray
255
+ the result of morphological erosion
256
+
257
+ Examples
258
+ --------
259
+ ```python
260
+ eroded = binary_erosion(x)
261
+ ```
262
+ """
263
+ return _binary_erosion(image, footprint, output, boxed, num_threads, backend)
264
+
265
+
266
+ _binary_closing = morphology_op_wrapper(
267
+ 'binary_closing',
268
+ {
269
+ Scipy(): scipy_binary_closing,
270
+ Cython(fast=False): morphology_composition_args(cython_binary_erosion, cython_binary_dilation),
271
+ Cython(fast=True): morphology_composition_args(cython_fast_binary_erosion, cython_fast_binary_dilation),
272
+ },
273
+ )
274
+
275
+
276
+ def binary_closing(
277
+ image: np.ndarray,
278
+ footprint: np.ndarray = None,
279
+ output: np.ndarray = None,
280
+ boxed: bool = False,
281
+ num_threads: int = -1,
282
+ backend: BackendLike = None,
283
+ ) -> np.ndarray:
284
+ """
285
+ Fast parallelizable binary morphological closing of an image
286
+
287
+ Parameters
288
+ ----------
289
+ image: np.ndarray
290
+ input image
291
+ footprint: np.ndarray
292
+ the neighborhood expressed as a n-D array of 1's and 0's. If None, use a cross-shaped footprint (connectivity=1)
293
+ output: np.ndarray
294
+ array of the same shape as input, into which the output is placed (must be C-contiguous). By default, a new
295
+ array is created
296
+ boxed: bool
297
+ if True, closing is performed on cropped image which may speed up computation depedning on how localized True
298
+ pixels are. This may induce differences with Scikit-Image implementation at border pixels if footprint is
299
+ exotic (has even shape or center pixel is False)
300
+ num_threads: int
301
+ the number of threads to use for computation. Default = the cpu count. If negative value passed
302
+ cpu count + num_threads + 1 threads will be used
303
+ backend: BackendLike
304
+ which backend to use. `cython` and `scipy` are available, `cython` is used by default
305
+
306
+ Returns
307
+ -------
308
+ closed: np.ndarray
309
+ the result of morphological closing
310
+
311
+ Examples
312
+ --------
313
+ ```python
314
+ closed = binary_closing(x)
315
+ ```
316
+ """
317
+
318
+ return _binary_closing(image, footprint, output, boxed, num_threads, backend)
319
+
320
+
321
+ _binary_opening = morphology_op_wrapper(
322
+ 'binary_opening',
323
+ {
324
+ Scipy(): scipy_binary_opening,
325
+ Cython(fast=False): morphology_composition_args(cython_binary_dilation, cython_binary_erosion),
326
+ Cython(fast=True): morphology_composition_args(cython_fast_binary_dilation, cython_fast_binary_erosion),
327
+ },
328
+ )
329
+
330
+
331
+ def binary_opening(
332
+ image: np.ndarray,
333
+ footprint: np.ndarray = None,
334
+ output: np.ndarray = None,
335
+ boxed: bool = False,
336
+ num_threads: int = -1,
337
+ backend: BackendLike = None,
338
+ ) -> np.ndarray:
339
+ """
340
+ Fast parallelizable binary morphological opening of an image
341
+
342
+ Parameters
343
+ ----------
344
+ image: np.ndarray
345
+ input image
346
+ footprint: np.ndarray
347
+ the neighborhood expressed as a n-D array of 1's and 0's. If None, use a cross-shaped footprint (connectivity=1)
348
+ output: np.ndarray
349
+ array of the same shape as input, into which the output is placed (must be C-contiguous). By default, a new
350
+ array is created
351
+ boxed: bool
352
+ if True, opening is performed on cropped image which may speed up computation depedning on how localized True
353
+ pixels are. This may induce differences with Scikit-Image implementation at border pixels if footprint is
354
+ exotic (has even shape or center pixel is False)
355
+ num_threads: int
356
+ the number of threads to use for computation. Default = the cpu count. If negative value passed
357
+ cpu count + num_threads + 1 threads will be used
358
+ backend: BackendLike
359
+ which backend to use. `cython` and `scipy` are available, `cython` is used by default
360
+
361
+ Returns
362
+ -------
363
+ opened: np.ndarray
364
+ the result of morphological opening
365
+
366
+ Examples
367
+ --------
368
+ ```python
369
+ opened = binary_opening(x)
370
+ ```
371
+ """
372
+
373
+ return _binary_opening(image, footprint, output, boxed, num_threads, backend)
374
+
375
+
376
+ def distance_transform_edt(
377
+ image: np.ndarray,
378
+ sampling: Tuple[float] = None,
379
+ return_distances: bool = True,
380
+ return_indices: bool = False,
381
+ num_threads: int = -1,
382
+ backend: BackendLike = None,
383
+ ) -> Union[np.ndarray, Tuple[np.ndarray]]:
384
+ """
385
+ Fast parallelizable Euclidean distance transform for <= 3D inputs
386
+
387
+ This function calculates the distance transform of the `image`, by
388
+ replacing each foreground (non-zero) element, with its
389
+ shortest distance to the background (any zero-valued element).
390
+
391
+ In addition to the distance transform, the feature transform can
392
+ be calculated. In this case the index of the closest background
393
+ element to each foreground element is returned in a separate array.
394
+
395
+ Parameters
396
+ ----------
397
+ image : array_like
398
+ input data to transform. Can be any type but will be converted
399
+ into binary: 1 wherever input equates to True, 0 elsewhere
400
+ sampling : tuple of `image.ndim` floats, optional
401
+ spacing of elements along each dimension. If a sequence, must be of
402
+ length equal to the input rank; if a single number, this is used for
403
+ all axes. If not specified, a grid spacing of unity is implied
404
+ return_distances : bool, optional
405
+ whether to calculate the distance transform.
406
+ Default is True
407
+ return_indices : bool, optional
408
+ whether to calculate the feature transform.
409
+ Default is False
410
+ num_threads: int
411
+ the number of threads to use for computation. Default = the cpu count. If negative value passed
412
+ cpu count + num_threads + 1 threads will be used
413
+ backend: BackendLike
414
+ which backend to use. `cython` and `scipy` are available, `cython` is used by default
415
+
416
+ Returns
417
+ -------
418
+ distances : float32 ndarray, optional
419
+ the calculated distance transform. Returned only when
420
+ `return_distances` is True and `distances` is not supplied.
421
+ It will have the same shape as the input array
422
+ indices : int32 ndarray, optional
423
+ the calculated feature transform. It has an input-shaped array for each
424
+ dimension of the input. See example below.
425
+ Returned only when `return_indices` is True and `indices` is not
426
+ supplied
427
+
428
+ Notes
429
+ -----
430
+ The Euclidean distance transform gives values of the Euclidean
431
+ distance::
432
+
433
+ n
434
+ y_i = sqrt(sum (x[i]-b[i])**2)
435
+ i
436
+
437
+ where b[i] is the background point (value 0) with the smallest
438
+ Euclidean distance to input points x[i], and n is the
439
+ number of dimensions.
440
+
441
+ Examples
442
+ --------
443
+ import numpy as np
444
+ a = np.array(([0,1,1,1,1],
445
+ [0,0,1,1,1],
446
+ [0,1,1,1,1],
447
+ [0,1,1,1,0],
448
+ [0,1,1,0,0]))
449
+ distance_transform_edt(a)
450
+ array([[ 0. , 1. , 1.4142, 2.2361, 3. ],
451
+ [ 0. , 0. , 1. , 2. , 2. ],
452
+ [ 0. , 1. , 1.4142, 1.4142, 1. ],
453
+ [ 0. , 1. , 1.4142, 1. , 0. ],
454
+ [ 0. , 1. , 1. , 0. , 0. ]])
455
+
456
+ With a sampling of 2 units along x, 1 along y:
457
+
458
+ distance_transform_edt(a, sampling=[2, 1])
459
+ array([[ 0. , 1. , 2. , 2.8284, 3.6056],
460
+ [ 0. , 0. , 1. , 2. , 3. ],
461
+ [ 0. , 1. , 2. , 2.2361, 2. ],
462
+ [ 0. , 1. , 2. , 1. , 0. ],
463
+ [ 0. , 1. , 1. , 0. , 0. ]])
464
+
465
+ Asking for indices as well:
466
+
467
+ edt, inds = distance_transform_edt(a, return_indices=True)
468
+ inds
469
+ array([[[0, 0, 1, 1, 3],
470
+ [1, 1, 1, 1, 3],
471
+ [2, 2, 1, 3, 3],
472
+ [3, 3, 4, 4, 3],
473
+ [4, 4, 4, 4, 4]],
474
+ [[0, 0, 1, 1, 4],
475
+ [0, 1, 1, 1, 4],
476
+ [0, 0, 1, 4, 4],
477
+ [0, 0, 3, 3, 4],
478
+ [0, 0, 3, 3, 4]]])
479
+ """
480
+ backend = resolve_backend(backend, warn_stacklevel=3)
481
+ if backend.name not in ('Scipy', 'Cython'):
482
+ raise ValueError(f'Unsupported backend "{backend.name}".')
483
+
484
+ num_threads = normalize_num_threads(num_threads, backend, warn_stacklevel=3)
485
+
486
+ if backend.name == 'Scipy':
487
+ return scipy_distance_transform_edt(image, sampling, return_distances, return_indices)
488
+
489
+ if image.ndim > 3:
490
+ warn("Fast Euclidean Distance Transform is only supported for ndim<=3. Falling back to scipy's implementation.")
491
+ return scipy_distance_transform_edt(image, sampling, return_distances, return_indices)
492
+
493
+ if (not return_distances) and (not return_indices):
494
+ raise RuntimeError('At least one of `return_distances`/`return_indices` must be True')
495
+ if image.dtype != bool:
496
+ image = np.atleast_1d(np.where(image, 1, 0))
497
+ if sampling is not None:
498
+ sampling = _ni_support._normalize_sequence(sampling, image.ndim)
499
+ sampling = np.asarray(sampling, dtype=np.float64)
500
+ if not sampling.flags.contiguous:
501
+ sampling = sampling.copy()
502
+
503
+ if return_indices:
504
+ ft = np.zeros((image.ndim,) + image.shape, dtype=np.int32)
505
+ euclidean_feature_transform(image, sampling, ft)
506
+
507
+ if return_distances:
508
+ if sampling is not None:
509
+ dt = edt(image, anisotropy=sampling.astype(np.float32), parallel=num_threads)
510
+ else:
511
+ dt = edt(image, parallel=num_threads)
512
+
513
+ result = []
514
+ if return_distances:
515
+ result.append(dt)
516
+ if return_indices:
517
+ result.append(ft)
518
+
519
+ if len(result) == 2:
520
+ return tuple(result)
521
+
522
+ if len(result) == 1:
523
+ return result[0]
524
+
525
+ return None