1import collections.abc as cabc
2import itertools
3
4import pyxu.abc as pxa
5import pyxu.info.deps as pxd
6import pyxu.info.ptype as pxt
7import pyxu.operator.linop.base as pxlb
8import pyxu.operator.linop.diff as pxld
9import pyxu.operator.linop.pad as pxlp
10import pyxu.operator.linop.stencil.stencil as pxls
11import pyxu.runtime as pxrt
12import pyxu.util as pxu
13
14try:
15 import scipy.ndimage._filters as scif
16except ImportError:
17 import scipy.ndimage.filters as scif
18
19import functools
20import typing as typ
21
22import numpy as np
23
24IndexSpec = cabc.Sequence[pxt.Integer]
25KernelSpec = pxls.Stencil.KernelSpec
26ModeSpec = pxlp.Pad.ModeSpec
27
28
29__all__ = [
30 "MovingAverage",
31 "GaussianFilter",
32 "DifferenceOfGaussians",
33 "DoG",
34 "Laplace",
35 "Sobel",
36 "Prewitt",
37 "Scharr",
38 "StructureTensor",
39]
40
41
42def _to_canonical_form(_, dim_shape):
43 if not isinstance(_, cabc.Sequence):
44 _ = (_,) * len(dim_shape)
45 else:
46 assert len(_) == len(dim_shape)
47 return _
48
49
50def _get_axes(axis, ndim):
51 if axis is None:
52 axes = list(range(ndim))
53 elif np.isscalar(axis):
54 axes = [axis]
55 else:
56 axes = axis
57 return axes
58
59
60def _sanitize_inputs(dim_shape, dtype, gpu):
61 ndim = len(dim_shape)
62
63 if dtype is None:
64 dtype = pxrt.Width.DOUBLE.value
65
66 if gpu:
67 assert pxd.CUPY_ENABLED
68 import cupy as xp
69 else:
70 import numpy as xp
71 return ndim, dtype, xp
72
73
[docs]
74def MovingAverage(
75 dim_shape: pxt.NDArrayShape,
76 size: typ.Union[typ.Tuple, pxt.Integer],
77 center: typ.Optional[IndexSpec] = None,
78 mode: ModeSpec = "constant",
79 gpu: bool = False,
80 dtype: typ.Optional[pxt.DType] = None,
81):
82 r"""
83 Multi-dimensional moving average or uniform filter.
84
85 Notes
86 -----
87 This operator performs a convolution between the input :math:`D`-dimensional NDArray :math:`\mathbf{x} \in
88 \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` and a uniform :math:`D`-dimensional filter :math:`\mathbf{h} \in
89 \mathbb{R}^{\text{size} \times \cdots \times \text{size}}` that computes the `size`-point local mean values using
90 separable kernels for improved performance.
91
92 .. math::
93
94 y_{i} = \frac{1}{|\mathcal{N}_{i}|}\sum_{j \in \mathcal{N}_{i}} x_{j}
95
96 Where :math:`\mathcal{N}_{i}` is the set of elements neighbouring the :math:`i`-th element of the input array, and
97 :math:`\mathcal{N}_{i}` denotes its cardinality, i.e. the total number of neighbors.
98
99
100 Parameters
101 ----------
102 dim_shape: NDArrayShape
103 Shape of the input array.
104 size: int, tuple
105 Size of the moving average kernel.
106
107 If a single integer value is provided, then the moving average filter will have as many dimensions as the input
108 array. If a tuple is provided, it should contain as many elements as `dim_shape`. For example, the ``size=(1,
109 3)`` will convolve the input image with the filter ``[[1, 1, 1]] / 3``.
110
111 center: ~pyxu.operator.linop.filter.IndexSpec
112 (i_1, ..., i_D) index of the kernel's center.
113
114 `center` defines how a kernel is overlaid on inputs to produce outputs.
115 For odd `size`, it defaults to the central element (``center=size//2``).
116 For even `size` the desired center indices must be provided.
117
118 mode: str, list[str]
119 Boundary conditions.
120 Multiple forms are accepted:
121
122 * str: unique mode shared amongst dimensions.
123 Must be one of:
124
125 * 'constant' (default): zero-padding
126 * 'wrap'
127 * 'reflect'
128 * 'symmetric'
129 * 'edge'
130 * tuple[str, ...]: the `d`-th dimension uses `mode[d]` as boundary condition.
131
132 (See :py:func:`numpy.pad` for details.)
133 gpu: bool
134 Input NDArray type (`True` for GPU, `False` for CPU). Defaults to `False`.
135 dtype: DType
136 Working precision of the linear operator.
137
138 Returns
139 -------
140 op: OpT
141 MovingAverage
142
143 Example
144 -------
145
146 .. plot::
147
148 import matplotlib.pyplot as plt
149 import numpy as np
150 from pyxu.operator import MovingAverage
151
152 dim_shape = (11, 11)
153 image = np.zeros(dim_shape)
154 image[5, 5] = 1.
155
156 ma = MovingAverage(dim_shape, size=5)
157 out = ma(image)
158 plt.figure(figsize=(10, 5))
159 plt.subplot(121)
160 plt.imshow(image)
161 plt.colorbar()
162 plt.subplot(122)
163 plt.imshow(out)
164 plt.colorbar()
165
166 See Also
167 --------
168 :py:class:`~pyxu.operator.GaussianFilter`
169 """
170 size = _to_canonical_form(size, dim_shape)
171 if center is None:
172 assert all([s % 2 == 1 for s in size]), (
173 "Can only infer center for odd `size`s. For even `size`s, please " "provide the desired `center`s."
174 )
175 center = [s // 2 for s in size]
176
177 ndim, dtype, xp = _sanitize_inputs(dim_shape, dtype, gpu)
178
179 kernel = [xp.ones(s, dtype=dtype) for s in size] # use separable filters
180 scale = 1 / np.prod(size)
181
182 op = scale * pxls.Stencil(dim_shape=dim_shape, kernel=kernel, center=center, mode=mode)
183 op._name = "MovingAverage"
184 return op
185
186
187def GaussianFilter(
188 dim_shape: pxt.NDArrayShape,
189 sigma: typ.Union[typ.Tuple[pxt.Real], pxt.Real] = 1.0,
190 truncate: typ.Union[typ.Tuple[pxt.Real], pxt.Real] = 3.0,
191 order: typ.Union[typ.Tuple[pxt.Integer], pxt.Integer] = 0,
192 mode: ModeSpec = "constant",
193 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
194 gpu: bool = False,
195 dtype: typ.Optional[pxt.DType] = None,
196):
197 r"""
198 Multi-dimensional Gaussian filter.
199
200 Notes
201 -----
202 This operator performs a convolution between the input :math:`D`-dimensional NDArray :math:`\mathbf{x} \in
203 \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` and a Gaussian :math:`D`-dimensional filter :math:`\mathbf{h} \in
204 \mathbb{R}^{\text{size} \times \cdots \times \text{size}}` using separable kernels for improved performance.
205
206 .. math::
207
208 y_{i} = \sum_{j \in \mathcal{N}_{i}} a_{j} x_{j} \exp(\frac{d_{ij}^{2}}{\sigma^{2}})
209
210 Where :math:`\mathcal{N}_{i}` is the set of elements neighbouring the :math:`i`-th element of the input array, and
211 :math:`a_{j} = \sum_{j \in \mathcal{N}_{i}} a_{j} \exp(\frac{d_{ij}^{2}}{\sigma^{2}})` normalizes the kernel to sum
212 to one.
213
214
215 Parameters
216 ----------
217 dim_shape: NDArrayShape
218 Shape of the input array.
219 sigma: float, tuple
220 Standard deviation of the Gaussian kernel.
221
222 If a scalar value is provided, then the Gaussian filter will have as many dimensions as the input array.
223 If a tuple is provided, it should contain as many elements as `dim_shape`.
224 Use ``0`` to prevent filtering in a given dimension.
225 For example, the ``sigma=(0, 3)`` will convolve the input image in its last dimension.
226 truncate: float, tuple
227 Truncate the filter at this many standard deviations.
228 Defaults to 3.0.
229 order: int, tuple
230 Gaussian derivative order.
231
232 Use ``0`` for the standard Gaussian kernel.
233 mode: str, list[str]
234 Boundary conditions.
235 Multiple forms are accepted:
236
237 * str: unique mode shared amongst dimensions.
238 Must be one of:
239
240 * 'constant' (default): zero-padding
241 * 'wrap'
242 * 'reflect'
243 * 'symmetric'
244 * 'edge'
245 * tuple[str, ...]: the `d`-th dimension uses `mode[d]` as boundary condition.
246
247 (See :py:func:`numpy.pad` for details.)
248 sampling: Real, list[Real]
249 Sampling step (i.e. distance between two consecutive elements of an array).
250 Defaults to 1.
251 gpu: bool
252 Input NDArray type (`True` for GPU, `False` for CPU).
253 Defaults to `False`.
254 dtype: DType
255 Working precision of the linear operator.
256
257 Returns
258 -------
259 op: OpT
260 GaussianFilter
261
262 Example
263 -------
264
265 .. plot::
266
267 import matplotlib.pyplot as plt
268 import numpy as np
269 from pyxu.operator import GaussianFilter
270
271 dim_shape = (11, 11)
272 image = np.zeros(dim_shape)
273 image[5, 5] = 1.
274
275 gaussian = GaussianFilter(dim_shape, sigma=3)
276 out = gaussian(image)
277 plt.figure(figsize=(10, 5))
278 plt.subplot(121)
279 plt.imshow(image)
280 plt.colorbar()
281 plt.subplot(122)
282 plt.imshow(out)
283 plt.colorbar()
284
285 See Also
286 --------
287 :py:class:`~pyxu.operator.MovingAverage`,
288 :py:class:`~pyxu.operator.DifferenceOfGaussians`
289 """
290
291 ndim, dtype, xp = _sanitize_inputs(dim_shape, dtype, gpu)
292 sigma = _to_canonical_form(sigma, dim_shape)
293 truncate = _to_canonical_form(truncate, dim_shape)
294 order = _to_canonical_form(order, dim_shape)
295 sampling = _to_canonical_form(sampling, dim_shape)
296
297 kernel = [
298 xp.array([1], dtype=dtype),
299 ] * len(dim_shape)
300 center = [0 for _ in range(len(dim_shape))]
301
302 for i, (sigma_, truncate_, order_, sampling_) in enumerate(zip(sigma, truncate, order, sampling)):
303 if sigma_:
304 sigma_pix = sigma_ / sampling_
305 radius = int(truncate_ * float(sigma_pix) + 0.5)
306 kernel[i] = xp.asarray(np.flip(scif._gaussian_kernel1d(sigma_pix, order_, radius)), dtype=dtype)
307 kernel[i] /= sampling_**order_
308 center[i] = radius
309
310 op = pxls.Stencil(dim_shape=dim_shape, kernel=kernel, center=center, mode=mode)
311 op._name = "GaussianFilter"
312 return op
313
314
[docs]
315def DifferenceOfGaussians(
316 dim_shape: pxt.NDArrayShape,
317 low_sigma=1.0,
318 high_sigma=None,
319 low_truncate=3.0,
320 high_truncate=3.0,
321 mode: ModeSpec = "constant",
322 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
323 gpu: bool = False,
324 dtype: typ.Optional[pxt.DType] = None,
325):
326 r"""
327 Multi-dimensional Difference of Gaussians filter.
328
329 Notes
330 -----
331
332 This operator uses the Difference of Gaussians (DoG) method to a :math:`D`-dimensional NDArray :math:`\mathbf{x} \in
333 \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` using separable kernels for improved performance.
334 The DoG method blurs the input image with two Gaussian kernels with different sigma, and subtracts the more-blurred
335 signal from the less-blurred image.
336 This creates an output signal containing only the information from the original signal at the spatial scale
337 indicated by the two sigmas.
338
339 Parameters
340 ----------
341 dim_shape: NDArrayShape
342 Shape of the input array.
343 low_sigma: float, tuple
344 Standard deviation of the Gaussian kernel with smaller sigmas across all axes.
345
346 If a scalar value is provided, then the Gaussian filter will have as many dimensions as the input array.
347 If a tuple is provided, it should contain as many elements as `dim_shape`.
348 Use ``0`` to prevent filtering in a given dimension.
349 For example, the ``low_sigma=(0, 3)`` will convolve the input image in its last dimension.
350 high_sigma: float, tuple, None
351 Standard deviation of the Gaussian kernel with larger sigmas across all axes.
352 If ``None`` is given (default), sigmas for all axes are calculated as ``1.6 * low_sigma``.
353 low_truncate: float, tuple
354 Truncate the filter at this many standard deviations.
355 Defaults to 3.0.
356 high_truncate: float, tuple
357 Truncate the filter at this many standard deviations.
358 Defaults to 3.0.
359 mode: str, list[str]
360 Boundary conditions.
361 Multiple forms are accepted:
362
363 * str: unique mode shared amongst dimensions.
364 Must be one of:
365
366 * 'constant' (default): zero-padding
367 * 'wrap'
368 * 'reflect'
369 * 'symmetric'
370 * 'edge'
371 * tuple[str, ...]: the `d`-th dimension uses `mode[d]` as boundary condition.
372
373 (See :py:func:`numpy.pad` for details.)
374 sampling: Real, list[Real]
375 Sampling step (i.e. distance between two consecutive elements of an array).
376 Defaults to 1.
377 gpu: bool
378 Input NDArray type (`True` for GPU, `False` for CPU). Defaults to `False`.
379 dtype: DType
380 Working precision of the linear operator.
381
382 Returns
383 -------
384 op: OpT
385 DifferenceOfGaussians
386
387 Example
388 -------
389
390 .. plot::
391
392 import matplotlib.pyplot as plt
393 import numpy as np
394 from pyxu.operator import DoG
395
396 dim_shape = (11, 11)
397 image = np.zeros(dim_shape)
398 image[5, 5] = 1.
399
400 dog = DoG(dim_shape, low_sigma=3)
401 out = dog(image)
402 plt.figure(figsize=(10, 5))
403 plt.subplot(121)
404 plt.imshow(image)
405 plt.colorbar()
406 plt.subplot(122)
407 plt.imshow(out)
408 plt.colorbar()
409
410 See Also
411 --------
412 :py:class:`~pyxu.operator.Gaussian`,
413 :py:class:`~pyxu.operator.Sobel`,
414 :py:class:`~pyxu.operator.Prewitt`,
415 :py:class:`~pyxu.operator.Scharr`,
416 :py:class:`~pyxu.operator.StructureTensor`
417 """
418
419 low_sigma = _to_canonical_form(low_sigma, dim_shape)
420 if high_sigma is None:
421 high_sigma = tuple(s * 1.6 for s in low_sigma)
422
423 high_sigma = _to_canonical_form(high_sigma, dim_shape)
424 low_truncate = _to_canonical_form(low_truncate, dim_shape)
425 high_truncate = _to_canonical_form(high_truncate, dim_shape)
426
427 kwargs = {
428 "dim_shape": dim_shape,
429 "order": 0,
430 "mode": mode,
431 "gpu": gpu,
432 "dtype": dtype,
433 "sampling": sampling,
434 }
435 op_low = GaussianFilter(sigma=low_sigma, truncate=low_truncate, **kwargs)
436 op_high = GaussianFilter(sigma=high_sigma, truncate=high_truncate, **kwargs)
437 op = op_low - op_high
438 op._name = "DifferenceOfGaussians"
439 return op
440
441
442DoG = DifferenceOfGaussians #: Alias of :py:func:`~pyxu.operator.DifferenceOfGaussians`.
443
444
[docs]
445def Laplace(
446 dim_shape: pxt.NDArrayShape,
447 mode: ModeSpec = "constant",
448 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
449 gpu: bool = False,
450 dtype: typ.Optional[pxt.DType] = None,
451):
452 r"""
453 Multi-dimensional Laplace filter.
454
455 The implementation is based on second derivatives approximated via finite differences.
456
457 Notes
458 -----
459 This operator uses the applies the Laplace kernel :math:`[1 -2 1]` to a :math:`D`-dimensional NDArray
460 :math:`\mathbf{x} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` using separable kernels for improved
461 performance. The Laplace filter is commonly used to find high-frequency components in the signal, such as for
462 example, the edges in an image.
463
464 Parameters
465 ----------
466 dim_shape: NDArrayShape
467 Shape of the input array.
468 mode: str, list[str]
469 Boundary conditions.
470 Multiple forms are accepted:
471
472 * str: unique mode shared amongst dimensions.
473 Must be one of:
474
475 * 'constant' (default): zero-padding
476 * 'wrap'
477 * 'reflect'
478 * 'symmetric'
479 * 'edge'
480 * tuple[str, ...]: the `d`-th dimension uses `mode[d]` as boundary condition.
481
482 (See :py:func:`numpy.pad` for details.)
483 sampling: Real, list[Real]
484 Sampling step (i.e. distance between two consecutive elements of an array).
485 Defaults to 1.
486 gpu: bool
487 Input NDArray type (`True` for GPU, `False` for CPU). Defaults to `False`.
488 dtype: DType
489 Working precision of the linear operator.
490
491 Returns
492 -------
493 op: OpT
494 DifferenceOfGaussians
495
496 Example
497 -------
498
499 .. plot::
500
501 import matplotlib.pyplot as plt
502 import numpy as np
503 from pyxu.operator import Laplace
504
505 dim_shape = (11, 11)
506 image = np.zeros(dim_shape)
507 image[5, 5] = 1.
508
509 laplace = Laplace(dim_shape)
510 out = laplace(image)
511 plt.figure(figsize=(10, 5))
512 plt.subplot(121)
513 plt.imshow(image)
514 plt.colorbar()
515 plt.subplot(122)
516 plt.imshow(out)
517 plt.colorbar()
518
519 See Also
520 --------
521 :py:class:`~pyxu.operator.Sobel`,
522 :py:class:`~pyxu.operator.Prewitt`,
523 :py:class:`~pyxu.operator.Scharr`,
524 """
525
526 ndim, dtype, xp = _sanitize_inputs(dim_shape, dtype, gpu)
527 sampling = _to_canonical_form(sampling, dim_shape)
528 centers = [[1 if i == dim else 0 for i in range(ndim)] for dim in range(ndim)]
529 kernels = [
530 xp.array([1.0, -2.0, 1.0], dtype=dtype).reshape([-1 if i == dim else 1 for i in range(ndim)]) / sampling[dim]
531 for dim in range(ndim)
532 ]
533 ops = [pxls.Stencil(dim_shape=dim_shape, kernel=k, center=c, mode=mode) for (k, c) in zip(kernels, centers)]
534 op = functools.reduce(lambda x, y: x + y, ops)
535 op._name = "Laplace"
536 return op
537
538
[docs]
539def Sobel(
540 dim_shape: pxt.NDArrayShape,
541 axis: typ.Optional[typ.Tuple] = None,
542 mode: ModeSpec = "constant",
543 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
544 gpu: bool = False,
545 dtype: typ.Optional[pxt.DType] = None,
546):
547 r"""
548 Multi-dimensional Sobel filter.
549
550 Notes
551 -----
552
553 This operator uses the applies the multi-dimensional Sobel filter to a :math:`D`-dimensional NDArray
554 :math:`\mathbf{x} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` using separable kernels for improved
555 performance. The Sobel filter applies the following edge filter in the dimensions of interest: ``[1, 0, -1]`` and
556 the smoothing filter on the rest of dimensions: ``[1, 2, 1] / 4``. The Sobel filter is commonly used to find
557 high-frequency components in the signal, such as for example, the edges in an image.
558
559 Parameters
560 ----------
561 dim_shape: NDArrayShape
562 Shape of the input array.
563 axis: int, tuple
564 Compute the edge filter along this axis. If not provided, the edge magnitude is computed.
565
566 This is defined as: ``np.sqrt(sum([sobel(array, axis=i)**2 for i in range(array.ndim)]) / array.ndim)`` The
567 magnitude is also computed if axis is a sequence.
568
569 mode: str, list[str]
570 Boundary conditions.
571 Multiple forms are accepted:
572
573 * str: unique mode shared amongst dimensions.
574 Must be one of:
575
576 * 'constant' (default): zero-padding
577 * 'wrap'
578 * 'reflect'
579 * 'symmetric'
580 * 'edge'
581 * tuple[str, ...]: the `d`-th dimension uses `mode[d]` as boundary condition.
582
583 (See :py:func:`numpy.pad` for details.)
584 sampling: Real, list[Real]
585 Sampling step (i.e. distance between two consecutive elements of an array).
586 Defaults to 1.
587 gpu: bool
588 Input NDArray type (`True` for GPU, `False` for CPU). Defaults to `False`.
589 dtype: DType
590 Working precision of the linear operator.
591
592 Returns
593 -------
594 op: OpT
595 Sobel
596
597 Example
598 -------
599
600 .. plot::
601
602 import matplotlib.pyplot as plt
603 import numpy as np
604 from pyxu.operator import Sobel
605
606 dim_shape = (11, 11)
607 image = np.zeros(dim_shape)
608 image[5, 5] = 1.
609
610 sobel = Sobel(dim_shape)
611 out = sobel(image)
612 plt.figure(figsize=(10, 5))
613 plt.subplot(121)
614 plt.imshow(image)
615 plt.colorbar()
616 plt.subplot(122)
617 plt.imshow(out)
618 plt.colorbar()
619
620 See Also
621 --------
622 :py:class:`~pyxu.operator.Prewitt`,
623 :py:class:`~pyxu.operator.Scharr`,
624 """
625 smooth_kernel = np.array([1, 2, 1]) / 4
626 return _EdgeFilter(
627 dim_shape=dim_shape,
628 smooth_kernel=smooth_kernel,
629 filter_name="SobelFilter",
630 axis=axis,
631 mode=mode,
632 sampling=sampling,
633 gpu=gpu,
634 dtype=dtype,
635 )
636
637
[docs]
638def Prewitt(
639 dim_shape: pxt.NDArrayShape,
640 axis: typ.Optional[typ.Tuple] = None,
641 mode: ModeSpec = "constant",
642 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
643 gpu: bool = False,
644 dtype: typ.Optional[pxt.DType] = None,
645):
646 r"""
647 Multi-dimensional Prewitt filter.
648
649 Notes
650 -----
651
652 This operator uses the applies the multi-dimensional Prewitt filter to a :math:`D`-dimensional NDArray
653 :math:`\mathbf{x} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` using separable kernels for improved
654 performance. The Prewitt filter applies the following edge filter in the dimensions of interest: ``[1, 0, -1]``,
655 and the smoothing filter on the rest of dimensions: ``[1, 1, 1] / 3``. The Prewitt filter is commonly used to find
656 high-frequency components in the signal, such as for example, the edges in an image.
657
658 Parameters
659 ----------
660 dim_shape: NDArrayShape
661 Shape of the input array.
662 axis: int, tuple
663 Compute the edge filter along this axis. If not provided, the edge magnitude is computed. This is defined as:
664
665 ``np.sqrt(sum([prewitt(array, axis=i)**2 for i in range(array.ndim)]) / array.ndim)`` The magnitude is also
666 computed if axis is a sequence.
667
668 mode: str, list[str]
669 Boundary conditions.
670 Multiple forms are accepted:
671
672 * str: unique mode shared amongst dimensions.
673 Must be one of:
674
675 * 'constant' (default): zero-padding
676 * 'wrap'
677 * 'reflect'
678 * 'symmetric'
679 * 'edge'
680 * tuple[str, ...]: the `d`-th dimension uses `mode[d]` as boundary condition.
681
682 (See :py:func:`numpy.pad` for details.)
683 sampling: Real, list[Real]
684 Sampling step (i.e. distance between two consecutive elements of an array).
685 Defaults to 1.
686 gpu: bool
687 Input NDArray type (`True` for GPU, `False` for CPU). Defaults to `False`.
688 dtype: DType
689 Working precision of the linear operator.
690
691 Returns
692 -------
693 op: OpT
694 Prewitt
695
696 Example
697 -------
698
699 .. plot::
700
701 import matplotlib.pyplot as plt
702 import numpy as np
703 from pyxu.operator import Prewitt
704
705 dim_shape = (11, 11)
706 image = np.zeros(dim_shape)
707 image[5, 5] = 1.
708
709 prewitt = Prewitt(dim_shape)
710 out = prewitt(image)
711 plt.figure(figsize=(10, 5))
712 plt.subplot(121)
713 plt.imshow(image)
714 plt.colorbar()
715 plt.subplot(122)
716 plt.imshow(out)
717 plt.colorbar()
718
719 See Also
720 --------
721 :py:class:`~pyxu.operator.Sobel`,
722 :py:class:`~pyxu.operator.Scharr`,
723 """
724 smooth_kernel = np.full((3,), 1 / 3)
725 return _EdgeFilter(
726 dim_shape=dim_shape,
727 smooth_kernel=smooth_kernel,
728 filter_name="Prewitt",
729 axis=axis,
730 mode=mode,
731 sampling=sampling,
732 gpu=gpu,
733 dtype=dtype,
734 )
735
736
[docs]
737def Scharr(
738 dim_shape: pxt.NDArrayShape,
739 axis: typ.Optional[typ.Tuple] = None,
740 mode: ModeSpec = "constant",
741 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
742 gpu: bool = False,
743 dtype: typ.Optional[pxt.DType] = None,
744):
745 r"""
746 Multi-dimensional Scharr filter.
747
748 Notes
749 -----
750
751 This operator uses the applies the multi-dimensional Scharr filter to a :math:`D`-dimensional NDArray
752 :math:`\mathbf{x} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` using separable kernels for improved
753 performance. The Scharr filter applies the following edge filter in the dimensions of interest: ``[1, 0, -1]``, and
754 the smoothing filter on the rest of dimensions: ``[3, 10, 3] / 16``. The Scharr filter is commonly used to find
755 high-frequency components in the signal, such as for example, the edges in an image.
756
757 Parameters
758 ----------
759 dim_shape: NDArrayShape
760 Shape of the input array.
761 axis: int, tuple
762 Compute the edge filter along this axis. If not provided, the edge magnitude is computed. This is defined as:
763
764 ``np.sqrt(sum([scharr(array, axis=i)**2 for i in range(array.ndim)]) / array.ndim)`` The magnitude is also
765 computed if axis is a sequence.
766 mode: str, list[str]
767 Boundary conditions.
768 Multiple forms are accepted:
769
770 * str: unique mode shared amongst dimensions.
771 Must be one of:
772
773 * 'constant' (default): zero-padding
774 * 'wrap'
775 * 'reflect'
776 * 'symmetric'
777 * 'edge'
778 * tuple[str, ...]: the `d`-th dimension uses `mode[d]` as boundary condition.
779
780 (See :py:func:`numpy.pad` for details.)
781 sampling: Real, list[Real]
782 Sampling step (i.e. distance between two consecutive elements of an array).
783 Defaults to 1.
784 gpu: bool
785 Input NDArray type (`True` for GPU, `False` for CPU). Defaults to `False`.
786 dtype: DType
787 Working precision of the linear operator.
788
789 Returns
790 -------
791 op: OpT
792 Scharr
793
794 Example
795 -------
796
797 .. plot::
798
799 import matplotlib.pyplot as plt
800 import numpy as np
801 from pyxu.operator import Scharr
802
803 dim_shape = (11, 11)
804 image = np.zeros(dim_shape)
805 image[5, 5] = 1.
806
807 scharr = Scharr(dim_shape)
808 out = scharr(image)
809 plt.figure(figsize=(10, 5))
810 plt.subplot(121)
811 plt.imshow(image)
812 plt.colorbar()
813 plt.subplot(122)
814 plt.imshow(out)
815 plt.colorbar()
816
817 See Also
818 --------
819 :py:class:`~pyxu.operator.Sobel`,
820 :py:class:`~pyxu.operator.Prewitt`,
821 """
822 smooth_kernel = np.array([3, 10, 3]) / 16
823 return _EdgeFilter(
824 dim_shape=dim_shape,
825 smooth_kernel=smooth_kernel,
826 filter_name="Scharr",
827 axis=axis,
828 mode=mode,
829 sampling=sampling,
830 gpu=gpu,
831 dtype=dtype,
832 )
833
834
835def _EdgeFilter(
836 dim_shape: pxt.NDArrayShape,
837 smooth_kernel: KernelSpec,
838 filter_name: str,
839 axis: typ.Optional[typ.Tuple] = None,
840 mode: ModeSpec = "constant",
841 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
842 gpu: bool = False,
843 dtype: typ.Optional[pxt.DType] = None,
844):
845 from pyxu.operator import Sqrt, Square
846
847 square = Square(dim_shape=dim_shape)
848 sqrt = Sqrt(dim_shape=dim_shape)
849
850 ndim, dtype, xp = _sanitize_inputs(dim_shape, dtype, gpu)
851 sampling = _to_canonical_form(sampling, dim_shape)
852
853 axes = _get_axes(axis, ndim)
854
855 return_magnitude = len(axes) > 1
856
857 op_list = []
858 for edge_dim in axes:
859 kernel = [xp.array(1, dtype=dtype)] * len(dim_shape)
860 center = np.ones(len(dim_shape), dtype=int)
861 # We define the kernel reversed compared to Scipy or Skimage because we use correlation instead of convolution
862 kernel[edge_dim] = xp.array([-1, 0, 1], dtype=dtype) / sampling[edge_dim]
863 smooth_axes = list(set(range(ndim)) - {edge_dim})
864 for smooth_dim in smooth_axes:
865 kernel[smooth_dim] = xp.asarray(smooth_kernel, dtype=dtype) / sampling[smooth_dim]
866
867 if return_magnitude:
868 op_list.append(square * pxls.Stencil(dim_shape=dim_shape, kernel=kernel, center=center, mode=mode))
869 else:
870 op_list.append(pxls.Stencil(dim_shape=dim_shape, kernel=kernel, center=center, mode=mode))
871
872 op = functools.reduce(lambda x, y: x + y, op_list)
873 if return_magnitude:
874 op = (1 / np.sqrt(ndim)) * (sqrt * op)
875
876 op._name = filter_name
877 return op
878
879
[docs]
880class StructureTensor(pxa.DiffMap):
881 r"""
882 Structure tensor operator.
883
884 Notes
885 -----
886 The Structure Tensor, also known as the second-order moment tensor or the inertia tensor, is a matrix derived from
887 the gradient of a function. It describes the distribution of the gradient (i.e., its prominent directions) in a
888 specified neighbourhood around a point, and the degree to which those directions are coherent. The structure tensor
889 of a :math:`D`-dimensional signal :math:`\mathbf{f} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` can be
890 written as:
891
892 .. math::
893
894 \mathbf{S}_\sigma \mathbf{f} = \mathbf{g}_{\sigma} * \nabla\mathbf{f} (\nabla\mathbf{f})^{\top} = \mathbf{g}_{\sigma} *
895 \begin{bmatrix}
896 \left( \dfrac{ \partial\mathbf{f} }{ \partial x_{0} } \right)^2 & \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{0}\,\partial x_{1} } & \cdots & \dfrac{ \partial\mathbf{f} }{ \partial x_{0} } \dfrac{ \partial\mathbf{f} }{ \partial x_{D-1} } \\
897 \dfrac{ \partial\mathbf{f} }{ \partial x_{1} } \dfrac{ \partial\mathbf{f} }{ \partial x_{0} } & \left( \dfrac{ \partial\mathbf{f} }{ \partial x_{1} }\right)^2 & \cdots & \dfrac{ \partial\mathbf{f} }{ \partial x_{1} } \dfrac{ \partial\mathbf{f} }{ \partial x_{D-1} } \\
898 \vdots & \vdots & \ddots & \vdots \\
899 \dfrac{ \partial\mathbf{f} }{ \partial x_{D-1} } \dfrac{ \partial\mathbf{f} }{ \partial x_{0} } & \dfrac{ \partial\mathbf{f} }{ \partial x_{D-1} } \dfrac{ \partial\mathbf{f} }{ \partial x_{1} } & \cdots & \left( \dfrac{ \partial\mathbf{f} }{ \partial x_{D-1}} \right)^2
900 \end{bmatrix},
901
902 where :math:`\mathbf{g}_{\sigma} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` is a discrete Gaussian filter
903 with standard variation :math:`\sigma` with which a convolution is performed elementwise.
904
905 However, due to the symmetry of the structure tensor, only the upper triangular part is computed in practice:
906
907 .. math::
908
909 \mathbf{H}_{\mathbf{v}_1, \ldots ,\mathbf{v}_m} \mathbf{f} = \mathbf{g}_{\sigma} * \begin{bmatrix}
910 \left( \dfrac{ \partial\mathbf{f} }{ \partial x_{0} } \right)^2 \\
911 \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{0}\,\partial x_{1} } \\
912 \vdots \\
913 \left( \dfrac{ \partial\mathbf{f} }{ \partial x_{D-1}} \right)^2
914 \end{bmatrix} \mathbf{f} \in \mathbb{R}^{\frac{D (D-1)}{2} \times N_0 \times \cdots \times N_{D-1}}
915
916 Remark
917 ------
918 In case of using the finite differences (`diff_type="fd"`), the finite difference scheme defaults to `central` (see
919 :py:class:`~pyxu.operator.PartialDerivative`).
920
921 Example
922 -------
923
924 .. plot::
925
926 import numpy as np
927 import matplotlib.pyplot as plt
928 from pyxu.operator import StructureTensor
929 from pyxu.util.misc import peaks
930
931 # Define input image
932 n = 1000
933 x = np.linspace(-3, 3, n)
934 xx, yy = np.meshgrid(x, x)
935 image = peaks(xx, yy)
936 nsamples = 2
937 dim_shape = image.shape # (1000, 1000)
938 images = np.tile(image, (nsamples, 1, 1))
939 # Instantiate structure tensor operator
940 structure_tensor = StructureTensor(dim_shape=dim_shape)
941
942 outputs = structure_tensor(images)
943 print(outputs.shape) # (2, 3, 1000, 1000)
944 # Plot
945 plt.figure()
946 plt.imshow(images[0])
947 plt.colorbar()
948 plt.title("Image")
949 plt.axis("off")
950
951 plt.figure()
952 plt.imshow(outputs[0][0])
953 plt.colorbar()
954 plt.title(r"$\hat{S}_{xx}$")
955 plt.axis("off")
956
957 plt.figure()
958 plt.imshow(outputs[0][1])
959 plt.colorbar()
960 plt.title(r"$\hat{S}_{xy}$")
961 plt.axis("off")
962
963 plt.figure()
964 plt.imshow(outputs[0][2])
965 plt.colorbar()
966 plt.title(r"$\hat{S}_{yy}$")
967 plt.axis("off")
968
969 See Also
970 --------
971 :py:class:`~pyxu.operator.PartialDerivative`,
972 :py:class:`~pyxu.operator.Gradient`,
973 :py:class:`~pyxu.operator.Hessian`
974 """
975
976 def __init__(
977 self,
978 dim_shape: pxt.NDArrayShape,
979 diff_method="fd",
980 smooth_sigma: typ.Union[pxt.Real, tuple[pxt.Real, ...]] = 1.0,
981 smooth_truncate: typ.Union[pxt.Real, tuple[pxt.Real, ...]] = 3.0,
982 mode: ModeSpec = "constant",
983 sampling: typ.Union[pxt.Real, tuple[pxt.Real, ...]] = 1,
984 gpu: bool = False,
985 dtype: typ.Optional[pxt.DType] = None,
986 parallel: bool = False,
987 **diff_kwargs,
988 ):
989 ndim = len(dim_shape)
990 ntriu = (ndim * (ndim + 1)) // 2
991 super().__init__(dim_shape=dim_shape, codim_shape=(ntriu,) + dim_shape)
992 self.directions = tuple(
993 list(_) for _ in itertools.combinations_with_replacement(np.arange(len(dim_shape)).astype(int), 2)
994 )
995
996 if diff_method == "fd":
997 diff_kwargs.update({"scheme": diff_kwargs.pop("scheme", "central")})
998
999 self.grad = pxld.Gradient(
1000 dim_shape=dim_shape,
1001 directions=None,
1002 diff_method=diff_method,
1003 mode=mode,
1004 gpu=gpu,
1005 dtype=dtype,
1006 sampling=sampling,
1007 parallel=parallel,
1008 **diff_kwargs,
1009 )
1010
1011 if smooth_sigma:
1012 self.smooth = GaussianFilter(
1013 dim_shape=dim_shape,
1014 sigma=smooth_sigma,
1015 truncate=smooth_truncate,
1016 order=0,
1017 mode=mode,
1018 sampling=sampling,
1019 gpu=gpu,
1020 dtype=dtype,
1021 )
1022 else:
1023 self.smooth = pxlb.IdentityOp(dim_shape=dim_shape)
1024
[docs]
1025 def apply(self, arr):
1026 xp = pxu.get_array_module(arr)
1027 sh = arr.shape[: -self.dim_rank]
1028 grad = self.grad(arr)
1029
1030 def slice_(ax):
1031 return (slice(None),) * len(sh) + (slice(ax, ax + 1),)
1032
1033 return xp.concatenate(
1034 [self.smooth((grad[slice_(i)] * grad[slice_(j)])) for i, j in self.directions],
1035 axis=len(sh),
1036 ).reshape(*sh, len(self.directions), *self.dim_shape)