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.

Files changed (94) hide show
  1. build/lib/pyTEMlib/__init__.py +33 -0
  2. build/lib/pyTEMlib/animation.py +640 -0
  3. build/lib/pyTEMlib/atom_tools.py +238 -0
  4. build/lib/pyTEMlib/config_dir.py +31 -0
  5. build/lib/pyTEMlib/crystal_tools.py +1219 -0
  6. build/lib/pyTEMlib/diffraction_plot.py +756 -0
  7. build/lib/pyTEMlib/dynamic_scattering.py +293 -0
  8. build/lib/pyTEMlib/eds_tools.py +826 -0
  9. build/lib/pyTEMlib/eds_xsections.py +432 -0
  10. build/lib/pyTEMlib/eels_tools/__init__.py +44 -0
  11. build/lib/pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  12. build/lib/pyTEMlib/eels_tools/eels_database.py +134 -0
  13. build/lib/pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  14. build/lib/pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  15. build/lib/pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  16. build/lib/pyTEMlib/file_reader.py +274 -0
  17. build/lib/pyTEMlib/file_tools.py +811 -0
  18. build/lib/pyTEMlib/get_bote_salvat.py +69 -0
  19. build/lib/pyTEMlib/graph_tools.py +1153 -0
  20. build/lib/pyTEMlib/graph_viz.py +599 -0
  21. build/lib/pyTEMlib/image/__init__.py +37 -0
  22. build/lib/pyTEMlib/image/image_atoms.py +270 -0
  23. build/lib/pyTEMlib/image/image_clean.py +197 -0
  24. build/lib/pyTEMlib/image/image_distortion.py +299 -0
  25. build/lib/pyTEMlib/image/image_fft.py +277 -0
  26. build/lib/pyTEMlib/image/image_graph.py +926 -0
  27. build/lib/pyTEMlib/image/image_registration.py +316 -0
  28. build/lib/pyTEMlib/image/image_utilities.py +309 -0
  29. build/lib/pyTEMlib/image/image_window.py +421 -0
  30. build/lib/pyTEMlib/image_tools.py +699 -0
  31. build/lib/pyTEMlib/interactive_image.py +1 -0
  32. build/lib/pyTEMlib/kinematic_scattering.py +1196 -0
  33. build/lib/pyTEMlib/microscope.py +61 -0
  34. build/lib/pyTEMlib/probe_tools.py +906 -0
  35. build/lib/pyTEMlib/sidpy_tools.py +153 -0
  36. build/lib/pyTEMlib/simulation_tools.py +104 -0
  37. build/lib/pyTEMlib/test.py +437 -0
  38. build/lib/pyTEMlib/utilities.py +314 -0
  39. build/lib/pyTEMlib/version.py +5 -0
  40. build/lib/pyTEMlib/xrpa_x_sections.py +20976 -0
  41. pyTEMlib/__init__.py +25 -3
  42. pyTEMlib/animation.py +31 -22
  43. pyTEMlib/atom_tools.py +29 -34
  44. pyTEMlib/config_dir.py +2 -28
  45. pyTEMlib/crystal_tools.py +129 -165
  46. pyTEMlib/eds_tools.py +559 -342
  47. pyTEMlib/eds_xsections.py +432 -0
  48. pyTEMlib/eels_tools/__init__.py +44 -0
  49. pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  50. pyTEMlib/eels_tools/eels_database.py +134 -0
  51. pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  52. pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  53. pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  54. pyTEMlib/file_reader.py +274 -0
  55. pyTEMlib/file_tools.py +260 -1130
  56. pyTEMlib/get_bote_salvat.py +69 -0
  57. pyTEMlib/graph_tools.py +101 -174
  58. pyTEMlib/graph_viz.py +150 -0
  59. pyTEMlib/image/__init__.py +37 -0
  60. pyTEMlib/image/image_atoms.py +270 -0
  61. pyTEMlib/image/image_clean.py +197 -0
  62. pyTEMlib/image/image_distortion.py +299 -0
  63. pyTEMlib/image/image_fft.py +277 -0
  64. pyTEMlib/image/image_graph.py +926 -0
  65. pyTEMlib/image/image_registration.py +316 -0
  66. pyTEMlib/image/image_utilities.py +309 -0
  67. pyTEMlib/image/image_window.py +421 -0
  68. pyTEMlib/image_tools.py +154 -915
  69. pyTEMlib/kinematic_scattering.py +1 -1
  70. pyTEMlib/probe_tools.py +1 -1
  71. pyTEMlib/test.py +437 -0
  72. pyTEMlib/utilities.py +314 -0
  73. pyTEMlib/version.py +2 -3
  74. pyTEMlib/xrpa_x_sections.py +14 -10
  75. {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/METADATA +13 -16
  76. pytemlib-0.2025.9.1.dist-info/RECORD +86 -0
  77. {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/WHEEL +1 -1
  78. pytemlib-0.2025.9.1.dist-info/top_level.txt +6 -0
  79. pyTEMlib/core_loss_widget.py +0 -721
  80. pyTEMlib/eels_dialog.py +0 -754
  81. pyTEMlib/eels_dialog_utilities.py +0 -1199
  82. pyTEMlib/eels_tools.py +0 -2359
  83. pyTEMlib/file_tools_qt.py +0 -193
  84. pyTEMlib/image_dialog.py +0 -158
  85. pyTEMlib/image_dlg.py +0 -146
  86. pyTEMlib/info_widget.py +0 -1086
  87. pyTEMlib/info_widget3.py +0 -1120
  88. pyTEMlib/low_loss_widget.py +0 -479
  89. pyTEMlib/peak_dialog.py +0 -1129
  90. pyTEMlib/peak_dlg.py +0 -286
  91. pytemlib-0.2025.4.1.dist-info/RECORD +0 -38
  92. pytemlib-0.2025.4.1.dist-info/top_level.txt +0 -1
  93. {pytemlib-0.2025.4.1.dist-info → pytemlib-0.2025.9.1.dist-info}/entry_points.txt +0 -0
  94. {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