Registration of a Stack of Images¶
by
Gerd Duscher
Materials Science & Engineering
Joint Institute of Advanced Materials
The University of Tennessee, Knoxville
We us this notebook only for a stack of images.
Install pyTEMlib¶
If you have not done so in the Introduction Notebook, please install pyTEMlib with the code cell below.
import sys
import importlib.metadata
def test_package(package_name):
"""Test if package exists and returns version or -1"""
try:
version = importlib.metadata.version(package_name)
except importlib.metadata.PackageNotFoundError:
version = '-1'
return version
if test_package('pyTEMlib') < '0.2025.2.0':
print('installing pyTEMlib')
!{sys.executable} -m pip install --upgrade pyTEMlib -q
print('done')
done
Import the usual libraries¶
You’ll need at least pyTEMlib version 0.2020.04.2
You can load that library with the code cell above:
%matplotlib widget
import numpy as np
import matplotlib.pylab as plt
import sys
if 'google.colab' in sys.modules:
from google.colab import output
output.enable_custom_widget_manager()
from google.colab import drive
import sys
sys.path.insert(0, '../../')
import sidpy
%load_ext autoreload
%autoreload 2
if 'google.colab' in sys.modules:
drive.mount("/content/drive")
import pyTEMlib.file_tools # File input/ output library
import pyTEMlib.image_tools
import pyTEMlib.probe_tools
import pyTEMlib.atom_tools
# For archiving reasons it is a good idea to print the version numbers out at this point
print('pyTEM version: ', pyTEMlib.__version__)
## Do all of registration
notebook_tags= {'notebook': 'Image_Registration',
'notebook_version': '2025_03_13',
'pyTEM version': pyTEMlib.__version__}
You don't have igor2 installed. If you wish to open igor files, you will need to install it (pip install igor2) before attempting.
Symmetry functions of spglib enabled
pyTEM version: 0.2025.04.2
Load an image stack :¶
Please, load an image stack.
A stack of images is used to reduce noise, but for an added image the images have to be aligned to compensate for drift and other microscope instabilities.
You select here (with the Select Main button,) a file of your image.
Note that the Add should only used for reference data
fileWidget = pyTEMlib.file_tools.FileWidget()
Some of the metadata is available in the metadata attribute of the dataset but all information can be found in original metadata.
dataset = fileWidget.selected_dataset
v = dataset.plot()Complete Registration¶
Takes a while, depending on your computer between 1 and 10 minutes
Rigid Registration¶
Using sub-pixel accuracy registration determination method of:
Manuel Guizar-Sicairos, Samuel T. Thurman, and James R. Fienup, “Efficient subpixel image registration algorithms,” Optics Letters 33, 156-158 (2008). DOI:10
as implemented in phase_cross_correlation function by scikit-image in the registration package.
rigid_registered = pyTEMlib.image_tools.rigid_registration(dataset, normalization='phase')[0. 1.]
[0. 0.]
[ 0. -4.]
[0. 2.]
[0. 0.]
[0. 0.]
[-1. 0.]
[-1. 0.]
[-1. 0.]
[-3. 0.]
[1. 0.]
[0. 0.]
[ 0. -1.]
[-1. 0.]
[-1. 0.]
[ 0. -3.]
[0. 0.]
[0. 0.]
[-2. 0.]
v = rigid_registered.plot()Non-Rigid Registration¶
Here we use the Diffeomorphic Demon Non-Rigid Registration as provided by simpleITK.
Please Cite:
non_rigid= pyTEMlib.image_tools.demon_registration(rigid_registered):-)
You have successfully completed Diffeomorphic Demons Registration
view2 = non_rigid.plot()Or do it all together
demon_registered, rigid_registered = pyTEMlib.image_tools.complete_registration(dataset)
demon_registered.data_type = 'image_stack'
view2 = demon_registered.plot()Rigid_Registration
Stack contains 10 images, each with 1024 pixels in x-direction and 1024 pixels in y-direction
sidpy.Dataset of type IMAGE_STACK with:
dask.array<array, shape=(10, 1023, 1022), dtype=float64, chunksize=(10, 1023, 1022), chunktype=numpy.ndarray>
data contains: intensity (counts)
and Dimensions:
frame: time (frame) of size (10,)
x: Length (nm) of size (1023,)
y: Length (nm) of size (1022,)
with metadata: ['analysis', 'drift', 'input_crop', 'input_shape', 'experiment']
Non-Rigid_Registration
:-)
You have successfully completed Diffeomorphic Demons Registration
You will need to zoom in to see that the images are changing.
Compare this to the rigid registered stack
view3 = rigid_registered.plot()Check Drift¶
rigid_registered.metadata['analysis']['rigid_registration']['drift']array([[-1., -6.],
[ 0., -5.],
[ 0., -4.],
[ 0., -4.],
[-2., -4.],
[-4., -4.],
[-4., -3.],
[-5., -3.],
[-4., -2.],
[-2., -1.],
[ 0., 0.],
[ 1., 1.],
[ 1., 1.],
[ 1., 1.],
[ 0., 1.],
[ 0., 1.],
[ 0., 2.],
[ 0., 2.],
[ 0., 3.],
[ 0., 4.]])drift = rigid_registered.metadata['analysis']['rigid_registration']['drift']
polynom_degree = 2 # 1 is linear fit, 2 is parabolic fit, ...
x = np.linspace(0,drift.shape[0]-1,drift.shape[0])
line_fit_x = np.polyfit(x, drift[:,0], polynom_degree)
poly_x = np.poly1d(line_fit_x)
line_fit_y = np.polyfit(x, drift[:,1], polynom_degree)
poly_y = np.poly1d(line_fit_y)
plt.figure()
plt.axhline(color = 'gray')
plt.plot(x, drift[:,0], label = 'drift x')
plt.plot(x, drift[:,1], label = 'drift y')
plt.plot(x, poly_x(x), label = 'fit_drift_x')
plt.plot(x, poly_y(x), label = 'fit_drift_y')
plt.legend();
ax_pixels = plt.gca()
ax_pixels.step(1, 1)
scaleX = (rigid_registered.x[1]-rigid_registered.x[0])*1000. #in pm
ax_pm = ax_pixels.twinx()
x_1, x_2 = ax_pixels.get_ylim()
ax_pm.set_ylim(x_1*scaleX, x_2*scaleX)
ax_pixels.set_ylabel('drift [pixels]')
ax_pm.set_ylabel('drift [pm]')
ax_pixels.set_xlabel('image number');
plt.tight_layout()
Find Atom Positions¶
Lucy -Richardson Deconvolution¶
Lucy - Richardson Deconvolution removes noise and convolutes the intensity back into the atom (columns).
Here we use a slightly modified Lucy - Richardson Deconvolution which stops when converged.
Ideally the atom_size should be as large as the atoms in the image.
A good Lucy-Richardson Deconvolution should result in an image with atoms of a radius of about 2 pixels.
The number of steps to convergence should be less than 300 for a good approximation of atom_size.
we use the non-rigid registered datset
# ------- Input ------
atoms_size = 0.08 # in nm
# --------------------
image = non_rigid.sum(axis=0)
image.data = skimage.filters.difference_of_gaussians(image, 5, 20)
out_tags = {}
image2.metadata['experiment']= {'convergence_angle': 30, 'acceleration_voltage': 200000.}
scale_x = image.x.slope
gauss_diameter = atoms_size/scale_x
print(gauss_diameter)
if gauss_diameter < 1:
print('smal')
gauss_diameter = 1
print(gauss_diameter, gauss_diameter*scale_x)
gauss_probe = pyTEMlib.probe_tools.make_gauss(image.shape[0], image.shape[1], gauss_diameter)
print('Deconvolution of ', dataset.title)
LR_dataset = pyTEMlib.image_tools.decon_lr(image, gauss_probe, verbose=False)
extent = LR_dataset.get_extent([0,1])
fig, ax = plt.subplots(1, 2, figsize=(8, 4), sharex=True, sharey=True)
ax[0].imshow(image.T, extent = extent,vmax=np.median(np.array(image))+3*np.std(np.array(image)))
ax[1].imshow(LR_dataset.T, extent = extent, vmax=np.median(np.array(LR_dataset))+3*np.std(np.array(LR_dataset)));1.3313608508711974
1.3313608508711974 0.08
Deconvolution of HAADF
converged in 39 iterations
Lucy-Richardson deconvolution converged in 39 iterations
v = LR_dataset.plot(cmap ='gray', vmax=500000)new_image = np.array(image)
new_image = new_image - new_image.min()
fft_transform = (np.fft.fftshift(np.fft.fft2(np.array(new_image))))
dset = imageimage_dims = dset.get_image_dims(return_axis=True)
units_x = '1/' + image_dims[0].units
units_y = '1/' + image_dims[1].units
fft_dset = sidpy.Dataset.from_array(fft_transform)
fft_dset.quantity = dset.quantity
fft_dset.units = 'a.u.'
fft_dset.data_type = 'IMAGE'
fft_dset.source = dset.title
fft_dset.modality = 'fft'
fft_dset.set_dimension(0, sidpy.Dimension(np.fft.fftshift(np.fft.fftfreq(new_image.shape[0],
d=dset.x[1]-dset.x[0])),
name='u', units=units_x, dimension_type='RECIPROCAL',
quantity='reciprocal_length'))
fft_dset.set_dimension(1, sidpy.Dimension(np.fft.fftshift(np.fft.fftfreq(new_image.shape[1],
d=dset.y[1]- dset.y[0])),
name='v', units=units_y, dimension_type='RECIPROCAL',
quantity='reciprocal_length'))
filtered_power_spectrum = pyTEMlib.image_tools.power_spectrum(image)
filtered_power_spectrum.view_metadata()
print('source: ', filtered_power_spectrum.source)
view = filtered_power_spectrum.plot()
### Log Deconvolutionfft :
smoothing : 3
minimum_intensity : 12.735340729817992
maximum_intensity : 18.906446933063556
source: sum_aggregate_Non-Rigid Registration
tags = {'analysis': {'name': 'Lucy_Richardson',
'input': dataset.title,
'probe_diameter': gauss_diameter,
'kind_of_probe': 'Gauss',
'probe_width': atoms_size
}}
LR_dataset.metadata['analysis'].update(notebook_tags)
datasets = fileWidget.datasets
datasets['LR_decon'] = LR_dataset
datasets['Sum_non_ridgid'] = imageAtom Detection¶
Choose threshold and atom size so that all atoms or at least all bright atoms of an unit cell are found
import skimage
# ------- Input ------
threshold = 20.9 #usally between 0.01 and 0.9 the smaller the more atoms
atom_size = .1 #in nm
# ----------------------
image = LR_dataset
#image = image_choice.dataset
# scale_x = pyTEMlib.file_tools.get_slope(image.dim_1)
blobs = skimage.feature.blob_log(image, max_sigma=atom_size/scale_x, threshold=threshold)
fig1, ax = plt.subplots(1, 1,figsize=(8,7), sharex=True, sharey=True)
plt.title("blob detection ")
plt.imshow(image.T, interpolation='nearest',cmap='gray', vmax=np.median(np.array(image))+3*np.std(np.array(image)))
plt.scatter(blobs[:, 0], blobs[:, 1], c='r', s=20, alpha = .5);Log Atom Positions¶
out_tags = {}
out_tags['analysis']= 'Atom Positions'
out_tags['notebook']= __notebook__
out_tags['notebook_version']= __notebook_version__
out_tags['atoms'] = blobs
out_tags['atom_size'] = atom_size #in nm gives the size of the atoms or resolution
out_tags['threshold'] = threshold #between 0.01 and 0.1
out_tags['pixel_size'] = scale_x
out_tags['name'] = 'Atom finding'
out_tags['title'] = out_tags['name']
atom_group = ft.log_results(current_channel, attributes=out_tags)
for key in current_channel:
if 'Log' in key:
if 'analysis' in current_channel[key]:
print(f"{key} includes analysis: {current_channel[key]['analysis'][()]}")---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[76], line 3
1 out_tags = {}
2 out_tags['analysis']= 'Atom Positions'
----> 3 out_tags['notebook']= __notebook__
4 out_tags['notebook_version']= __notebook_version__
6 out_tags['atoms'] = blobs
NameError: name '__notebook__' is not definedimport joblib
def process_data(data):
# Simulate a time-consuming data processing step
import time
time.sleep(2)
return [data ** 2]*15
data = [1, 2, 3, 4, 5, 6]
# Parallel(n_jobs=workjob_num)(delayed(func_be_applied)(aug) for elem in array
results = joblib.Parallel(n_jobs=6)(joblib.delayed(process_data)(data[d]) for d in range(6))
print(results)[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], [9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9], [16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16], [25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25], [36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36]]
from joblib import Parallel, delayed
class A(object):
def __init__(self, x):
self.x = x
def p(self):
self.y = self.x**2
return self.y
if __name__ == '__main__':
runs = [A(x) for x in range(20)]
with Parallel(n_jobs=2, verbose=5) as parallel:
delayed_funcs = [delayed(lambda x:x.p())(run) for run in runs]
run_A = parallel(delayed_funcs)[Parallel(n_jobs=2)]: Using backend LokyBackend with 2 concurrent workers.
[Parallel(n_jobs=2)]: Done 20 out of 20 | elapsed: 0.3s finished
a = np.arange(12).reshape(2,2,3)
it = np.nditer(a, flags=['multi_index'])
it.remove_axis(2)
for x in it:
print(x, a[it.multi_index], it.multi_index)0 [0 1 2] (0, 0)
3 [3 4 5] (0, 1)
6 [6 7 8] (1, 0)
9 [ 9 10 11] (1, 1)
a[1,0]array([6, 7, 8])a[np.indices([2,2])]array([[[[[ 0, 1, 2],
[ 3, 4, 5]],
[[ 0, 1, 2],
[ 3, 4, 5]]],
[[[ 6, 7, 8],
[ 9, 10, 11]],
[[ 6, 7, 8],
[ 9, 10, 11]]]],
[[[[ 0, 1, 2],
[ 3, 4, 5]],
[[ 6, 7, 8],
[ 9, 10, 11]]],
[[[ 0, 1, 2],
[ 3, 4, 5]],
[[ 6, 7, 8],
[ 9, 10, 11]]]]])blobs
import ase
structure = ase.Atoms('Cu'*len(blobs), positions=blobs*[1,1,0], cell=[image.x[-1],image.y[-1],1], pbc=True)
st
Atoms(symbols='Cu495', pbc=True, cell=[7.060444952883329, 7.0003560596673005, 1.0])Refine All Atom Positions¶
Fitting of a Gaussian into the center of an atom
There will be convergence errors if the atom_radius value is too large or too small
image_choice = ft.ChooseDataset(current_channel) # ------- Input ------
atom_radius = 3 # in pixel
# ----------------------
if image_choice.dataset.data_type.name == 'IMAGE_STACK':
image = image_choice.dataset.sum(axis=0)
else:
image = image_choice.dataset
#atoms = atom_group['atoms'][()]
atoms = blobs
image = image-image.min()
print(image)
#atom_radius = 2
MaxInt = 0
MinInt = 0
maxDist = 2
sym = pyTEMlib.atom_tools.atom_refine(np.array(image), atoms, atom_radius, max_int = 0, min_int = 0, max_dist = 2)
refined_atoms = np.array(sym['atoms'])
fig, ax = plt.subplots(1, 2, figsize=(8, 4), sharex=True, sharey=True)
ax[0].imshow(image.T)
ax[0].scatter(refined_atoms[:,0],refined_atoms[:,1], s=10, alpha = 0.3, color = 'red')
ax[0].set_title('refined atom postion')
ax[1].imshow(image.T)
ax[1].scatter(atoms[:, 0], atoms[:, 1], c='r', s=10, alpha = .3);
ax[1].set_title('blobs on image');sidpy.Dataset of type IMAGE_STACK with:
dask.array<sub, shape=(512, 512), dtype=float64, chunksize=(512, 512), chunktype=numpy.ndarray>
data contains: generic (generic)
and Dimensions:
y: distance (nm) of size (512,)
x: distance (nm) of size (512,)
with metadata: ['metadata', 'original_metadata']
using radius 3 pixels
fileWidget.datasets{'Channel_000': sidpy.Dataset of type IMAGE_STACK with:
dask.array<array, shape=(20, 512, 512), dtype=float64, chunksize=(20, 512, 512), chunktype=numpy.ndarray>
data contains: intensity (counts)
and Dimensions:
frame: time (frame) of size (20,)
x: distance (nm) of size (512,)
y: distance (nm) of size (512,)
with metadata: ['experiment', 'filename'],
'Channel_001': sidpy.Dataset of type IMAGE_STACK with:
dask.array<array, shape=(20, 512, 512), dtype=float64, chunksize=(20, 512, 512), chunktype=numpy.ndarray>
data contains: intensity (counts)
and Dimensions:
frame: time (frame) of size (20,)
x: distance (nm) of size (512,)
y: distance (nm) of size (512,)
with metadata: ['experiment', 'filename'],
'Channel_002': sidpy.Dataset of type IMAGE_STACK with:
dask.array<array, shape=(20, 512, 512), dtype=float64, chunksize=(20, 512, 512), chunktype=numpy.ndarray>
data contains: intensity (counts)
and Dimensions:
frame: time (frame) of size (20,)
x: distance (nm) of size (512,)
y: distance (nm) of size (512,)
with metadata: ['experiment', 'filename'],
'Channel_003': sidpy.Dataset of type IMAGE_STACK with:
dask.array<array, shape=(20, 512, 512), dtype=float64, chunksize=(20, 512, 512), chunktype=numpy.ndarray>
data contains: intensity (counts)
and Dimensions:
frame: time (frame) of size (20,)
x: distance (nm) of size (512,)
y: distance (nm) of size (512,)
with metadata: ['experiment', 'filename']}Close File¶
Close file when finished and everyhting went well.
h5_file = dataset.h5_dataset.file
print(h5_file.filename)
h5_file.close()C:/Users/gduscher/Documents/Github/Image_Distortion\Recording of SuperScan (HAADF)-3.hf5
Appendix¶
Demon Registration¶
Here we use the Diffeomorphic Demon Non-Rigid Registration as provided by simpleITK.
Please Cite:
and
This Non-Rigid Registration consists of the following steps:
determine
referenceimageFor this we use the average of the rigid registered stack
this averaged stack is then smeared with a Gaussian of sigma 2 pixel to reduce noise
under the assumption that high frequency scan distortions cancel out over several images, we, therefore, obtained the center of mass of the atoms.
perform the
demon registrationfilter to determine a distortion matrixeach single image of a stack is first smeared with a Gaussian of sigma of 2pixels
then the deformation matrix is determined for these images
the deformation matrix is a matrix where each pixel has a vector ( x, and y value) for the relative shift of this pixel.
This deformation matrix is used to
transformthe imageThe transformation is performed on the original image.
Important, here, is to set the interpolator method, (the image needs to be interpolated because the new pixels are not on an integer grid.)
Let’s see what the different interpolators do.
| Method | RMS contrast | Standard | Mean |
|---|---|---|---|
| original | 0.1965806 | 0.07764114 | 0.3949583 |
| Linear | 0.20159315 | 0.079470366 | 0.39421165 |
| BSpline | 0.20162606 | 0.0794831 | 0.39421043 |
| Gaussian | 0.14310582 | 0.056414302 | 0.39421389 |
| Hamming | 0.20163293 | 0.07948672 | 0.39421496 |
The Gaussian interpolator as the only one seems to smear the signal.
We will use the Bspline method a fast and simple method that does not introduce spurious features and does not smear the signal.
Full Code of Demon registration¶
import simpleITK as sitk
def DemonReg(cube, verbose = False):
"""
Diffeomorphic Demon Non-Rigid Registration
Usage:
------
DemReg = DemonReg(cube, verbose = False)
Input:
cube: stack of image after rigid registration and cropping
Output:
DemReg: stack of images with non-rigid registration
Dempends on:
simpleITK and numpy
Please Cite: http://www.simpleitk.org/SimpleITK/project/parti.html
and T. Vercauteren, X. Pennec, A. Perchant and N. Ayache
Diffeomorphic Demons Using ITK\'s Finite Difference Solver Hierarchy
The Insight Journal, http://hdl.handle.net/1926/510 2007
"""
DemReg = np.zeros_like(cube)
nimages = cube.shape[0]
print(nimages)
# create fixed image by summing over rigid registration
fixed_np = np.average(current_dataset, axis=0)
fixed = sitk.GetImageFromArray(fixed_np)
fixed = sitk.DiscreteGaussian(fixed, 2.0)
#demons = sitk.SymmetricForcesDemonsRegistrationFilter()
demons = sitk.DiffeomorphicDemonsRegistrationFilter()
demons.SetNumberOfIterations(200)
demons.SetStandardDeviations(1.0)
resampler = sitk.ResampleImageFilter()
resampler.SetReferenceImage(fixed);
resampler.SetInterpolator(sitk.sitkBspline)
resampler.SetDefaultPixelValue(0)
done = 0
for i in range(nimages):
if done < int((i+1)/nimages*50):
done = int((i+1)/nimages*50)
sys.stdout.write('\r')
# progress output :
sys.stdout.write("[%-50s] %d%%" % ('*'*done, 2*done))
sys.stdout.flush()
moving = sitk.GetImageFromArray(cube[i])
movingf = sitk.DiscreteGaussian(moving, 2.0)
displacementField = demons.Execute(fixed,movingf)
outTx = sitk.DisplacementFieldTransform( displacementField )
resampler.SetTransform(outTx)
out = resampler.Execute(moving)
DemReg[i,:,:] = sitk.GetArrayFromImage(out)
#print('image ', i)
print(':-)')
print('You have succesfully completed Diffeomorphic Demons Registration')
return DemReg
