/
ebsd.py
3369 lines (3007 loc) · 128 KB
/
ebsd.py
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2019-2023 The kikuchipy developers
#
# This file is part of kikuchipy.
#
# kikuchipy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# kikuchipy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with kikuchipy. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import copy
import datetime
import gc
import logging
import os
from typing import Union, List, Optional, Tuple, Iterable
import warnings
import dask
import dask.array as da
from dask.diagnostics import ProgressBar
import hyperspy.api as hs
from hyperspy.axes import AxesManager
from hyperspy.signals import Signal2D
from hyperspy.learn.mva import LearningResults
from hyperspy.roi import BaseInteractiveROI
from h5py import File
import numpy as np
from orix.crystal_map import CrystalMap, PhaseList
from orix.quaternion import Rotation
from scipy.ndimage import correlate, gaussian_filter
from skimage.util.dtype import dtype_range
from kikuchipy import _pyebsdindex_installed
from kikuchipy.detectors import EBSDDetector
from kikuchipy.filters.fft_barnes import _fft_filter, _fft_filter_setup
from kikuchipy.filters.window import Window
from kikuchipy.indexing._dictionary_indexing import _dictionary_indexing
from kikuchipy.indexing._hough_indexing import (
_get_pyebsdindex_phaselist,
_indexer_is_compatible_with_kikuchipy,
_hough_indexing,
_optimize_pc,
)
from kikuchipy.indexing._refinement._refinement import (
_refine_orientation,
_refine_orientation_pc,
_refine_pc,
)
from kikuchipy.indexing.similarity_metrics import (
SimilarityMetric,
NormalizedCrossCorrelationMetric,
NormalizedDotProductMetric,
)
from kikuchipy.io._io import _save
from kikuchipy.pattern import chunk
from kikuchipy.pattern.chunk import _average_neighbour_patterns
from kikuchipy.pattern._pattern import (
fft_frequency_vectors,
fft_filter,
_downsample2d,
_dynamic_background_frequency_space_setup,
_get_image_quality,
_remove_static_background_subtract,
_remove_static_background_divide,
_remove_dynamic_background,
)
from kikuchipy.signals.util.array_tools import grid_indices
from kikuchipy.signals.util._dask import (
get_dask_array,
get_chunking,
_get_chunk_overlap_depth,
_rechunk_learning_results,
_update_learning_results,
)
from kikuchipy.signals.util._detector import _detector_is_compatible_with_signal
from kikuchipy.signals.util._crystal_map import (
_get_points_in_data_in_xmap,
_equal_phase,
_xmap_is_compatible_with_signal,
)
from kikuchipy.signals.util._map_helper import (
_get_neighbour_dot_product_matrices,
_get_average_dot_product_map,
)
from kikuchipy.signals.util._overwrite_hyperspy_methods import (
get_parameters,
insert_doc_disclaimer,
)
from kikuchipy.signals._kikuchipy_signal import KikuchipySignal2D, LazyKikuchipySignal2D
from kikuchipy.signals.virtual_bse_image import VirtualBSEImage
_logger = logging.getLogger(__name__)
class EBSD(KikuchipySignal2D):
"""Scan of Electron Backscatter Diffraction (EBSD) patterns.
This class extends HyperSpy's Signal2D class for EBSD patterns. Some
of the docstrings are obtained from HyperSpy. See the docstring of
:class:`~hyperspy._signals.signal2d.Signal2D` for the list of
inherited attributes and methods.
Parameters
----------
*args
See :class:`~hyperspy._signals.signal2d.Signal2D`.
detector : EBSDDetector, optional
Detector describing the EBSD detector-sample geometry. If not
given, this is a default detector (see :class:`EBSDDetector`).
static_background : ~numpy.ndarray or ~dask.array.Array, optional
Static background pattern. If not given, this is ``None``.
xmap : ~orix.crystal_map.CrystalMap
Crystal map containing the phases, unit cell rotations and
auxiliary properties of the EBSD dataset. If not given, this is
``None``.
**kwargs
See :class:`~hyperspy._signals.signal2d.Signal2D`.
See Also
--------
kikuchipy.data.nickel_ebsd_small :
An EBSD signal with ``(3, 3)`` experimental nickel patterns.
kikuchipy.data.nickel_ebsd_large :
An EBSD signal with ``(55, 75)`` experimental nickel patterns.
kikuchipy.data.silicon_ebsd_moving_screen_in :
An EBSD signal with one experimental silicon pattern.
kikuchipy.data.silicon_ebsd_moving_screen_out5mm :
An EBSD signal with one experimental silicon pattern.
kikuchipy.data.silicon_ebsd_moving_screen_out10mm :
An EBSD signal with one experimental silicon pattern.
Examples
--------
Load one of the example datasets and inspect some properties
>>> import kikuchipy as kp
>>> s = kp.data.nickel_ebsd_small()
>>> s
<EBSD, title: patterns Scan 1, dimensions: (3, 3|60, 60)>
>>> s.detector
EBSDDetector (60, 60), px_size 1 um, binning 8, tilt 0, azimuthal 0, pc (0.425, 0.213, 0.501)
>>> s.static_background
array([[84, 87, 90, ..., 27, 29, 30],
[87, 90, 93, ..., 27, 28, 30],
[92, 94, 97, ..., 39, 28, 29],
...,
[80, 82, 84, ..., 36, 30, 26],
[79, 80, 82, ..., 28, 26, 26],
[76, 78, 80, ..., 26, 26, 25]], dtype=uint8)
>>> s.xmap
Phase Orientations Name Space group Point group Proper point group Color
0 9 (100.0%) ni Fm-3m m-3m 432 tab:blue
Properties: scores
Scan unit: px
"""
_signal_type = "EBSD"
_alias_signal_types = ["electron_backscatter_diffraction"]
_custom_attributes = ["detector", "static_background", "xmap"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._detector = kwargs.get(
"detector",
EBSDDetector(
shape=self._signal_shape_rc,
px_size=self.axes_manager.signal_axes[0].scale,
),
)
self._static_background = kwargs.get("static_background")
self._xmap = kwargs.get("xmap")
# ---------------------- Custom attributes ----------------------- #
@property
def detector(self) -> EBSDDetector:
"""Return or set the detector describing the EBSD
detector-sample geometry.
Parameters
----------
value : EBSDDetector
EBSD detector.
"""
return self._detector
@detector.setter
def detector(self, value: EBSDDetector):
if _detector_is_compatible_with_signal(
detector=value,
nav_shape=self._navigation_shape_rc,
sig_shape=self._signal_shape_rc,
raise_if_not=True,
):
self._detector = value
@property
def xmap(self) -> Union[CrystalMap, None]:
"""Return or set the crystal map containing the phases, unit
cell rotations and auxiliary properties of the EBSD dataset.
Parameters
----------
value : ~orix.crystal_map.CrystalMap
Crystal map with the same shape as the signal navigation
shape.
"""
return self._xmap
@xmap.setter
def xmap(self, value: CrystalMap):
if _xmap_is_compatible_with_signal(
value, self.axes_manager.navigation_axes[::-1], raise_if_not=True
):
self._xmap = value
@property
def static_background(self) -> Union[np.ndarray, da.Array, None]:
"""Return or set the static background pattern.
Parameters
----------
value : ~numpy.ndarray or ~dask.array.Array
Static background pattern with the same (signal) shape and
data type as the EBSD signal.
"""
return self._static_background
@static_background.setter
def static_background(self, value: Union[np.ndarray, da.Array]):
if value.dtype != self.data.dtype:
warnings.warn("Background pattern has different data type from patterns")
if value.shape != self._signal_shape_rc:
warnings.warn("Background pattern has different shape from patterns")
self._static_background = value
# ------------------------ Custom methods ------------------------ #
def extract_grid(
self, grid_shape: Union[Tuple[int, int], int], return_indices: bool = False
) -> Union[Union[EBSD, LazyEBSD], Tuple[Union[EBSD, LazyEBSD], np.ndarray]]:
"""Return a new signal with patterns from positions in a grid of
shape ``grid_shape`` evenly spaced in navigation space.
Parameters
----------
grid_shape
Tuple of integers or just an integer signifying the number
of grid indices in each dimension. If 2D, the shape is
(n columns, n rows).
return_indices
Whether to return the indices of the extracted patterns into
:attr:`data` as an array of shape ``(2,) + grid_shape``.
Default is ``False``.
Returns
-------
new
New signal with patterns from indices in a grid
corresponding to ``grid_shape``. Attributes :attr:`xmap`,
:attr:`static_background` and :attr:`detector` are deep
copied.
indices
Indices of the extracted patterns into :attr:`data`,
returned if ``return_indices=True``.
Examples
--------
>>> import kikuchipy as kp
>>> s = kp.data.nickel_ebsd_large(lazy=True)
>>> s
<LazyEBSD, title: patterns Scan 1, dimensions: (75, 55|60, 60)>
>>> s2 = s.extract_grid((5, 4))
>>> s2
<LazyEBSD, title: patterns Scan 1, dimensions: (5, 4|60, 60)>
"""
if isinstance(grid_shape, int):
grid_shape = (grid_shape,)
nav_shape = self.axes_manager.navigation_shape
if len(grid_shape) != len(nav_shape) or any(
[g > n for g, n in zip(grid_shape, nav_shape)]
):
raise ValueError(
f"grid_shape {grid_shape} must be compatible with navigation shape "
f"{nav_shape}"
)
# NumPy order (rows, columns)
grid_shape = grid_shape[::-1]
nav_shape = nav_shape[::-1]
idx, spacing = grid_indices(grid_shape, nav_shape, return_spacing=True)
idx_tuple = tuple(idx)
# Data
if self._lazy:
data_new = self.data.vindex[idx_tuple]
else:
data_new = self.data[idx_tuple]
# Crystal map
if self.xmap is not None:
mask = np.zeros(nav_shape, dtype=bool)
mask[idx_tuple] = True
mask = mask.ravel()
xmap_new = self.xmap[mask].deepcopy()
else:
xmap_new = None
# EBSD detector
detector_new = self.detector.deepcopy()
if detector_new.navigation_shape == nav_shape:
detector_new.pc = detector_new.pc[idx_tuple]
elif detector_new.navigation_shape != (1,):
detector_new.pc = [0.5, 0.5, 0.5]
# Static background
bg_new = self.static_background
if bg_new is not None:
bg_new = bg_new.copy()
# Axes manager
am = self.axes_manager.deepcopy()
nav_idx = am.navigation_indices_in_array
for i, size, spacing_i in zip(nav_idx, grid_shape, spacing):
am[i].size = size
am[i].scale = spacing_i * am[i].scale
am_list = [a for a in am.as_dictionary().values()]
scan_dict = {
"data": data_new,
"xmap": xmap_new,
"detector": detector_new,
"static_background": bg_new,
"axes": am_list,
"metadata": self.metadata.as_dictionary(),
"original_metadata": self.original_metadata.as_dictionary(),
}
if self._lazy:
new = LazyEBSD(**scan_dict)
else:
new = EBSD(**scan_dict)
out = new
if return_indices:
out = (out, idx)
return out
def set_scan_calibration(
self, step_x: Union[int, float] = 1.0, step_y: Union[int, float] = 1.0
) -> None:
"""Set the step size in microns.
Parameters
----------
step_x
Scan step size in um per pixel in horizontal direction.
step_y
Scan step size in um per pixel in vertical direction.
See Also
--------
set_detector_calibration
Examples
--------
>>> import kikuchipy as kp
>>> s = kp.data.nickel_ebsd_small()
>>> s.axes_manager['x'].scale
1.5
>>> s.set_scan_calibration(step_x=2) # Microns
>>> s.axes_manager['x'].scale
2.0
"""
x, y = self.axes_manager.navigation_axes
x.name, y.name = ("x", "y")
x.scale, y.scale = (step_x, step_y)
x.units, y.units = ["um"] * 2
def set_detector_calibration(self, delta: Union[int, float]) -> None:
"""Set detector pixel size in microns. The offset is set to the
the detector center.
Parameters
----------
delta
Detector pixel size in microns.
See Also
--------
set_scan_calibration
Examples
--------
>>> import kikuchipy as kp
>>> s = kp.data.nickel_ebsd_small()
>>> s.axes_manager['dx'].scale # Default value
1.0
>>> s.set_detector_calibration(delta=70.)
>>> s.axes_manager['dx'].scale
70.0
"""
center = delta * np.array(self.axes_manager.signal_shape) / 2
dx, dy = self.axes_manager.signal_axes
dx.units, dy.units = ["um"] * 2
dx.scale, dy.scale = (delta, delta)
dx.offset, dy.offset = -center
def remove_static_background(
self,
operation: str = "subtract",
static_bg: Union[np.ndarray, da.Array, None] = None,
scale_bg: bool = False,
show_progressbar: Optional[bool] = None,
inplace: bool = True,
lazy_output: Optional[bool] = None,
) -> Union[None, EBSD, LazyEBSD]:
"""Remove the static background.
The removal is performed by subtracting or dividing by a static
background pattern. Resulting pattern intensities are rescaled
loosing relative intensities and stretched to fill the available
grey levels in the patterns' data type range.
Parameters
----------
operation
Whether to ``"subtract"`` (default) or ``"divide"`` by the
static background pattern.
static_bg
Static background pattern. If not given, the background is
obtained from the ``EBSD.static_background`` property.
scale_bg
Whether to scale the static background pattern to each
individual pattern's data range before removal. Default is
``False``.
show_progressbar
Whether to show a progressbar. If not given, the value of
:obj:`hyperspy.api.preferences.General.show_progressbar`
is used.
inplace
Whether to operate on the current signal or return a new
one. Default is ``True``.
lazy_output
Whether the returned signal is lazy. If not given this
follows from the current signal. Can only be ``True`` if
``inplace=False``.
Returns
-------
s_out
Background corrected signal, returned if ``inplace=False``.
Whether it is lazy is determined from ``lazy_output``.
See Also
--------
remove_dynamic_background
Examples
--------
It is assumed that a static background pattern of the same shape
and data type (e.g. 8-bit unsigned integer, ``uint8``) as the
patterns is available in signal metadata:
>>> import kikuchipy as kp
>>> s = kp.data.nickel_ebsd_small()
>>> s.static_background
array([[84, 87, 90, ..., 27, 29, 30],
[87, 90, 93, ..., 27, 28, 30],
[92, 94, 97, ..., 39, 28, 29],
...,
[80, 82, 84, ..., 36, 30, 26],
[79, 80, 82, ..., 28, 26, 26],
[76, 78, 80, ..., 26, 26, 25]], dtype=uint8)
The static background can be removed by subtracting or dividing
this background from each pattern:
>>> s.remove_static_background(operation="divide")
If the ``static_background`` property is ``None``, this must be
passed in the ``static_bg`` parameter as a ``numpy`` or ``dask``
array.
"""
if lazy_output and inplace:
raise ValueError("`lazy_output=True` requires `inplace=False`")
dtype = np.float32 # During processing
dtype_out = self.data.dtype.type
omin, omax = dtype_range[dtype_out]
# Get background pattern
if static_bg is None:
static_bg = self.static_background
try:
if not isinstance(static_bg, (np.ndarray, da.Array)):
raise ValueError
except (AttributeError, ValueError):
raise ValueError("`EBSD.static_background` is not a valid array")
if isinstance(static_bg, da.Array):
static_bg = static_bg.compute()
if dtype_out != static_bg.dtype:
raise ValueError(
f"Static background dtype_out {static_bg.dtype} is not the same as "
f"pattern dtype_out {dtype_out}"
)
pat_shape = self._signal_shape_rc # xy -> ij
bg_shape = static_bg.shape
if bg_shape != pat_shape:
raise ValueError(
f"Signal {pat_shape} and static background {bg_shape} shapes are not "
"the same"
)
static_bg = static_bg.astype(dtype)
# Remove background and rescale to input data type
if operation == "subtract":
operation_func = _remove_static_background_subtract
else:
operation_func = _remove_static_background_divide
map_kw = dict(
show_progressbar=show_progressbar,
parallel=True,
output_dtype=dtype_out,
static_bg=static_bg,
dtype_out=dtype_out,
omin=omin,
omax=omax,
scale_bg=scale_bg,
)
attrs = self._get_custom_attributes()
if inplace:
self.map(operation_func, inplace=True, **map_kw)
self._set_custom_attributes(attrs)
else:
s_out = self.map(
operation_func, inplace=False, lazy_output=lazy_output, **map_kw
)
s_out._set_custom_attributes(attrs)
return s_out
def remove_dynamic_background(
self,
operation: str = "subtract",
filter_domain: str = "frequency",
std: Union[int, float, None] = None,
truncate: Union[int, float] = 4.0,
show_progressbar: Optional[bool] = None,
inplace: bool = True,
lazy_output: Optional[bool] = None,
**kwargs,
) -> Union[None, EBSD, LazyEBSD]:
"""Remove the dynamic background.
The removal is performed by subtracting or dividing by a
Gaussian blurred version of each pattern. Resulting pattern
intensities are rescaled to fill the input patterns' data type
range individually.
Parameters
----------
operation
Whether to ``"subtract"`` (default) or ``"divide"`` by the
dynamic background pattern.
filter_domain
Whether to obtain the dynamic background by applying a
Gaussian convolution filter in the ``"frequency"`` (default)
or ``"spatial"`` domain.
std
Standard deviation of the Gaussian window. If None
(default), it is set to width/8.
truncate
Truncate the Gaussian window at this many standard
deviations. Default is ``4.0``.
show_progressbar
Whether to show a progressbar. If not given, the value of
:obj:`hyperspy.api.preferences.General.show_progressbar`
is used.
inplace
Whether to operate on the current signal or return a new
one. Default is ``True``.
lazy_output
Whether the returned signal is lazy. If not given this
follows from the current signal. Can only be ``True`` if
``inplace=False``.
**kwargs
Keyword arguments passed to the Gaussian blurring function
determined from ``filter_domain``.
Returns
-------
s_out
Background corrected signal, returned if ``inplace=False``.
Whether it is lazy is determined from ``lazy_output``.
See Also
--------
remove_static_background,
get_dynamic_background,
kikuchipy.pattern.remove_dynamic_background,
kikuchipy.pattern.get_dynamic_background
Examples
--------
Remove the static and dynamic background
>>> import kikuchipy as kp
>>> s = kp.data.nickel_ebsd_small()
>>> s.remove_static_background()
>>> s.remove_dynamic_background(operation="divide", std=5)
"""
if lazy_output and inplace:
raise ValueError("`lazy_output=True` requires `inplace=False`")
if std is None:
std = self.axes_manager.signal_shape[0] / 8
# Get filter function and set up necessary keyword arguments
if filter_domain == "frequency":
# FFT filter setup for Connelly Barnes' algorithm
filter_func = _fft_filter
(
kwargs["fft_shape"],
kwargs["window_shape"],
kwargs["transfer_function"],
kwargs["offset_before_fft"],
kwargs["offset_after_ifft"],
) = _dynamic_background_frequency_space_setup(
pattern_shape=self._signal_shape_rc,
std=std,
truncate=truncate,
)
elif filter_domain == "spatial":
filter_func = gaussian_filter
kwargs["sigma"] = std
kwargs["truncate"] = truncate
else:
filter_domains = ["frequency", "spatial"]
raise ValueError(f"{filter_domain} must be either of {filter_domains}")
map_func = _remove_dynamic_background
dtype_out = self.data.dtype.type
omin, omax = dtype_range[dtype_out]
map_kw = dict(
show_progressbar=show_progressbar,
parallel=True,
output_dtype=dtype_out,
filter_func=filter_func,
operation=operation,
dtype_out=dtype_out,
omin=omin,
omax=omax,
**kwargs,
)
attrs = self._get_custom_attributes()
if inplace:
self.map(map_func, inplace=True, **map_kw)
self._set_custom_attributes(attrs)
else:
s_out = self.map(map_func, inplace=False, lazy_output=lazy_output, **map_kw)
s_out._set_custom_attributes(attrs)
return s_out
def get_dynamic_background(
self,
filter_domain: str = "frequency",
std: Union[int, float, None] = None,
truncate: Union[int, float] = 4.0,
dtype_out: Union[str, np.dtype, type, None] = None,
show_progressbar: Optional[bool] = None,
lazy_output: Optional[bool] = None,
**kwargs,
) -> Union[EBSD, LazyEBSD]:
"""Return the dynamic background per pattern in a new signal.
Parameters
----------
filter_domain
Whether to apply a Gaussian convolution filter in the
``"frequency"`` (default) or ``"spatial"`` domain.
std
Standard deviation of the Gaussian window. If not given, it
is set to width/8.
truncate
Truncate the Gaussian filter at this many standard
deviations. Default is ``4.0``.
dtype_out
Data type of the background patterns. If not given, it is
set to the same data type as the input pattern.
show_progressbar
Whether to show a progressbar. If not given, the value of
:obj:`hyperspy.api.preferences.General.show_progressbar`
is used.
lazy_output
Whether the returned signal is lazy. If not given this
follows from the current signal.
**kwargs
Keyword arguments passed to the Gaussian blurring function
determined from ``filter_domain``.
Returns
-------
s_out
Signal with the large scale variations across the detector.
Whether it is lazy is determined from ``lazy_output``.
"""
if std is None:
std = self.axes_manager.signal_shape[-1] / 8
# Get filter function and set up necessary keyword arguments
if filter_domain == "frequency":
filter_func = _fft_filter
# FFT filter setup for Connelly Barnes' algorithm
(
kwargs["fft_shape"],
kwargs["window_shape"],
kwargs["transfer_function"],
kwargs["offset_before_fft"],
kwargs["offset_after_ifft"],
) = _dynamic_background_frequency_space_setup(
pattern_shape=self._signal_shape_rc,
std=std,
truncate=truncate,
)
elif filter_domain == "spatial":
filter_func = gaussian_filter
kwargs["sigma"] = std
kwargs["truncate"] = truncate
else:
filter_domains = ["frequency", "spatial"]
raise ValueError(f"{filter_domain} must be either of {filter_domains}")
if dtype_out is None:
dtype_out = self.data.dtype
else:
dtype_out = np.dtype(dtype_out)
dask_array = get_dask_array(self, dtype=dtype_out)
background_patterns = dask_array.map_blocks(
func=chunk.get_dynamic_background,
filter_func=filter_func,
dtype_out=dtype_out,
dtype=dtype_out,
**kwargs,
)
attrs = self._get_custom_attributes()
if lazy_output or (lazy_output is None and self._lazy):
s_out = LazyEBSD(background_patterns, **attrs)
else:
background_return = np.empty(
shape=background_patterns.shape, dtype=dtype_out
)
pbar = ProgressBar()
if show_progressbar or (
show_progressbar is None and hs.preferences.General.show_progressbar
):
pbar.register()
background_patterns.store(background_return, compute=True)
s_out = EBSD(background_return, **attrs)
try:
pbar.unregister()
except KeyError:
pass
return s_out
def fft_filter(
self,
transfer_function: Union[np.ndarray, Window],
function_domain: str,
shift: bool = False,
show_progressbar: Optional[bool] = None,
inplace: bool = True,
lazy_output: Optional[bool] = None,
) -> Union[None, EBSD, LazyEBSD]:
"""Filter patterns in the frequency domain.
Patterns are transformed via the Fast Fourier Transform (FFT) to
the frequency domain, where their spectrum is multiplied by the
``transfer_function``, and the filtered spectrum is subsequently
transformed to the spatial domain via the inverse FFT (IFFT).
Filtered patterns are rescaled to input data type range.
Note that if ``function_domain`` is ``"spatial"``, only real
valued FFT and IFFT is used.
Parameters
----------
transfer_function
Filter to apply to patterns. This can either be a transfer
function in the frequency domain of pattern shape or a
kernel in the spatial domain. What is passed is determined
from ``function_domain``.
function_domain
Options are ``"frequency"`` and ``"spatial"``, indicating,
respectively, whether the filter function passed to
``filter_function`` is a transfer function in the frequency
domain or a kernel in the spatial domain.
shift
Whether to shift the zero-frequency component to the center.
Default is ``False``. This is only used when
``function_domain="frequency"``.
show_progressbar
Whether to show a progressbar. If not given, the value of
:obj:`hyperspy.api.preferences.General.show_progressbar`
is used.
inplace
Whether to operate on the current signal or return a new
one. Default is ``True``.
lazy_output
Whether the returned signal is lazy. If not given this
follows from the current signal. Can only be ``True`` if
``inplace=False``.
Returns
-------
s_out
Filtered signal, returned if ``inplace=False``. Whether it
is lazy is determined from ``lazy_output``.
See Also
--------
kikuchipy.filters.Window
Examples
--------
Applying a Gaussian low pass filter with a cutoff frequency of
20:
>>> import kikuchipy as kp
>>> s = kp.data.nickel_ebsd_small()
>>> pattern_shape = s.axes_manager.signal_shape[::-1]
>>> w = kp.filters.Window(
... "lowpass", cutoff=20, shape=pattern_shape
... )
>>> s.fft_filter(
... transfer_function=w,
... function_domain="frequency",
... shift=True,
... )
"""
if lazy_output and inplace:
raise ValueError("`lazy_output=True` requires `inplace=False`")
dtype_out = self.data.dtype.type
dtype = np.float32
dask_array = get_dask_array(signal=self, dtype=dtype)
kwargs = {}
if function_domain == "frequency":
filter_func = fft_filter
kwargs["shift"] = shift
elif function_domain == "spatial":
filter_func = _fft_filter # Barnes
kwargs["window_shape"] = transfer_function.shape
# FFT filter setup
(
kwargs["fft_shape"],
transfer_function, # Padded
kwargs["offset_before_fft"],
kwargs["offset_after_ifft"],
) = _fft_filter_setup(
image_shape=self._signal_shape_rc,
window=transfer_function,
)
else:
function_domains = ["frequency", "spatial"]
raise ValueError(f"{function_domain} must be either of {function_domains}")
filtered_patterns = dask_array.map_blocks(
func=chunk.fft_filter,
filter_func=filter_func,
transfer_function=transfer_function,
dtype_out=dtype_out,
dtype=dtype_out,
**kwargs,
)
return_lazy = lazy_output or (lazy_output is None and self._lazy)
register_pbar = show_progressbar or (
show_progressbar is not None and hs.preferences.General.show_progressbar
)
if not return_lazy and register_pbar:
pbar = ProgressBar()
pbar.register()
if inplace:
if not return_lazy:
filtered_patterns.store(self.data, compute=True)
else:
self.data = filtered_patterns
s_out = None
else:
s_out = LazyEBSD(filtered_patterns, **self._get_custom_attributes())
if not return_lazy:
s_out.compute()
if not return_lazy and register_pbar:
pbar.unregister()
if s_out:
return s_out
def average_neighbour_patterns(
self,
window: Union[str, np.ndarray, da.Array, Window] = "circular",
window_shape: Tuple[int, ...] = (3, 3),
show_progressbar: Optional[bool] = None,
inplace: bool = True,
lazy_output: Optional[bool] = None,
**kwargs,
) -> Union[None, EBSD, LazyEBSD]:
"""Average patterns with its neighbours within a window.
The amount of averaging is specified by the window coefficients.
All patterns are averaged with the same window. Map borders are
extended with zeros. Resulting pattern intensities are rescaled
to fill the input patterns' data type range individually.
Averaging is accomplished by correlating the window with the
extended array of patterns using
:func:`scipy.ndimage.correlate`.
Parameters
----------
window
Name of averaging window or an array. Available types are
listed in :func:`scipy.signal.windows.get_window`, in
addition to a ``"circular"`` window (default) filled with
ones in which corner coefficients are set to zero. A window
element is considered to be in a corner if its radial
distance to the origin (window center) is shorter or equal
to the half width of the window's longest axis. A 1D or 2D
:class:`~numpy.ndarray`, :class:`~dask.array.Array` or
:class:`~kikuchipy.filters.Window` can also be passed.
window_shape
Shape of averaging window. Not used if a custom window or
:class:`~kikuchipy.filters.Window` is passed to ``window``.
This can be either 1D or 2D, and can be asymmetrical.
Default is ``(3, 3)``.
show_progressbar
Whether to show a progressbar. If not given, the value of
:obj:`hyperspy.api.preferences.General.show_progressbar`
is used.
inplace
Whether to operate on the current signal or return a new
one. Default is ``True``.
lazy_output
Whether the returned signal is lazy. If not given this
follows from the current signal. Can only be ``True`` if
``inplace=False``.
**kwargs
Keyword arguments passed to the available window type listed
in :func:`~scipy.signal.windows.get_window`. If not given,
the default values of that particular window are used.
Returns
-------
s_out
Averaged signal, returned if ``inplace=False``. Whether it
is lazy is determined from ``lazy_output``.
See Also
--------
kikuchipy.filters.Window, scipy.signal.windows.get_window,
scipy.ndimage.correlate
"""
if lazy_output and inplace:
raise ValueError("`lazy_output=True` requires `inplace=False`")
if isinstance(window, Window) and window.is_valid:
averaging_window = copy.copy(window)
else:
averaging_window = Window(window=window, shape=window_shape, **kwargs)
nav_shape = self._navigation_shape_rc
window_shape = averaging_window.shape
if window_shape in [(1,), (1, 1)]: