Radial Fourier Analysis scripting example

Sample data: Metallic glass

[1]:
%matplotlib inline
[2]:
import os

import matplotlib.pyplot as plt
from matplotlib import colors
from matplotlib import cm
import numpy as np

import libertem.api as lt
[3]:
ctx = lt.Context()
[4]:
data_base_path = os.environ.get("TESTDATA_BASE_PATH", "/home/alex/Data/")
[5]:
ds = ctx.load(
    'MIB',
    path=os.path.join(data_base_path, '200_20190328_185735/default.hdr'),
)
# ds.shape is (256, 256, 256, 256)
# Size: 8 GB
[6]:
cx, cy = 134, 129

These parameters allow running the analysis on a consumer-grade computer within a few minutes:

[7]:
# rfa = ctx.create_radial_fourier_analysis(
#     dataset=ds,
#     cx=cx,
#     cy=cy,
#     ri=0,
#     ro=45,
#     n_bins=45,
#     max_order=12
# )

A workstation with a powerful multicore CPU is more suitable for these parameters

[8]:
rfa = ctx.create_radial_fourier_analysis(
    dataset=ds,
    cx=cx,
    cy=cy,
    ri=0,
    ro=120,
    n_bins=120,
    max_order=36
)
[9]:
res = ctx.run(rfa, progress=True)

Calculate a spectrum for the entire scan as a function of radius

Note the “abs”: The coefficients are complex numbers with arbitrary phase angles. We sum up the absolute values to ignore orientations, otherwise they would cancel each other out. Summing the actual values could yield texture information.

[10]:
spectrum = np.abs(res.raw_results).sum(axis=(2, 3))
fig, axes = plt.subplots()
plt.imshow(spectrum, norm=colors.LogNorm())
plt.colorbar()
plt.xlabel("Order of Fourier component")
plt.ylabel("Radius / px")
plt.title("Spectrum summed over entire scan")
[10]:
Text(0.5, 1.0, 'Spectrum summed over entire scan')
../_images/app_radialfourier_13_1.png

Summing the absolutes of all non-zero components

This is qualitatively equivalent to the variance after filtering the dataset radially with a low-pass filter. High orders would have to be included and bins and Fourier transform would have to be normalized correctly for this to be quantitatively equivalent to the variance, i.e. Fluctuation EM.

[11]:
radial_variance = spectrum[:, 1:].sum(axis=1)
fig, axes = plt.subplots()
plt.plot(radial_variance)
plt.yscale('log')
plt.ylabel("Intensity / a.u.")
plt.xlabel("Radius / px")
plt.title("Radial variance profile summed over entire scan")
[11]:
Text(0.5, 1.0, 'Radial variance profile summed over entire scan')
../_images/app_radialfourier_15_1.png

We can do a radial profile of any order.

[12]:
radial_six = spectrum[:, 6]
fig, axes = plt.subplots()
plt.title("Radial profile of order 6 summed over entire scan")
plt.plot(radial_six)
plt.ylabel("Intensity / a.u.")
plt.xlabel("Radius / px")
plt.yscale('log')
../_images/app_radialfourier_17_0.png

The zero order component is just the radial sum.

[13]:
radial_sum = spectrum[:, 0]
fig, axes = plt.subplots()
plt.plot(radial_sum)
plt.yscale('log')
plt.ylabel("Intensity / a.u.")
plt.xlabel("Radius / px")
plt.title("Radial sum profile summed over entire scan")
[13]:
Text(0.5, 1.0, 'Radial sum profile summed over entire scan')
../_images/app_radialfourier_19_1.png

We determined a “ring of interest” from the radial plots or spectrum and put it in these variables for subsequent use.

[14]:
ri, ro = 30, 35

We analyze which coefficients are present in that ring

Note that the coefficients contain higher harmonics! The one with the highest value is of interest.

[15]:
psd = spectrum[ri:ro].sum(axis=0)
fig, axes = plt.subplots()
plt.plot(psd)
plt.title("Spectrum in \"ring of interest\" summed over entire scan")
plt.ylabel("Intensity / a.u.")
plt.xlabel("Order of Fourier component")
plt.yscale('log')
../_images/app_radialfourier_23_0.png

We calculate maps for selected orders across the scan

[16]:
maps = res.raw_results[ri:ro].sum(axis=0)
# We normalize higher orders with the total integrated intensity
maps[1:] /= np.abs(maps[0])
absmaps = np.abs(maps)

vmin = absmaps[1:].min()
vmax = absmaps[1:].max()

# Plotting the absolute values gives the best overview.
fig, axes = plt.subplots()
plt.imshow(absmaps[0])
plt.colorbar()
plt.ylabel("y scan dimension / px")
plt.xlabel("x scan dimension / px")
plt.title("Map of absolute of order 0")

for o in range(1, 7):
    fig, axes = plt.subplots()
    plt.imshow(absmaps[o], vmin=vmin, vmax=vmax)
    plt.colorbar()
    plt.ylabel("y scan dimension / px")
    plt.xlabel("x scan dimension / px")
    plt.title("Map of absolute of order %s" % o)
../_images/app_radialfourier_25_0.png
../_images/app_radialfourier_25_1.png
../_images/app_radialfourier_25_2.png
../_images/app_radialfourier_25_3.png
../_images/app_radialfourier_25_4.png
../_images/app_radialfourier_25_5.png
../_images/app_radialfourier_25_6.png

The phase gives information about orientation

[17]:
fig, axes = plt.subplots()
plt.imshow(np.angle(maps[o]), cmap=cm.twilight)
plt.colorbar()
plt.ylabel("y scan dimension / px")
plt.xlabel("x scan dimension / px")
plt.title("Map of phase of order %s" % o)
[17]:
Text(0.5, 1.0, 'Map of phase of order 6')
../_images/app_radialfourier_27_1.png

We determine which order is predominant

We reject values below a threshold to eliminate noise Limit to 20 orders for colormap and only use “typical crystalline” orders 2, 4, 6 to determine the threshold.

[18]:
below_threshold = absmaps[1:20] < absmaps[(2, 4, 6), ...].max() * 0.5
below_threshold = np.all(below_threshold, axis=0)
dominant = np.argmax(absmaps[1:20], axis=0) + 1
dominant[below_threshold] = 0

fig, axes = plt.subplots()
plt.imshow(dominant, cmap=cm.tab20, vmin=0, vmax=20)
plt.colorbar()
plt.ylabel("y scan dimension / px")
plt.xlabel("x scan dimension / px")
plt.title("Map of predominant order")
[18]:
Text(0.5, 1.0, 'Map of predominant order')
../_images/app_radialfourier_29_1.png
[ ]: