fibsem 0.2.1a0__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.
- fibsem/__init__.py +8 -0
- fibsem/_version.py +4 -0
- fibsem/acquire.py +274 -0
- fibsem/alignment.py +390 -0
- fibsem/calibration.py +267 -0
- fibsem/chat/.gitignore +6 -0
- fibsem/chat/main.py +56 -0
- fibsem/chat/requirements.txt +8 -0
- fibsem/config/deposition.dbp +0 -0
- fibsem/config/model.yaml +35 -0
- fibsem/config/protocol.yaml +99 -0
- fibsem/config/system.yaml +40 -0
- fibsem/config.py +102 -0
- fibsem/constants.py +29 -0
- fibsem/conversions.py +147 -0
- fibsem/detection/__init__.py +0 -0
- fibsem/detection/app.py +144 -0
- fibsem/detection/app2.py +114 -0
- fibsem/detection/detection.py +674 -0
- fibsem/detection/evaluation.py +101 -0
- fibsem/detection/evalulation.ipynb +184 -0
- fibsem/detection/grid_plot_summary.py +115 -0
- fibsem/detection/model_evaluation.py +209 -0
- fibsem/detection/notebook.ipynb +346 -0
- fibsem/detection/test_image.tif +0 -0
- fibsem/detection/utils.py +250 -0
- fibsem/gis.py +64 -0
- fibsem/imaging/.gitkeep +0 -0
- fibsem/imaging/__init__.py +0 -0
- fibsem/imaging/masks.py +227 -0
- fibsem/imaging/utils.py +67 -0
- fibsem/log/positions.yaml +14 -0
- fibsem/microscope.py +5137 -0
- fibsem/milling.py +643 -0
- fibsem/movement.py +205 -0
- fibsem/napari.yaml +13 -0
- fibsem/patterning.py +967 -0
- fibsem/segmentation/README.md +170 -0
- fibsem/segmentation/__init__.py +0 -0
- fibsem/segmentation/config.py +42 -0
- fibsem/segmentation/config.yml +14 -0
- fibsem/segmentation/dataset.py +153 -0
- fibsem/segmentation/docs/example_napari.png +0 -0
- fibsem/segmentation/docs/imgs/combined/combined.jpg +0 -0
- fibsem/segmentation/docs/imgs/labelled/label.tif +0 -0
- fibsem/segmentation/docs/imgs/raw/image.tif +0 -0
- fibsem/segmentation/example.ipynb +78 -0
- fibsem/segmentation/inference.py +103 -0
- fibsem/segmentation/model.py +106 -0
- fibsem/segmentation/models/.gitkeep +0 -0
- fibsem/segmentation/test_image.tif +0 -0
- fibsem/segmentation/train.py +287 -0
- fibsem/segmentation/utils.py +395 -0
- fibsem/structures.py +1985 -0
- fibsem/testing.ipynb +87 -0
- fibsem/ui/.gitkeep +0 -0
- fibsem/ui/FibsemAlignmentWidget.py +183 -0
- fibsem/ui/FibsemDetectionUI.py +256 -0
- fibsem/ui/FibsemDetectionWidget.py +515 -0
- fibsem/ui/FibsemEmbeddedDetectionWidget.py +298 -0
- fibsem/ui/FibsemGISWidget.py +306 -0
- fibsem/ui/FibsemImageSettingsWidget.py +517 -0
- fibsem/ui/FibsemLabellingUI.py +481 -0
- fibsem/ui/FibsemManipulatorWidget.py +328 -0
- fibsem/ui/FibsemMillingWidget.py +630 -0
- fibsem/ui/FibsemModelTrainingWidget.py +189 -0
- fibsem/ui/FibsemMovementWidget.py +318 -0
- fibsem/ui/FibsemMultiChemWidget.py +88 -0
- fibsem/ui/FibsemPositionsWidget.py +164 -0
- fibsem/ui/FibsemSegmentationModelWidget.py +139 -0
- fibsem/ui/FibsemSystemSetupWidget.py +323 -0
- fibsem/ui/FibsemUI.py +198 -0
- fibsem/ui/__init__.py +0 -0
- fibsem/ui/qtdesigner_files/CurrentAlignmentWidget.py +68 -0
- fibsem/ui/qtdesigner_files/CurrentAlignmentWidget.ui +105 -0
- fibsem/ui/qtdesigner_files/FibsemDetectionWidget.py +161 -0
- fibsem/ui/qtdesigner_files/FibsemDetectionWidget.ui +273 -0
- fibsem/ui/qtdesigner_files/FibsemEmbeddedDetectionWidget.py +53 -0
- fibsem/ui/qtdesigner_files/FibsemEmbeddedDetectionWidget.ui +83 -0
- fibsem/ui/qtdesigner_files/FibsemGISWidget.py +124 -0
- fibsem/ui/qtdesigner_files/FibsemGISWidget.ui +193 -0
- fibsem/ui/qtdesigner_files/FibsemLabellingUI.py +127 -0
- fibsem/ui/qtdesigner_files/FibsemLabellingUI.ui +232 -0
- fibsem/ui/qtdesigner_files/FibsemManipulatorWidget.py +121 -0
- fibsem/ui/qtdesigner_files/FibsemManipulatorWidget.ui +199 -0
- fibsem/ui/qtdesigner_files/FibsemMillingWidget.py +205 -0
- fibsem/ui/qtdesigner_files/FibsemMillingWidgetui.ui +346 -0
- fibsem/ui/qtdesigner_files/FibsemModelTrainingWidge.ui +110 -0
- fibsem/ui/qtdesigner_files/FibsemModelTrainingWidget.py +172 -0
- fibsem/ui/qtdesigner_files/FibsemModelTrainingWidget.ui +261 -0
- fibsem/ui/qtdesigner_files/FibsemMovementWidget.py +221 -0
- fibsem/ui/qtdesigner_files/FibsemMovementWidget.ui +378 -0
- fibsem/ui/qtdesigner_files/FibsemMultiChemWidget.py +86 -0
- fibsem/ui/qtdesigner_files/FibsemMultiChemWidget.ui +116 -0
- fibsem/ui/qtdesigner_files/FibsemPositionsWidget.py +79 -0
- fibsem/ui/qtdesigner_files/FibsemPositionsWidget.ui +110 -0
- fibsem/ui/qtdesigner_files/FibsemSegmentationModelWidget.py +72 -0
- fibsem/ui/qtdesigner_files/FibsemSegmentationModelWidget.ui +101 -0
- fibsem/ui/qtdesigner_files/FibsemSettingUI.py +270 -0
- fibsem/ui/qtdesigner_files/FibsemSettings.py +284 -0
- fibsem/ui/qtdesigner_files/FibsemSettingsUI.ui +438 -0
- fibsem/ui/qtdesigner_files/FibsemSystemSetupWidget.py +636 -0
- fibsem/ui/qtdesigner_files/FibsemSystemSetupWidget.ui +1107 -0
- fibsem/ui/qtdesigner_files/FibsemUI.py +66 -0
- fibsem/ui/qtdesigner_files/FibsemUI.ui +95 -0
- fibsem/ui/qtdesigner_files/ImageSettingsWidget.py +261 -0
- fibsem/ui/qtdesigner_files/ImageSettingsWidget.ui +455 -0
- fibsem/ui/qtdesigner_files/detection_dialog.py +93 -0
- fibsem/ui/qtdesigner_files/detection_dialog.ui +191 -0
- fibsem/ui/qtdesigner_files/needle_popup.py +30 -0
- fibsem/ui/qtdesigner_files/needle_popup.ui +28 -0
- fibsem/ui/qtdesigner_files/user_dialog.py +47 -0
- fibsem/ui/qtdesigner_files/user_dialog.ui +95 -0
- fibsem/ui/utils.py +711 -0
- fibsem/ui/windows.py +177 -0
- fibsem/utils.py +379 -0
- fibsem/validation.py +291 -0
- fibsem-0.2.1a0.dist-info/LICENSE +21 -0
- fibsem-0.2.1a0.dist-info/METADATA +226 -0
- fibsem-0.2.1a0.dist-info/RECORD +123 -0
- fibsem-0.2.1a0.dist-info/WHEEL +5 -0
- fibsem-0.2.1a0.dist-info/entry_points.txt +6 -0
- fibsem-0.2.1a0.dist-info/top_level.txt +1 -0
fibsem/__init__.py
ADDED
fibsem/_version.py
ADDED
fibsem/acquire.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from skimage import exposure
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from fibsem.structures import (
|
|
10
|
+
BeamType,
|
|
11
|
+
ImageSettings,
|
|
12
|
+
ReferenceImages,
|
|
13
|
+
FibsemImage,
|
|
14
|
+
FibsemRectangle,
|
|
15
|
+
)
|
|
16
|
+
from fibsem.microscope import FibsemMicroscope
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def take_reference_images(
|
|
20
|
+
microscope: FibsemMicroscope, image_settings: ImageSettings
|
|
21
|
+
) -> tuple[FibsemImage, FibsemImage]:
|
|
22
|
+
"""
|
|
23
|
+
Acquires a pair of electron and ion reference images using the specified imaging settings and
|
|
24
|
+
a FibsemMicroscope instance.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
microscope (FibsemMicroscope): A FibsemMicroscope instance for imaging.
|
|
28
|
+
image_settings (ImageSettings): An ImageSettings object with the desired imaging parameters.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A tuple containing a pair of FibsemImage objects, representing the electron and ion reference
|
|
32
|
+
images acquired using the specified microscope and image settings.
|
|
33
|
+
|
|
34
|
+
Notes:
|
|
35
|
+
- This function temporarily changes the `image_settings.beam_type` to `BeamType.ELECTRON`
|
|
36
|
+
and then `BeamType.ION` to acquire the electron and ion reference images, respectively.
|
|
37
|
+
It resets the `image_settings.beam_type` to the original value after acquiring the images.
|
|
38
|
+
- The `FibsemImage` objects in the returned tuple contain the image data as numpy arrays,
|
|
39
|
+
as well as other image metadata.
|
|
40
|
+
"""
|
|
41
|
+
tmp_beam_type = image_settings.beam_type
|
|
42
|
+
image_settings.beam_type = BeamType.ELECTRON
|
|
43
|
+
eb_image = new_image(microscope, image_settings)
|
|
44
|
+
image_settings.beam_type = BeamType.ION
|
|
45
|
+
ib_image = new_image(microscope, image_settings)
|
|
46
|
+
image_settings.beam_type = tmp_beam_type # reset to original beam type
|
|
47
|
+
|
|
48
|
+
return eb_image, ib_image
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def take_set_of_reference_images(
|
|
53
|
+
microscope: FibsemMicroscope,
|
|
54
|
+
image_settings: ImageSettings,
|
|
55
|
+
hfws: tuple[float],
|
|
56
|
+
label: str = "ref_image",
|
|
57
|
+
) -> ReferenceImages:
|
|
58
|
+
"""
|
|
59
|
+
Takes a set of reference images at low and high magnification using a FibsemMicroscope.
|
|
60
|
+
The image settings and half-field widths for the low- and high-resolution images are
|
|
61
|
+
specified using an ImageSettings object and a tuple of two floats, respectively.
|
|
62
|
+
The optional label parameter can be used to customize the image labels.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
microscope (FibsemMicroscope): A FibsemMicroscope object to acquire the images from.
|
|
66
|
+
image_settings (ImageSettings): An ImageSettings object with the desired imaging parameters.
|
|
67
|
+
hfws (Tuple[float, float]): A tuple of two floats specifying the half-field widths (in microns)
|
|
68
|
+
for the low- and high-resolution images, respectively.
|
|
69
|
+
label (str, optional): A label to be included in the image filenames. Defaults to "ref_image".
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A ReferenceImages object containing the low- and high-resolution electron and ion beam images.
|
|
73
|
+
|
|
74
|
+
Notes:
|
|
75
|
+
This function sets image_settings.save to True before taking the images.
|
|
76
|
+
The returned ReferenceImages object contains the electron and ion beam images as FibsemImage objects.
|
|
77
|
+
"""
|
|
78
|
+
# force save
|
|
79
|
+
image_settings.save = True
|
|
80
|
+
|
|
81
|
+
image_settings.hfw = hfws[0]
|
|
82
|
+
image_settings.label = f"{label}_low_res"
|
|
83
|
+
low_eb, low_ib = take_reference_images(microscope, image_settings)
|
|
84
|
+
|
|
85
|
+
image_settings.hfw = hfws[1]
|
|
86
|
+
image_settings.label = f"{label}_high_res"
|
|
87
|
+
high_eb, high_ib = take_reference_images(microscope, image_settings)
|
|
88
|
+
|
|
89
|
+
reference_images = ReferenceImages(low_eb, high_eb, low_ib, high_ib)
|
|
90
|
+
|
|
91
|
+
return reference_images
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def auto_gamma(
|
|
95
|
+
image: FibsemImage,
|
|
96
|
+
min_gamma: float = 0.15,
|
|
97
|
+
max_gamma: float = 1.8,
|
|
98
|
+
scale_factor: float = 0.01,
|
|
99
|
+
gamma_threshold: int = 45,
|
|
100
|
+
method: str = "autogamma",
|
|
101
|
+
) -> FibsemImage:
|
|
102
|
+
"""
|
|
103
|
+
Applies automatic gamma correction to the input `FibsemImage`.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
image (FibsemImage): The input `FibsemImage` to apply gamma correction to.
|
|
107
|
+
min_gamma (float): The minimum gamma value allowed in the correction. Defaults to 0.15.
|
|
108
|
+
max_gamma (float): The maximum gamma value allowed in the correction. Defaults to 1.8.
|
|
109
|
+
scale_factor (float): A scaling factor to adjust the gamma correction range based on the image
|
|
110
|
+
brightness. Defaults to 0.01.
|
|
111
|
+
gamma_threshold (int): The maximum threshold of brightness difference from the mid-gray value
|
|
112
|
+
(i.e., 128) before the gamma value is forced to 1.0. Defaults to 45.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A new `FibsemImage` object containing the gamma-corrected image data, with the same metadata
|
|
116
|
+
as the input image.
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
- This function applies gamma correction to the input image using the `skimage.exposure.adjust_gamma`
|
|
120
|
+
function, with the gamma value computed based on the mean intensity of the image.
|
|
121
|
+
- If the difference between the mean image intensity and the mid-gray value (i.e., 128) is greater
|
|
122
|
+
than the specified `gamma_threshold`, the gamma value is forced to 1.0 to avoid over-correction.
|
|
123
|
+
- The `FibsemImage` object in the returned list contains the gamma-corrected image data as a
|
|
124
|
+
numpy array, as well as other image metadata.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if method == "autogamma":
|
|
128
|
+
std = np.std(image.data) # unused variable?
|
|
129
|
+
mean = np.mean(image.data)
|
|
130
|
+
diff = mean - 255 / 2.0
|
|
131
|
+
gam = np.clip(
|
|
132
|
+
min_gamma, 1 + diff * scale_factor, max_gamma
|
|
133
|
+
)
|
|
134
|
+
if abs(diff) < gamma_threshold:
|
|
135
|
+
gam = 1.0
|
|
136
|
+
if image.metadata is not None:
|
|
137
|
+
logging.debug(
|
|
138
|
+
f"AUTO_GAMMA | {image.metadata.image_settings.beam_type} | {diff:.3f} | {gam:.3f}"
|
|
139
|
+
)
|
|
140
|
+
image_data = exposure.adjust_gamma(image.data, gam)
|
|
141
|
+
|
|
142
|
+
image = FibsemImage(data=image_data, metadata=image.metadata)
|
|
143
|
+
|
|
144
|
+
if method == "autoclahe":
|
|
145
|
+
image = apply_clahe(image)
|
|
146
|
+
|
|
147
|
+
return image
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def apply_clahe(
|
|
152
|
+
image: FibsemImage,
|
|
153
|
+
which_package: str = "skimage",
|
|
154
|
+
clip_limit_cv2: float = 15,
|
|
155
|
+
tile_grid_size: int = 8,
|
|
156
|
+
clip_limit_skimage: float = 0.02,
|
|
157
|
+
kernel_size = None
|
|
158
|
+
) -> FibsemImage:
|
|
159
|
+
"""
|
|
160
|
+
Applies Contrast Limited Adaptive Histogram Equalisation correction to the input `FibsemImage`.
|
|
161
|
+
image is divided into small blocks called "tiles" (tileSize is 8x8 by default in OpenCV). Then each of these
|
|
162
|
+
blocks are histogram equalized as usual. So in a small area, histogram would confine to a small region
|
|
163
|
+
(unless there is noise). If noise is there, it will be amplified. To avoid this, contrast limiting is applied.
|
|
164
|
+
If any histogram bin is above the specified contrast limit (by default 40 in OpenCV), those pixels are clipped and
|
|
165
|
+
distributed uniformly to other bins before applying histogram equalization. After equalization, to remove artifacts
|
|
166
|
+
in tile borders, bilinear interpolation is applied.
|
|
167
|
+
|
|
168
|
+
In OpenCV tileGridSize (tile_grid_size) is by default 8x8, clipLimit (clip_limit_cv2) is by default 40
|
|
169
|
+
|
|
170
|
+
In skimage kernel_size (int or array_like), is optional. It defines the shape of contextual regions used in the
|
|
171
|
+
algorithm. By default, kernel_size is 1/8 of image height by 1/8 of its width.
|
|
172
|
+
clip_limit float, optional. Clipping limit, normalized between 0 and 1 (higher values give more contrast).
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
image (FibsemImage): The input `FibsemImage` to apply gamma correction to.
|
|
176
|
+
|
|
177
|
+
type (str) Either "skimage" or "OpenCV" to apply the filter from the corresponding library
|
|
178
|
+
|
|
179
|
+
clip_limit_cv2 (float): used if which_package=="OpenCV". Defaults to 15. (by default 40 in OpenCV)
|
|
180
|
+
tile_grid_size (int): used if which_package=='OpenCV'. Defaults to 8x8 pixels.
|
|
181
|
+
|
|
182
|
+
clip_limit_skimage (float): used if which_package=="skimage". Defaults to 0.01. Clipping limit, normalised between 0 and 1 (higher values give more contrast).
|
|
183
|
+
tile_grid_size (int): used if which_package=='skimage'. if None, defaults to kernel_size is 1/8 of image height by 1/8 of its width.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
A new `FibsemImage` object containing the clahe-enhanced image data, with the same metadata
|
|
187
|
+
as the input image.
|
|
188
|
+
|
|
189
|
+
Notes:
|
|
190
|
+
- This function applies gamma correction to the input image using either the `cv2.createCLAHE` or `skimage.exposure.equalize_adapthist` function
|
|
191
|
+
- The `FibsemImage` object in the returned list contains the clahe-enhanced image data as a
|
|
192
|
+
numpy array, as well as other image metadata.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
OpenCV requires 8-bit images for CLAHE, skimage requires either 8-bit images or arrays with values between [0,1]
|
|
197
|
+
Here, we convert the raw data into an 8-bit image to proceed
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
temp = image.data
|
|
201
|
+
temp = temp / temp.max()
|
|
202
|
+
temp = (temp * 2**8).astype(np.uint8)
|
|
203
|
+
|
|
204
|
+
if which_package=='OpenCV':
|
|
205
|
+
import cv2
|
|
206
|
+
tile_grid_size = int(tile_grid_size)
|
|
207
|
+
clahe = cv2.createCLAHE(clipLimit=clip_limit_cv2,
|
|
208
|
+
tileGridSize=(tile_grid_size,tile_grid_size))
|
|
209
|
+
image_data = clahe.apply(temp)
|
|
210
|
+
|
|
211
|
+
else: # default filter
|
|
212
|
+
# nbin = 256 default, for 8-bit images
|
|
213
|
+
image_data = exposure.equalize_adapthist(temp,
|
|
214
|
+
kernel_size=kernel_size,
|
|
215
|
+
clip_limit=clip_limit_skimage, nbins=256)
|
|
216
|
+
import skimage
|
|
217
|
+
image_data = skimage.img_as_ubyte(image_data)
|
|
218
|
+
|
|
219
|
+
return FibsemImage(data=image_data, metadata=image.metadata)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def new_image(
|
|
224
|
+
microscope: FibsemMicroscope,
|
|
225
|
+
settings: ImageSettings,
|
|
226
|
+
) -> FibsemImage:
|
|
227
|
+
"""Apply the given image settings and acquire a new image.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
microscope (FibsemMicroscope): The FibsemMicroscope instance used to acquire the image.
|
|
231
|
+
settings (ImageSettings): The image settings used to acquire the image.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
FibsemImage: The acquired image.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
# set label
|
|
238
|
+
if settings.beam_type is BeamType.ELECTRON:
|
|
239
|
+
label = f"{settings.label}_eb"
|
|
240
|
+
|
|
241
|
+
if settings.beam_type is BeamType.ION:
|
|
242
|
+
label = f"{settings.label}_ib"
|
|
243
|
+
|
|
244
|
+
# run autocontrast
|
|
245
|
+
if settings.autocontrast:
|
|
246
|
+
microscope.autocontrast(beam_type=settings.beam_type)
|
|
247
|
+
|
|
248
|
+
# acquire the image
|
|
249
|
+
image = microscope.acquire_image(
|
|
250
|
+
image_settings=settings,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if settings.gamma_enabled:
|
|
254
|
+
image = auto_gamma(image)
|
|
255
|
+
|
|
256
|
+
# save image
|
|
257
|
+
if settings.save:
|
|
258
|
+
filename = os.path.join(settings.save_path, label)
|
|
259
|
+
image.save(save_path=filename)
|
|
260
|
+
|
|
261
|
+
return image
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def last_image(microscope: FibsemMicroscope, beam_type: BeamType) -> FibsemImage:
|
|
265
|
+
"""_summary_
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
microscope (FibsemMicroscope): microscope instance
|
|
269
|
+
beam_type (BeamType): beam type for image
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
FibsemImage: last image acquired by the microscope
|
|
273
|
+
"""
|
|
274
|
+
return microscope.last_image(beam_type=beam_type)
|
fibsem/alignment.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from scipy import fftpack
|
|
6
|
+
|
|
7
|
+
from fibsem import acquire, calibration, utils, validation
|
|
8
|
+
from fibsem.imaging import masks
|
|
9
|
+
from fibsem.imaging import utils as image_utils
|
|
10
|
+
from fibsem.structures import (
|
|
11
|
+
BeamType,
|
|
12
|
+
ImageSettings,
|
|
13
|
+
MicroscopeSettings,
|
|
14
|
+
ReferenceImages,
|
|
15
|
+
FibsemImage,
|
|
16
|
+
FibsemRectangle,
|
|
17
|
+
)
|
|
18
|
+
from fibsem.microscope import FibsemMicroscope
|
|
19
|
+
from typing import Union
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def auto_eucentric_correction(
|
|
23
|
+
microscope: FibsemMicroscope,
|
|
24
|
+
settings: MicroscopeSettings,
|
|
25
|
+
image_settings: ImageSettings,
|
|
26
|
+
tilt_degrees: int = 25,
|
|
27
|
+
xcorr_limit: int = 250,
|
|
28
|
+
) -> None:
|
|
29
|
+
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
image_settings.save = False
|
|
33
|
+
image_settings.beam_type = BeamType.ELECTRON
|
|
34
|
+
calibration.auto_charge_neutralisation(
|
|
35
|
+
microscope.connection, image_settings
|
|
36
|
+
) # TODO: need to change this function
|
|
37
|
+
|
|
38
|
+
for hfw in [400e-6, 150e-6, 80e-6, 80e-6]:
|
|
39
|
+
image_settings.hfw = hfw
|
|
40
|
+
|
|
41
|
+
correct_stage_eucentric_alignment(
|
|
42
|
+
microscope,
|
|
43
|
+
settings,
|
|
44
|
+
image_settings,
|
|
45
|
+
tilt_degrees=tilt_degrees,
|
|
46
|
+
xcorr_limit=xcorr_limit,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def beam_shift_alignment(
|
|
51
|
+
microscope: FibsemMicroscope,
|
|
52
|
+
image_settings: ImageSettings,
|
|
53
|
+
ref_image: FibsemImage,
|
|
54
|
+
reduced_area: FibsemRectangle = None,
|
|
55
|
+
):
|
|
56
|
+
"""Aligns the images by adjusting the beam shift instead of moving the stage.
|
|
57
|
+
|
|
58
|
+
This method uses cross-correlation between the reference image and a new image to calculate the
|
|
59
|
+
optimal beam shift for alignment. This approach offers increased precision, but a lower range
|
|
60
|
+
compared to stage movement.
|
|
61
|
+
|
|
62
|
+
NOTE: Only shift the ion beam, never the electron beam.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
microscope (FibsemMicroscope): An OpenFIBSEM microscope client.
|
|
66
|
+
image_settings (acquire.ImageSettings): Settings for taking the image.
|
|
67
|
+
ref_image (FibsemImage): The reference image to align to.
|
|
68
|
+
reduced_area (FibseRectangle): The reduced area to image with.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If `image_settings.beam_type` is not set to `BeamType.ION`.
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
image_settings.reduced_area = reduced_area
|
|
75
|
+
new_image = acquire.new_image(
|
|
76
|
+
microscope, settings=image_settings
|
|
77
|
+
)
|
|
78
|
+
dx, dy, _ = shift_from_crosscorrelation(
|
|
79
|
+
ref_image, new_image, lowpass=50, highpass=4, sigma=5, use_rect_mask=True
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# adjust beamshift
|
|
83
|
+
microscope.beam_shift(dx, dy, image_settings.beam_type)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def correct_stage_drift(
|
|
87
|
+
microscope: FibsemMicroscope,
|
|
88
|
+
settings: MicroscopeSettings,
|
|
89
|
+
reference_images: ReferenceImages,
|
|
90
|
+
alignment: tuple[BeamType, BeamType] = (BeamType.ELECTRON, BeamType.ELECTRON),
|
|
91
|
+
rotate: bool = False,
|
|
92
|
+
use_ref_mask: bool = False,
|
|
93
|
+
mask_scale: int = 4,
|
|
94
|
+
xcorr_limit: Union[tuple[int, int], None] = None,
|
|
95
|
+
constrain_vertical: bool = False,
|
|
96
|
+
) -> bool:
|
|
97
|
+
"""Corrects the stage drift by aligning low- and high-resolution reference images
|
|
98
|
+
using cross-correlation.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
microscope (FibsemMicroscope): The microscope used for image acquisition.
|
|
102
|
+
settings (MicroscopeSettings): The settings used for image acquisition.
|
|
103
|
+
reference_images (ReferenceImages): A container of low- and high-resolution
|
|
104
|
+
reference images.
|
|
105
|
+
alignment (tuple[BeamType, BeamType], optional): A tuple of two `BeamType`
|
|
106
|
+
objects, specifying the beam types used for the alignment of low- and
|
|
107
|
+
high-resolution images, respectively. Defaults to (BeamType.ELECTRON,
|
|
108
|
+
BeamType.ELECTRON).
|
|
109
|
+
rotate (bool, optional): Whether to rotate the reference images before
|
|
110
|
+
alignment. Defaults to False.
|
|
111
|
+
use_ref_mask (bool, optional): Whether to apply a mask to the reference images
|
|
112
|
+
before alignment. Defaults to False.
|
|
113
|
+
mask_scale (int, optional): The scale factor used for creating the mask. Defaults
|
|
114
|
+
to 4.
|
|
115
|
+
xcorr_limit (tuple[int, int] | None, optional): A tuple of two integers that
|
|
116
|
+
represent the minimum and maximum cross-correlation values allowed for the
|
|
117
|
+
alignment. If not specified, the values are set to (None, None), which means
|
|
118
|
+
there are no limits. Defaults to None.
|
|
119
|
+
constrain_vertical (bool, optional): Whether to constrain the alignment to the
|
|
120
|
+
vertical axis. Defaults to False.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
bool: True if the stage drift correction was successful, False otherwise.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# set reference images
|
|
127
|
+
if alignment[0] is BeamType.ELECTRON:
|
|
128
|
+
ref_lowres, ref_highres = (
|
|
129
|
+
reference_images.low_res_eb,
|
|
130
|
+
reference_images.high_res_eb,
|
|
131
|
+
)
|
|
132
|
+
if alignment[0] is BeamType.ION:
|
|
133
|
+
ref_lowres, ref_highres = (
|
|
134
|
+
reference_images.low_res_ib,
|
|
135
|
+
reference_images.high_res_ib,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if xcorr_limit is None:
|
|
139
|
+
xcorr_limit = (None, None)
|
|
140
|
+
|
|
141
|
+
# rotate reference
|
|
142
|
+
if rotate:
|
|
143
|
+
ref_lowres = image_utils.rotate_image(ref_lowres)
|
|
144
|
+
ref_highres = image_utils.rotate_image(ref_highres)
|
|
145
|
+
|
|
146
|
+
# align lowres, then highres
|
|
147
|
+
for i, ref_image in enumerate([ref_lowres, ref_highres]):
|
|
148
|
+
|
|
149
|
+
if use_ref_mask:
|
|
150
|
+
ref_mask = masks.create_lamella_mask(
|
|
151
|
+
ref_image,
|
|
152
|
+
settings.protocol["lamella"],
|
|
153
|
+
scale=mask_scale,
|
|
154
|
+
use_trench_height=True,
|
|
155
|
+
) # TODO: refactor, liftout specific
|
|
156
|
+
else:
|
|
157
|
+
ref_mask = None
|
|
158
|
+
|
|
159
|
+
# take new images
|
|
160
|
+
# set new image settings (same as reference)
|
|
161
|
+
# settings.image = utils.match_image_settings(
|
|
162
|
+
# ref_image, settings.image, beam_type=alignment[1]
|
|
163
|
+
# )
|
|
164
|
+
settings.image = ImageSettings.fromFibsemImage(ref_image)
|
|
165
|
+
settings.image.beam_type = alignment[1]
|
|
166
|
+
new_image = acquire.new_image(microscope, settings.image)
|
|
167
|
+
|
|
168
|
+
# crosscorrelation alignment
|
|
169
|
+
ret = align_using_reference_images(
|
|
170
|
+
microscope,
|
|
171
|
+
settings,
|
|
172
|
+
ref_image,
|
|
173
|
+
new_image,
|
|
174
|
+
ref_mask=ref_mask,
|
|
175
|
+
xcorr_limit=xcorr_limit[i],
|
|
176
|
+
constrain_vertical=constrain_vertical,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if ret is False:
|
|
180
|
+
break # cross correlation has failed...
|
|
181
|
+
|
|
182
|
+
return ret
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def align_using_reference_images(
|
|
186
|
+
microscope: FibsemMicroscope,
|
|
187
|
+
settings: MicroscopeSettings,
|
|
188
|
+
ref_image: FibsemImage,
|
|
189
|
+
new_image: FibsemImage,
|
|
190
|
+
ref_mask: np.ndarray = None,
|
|
191
|
+
xcorr_limit: int = None,
|
|
192
|
+
constrain_vertical: bool = False,
|
|
193
|
+
) -> bool:
|
|
194
|
+
"""
|
|
195
|
+
Uses cross-correlation to align a new image to a reference image.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
microscope: A FibsemMicroscope instance representing the microscope being used.
|
|
199
|
+
settings: A MicroscopeSettings instance representing the settings for the imaging session.
|
|
200
|
+
ref_image: A FibsemImage instance representing the reference image to which the new image will be aligned.
|
|
201
|
+
new_image: A FibsemImage instance representing the new image that will be aligned to the reference image.
|
|
202
|
+
ref_mask: A numpy array representing a mask to apply to the reference image during alignment. Default is None.
|
|
203
|
+
xcorr_limit: An integer representing the limit for the cross-correlation coefficient. If the coefficient is below
|
|
204
|
+
this limit, alignment will fail. Default is None.
|
|
205
|
+
constrain_vertical: A boolean indicating whether to constrain movement to the vertical axis. If True, movement
|
|
206
|
+
will be restricted to the vertical axis, which is useful for eucentric movement. If False, movement will be
|
|
207
|
+
allowed on both the X and Y axes. Default is False.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
A boolean indicating whether the alignment was successful. True if the alignment was successful, False otherwise.
|
|
211
|
+
"""
|
|
212
|
+
# get beam type
|
|
213
|
+
ref_beam_type = BeamType[ref_image.metadata.image_settings.beam_type.name.upper()]
|
|
214
|
+
new_beam_type = BeamType[new_image.metadata.image_settings.beam_type.name.upper()]
|
|
215
|
+
|
|
216
|
+
logging.info(
|
|
217
|
+
f"aligning {ref_beam_type.name} reference image to {new_beam_type.name}."
|
|
218
|
+
)
|
|
219
|
+
sigma = 6
|
|
220
|
+
hp_px = 8
|
|
221
|
+
lp_px = 128 # MAGIC_NUMBER
|
|
222
|
+
|
|
223
|
+
dx, dy, xcorr = shift_from_crosscorrelation(
|
|
224
|
+
ref_image,
|
|
225
|
+
new_image,
|
|
226
|
+
lowpass=lp_px,
|
|
227
|
+
highpass=hp_px,
|
|
228
|
+
sigma=sigma,
|
|
229
|
+
use_rect_mask=True,
|
|
230
|
+
ref_mask=ref_mask,
|
|
231
|
+
xcorr_limit=xcorr_limit,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
shift_within_tolerance = (
|
|
235
|
+
validation.check_shift_within_tolerance( # TODO: Abstract validation.py
|
|
236
|
+
dx=dx, dy=dy, ref_image=ref_image, limit=0.5
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if shift_within_tolerance:
|
|
241
|
+
|
|
242
|
+
# vertical constraint = eucentric movement
|
|
243
|
+
if constrain_vertical:
|
|
244
|
+
microscope.eucentric_move(
|
|
245
|
+
settings=settings, dy=-dy
|
|
246
|
+
) # FLAG_TEST
|
|
247
|
+
else:
|
|
248
|
+
# move the stage
|
|
249
|
+
microscope.stable_move(
|
|
250
|
+
settings=settings,
|
|
251
|
+
dx=dx,
|
|
252
|
+
dy=-dy,
|
|
253
|
+
beam_type=new_beam_type,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return shift_within_tolerance
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def shift_from_crosscorrelation(
|
|
260
|
+
ref_image: FibsemImage,
|
|
261
|
+
new_image: FibsemImage,
|
|
262
|
+
lowpass: int = 128,
|
|
263
|
+
highpass: int = 6,
|
|
264
|
+
sigma: int = 6,
|
|
265
|
+
use_rect_mask: bool = False,
|
|
266
|
+
ref_mask: np.ndarray = None,
|
|
267
|
+
xcorr_limit: int = None,
|
|
268
|
+
) -> tuple[float, float, np.ndarray]:
|
|
269
|
+
"""Calculates the shift between two images by cross-correlating them and finding the position of maximum correlation.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
ref_image (FibsemImage): The reference image.
|
|
273
|
+
new_image (FibsemImage): The new image to align to the reference.
|
|
274
|
+
lowpass (int, optional): The low-pass filter frequency (in pixels) for the bandpass filter used to
|
|
275
|
+
enhance the correlation signal. Defaults to 128.
|
|
276
|
+
highpass (int, optional): The high-pass filter frequency (in pixels) for the bandpass filter used to
|
|
277
|
+
enhance the correlation signal. Defaults to 6.
|
|
278
|
+
sigma (int, optional): The standard deviation (in pixels) of the Gaussian filter used to create the bandpass
|
|
279
|
+
mask. Defaults to 6.
|
|
280
|
+
use_rect_mask (bool, optional): Whether to use a rectangular mask for the correlation. If True, the correlation
|
|
281
|
+
is performed only inside a rectangle that covers most of the image, to reduce the effect of noise at the
|
|
282
|
+
edges. Defaults to False.
|
|
283
|
+
ref_mask (np.ndarray, optional): A mask to apply to the reference image before correlation. If not None,
|
|
284
|
+
it should be a binary array with the same shape as the images. Pixels with value 0 will be ignored in the
|
|
285
|
+
correlation. Defaults to None.
|
|
286
|
+
xcorr_limit (int, optional): If not None, the correlation map will be circularly masked to a square
|
|
287
|
+
with sides of length 2 * xcorr_limit + 1, centred on the maximum correlation peak. This can be used to
|
|
288
|
+
limit the search range and improve the accuracy of the shift. Defaults to None.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
A tuple (x_shift, y_shift, xcorr), where x_shift and y_shift are the shifts along x and y (in meters),
|
|
292
|
+
and xcorr is the cross-correlation map between the images.
|
|
293
|
+
"""
|
|
294
|
+
# get pixel_size
|
|
295
|
+
pixelsize_x = new_image.metadata.pixel_size.x
|
|
296
|
+
pixelsize_y = new_image.metadata.pixel_size.y
|
|
297
|
+
|
|
298
|
+
# normalise both images
|
|
299
|
+
ref_data_norm = image_utils.normalise_image(ref_image)
|
|
300
|
+
new_data_norm = image_utils.normalise_image(new_image)
|
|
301
|
+
|
|
302
|
+
# cross-correlate normalised images
|
|
303
|
+
if use_rect_mask:
|
|
304
|
+
rect_mask = masks._mask_rectangular(new_data_norm.shape)
|
|
305
|
+
ref_data_norm = rect_mask * ref_data_norm
|
|
306
|
+
new_data_norm = rect_mask * new_data_norm
|
|
307
|
+
|
|
308
|
+
if ref_mask is not None:
|
|
309
|
+
ref_data_norm = ref_mask * ref_data_norm # mask the reference
|
|
310
|
+
|
|
311
|
+
# bandpass mask
|
|
312
|
+
bandpass = masks.create_bandpass_mask(
|
|
313
|
+
shape=ref_data_norm.shape, lp=lowpass, hp=highpass, sigma=sigma
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# crosscorrelation
|
|
317
|
+
xcorr = crosscorrelation_v2(ref_data_norm, new_data_norm, bandpass=bandpass)
|
|
318
|
+
|
|
319
|
+
# limit xcorr range
|
|
320
|
+
if xcorr_limit:
|
|
321
|
+
xcorr = masks.apply_circular_mask(xcorr, xcorr_limit)
|
|
322
|
+
|
|
323
|
+
# calculate maximum crosscorrelation
|
|
324
|
+
maxX, maxY = np.unravel_index(np.argmax(xcorr), xcorr.shape) # TODO: backwards
|
|
325
|
+
cen = np.asarray(xcorr.shape) / 2
|
|
326
|
+
err = np.array(cen - [maxX, maxY], int)
|
|
327
|
+
|
|
328
|
+
# calculate shift in metres
|
|
329
|
+
x_shift = err[1] * pixelsize_x
|
|
330
|
+
y_shift = err[0] * pixelsize_y # this could be the issue?
|
|
331
|
+
|
|
332
|
+
logging.debug(f"cross-correlation:")
|
|
333
|
+
logging.debug(f"pixelsize: x: {pixelsize_x:.2e}, y: {pixelsize_y:.2e}")
|
|
334
|
+
logging.debug(f"maxX: {maxX}, {maxY}, centre: {cen}")
|
|
335
|
+
logging.debug(f"x: {err[1]}px, y: {err[0]}px")
|
|
336
|
+
logging.debug(f"x: {x_shift:.2e}m, y: {y_shift:.2e} meters")
|
|
337
|
+
|
|
338
|
+
# metres
|
|
339
|
+
return x_shift, y_shift, xcorr
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def crosscorrelation_v2(
|
|
343
|
+
img1: np.ndarray, img2: np.ndarray, bandpass: np.ndarray = None
|
|
344
|
+
) -> np.ndarray:
|
|
345
|
+
"""
|
|
346
|
+
Cross-correlate two images using Fourier convolution matching.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
img1 (np.ndarray): The reference image.
|
|
350
|
+
img2 (np.ndarray): The new image to be cross-correlated with the reference.
|
|
351
|
+
bandpass (np.ndarray, optional): A bandpass mask to apply to both images before cross-correlation. Defaults to None.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
np.ndarray: The cross-correlation map between the two images.
|
|
355
|
+
"""
|
|
356
|
+
if img1.shape != img2.shape:
|
|
357
|
+
err = (
|
|
358
|
+
f"Image 1 {img1.shape} and Image 2 {img2.shape} need to have the same shape"
|
|
359
|
+
)
|
|
360
|
+
logging.error(err)
|
|
361
|
+
raise ValueError(err)
|
|
362
|
+
|
|
363
|
+
if bandpass is None:
|
|
364
|
+
bandpass = np.ones_like(img1)
|
|
365
|
+
|
|
366
|
+
n_pixels = img1.shape[0] * img1.shape[1]
|
|
367
|
+
|
|
368
|
+
img1ft = np.fft.ifftshift(bandpass * np.fft.fftshift(np.fft.fft2(img1)))
|
|
369
|
+
tmp = img1ft * np.conj(img1ft)
|
|
370
|
+
img1ft = n_pixels * img1ft / np.sqrt(tmp.sum())
|
|
371
|
+
|
|
372
|
+
img2ft = np.fft.ifftshift(bandpass * np.fft.fftshift(np.fft.fft2(img2)))
|
|
373
|
+
img2ft[0, 0] = 0
|
|
374
|
+
tmp = img2ft * np.conj(img2ft)
|
|
375
|
+
|
|
376
|
+
img2ft = n_pixels * img2ft / np.sqrt(tmp.sum())
|
|
377
|
+
|
|
378
|
+
# import matplotlib.pyplot as plt
|
|
379
|
+
# fig, ax = plt.subplots(1, 2, figsize=(15, 15))
|
|
380
|
+
# ax[0].imshow(np.fft.ifft2(img1ft).real)
|
|
381
|
+
# ax[1].imshow(np.fft.ifft2(img2ft).real)
|
|
382
|
+
# plt.show()
|
|
383
|
+
|
|
384
|
+
# plt.title("Power Spectra")
|
|
385
|
+
# plt.imshow(np.log(np.abs(np.fft.fftshift(np.fft.fft2(img1)))))
|
|
386
|
+
# plt.show()
|
|
387
|
+
|
|
388
|
+
xcorr = np.real(np.fft.fftshift(np.fft.ifft2(img1ft * np.conj(img2ft))))
|
|
389
|
+
|
|
390
|
+
return xcorr
|