nabu 2024.1.10__py3-none-any.whl → 2024.2.0__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.
- nabu/__init__.py +1 -1
- nabu/app/bootstrap.py +2 -3
- nabu/app/cast_volume.py +4 -2
- nabu/app/cli_configs.py +5 -0
- nabu/app/composite_cor.py +1 -1
- nabu/app/create_distortion_map_from_poly.py +5 -6
- nabu/app/diag_to_pix.py +7 -19
- nabu/app/diag_to_rot.py +14 -29
- nabu/app/double_flatfield.py +32 -44
- nabu/app/parse_reconstruction_log.py +3 -0
- nabu/app/reconstruct.py +53 -15
- nabu/app/reconstruct_helical.py +2 -2
- nabu/app/stitching.py +27 -13
- nabu/app/tests/__init__.py +0 -0
- nabu/app/tests/test_reduce_dark_flat.py +4 -1
- nabu/cuda/kernel.py +11 -2
- nabu/cuda/processing.py +2 -2
- nabu/cuda/src/cone.cu +77 -0
- nabu/cuda/src/hierarchical_backproj.cu +271 -0
- nabu/cuda/utils.py +0 -6
- nabu/estimation/alignment.py +5 -19
- nabu/estimation/cor.py +173 -599
- nabu/estimation/cor_sino.py +356 -26
- nabu/estimation/focus.py +63 -11
- nabu/estimation/tests/test_cor.py +124 -58
- nabu/estimation/tests/test_focus.py +6 -6
- nabu/estimation/tilt.py +2 -1
- nabu/estimation/utils.py +5 -33
- nabu/io/__init__.py +1 -1
- nabu/io/cast_volume.py +1 -1
- nabu/io/reader.py +416 -21
- nabu/io/tests/test_readers.py +422 -0
- nabu/io/tests/test_writers.py +1 -102
- nabu/io/writer.py +4 -433
- nabu/opencl/kernel.py +14 -3
- nabu/opencl/processing.py +8 -0
- nabu/pipeline/config_validators.py +5 -2
- nabu/pipeline/datadump.py +12 -5
- nabu/pipeline/estimators.py +162 -188
- nabu/pipeline/fullfield/chunked.py +168 -92
- nabu/pipeline/fullfield/chunked_cuda.py +7 -3
- nabu/pipeline/fullfield/computations.py +2 -7
- nabu/pipeline/fullfield/dataset_validator.py +0 -4
- nabu/pipeline/fullfield/nabu_config.py +37 -13
- nabu/pipeline/fullfield/processconfig.py +22 -13
- nabu/pipeline/fullfield/reconstruction.py +13 -9
- nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
- nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
- nabu/pipeline/helical/helical_reconstruction.py +1 -1
- nabu/pipeline/params.py +21 -1
- nabu/pipeline/processconfig.py +1 -12
- nabu/pipeline/reader.py +146 -0
- nabu/pipeline/tests/test_estimators.py +44 -72
- nabu/pipeline/utils.py +4 -2
- nabu/pipeline/writer.py +10 -2
- nabu/preproc/ccd_cuda.py +1 -1
- nabu/preproc/ctf.py +14 -7
- nabu/preproc/ctf_cuda.py +2 -3
- nabu/preproc/double_flatfield.py +5 -12
- nabu/preproc/double_flatfield_cuda.py +2 -2
- nabu/preproc/flatfield.py +5 -1
- nabu/preproc/flatfield_cuda.py +5 -1
- nabu/preproc/phase.py +24 -73
- nabu/preproc/phase_cuda.py +5 -8
- nabu/preproc/tests/test_ctf.py +11 -7
- nabu/preproc/tests/test_flatfield.py +67 -122
- nabu/preproc/tests/test_paganin.py +54 -30
- nabu/processing/azim.py +206 -0
- nabu/processing/convolution_cuda.py +1 -1
- nabu/processing/fft_cuda.py +15 -17
- nabu/processing/histogram.py +2 -0
- nabu/processing/histogram_cuda.py +2 -1
- nabu/processing/kernel_base.py +3 -0
- nabu/processing/muladd_cuda.py +1 -0
- nabu/processing/padding_opencl.py +1 -1
- nabu/processing/roll_opencl.py +1 -0
- nabu/processing/rotation_cuda.py +2 -2
- nabu/processing/tests/test_fft.py +17 -10
- nabu/processing/unsharp_cuda.py +1 -1
- nabu/reconstruction/cone.py +104 -40
- nabu/reconstruction/fbp.py +3 -0
- nabu/reconstruction/fbp_base.py +7 -2
- nabu/reconstruction/filtering.py +20 -7
- nabu/reconstruction/filtering_cuda.py +7 -1
- nabu/reconstruction/hbp.py +424 -0
- nabu/reconstruction/mlem.py +99 -0
- nabu/reconstruction/reconstructor.py +2 -0
- nabu/reconstruction/rings_cuda.py +19 -19
- nabu/reconstruction/sinogram_cuda.py +1 -0
- nabu/reconstruction/sinogram_opencl.py +3 -1
- nabu/reconstruction/tests/test_cone.py +10 -5
- nabu/reconstruction/tests/test_deringer.py +7 -6
- nabu/reconstruction/tests/test_fbp.py +124 -10
- nabu/reconstruction/tests/test_filtering.py +13 -11
- nabu/reconstruction/tests/test_halftomo.py +30 -4
- nabu/reconstruction/tests/test_mlem.py +91 -0
- nabu/reconstruction/tests/test_reconstructor.py +8 -3
- nabu/resources/dataset_analyzer.py +142 -92
- nabu/resources/gpu.py +1 -0
- nabu/resources/nxflatfield.py +134 -125
- nabu/resources/templates/id16a_fluo.conf +42 -0
- nabu/resources/tests/test_extract.py +10 -0
- nabu/resources/tests/test_nxflatfield.py +2 -2
- nabu/stitching/alignment.py +80 -24
- nabu/stitching/config.py +105 -68
- nabu/stitching/definitions.py +1 -0
- nabu/stitching/frame_composition.py +68 -60
- nabu/stitching/overlap.py +91 -51
- nabu/stitching/single_axis_stitching.py +32 -0
- nabu/stitching/slurm_utils.py +6 -6
- nabu/stitching/stitcher/__init__.py +0 -0
- nabu/stitching/stitcher/base.py +124 -0
- nabu/stitching/stitcher/dumper/__init__.py +3 -0
- nabu/stitching/stitcher/dumper/base.py +94 -0
- nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
- nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
- nabu/stitching/stitcher/post_processing.py +555 -0
- nabu/stitching/stitcher/pre_processing.py +1068 -0
- nabu/stitching/stitcher/single_axis.py +484 -0
- nabu/stitching/stitcher/stitcher.py +0 -0
- nabu/stitching/stitcher/y_stitcher.py +13 -0
- nabu/stitching/stitcher/z_stitcher.py +45 -0
- nabu/stitching/stitcher_2D.py +278 -0
- nabu/stitching/tests/test_config.py +12 -37
- nabu/stitching/tests/test_frame_composition.py +33 -59
- nabu/stitching/tests/test_overlap.py +149 -7
- nabu/stitching/tests/test_utils.py +1 -1
- nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
- nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
- nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
- nabu/stitching/utils/__init__.py +1 -0
- nabu/stitching/utils/post_processing.py +281 -0
- nabu/stitching/utils/tests/test_post-processing.py +21 -0
- nabu/stitching/{utils.py → utils/utils.py} +79 -52
- nabu/stitching/y_stitching.py +27 -0
- nabu/stitching/z_stitching.py +32 -2281
- nabu/testutils.py +1 -152
- nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
- nabu/utils.py +158 -61
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/METADATA +24 -17
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/RECORD +145 -121
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/WHEEL +1 -1
- nabu/io/tiffwriter_zmm.py +0 -99
- nabu/pipeline/fallback_utils.py +0 -149
- nabu/pipeline/helical/tests/test_accumulator.py +0 -158
- nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
- nabu/pipeline/helical/tests/test_strategy.py +0 -61
- nabu/pipeline/helical/utils.py +0 -51
- nabu/pipeline/tests/test_chunk_reader.py +0 -74
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/top_level.txt +0 -0
nabu/estimation/cor.py
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
import math
|
2
2
|
import numpy as np
|
3
|
-
from
|
4
|
-
from numbers import Real
|
3
|
+
from ..utils import deprecated_class, deprecation_warning, is_scalar
|
5
4
|
from ..misc import fourier_filters
|
6
5
|
from .alignment import AlignmentBase, plt, progress_bar, local_fftn, local_ifftn
|
7
|
-
|
6
|
+
|
8
7
|
|
9
8
|
# three possible values for the validity check, which can optionally be returned by the find_shifts methods
|
10
9
|
cor_result_validity = {
|
@@ -16,10 +15,12 @@ cor_result_validity = {
|
|
16
15
|
|
17
16
|
|
18
17
|
class CenterOfRotation(AlignmentBase):
|
18
|
+
|
19
19
|
def find_shift(
|
20
20
|
self,
|
21
21
|
img_1: np.ndarray,
|
22
22
|
img_2: np.ndarray,
|
23
|
+
side=None,
|
23
24
|
shift_axis: int = -1,
|
24
25
|
roi_yxhw=None,
|
25
26
|
median_filt_shape=None,
|
@@ -28,7 +29,7 @@ class CenterOfRotation(AlignmentBase):
|
|
28
29
|
high_pass=None,
|
29
30
|
low_pass=None,
|
30
31
|
return_validity=False,
|
31
|
-
|
32
|
+
return_relative_to_middle=None,
|
32
33
|
):
|
33
34
|
"""Find the Center of Rotation (CoR), given two images.
|
34
35
|
|
@@ -108,6 +109,15 @@ class CenterOfRotation(AlignmentBase):
|
|
108
109
|
|
109
110
|
>>> cor_position = CoR_calc.find_shift(radio1, radio2, median_filt_shape=(3, 3))
|
110
111
|
"""
|
112
|
+
# COMPAT.
|
113
|
+
if return_relative_to_middle is None:
|
114
|
+
deprecation_warning(
|
115
|
+
"The current default behavior is to return the shift relative the the middle of the image. In a future release, this function will return the shift relative to the left-most pixel. To keep the current behavior, please use 'return_relative_to_middle=True'.",
|
116
|
+
do_print=True,
|
117
|
+
func_name="CenterOfRotation.find_shift",
|
118
|
+
)
|
119
|
+
return_relative_to_middle = True # the kwarg above will be False by default in a future release
|
120
|
+
# ---
|
111
121
|
|
112
122
|
self._check_img_pair_sizes(img_1, img_2)
|
113
123
|
|
@@ -131,17 +141,20 @@ class CenterOfRotation(AlignmentBase):
|
|
131
141
|
|
132
142
|
estimated_cor = fitted_shifts_vh[shift_axis] / 2.0
|
133
143
|
|
134
|
-
if
|
135
|
-
near_pos =
|
144
|
+
if is_scalar(side):
|
145
|
+
near_pos = side - (img_1.shape[-1] - 1) / 2
|
136
146
|
if (
|
137
147
|
np.abs(near_pos - estimated_cor) / near_pos > 0.2
|
138
|
-
): # For comparison, near_pos is RELATIVE (as estimated_cor is).
|
148
|
+
): # For comparison, near_pos is RELATIVE to the middle of image (as estimated_cor is).
|
139
149
|
validity_check_result = cor_result_validity["questionable"]
|
140
150
|
else:
|
141
151
|
validity_check_result = cor_result_validity["sound"]
|
142
152
|
else:
|
143
153
|
validity_check_result = cor_result_validity["unknown"]
|
144
154
|
|
155
|
+
if not (return_relative_to_middle):
|
156
|
+
estimated_cor += (img_1.shape[-1] - 1) / 2
|
157
|
+
|
145
158
|
if return_validity:
|
146
159
|
return estimated_cor, validity_check_result
|
147
160
|
else:
|
@@ -153,16 +166,15 @@ class CenterOfRotationSlidingWindow(CenterOfRotation):
|
|
153
166
|
self,
|
154
167
|
img_1: np.ndarray,
|
155
168
|
img_2: np.ndarray,
|
156
|
-
side,
|
169
|
+
side="center",
|
157
170
|
window_width=None,
|
158
171
|
roi_yxhw=None,
|
159
172
|
median_filt_shape=None,
|
160
|
-
padding_mode=None,
|
161
173
|
peak_fit_radius=1,
|
162
174
|
high_pass=None,
|
163
175
|
low_pass=None,
|
164
176
|
return_validity=False,
|
165
|
-
|
177
|
+
return_relative_to_middle=None,
|
166
178
|
):
|
167
179
|
"""Semi-automatically find the Center of Rotation (CoR), given two images
|
168
180
|
or sinograms. Suitable for half-aquisition scan.
|
@@ -170,86 +182,28 @@ class CenterOfRotationSlidingWindow(CenterOfRotation):
|
|
170
182
|
This method finds the half-shift between two opposite images, by
|
171
183
|
minimizing difference over a moving window.
|
172
184
|
|
173
|
-
|
174
|
-
aligning the sample rotation axis. Given the following values:
|
175
|
-
|
176
|
-
- L1: distance from source to motor
|
177
|
-
- L2: distance from source to detector
|
178
|
-
- ps: physical pixel size
|
179
|
-
- v: output of this function
|
180
|
-
|
181
|
-
displacement of motor = (L1 / L2 * ps) * v
|
185
|
+
Parameters and usage is the same as CenterOfRotation, except for the following two parameters.
|
182
186
|
|
183
187
|
Parameters
|
184
188
|
----------
|
185
|
-
|
186
|
-
|
187
|
-
img_2: numpy.ndarray
|
188
|
-
Second image, it needs to have been flipped already (e.g. using numpy.fliplr).
|
189
|
-
side: string
|
190
|
-
Expected region of the CoR. Allowed values: 'left', 'center' or 'right'.
|
189
|
+
side: string or float, optional
|
190
|
+
Expected region of the CoR. Allowed values: 'left', 'center' or 'right'. Default is 'center'
|
191
191
|
window_width: int, optional
|
192
|
-
Width of window that will slide on the other image / part of the
|
193
|
-
sinogram. Default is None.
|
194
|
-
roi_yxhw: (2, ) or (4, ) numpy.ndarray, tuple, or array, optional
|
195
|
-
4 elements vector containing: vertical and horizontal coordinates
|
196
|
-
of first pixel, plus height and width of the Region of Interest (RoI).
|
197
|
-
Or a 2 elements vector containing: plus height and width of the
|
198
|
-
centered Region of Interest (RoI).
|
199
|
-
Default is None -> deactivated.
|
200
|
-
median_filt_shape: (2, ) numpy.ndarray, tuple, or array, optional
|
201
|
-
Shape of the median filter window. Default is None -> deactivated.
|
202
|
-
padding_mode: str in numpy.pad's mode list, optional
|
203
|
-
Padding mode, which determines the type of convolution. If None or
|
204
|
-
'wrap' are passed, this resorts to the traditional circular convolution.
|
205
|
-
If 'edge' or 'constant' are passed, it results in a linear convolution.
|
206
|
-
Default is the circular convolution.
|
207
|
-
All options are:
|
208
|
-
None | 'constant' | 'edge' | 'linear_ramp' | 'maximum' | 'mean'
|
209
|
-
| 'median' | 'minimum' | 'reflect' | 'symmetric' |'wrap'
|
210
|
-
peak_fit_radius: int, optional
|
211
|
-
Radius size around the max correlation pixel, for sub-pixel fitting.
|
212
|
-
Minimum and default value is 1.
|
213
|
-
low_pass: float or sequence of two floats
|
214
|
-
Low-pass filter properties, as described in `nabu.misc.fourier_filters`
|
215
|
-
high_pass: float or sequence of two floats
|
216
|
-
High-pass filter properties, as described in `nabu.misc.fourier_filters`
|
217
|
-
return_validity: a boolean, defaults to false
|
218
|
-
if set to True adds a second return value which may have three string values.
|
219
|
-
These values are "unknown", "sound", "questionable".
|
220
|
-
It will be "uknown" if the validation method is not implemented
|
221
|
-
and it will be "sound" or "questionable" if it is implemented.
|
222
|
-
|
223
|
-
Raises
|
224
|
-
------
|
225
|
-
ValueError
|
226
|
-
In case images are not 2-dimensional or have different sizes.
|
227
|
-
|
228
|
-
Returns
|
229
|
-
-------
|
230
|
-
float
|
231
|
-
Estimated center of rotation position from the center of the RoI in pixels.
|
232
|
-
|
233
|
-
Examples
|
234
|
-
--------
|
235
|
-
The following code computes the center of rotation position for two
|
236
|
-
given images in a tomography scan, where the second image is taken at
|
237
|
-
180 degrees from the first.
|
238
|
-
|
239
|
-
>>> radio1 = data[0, :, :]
|
240
|
-
... radio2 = np.fliplr(data[1, :, :])
|
241
|
-
... CoR_calc = CenterOfRotationSlidingWindow()
|
242
|
-
... cor_position = CoR_calc.find_shift(radio1, radio2)
|
243
|
-
|
244
|
-
Or for noisy images:
|
245
|
-
|
246
|
-
>>> cor_position = CoR_calc.find_shift(radio1, radio2, median_filt_shape=(3, 3))
|
192
|
+
Width of window that will slide on the other image / part of the sinogram. Default is None.
|
247
193
|
"""
|
248
|
-
|
194
|
+
# COMPAT.
|
195
|
+
if return_relative_to_middle is None:
|
196
|
+
deprecation_warning(
|
197
|
+
"The current default behavior is to return the shift relative the the middle of the image. In a future release, this function will return the shift relative to the left-most pixel. To keep the current behavior, please use 'return_relative_to_middle=True'.",
|
198
|
+
do_print=True,
|
199
|
+
func_name="CenterOfRotationSlidingWindow.find_shift",
|
200
|
+
)
|
201
|
+
return_relative_to_middle = True # the kwarg above will be False by default in a future release
|
202
|
+
# ---
|
249
203
|
validity_check_result = cor_result_validity["unknown"]
|
250
204
|
|
251
205
|
if side is None:
|
252
|
-
raise ValueError("Side should be one of 'left', 'right',
|
206
|
+
raise ValueError("Side should be one of 'left', 'right', 'center' or a scalar. 'None' was given instead")
|
253
207
|
|
254
208
|
self._check_img_pair_sizes(img_1, img_2)
|
255
209
|
|
@@ -267,42 +221,36 @@ class CenterOfRotationSlidingWindow(CenterOfRotation):
|
|
267
221
|
img_2, roi_yxhw=roi_yxhw, median_filt_shape=median_filt_shape, high_pass=high_pass, low_pass=low_pass
|
268
222
|
)
|
269
223
|
img_shape = img_2.shape
|
224
|
+
img_width = img_shape[-1]
|
270
225
|
|
271
|
-
|
272
|
-
if near_pos is None:
|
226
|
+
if isinstance(side, str):
|
273
227
|
if window_width is None:
|
274
|
-
if side
|
275
|
-
window_width = round(
|
228
|
+
if side == "center":
|
229
|
+
window_width = round(img_width / 4.0 * 3.0)
|
276
230
|
else:
|
277
|
-
window_width = round(
|
231
|
+
window_width = round(img_width / 10)
|
278
232
|
window_shift = window_width // 2
|
279
233
|
window_width = window_shift * 2 + 1
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
234
|
+
if side == "right":
|
235
|
+
win_2_start = 0
|
236
|
+
elif side == "left":
|
237
|
+
win_2_start = img_width - window_width
|
238
|
+
else:
|
239
|
+
win_2_start = img_width // 2 - window_shift
|
284
240
|
else:
|
285
|
-
abs_pos =
|
286
|
-
|
241
|
+
abs_pos = int(side + img_width // 2)
|
242
|
+
window_fraction = 0.1 # Hard-coded ?
|
287
243
|
|
288
|
-
|
289
|
-
window_shift =
|
290
|
-
window_width =
|
244
|
+
window_width = round(window_fraction * img_width)
|
245
|
+
window_shift = window_width // 2
|
246
|
+
window_width = window_shift * 2 + 1
|
291
247
|
|
292
|
-
|
293
|
-
|
294
|
-
win_1_start_seed = 2 * near_pos - sliding_shift
|
248
|
+
win_2_start = np.clip(abs_pos - window_shift, 0, img_width // 2 - 1)
|
249
|
+
win_2_start = img_width // 2 - 1 - win_2_start
|
295
250
|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
win_2_start = img_shape[-1] - window_width
|
300
|
-
elif side.lower() == "center":
|
301
|
-
win_2_start = img_shape[-1] // 2 - window_shift
|
302
|
-
else:
|
303
|
-
raise ValueError(
|
304
|
-
"Side should be one of 'left', 'right', or 'center'. '%s' was given instead" % side.lower()
|
305
|
-
)
|
251
|
+
win_1_start_seed = 0
|
252
|
+
# number of pixels where the window will "slide".
|
253
|
+
n = img_width - window_width
|
306
254
|
|
307
255
|
win_2_end = win_2_start + window_width
|
308
256
|
|
@@ -334,14 +282,14 @@ class CenterOfRotationSlidingWindow(CenterOfRotation):
|
|
334
282
|
win_pos_max, win_val_max = self.refine_max_position_1d(f_vals, return_vertex_val=True)
|
335
283
|
|
336
284
|
# Derive the COR
|
337
|
-
if
|
285
|
+
if is_scalar(side):
|
338
286
|
cor_h = -(win_2_start - (win_1_start_seed + win_ind_max + win_pos_max)) / 2.0
|
339
287
|
cor_pos = -(win_2_start - (win_1_start_seed + np.arange(n))) / 2.0
|
340
288
|
else:
|
341
289
|
cor_h = -(win_2_start - (win_ind_max + win_pos_max)) / 2.0
|
342
290
|
cor_pos = -(win_2_start - np.arange(n)) / 2.0
|
343
291
|
|
344
|
-
if (side
|
292
|
+
if (side == "right" and win_ind_max == 0) or (side == "left" and win_ind_max == n):
|
345
293
|
self.logger.warning("Sliding window width %d might be too large!" % window_width)
|
346
294
|
|
347
295
|
if self.verbose:
|
@@ -357,6 +305,9 @@ class CenterOfRotationSlidingWindow(CenterOfRotation):
|
|
357
305
|
plt.legend()
|
358
306
|
plt.show(block=False)
|
359
307
|
|
308
|
+
if not (return_relative_to_middle):
|
309
|
+
cor_h += (img_width - 1) / 2.0
|
310
|
+
|
360
311
|
if return_validity:
|
361
312
|
return cor_h, validity_check_result
|
362
313
|
else:
|
@@ -377,6 +328,7 @@ class CenterOfRotationGrowingWindow(CenterOfRotation):
|
|
377
328
|
high_pass=None,
|
378
329
|
low_pass=None,
|
379
330
|
return_validity=False,
|
331
|
+
return_relative_to_middle=None,
|
380
332
|
):
|
381
333
|
"""Automatically find the Center of Rotation (CoR), given two images or
|
382
334
|
sinograms. Suitable for half-aquisition scan.
|
@@ -384,85 +336,23 @@ class CenterOfRotationGrowingWindow(CenterOfRotation):
|
|
384
336
|
This method finds the half-shift between two opposite images, by
|
385
337
|
minimizing difference over a moving window.
|
386
338
|
|
387
|
-
|
388
|
-
aligning the sample rotation axis. Given the following values:
|
389
|
-
|
390
|
-
- L1: distance from source to motor
|
391
|
-
- L2: distance from source to detector
|
392
|
-
- ps: physical pixel size
|
393
|
-
- v: output of this function
|
394
|
-
|
395
|
-
displacement of motor = (L1 / L2 * ps) * v
|
339
|
+
Usage and parameters are the same as CenterOfRotationSlidingWindow, except for the following parameter.
|
396
340
|
|
397
341
|
Parameters
|
398
342
|
----------
|
399
|
-
img_1: numpy.ndarray
|
400
|
-
First image
|
401
|
-
img_2: numpy.ndarray
|
402
|
-
Second image, it needs to have been flipped already (e.g. using numpy.fliplr).
|
403
|
-
side: string, optional
|
404
|
-
Expected region of the CoR. Allowed values: 'left', 'center', 'right', or 'all'.
|
405
|
-
Default is 'all'.
|
406
343
|
min_window_width: int, optional
|
407
344
|
Minimum window width that covers the common region of the two images /
|
408
345
|
sinograms. Default is 11.
|
409
|
-
roi_yxhw: (2, ) or (4, ) numpy.ndarray, tuple, or array, optional
|
410
|
-
4 elements vector containing: vertical and horizontal coordinates
|
411
|
-
of first pixel, plus height and width of the Region of Interest (RoI).
|
412
|
-
Or a 2 elements vector containing: plus height and width of the
|
413
|
-
centered Region of Interest (RoI).
|
414
|
-
Default is None -> deactivated.
|
415
|
-
median_filt_shape: (2, ) numpy.ndarray, tuple, or array, optional
|
416
|
-
Shape of the median filter window. Default is None -> deactivated.
|
417
|
-
padding_mode: str in numpy.pad's mode list, optional
|
418
|
-
Padding mode, which determines the type of convolution. If None or
|
419
|
-
'wrap' are passed, this resorts to the traditional circular convolution.
|
420
|
-
If 'edge' or 'constant' are passed, it results in a linear convolution.
|
421
|
-
Default is the circular convolution.
|
422
|
-
All options are:
|
423
|
-
None | 'constant' | 'edge' | 'linear_ramp' | 'maximum' | 'mean'
|
424
|
-
| 'median' | 'minimum' | 'reflect' | 'symmetric' |'wrap'
|
425
|
-
peak_fit_radius: int, optional
|
426
|
-
Radius size around the max correlation pixel, for sub-pixel fitting.
|
427
|
-
Minimum and default value is 1.
|
428
|
-
low_pass: float or sequence of two floats
|
429
|
-
Low-pass filter properties, as described in `nabu.misc.fourier_filters`
|
430
|
-
high_pass: float or sequence of two floats
|
431
|
-
High-pass filter properties, as described in `nabu.misc.fourier_filters`
|
432
|
-
return_validity: a boolean, defaults to false
|
433
|
-
if set to True adds a second return value which may have three string values.
|
434
|
-
These values are "unknown", "sound", "questionable".
|
435
|
-
It will be "uknown" if the validation method is not implemented
|
436
|
-
and it will be "sound" or "questionable" if it is implemented.
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
Raises
|
441
|
-
------
|
442
|
-
ValueError
|
443
|
-
In case images are not 2-dimensional or have different sizes.
|
444
|
-
|
445
|
-
Returns
|
446
|
-
-------
|
447
|
-
float
|
448
|
-
Estimated center of rotation position from the center of the RoI in pixels.
|
449
|
-
|
450
|
-
Examples
|
451
|
-
--------
|
452
|
-
The following code computes the center of rotation position for two
|
453
|
-
given images in a tomography scan, where the second image is taken at
|
454
|
-
180 degrees from the first.
|
455
|
-
|
456
|
-
>>> radio1 = data[0, :, :]
|
457
|
-
... radio2 = np.fliplr(data[1, :, :])
|
458
|
-
... CoR_calc = CenterOfRotationGrowingWindow()
|
459
|
-
... cor_position = CoR_calc.find_shift(radio1, radio2)
|
460
|
-
|
461
|
-
Or for noisy images:
|
462
|
-
|
463
|
-
>>> cor_position = CoR_calc.find_shift(radio1, radio2, median_filt_shape=(3, 3))
|
464
346
|
"""
|
465
|
-
|
347
|
+
# COMPAT.
|
348
|
+
if return_relative_to_middle is None:
|
349
|
+
deprecation_warning(
|
350
|
+
"The current default behavior is to return the shift relative the the middle of the image. In a future release, this function will return the shift relative to the left-most pixel. To keep the current behavior, please use 'return_relative_to_middle=True'.",
|
351
|
+
do_print=True,
|
352
|
+
func_name="CenterOfRotationGrowingWindow.find_shift",
|
353
|
+
)
|
354
|
+
return_relative_to_middle = True # the kwarg above will be False by default in a future release
|
355
|
+
# ---
|
466
356
|
validity_check_result = cor_result_validity["unknown"]
|
467
357
|
|
468
358
|
self._check_img_pair_sizes(img_1, img_2)
|
@@ -491,38 +381,35 @@ class CenterOfRotationGrowingWindow(CenterOfRotation):
|
|
491
381
|
img_lower_half_size = np.floor(img_shape[-1] / 2).astype(np.intp)
|
492
382
|
img_upper_half_size = np.ceil(img_shape[-1] / 2).astype(np.intp)
|
493
383
|
|
494
|
-
|
495
|
-
self.
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
384
|
+
if is_scalar(side):
|
385
|
+
self.logger.error(
|
386
|
+
"Passing a first CoR guess is not supported for CenterOfRotationGrowingWindow. Using side='all'."
|
387
|
+
)
|
388
|
+
side = "all"
|
389
|
+
if side.lower() == "right":
|
390
|
+
win_1_mid_start = img_lower_half_size
|
391
|
+
win_1_mid_end = np.floor(img_shape[-1] * 3 / 2).astype(np.intp) - min_window_width
|
392
|
+
win_2_mid_start = -img_upper_half_size + min_window_width
|
393
|
+
win_2_mid_end = img_upper_half_size
|
394
|
+
elif side.lower() == "left":
|
395
|
+
win_1_mid_start = -img_lower_half_size + min_window_width
|
396
|
+
win_1_mid_end = img_lower_half_size
|
397
|
+
win_2_mid_start = img_upper_half_size
|
398
|
+
win_2_mid_end = np.ceil(img_shape[-1] * 3 / 2).astype(np.intp) - min_window_width
|
399
|
+
elif side.lower() == "center":
|
400
|
+
win_1_mid_start = 0
|
401
|
+
win_1_mid_end = img_shape[-1]
|
402
|
+
win_2_mid_start = 0
|
403
|
+
win_2_mid_end = img_shape[-1]
|
404
|
+
elif side.lower() == "all":
|
405
|
+
win_1_mid_start = -img_lower_half_size + min_window_width
|
406
|
+
win_1_mid_end = np.floor(img_shape[-1] * 3 / 2).astype(np.intp) - min_window_width
|
407
|
+
win_2_mid_start = -img_upper_half_size + min_window_width
|
408
|
+
win_2_mid_end = np.ceil(img_shape[-1] * 3 / 2).astype(np.intp) - min_window_width
|
501
409
|
else:
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
win_2_mid_start = -img_upper_half_size + min_window_width
|
506
|
-
win_2_mid_end = img_upper_half_size
|
507
|
-
elif side.lower() == "left":
|
508
|
-
win_1_mid_start = -img_lower_half_size + min_window_width
|
509
|
-
win_1_mid_end = img_lower_half_size
|
510
|
-
win_2_mid_start = img_upper_half_size
|
511
|
-
win_2_mid_end = np.ceil(img_shape[-1] * 3 / 2).astype(np.intp) - min_window_width
|
512
|
-
elif side.lower() == "center":
|
513
|
-
win_1_mid_start = 0
|
514
|
-
win_1_mid_end = img_shape[-1]
|
515
|
-
win_2_mid_start = 0
|
516
|
-
win_2_mid_end = img_shape[-1]
|
517
|
-
elif side.lower() == "all":
|
518
|
-
win_1_mid_start = -img_lower_half_size + min_window_width
|
519
|
-
win_1_mid_end = np.floor(img_shape[-1] * 3 / 2).astype(np.intp) - min_window_width
|
520
|
-
win_2_mid_start = -img_upper_half_size + min_window_width
|
521
|
-
win_2_mid_end = np.ceil(img_shape[-1] * 3 / 2).astype(np.intp) - min_window_width
|
522
|
-
else:
|
523
|
-
raise ValueError(
|
524
|
-
"Side should be one of 'left', 'right', or 'center'. '%s' was given instead" % side.lower()
|
525
|
-
)
|
410
|
+
raise ValueError(
|
411
|
+
"Side should be one of 'left', 'right', or 'center' or 'all'. '%s' was given instead" % side.lower()
|
412
|
+
)
|
526
413
|
|
527
414
|
n1 = win_1_mid_end - win_1_mid_start
|
528
415
|
n2 = win_2_mid_end - win_2_mid_start
|
@@ -579,6 +466,9 @@ class CenterOfRotationGrowingWindow(CenterOfRotation):
|
|
579
466
|
ax.set_title("Window dispersions")
|
580
467
|
plt.show(block=False)
|
581
468
|
|
469
|
+
if not (return_relative_to_middle):
|
470
|
+
cor_h += (img_shape[-1] - 1) / 2.0
|
471
|
+
|
582
472
|
if return_validity:
|
583
473
|
return cor_h, validity_check_result
|
584
474
|
else:
|
@@ -617,6 +507,7 @@ class CenterOfRotationAdaptiveSearch(CenterOfRotation):
|
|
617
507
|
margins=None,
|
618
508
|
filtered_cost=True,
|
619
509
|
return_validity=False,
|
510
|
+
return_relative_to_middle=None,
|
620
511
|
):
|
621
512
|
"""Find the Center of Rotation (CoR), given two images.
|
622
513
|
|
@@ -624,79 +515,25 @@ class CenterOfRotationAdaptiveSearch(CenterOfRotation):
|
|
624
515
|
means of correlation computed in Fourier space.
|
625
516
|
A global search is done on on the detector span (minus a margin) without assuming centered scan conditions.
|
626
517
|
|
627
|
-
|
628
|
-
aligning the sample rotation axis. Given the following values:
|
629
|
-
|
630
|
-
- L1: distance from source to motor
|
631
|
-
- L2: distance from source to detector
|
632
|
-
- ps: physical pixel size
|
633
|
-
- v: output of this function
|
634
|
-
|
635
|
-
displacement of motor = (L1 / L2 * ps) * v
|
518
|
+
Usage and parameters are the same as CenterOfRotation, except for the following parameters.
|
636
519
|
|
637
520
|
Parameters
|
638
521
|
----------
|
639
|
-
img_1: numpy.ndarray
|
640
|
-
First image
|
641
|
-
img_2: numpy.ndarray
|
642
|
-
Second image, it needs to have been flipped already (e.g. using numpy.fliplr).
|
643
|
-
roi_yxhw: (2, ) or (4, ) numpy.ndarray, tuple, or array, optional
|
644
|
-
4 elements vector containing: vertical and horizontal coordinates
|
645
|
-
of first pixel, plus height and width of the Region of Interest (RoI).
|
646
|
-
Or a 2 elements vector containing: plus height and width of the
|
647
|
-
centered Region of Interest (RoI).
|
648
|
-
Default is None -> deactivated.
|
649
|
-
median_filt_shape: (2, ) numpy.ndarray, tuple, or array, optional
|
650
|
-
Shape of the median filter window. Default is None -> deactivated.
|
651
|
-
padding_mode: str in numpy.pad's mode list, optional
|
652
|
-
Padding mode, which determines the type of convolution. If None or
|
653
|
-
'wrap' are passed, this resorts to the traditional circular convolution.
|
654
|
-
If 'edge' or 'constant' are passed, it results in a linear convolution.
|
655
|
-
Default is the circular convolution.
|
656
|
-
All options are:
|
657
|
-
None | 'constant' | 'edge' | 'linear_ramp' | 'maximum' | 'mean'
|
658
|
-
| 'median' | 'minimum' | 'reflect' | 'symmetric' |'wrap'
|
659
|
-
low_pass: float or sequence of two floats.
|
660
|
-
Low-pass filter properties, as described in `nabu.misc.fourier_filters`
|
661
|
-
high_pass: float or sequence of two floats
|
662
|
-
High-pass filter properties, as described in `nabu.misc.fourier_filters`.
|
663
522
|
margins: None or a couple of floats or ints
|
664
523
|
if margins is None or in the form of (margin1,margin2) the search is done between margin1 and dim_x-1-margin2.
|
665
524
|
If left to None then by default (margin1,margin2) = ( 10, 10 ).
|
666
525
|
filtered_cost: boolean.
|
667
526
|
True by default. It triggers the use of filtered images in the calculation of the cost function.
|
668
|
-
return_validity: a boolean, defaults to false
|
669
|
-
if set to True adds a second return value which may have three string values.
|
670
|
-
These values are "unknown", "sound", "questionable".
|
671
|
-
It will be "uknown" if the validation method is not implemented
|
672
|
-
and it will be "sound" or "questionable" if it is implemented.
|
673
|
-
|
674
|
-
Raises
|
675
|
-
------
|
676
|
-
ValueError
|
677
|
-
In case images are not 2-dimensional or have different sizes.
|
678
|
-
|
679
|
-
Returns
|
680
|
-
-------
|
681
|
-
float
|
682
|
-
Estimated center of rotation position from the center of the RoI in pixels.
|
683
|
-
|
684
|
-
Examples
|
685
|
-
--------
|
686
|
-
The following code computes the center of rotation position for two
|
687
|
-
given images in a tomography scan, where the second image is taken at
|
688
|
-
180 degrees from the first.
|
689
|
-
|
690
|
-
>>> radio1 = data[0, :, :]
|
691
|
-
... radio2 = np.fliplr(data[1, :, :])
|
692
|
-
... CoR_calc = CenterOfRotationAdaptiveSearch()
|
693
|
-
... cor_position = CoR_calc.find_shift(radio1, radio2)
|
694
|
-
|
695
|
-
Or for noisy images:
|
696
|
-
|
697
|
-
>>> cor_position = CoR_calc.find_shift(radio1, radio2, median_filt_shape=(3, 3), high_pass=20, low_pass=1 )
|
698
527
|
"""
|
699
|
-
|
528
|
+
# COMPAT.
|
529
|
+
if return_relative_to_middle is None:
|
530
|
+
deprecation_warning(
|
531
|
+
"The current default behavior is to return the shift relative the the middle of the image. In a future release, this function will return the shift relative to the left-most pixel. To keep the current behavior, please use 'return_relative_to_middle=True'.",
|
532
|
+
do_print=True,
|
533
|
+
func_name="CenterOfRotationAdaptiveSearch.find_shift",
|
534
|
+
)
|
535
|
+
return_relative_to_middle = True # the kwarg above will be False by default in a future release
|
536
|
+
# ---
|
700
537
|
validity_check_result = cor_result_validity["unknown"]
|
701
538
|
|
702
539
|
self._check_img_pair_sizes(img_1, img_2)
|
@@ -773,6 +610,7 @@ class CenterOfRotationAdaptiveSearch(CenterOfRotation):
|
|
773
610
|
low_pass=low_pass,
|
774
611
|
high_pass=high_pass,
|
775
612
|
roi_yxhw=roi_yxhw,
|
613
|
+
return_relative_to_middle=True,
|
776
614
|
)
|
777
615
|
except ValueError as err:
|
778
616
|
if "positions are outside the input margins" in str(err):
|
@@ -890,6 +728,9 @@ class CenterOfRotationAdaptiveSearch(CenterOfRotation):
|
|
890
728
|
# The return value is the optimum which had been placed in the middle of the neighborood
|
891
729
|
cor_position = min_neighborood[2]
|
892
730
|
|
731
|
+
if not (return_relative_to_middle):
|
732
|
+
cor_position += (img_1.shape[-1] - 1) / 2.0
|
733
|
+
|
893
734
|
if return_validity:
|
894
735
|
return cor_position, validity_check_result
|
895
736
|
else:
|
@@ -898,217 +739,13 @@ class CenterOfRotationAdaptiveSearch(CenterOfRotation):
|
|
898
739
|
__call__ = find_shift
|
899
740
|
|
900
741
|
|
901
|
-
class
|
902
|
-
"""This CoR estimation algo is proposed by V. Valls (BCU). It is based on the Fourier
|
903
|
-
transform of the columns on the sinogram.
|
904
|
-
It requires an initial guesss of the CoR wich is retrieved from
|
905
|
-
dataset_info.dataset_scanner.estimated_cor_from_motor. It is assumed in mm and pixel size in um.
|
906
|
-
Options are (for the moment) hard-coded in the SinoCORFinder.cor_finder.extra_options dict.
|
907
|
-
"""
|
908
|
-
|
909
|
-
_default_cor_options = {
|
910
|
-
"crop_around_cor": False,
|
911
|
-
"side": "center",
|
912
|
-
"near_pos": None,
|
913
|
-
"near_std": 100,
|
914
|
-
"near_width": 20,
|
915
|
-
"near_shape": "tukey",
|
916
|
-
"near_weight": 0.1,
|
917
|
-
"near_alpha": 0.5,
|
918
|
-
"shift_sino": True,
|
919
|
-
"near_step": 0.5,
|
920
|
-
"refine": False,
|
921
|
-
}
|
922
|
-
|
923
|
-
def _freq_radio(self, sinos, ifrom, ito):
|
924
|
-
size = (sinos.shape[0] + sinos.shape[0] % 2) // 2
|
925
|
-
fs = np.empty((size, sinos.shape[1]))
|
926
|
-
for i in range(ifrom, ito):
|
927
|
-
line = sinos[:, i]
|
928
|
-
f_signal = rfft(line)
|
929
|
-
f = np.abs(f_signal[: (f_signal.size - 1) // 2 + 1])
|
930
|
-
f2 = np.abs(f_signal[(f_signal.size - 1) // 2 + 1 :][::-1])
|
931
|
-
if len(f) > len(f2):
|
932
|
-
f[1:] += f2
|
933
|
-
else:
|
934
|
-
f[0:] += f2
|
935
|
-
fs[:, i] = f
|
936
|
-
with np.errstate(divide="ignore", invalid="ignore", under="ignore"):
|
937
|
-
fs = np.log(fs)
|
938
|
-
return fs
|
939
|
-
|
940
|
-
def gaussian(self, p, x):
|
941
|
-
return p[3] + p[2] * np.exp(-((x - p[0]) ** 2) / (2 * p[1] ** 2))
|
942
|
-
|
943
|
-
def tukey(self, p, x):
|
944
|
-
pos, std, alpha, height, background = p
|
945
|
-
alpha = np.clip(alpha, 0, 1)
|
946
|
-
pi = np.pi
|
947
|
-
inv_alpha = 1 - alpha
|
948
|
-
width = std / (1 - alpha * 0.5)
|
949
|
-
xx = (np.abs(x - pos) - (width * 0.5 * inv_alpha)) / (width * 0.5 * alpha)
|
950
|
-
xx = np.clip(xx, 0, 1)
|
951
|
-
return (0.5 + np.cos(pi * xx) * 0.5) * height + background
|
952
|
-
|
953
|
-
def sinlet(self, p, x):
|
954
|
-
std = p[1] * 2.5
|
955
|
-
lin = np.maximum(0, std - np.abs(p[0] - x)) * 0.5 * np.pi / std
|
956
|
-
return p[3] + p[2] * np.sin(lin)
|
957
|
-
|
958
|
-
def _px(self, detector_width, abs_pos, near_std):
|
959
|
-
sym_range = None
|
960
|
-
if abs_pos is not None:
|
961
|
-
if self.cor_options["crop_around_cor"]:
|
962
|
-
sym_range = int(abs_pos - near_std * 2), int(abs_pos + near_std * 2)
|
963
|
-
|
964
|
-
window = self.cor_options["near_width"]
|
965
|
-
if sym_range is not None:
|
966
|
-
xx_from = max(window, sym_range[0])
|
967
|
-
xx_to = max(xx_from, min(detector_width - window, sym_range[1]))
|
968
|
-
if xx_from == xx_to:
|
969
|
-
sym_range = None
|
970
|
-
if sym_range is None:
|
971
|
-
xx_from = window
|
972
|
-
xx_to = detector_width - window
|
973
|
-
|
974
|
-
xx = np.arange(xx_from, xx_to, self.cor_options["near_step"])
|
975
|
-
|
976
|
-
return xx
|
977
|
-
|
978
|
-
def _symmetry_correlation(self, px, array, angles):
|
979
|
-
window = self.cor_options["near_width"]
|
980
|
-
if self.cor_options["shift_sino"]:
|
981
|
-
shift_index = np.argmin(np.abs(angles - np.pi)) - np.argmin(np.abs(angles - 0))
|
982
|
-
else:
|
983
|
-
shift_index = None
|
984
|
-
px_from = int(px[0])
|
985
|
-
px_to = int(np.ceil(px[-1]))
|
986
|
-
f_coef = np.empty(len(px))
|
987
|
-
f_array = self._freq_radio(array, px_from - window, px_to + window)
|
988
|
-
if shift_index is not None:
|
989
|
-
shift_array = np.empty(array.shape, dtype=array.dtype)
|
990
|
-
shift_array[0 : len(shift_array) - shift_index, :] = array[shift_index:, :]
|
991
|
-
shift_array[len(shift_array) - shift_index :, :] = array[:shift_index, :]
|
992
|
-
f_shift_array = self._freq_radio(shift_array, px_from - window, px_to + window)
|
993
|
-
else:
|
994
|
-
f_shift_array = f_array
|
995
|
-
|
996
|
-
for j, x in enumerate(px):
|
997
|
-
i = int(np.floor(x))
|
998
|
-
if x - i > 0.4: # TO DO : Specific to near_step = 0.5?
|
999
|
-
f_left = f_array[:, i - window : i]
|
1000
|
-
f_right = f_shift_array[:, i + 1 : i + window + 1][:, ::-1]
|
1001
|
-
else:
|
1002
|
-
f_left = f_array[:, i - window : i]
|
1003
|
-
f_right = f_shift_array[:, i : i + window][:, ::-1]
|
1004
|
-
with np.errstate(divide="ignore", invalid="ignore"):
|
1005
|
-
f_coef[j] = np.sum(np.abs(f_left - f_right))
|
1006
|
-
return f_coef
|
1007
|
-
|
1008
|
-
def _cor_correlation(self, px, abs_pos, near_std):
|
1009
|
-
if abs_pos is not None:
|
1010
|
-
signal = self.cor_options["near_shape"]
|
1011
|
-
weight = self.cor_options["near_weight"]
|
1012
|
-
alpha = self.cor_options["near_alpha"]
|
1013
|
-
if signal == "sinlet":
|
1014
|
-
coef = self.sinlet((abs_pos, near_std, -weight, 1), px)
|
1015
|
-
elif signal == "gaussian":
|
1016
|
-
coef = self.gaussian((abs_pos, near_std, -weight, 1), px)
|
1017
|
-
elif signal == "tukey":
|
1018
|
-
coef = self.tukey((abs_pos, near_std * 2, alpha, -weight, 1), px)
|
1019
|
-
else:
|
1020
|
-
raise ValueError("Shape unsupported")
|
1021
|
-
else:
|
1022
|
-
coef = np.ones_like(px)
|
1023
|
-
return coef
|
1024
|
-
|
1025
|
-
def find_shift(
|
1026
|
-
self,
|
1027
|
-
img_1,
|
1028
|
-
img_2,
|
1029
|
-
angles,
|
1030
|
-
side,
|
1031
|
-
roi_yxhw=None,
|
1032
|
-
median_filt_shape=None,
|
1033
|
-
padding_mode=None,
|
1034
|
-
peak_fit_radius=1,
|
1035
|
-
high_pass=None,
|
1036
|
-
low_pass=None,
|
1037
|
-
):
|
1038
|
-
sinos = np.vstack([img_1, np.fliplr(img_2).copy()])
|
1039
|
-
detector_width = sinos.shape[1]
|
1040
|
-
|
1041
|
-
increment = np.abs(angles[0] - angles[1])
|
1042
|
-
if np.abs(angles[0] - angles[-1]) < (360 - 0.5) * np.pi / 180 - increment:
|
1043
|
-
self.logger.warning("Not enough angles, estimator skipped")
|
1044
|
-
return None
|
1045
|
-
|
1046
|
-
near_pos = self.cor_options.get("near_pos", None) # A RELATIVE estimation of the COR
|
1047
|
-
|
1048
|
-
# Default coarse estimate to center of detector
|
1049
|
-
# if no one is given either in NX or by user.
|
1050
|
-
if near_pos is None:
|
1051
|
-
self.logger.warning("No initial guess was found (from metadata or user) for CoR")
|
1052
|
-
self.logger.warning("Setting initial guess to center of detector.")
|
1053
|
-
if side == "center":
|
1054
|
-
abs_pos = detector_width // 2
|
1055
|
-
elif side == "left":
|
1056
|
-
abs_pos = detector_width // 4
|
1057
|
-
elif side == "right":
|
1058
|
-
abs_pos = detector_width * 3 // 4
|
1059
|
-
elif side == "near":
|
1060
|
-
abs_pos = detector_width // 2
|
1061
|
-
else:
|
1062
|
-
raise ValueError(f"side '{side}' is not handled")
|
1063
|
-
elif isinstance(near_pos, (int, float)): # Convert RELATIVE to ABSOLUTE position
|
1064
|
-
abs_pos = near_pos + detector_width / 2
|
1065
|
-
|
1066
|
-
near_std = None
|
1067
|
-
if abs_pos is not None:
|
1068
|
-
near_std = self.cor_options["near_std"]
|
1069
|
-
|
1070
|
-
px = self._px(detector_width, abs_pos, near_std)
|
1071
|
-
|
1072
|
-
coef_f = self._symmetry_correlation(
|
1073
|
-
px,
|
1074
|
-
sinos,
|
1075
|
-
angles,
|
1076
|
-
)
|
1077
|
-
coef_p = self._cor_correlation(px, abs_pos, near_std)
|
1078
|
-
coef = coef_f * coef_p
|
1079
|
-
|
1080
|
-
if len(px) > 0:
|
1081
|
-
if self.cor_options["refine"]:
|
1082
|
-
f_vals, f_pos = self.extract_peak_regions_1d(-coef, peak_radius=20, cc_coords=px)
|
1083
|
-
cor, _ = self.refine_max_position_1d(f_vals, fx=f_pos, return_vertex_val=True)
|
1084
|
-
else:
|
1085
|
-
cor = px[np.argmin(coef)]
|
1086
|
-
cor = cor - detector_width / 2
|
1087
|
-
else:
|
1088
|
-
cor = None
|
1089
|
-
|
1090
|
-
return cor
|
1091
|
-
|
1092
|
-
__call__ = find_shift
|
1093
|
-
|
1094
|
-
|
1095
|
-
class CenterOfRotationOctaveAccurate(AlignmentBase):
|
742
|
+
class CenterOfRotationOctaveAccurate(CenterOfRotation):
|
1096
743
|
"""This is a Python implementation of Octave/fastomo3/accurate COR estimator.
|
1097
744
|
The Octave 'accurate' function is renamed `local_correlation`.
|
1098
745
|
The Nabu standard `find_shift` has the same API as the other COR estimators (sliding, growing...)
|
1099
746
|
|
1100
|
-
The class inherits directly from AlignmentBase.
|
1101
747
|
"""
|
1102
748
|
|
1103
|
-
_default_cor_options = {
|
1104
|
-
"maxsize": [5, 5],
|
1105
|
-
"refine": None,
|
1106
|
-
"pmcc": False,
|
1107
|
-
"normalize": True,
|
1108
|
-
"low_pass": 0.01,
|
1109
|
-
"limz": 0.5,
|
1110
|
-
}
|
1111
|
-
|
1112
749
|
def _cut(self, im, nrows, ncols, new_center_row=None, new_center_col=None):
|
1113
750
|
"""Cuts a sub-matrix out of a larger matrix.
|
1114
751
|
Cuts in the center of the original matrix, except if new center is specified
|
@@ -1331,7 +968,7 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1331
968
|
|
1332
969
|
rapp_hist = []
|
1333
970
|
if np.sum(np.abs(cor_estimate) + 1 >= z1.shape):
|
1334
|
-
self.logger.
|
971
|
+
self.logger.debug(f"Approximate shift of [{cor_estimate[0]},{cor_estimate[1]}] is too large, setting [0 0]")
|
1335
972
|
cor_estimate = np.array([0, 0])
|
1336
973
|
maxsize = np.minimum(maxsize, np.floor((np.array(z1.shape) - 1) / 2)).astype(int)
|
1337
974
|
maxsize = np.minimum(maxsize, np.array(z1.shape) - np.abs(cor_estimate) - 1).astype(int)
|
@@ -1400,25 +1037,21 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1400
1037
|
cor_estimate = c
|
1401
1038
|
# Check that new shift estimate was not already done (avoid eternal loop)
|
1402
1039
|
if self._checkifpart(cor_estimate, rapp_hist):
|
1403
|
-
|
1404
|
-
self.logger.info(f"Stuck in loop?")
|
1040
|
+
self.logger.debug("Stuck in loop?")
|
1405
1041
|
refine = True
|
1406
1042
|
shiftfound = True
|
1407
1043
|
c = np.array([np.nan, np.nan])
|
1408
1044
|
else:
|
1409
1045
|
rapp_hist.append(cor_estimate)
|
1410
|
-
|
1411
|
-
self.logger.info(f"Changing shift estimate: {cor_estimate}")
|
1046
|
+
self.logger.debug(f"Changing shift estimate: {cor_estimate}")
|
1412
1047
|
maxsize = np.minimum(maxsize, np.array(z1.shape) - np.abs(cor_estimate) - 1).astype(int)
|
1413
1048
|
if (maxsize == 0).sum():
|
1414
|
-
|
1415
|
-
self.logger.info(f"Edge of image reached")
|
1049
|
+
self.logger.debug("Edge of image reached")
|
1416
1050
|
refine = False
|
1417
1051
|
shiftfound = True
|
1418
1052
|
c = np.array([np.nan, np.nan])
|
1419
1053
|
elif len(rapp_hist) > 0:
|
1420
|
-
|
1421
|
-
self.logger.info("\n")
|
1054
|
+
self.logger.debug("\n")
|
1422
1055
|
|
1423
1056
|
####################################
|
1424
1057
|
# refine result; useful when shifts are not integer values
|
@@ -1455,102 +1088,31 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1455
1088
|
|
1456
1089
|
def find_shift(
|
1457
1090
|
self,
|
1458
|
-
img_1
|
1459
|
-
img_2
|
1460
|
-
side
|
1091
|
+
img_1,
|
1092
|
+
img_2,
|
1093
|
+
side="center",
|
1461
1094
|
roi_yxhw=None,
|
1462
1095
|
median_filt_shape=None,
|
1463
1096
|
padding_mode=None,
|
1464
1097
|
low_pass=0.01,
|
1465
1098
|
high_pass=None,
|
1099
|
+
maxsize=[5, 5],
|
1100
|
+
refine=None,
|
1101
|
+
pmcc=False,
|
1102
|
+
normalize=True,
|
1103
|
+
limz=0.5,
|
1104
|
+
return_relative_to_middle=None,
|
1466
1105
|
):
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
1475
|
-
The output of this function, allows to compute motor movements for
|
1476
|
-
aligning the sample rotation axis. Given the following values:
|
1477
|
-
|
1478
|
-
- L1: distance from source to motor
|
1479
|
-
- L2: distance from source to detector
|
1480
|
-
- ps: physical pixel size
|
1481
|
-
- v: output of this function
|
1482
|
-
|
1483
|
-
displacement of motor = (L1 / L2 * ps) * v
|
1484
|
-
|
1485
|
-
Parameters
|
1486
|
-
----------
|
1487
|
-
img_1: numpy.ndarray
|
1488
|
-
First image
|
1489
|
-
img_2: numpy.ndarray
|
1490
|
-
Second image, it needs to have been flipped already (e.g. using numpy.fliplr).
|
1491
|
-
side: string
|
1492
|
-
Expected region of the CoR. Must be 'center' in that case.
|
1493
|
-
roi_yxhw: (2, ) or (4, ) numpy.ndarray, tuple, or array, optional
|
1494
|
-
4 elements vector containing: vertical and horizontal coordinates
|
1495
|
-
of first pixel, plus height and width of the Region of Interest (RoI).
|
1496
|
-
Or a 2 elements vector containing: plus height and width of the
|
1497
|
-
centered Region of Interest (RoI).
|
1498
|
-
Default is None -> deactivated.
|
1499
|
-
The ROI will be used for the global estimate.
|
1500
|
-
median_filt_shape: (2, ) numpy.ndarray, tuple, or array, optional
|
1501
|
-
Shape of the median filter window. Default is None -> deactivated.
|
1502
|
-
padding_mode: str in numpy.pad's mode list, optional
|
1503
|
-
Padding mode, which determines the type of convolution. If None or
|
1504
|
-
'wrap' are passed, this resorts to the traditional circular convolution.
|
1505
|
-
If 'edge' or 'constant' are passed, it results in a linear convolution.
|
1506
|
-
Default is the circular convolution.
|
1507
|
-
All options are:
|
1508
|
-
None | 'constant' | 'edge' | 'linear_ramp' | 'maximum' | 'mean'
|
1509
|
-
| 'median' | 'minimum' | 'reflect' | 'symmetric' |'wrap'
|
1510
|
-
low_pass: float or sequence of two floats
|
1511
|
-
Low-pass filter properties, as described in `nabu.misc.fourier_filters`
|
1512
|
-
high_pass: float or sequence of two floats
|
1513
|
-
High-pass filter properties, as described in `nabu.misc.fourier_filters`
|
1514
|
-
|
1515
|
-
Raises
|
1516
|
-
------
|
1517
|
-
ValueError
|
1518
|
-
In case images are not 2-dimensional or have different sizes.
|
1519
|
-
|
1520
|
-
Returns
|
1521
|
-
-------
|
1522
|
-
float
|
1523
|
-
Estimated center of rotation position from the center of the RoI in pixels.
|
1524
|
-
|
1525
|
-
Examples
|
1526
|
-
--------
|
1527
|
-
The following code computes the center of rotation position for two
|
1528
|
-
given images in a tomography scan, where the second image is taken at
|
1529
|
-
180 degrees from the first.
|
1530
|
-
|
1531
|
-
>>> radio1 = data[0, :, :]
|
1532
|
-
... radio2 = np.fliplr(data[1, :, :])
|
1533
|
-
... CoR_calc = CenterOfRotationOctaveAccurate()
|
1534
|
-
... cor_position = CoR_calc.find_shift(radio1, radio2)
|
1535
|
-
|
1536
|
-
Or for noisy images:
|
1537
|
-
|
1538
|
-
>>> cor_position = CoR_calc.find_shift(radio1, radio2, median_filt_shape=(3, 3))
|
1539
|
-
"""
|
1540
|
-
|
1541
|
-
self.logger.info(
|
1542
|
-
f"Estimation of the COR with following options: high_pass={high_pass}, low_pass={low_pass}, limz={self.cor_options['limz']}."
|
1543
|
-
)
|
1544
|
-
|
1545
|
-
self._check_img_pair_sizes(img_1, img_2)
|
1546
|
-
|
1547
|
-
if side != "center":
|
1548
|
-
self.logger.fatal(
|
1549
|
-
"The accurate algorithm cannot handle half acq. Use 'near', 'fourier-angles', 'sliding-window' or 'growing-window' instead."
|
1550
|
-
)
|
1551
|
-
raise ValueError(
|
1552
|
-
"The accurate algorithm cannot handle half acq. Use 'near', 'fourier-angles', 'sliding-window' or 'growing-window' instead."
|
1106
|
+
# COMPAT.
|
1107
|
+
if return_relative_to_middle is None:
|
1108
|
+
deprecation_warning(
|
1109
|
+
"The current default behavior is to return the shift relative the the middle of the image. In a future release, this function will return the shift relative to the left-most pixel. To keep the current behavior, please use 'return_relative_to_middle=True'.",
|
1110
|
+
do_print=True,
|
1111
|
+
func_name="CenterOfRotationOctaveAccurate.find_shift",
|
1553
1112
|
)
|
1113
|
+
return_relative_to_middle = True # the kwarg above will be False by default in a future release
|
1114
|
+
# ---
|
1115
|
+
self._check_img_pair_sizes(img_1, img_2)
|
1554
1116
|
|
1555
1117
|
img_shape = img_2.shape
|
1556
1118
|
roi_yxhw = self._determine_roi(img_shape, roi_yxhw)
|
@@ -1572,10 +1134,10 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1572
1134
|
shift -= np.array(img_shape) // 2
|
1573
1135
|
|
1574
1136
|
# The real "accurate" starts here (i.e. the octave findshift() func).
|
1575
|
-
if np.abs(shift[0]) > 10 *
|
1137
|
+
if np.abs(shift[0]) > 10 * limz:
|
1576
1138
|
# This is suspiscious. We don't trust results of correlate.
|
1577
|
-
self.logger.
|
1578
|
-
self.logger.
|
1139
|
+
self.logger.warning("Pre-correlation yields {shift[0]} pixels vertical motion")
|
1140
|
+
self.logger.warning("We do not consider it.")
|
1579
1141
|
shift = (0, 0)
|
1580
1142
|
|
1581
1143
|
# Limit the size of region for comparison to cutsize in both directions.
|
@@ -1590,32 +1152,44 @@ class CenterOfRotationOctaveAccurate(AlignmentBase):
|
|
1590
1152
|
shift = oldshift + self._local_correlation(
|
1591
1153
|
im0,
|
1592
1154
|
im1,
|
1593
|
-
maxsize=
|
1594
|
-
refine=
|
1595
|
-
pmcc=
|
1596
|
-
normalize=
|
1155
|
+
maxsize=maxsize,
|
1156
|
+
refine=refine,
|
1157
|
+
pmcc=pmcc,
|
1158
|
+
normalize=normalize,
|
1597
1159
|
)
|
1598
1160
|
else:
|
1599
1161
|
shift = self._local_correlation(
|
1600
1162
|
img_1,
|
1601
1163
|
img_2,
|
1602
|
-
maxsize=
|
1164
|
+
maxsize=maxsize,
|
1603
1165
|
cor_estimate=oldshift,
|
1604
|
-
refine=
|
1605
|
-
pmcc=
|
1606
|
-
normalize=
|
1166
|
+
refine=refine,
|
1167
|
+
pmcc=pmcc,
|
1168
|
+
normalize=normalize,
|
1607
1169
|
)
|
1608
1170
|
if ((shift - oldshift) ** 2).sum() > 4:
|
1609
|
-
self.logger.
|
1610
|
-
self.logger.
|
1171
|
+
self.logger.warning(f"Pre-correlation ({oldshift}) and accurate correlation ({shift}) are not consistent.")
|
1172
|
+
self.logger.warning("Please check!!!")
|
1611
1173
|
|
1612
1174
|
offset = shift[1] / 2
|
1613
1175
|
|
1614
|
-
if np.abs(shift[0]) >
|
1615
|
-
self.logger.
|
1616
|
-
self.logger.
|
1617
|
-
self.logger.
|
1176
|
+
if np.abs(shift[0]) > limz:
|
1177
|
+
self.logger.debug("Verify alignment or sample motion.")
|
1178
|
+
self.logger.debug(f"Verical motion: {shift[0]} pixels.")
|
1179
|
+
self.logger.debug(f"Offset?: {offset} pixels.")
|
1618
1180
|
else:
|
1619
|
-
self.logger.
|
1181
|
+
self.logger.debug(f"Offset?: {offset} pixels.")
|
1182
|
+
|
1183
|
+
if not (return_relative_to_middle):
|
1184
|
+
offset += (img_shape[1] - 1) / 2
|
1620
1185
|
|
1621
1186
|
return offset
|
1187
|
+
|
1188
|
+
|
1189
|
+
# COMPAT.
|
1190
|
+
from .cor_sino import CenterOfRotationFourierAngles as CenterOfRotationFourierAngles0
|
1191
|
+
|
1192
|
+
CenterOfRotationFourierAngles = deprecated_class(
|
1193
|
+
"CenterOfRotationFourierAngles was moved to nabu.estimation.cor_sino", do_print=True
|
1194
|
+
)(CenterOfRotationFourierAngles0)
|
1195
|
+
#
|