nabu 2024.1.9__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.
Files changed (151) hide show
  1. nabu/__init__.py +1 -1
  2. nabu/app/bootstrap.py +2 -3
  3. nabu/app/cast_volume.py +4 -2
  4. nabu/app/cli_configs.py +5 -0
  5. nabu/app/composite_cor.py +1 -1
  6. nabu/app/create_distortion_map_from_poly.py +5 -6
  7. nabu/app/diag_to_pix.py +7 -19
  8. nabu/app/diag_to_rot.py +14 -29
  9. nabu/app/double_flatfield.py +32 -44
  10. nabu/app/parse_reconstruction_log.py +3 -0
  11. nabu/app/reconstruct.py +53 -15
  12. nabu/app/reconstruct_helical.py +2 -2
  13. nabu/app/stitching.py +27 -13
  14. nabu/app/tests/test_reduce_dark_flat.py +4 -1
  15. nabu/cuda/kernel.py +11 -2
  16. nabu/cuda/processing.py +2 -2
  17. nabu/cuda/src/cone.cu +77 -0
  18. nabu/cuda/src/hierarchical_backproj.cu +271 -0
  19. nabu/cuda/utils.py +0 -6
  20. nabu/estimation/alignment.py +5 -19
  21. nabu/estimation/cor.py +173 -599
  22. nabu/estimation/cor_sino.py +356 -26
  23. nabu/estimation/focus.py +63 -11
  24. nabu/estimation/tests/test_cor.py +124 -58
  25. nabu/estimation/tests/test_focus.py +6 -6
  26. nabu/estimation/tilt.py +2 -1
  27. nabu/estimation/utils.py +5 -33
  28. nabu/io/__init__.py +1 -1
  29. nabu/io/cast_volume.py +1 -1
  30. nabu/io/reader.py +416 -21
  31. nabu/io/tests/test_readers.py +422 -0
  32. nabu/io/tests/test_writers.py +1 -102
  33. nabu/io/writer.py +4 -433
  34. nabu/opencl/kernel.py +14 -3
  35. nabu/opencl/processing.py +8 -0
  36. nabu/pipeline/config_validators.py +5 -2
  37. nabu/pipeline/datadump.py +12 -5
  38. nabu/pipeline/estimators.py +162 -188
  39. nabu/pipeline/fullfield/chunked.py +168 -92
  40. nabu/pipeline/fullfield/chunked_cuda.py +7 -3
  41. nabu/pipeline/fullfield/computations.py +2 -7
  42. nabu/pipeline/fullfield/dataset_validator.py +0 -4
  43. nabu/pipeline/fullfield/nabu_config.py +37 -13
  44. nabu/pipeline/fullfield/processconfig.py +22 -13
  45. nabu/pipeline/fullfield/reconstruction.py +13 -9
  46. nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
  47. nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
  48. nabu/pipeline/helical/helical_reconstruction.py +1 -1
  49. nabu/pipeline/params.py +21 -1
  50. nabu/pipeline/processconfig.py +1 -12
  51. nabu/pipeline/reader.py +146 -0
  52. nabu/pipeline/tests/test_estimators.py +44 -72
  53. nabu/pipeline/utils.py +4 -2
  54. nabu/pipeline/writer.py +10 -2
  55. nabu/preproc/ccd_cuda.py +1 -1
  56. nabu/preproc/ctf.py +14 -7
  57. nabu/preproc/ctf_cuda.py +2 -3
  58. nabu/preproc/double_flatfield.py +5 -12
  59. nabu/preproc/double_flatfield_cuda.py +2 -2
  60. nabu/preproc/flatfield.py +5 -1
  61. nabu/preproc/flatfield_cuda.py +5 -1
  62. nabu/preproc/phase.py +24 -73
  63. nabu/preproc/phase_cuda.py +5 -8
  64. nabu/preproc/tests/test_ctf.py +11 -7
  65. nabu/preproc/tests/test_flatfield.py +67 -122
  66. nabu/preproc/tests/test_paganin.py +54 -30
  67. nabu/processing/azim.py +206 -0
  68. nabu/processing/convolution_cuda.py +1 -1
  69. nabu/processing/fft_cuda.py +15 -17
  70. nabu/processing/histogram.py +2 -0
  71. nabu/processing/histogram_cuda.py +2 -1
  72. nabu/processing/kernel_base.py +3 -0
  73. nabu/processing/muladd_cuda.py +1 -0
  74. nabu/processing/padding_opencl.py +1 -1
  75. nabu/processing/roll_opencl.py +1 -0
  76. nabu/processing/rotation_cuda.py +2 -2
  77. nabu/processing/tests/test_fft.py +17 -10
  78. nabu/processing/unsharp_cuda.py +1 -1
  79. nabu/reconstruction/cone.py +104 -40
  80. nabu/reconstruction/fbp.py +3 -0
  81. nabu/reconstruction/fbp_base.py +7 -2
  82. nabu/reconstruction/filtering.py +20 -7
  83. nabu/reconstruction/filtering_cuda.py +7 -1
  84. nabu/reconstruction/hbp.py +424 -0
  85. nabu/reconstruction/mlem.py +99 -0
  86. nabu/reconstruction/reconstructor.py +2 -0
  87. nabu/reconstruction/rings_cuda.py +19 -19
  88. nabu/reconstruction/sinogram_cuda.py +1 -0
  89. nabu/reconstruction/sinogram_opencl.py +3 -1
  90. nabu/reconstruction/tests/test_cone.py +10 -5
  91. nabu/reconstruction/tests/test_deringer.py +7 -6
  92. nabu/reconstruction/tests/test_fbp.py +124 -10
  93. nabu/reconstruction/tests/test_filtering.py +13 -11
  94. nabu/reconstruction/tests/test_halftomo.py +30 -4
  95. nabu/reconstruction/tests/test_mlem.py +91 -0
  96. nabu/reconstruction/tests/test_reconstructor.py +8 -3
  97. nabu/resources/dataset_analyzer.py +142 -92
  98. nabu/resources/gpu.py +1 -0
  99. nabu/resources/nxflatfield.py +134 -125
  100. nabu/resources/templates/id16a_fluo.conf +42 -0
  101. nabu/resources/tests/test_extract.py +10 -0
  102. nabu/resources/tests/test_nxflatfield.py +2 -2
  103. nabu/stitching/alignment.py +80 -24
  104. nabu/stitching/config.py +105 -68
  105. nabu/stitching/definitions.py +1 -0
  106. nabu/stitching/frame_composition.py +68 -60
  107. nabu/stitching/overlap.py +91 -51
  108. nabu/stitching/single_axis_stitching.py +32 -0
  109. nabu/stitching/slurm_utils.py +6 -6
  110. nabu/stitching/stitcher/__init__.py +0 -0
  111. nabu/stitching/stitcher/base.py +124 -0
  112. nabu/stitching/stitcher/dumper/__init__.py +3 -0
  113. nabu/stitching/stitcher/dumper/base.py +94 -0
  114. nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
  115. nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
  116. nabu/stitching/stitcher/post_processing.py +555 -0
  117. nabu/stitching/stitcher/pre_processing.py +1068 -0
  118. nabu/stitching/stitcher/single_axis.py +484 -0
  119. nabu/stitching/stitcher/stitcher.py +0 -0
  120. nabu/stitching/stitcher/y_stitcher.py +13 -0
  121. nabu/stitching/stitcher/z_stitcher.py +45 -0
  122. nabu/stitching/stitcher_2D.py +278 -0
  123. nabu/stitching/tests/test_config.py +12 -37
  124. nabu/stitching/tests/test_frame_composition.py +33 -59
  125. nabu/stitching/tests/test_overlap.py +149 -7
  126. nabu/stitching/tests/test_utils.py +1 -1
  127. nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
  128. nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
  129. nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
  130. nabu/stitching/utils/__init__.py +1 -0
  131. nabu/stitching/utils/post_processing.py +281 -0
  132. nabu/stitching/utils/tests/test_post-processing.py +21 -0
  133. nabu/stitching/{utils.py → utils/utils.py} +79 -52
  134. nabu/stitching/y_stitching.py +27 -0
  135. nabu/stitching/z_stitching.py +32 -2263
  136. nabu/testutils.py +1 -152
  137. nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
  138. nabu/utils.py +158 -61
  139. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/METADATA +10 -3
  140. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/RECORD +144 -121
  141. nabu/io/tiffwriter_zmm.py +0 -99
  142. nabu/pipeline/fallback_utils.py +0 -149
  143. nabu/pipeline/helical/tests/test_accumulator.py +0 -158
  144. nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
  145. nabu/pipeline/helical/tests/test_strategy.py +0 -61
  146. nabu/pipeline/helical/utils.py +0 -51
  147. nabu/pipeline/tests/test_chunk_reader.py +0 -74
  148. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
  149. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/WHEEL +0 -0
  150. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
  151. {nabu-2024.1.9.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 scipy.fftpack import rfft
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
- from ..resources.utils import extract_parameters
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
- cor_options=None,
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 isinstance(self.cor_options.get("near_pos", None), (int, float)):
135
- near_pos = self.cor_options["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
- cor_options=None,
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
- The output of this function, allows to compute motor movements for
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
- img_1: numpy.ndarray
186
- First image
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', or 'center'. 'None' was given instead")
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
- near_pos = self.cor_options.get("near_pos", None)
272
- if near_pos is None:
226
+ if isinstance(side, str):
273
227
  if window_width is None:
274
- if side.lower() == "center":
275
- window_width = round(img_shape[-1] / 4.0 * 3.0)
228
+ if side == "center":
229
+ window_width = round(img_width / 4.0 * 3.0)
276
230
  else:
277
- window_width = round(img_shape[-1] / 10)
231
+ window_width = round(img_width / 10)
278
232
  window_shift = window_width // 2
279
233
  window_width = window_shift * 2 + 1
280
-
281
- win_1_start_seed = 0
282
- # number of pixels where the window will "slide".
283
- n = img_shape[-1] - window_width
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 = near_pos + img_shape[-1] // 2
286
- offset = min(img_shape[-1] - abs_pos, abs_pos) # distance to closest edge.
241
+ abs_pos = int(side + img_width // 2)
242
+ window_fraction = 0.1 # Hard-coded ?
287
243
 
288
- window_fraction = 0.4 # Hard-coded ?
289
- window_shift = int(np.floor(offset * window_fraction))
290
- window_width = 2 * window_shift + 1
244
+ window_width = round(window_fraction * img_width)
245
+ window_shift = window_width // 2
246
+ window_width = window_shift * 2 + 1
291
247
 
292
- sliding_shift = int(np.floor(offset * (1 - window_fraction))) - 1
293
- n = 2 * sliding_shift + 1
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
- if side.lower() == "right":
297
- win_2_start = 0
298
- elif side.lower() == "left":
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 isinstance(near_pos, Real):
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.lower() == "right" and win_ind_max == 0) or (side.lower() == "left" and win_ind_max == n):
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
- The output of this function, allows to compute motor movements for
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
- use_estimate_from_motor = "near_pos" in self.cor_options.keys() and isinstance(
495
- self.cor_options["near_pos"], (int, float)
496
- )
497
- use_estimate_from_motor = False # Not yet implemented.
498
- if use_estimate_from_motor:
499
- near_pos = self.cor_options["near_pos"]
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
- if side.lower() == "right":
503
- win_1_mid_start = img_lower_half_size
504
- win_1_mid_end = np.floor(img_shape[-1] * 3 / 2).astype(np.intp) - min_window_width
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
- The output of this function, allows to compute motor movements for
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 CenterOfRotationFourierAngles(CenterOfRotation):
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.info(f"Approximate shift of [{cor_estimate[0]},{cor_estimate[1]}] is too large, setting [0 0]")
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
- if self.verbose:
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
- if self.verbose:
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
- if self.verbose:
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
- if self.verbose:
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: np.ndarray,
1459
- img_2: np.ndarray,
1460
- side: str,
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
- """Automatically finds the Center of Rotation (CoR), given two images
1468
- (projections/radiographs). Suitable for half-aquisition scan.
1469
-
1470
- This method finds the half-shift between two opposite images, by
1471
- minimizing the variance of small ROI around a global COR estimate
1472
- (obtained by maximizing Fourier-space computed global correlations).
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 * self.cor_options["limz"]:
1137
+ if np.abs(shift[0]) > 10 * limz:
1576
1138
  # This is suspiscious. We don't trust results of correlate.
1577
- self.logger.info(f"Pre-correlation yields {shift[0]} pixels vertical motion")
1578
- self.logger.info(f"We do not consider it.")
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=self.cor_options["maxsize"],
1594
- refine=self.cor_options["refine"],
1595
- pmcc=self.cor_options["pmcc"],
1596
- normalize=self.cor_options["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=self.cor_options["maxsize"],
1164
+ maxsize=maxsize,
1603
1165
  cor_estimate=oldshift,
1604
- refine=self.cor_options["refine"],
1605
- pmcc=self.cor_options["pmcc"],
1606
- normalize=self.cor_options["normalize"],
1166
+ refine=refine,
1167
+ pmcc=pmcc,
1168
+ normalize=normalize,
1607
1169
  )
1608
1170
  if ((shift - oldshift) ** 2).sum() > 4:
1609
- self.logger.info(f"Pre-correlation ({oldshift}) and accurate correlation ({shift}) are not consistent.")
1610
- self.logger.info("Please check!!!")
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]) > self.cor_options["limz"]:
1615
- self.logger.info("Verify alignment or sample motion.")
1616
- self.logger.info(f"Verical motion: {shift[0]} pixels.")
1617
- self.logger.info(f"Offset?: {offset} pixels.")
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.info(f"Offset?: {offset} pixels.")
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
+ #