pyTEMlib 0.2025.4.1__py3-none-any.whl → 0.2025.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyTEMlib might be problematic. Click here for more details.
- build/lib/pyTEMlib/__init__.py +33 -0
- build/lib/pyTEMlib/animation.py +640 -0
- build/lib/pyTEMlib/atom_tools.py +238 -0
- build/lib/pyTEMlib/config_dir.py +31 -0
- build/lib/pyTEMlib/crystal_tools.py +1219 -0
- build/lib/pyTEMlib/diffraction_plot.py +756 -0
- build/lib/pyTEMlib/dynamic_scattering.py +293 -0
- build/lib/pyTEMlib/eds_tools.py +826 -0
- build/lib/pyTEMlib/eds_xsections.py +432 -0
- build/lib/pyTEMlib/eels_tools/__init__.py +44 -0
- build/lib/pyTEMlib/eels_tools/core_loss_tools.py +751 -0
- build/lib/pyTEMlib/eels_tools/eels_database.py +134 -0
- build/lib/pyTEMlib/eels_tools/low_loss_tools.py +655 -0
- build/lib/pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
- build/lib/pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
- build/lib/pyTEMlib/file_reader.py +274 -0
- build/lib/pyTEMlib/file_tools.py +811 -0
- build/lib/pyTEMlib/get_bote_salvat.py +69 -0
- build/lib/pyTEMlib/graph_tools.py +1153 -0
- build/lib/pyTEMlib/graph_viz.py +599 -0
- build/lib/pyTEMlib/image/__init__.py +37 -0
- build/lib/pyTEMlib/image/image_atoms.py +270 -0
- build/lib/pyTEMlib/image/image_clean.py +197 -0
- build/lib/pyTEMlib/image/image_distortion.py +299 -0
- build/lib/pyTEMlib/image/image_fft.py +277 -0
- build/lib/pyTEMlib/image/image_graph.py +926 -0
- build/lib/pyTEMlib/image/image_registration.py +316 -0
- build/lib/pyTEMlib/image/image_utilities.py +309 -0
- build/lib/pyTEMlib/image/image_window.py +421 -0
- build/lib/pyTEMlib/image_tools.py +699 -0
- build/lib/pyTEMlib/interactive_image.py +1 -0
- build/lib/pyTEMlib/kinematic_scattering.py +1196 -0
- build/lib/pyTEMlib/microscope.py +61 -0
- build/lib/pyTEMlib/probe_tools.py +906 -0
- build/lib/pyTEMlib/sidpy_tools.py +153 -0
- build/lib/pyTEMlib/simulation_tools.py +104 -0
- build/lib/pyTEMlib/test.py +437 -0
- build/lib/pyTEMlib/utilities.py +314 -0
- build/lib/pyTEMlib/version.py +5 -0
- build/lib/pyTEMlib/xrpa_x_sections.py +20976 -0
- pyTEMlib/__init__.py +25 -3
- pyTEMlib/animation.py +31 -22
- pyTEMlib/atom_tools.py +29 -34
- pyTEMlib/config_dir.py +2 -28
- pyTEMlib/crystal_tools.py +129 -165
- pyTEMlib/eds_tools.py +559 -342
- pyTEMlib/eds_xsections.py +432 -0
- pyTEMlib/eels_tools/__init__.py +44 -0
- pyTEMlib/eels_tools/core_loss_tools.py +751 -0
- pyTEMlib/eels_tools/eels_database.py +134 -0
- pyTEMlib/eels_tools/low_loss_tools.py +655 -0
- pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
- pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
- pyTEMlib/file_reader.py +274 -0
- pyTEMlib/file_tools.py +260 -1130
- pyTEMlib/get_bote_salvat.py +69 -0
- pyTEMlib/graph_tools.py +101 -174
- pyTEMlib/graph_viz.py +150 -0
- pyTEMlib/image/__init__.py +37 -0
- pyTEMlib/image/image_atoms.py +270 -0
- pyTEMlib/image/image_clean.py +197 -0
- pyTEMlib/image/image_distortion.py +299 -0
- pyTEMlib/image/image_fft.py +277 -0
- pyTEMlib/image/image_graph.py +926 -0
- pyTEMlib/image/image_registration.py +316 -0
- pyTEMlib/image/image_utilities.py +309 -0
- pyTEMlib/image/image_window.py +421 -0
- pyTEMlib/image_tools.py +154 -915
- pyTEMlib/kinematic_scattering.py +1 -1
- pyTEMlib/probe_tools.py +1 -1
- pyTEMlib/test.py +437 -0
- pyTEMlib/utilities.py +314 -0
- pyTEMlib/version.py +2 -3
- pyTEMlib/xrpa_x_sections.py +14 -10
- {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/METADATA +13 -16
- pytemlib-0.2025.9.1.dist-info/RECORD +86 -0
- {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/WHEEL +1 -1
- pytemlib-0.2025.9.1.dist-info/top_level.txt +6 -0
- pyTEMlib/core_loss_widget.py +0 -721
- pyTEMlib/eels_dialog.py +0 -754
- pyTEMlib/eels_dialog_utilities.py +0 -1199
- pyTEMlib/eels_tools.py +0 -2359
- pyTEMlib/file_tools_qt.py +0 -193
- pyTEMlib/image_dialog.py +0 -158
- pyTEMlib/image_dlg.py +0 -146
- pyTEMlib/info_widget.py +0 -1086
- pyTEMlib/info_widget3.py +0 -1120
- pyTEMlib/low_loss_widget.py +0 -479
- pyTEMlib/peak_dialog.py +0 -1129
- pyTEMlib/peak_dlg.py +0 -286
- pytemlib-0.2025.4.1.dist-info/RECORD +0 -38
- pytemlib-0.2025.4.1.dist-info/top_level.txt +0 -1
- {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/entry_points.txt +0 -0
- {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"""
|
|
2
|
+
image_tools.py
|
|
3
|
+
by Gerd Duscher, UTK
|
|
4
|
+
part of pyTEMlib
|
|
5
|
+
MIT license except where stated differently
|
|
6
|
+
|
|
7
|
+
This version is build on top of pycroscopy.image package of the pycrocsopy ecosysgtem.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
import scipy
|
|
13
|
+
import skimage
|
|
14
|
+
import sklearn
|
|
15
|
+
import matplotlib
|
|
16
|
+
import matplotlib.pylab as plt
|
|
17
|
+
|
|
18
|
+
import sidpy
|
|
19
|
+
import pyTEMlib
|
|
20
|
+
|
|
21
|
+
## import all function of the image package of pycroscopy
|
|
22
|
+
from .image import *
|
|
23
|
+
from .image.image_utilities import pol2cart, cart2pol, xy2polar
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_atomic_pseudo_potential(fov, atoms, size=512, rotation=0):
|
|
27
|
+
"""Big assumption: the atoms are not near the edge of the unit cell
|
|
28
|
+
# If any atoms are close to the edge (ex. [0,0]) then the potential will be clipped
|
|
29
|
+
# before calling the function, shift the atoms to the center of the unit cell
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
pixel_size = fov / size
|
|
33
|
+
max_size = int(size * np.sqrt(2) + 1) # Maximum size to accommodate rotation
|
|
34
|
+
|
|
35
|
+
# Create unit cell potential
|
|
36
|
+
positions = atoms.get_positions()[:, :2]
|
|
37
|
+
atomic_numbers = atoms.get_atomic_numbers()
|
|
38
|
+
unit_cell_size = atoms.cell.cellpar()[:2]
|
|
39
|
+
|
|
40
|
+
unit_cell_potential = np.zeros((max_size, max_size))
|
|
41
|
+
for pos, atomic_number in zip(positions, atomic_numbers):
|
|
42
|
+
x = pos[0] / pixel_size
|
|
43
|
+
y = pos[1] / pixel_size
|
|
44
|
+
atom_width = 0.5 # Angstrom
|
|
45
|
+
# important for images at various fov. Room for improvement with theory
|
|
46
|
+
gauss_width = atom_width/pixel_size
|
|
47
|
+
gauss = pyTEMlib.probe_tools.make_gauss(max_size, max_size,
|
|
48
|
+
width=gauss_width,
|
|
49
|
+
x0=x, y0=y)
|
|
50
|
+
unit_cell_potential += gauss * atomic_number # gauss is already normalized to 1
|
|
51
|
+
|
|
52
|
+
# Create interpolation function for unit cell potential
|
|
53
|
+
x_grid = np.linspace(0, fov * max_size / size, max_size)
|
|
54
|
+
y_grid = np.linspace(0, fov * max_size / size, max_size)
|
|
55
|
+
interpolator = scipy.interpolate.RegularGridInterpolator((x_grid, y_grid),
|
|
56
|
+
unit_cell_potential,
|
|
57
|
+
bounds_error=False,
|
|
58
|
+
fill_value=0)
|
|
59
|
+
# Vectorized computation of the full potential map with max_size
|
|
60
|
+
x_coords, y_coords = np.meshgrid(np.linspace(0, fov, max_size),
|
|
61
|
+
np.linspace(0, fov, max_size),
|
|
62
|
+
indexing="ij")
|
|
63
|
+
xtal_x = x_coords % unit_cell_size[0]
|
|
64
|
+
xtal_y = y_coords % unit_cell_size[1]
|
|
65
|
+
potential_map = interpolator((xtal_x.ravel(), xtal_y.ravel())).reshape(max_size, max_size)
|
|
66
|
+
|
|
67
|
+
# Rotate and crop the potential map
|
|
68
|
+
potential_map = scipy.ndimage.rotate(potential_map, rotation, reshape=False)
|
|
69
|
+
center = potential_map.shape[0] // 2
|
|
70
|
+
potential_map = potential_map[center - size // 2:center + size // 2,
|
|
71
|
+
center - size // 2:center + size // 2]
|
|
72
|
+
potential_map = scipy.ndimage.gaussian_filter(potential_map,3)
|
|
73
|
+
|
|
74
|
+
return potential_map
|
|
75
|
+
|
|
76
|
+
def convolve_probe(ab, potential):
|
|
77
|
+
""" Convolve probe with potential using FFT based convolution"""
|
|
78
|
+
# the pixel sizes should be the exact same as the potential
|
|
79
|
+
final_sizes = potential.shape
|
|
80
|
+
|
|
81
|
+
# Perform FFT-based convolution
|
|
82
|
+
pad_height = pad_width = potential.shape[0] // 2
|
|
83
|
+
potential = np.pad(potential, ((pad_height, pad_height),
|
|
84
|
+
(pad_width, pad_width)), mode='constant')
|
|
85
|
+
|
|
86
|
+
probe, _, _ = pyTEMlib.probe_tools.get_probe(ab, potential.shape[0],
|
|
87
|
+
potential.shape[1],
|
|
88
|
+
scale='mrad', verbose=False)
|
|
89
|
+
convolved = scipy.signal.fftconvolve(potential, probe, mode='same')
|
|
90
|
+
|
|
91
|
+
# Crop to original potential size
|
|
92
|
+
start_row = pad_height
|
|
93
|
+
start_col = pad_width
|
|
94
|
+
end_row = start_row + final_sizes[0]
|
|
95
|
+
end_col = start_col + final_sizes[1]
|
|
96
|
+
|
|
97
|
+
image = convolved[start_row:end_row, start_col:end_col]
|
|
98
|
+
return probe, image
|
|
99
|
+
|
|
100
|
+
def get_wavelength(e0):
|
|
101
|
+
"""
|
|
102
|
+
Calculates the relativistic corrected de Broglie wave length of an electron
|
|
103
|
+
# Wavelength in 1/nm
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
e0: float
|
|
107
|
+
acceleration voltage in volt
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
wave length in 1/nm
|
|
112
|
+
"""
|
|
113
|
+
ev = scipy.constants.e * e0
|
|
114
|
+
h = scipy.constants.h
|
|
115
|
+
m = scipy.constants.m_e
|
|
116
|
+
c = scipy.constants.c
|
|
117
|
+
return h / np.sqrt(2 * m * ev * (1 + ev / (2 * m * c**2))) * 10**9
|
|
118
|
+
|
|
119
|
+
def fourier_transform(dset: sidpy.Dataset) -> sidpy.Dataset:
|
|
120
|
+
"""
|
|
121
|
+
Reads information into dictionary 'tags', performs 'FFT',
|
|
122
|
+
and provides a smoothed FT and reciprocal
|
|
123
|
+
and intensity limits for visualization.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
dset: sidpy.Dataset
|
|
128
|
+
image
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
fft_dset: sidpy.Dataset
|
|
133
|
+
Fourier transform with correct dimensions
|
|
134
|
+
|
|
135
|
+
Example
|
|
136
|
+
-------
|
|
137
|
+
>>> fft_dataset = fourier_transform(sidpy_dataset)
|
|
138
|
+
>>> fft_dataset.plot()
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
assert isinstance(dset, sidpy.Dataset), 'Expected a sidpy Dataset'
|
|
142
|
+
|
|
143
|
+
selection = []
|
|
144
|
+
image_dims = dset.get_image_dims(return_axis=True)
|
|
145
|
+
if dset.data_type.name == 'IMAGE_STACK':
|
|
146
|
+
stack_dim = dset.get_dimensions_by_type('TEMPORAL')
|
|
147
|
+
|
|
148
|
+
if len(image_dims) != 2:
|
|
149
|
+
raise ValueError('need at least two SPATIAL dimension for an image stack')
|
|
150
|
+
|
|
151
|
+
for i in range(dset.ndim):
|
|
152
|
+
if i in image_dims:
|
|
153
|
+
selection.append(slice(None))
|
|
154
|
+
if len(stack_dim) == 0:
|
|
155
|
+
stack_dim = i
|
|
156
|
+
selection.append(slice(None))
|
|
157
|
+
elif i in stack_dim:
|
|
158
|
+
stack_dim = i
|
|
159
|
+
selection.append(slice(None))
|
|
160
|
+
else:
|
|
161
|
+
selection.append(slice(0, 1))
|
|
162
|
+
|
|
163
|
+
image_stack = np.squeeze(np.array(dset)[selection])
|
|
164
|
+
new_image = np.sum(np.array(image_stack), axis=stack_dim)
|
|
165
|
+
elif dset.data_type.name == 'IMAGE':
|
|
166
|
+
new_image = np.array(dset)
|
|
167
|
+
else:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
new_image = new_image - new_image.min()
|
|
171
|
+
fft_transform = (np.fft.fftshift(np.fft.fft2(np.array(new_image))))
|
|
172
|
+
|
|
173
|
+
image_dims = dset.get_image_dims(return_axis=True)
|
|
174
|
+
|
|
175
|
+
units_x = '1/' + image_dims[0].units
|
|
176
|
+
units_y = '1/' + image_dims[1].units
|
|
177
|
+
fft_dset = sidpy.Dataset.from_array(fft_transform)
|
|
178
|
+
fft_dset.quantity = dset.quantity
|
|
179
|
+
fft_dset.units = 'a.u.'
|
|
180
|
+
fft_dset.data_type = 'IMAGE'
|
|
181
|
+
fft_dset.source = dset.title
|
|
182
|
+
fft_dset.modality = 'fft'
|
|
183
|
+
axis = np.fft.fftshift(np.fft.fftfreq(new_image.shape[0], d=dset.x[1]-dset.x[0]))
|
|
184
|
+
fft_dset.set_dimension(0, sidpy.Dimension(axis,
|
|
185
|
+
name='u', units=units_x,
|
|
186
|
+
dimension_type='RECIPROCAL',
|
|
187
|
+
quantity='reciprocal_length'))
|
|
188
|
+
axis = np.fft.fftshift(np.fft.fftfreq(new_image.shape[1], d=dset.y[1]-dset.y[0]))
|
|
189
|
+
fft_dset.set_dimension(1, sidpy.Dimension(axis,
|
|
190
|
+
name='v', units=units_y,
|
|
191
|
+
dimension_type='RECIPROCAL',
|
|
192
|
+
quantity='reciprocal_length'))
|
|
193
|
+
return fft_dset
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def center_diffractogram(dset, return_plot = True, smoothing = 1,
|
|
197
|
+
min_samples = 10, beamstop_size = 0.1):
|
|
198
|
+
"""Find center of diffractogram by fitting a circle to the diffraction ring"""
|
|
199
|
+
mean_radius = 0
|
|
200
|
+
try:
|
|
201
|
+
diff = np.array(dset).T.astype(np.float16)
|
|
202
|
+
diff[diff < 0] = 0
|
|
203
|
+
threshold = skimage.filters.threshold_otsu(diff)
|
|
204
|
+
binary = (diff > threshold).astype(float)
|
|
205
|
+
# Smooth before edge detection
|
|
206
|
+
smoothed_image = scipy.ndimage.gaussian_filter(binary, sigma=smoothing)
|
|
207
|
+
smooth_threshold = skimage.filters.threshold_otsu(smoothed_image)
|
|
208
|
+
smooth_binary = (smoothed_image > smooth_threshold).astype(float)
|
|
209
|
+
|
|
210
|
+
# add a circle to mask the beamstop
|
|
211
|
+
x, y = np.meshgrid(np.arange(dset.shape[0]), np.arange(dset.shape[1]))
|
|
212
|
+
radius = (x - dset.shape[0] / 2) ** 2 + (y - dset.shape[1] / 2) ** 2
|
|
213
|
+
circle = radius < (beamstop_size * dset.shape[0] / 2) ** 2
|
|
214
|
+
smooth_binary[circle] = 1
|
|
215
|
+
|
|
216
|
+
# Find the edges using the Sobel operator
|
|
217
|
+
edges = skimage.filters.sobel(smooth_binary)
|
|
218
|
+
edge_points = np.argwhere(edges)
|
|
219
|
+
|
|
220
|
+
# Use DBSCAN to cluster the edge points
|
|
221
|
+
db = sklearn.cluster.DBSCAN(eps=10, min_samples=min_samples).fit(edge_points)
|
|
222
|
+
labels = db.labels_
|
|
223
|
+
if len(set(labels)) == 1:
|
|
224
|
+
raise ValueError("DBSCAN clustering resulted in only one group, check the parameters.")
|
|
225
|
+
|
|
226
|
+
# Get the largest group of edge points
|
|
227
|
+
unique, counts = np.unique(labels, return_counts=True)
|
|
228
|
+
counts = dict(zip(unique, counts))
|
|
229
|
+
largest_group = max(counts, key=counts.get)
|
|
230
|
+
edge_points = edge_points[labels == largest_group]
|
|
231
|
+
|
|
232
|
+
# Fit a circle to the diffraction ring
|
|
233
|
+
def calc_distance(c, x, y):
|
|
234
|
+
ri = np.sqrt((x - c[0])**2 + (y - c[1])**2)
|
|
235
|
+
return ri - ri.mean()
|
|
236
|
+
x_m = np.mean(edge_points[:, 1])
|
|
237
|
+
y_m = np.mean(edge_points[:, 0])
|
|
238
|
+
center_guess = x_m, y_m
|
|
239
|
+
center, _ = scipy.optimize.leastsq(calc_distance, center_guess,
|
|
240
|
+
args=(edge_points[:, 1], edge_points[:, 0]))
|
|
241
|
+
mean_radius = (np.mean(calc_distance(center, edge_points[:, 1], edge_points[:, 0]))
|
|
242
|
+
+ np.sqrt((edge_points[:, 1] - center[0])**2
|
|
243
|
+
+ (edge_points[:, 0] - center[1])**2).mean())
|
|
244
|
+
finally:
|
|
245
|
+
if return_plot:
|
|
246
|
+
fig, ax = plt.subplots(1, 5, figsize=(14, 4), sharex=True, sharey=True)
|
|
247
|
+
ax[0].set_title('Diffractogram')
|
|
248
|
+
ax[0].imshow(dset.T, cmap='viridis')
|
|
249
|
+
ax[1].set_title('Otsu Binary Image')
|
|
250
|
+
ax[1].imshow(binary, cmap='gray')
|
|
251
|
+
ax[2].set_title('Smoothed Binary Image')
|
|
252
|
+
ax[2].imshow(smoothed_image, cmap='gray')
|
|
253
|
+
|
|
254
|
+
ax[3].set_title('Smoothed Binary Image')
|
|
255
|
+
ax[3].imshow(smooth_binary, cmap='gray')
|
|
256
|
+
ax[4].set_title('Edge Detection and Fitting')
|
|
257
|
+
ax[4].imshow(edges, cmap='gray')
|
|
258
|
+
ax[4].scatter(center[0], center[1], c='r', s=10)
|
|
259
|
+
circle = plt.Circle(center, mean_radius, color='red', fill=False)
|
|
260
|
+
ax[4].add_artist(circle)
|
|
261
|
+
for axis in ax:
|
|
262
|
+
axis.axis('off')
|
|
263
|
+
fig.tight_layout()
|
|
264
|
+
return center
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class ImageWithLineProfile:
|
|
268
|
+
"""Image with line profile"""
|
|
269
|
+
|
|
270
|
+
def __init__(self, data, extent, title=''):
|
|
271
|
+
fig, ax = plt.subplots(1, 1)
|
|
272
|
+
self.figure = fig
|
|
273
|
+
self.title = title
|
|
274
|
+
self.line_plot = False
|
|
275
|
+
self.ax = ax
|
|
276
|
+
self.data = data
|
|
277
|
+
self.extent = extent
|
|
278
|
+
self.ax.imshow(data, extent=extent)
|
|
279
|
+
self.ax.set_title(title)
|
|
280
|
+
self.line, = self.ax.plot([0], [0], color='orange') # empty line
|
|
281
|
+
self.end_x = self.line.get_xdata()
|
|
282
|
+
self.end_y = self.line.get_ydata()
|
|
283
|
+
self.x = 0
|
|
284
|
+
self.y = 0
|
|
285
|
+
self.z = 0
|
|
286
|
+
self.start_x = self.end_x
|
|
287
|
+
self.start_y = self.end_y
|
|
288
|
+
self.moved_point = [0, 0]
|
|
289
|
+
self.new_point = [0, 0]
|
|
290
|
+
self.cid = self.line.figure.canvas.mpl_connect('button_press_event', self)
|
|
291
|
+
|
|
292
|
+
def __call__(self, event):
|
|
293
|
+
if event.inaxes != self.line.axes:
|
|
294
|
+
return
|
|
295
|
+
self.start_x = self.end_x
|
|
296
|
+
self.start_y = self.end_y
|
|
297
|
+
|
|
298
|
+
self.line.set_data([self.start_x, event.xdata], [self.start_y, event.ydata])
|
|
299
|
+
self.line.figure.canvas.draw()
|
|
300
|
+
|
|
301
|
+
self.end_x = event.xdata
|
|
302
|
+
self.end_y = event.ydata
|
|
303
|
+
|
|
304
|
+
self.update()
|
|
305
|
+
|
|
306
|
+
def update(self):
|
|
307
|
+
""" Update line profile"""
|
|
308
|
+
if not self.line_plot:
|
|
309
|
+
self.line_plot = True
|
|
310
|
+
self.figure.clear()
|
|
311
|
+
self.ax = self.figure.subplots(2, 1)
|
|
312
|
+
self.ax[0].imshow(self.data, extent=self.extent)
|
|
313
|
+
self.ax[0].set_title(self.title)
|
|
314
|
+
|
|
315
|
+
self.line, = self.ax[0].plot([0], [0], color='orange') # empty line
|
|
316
|
+
self.line_plot, = self.ax[1].plot([], [], color='orange')
|
|
317
|
+
self.ax[1].set_xlabel('distance [nm]')
|
|
318
|
+
|
|
319
|
+
x0 = self.start_x
|
|
320
|
+
x1 = self.end_x
|
|
321
|
+
y0 = self.start_y
|
|
322
|
+
y1 = self.end_y
|
|
323
|
+
length_plot = np.sqrt((x1-x0)**2+(y1-y0)**2)
|
|
324
|
+
|
|
325
|
+
num = length_plot*(self.data.shape[0]/self.extent[1])
|
|
326
|
+
x = np.linspace(x0, x1, num)*(self.data.shape[0]/self.extent[1])
|
|
327
|
+
y = np.linspace(y0, y1, num)*(self.data.shape[0]/self.extent[1])
|
|
328
|
+
|
|
329
|
+
# Extract the values along the line, using cubic interpolation
|
|
330
|
+
zi2 = scipy.ndimage.map_coordinates(self.data.T, np.vstack((x, y)))
|
|
331
|
+
|
|
332
|
+
x_axis = np.linspace(0, length_plot, len(zi2))
|
|
333
|
+
self.x = x_axis
|
|
334
|
+
self.z = zi2
|
|
335
|
+
|
|
336
|
+
self.line_plot.set_xdata(x_axis)
|
|
337
|
+
self.line_plot.set_ydata(zi2)
|
|
338
|
+
self.ax[1].set_xlim(0, x_axis.max())
|
|
339
|
+
self.ax[1].set_ylim(zi2.min(), zi2.max())
|
|
340
|
+
self.ax[1].draw()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class LineSelector(matplotlib.widgets.PolygonSelector):
|
|
344
|
+
""" Line selector with adjustable line width"""
|
|
345
|
+
def __init__(self, ax, onselect, line_width=1, **kwargs):
|
|
346
|
+
super().__init__(ax, onselect, **kwargs)
|
|
347
|
+
bounds = ax.viewLim.get_points()
|
|
348
|
+
np.max(bounds[0])
|
|
349
|
+
self.line_verts = np.array([[np.max(bounds[1])/2, np.max(bounds[0])/5],
|
|
350
|
+
[np.max(bounds[1])/2, np.max(bounds[0])/5+1],
|
|
351
|
+
[np.max(bounds[1])/5, np.max(bounds[0])/2],
|
|
352
|
+
[np.max(bounds[1])/5, np.max(bounds[0])/2]])
|
|
353
|
+
self.verts = self.line_verts
|
|
354
|
+
self.line_width = line_width
|
|
355
|
+
|
|
356
|
+
def set_linewidth(self, line_width=None):
|
|
357
|
+
""" Set the line width of the line selector"""
|
|
358
|
+
if line_width is not None:
|
|
359
|
+
self.line_width = line_width
|
|
360
|
+
|
|
361
|
+
m = -(self.line_verts[0, 1]-self.line_verts[3, 1])/(self.line_verts[0, 0]
|
|
362
|
+
-self.line_verts[3, 0])
|
|
363
|
+
c = 1/np.sqrt(1+m**2)
|
|
364
|
+
s = c*m
|
|
365
|
+
self.line_verts[1] = [self.line_verts[0, 0]+self.line_width*s,
|
|
366
|
+
self.line_verts[0, 1]+self.line_width*c]
|
|
367
|
+
self.line_verts[2] = [self.line_verts[3, 0]+self.line_width*s,
|
|
368
|
+
self.line_verts[3, 1]+self.line_width*c]
|
|
369
|
+
self.verts = self.line_verts.copy()
|
|
370
|
+
|
|
371
|
+
def onmove(self, event):
|
|
372
|
+
super().onmove(event)
|
|
373
|
+
if np.max(np.linalg.norm(self.line_verts-self.verts, axis=1)) > 1:
|
|
374
|
+
self.moved_point = np.argmax(np.linalg.norm(self.line_verts-self.verts, axis=1))
|
|
375
|
+
|
|
376
|
+
self.new_point = self.verts[self.moved_point]
|
|
377
|
+
moved_point = int(np.floor(self.moved_point/2)*3)
|
|
378
|
+
self.moved_point = moved_point
|
|
379
|
+
self.line_verts[moved_point] = self.new_point
|
|
380
|
+
self.set_linewidth()
|
|
381
|
+
|
|
382
|
+
def get_profile(dataset, line, spline_order=-1):
|
|
383
|
+
"""
|
|
384
|
+
This function extracts a line profile from a given dataset.
|
|
385
|
+
The line profile is a representation of the data values
|
|
386
|
+
along a specified line in the dataset.
|
|
387
|
+
This function works for both image and spectral image data types.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
dataset (sidpy.Dataset): The input dataset from which to extract the line profile.
|
|
391
|
+
line (list): A list specifying the line along which the profile should be extracted.
|
|
392
|
+
spline_order (int, optional): The order of the spline interpolation to use.
|
|
393
|
+
Default is -1, which means no interpolation.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
profile_dataset (sidpy.Dataset): A new sidpy.Dataset containing the line profile.
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
"""
|
|
400
|
+
xv, yv = get_line_selection_points(line)
|
|
401
|
+
if dataset.data_type.name == 'IMAGE':
|
|
402
|
+
image_dims = dataset.get_image_dims(return_axis=True)
|
|
403
|
+
xv /= image_dims[0].slope
|
|
404
|
+
yv /= image_dims[1].slope
|
|
405
|
+
profile = scipy.ndimage.map_coordinates(np.array(dataset), [xv, yv])
|
|
406
|
+
|
|
407
|
+
profile_dataset = sidpy.Dataset.from_array(profile.sum(axis=0))
|
|
408
|
+
profile_dataset.data_type='spectrum'
|
|
409
|
+
profile_dataset.units = dataset.units
|
|
410
|
+
profile_dataset.quantity = dataset.quantity
|
|
411
|
+
profile_dataset.set_dimension(0, sidpy.Dimension(np.linspace(xv[0,0], xv[-1,-1],
|
|
412
|
+
profile_dataset.shape[0]),
|
|
413
|
+
name='x', units=dataset.x.units,
|
|
414
|
+
quantity=dataset.x.quantity,
|
|
415
|
+
dimension_type='spatial'))
|
|
416
|
+
|
|
417
|
+
if dataset.data_type.name == 'SPECTRAL_IMAGE':
|
|
418
|
+
spectral_axis = dataset.get_spectral_dims(return_axis=True)[0]
|
|
419
|
+
if spline_order > -1:
|
|
420
|
+
xv, yv, zv = get_line_selection_points_interpolated(line, z_length=dataset.shape[2])
|
|
421
|
+
profile = scipy.ndimage.map_coordinates(np.array(dataset), [xv, yv, zv],
|
|
422
|
+
order=spline_order)
|
|
423
|
+
profile = profile.sum(axis=0)
|
|
424
|
+
profile = np.stack([profile, profile], axis=1)
|
|
425
|
+
start = xv[0, 0, 0]
|
|
426
|
+
else:
|
|
427
|
+
profile = get_line_profile(np.array(dataset), xv, yv, len(spectral_axis))
|
|
428
|
+
start = xv[0, 0]
|
|
429
|
+
print(profile.shape)
|
|
430
|
+
profile_dataset = sidpy.Dataset.from_array(profile)
|
|
431
|
+
profile_dataset.data_type='spectral_image'
|
|
432
|
+
profile_dataset.units = dataset.units
|
|
433
|
+
profile_dataset.quantity = dataset.quantity
|
|
434
|
+
profile_dataset.set_dimension(0, sidpy.Dimension(np.arange(profile_dataset.shape[0])+start,
|
|
435
|
+
name='x', units=dataset.x.units,
|
|
436
|
+
quantity=dataset.x.quantity,
|
|
437
|
+
dimension_type='spatial'))
|
|
438
|
+
profile_dataset.set_dimension(1, sidpy.Dimension([0, 1],
|
|
439
|
+
name='y', units=dataset.x.units,
|
|
440
|
+
quantity=dataset.x.quantity,
|
|
441
|
+
dimension_type='spatial'))
|
|
442
|
+
|
|
443
|
+
profile_dataset.set_dimension(2, spectral_axis)
|
|
444
|
+
return profile_dataset
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def get_line_selection_points_interpolated(line, z_length=1):
|
|
448
|
+
""" Get line selection points from line selector with interpolation in z direction"""
|
|
449
|
+
start_point = line.line_verts[3]
|
|
450
|
+
right_point = line.line_verts[0]
|
|
451
|
+
low_point = line.line_verts[2]
|
|
452
|
+
|
|
453
|
+
if start_point[0] > right_point[0]:
|
|
454
|
+
start_point = line.line_verts[0]
|
|
455
|
+
right_point = line.line_verts[3]
|
|
456
|
+
low_point = line.line_verts[1]
|
|
457
|
+
m = (right_point[1] - start_point[1]) / (right_point[0] - start_point[0])
|
|
458
|
+
length_x = int(abs(start_point[0]-right_point[0]))
|
|
459
|
+
length_v = int(np.linalg.norm(start_point-right_point))
|
|
460
|
+
|
|
461
|
+
linewidth = int(abs(start_point[1]-low_point[1]))
|
|
462
|
+
x = np.linspace(0,length_x, length_v)
|
|
463
|
+
y = np.linspace(0,linewidth, line.line_width)
|
|
464
|
+
if z_length > 1:
|
|
465
|
+
z = np.linspace(0, z_length, z_length)
|
|
466
|
+
xv, yv, zv = np.meshgrid(x, y, np.arange(z_length))
|
|
467
|
+
x = np.atleast_2d(x).repeat(z_length, axis=0).T
|
|
468
|
+
y = np.atleast_2d(y).repeat(z_length, axis=0).T
|
|
469
|
+
else:
|
|
470
|
+
xv, yv = np.meshgrid(x, y)
|
|
471
|
+
yv = yv + x*m + start_point[1]
|
|
472
|
+
xv = (xv.swapaxes(0,1) -y*m ).swapaxes(0,1) + start_point[0]
|
|
473
|
+
|
|
474
|
+
if z_length > 1:
|
|
475
|
+
return xv, yv, zv
|
|
476
|
+
else:
|
|
477
|
+
return xv, yv
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def get_line_selection_points(line):
|
|
481
|
+
""" Get line selection points from line selector"""
|
|
482
|
+
start_point = line.line_verts[3]
|
|
483
|
+
right_point = line.line_verts[0]
|
|
484
|
+
low_point = line.line_verts[2]
|
|
485
|
+
|
|
486
|
+
if start_point[0] > right_point[0]:
|
|
487
|
+
start_point = line.line_verts[0]
|
|
488
|
+
right_point = line.line_verts[3]
|
|
489
|
+
low_point = line.line_verts[1]
|
|
490
|
+
m = (right_point[1] - start_point[1]) / (right_point[0] - start_point[0])
|
|
491
|
+
length_x = int(abs(start_point[0]-right_point[0]))
|
|
492
|
+
length_v = int(np.linalg.norm(start_point-right_point))
|
|
493
|
+
|
|
494
|
+
linewidth = int(abs(start_point[1]-low_point[1]))
|
|
495
|
+
x = np.linspace(0,length_x, length_v)
|
|
496
|
+
y = np.linspace(0,linewidth, line.line_width)
|
|
497
|
+
xv, yv = np.meshgrid(x, y)
|
|
498
|
+
|
|
499
|
+
yy = yv +x*m+start_point[1]
|
|
500
|
+
xx = (xv.T -y*m ).T + start_point[0]
|
|
501
|
+
return xx, yy
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def get_line_profile(data, xv, yv, z_length):
|
|
505
|
+
""" Get line profile from data array"""
|
|
506
|
+
profile = np.zeros([len(xv[0]), 2, z_length])
|
|
507
|
+
for index_x in range(xv.shape[1]):
|
|
508
|
+
for index_y in range(xv.shape[0]):
|
|
509
|
+
x = int(xv[index_y, index_x])
|
|
510
|
+
y = int(yv[index_y, index_x])
|
|
511
|
+
if x< data.shape[0] and x>0 and y < data.shape[1] and y>0:
|
|
512
|
+
profile[index_x, 0] +=data[x, y]
|
|
513
|
+
return profile
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def histogram_plot(image_tags):
|
|
517
|
+
"""interactive histogram"""
|
|
518
|
+
nbins = 75
|
|
519
|
+
color_map_list = ['gray', 'viridis', 'jet', 'hot']
|
|
520
|
+
if 'minimum_intensity' not in image_tags:
|
|
521
|
+
image_tags['minimum_intensity'] = image_tags['plotimage'].min()
|
|
522
|
+
if 'maximum_intensity' not in image_tags:
|
|
523
|
+
image_tags['maximum_intensity'] = image_tags['plotimage'].max()
|
|
524
|
+
data = image_tags['plotimage']
|
|
525
|
+
vmin = image_tags['minimum_intensity']
|
|
526
|
+
vmax = image_tags['maximum_intensity']
|
|
527
|
+
if 'color_map' not in image_tags:
|
|
528
|
+
image_tags['color_map'] = color_map_list[0]
|
|
529
|
+
|
|
530
|
+
cmap = plt.cm.get_cmap(image_tags['color_map'])
|
|
531
|
+
colors = cmap(np.linspace(0., 1., nbins))
|
|
532
|
+
norm2 = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
533
|
+
hist, bin_edges = np.histogram(data, np.linspace(vmin, vmax, nbins), density=True)
|
|
534
|
+
|
|
535
|
+
width = bin_edges[1]-bin_edges[0]
|
|
536
|
+
event2 = None
|
|
537
|
+
def onselect(vmin, vmax):
|
|
538
|
+
ax1.clear()
|
|
539
|
+
cmap = plt.cm.get_cmap(image_tags['color_map'])
|
|
540
|
+
colors = cmap(np.linspace(0., 1., nbins))
|
|
541
|
+
norm2 = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
542
|
+
hist2, bin_edges2 = np.histogram(data, np.linspace(vmin, vmax, nbins), density=True)
|
|
543
|
+
|
|
544
|
+
width2 = bin_edges2[1]-bin_edges2[0]
|
|
545
|
+
for i in range(nbins-1):
|
|
546
|
+
histogram[i].xy = (bin_edges2[i], 0)
|
|
547
|
+
histogram[i].set_height(hist2[i])
|
|
548
|
+
histogram[i].set_width(width2)
|
|
549
|
+
histogram[i].set_facecolor(colors[i])
|
|
550
|
+
ax.set_xlim(vmin, vmax)
|
|
551
|
+
ax.set_ylim(0, hist2.max()*1.01)
|
|
552
|
+
|
|
553
|
+
# cb1 = matplotlib.colorbar.ColorbarBase(ax1, cmap=cmap,
|
|
554
|
+
# norm=norm2, orientation='horizontal')
|
|
555
|
+
|
|
556
|
+
image_tags['minimum_intensity'] = vmin
|
|
557
|
+
image_tags['maximum_intensity'] = vmax
|
|
558
|
+
|
|
559
|
+
def onclick(event):
|
|
560
|
+
global event2
|
|
561
|
+
event2 = event
|
|
562
|
+
button_click = 'double' if event.dblclick else 'single'
|
|
563
|
+
print(f"{button_click} click: button={event.button},"
|
|
564
|
+
+ f" x={event.x}, y={event.y}, xdata={event.xdata}, ydata={event.ydata}")
|
|
565
|
+
if event.inaxes == ax1:
|
|
566
|
+
if event.button == 3:
|
|
567
|
+
ind = color_map_list.index(image_tags['color_map'])+1
|
|
568
|
+
if ind == len(color_map_list):
|
|
569
|
+
ind = 0
|
|
570
|
+
image_tags['color_map'] = color_map_list[ind] # 'viridis'
|
|
571
|
+
vmin = image_tags['minimum_intensity']
|
|
572
|
+
vmax = image_tags['maximum_intensity']
|
|
573
|
+
else:
|
|
574
|
+
vmax = data.max()
|
|
575
|
+
vmin = data.min()
|
|
576
|
+
onselect(vmin, vmax)
|
|
577
|
+
|
|
578
|
+
fig2 = plt.figure()
|
|
579
|
+
|
|
580
|
+
ax = fig2.add_axes([0., 0.2, 0.9, 0.7])
|
|
581
|
+
ax1 = fig2.add_axes([0., 0.15, 0.9, 0.05])
|
|
582
|
+
|
|
583
|
+
histogram = ax.bar(bin_edges[0:-1], hist, width=width, color=colors,
|
|
584
|
+
edgecolor='black', alpha=0.8)
|
|
585
|
+
onselect(vmin, vmax)
|
|
586
|
+
cb1 = matplotlib.colorbar.ColorbarBase(ax1, cmap=cmap, norm=norm2, orientation='horizontal')
|
|
587
|
+
|
|
588
|
+
rectprops = dict(facecolor='blue', alpha=0.5)
|
|
589
|
+
|
|
590
|
+
span = matplotlib.widgets.SpanSelector(ax, onselect, 'horizontal', props=rectprops)
|
|
591
|
+
|
|
592
|
+
cid = fig2.canvas.mpl_connect('button_press_event', onclick)
|
|
593
|
+
return span
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def calculate_ctf(wavelength, cs, defocus, k):
|
|
597
|
+
""" Calculate Contrast Transfer Function
|
|
598
|
+
|
|
599
|
+
everything in nm
|
|
600
|
+
|
|
601
|
+
Parameters
|
|
602
|
+
----------
|
|
603
|
+
wavelength: float
|
|
604
|
+
deBroglie wavelength of electrons
|
|
605
|
+
cs: float
|
|
606
|
+
spherical aberration coefficient
|
|
607
|
+
defocus: float
|
|
608
|
+
defocus
|
|
609
|
+
k: numpy array
|
|
610
|
+
reciprocal scale
|
|
611
|
+
|
|
612
|
+
Returns
|
|
613
|
+
-------
|
|
614
|
+
ctf: numpy array
|
|
615
|
+
contrast transfer function
|
|
616
|
+
|
|
617
|
+
"""
|
|
618
|
+
ctf = np.sin(np.pi*defocus*wavelength*k**2+0.5*np.pi*cs*wavelength**3*k**4)
|
|
619
|
+
return ctf
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def calculate_scherzer(wavelength, cs):
|
|
623
|
+
"""
|
|
624
|
+
Calculate the Scherzer defocus. Cs is in mm, lambda is in nm
|
|
625
|
+
|
|
626
|
+
# Input and output in nm
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
scherzer = -1.155*(cs*wavelength)**0.5 # in m
|
|
630
|
+
return scherzer
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def get_rotation(experiment_spots, crystal_spots):
|
|
634
|
+
"""Get rotation by comparing spots in diffractogram to diffraction Bragg spots
|
|
635
|
+
|
|
636
|
+
Parameter
|
|
637
|
+
---------
|
|
638
|
+
experiment_spots: numpy array (nx2)
|
|
639
|
+
positions (in 1/nm) of spots in diffractogram
|
|
640
|
+
crystal_spots: numpy array (nx2)
|
|
641
|
+
positions (in 1/nm) of Bragg spots according to kinematic scattering theory
|
|
642
|
+
"""
|
|
643
|
+
r_experiment, phi_experiment = cart2pol(experiment_spots)
|
|
644
|
+
|
|
645
|
+
# get crystal spots of same length and sort them by angle as well
|
|
646
|
+
r_crystal, phi_crystal, _ = xy2polar(crystal_spots)
|
|
647
|
+
angle_index = np.argmin(np.abs(r_experiment-r_crystal[1]))
|
|
648
|
+
rotation_angle = phi_experiment[angle_index] % (2*np.pi) - phi_crystal[1]
|
|
649
|
+
print(phi_experiment[angle_index])
|
|
650
|
+
st = np.sin(rotation_angle)
|
|
651
|
+
ct = np.cos(rotation_angle)
|
|
652
|
+
rotation_matrix = np.array([[ct, -st], [st, ct]])
|
|
653
|
+
|
|
654
|
+
return rotation_matrix, rotation_angle
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def calibrate_image_scale(fft_tags, spots_reference, spots_experiment):
|
|
658
|
+
"""depreciated get change of scale from comparison of spots to Bragg angles """
|
|
659
|
+
|
|
660
|
+
dist_reference = np.linalg.norm(spots_reference, axis=1)
|
|
661
|
+
distance_experiment = np.linalg.norm(spots_experiment, axis=1)
|
|
662
|
+
|
|
663
|
+
first_reflections = abs(distance_experiment - dist_reference.min()) < .2
|
|
664
|
+
print('Evaluate ', first_reflections.sum(), 'reflections')
|
|
665
|
+
closest_exp_reflections = spots_experiment[first_reflections]
|
|
666
|
+
|
|
667
|
+
def func(params, xdata, ydata):
|
|
668
|
+
dgx, dgy = params
|
|
669
|
+
return np.sqrt((xdata * dgx) ** 2 + (ydata * dgy) ** 2) - dist_reference.min()
|
|
670
|
+
|
|
671
|
+
x0 = [1.001, 0.999]
|
|
672
|
+
[dg, _] = scipy.optimize.leastsq(func, x0, args=(closest_exp_reflections[:, 0],
|
|
673
|
+
closest_exp_reflections[:, 1]))
|
|
674
|
+
return dg
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def align_crystal_reflections(spots, crystals):
|
|
678
|
+
""" Depreciated - use diffraction spots"""
|
|
679
|
+
|
|
680
|
+
crystal_reflections_polar = []
|
|
681
|
+
angles = []
|
|
682
|
+
exp_r, exp_phi = cart2pol(spots) # just in polar coordinates
|
|
683
|
+
|
|
684
|
+
for tags in crystals:
|
|
685
|
+
# sorted by r and phi , only positive angles
|
|
686
|
+
r, phi, indices = xy2polar(tags['allowed']['g'])
|
|
687
|
+
# we mask the experimental values that are found already
|
|
688
|
+
angle = 0.
|
|
689
|
+
|
|
690
|
+
angle_i = np.argmin(np.abs(exp_r - r[1]))
|
|
691
|
+
angle = exp_phi[angle_i] - phi[0]
|
|
692
|
+
angles.append(angle) # Determine rotation angle
|
|
693
|
+
|
|
694
|
+
crystal_reflections_polar.append([r, angle + phi, indices])
|
|
695
|
+
tags['allowed']['g_rotated'] = pol2cart(r, angle + phi)
|
|
696
|
+
"""for spot in tags['allowed']['g']:
|
|
697
|
+
dif = np.linalg.norm(spots[:, 0:2]-spot[0:2], axis=1)
|
|
698
|
+
"""
|
|
699
|
+
return crystal_reflections_polar, angles
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from pyTEMlib import image_dialog
|