This notebook is part of the kikuchipy documentation Links to the documentation won't work from the notebook.

Feature maps

This section details methods for extracting information from pattern intensities, called feature maps (for lack of a better description).

Let's import the necessary libraries and a Nickel EBSD test data set:

In [ ]:
# Exchange inline for notebook or qt5 (from pyqt) for interactive plotting
%matplotlib inline

import hyperspy.api as hs
import matplotlib.pyplot as plt
import numpy as np
import kikuchipy as kp

# Use kp.load("data.h5") to load your own data
s =  # External download

Image quality

The image quality metric $Q$ presented by Krieger Lassen Lassen (1994) can be calculated for an EBSD signal with get_image_quality(), or, for a single pattern (numpy.ndarray), with get_image_quality(). Following the notation in Marquardt et al. (2017), it is given by

$$ \begin{align} Q &= 1 - \frac{J}{J_{\mathrm{res}}w_{\mathrm{tot}}},\\ J &= \sum_{h = -N/2}^{N/2} \sum_{k = -N/2}^{N/2} w(h, k) \left|\mathbf{q}\right|^2,\\ J_{\mathrm{res}} &= \frac{1}{N^2} \sum_{h = -N/2}^{N/2} \sum_{k = -N/2}^{N/2} \left|\mathbf{q}\right|^2,\\ w_{\mathrm{tot}} &= \sum_{h = -N/2}^{N/2} \sum_{k = -N/2}^{N/2} w(h, k). \end{align} $$

The function $w(h, k)$ is the Fast Fourier Transform (FFT) power spectrum of the EBSD pattern, and the vectors $\mathbf{q}$ are the frequency vectors with components $(h, k)$. The sharper the Kikuchi bands, the greater the high frequency content of the power spectrum, and thus the closer $Q$ will be to unity.

Since we want to use the image quality metric to determine how pronounced the Kikuchi bands in our patterns are, we first remove the static and dynamic background:

In [ ]:

To visualize parts of the computation, we compute the power spectrum of a pattern in the Nickel EBSD data set and the frequency vectors, shift the zero-frequency components to the centre, and plot them:

In [ ]:
p = s.inav[20, 11].data
p_fft = kp.pattern.fft(p, shift=True)
q = kp.pattern.fft_frequency_vectors(shape=p.shape)

fig, ax = plt.subplots(figsize=(22, 6), ncols=3)
title_kwargs = dict(fontsize=22, pad=15)

im0 = ax[0].imshow(p, cmap="gray")
ax[0].set_title("Pattern", **title_kwargs)
fig.colorbar(im0, ax=ax[0])

im1 = ax[1].imshow(np.log(kp.pattern.fft_spectrum(p_fft)), cmap="gray")
ax[1].set_title("Log of shifted power spectrum of FFT", **title_kwargs)
fig.colorbar(im1, ax=ax[1])

im2 = ax[2].imshow(np.fft.fftshift(q), cmap="gray")
ax[2].set_title(r"Shifted frequency vectors $q$", **title_kwargs)
_ = fig.colorbar(im2, ax=ax[2])

If we don't want the EBSD patterns to be zero-mean normalized before computing $Q$, we must pass get_image_quality(normalize=False).

Let's compute the image quality $Q$ and plot it for the entire data set:

In [ ]:
iq = s.get_image_quality(normalize=True)  # Default
In [ ]:
plt.figure(figsize=(10, 6))
plt.imshow(iq, cmap="gray")
plt.colorbar(label=r"Image quality, $Q$", pad=0.01)
_ = plt.axis("off")

If we want to use this map to navigate around in when plotting patterns, we can easily do that as explained in the visualizing patterns guide.

Average dot product

The average dot product, or normalized cross-correlation when centering each pattern's intensity about zero and normalizing the intensities to a standard deviation $\sigma$ of 1 (which is the default behaviour), between each pattern and their four nearest neighbours, can be obtained for an EBSD signal with get_average_neighbour_dot_product_map()

In [ ]:
adp = s.get_average_neighbour_dot_product_map()
In [ ]:
plt.figure(figsize=(10, 6))
plt.imshow(adp, cmap="gray")
plt.colorbar(label="Average dot product", pad=0.01)
_ = plt.axis("off")

The map displays how similar each pattern is to its neighbours. Grain boundaries, and some scratches on the sample, can be clearly seen as pixels with a lower value, signifying that they are more dissimilar to their neighbouring pixels, as the ones within grains where the neighbour pixel similarity is high.

The map above was created by averaging the dot product matrix per map point, created by calculating the dot product between each pattern and their four nearest neighbours, which can be seen in the black spots (uneven sample surface) in the left grains

In [ ]:
w1 = kp.filters.Window()

We could instead average with e.g. the eight nearest neighbours

In [ ]:
w2 = kp.filters.Window(window="rectangular", shape=(3, 3))
In [ ]:
adp2 = s.get_average_neighbour_dot_product_map(window=w2)

plt.figure(figsize=(10, 6))
plt.imshow(adp2, cmap="gray")
plt.colorbar(label="Average dot product", pad=0.01)
_ = plt.axis("off")

Note that the window coefficients must be integers.

We can also control whether pattern intensities should be centered about zero and/or whether they should be normalized prior to calculating the dot products by passing zero_mean=False and/or normalize=False. These are True by default. The data type of the output map, 32-bit floating point by default, can be set by passing e.g. dtype_out=np.float64.

We can obtain the dot product matrices per map point, that is the matrices before they are averaged, with get_neighbour_dot_product_matrices(). Let's see similar a pattern on a grain boundary in map point (x, y) = (50, 19) is to all its nearest neighbour in a (5, 5) window centered on that point

In [ ]:
w3 = kp.filters.Window("rectangular", shape=(5, 5))
dp_matrices = s.get_neighbour_dot_product_matrices(window=w3)
In [ ]:
x, y = (50, 19)
s_dp_matrices = hs.signals.Signal2D(dp_matrices)
s_dp_matrices.inav[x, y].plot()

We can see that the pattern is more similar to the patterns up to the right, while it is quite dissimilar to the patterns to the lower left. Let's visualize this more clearly, as is done e.g. in Fig. 1 by Brewick et al. (2019)

In [ ]:
y_n, x_n = w3.n_neighbours

s2 = s.inav[x - x_n : x + x_n + 1, y - y_n : y + y_n + 1].deepcopy()
s2.rescale_intensity(percentiles=(0.5, 99.5))  # Stretch the contrast a bit
s3 = s2 * s_dp_matrices.inav[x, y].T  # Signals must have same navigation shape
In [ ]:
_ = hs.plot.plot_images(
    padding=dict(wspace=0, hspace=-0.05),
    fig=plt.figure(figsize=(10, 10)),

Finally, we can pass this dot product matrix directly to get_average_neighbour_dot_product_map() via the dp_matrices parameter to obtain the average dot product map from these matrices

In [ ]:
adp3 = s.get_average_neighbour_dot_product_map(dp_matrices=dp_matrices)

Let's plot this and highlight the location of the pattern on the grain boundary above with a red circle

In [ ]:
plt.figure(figsize=(10, 6))
plt.imshow(adp3, cmap="gray")
plt.colorbar(label="Average dot product", pad=0.01)
plt.scatter(x=x, y=y, marker="o", c="r", s=50)
_ = plt.axis("off")