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.
Files changed (123) hide show
  1. fibsem/__init__.py +8 -0
  2. fibsem/_version.py +4 -0
  3. fibsem/acquire.py +274 -0
  4. fibsem/alignment.py +390 -0
  5. fibsem/calibration.py +267 -0
  6. fibsem/chat/.gitignore +6 -0
  7. fibsem/chat/main.py +56 -0
  8. fibsem/chat/requirements.txt +8 -0
  9. fibsem/config/deposition.dbp +0 -0
  10. fibsem/config/model.yaml +35 -0
  11. fibsem/config/protocol.yaml +99 -0
  12. fibsem/config/system.yaml +40 -0
  13. fibsem/config.py +102 -0
  14. fibsem/constants.py +29 -0
  15. fibsem/conversions.py +147 -0
  16. fibsem/detection/__init__.py +0 -0
  17. fibsem/detection/app.py +144 -0
  18. fibsem/detection/app2.py +114 -0
  19. fibsem/detection/detection.py +674 -0
  20. fibsem/detection/evaluation.py +101 -0
  21. fibsem/detection/evalulation.ipynb +184 -0
  22. fibsem/detection/grid_plot_summary.py +115 -0
  23. fibsem/detection/model_evaluation.py +209 -0
  24. fibsem/detection/notebook.ipynb +346 -0
  25. fibsem/detection/test_image.tif +0 -0
  26. fibsem/detection/utils.py +250 -0
  27. fibsem/gis.py +64 -0
  28. fibsem/imaging/.gitkeep +0 -0
  29. fibsem/imaging/__init__.py +0 -0
  30. fibsem/imaging/masks.py +227 -0
  31. fibsem/imaging/utils.py +67 -0
  32. fibsem/log/positions.yaml +14 -0
  33. fibsem/microscope.py +5137 -0
  34. fibsem/milling.py +643 -0
  35. fibsem/movement.py +205 -0
  36. fibsem/napari.yaml +13 -0
  37. fibsem/patterning.py +967 -0
  38. fibsem/segmentation/README.md +170 -0
  39. fibsem/segmentation/__init__.py +0 -0
  40. fibsem/segmentation/config.py +42 -0
  41. fibsem/segmentation/config.yml +14 -0
  42. fibsem/segmentation/dataset.py +153 -0
  43. fibsem/segmentation/docs/example_napari.png +0 -0
  44. fibsem/segmentation/docs/imgs/combined/combined.jpg +0 -0
  45. fibsem/segmentation/docs/imgs/labelled/label.tif +0 -0
  46. fibsem/segmentation/docs/imgs/raw/image.tif +0 -0
  47. fibsem/segmentation/example.ipynb +78 -0
  48. fibsem/segmentation/inference.py +103 -0
  49. fibsem/segmentation/model.py +106 -0
  50. fibsem/segmentation/models/.gitkeep +0 -0
  51. fibsem/segmentation/test_image.tif +0 -0
  52. fibsem/segmentation/train.py +287 -0
  53. fibsem/segmentation/utils.py +395 -0
  54. fibsem/structures.py +1985 -0
  55. fibsem/testing.ipynb +87 -0
  56. fibsem/ui/.gitkeep +0 -0
  57. fibsem/ui/FibsemAlignmentWidget.py +183 -0
  58. fibsem/ui/FibsemDetectionUI.py +256 -0
  59. fibsem/ui/FibsemDetectionWidget.py +515 -0
  60. fibsem/ui/FibsemEmbeddedDetectionWidget.py +298 -0
  61. fibsem/ui/FibsemGISWidget.py +306 -0
  62. fibsem/ui/FibsemImageSettingsWidget.py +517 -0
  63. fibsem/ui/FibsemLabellingUI.py +481 -0
  64. fibsem/ui/FibsemManipulatorWidget.py +328 -0
  65. fibsem/ui/FibsemMillingWidget.py +630 -0
  66. fibsem/ui/FibsemModelTrainingWidget.py +189 -0
  67. fibsem/ui/FibsemMovementWidget.py +318 -0
  68. fibsem/ui/FibsemMultiChemWidget.py +88 -0
  69. fibsem/ui/FibsemPositionsWidget.py +164 -0
  70. fibsem/ui/FibsemSegmentationModelWidget.py +139 -0
  71. fibsem/ui/FibsemSystemSetupWidget.py +323 -0
  72. fibsem/ui/FibsemUI.py +198 -0
  73. fibsem/ui/__init__.py +0 -0
  74. fibsem/ui/qtdesigner_files/CurrentAlignmentWidget.py +68 -0
  75. fibsem/ui/qtdesigner_files/CurrentAlignmentWidget.ui +105 -0
  76. fibsem/ui/qtdesigner_files/FibsemDetectionWidget.py +161 -0
  77. fibsem/ui/qtdesigner_files/FibsemDetectionWidget.ui +273 -0
  78. fibsem/ui/qtdesigner_files/FibsemEmbeddedDetectionWidget.py +53 -0
  79. fibsem/ui/qtdesigner_files/FibsemEmbeddedDetectionWidget.ui +83 -0
  80. fibsem/ui/qtdesigner_files/FibsemGISWidget.py +124 -0
  81. fibsem/ui/qtdesigner_files/FibsemGISWidget.ui +193 -0
  82. fibsem/ui/qtdesigner_files/FibsemLabellingUI.py +127 -0
  83. fibsem/ui/qtdesigner_files/FibsemLabellingUI.ui +232 -0
  84. fibsem/ui/qtdesigner_files/FibsemManipulatorWidget.py +121 -0
  85. fibsem/ui/qtdesigner_files/FibsemManipulatorWidget.ui +199 -0
  86. fibsem/ui/qtdesigner_files/FibsemMillingWidget.py +205 -0
  87. fibsem/ui/qtdesigner_files/FibsemMillingWidgetui.ui +346 -0
  88. fibsem/ui/qtdesigner_files/FibsemModelTrainingWidge.ui +110 -0
  89. fibsem/ui/qtdesigner_files/FibsemModelTrainingWidget.py +172 -0
  90. fibsem/ui/qtdesigner_files/FibsemModelTrainingWidget.ui +261 -0
  91. fibsem/ui/qtdesigner_files/FibsemMovementWidget.py +221 -0
  92. fibsem/ui/qtdesigner_files/FibsemMovementWidget.ui +378 -0
  93. fibsem/ui/qtdesigner_files/FibsemMultiChemWidget.py +86 -0
  94. fibsem/ui/qtdesigner_files/FibsemMultiChemWidget.ui +116 -0
  95. fibsem/ui/qtdesigner_files/FibsemPositionsWidget.py +79 -0
  96. fibsem/ui/qtdesigner_files/FibsemPositionsWidget.ui +110 -0
  97. fibsem/ui/qtdesigner_files/FibsemSegmentationModelWidget.py +72 -0
  98. fibsem/ui/qtdesigner_files/FibsemSegmentationModelWidget.ui +101 -0
  99. fibsem/ui/qtdesigner_files/FibsemSettingUI.py +270 -0
  100. fibsem/ui/qtdesigner_files/FibsemSettings.py +284 -0
  101. fibsem/ui/qtdesigner_files/FibsemSettingsUI.ui +438 -0
  102. fibsem/ui/qtdesigner_files/FibsemSystemSetupWidget.py +636 -0
  103. fibsem/ui/qtdesigner_files/FibsemSystemSetupWidget.ui +1107 -0
  104. fibsem/ui/qtdesigner_files/FibsemUI.py +66 -0
  105. fibsem/ui/qtdesigner_files/FibsemUI.ui +95 -0
  106. fibsem/ui/qtdesigner_files/ImageSettingsWidget.py +261 -0
  107. fibsem/ui/qtdesigner_files/ImageSettingsWidget.ui +455 -0
  108. fibsem/ui/qtdesigner_files/detection_dialog.py +93 -0
  109. fibsem/ui/qtdesigner_files/detection_dialog.ui +191 -0
  110. fibsem/ui/qtdesigner_files/needle_popup.py +30 -0
  111. fibsem/ui/qtdesigner_files/needle_popup.ui +28 -0
  112. fibsem/ui/qtdesigner_files/user_dialog.py +47 -0
  113. fibsem/ui/qtdesigner_files/user_dialog.ui +95 -0
  114. fibsem/ui/utils.py +711 -0
  115. fibsem/ui/windows.py +177 -0
  116. fibsem/utils.py +379 -0
  117. fibsem/validation.py +291 -0
  118. fibsem-0.2.1a0.dist-info/LICENSE +21 -0
  119. fibsem-0.2.1a0.dist-info/METADATA +226 -0
  120. fibsem-0.2.1a0.dist-info/RECORD +123 -0
  121. fibsem-0.2.1a0.dist-info/WHEEL +5 -0
  122. fibsem-0.2.1a0.dist-info/entry_points.txt +6 -0
  123. fibsem-0.2.1a0.dist-info/top_level.txt +1 -0
fibsem/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+
2
+ try:
3
+ import importlib.metadata
4
+ __version__ = importlib.metadata.version('fibsem')
5
+ except ModuleNotFoundError:
6
+ __version__ = "unknown"
7
+
8
+
fibsem/_version.py ADDED
@@ -0,0 +1,4 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ __version__ = version = '0.1.dev1311+gf3ad026.d20230711'
4
+ __version_tuple__ = version_tuple = (0, 1, 'dev1311', 'gf3ad026.d20230711')
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