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.
- 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 -928
- 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.2.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.2.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.2.dist-info/RECORD +0 -38
- pytemlib-0.2025.4.2.dist-info/top_level.txt +0 -1
- {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/entry_points.txt +0 -0
- {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
|