#!/usr/bin/env python # coding: utf-8 # This notebook is part of the `kikuchipy` documentation https://kikuchipy.org. # Links to the documentation won't work from the notebook. # # Pattern matching # # Crystal orientations can be determined from experimental EBSD patterns by # matching them to a dictionary of simulated patterns of known orientations # Chen et al. (2015), # Nolze et al. (2016), # Foden et al. (2019). # # Here, we will demonstrate pattern matching using a Ni data set of 4125 EBSD # patterns and a dynamically simulated master pattern from EMsoft, both of low # resolution and found in the [kikuchipy.data](reference.rst#data) module. # #
# # Note # # The generated pattern dictionary is discrete, but no refinement of the best # matching orientation is provided. The need for the latter is discussed in e.g. # Singh et al. (2017). # #
# # Before we can generate a dictionary of # simulated patterns, we need a master pattern containing all possible scattering # vectors for a candidate phase. This can simulated done using EMsoft # Callahan and De Graef (2013) # Jackson et al. (2014), and then read # into kikuchipy. # First, we import libraries and load the small experimental Nickel test data. # In[ ]: # exchange inline for qt5 for interactive plotting from the pyqt package get_ipython().run_line_magic('matplotlib', 'inline') import tempfile import matplotlib.pyplot as plt plt.rcParams["font.size"] = 15 import hyperspy.api as hs import numpy as np from orix import sampling, plot, io import kikuchipy as kp # Use kp.load("data.h5") to load your own data s = kp.data.nickel_ebsd_large(allow_download=True) # External download s # To obtain a good match, we must increase the signal-to-noise ratio. In this # pattern matching analysis, the Kikuchi bands are considered the signal, and the # angle-dependent backscatter intensity, along with unwanted detector effects, # are considered to be noise. See the # [pattern processing guide](pattern_processing.rst) for further details. # In[ ]: s.remove_static_background() s.remove_dynamic_background() # Next, we load a dynamically simulated Nickel master pattern generated with # EMsoft, in the northern hemisphere projection of the square Lambert projection # for an accelerating voltage of 20 keV. # In[ ]: mp = kp.data.nickel_ebsd_master_pattern_small(projection="lambert", energy=20) mp # In[ ]: mp.plot() # The Nickel phase information, specifically the crystal symmetry, asymmetric atom # positions, and crystal lattice, is conveniently stored in an # [orix.crystal_map.Phase](https://orix.readthedocs.io/en/stable/reference.html#orix.crystal_map.phase_list.Phase). # In[ ]: ni = mp.phase ni # In[ ]: ni.structure # Element, x, y, z, site occupation # In[ ]: ni.structure.lattice # nm and degrees # If we don't know anything about the possible crystal (unit cell) orientations in # our sample, the safest thing to do is to generate a dictionary of orientations # uniformly distributed in a candidate phase's orientation space. To achieve this, # we sample the Rodrigues Fundamental Zone of the proper point group *432* with a # 4$^{\circ}$ characteristic distance between orientations (we can either pass # in the proper point group, or the space group, which is a subgroup of the proper # point group) using # [orix.sampling.get_sample_fundamental()](https://orix.readthedocs.io/en/stable/reference.html#orix.sampling.sample_generators.get_sample_fundamental). # In[ ]: r = sampling.get_sample_fundamental( resolution=4, space_group=ni.space_group.number ) r # This sampling resulted in 14423 crystal orientations. # #
# # Note # # A characteristic distance of 4$^{\circ}$ results in a course sampling of # orientation space; a shorter distance should be used for real experimental work. # #
# Now that we have our master pattern and crystal orientations, we need to # describe the EBSD detector's position with respect to the sample (interaction # volume). This ensures that projecting parts of the master pattern onto our # detector yields dynamically simulated patterns resembling our experimental ones. # See the [reference frames](reference_frames.rst) user guide and the # [EBSDDetector](reference.rst#ebsddetector) class for further details. # In[ ]: detector = kp.detectors.EBSDDetector( shape=s.axes_manager.signal_shape[::-1], pc=[0.421, 0.7794, 0.5049], sample_tilt=70, convention="tsl", ) detector # Let's double check the projection/pattern center (PC) position on the detector # using # [plot()](reference.rst#kikuchipy.detectors.ebsd_detector.EBSDDetector.plot). # In[ ]: detector.plot(coordinates="gnomonic", pattern=s.inav[0, 0].data) # Now we're ready to generate our dictionary of simulated patterns by projecting # parts of the master pattern onto our detector for all sampled orientations, # using the # [get_patterns()](reference.rst#kikuchipy.signals.ebsdmasterpattern.get_patterns) # method. The method assumes the crystal orientations are represented with respect # to the EDAX TSL sample reference frame RD-TD-ND. # In[ ]: sim = mp.get_patterns( rotations=r, detector=detector, energy=20, dtype_out=s.data.dtype, compute=True ) sim # Let's inspect the three first of the 14423 simulated patterns. # In[ ]: #sim.plot() # Plot the patterns with a navigator for easy inspection fig, ax = plt.subplots(ncols=3, figsize=(18, 6)) for i in range(3): ax[i].imshow(sim.inav[i].data, cmap="gray") euler = np.rad2deg(sim.xmap[i].rotations.to_euler())[0] ax[i].set_title( f"($\phi_1, \Phi, \phi_2)$ = {np.array_str(euler, precision=1)}" ) ax[i].axis("off") fig.tight_layout() # Finally, let's use the # [match_patterns()](reference.rst#kikuchipy.signals.EBSD.match_patterns) method # to match the simulated patterns to our nine experimental patterns, using the # [zero-mean normalized cross correlation (NCC)](reference.rst#kikuchipy.indexing.similarity_metrics.ncc) # coefficient $r$ # Gonzalez & Woods (2017), which is # the default similarity metric. Let's keep the 10 best matching orientations. A # number of 4125 * 14423 comparisons is quite small, which we can do in memory all # at once. However, in cases where the number of comparisons are too big for our # memory to handle, we can slice our simulated pattern data into $n$ slices. To # demonstrate this, we use 10 slices here. The results are returned as a # [orix.crystal_map.CrystalMap](https://orix.readthedocs.io/en/latest/reference.html#crystalmap). # In[ ]: xmap = s.match_patterns(sim, n_slices=10, keep_n=10) xmap # The results can be exported to an HDF5 file re-readable by orix. # In[ ]: temp_dir = tempfile.mkdtemp() xmap_file = temp_dir + "ni.h5" io.save(xmap_file, xmap) # Let's inspect our matching results by plotting a map of the highest $r$ # (stored in the `scores` property). # In[ ]: fig, ax = plt.subplots(subplot_kw=dict(projection="plot_map")) ax.plot_map(xmap, xmap.scores[:, 0], scalebar=False) ax.add_colorbar(label=r"$r$") _ = ax.axis("off") # We can use the crystal map property `simulation_indices` to get the best # matching simulated patterns from the dictionary of simulated patterns. # In[ ]: best_patterns = sim.data[xmap.simulation_indices[:, 0]].reshape(s.data.shape) s_best = kp.signals.EBSD(best_patterns) s_best # The simplest way to visually compare the experimental and best matching # simulated patterns are to # [plot them in the same navigator](visualizing_patterns.ipynb#plot-multiple-signals). # Here, we use the highest $r$ as a navigator. When using an interactive backend # like `Qt5Agg`, we can then move the red square around to look at the patterns in # each point. # In[ ]: ncc_navigator = hs.signals.Signal2D(xmap.get_map_data(xmap.scores[:, 0])) hs.plot.plot_signals([s, s_best], navigator=hs.signals.Signal2D(ncc_navigator)) # Let's also plot the best matches for patterns from two grains # In[ ]: grain1 = (0, 0) grain2 = (30, 10) fig, ax = plt.subplots(ncols=2, nrows=2, figsize=(10, 10)) ax[0, 0].imshow(s.inav[grain1].data, cmap="gray") ax[0, 0].axis("off") ax[0, 1].imshow(s_best.inav[grain1].data, cmap="gray") ax[0, 1].axis("off") ax[1, 0].imshow(s.inav[grain2].data, cmap="gray") ax[1, 0].axis("off") ax[1, 1].imshow(s_best.inav[grain2].data, cmap="gray") ax[1, 1].axis("off") fig.tight_layout(h_pad=0.5, w_pad=1) # In[ ]: import os os.rmdir(temp_dir)