pyTEMlib 0.2025.4.2__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 -928
  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.2.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.2.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.2.dist-info/RECORD +0 -38
  92. pytemlib-0.2025.4.2.dist-info/top_level.txt +0 -1
  93. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/entry_points.txt +0 -0
  94. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,299 @@
1
+ """
2
+ image_distortion part of pycroscopy
3
+
4
+ Author: Gerd Duscher
5
+
6
+ Distortions of scanned images are determined by comparing experimentally
7
+ obtained unit cells with ideal ones.
8
+
9
+ """
10
+ import numpy as np
11
+ import scipy
12
+ import skimage
13
+
14
+ import SimpleITK
15
+
16
+ from tqdm.auto import trange
17
+
18
+ ####################
19
+ # Distortion Matrix
20
+ ####################
21
+ def get_distortion_matrix(atoms: np.ndarray, ideal_lattice: np.ndarray) -> np.ndarray:
22
+ """ Calculates distortion matrix
23
+
24
+ Calculates the distortion matrix by comparing ideal and distorted Voronoi tiles
25
+ Parameters
26
+ ----------
27
+ atoms: numpy array (Nx2)
28
+ atomic positions
29
+ ideal_lattice: numpy array (Mx2)
30
+ ideal lattice positions
31
+
32
+ Returns
33
+ -------
34
+ numpy array (Nx4)
35
+ distortion matrix
36
+ """
37
+ vor = scipy.spatial.Voronoi(atoms)
38
+ # determine a middle Voronoi tile
39
+ ideal_vor = scipy.spatial.Voronoi(ideal_lattice)
40
+ near_center = np.average(ideal_lattice, axis=0)
41
+ index = np.argmin(np.linalg.norm(ideal_lattice - near_center, axis=0))
42
+
43
+ # the ideal vertices fo such an Voronoi tile (are there crystals with more than one voronoi?)
44
+ ideal_vertices = ideal_vor.vertices[ideal_vor.regions[ideal_vor.point_region[index]]]
45
+ ideal_vertices = get_significant_vertices(ideal_vertices - np.average(ideal_vertices, axis=0))
46
+
47
+ distortion_matrix = []
48
+ for index in trange(vor.points.shape[0]):
49
+
50
+ # determine vertices of Voronoi polygons of an atom with number index
51
+ poly_point = vor.points[index]
52
+ vertices = vor.vertices[vor.regions[vor.point_region[index]]]
53
+ poly_vertices = get_significant_vertices(vertices - poly_point)
54
+
55
+ # where ATOM has to be moved (not pixel)
56
+ ideal_point = ideal_lattice[index]
57
+
58
+ # transform voronoi to ideal one and keep transformation matrix A
59
+ uncorrected, corrected, _ = transform_voronoi(poly_vertices, ideal_vertices)
60
+
61
+ # pixel positions
62
+ corrected = corrected + ideal_point + (np.rint(poly_point) - poly_point)
63
+ for i in range(len(corrected)):
64
+ # original image pixels
65
+ x, y = uncorrected[i] + np.rint(poly_point)
66
+ # collect the two origin and target coordinates and store
67
+ distortion_matrix.append([x, y, corrected[i, 0], corrected[i, 1]])
68
+ print()
69
+ return np.array(distortion_matrix)
70
+
71
+
72
+ def undistort(distortion_matrix, image_data):
73
+ """ Undistort image according to distortion matrix
74
+
75
+ Uses the griddata interpolation of scipy to apply distortion matrix to image.
76
+ The distortion matrix contains in origin and target pixel coordinates
77
+ target is where the pixel has to be moved (floats)
78
+
79
+ Parameters
80
+ ----------
81
+ distortion_matrix: numpy array (Nx2)
82
+ distortion matrix (format N x 2)
83
+ image_data: numpy array or sidpy.Dataset
84
+ image
85
+
86
+ Returns
87
+ -------
88
+ interpolated: numpy array
89
+ undistorted image
90
+ """
91
+
92
+ intensity_values = image_data[(distortion_matrix[:, 0].astype(int),
93
+ distortion_matrix[:, 1].astype(int))]
94
+
95
+ corrected = distortion_matrix[:, 2:4]
96
+
97
+ size_x, size_y = 2 ** np.round(np.log2(image_data.shape[0:2])) # nearest power of 2
98
+ size_x = int(size_x)
99
+ size_y = int(size_y)
100
+ grid_x, grid_y = np.mgrid[0:size_x - 1:size_x * 1j, 0:size_y - 1:size_y * 1j]
101
+ print('interpolate')
102
+
103
+ interpolated = scipy.interpolate.griddata(np.array(corrected),
104
+ np.array(intensity_values),
105
+ (grid_x, grid_y), method='linear')
106
+ return interpolated
107
+
108
+
109
+ def transform_voronoi(vertices, ideal_voronoi):
110
+ """ find transformation matrix A between a distorted polygon and a
111
+ perfect reference one
112
+
113
+ Returns
114
+ -------
115
+ uncorrected: list of points:
116
+ all points on a grid within original polygon
117
+ corrected: list of points:
118
+ coordinates of these points where pixel have to move to
119
+ aa: 2x2 matrix A:
120
+ transformation matrix
121
+ """
122
+ # Find Transformation Matrix, note polygons have to be ordered first.
123
+ sort_vert = []
124
+ for vert in ideal_voronoi:
125
+ sort_vert.append(np.argmin(np.linalg.norm(vertices - vert, axis=1)))
126
+ vertices = np.array(vertices)[sort_vert]
127
+
128
+ # Solve the least squares problem X * A = Y
129
+ # to find our transformation matrix aa = A
130
+ aa, _, _, _ = np.linalg.lstsq(vertices, ideal_voronoi, rcond=None)
131
+
132
+ # expand polygon to include more points in distortion matrix
133
+ vertices2 = vertices + np.sign(vertices) # +np.sign(vertices)
134
+
135
+ ext_v = int(np.abs(vertices2).max() + 1)
136
+
137
+ polygon_grid = np.mgrid[0:ext_v * 2 + 1, :ext_v * 2 + 1] - ext_v
138
+ polygon_grid = np.swapaxes(polygon_grid, 0, 2)
139
+ polygon_array = polygon_grid.reshape(-1, polygon_grid.shape[-1])
140
+
141
+ p = skimage.measure.points_in_poly(polygon_array, vertices2)
142
+ uncorrected = polygon_array[p]
143
+ corrected = np.dot(uncorrected, aa)
144
+ return uncorrected, corrected, aa
145
+
146
+
147
+ def get_maximum_view(distortion_matrix: np.ndarray):
148
+ """
149
+ Determines the largest rectangular view within a distorted image matrix
150
+ ----------
151
+ distortion_matrix : np.ndarray
152
+ A 3D numpy array representing the distortion matrix of the image,
153
+ where invalid pixels are marked with -1000.
154
+ Returns
155
+ -------
156
+ np.ndarray
157
+ A 1D numpy array of four integers [row_start, row_end, col_start, col_end]
158
+ representing the coordinates of the maximal valid view within the
159
+ distortion matrix.
160
+ """
161
+ distortion_matrix_extent = np.ones(distortion_matrix.shape[1:], dtype=int)
162
+ distortion_matrix_extent[distortion_matrix[0] == -1000.] = 0
163
+
164
+ area = distortion_matrix_extent
165
+ view_square = np.array([0, distortion_matrix.shape[1] - 1, 0,
166
+ distortion_matrix.shape[2] - 1], dtype=int)
167
+ while np.array(np.where(area == 0)).shape[1] > 0:
168
+ view_square = view_square + [1, -1, 1, -1]
169
+ area = distortion_matrix_extent[view_square[0]:view_square[1],
170
+ view_square[2]:view_square[3]]
171
+
172
+ change = [-int(np.sum(np.min(distortion_matrix_extent[:view_square[0], view_square[2]:view_square[3]], axis=1))),
173
+ int(np.sum(np.min(distortion_matrix_extent[view_square[1]:, view_square[2]:view_square[3]], axis=1))),
174
+ -int(np.sum(np.min(distortion_matrix_extent[view_square[0]:view_square[1], :view_square[2]], axis=0))),
175
+ int(np.sum(np.min(distortion_matrix_extent[view_square[0]:view_square[1], view_square[3]:], axis=0)))]
176
+
177
+ return np.array(view_square) + change
178
+
179
+
180
+ def get_significant_vertices(vertices, distance=3):
181
+ """Calculate average for all points that are closer than distance apart,
182
+ otherwise leave the points alone
183
+
184
+ Parameters
185
+ ----------
186
+ vertices: numpy array (n,2)
187
+ list of points
188
+ distance: float
189
+ (in same scale as points )
190
+
191
+ Returns
192
+ -------
193
+ ideal_vertices: list of floats
194
+ list of points that are all a minimum of 3 apart.
195
+ """
196
+ tt = scipy.spatial.KDTree(np.array(vertices))
197
+ near = tt.query_ball_point(vertices, distance)
198
+ ideal_vertices = []
199
+ for indices in near:
200
+ if len(indices) == 1:
201
+ ideal_vertices.append(vertices[indices][0])
202
+ else:
203
+ ideal_vertices.append(np.average(vertices[indices], axis=0))
204
+ ideal_vertices = np.unique(np.array(ideal_vertices), axis=0)
205
+ angles = np.arctan2(ideal_vertices[:, 1], ideal_vertices[:, 0])
206
+ ang_sort = np.argsort(angles)
207
+ ideal_vertices = ideal_vertices[ang_sort]
208
+
209
+ return ideal_vertices
210
+
211
+
212
+ def undistort_sitk(image_data, distortion_matrix):
213
+ """ use simple ITK to undistort image
214
+
215
+ Parameters
216
+ ----------
217
+ image_data: numpy array with size NxM
218
+ distortion_matrix: sidpy.Dataset or numpy array with size 2 x P x Q
219
+ with P, Q >= M, N
220
+
221
+ Returns
222
+ -------
223
+ image: numpy array MXN
224
+
225
+ """
226
+ resampler = SimpleITK.ResampleImageFilter()
227
+ resampler.SetReferenceImage(SimpleITK.GetImageFromArray(image_data))
228
+ resampler.SetInterpolator(SimpleITK.sitkBSpline)
229
+ resampler.SetDefaultPixelValue(0)
230
+
231
+ distortion_matrix2 = distortion_matrix[:, :image_data.shape[0], :image_data.shape[1]]
232
+
233
+ displ2 = SimpleITK.Compose([SimpleITK.GetImageFromArray(-distortion_matrix2[1]),
234
+ SimpleITK.GetImageFromArray(-distortion_matrix2[0])])
235
+ out_tx = SimpleITK.DisplacementFieldTransform(displ2)
236
+ resampler.SetTransform(out_tx)
237
+ out = resampler.Execute(SimpleITK.GetImageFromArray(image_data))
238
+ return SimpleITK.GetArrayFromImage(out)
239
+
240
+
241
+ def undistort_stack_sitk(distortion_matrix, image_stack):
242
+ """
243
+ use simple ITK to undistort stack of image
244
+ input:
245
+ image: numpy array with size NxM
246
+ distortion_matrix: h5 Dataset or numpy array with size 2 x P x Q
247
+ with P, Q >= M, N
248
+ output:
249
+ image M, N
250
+ """
251
+ resampler = SimpleITK.ResampleImageFilter()
252
+ resampler.SetReferenceImage(SimpleITK.GetImageFromArray(image_stack[0]))
253
+ resampler.SetInterpolator(SimpleITK.sitkBSpline)
254
+ resampler.SetDefaultPixelValue(0)
255
+
256
+ displ2 = SimpleITK.Compose([SimpleITK.GetImageFromArray(-distortion_matrix[1]),
257
+ SimpleITK.GetImageFromArray(-distortion_matrix[0])])
258
+ out_tx = SimpleITK.DisplacementFieldTransform(displ2)
259
+ resampler.SetTransform(out_tx)
260
+ interpolated = np.zeros(image_stack.shape)
261
+ nimages = image_stack.shape[0]
262
+ for i in trange(nimages):
263
+ out = resampler.Execute(SimpleITK.GetImageFromArray(image_stack[i]))
264
+ interpolated[i] = SimpleITK.GetArrayFromImage(out)
265
+ return interpolated
266
+
267
+
268
+ def undistort_stack(distortion_matrix, data):
269
+ """ Undistort stack with distortion matrix
270
+
271
+ Use the griddata interpolation of scipy to apply distortion matrix to image
272
+ The distortion matrix contains in each pixel where the pixel has to be
273
+ moved (floats)
274
+
275
+ Parameters
276
+ ----------
277
+ distortion_matrix: numpy array
278
+ distortion matrix to undistort image (format image.shape[0],
279
+ image.shape[2], 2)
280
+ data: numpy array or sidpy.Dataset
281
+ image
282
+ """
283
+ corrected = distortion_matrix[:, 2:4]
284
+ intensity_values = data[:, distortion_matrix[:, 0].astype(int),
285
+ distortion_matrix[:, 1].astype(int)]
286
+ size_x, size_y = 2 ** np.round(np.log2(data.shape[1:])) # nearest power of 2
287
+ size_x = int(size_x)
288
+ size_y = int(size_y)
289
+
290
+ grid_x, grid_y = np.mgrid[0:size_x - 1:size_x * 1j, 0:size_y - 1:size_y * 1j]
291
+ interpolated = np.zeros([data.shape[0], size_x, size_y])
292
+ nimages = data.shape[0]
293
+ for i in trange(nimages):
294
+ interpolated[i, :, :] = scipy.interpolate.griddata(corrected,
295
+ intensity_values[i, :],
296
+ (grid_x, grid_y),
297
+ method='linear')
298
+ print(':-) \n You have successfully completed undistortion of image stack')
299
+ return interpolated
@@ -0,0 +1,277 @@
1
+ """
2
+ Image_fft
3
+ part of pycroscopy
4
+
5
+ author: Gerd Duscher, UTK
6
+
7
+ """
8
+
9
+ import itertools
10
+ import collections
11
+
12
+ import numpy as np
13
+ import scipy
14
+ import sklearn
15
+
16
+ import sidpy
17
+
18
+ def fourier_transform(dset: sidpy.Dataset) -> sidpy.Dataset:
19
+ """
20
+ Reads information into dictionary 'tags', performs 'FFT', and provides
21
+ a smoothed FT and reciprocal and intensity limits for visualization.
22
+
23
+ Parameters
24
+ ----------
25
+ dset: sidpy.Dataset
26
+ image
27
+
28
+ Returns
29
+ -------
30
+ fft_dset: sidpy.Dataset
31
+ Fourier transform with correct dimensions
32
+
33
+ Example
34
+ -------
35
+ >>> fft_dataset = fourier_transform(sidpy_dataset)
36
+ >>> fft_dataset.plot()
37
+ """
38
+
39
+ assert isinstance(dset, sidpy.Dataset), 'Expected a sidpy Dataset'
40
+
41
+ selection = []
42
+ image_dims = dset.get_image_dims(return_axis=True)
43
+ if dset.data_type.name == 'IMAGE_STACK':
44
+ stack_dim = dset.get_dimensions_by_type('TEMPORAL')
45
+
46
+ if len(image_dims) != 2:
47
+ raise ValueError('need at least two SPATIAL dimension for an image stack')
48
+
49
+ for i in range(dset.ndim):
50
+ if i in image_dims:
51
+ selection.append(slice(None))
52
+ if len(stack_dim) == 0:
53
+ selection.append(slice(None))
54
+ elif i in stack_dim:
55
+ selection.append(slice(None))
56
+ else:
57
+ selection.append(slice(0, 1))
58
+
59
+ image_stack = np.squeeze(np.array(dset)[selection])
60
+ new_image = np.sum(np.array(image_stack), axis=stack_dim)
61
+ elif dset.data_type.name == 'IMAGE':
62
+ new_image = np.array(dset)
63
+ else:
64
+ return None
65
+
66
+ new_image = new_image - new_image.min()
67
+
68
+ fft_transform = (np.fft.fftshift(np.fft.fft2(np.array(new_image))))
69
+
70
+ image_dims = dset.get_image_dims(return_axis=True)
71
+
72
+ units_x = '1/' + image_dims[0].units
73
+ units_y = '1/' + image_dims[1].units
74
+
75
+ fft_dset = sidpy.Dataset.from_array(fft_transform)
76
+ fft_dset.quantity = dset.quantity
77
+ fft_dset.units = 'a.u.'
78
+ fft_dset.data_type = 'IMAGE'
79
+ fft_dset.source = dset.title
80
+ fft_dset.modality = 'fft'
81
+
82
+ fft_dset.set_dimension(0, sidpy.Dimension(np.fft.fftshift(np.fft.fftfreq(new_image.shape[0],
83
+ d=dset.x[1]-dset.x[0])),
84
+ name='u', units=units_x, dimension_type='RECIPROCAL',
85
+ quantity='reciprocal_length'))
86
+ fft_dset.set_dimension(1, sidpy.Dimension(np.fft.fftshift(np.fft.fftfreq(new_image.shape[1],
87
+ d=dset.y[1]- dset.y[0])),
88
+ name='v', units=units_y, dimension_type='RECIPROCAL',
89
+ quantity='reciprocal_length'))
90
+ return fft_dset
91
+
92
+
93
+ def power_spectrum(dset: sidpy.Dataset, smoothing: int=3) -> sidpy.Dataset:
94
+ """
95
+ Calculate power spectrum
96
+
97
+ Parameters
98
+ ----------
99
+ dset: sidpy.Dataset
100
+ image
101
+ smoothing: int
102
+ Gaussian smoothing
103
+
104
+ Returns
105
+ -------
106
+ power_spec: sidpy.Dataset
107
+ power spectrum with correct dimensions
108
+
109
+ """
110
+
111
+ fft_transform = fourier_transform(dset) # dset.fft()
112
+ fft_mag = np.abs(fft_transform)
113
+ fft_mag2 = scipy.ndimage.gaussian_filter(fft_mag, sigma=(smoothing, smoothing), order=0)
114
+
115
+ power_spec = fft_transform.like_data(np.log(1.+fft_mag2))
116
+
117
+ # prepare mask
118
+ x, y = np.meshgrid(power_spec.v.values, power_spec.u.values)
119
+ mask = np.zeros(power_spec.shape)
120
+
121
+ mask_spot = x ** 2 + y ** 2 > 1 ** 2
122
+ mask = mask + mask_spot
123
+ mask_spot = x ** 2 + y ** 2 < 11 ** 2
124
+ mask = mask + mask_spot
125
+
126
+ mask[np.where(mask == 1)] = 0 # just in case of overlapping disks
127
+
128
+ minimum_intensity = np.array(power_spec)[np.where(mask == 2)].min() * 0.95
129
+ maximum_intensity = np.array(power_spec)[np.where(mask == 2)].max() * 1.05
130
+ power_spec.metadata = {'fft': {'smoothing': smoothing,
131
+ 'minimum_intensity': minimum_intensity,
132
+ 'maximum_intensity': maximum_intensity}}
133
+ power_spec.title = 'power spectrum ' + power_spec.source
134
+ return power_spec
135
+
136
+
137
+ def diffractogram_spots(dset: sidpy.Dataset,
138
+ spot_threshold: float,
139
+ return_center: bool = True,
140
+ eps: float=0.1) -> tuple[np.ndarray, np.ndarray | list[float]]:
141
+ """Find spots in diffractogram and sort them by distance from center
142
+
143
+ Uses blob_log from scipy.spatial
144
+
145
+ Parameters
146
+ ----------
147
+ dset: sidpy.Dataset
148
+ diffractogram
149
+ spot_threshold: float
150
+ threshold for blob finder
151
+ return_center: bool, optional
152
+ return center of image if true
153
+ eps: float, optional
154
+ threshold for blob finder
155
+
156
+ Returns
157
+ -------
158
+ spots: numpy array
159
+ sorted position (x,y) and radius (r) of all spots
160
+ """
161
+
162
+ # spot detection (for future reference there is no symmetry assumed here)
163
+ data = np.array(np.log(1+np.abs(dset)))
164
+ data = data - data.min()
165
+ data = data/data.max()
166
+ # some images are strange and blob_log does not work on the power spectrum
167
+ try:
168
+ spots_random = scipy.features.blob_log(data, max_sigma=5, threshold=spot_threshold)
169
+ except ValueError:
170
+ spots_random = scipy.features.peak_local_max(np.array(data.T),
171
+ min_distance=3,
172
+ threshold_rel=spot_threshold)
173
+ spots_random = np.hstack(spots_random, np.zeros((spots_random.shape[0], 1)))
174
+
175
+ # print(f'Found {spots_random.shape[0]} reflections')
176
+
177
+ # Needed for conversion from pixel to Reciprocal space
178
+ image_dims = dset.get_image_dims(return_axis=True)
179
+ rec_scale = np.array([image_dims[0].slope, image_dims[1].slope])
180
+
181
+ spots_random[:, :2] = spots_random[:, :2]*rec_scale+[dset.u.values[0], dset.v.values[0]]
182
+ # sort reflections
183
+ spots_random[:, 2] = np.linalg.norm(spots_random[:, 0:2], axis=1)
184
+ spots_index = np.argsort(spots_random[:, 2])
185
+ spots = spots_random[spots_index]
186
+ # third row is angles
187
+ spots[:, 2] = np.arctan2(spots[:, 0], spots[:, 1])
188
+
189
+ center = [0, 0]
190
+
191
+ if return_center:
192
+ points = spots[:, 0:2]
193
+
194
+ # Calculate the midpoints between all points
195
+ reshaped_points = points[:, np.newaxis, :]
196
+ midpoints = (reshaped_points + reshaped_points.transpose(1, 0, 2)) / 2.0
197
+ midpoints = midpoints.reshape(-1, 2)
198
+
199
+ # Find the most dense cluster of midpoints
200
+ dbscan = sklearn.cluster.DBSCAN(eps=eps, min_samples=2)
201
+ labels = dbscan.fit_predict(midpoints)
202
+ cluster_counter = collections.Counter(labels)
203
+ largest_cluster_label = max(cluster_counter, key=cluster_counter.get)
204
+ largest_cluster_points = midpoints[labels == largest_cluster_label]
205
+
206
+ # Average of these midpoints must be the center
207
+ center = np.mean(largest_cluster_points, axis=0)
208
+
209
+ return spots, center
210
+
211
+
212
+ def adaptive_fourier_filter(dset: sidpy.Dataset,
213
+ spots: np.ndarray,
214
+ low_pass: float = 3,
215
+ reflection_radius: float = 0.3) -> sidpy.Dataset:
216
+ """
217
+ Use spots in diffractogram for a Fourier Filter
218
+
219
+ Parameters:
220
+ -----------
221
+ dset: sidpy.Dataset
222
+ image to be filtered
223
+ spots: np.ndarray(N,2)
224
+ sorted spots in diffractogram in 1/nm
225
+ low_pass: float
226
+ low pass filter in center of diffractogram in 1/nm
227
+ reflection_radius: float
228
+ radius of masked reflections in 1/nm
229
+
230
+ Output:
231
+ -------
232
+ Fourier filtered image
233
+ """
234
+
235
+ fft_transform = fourier_transform(dset)
236
+
237
+ # prepare mask
238
+ x, y = np.meshgrid(fft_transform.v.values, fft_transform.u.values)
239
+ mask = np.zeros(dset.shape)
240
+
241
+ # mask reflections
242
+ for spot in spots:
243
+ mask_spot = (x - spot[1]) ** 2 + (y - spot[0]) ** 2 < reflection_radius ** 2 # make a spot
244
+ mask = mask + mask_spot # add spot to mask
245
+
246
+ # mask zero region larger (low-pass filter = intensity variations)
247
+ mask_spot = x ** 2 + y ** 2 < low_pass ** 2
248
+ mask = mask + mask_spot
249
+ mask[np.where(mask > 1)] = 1
250
+ fft_filtered = np.array(fft_transform * mask)
251
+
252
+ filtered_image = dset.like_data(np.fft.ifft2(np.fft.fftshift(fft_filtered)).real)
253
+ filtered_image.title = 'Fourier filtered ' + dset.title
254
+ filtered_image.source = dset.title
255
+ filtered_image.metadata = {'analysis': 'adaptive fourier filtered', 'spots': spots,
256
+ 'low_pass': low_pass, 'reflection_radius': reflection_radius}
257
+ return filtered_image
258
+
259
+
260
+ def rotational_symmetry_diffractogram(spots: np.ndarray) -> list[int]:
261
+ """ Test rotational symmetry of diffraction spots"""
262
+
263
+ rotation_symmetry = []
264
+ for n in [2, 3, 4, 6]:
265
+ cc = np.array(
266
+ [[np.cos(2 * np.pi / n), np.sin(2 * np.pi / n), 0],
267
+ [-np.sin(2 * np.pi / n), np.cos(2 * np.pi / n), 0],
268
+ [0, 0, 1]])
269
+ sym_spots = np.dot(spots, cc)
270
+ dif = []
271
+ for p0, p1 in itertools.product(sym_spots[:, 0:2], spots[:, 0:2]):
272
+ dif.append(np.linalg.norm(p0 - p1))
273
+ dif = np.array(sorted(dif))
274
+
275
+ if dif[int(spots.shape[0] * .7)] < 0.2:
276
+ rotation_symmetry.append(n)
277
+ return rotation_symmetry