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,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
image_registration.py
|
|
3
|
+
by Gerd Duscher, UTK
|
|
4
|
+
part of pycroscopy.image
|
|
5
|
+
MIT license except where stated differently
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import skimage
|
|
12
|
+
import scipy
|
|
13
|
+
|
|
14
|
+
from tqdm.auto import trange
|
|
15
|
+
|
|
16
|
+
import sidpy
|
|
17
|
+
_SIMPLEITK_PRESENT = True
|
|
18
|
+
try:
|
|
19
|
+
import SimpleITK
|
|
20
|
+
except ModuleNotFoundError:
|
|
21
|
+
_SIMPLEITK_PRESENT = False
|
|
22
|
+
if not _SIMPLEITK_PRESENT:
|
|
23
|
+
print('SimpleITK not installed; Registration Functions for Image Stacks not available')
|
|
24
|
+
|
|
25
|
+
#####################################################
|
|
26
|
+
# Registration Functions
|
|
27
|
+
#####################################################
|
|
28
|
+
|
|
29
|
+
def complete_registration(main_dataset: sidpy.Dataset) -> typing.Tuple[sidpy.Dataset,
|
|
30
|
+
sidpy.Dataset]:
|
|
31
|
+
"""Rigid and then non-rigid (demon) registration
|
|
32
|
+
|
|
33
|
+
Performs rigid and then non-rigid registration, please see individual functions:
|
|
34
|
+
- rigid_registration
|
|
35
|
+
- demon_registration
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
main_dataset: sidpy.Dataset
|
|
40
|
+
dataset of data_type 'IMAGE_STACK' to be registered
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
non_rigid_registered: sidpy.Dataset
|
|
45
|
+
rigid_registered_dataset: sidpy.Dataset
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
if main_dataset.data_type.name != 'IMAGE_STACK':
|
|
50
|
+
raise TypeError('Registration makes only sense for an image stack')
|
|
51
|
+
|
|
52
|
+
rigid_registered_dataset = rigid_registration(main_dataset)
|
|
53
|
+
|
|
54
|
+
rigid_registered_dataset.data_type = 'IMAGE_STACK'
|
|
55
|
+
|
|
56
|
+
non_rigid_registered = demon_registration(rigid_registered_dataset)
|
|
57
|
+
return non_rigid_registered, rigid_registered_dataset
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def demon_registration(dataset: sidpy.Dataset, verbose: bool=False) -> sidpy.Dataset:
|
|
61
|
+
"""
|
|
62
|
+
Diffeomorphic Demon Non-Rigid Registration
|
|
63
|
+
|
|
64
|
+
Depends on:
|
|
65
|
+
simpleITK and numpy
|
|
66
|
+
Please Cite: http://www.simpleitk.org/SimpleITK/project/parti.html
|
|
67
|
+
and T. Vercauteren, X. Pennec, A. Perchant and N. Ayache
|
|
68
|
+
Diffeomorphic Demons Using ITK\'s Finite Difference Solver Hierarchy
|
|
69
|
+
The Insight Journal, http://hdl.handle.net/1926/510 2007
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
dataset: sidpy.Dataset
|
|
74
|
+
stack of image after rigid registration and cropping
|
|
75
|
+
verbose: boolean
|
|
76
|
+
optional for increased output
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
dem_reg: stack of images with non-rigid registration
|
|
80
|
+
|
|
81
|
+
Example
|
|
82
|
+
-------
|
|
83
|
+
dem_reg = demon_reg(stack_dataset, verbose=False)
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
if dataset.data_type.name != 'IMAGE_STACK':
|
|
87
|
+
raise TypeError('Registration makes only sense for an image stack')
|
|
88
|
+
|
|
89
|
+
dem_reg = np.zeros(dataset.shape)
|
|
90
|
+
nimages = dataset.shape[0]
|
|
91
|
+
if verbose:
|
|
92
|
+
print(nimages)
|
|
93
|
+
# create fixed image by summing over rigid registration
|
|
94
|
+
|
|
95
|
+
fixed_np = np.average(np.array(dataset), axis=0)
|
|
96
|
+
|
|
97
|
+
if not _SIMPLEITK_PRESENT:
|
|
98
|
+
print('This feature is not available:')
|
|
99
|
+
print('Please install simpleITK with: conda install simpleitk -c simpleitk')
|
|
100
|
+
|
|
101
|
+
fixed = SimpleITK.GetImageFromArray(fixed_np)
|
|
102
|
+
fixed = SimpleITK.DiscreteGaussian(fixed, 2.0)
|
|
103
|
+
|
|
104
|
+
demons = SimpleITK.DiffeomorphicDemonsRegistrationFilter()
|
|
105
|
+
|
|
106
|
+
demons.SetNumberOfIterations(200)
|
|
107
|
+
demons.SetStandardDeviations(1.0)
|
|
108
|
+
|
|
109
|
+
resampler = SimpleITK.ResampleImageFilter()
|
|
110
|
+
resampler.SetReferenceImage(fixed)
|
|
111
|
+
resampler.SetInterpolator(SimpleITK.sitkBSpline)
|
|
112
|
+
resampler.SetDefaultPixelValue(0)
|
|
113
|
+
|
|
114
|
+
for i in trange(nimages):
|
|
115
|
+
moving = SimpleITK.GetImageFromArray(dataset[i])
|
|
116
|
+
moving_f = SimpleITK.DiscreteGaussian(moving, 2.0)
|
|
117
|
+
displacement_field = demons.Execute(fixed, moving_f)
|
|
118
|
+
out_tx = SimpleITK.DisplacementFieldTransform(displacement_field)
|
|
119
|
+
resampler.SetTransform(out_tx)
|
|
120
|
+
out = resampler.Execute(moving)
|
|
121
|
+
dem_reg[i, :, :] = SimpleITK.GetArrayFromImage(out)
|
|
122
|
+
|
|
123
|
+
print(':-)')
|
|
124
|
+
print('You have successfully completed Diffeomorphic Demons Registration')
|
|
125
|
+
|
|
126
|
+
demon_registered = dataset.like_data(dem_reg)
|
|
127
|
+
demon_registered.title = 'Non-Rigid Registration'
|
|
128
|
+
demon_registered.source = dataset.title
|
|
129
|
+
|
|
130
|
+
demon_registered.metadata =dataset.metadata.copy()
|
|
131
|
+
if 'analysis' not in demon_registered.metadata:
|
|
132
|
+
demon_registered.metadata['analysis'] = {}
|
|
133
|
+
demon_registered.metadata['analysis']['non_rigid_demon_registration'] = {'package': 'simpleITK',
|
|
134
|
+
'method': 'DiscreteGaussian',
|
|
135
|
+
'variance': 2,
|
|
136
|
+
'input_dataset': dataset.source}
|
|
137
|
+
demon_registered.data_type = 'IMAGE_STACK'
|
|
138
|
+
return demon_registered
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ##############################
|
|
142
|
+
# Rigid Registration New 05/09/2024
|
|
143
|
+
# ##############################
|
|
144
|
+
def rigid_registration(dataset: sidpy.Dataset, normalization: typing.Optional[str] = None) -> sidpy.Dataset:
|
|
145
|
+
"""
|
|
146
|
+
Rigid registration of image stack with pixel accuracy
|
|
147
|
+
|
|
148
|
+
Uses simple cross_correlation
|
|
149
|
+
(we determine drift from one image to next)
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
dataset: sidpy.Dataset
|
|
154
|
+
sidpy dataset with image_stack dataset
|
|
155
|
+
normalization: str or None
|
|
156
|
+
if 'phase' then phase cross correlation is used, otherwise
|
|
157
|
+
normalized cross correlation is used
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
rigid_registered: sidpy.Dataset
|
|
162
|
+
Registered Stack and drift (with respect to center image)
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
if dataset.data_type.name != 'IMAGE_STACK':
|
|
166
|
+
raise TypeError('Registration makes only sense for an image stack')
|
|
167
|
+
|
|
168
|
+
if isinstance (normalization, str):
|
|
169
|
+
if normalization.lower() != 'phase':
|
|
170
|
+
normalization = None
|
|
171
|
+
else:
|
|
172
|
+
normalization = None
|
|
173
|
+
image_dimensions = dataset.get_image_dims(return_axis=True)
|
|
174
|
+
if dataset.get_dimensions_by_type('TEMPORAL')[0] != 0:
|
|
175
|
+
x = image_dimensions[0]
|
|
176
|
+
y = image_dimensions[1]
|
|
177
|
+
z = dataset.get_dimensions_by_type('TEMPORAL', return_axis=True)[0]
|
|
178
|
+
metadata = dataset.metadata.copy()
|
|
179
|
+
original_metadata = dataset.original_metadata.copy()
|
|
180
|
+
arr = np.rollaxis(np.array(dataset), 2, 0)
|
|
181
|
+
dataset = sidpy.Dataset.from_array(arr, title=dataset.title, data_type='IMAGE_STACK',
|
|
182
|
+
quantity=dataset.quantity, units=dataset.units)
|
|
183
|
+
dataset.set_dimension(0, sidpy.Dimension(z.values, name='frame', units='frame', quantity='time',
|
|
184
|
+
dimension_type='temporal'))
|
|
185
|
+
dataset.set_dimension(1, x)
|
|
186
|
+
dataset.set_dimension(2, y)
|
|
187
|
+
dataset.metadata = metadata
|
|
188
|
+
dataset.original_metadata = original_metadata
|
|
189
|
+
|
|
190
|
+
stack_dim = dataset.get_dimensions_by_type('TEMPORAL', return_axis=True)[0]
|
|
191
|
+
image_dim = dataset.get_image_dims(return_axis=True)
|
|
192
|
+
if len(image_dim) != 2:
|
|
193
|
+
raise ValueError('need at least two SPATIAL dimension for an image stack')
|
|
194
|
+
|
|
195
|
+
relative_drift = [[0., 0.]]
|
|
196
|
+
im1 = np.fft.fft2(np.array(dataset[0]))
|
|
197
|
+
for i in range(1, len(stack_dim)):
|
|
198
|
+
im2 = np.fft.fft2(np.array(dataset[i]))
|
|
199
|
+
shift, error, _ = skimage.registration.phase_cross_correlation(im1, im2,
|
|
200
|
+
normalization=normalization,
|
|
201
|
+
space='fourier')
|
|
202
|
+
im1 = im2.copy()
|
|
203
|
+
relative_drift.append(shift)
|
|
204
|
+
|
|
205
|
+
rig_reg, drift = rig_reg_drift(dataset, relative_drift)
|
|
206
|
+
crop_reg, input_crop = crop_image_stack(rig_reg, drift)
|
|
207
|
+
|
|
208
|
+
rigid_registered = sidpy.Dataset.from_array(crop_reg,
|
|
209
|
+
title='Rigid Registration',
|
|
210
|
+
data_type='IMAGE_STACK',
|
|
211
|
+
quantity=dataset.quantity,
|
|
212
|
+
units=dataset.units)
|
|
213
|
+
rigid_registered.title = 'Rigid_Registration'
|
|
214
|
+
rigid_registered.source = dataset.title
|
|
215
|
+
rigid_registered.metadata['analysis'] = {'rigid_registration': {'drift': drift,
|
|
216
|
+
'input_crop': input_crop, 'input_shape': dataset.shape[1:]}}
|
|
217
|
+
|
|
218
|
+
if 'experiment' in dataset.metadata:
|
|
219
|
+
rigid_registered.metadata['experiment'] = dataset.metadata['experiment'].copy()
|
|
220
|
+
rigid_registered.set_dimension(0, sidpy.Dimension(np.arange(rigid_registered.shape[0]),
|
|
221
|
+
name='frame', units='frame', quantity='time',
|
|
222
|
+
dimension_type='temporal'))
|
|
223
|
+
|
|
224
|
+
array_x = image_dim[0].values[input_crop[0]:input_crop[1]]
|
|
225
|
+
rigid_registered.set_dimension(1, sidpy.Dimension(array_x, name='x',
|
|
226
|
+
units='nm', quantity='Length',
|
|
227
|
+
dimension_type='spatial'))
|
|
228
|
+
array_y =image_dim[1].values[input_crop[2]:input_crop[3]]
|
|
229
|
+
rigid_registered.set_dimension(2, sidpy.Dimension(array_y, name='y',
|
|
230
|
+
units='nm', quantity='Length',
|
|
231
|
+
dimension_type='spatial'))
|
|
232
|
+
rigid_registered.data_type = 'IMAGE_STACK'
|
|
233
|
+
return rigid_registered.rechunk({0: 'auto', 1: -1, 2: -1})
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def rig_reg_drift(dset: sidpy.Dataset,
|
|
237
|
+
rel_drift: typing.Union[typing.List[typing.List[float]], np.ndarray]
|
|
238
|
+
) -> typing.Tuple[np.ndarray, np.ndarray]:
|
|
239
|
+
""" Shifting images on top of each other
|
|
240
|
+
|
|
241
|
+
Uses relative drift to shift images on top of each other,
|
|
242
|
+
with center image as reference.
|
|
243
|
+
Shifting is done with shift routine of ndimage from scipy.
|
|
244
|
+
This function is used by rigid_registration routine
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
dset: sidpy.Dataset
|
|
249
|
+
dataset with image_stack
|
|
250
|
+
rel_drift:
|
|
251
|
+
relative_drift from image to image as list of [shiftx, shifty]
|
|
252
|
+
|
|
253
|
+
Returns
|
|
254
|
+
-------
|
|
255
|
+
stack: numpy array
|
|
256
|
+
drift: list of drift in pixel
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
frame_dim = []
|
|
260
|
+
spatial_dim = []
|
|
261
|
+
selection = []
|
|
262
|
+
|
|
263
|
+
for i, axis in dset._axes.items():
|
|
264
|
+
if axis.dimension_type.name == 'SPATIAL':
|
|
265
|
+
spatial_dim.append(i)
|
|
266
|
+
selection.append(slice(None))
|
|
267
|
+
else:
|
|
268
|
+
frame_dim.append(i)
|
|
269
|
+
selection.append(slice(0, 1))
|
|
270
|
+
|
|
271
|
+
if len(spatial_dim) != 2:
|
|
272
|
+
print('need two spatial dimensions')
|
|
273
|
+
if len(frame_dim) != 1:
|
|
274
|
+
print('need one frame dimensions')
|
|
275
|
+
|
|
276
|
+
rig_reg = np.zeros([dset.shape[frame_dim[0]], dset.shape[spatial_dim[0]], dset.shape[spatial_dim[1]]])
|
|
277
|
+
|
|
278
|
+
# absolute drift
|
|
279
|
+
drift = np.array(rel_drift).copy()
|
|
280
|
+
|
|
281
|
+
drift[0] = [0, 0]
|
|
282
|
+
for i in range(1, drift.shape[0]):
|
|
283
|
+
drift[i] = drift[i - 1] + rel_drift[i]
|
|
284
|
+
center_drift = drift[int(drift.shape[0] / 2)]
|
|
285
|
+
drift = drift - center_drift
|
|
286
|
+
# Shift images
|
|
287
|
+
for i in range(rig_reg.shape[0]):
|
|
288
|
+
selection[frame_dim[0]] = slice(i, i+1)
|
|
289
|
+
# Now we shift
|
|
290
|
+
rig_reg[i, :, :] = scipy.ndimage.shift(dset[tuple(selection)].squeeze().compute(),
|
|
291
|
+
[drift[i, 0], drift[i, 1]], order=3)
|
|
292
|
+
return rig_reg, drift
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def crop_image_stack(rig_reg: np.ndarray, drift: typing.Union[np.ndarray, list]
|
|
297
|
+
) -> typing.Tuple[np.ndarray, list[int]]:
|
|
298
|
+
"""Crop images in stack according to drift
|
|
299
|
+
|
|
300
|
+
This function is used by rigid_registration routine
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
rig_reg: numpy array (N,x,y)
|
|
305
|
+
drift: list (2,B)
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
numpy array
|
|
310
|
+
"""
|
|
311
|
+
xpmax = int(rig_reg.shape[1] - -np.floor(np.min(np.array(drift)[:, 0])))
|
|
312
|
+
xpmin = int(np.ceil(np.max(np.array(drift)[:, 0])))
|
|
313
|
+
ypmax = int(rig_reg.shape[1] - -np.floor(np.min(np.array(drift)[:, 1])))
|
|
314
|
+
ypmin = int(np.ceil(np.max(np.array(drift)[:, 1])))
|
|
315
|
+
|
|
316
|
+
return rig_reg[:, xpmin:xpmax, ypmin:ypmax:], [xpmin, xpmax, ypmin, ypmax]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
image_utilities part of image package of pycroscopy
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import scipy
|
|
7
|
+
from skimage.restoration import inpaint
|
|
8
|
+
|
|
9
|
+
import sidpy
|
|
10
|
+
|
|
11
|
+
def crop_image(dataset: sidpy.Dataset, corners: np.ndarray) -> sidpy.Dataset:
|
|
12
|
+
"""
|
|
13
|
+
Crops an image according to the corners given in the format of
|
|
14
|
+
matplotlib.widget.RectangleSelector.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
dataset: sidpy.Dataset
|
|
19
|
+
An instance of sidpy.Dataset representing the image to be cropped.
|
|
20
|
+
corners: np.ndarray
|
|
21
|
+
A 1D array of length 4 containing the corners of the rectangular region
|
|
22
|
+
to be cropped. The order of the corners should be (x1, y1, x2, y2).
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
sidpy.Dataset
|
|
27
|
+
A new instance of sidpy.Dataset representing the cropped image.
|
|
28
|
+
|
|
29
|
+
Raises
|
|
30
|
+
------
|
|
31
|
+
ValueError
|
|
32
|
+
If dataset is not an instance of sidpy.Dataset or if dataset is not an image
|
|
33
|
+
dataset. If corners parameter is not of correct shape or size.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(dataset, sidpy.Dataset):
|
|
37
|
+
raise ValueError('Input dataset is not an instance of sidpy.Dataset')
|
|
38
|
+
if not dataset.data_type.name == 'IMAGE':
|
|
39
|
+
raise ValueError('Only image datasets are supported at this point')
|
|
40
|
+
|
|
41
|
+
if corners.shape != (4,):
|
|
42
|
+
raise ValueError(f'Input corners parameter should have shape (4,) but got shape {corners.shape}')
|
|
43
|
+
if corners[2]-corners[0] <= 0 or corners[3]-corners[1] <= 0:
|
|
44
|
+
raise ValueError('Invalid input corners parameter')
|
|
45
|
+
|
|
46
|
+
pixel_size = np.array([dataset.x[1]-dataset.x[0], dataset.y[1]-dataset.y[0]])
|
|
47
|
+
corners /= pixel_size
|
|
48
|
+
|
|
49
|
+
selection = np.stack([np.min(corners[:2])+0.5, np.max(corners[2:])+0.5]).astype(int)
|
|
50
|
+
|
|
51
|
+
cropped_dset = dataset.like_data(dataset[selection[0, 0]:selection[1, 0],
|
|
52
|
+
selection[0, 1]:selection[1, 1]])
|
|
53
|
+
cropped_dset.title = 'cropped_' + dataset.title
|
|
54
|
+
cropped_dset.source = dataset.title
|
|
55
|
+
cropped_dset.metadata = {'crop_dimension': selection, 'original_dimensions': dataset.shape}
|
|
56
|
+
|
|
57
|
+
return cropped_dset
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def flatten_image(sid_dset, order=1, flatten_axis = 'row', method = 'line_fit'):
|
|
61
|
+
"""
|
|
62
|
+
Flattens an image according to the method chosen. Used heavily for AFM/STM images
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
dataset: sidpy.Dataset
|
|
67
|
+
An instance of sidpy.Dataset representing the image to be flattened.
|
|
68
|
+
order: integer,
|
|
69
|
+
Optional, default = 1. Ordfor the polynomial fit.
|
|
70
|
+
flatten_axis: string,
|
|
71
|
+
Optional, default = 'row'. Axis along which to flatten the image.
|
|
72
|
+
method: string,
|
|
73
|
+
Optional, default = 'line_fit'. Method to use for flattening the image.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
sidpy.Dataset
|
|
78
|
+
A new instance of sidpy.Dataset representing the flattened image.
|
|
79
|
+
"""
|
|
80
|
+
#TODO: lots of cleanup in this function required...
|
|
81
|
+
new_sid_dset = sid_dset.copy()
|
|
82
|
+
assert len(new_sid_dset._axes) == 2, "Dataset must be 2-D for this function"
|
|
83
|
+
assert new_sid_dset.data_type == sidpy.DataType.IMAGE, "Dataset must IMAGE for this function"
|
|
84
|
+
#check the spatial dimensions, flatten along each row
|
|
85
|
+
if flatten_axis == 'row':
|
|
86
|
+
num_pts = sid_dset.shape[0] # this is hard coded, it shouldn't be
|
|
87
|
+
elif flatten_axis == 'col':
|
|
88
|
+
num_pts = sid_dset.shape[1] # this is hard coded, but it shouldn't be
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError(f"Gave flatten axis of {flatten_axis} but only 'row', 'col' are allowed")
|
|
91
|
+
|
|
92
|
+
data_flat = np.zeros(sid_dset.shape) #again this should be the spatial (2 dimensional) part only
|
|
93
|
+
print(sid_dset.shape, num_pts)
|
|
94
|
+
if method == 'line_fit':
|
|
95
|
+
for line in range(num_pts):
|
|
96
|
+
if flatten_axis=='row':
|
|
97
|
+
line_data = np.array(sid_dset[:])[line,:]
|
|
98
|
+
elif flatten_axis=='col':
|
|
99
|
+
line_data = np.array(sid_dset[:])[:,line]
|
|
100
|
+
p = np.polyfit(np.arange(len(line_data)), line_data,order)
|
|
101
|
+
lin_est = np.polyval(p,np.arange(len(line_data)))
|
|
102
|
+
new_line = line_data - lin_est
|
|
103
|
+
data_flat[line] = new_line
|
|
104
|
+
elif method == 'plane_fit':
|
|
105
|
+
#TODO: implement plane fit
|
|
106
|
+
pass
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError("Gave method of {method} but only 'line_fit', 'plane_fit' are allowed")
|
|
109
|
+
|
|
110
|
+
new_sid_dset[:] = data_flat
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def rebin(im, binning=2):
|
|
114
|
+
"""
|
|
115
|
+
rebin an image by the number of pixels in x and y direction given by binning
|
|
116
|
+
|
|
117
|
+
Parameter
|
|
118
|
+
---------
|
|
119
|
+
image: numpy array in 2 dimensions or sidpy.Dataset of data_type 'Image'
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
binned image as numpy array or sidpy.Dataset
|
|
124
|
+
"""
|
|
125
|
+
if len(im.shape) == 2:
|
|
126
|
+
rebinned_image = np.array(im).reshape((im.shape[0]//binning,
|
|
127
|
+
binning, im.shape[1]//binning,
|
|
128
|
+
binning)).mean(axis=3).mean(1)
|
|
129
|
+
if isinstance(im, sidpy.Dataset):
|
|
130
|
+
rebinned_image = im.like_data(rebinned_image)
|
|
131
|
+
rebinned_image.title = 'rebinned_' + im.title
|
|
132
|
+
rebinned_image.data_type = 'image'
|
|
133
|
+
im_dims = im.get_image_dims(return_axis=True)
|
|
134
|
+
|
|
135
|
+
rebinned_image.set_dimension(0, sidpy.Dimension(np.arange(rebinned_image.shape[0])/im_dims[0].slope,
|
|
136
|
+
name='x', units=im_dims[0].units,
|
|
137
|
+
dimension_type=im_dims[0].dimension_type,
|
|
138
|
+
quantity=im_dims[0].quantity))
|
|
139
|
+
rebinned_image.set_dimension(1, sidpy.Dimension(np.arange(rebinned_image.shape[1])/im_dims[1].slope,
|
|
140
|
+
name='y', units=im_dims[1].units,
|
|
141
|
+
dimension_type=im_dims[1].dimension_type,
|
|
142
|
+
quantity=im_dims[1].quantity))
|
|
143
|
+
return rebinned_image
|
|
144
|
+
else:
|
|
145
|
+
raise TypeError('not a 2D image')
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cart2pol(points):
|
|
149
|
+
"""Cartesian to polar coordinate conversion
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
---------
|
|
153
|
+
points: float or numpy array
|
|
154
|
+
points to be converted (Nx2)
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
rho: float or numpy array
|
|
159
|
+
distance
|
|
160
|
+
phi: float or numpy array
|
|
161
|
+
angle
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
rho = np.linalg.norm(points[:, 0:2], axis=1)
|
|
165
|
+
phi = np.arctan2(points[:, 1], points[:, 0])
|
|
166
|
+
|
|
167
|
+
return rho, phi
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def pol2cart(rho, phi):
|
|
171
|
+
"""Polar to Cartesian coordinate conversion
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
rho: float or numpy array
|
|
176
|
+
distance
|
|
177
|
+
phi: float or numpy array
|
|
178
|
+
angle
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
x: float or numpy array
|
|
183
|
+
x coordinates of converted points(Nx2)
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
x = rho * np.cos(phi)
|
|
187
|
+
y = rho * np.sin(phi)
|
|
188
|
+
return x, y
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def xy2polar(points, rounding=1e-3):
|
|
192
|
+
""" Conversion from carthesian to polar coordinates
|
|
193
|
+
|
|
194
|
+
the angles and distances are sorted by r and then phi
|
|
195
|
+
The indices of this sort is also returned
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
points: numpy array
|
|
200
|
+
number of points in axis 0 first two elements in axis 1 are x and y
|
|
201
|
+
rounding: int
|
|
202
|
+
optional rounding in significant digits
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
r, phi, sorted_indices
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
r, phi = cart2pol(points)
|
|
210
|
+
|
|
211
|
+
r = (np.floor(r/rounding))*rounding # Remove rounding error differences
|
|
212
|
+
|
|
213
|
+
sorted_indices = np.lexsort((phi, r)) # sort first by r and then by phi
|
|
214
|
+
r = r[sorted_indices]
|
|
215
|
+
phi = phi[sorted_indices]
|
|
216
|
+
|
|
217
|
+
return r, phi, sorted_indices
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def cartesian2polar(x, y, grid, r, t, order=3):
|
|
221
|
+
"""Transform cartesian grid to polar grid
|
|
222
|
+
|
|
223
|
+
Used by warp
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
rr, tt = np.meshgrid(r, t)
|
|
227
|
+
|
|
228
|
+
new_x = rr*np.cos(tt)
|
|
229
|
+
new_y = rr*np.sin(tt)
|
|
230
|
+
|
|
231
|
+
ix = scipy.interpolate.interp1d(x, np.arange(len(x)))
|
|
232
|
+
iy = scipy.interpolate.interp1d(y, np.arange(len(y)))
|
|
233
|
+
|
|
234
|
+
new_ix = ix(new_x.ravel())
|
|
235
|
+
new_iy = iy(new_y.ravel())
|
|
236
|
+
|
|
237
|
+
return scipy.ndimage.map_coordinates(grid, np.array([new_ix, new_iy]),
|
|
238
|
+
order=order).reshape(new_x.shape)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def warp(diff, center):
|
|
242
|
+
"""Takes a diffraction pattern (as a sidpy dataset)and warps it to a polar grid"""
|
|
243
|
+
|
|
244
|
+
# Define original polar grid
|
|
245
|
+
nx = np.shape(diff)[0]
|
|
246
|
+
ny = np.shape(diff)[1]
|
|
247
|
+
|
|
248
|
+
x = np.linspace(1, nx, nx, endpoint=True)-center[0]
|
|
249
|
+
y = np.linspace(1, ny, ny, endpoint=True)-center[1]
|
|
250
|
+
z = diff
|
|
251
|
+
|
|
252
|
+
# Define new polar grid
|
|
253
|
+
nr = int(min([center[0], center[1], diff.shape[0]-center[0], diff.shape[1]-center[1]])-1)
|
|
254
|
+
nt = 360 * 3
|
|
255
|
+
|
|
256
|
+
r = np.linspace(1, nr, nr)
|
|
257
|
+
t = np.linspace(0., np.pi, nt, endpoint=False)
|
|
258
|
+
|
|
259
|
+
return cartesian2polar(x, y, z, r, t, order=3).T
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def inpaint_image(sid_dset, mask = None, channel = None):
|
|
263
|
+
"""Inpaints a sparse image, given a mask.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
sid_dset (_type_): sidpy Dataset
|
|
267
|
+
with two dimensions being of spatial or reciprocal type
|
|
268
|
+
mask (np.ndarry) : mask [0,1] same shape as sid_dset.
|
|
269
|
+
If providing a sidpy dataset and mask is in the metadata dict,
|
|
270
|
+
then this entry is optional
|
|
271
|
+
channel (int): (optional) for multi-channel datasets,
|
|
272
|
+
provide the channel to in-paint
|
|
273
|
+
"""
|
|
274
|
+
if len(sid_dset.shape)==2:
|
|
275
|
+
image_data = np.array(sid_dset).squeeze()
|
|
276
|
+
elif len(sid_dset.shape)==3:
|
|
277
|
+
image_dims = []
|
|
278
|
+
selection = []
|
|
279
|
+
for dim, axis in sid_dset._axes.items():
|
|
280
|
+
if axis.dimension_type in [sidpy.DimensionType.SPATIAL, sidpy.DimensionType.RECIPROCAL]:
|
|
281
|
+
selection.append(slice(None))
|
|
282
|
+
image_dims.append(dim)
|
|
283
|
+
else:
|
|
284
|
+
if channel is None:
|
|
285
|
+
channel=0
|
|
286
|
+
selection.append(slice(channel, channel+1))
|
|
287
|
+
|
|
288
|
+
image_data = np.array(sid_dset[tuple(selection)]).squeeze()
|
|
289
|
+
if mask is None:
|
|
290
|
+
mask_data = sid_dset.metadata["mask"]
|
|
291
|
+
mask = np.copy(mask_data)
|
|
292
|
+
mask[mask==1] = -1
|
|
293
|
+
mask[mask==0] = 1
|
|
294
|
+
mask[mask==-1] = 0
|
|
295
|
+
|
|
296
|
+
inpainted_data = inpaint.inpaint_biharmonic(image_data, mask)
|
|
297
|
+
|
|
298
|
+
#convert this into a sidpy dataset
|
|
299
|
+
data_set = sidpy.Dataset.from_array(inpainted_data, name='inpainted_image')
|
|
300
|
+
data_set.data_type = 'image' # supported
|
|
301
|
+
|
|
302
|
+
data_set.units = sid_dset.units
|
|
303
|
+
data_set.quantity = sid_dset.quantity
|
|
304
|
+
|
|
305
|
+
data_set.set_dimension(0, sid_dset.get_dimension_by_number(image_dims[0])[0])
|
|
306
|
+
data_set.set_dimension(1, sid_dset.get_dimension_by_number(image_dims[1])[0])
|
|
307
|
+
|
|
308
|
+
data_set.metadata["mask"] = mask
|
|
309
|
+
return data_set
|