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
@@ -1,30 +1,16 @@
1
- """
2
- This module provides global definitions and methods to compute COR in extrem
3
- Half Acquisition mode
4
- """
5
-
6
- __authors__ = ["C. Nemoz", "H.Payno"]
7
- __license__ = "MIT"
8
- __date__ = "13/04/2021"
9
-
10
1
  import numpy as np
11
2
  from scipy.signal import convolve2d
3
+ from scipy.fft import rfft
4
+
5
+ from ..utils import deprecation_warning, is_scalar
12
6
  from ..resources.logger import LoggerOrPrint
13
7
 
8
+ try:
9
+ from algotom.prep.calculation import find_center_vo, find_center_360
14
10
 
15
- def schift(mat, val):
16
- ker = np.zeros((3, 3))
17
- s = 1.0
18
- if val < 0:
19
- s = -1.0
20
- val = s * val
21
- ker[1, 1] = 1 - val
22
- if s > 0:
23
- ker[1, 2] = val
24
- else:
25
- ker[1, 0] = val
26
- mat = convolve2d(mat, ker, mode="same")
27
- return mat
11
+ __have_algotom__ = True
12
+ except ImportError:
13
+ __have_algotom__ = False
28
14
 
29
15
 
30
16
  class SinoCor:
@@ -57,6 +43,21 @@ class SinoCor:
57
43
 
58
44
  self.window_width = round(self.sx / 5)
59
45
 
46
+ @staticmethod
47
+ def schift(mat, val):
48
+ ker = np.zeros((3, 3))
49
+ s = 1.0
50
+ if val < 0:
51
+ s = -1.0
52
+ val = s * val
53
+ ker[1, 1] = 1 - val
54
+ if s > 0:
55
+ ker[1, 2] = val
56
+ else:
57
+ ker[1, 0] = val
58
+ mat = convolve2d(mat, ker, mode="same")
59
+ return mat
60
+
60
61
  def overlap(self, side="right", window_width=None):
61
62
  """
62
63
  Compute COR by minimizing difference of circulating ROI
@@ -90,7 +91,7 @@ class SinoCor:
90
91
  imax = i
91
92
  self.cor_abs = self.sx - (imax + window_width + 1.0) / 2.0
92
93
  self.cor_rel = self.sx / 2 - (imax + window_width + 1.0) / 2.0
93
- else:
94
+ elif side == "left":
94
95
  for i in nr:
95
96
  imout = self.data1[:, i : i + window_width] - self.data2[:, self.sx - window_width : self.sx]
96
97
  diff = imout.max() - imout.min()
@@ -99,6 +100,8 @@ class SinoCor:
99
100
  imax = i
100
101
  self.cor_abs = (imax + window_width - 1.0) / 2
101
102
  self.cor_rel = self.cor_abs - self.sx / 2.0 - 1
103
+ else:
104
+ raise ValueError(f"Invalid side given ({side}). should be 'left' or 'right'")
102
105
  if imax < 1:
103
106
  self.logger.warning("sliding width %d seems too large!" % window_width)
104
107
  self.rcor_abs = round(self.cor_abs)
@@ -151,7 +154,7 @@ class SinoCor:
151
154
  x0 = xc1 + pix
152
155
  for isf in isfr:
153
156
  if isf != 0:
154
- ims = schift(self.data1[:, x0 : x0 + xwin].copy(), -p_sign * isf)
157
+ ims = self.schift(self.data1[:, x0 : x0 + xwin].copy(), -p_sign * isf)
155
158
  else:
156
159
  ims = self.data1[:, x0 : x0 + xwin]
157
160
 
@@ -175,9 +178,336 @@ class SinoCorInterface:
175
178
  def __init__(self, logger=None, **kwargs):
176
179
  self._logger = logger
177
180
 
178
- def find_shift(self, img_1, img_2, side="right", window_width=None, neighborhood=7, shift_value=0.1, **kwargs):
181
+ def find_shift(
182
+ self,
183
+ img_1,
184
+ img_2,
185
+ side="right",
186
+ window_width=None,
187
+ neighborhood=7,
188
+ shift_value=0.1,
189
+ return_relative_to_middle=None,
190
+ **kwargs,
191
+ ):
192
+
193
+ # COMPAT.
194
+ if return_relative_to_middle is None:
195
+ deprecation_warning(
196
+ "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'.",
197
+ do_print=True,
198
+ func_name="CenterOfRotationCoarseToFine.find_shift",
199
+ )
200
+ return_relative_to_middle = True # the kwarg above will be False by default in a future release
201
+ # ---
202
+
179
203
  cor_finder = SinoCor(img_1, img_2, logger=self._logger)
180
204
  cor_finder.estimate_cor_coarse(side=side, window_width=window_width)
181
205
  cor = cor_finder.estimate_cor_fine(neighborhood=neighborhood, shift_value=shift_value)
182
206
  # offset will be added later - keep compatibility with result from AlignmentBase.find_shift()
183
- return cor - img_1.shape[1] / 2
207
+ if return_relative_to_middle:
208
+ return cor - (img_1.shape[1] - 1) / 2
209
+ else:
210
+ return cor
211
+
212
+
213
+ class CenterOfRotationFourierAngles:
214
+ """This CoR estimation algo is proposed by V. Valls (BCU). It is based on the Fourier
215
+ transform of the columns on the sinogram.
216
+ It requires an initial guesss of the CoR wich is retrieved from
217
+ dataset_info.dataset_scanner.x_rotation_axis_pixel_position. It is assumed in mm and pixel size in um.
218
+ Options are (for the moment) hard-coded in the SinoCORFinder.cor_finder.extra_options dict.
219
+ """
220
+
221
+ def __init__(self, *args, **kwargs):
222
+ pass
223
+
224
+ def _convert_from_fft_2_fftpack_format(self, f_signal, o_signal_length):
225
+ """
226
+ Converts a scipy.fft.rfft into the (legacy) scipy.fftpack.rfft format.
227
+ The fftpack.rfft returns a (roughly) twice as long array as fft.rfft as the latter returns an array
228
+ of complex numbers wheras the former returns an array with real and imag parts in consecutive
229
+ spots in the array.
230
+
231
+ Parameters
232
+ ----------
233
+ f_signal : array_like
234
+ The output of scipy.fft.rfft(signal)
235
+ o_signal_length : int
236
+ Size of the original signal (before FT).
237
+
238
+ Returns
239
+ -------
240
+ out
241
+ The rfft converted to the fftpack.rfft format (roughly twice as long).
242
+ """
243
+ out = np.zeros(o_signal_length, dtype=np.float32)
244
+ if o_signal_length % 2 == 0:
245
+ out[0] = f_signal[0].real
246
+ out[1::2] = f_signal[1:].real
247
+ out[2::2] = f_signal[1:-1].imag
248
+ else:
249
+ out[0] = f_signal[0].real
250
+ out[1::2] = f_signal[1:].real
251
+ out[2::2] = f_signal[1:].imag
252
+ return out
253
+
254
+ def _freq_radio(self, sinos, ifrom, ito):
255
+ size = (sinos.shape[0] + sinos.shape[0] % 2) // 2
256
+ fs = np.empty((size, sinos.shape[1]))
257
+ for i in range(ifrom, ito):
258
+ line = sinos[:, i]
259
+ f_signal = rfft(line)
260
+ f_signal = self._convert_from_fft_2_fftpack_format(f_signal, line.shape[0])
261
+ f = np.abs(f_signal[: (f_signal.size - 1) // 2 + 1])
262
+ f2 = np.abs(f_signal[(f_signal.size - 1) // 2 + 1 :][::-1])
263
+ if len(f) > len(f2):
264
+ f[1:] += f2
265
+ else:
266
+ f[0:] += f2
267
+ fs[:, i] = f
268
+ with np.errstate(divide="ignore", invalid="ignore", under="ignore"):
269
+ fs = np.log(fs)
270
+ return fs
271
+
272
+ def gaussian(self, p, x):
273
+ return p[3] + p[2] * np.exp(-((x - p[0]) ** 2) / (2 * p[1] ** 2))
274
+
275
+ def tukey(self, p, x):
276
+ pos, std, alpha, height, background = p
277
+ alpha = np.clip(alpha, 0, 1)
278
+ pi = np.pi
279
+ inv_alpha = 1 - alpha
280
+ width = std / (1 - alpha * 0.5)
281
+ xx = (np.abs(x - pos) - (width * 0.5 * inv_alpha)) / (width * 0.5 * alpha)
282
+ xx = np.clip(xx, 0, 1)
283
+ return (0.5 + np.cos(pi * xx) * 0.5) * height + background
284
+
285
+ def sinlet(self, p, x):
286
+ std = p[1] * 2.5
287
+ lin = np.maximum(0, std - np.abs(p[0] - x)) * 0.5 * np.pi / std
288
+ return p[3] + p[2] * np.sin(lin)
289
+
290
+ def _px(self, detector_width, abs_pos, near_width, near_std, crop_around_cor, near_step):
291
+ sym_range = None
292
+ if abs_pos is not None:
293
+ if crop_around_cor:
294
+ sym_range = int(abs_pos - near_std * 2), int(abs_pos + near_std * 2)
295
+
296
+ window = near_width
297
+ if sym_range is not None:
298
+ xx_from = max(window, sym_range[0])
299
+ xx_to = max(xx_from, min(detector_width - window, sym_range[1]))
300
+ if xx_from == xx_to:
301
+ sym_range = None
302
+ if sym_range is None:
303
+ xx_from = window
304
+ xx_to = detector_width - window
305
+
306
+ xx = np.arange(xx_from, xx_to, near_step)
307
+
308
+ return xx
309
+
310
+ def _symmetry_correlation(self, px, array, angles, window, shift_sino):
311
+ if shift_sino:
312
+ shift_index = np.argmin(np.abs(angles - np.pi)) - np.argmin(np.abs(angles - 0))
313
+ else:
314
+ shift_index = None
315
+ px_from = int(px[0])
316
+ px_to = int(np.ceil(px[-1]))
317
+ f_coef = np.empty(len(px))
318
+ f_array = self._freq_radio(array, px_from - window, px_to + window)
319
+ if shift_index is not None:
320
+ shift_array = np.empty(array.shape, dtype=array.dtype)
321
+ shift_array[0 : len(shift_array) - shift_index, :] = array[shift_index:, :]
322
+ shift_array[len(shift_array) - shift_index :, :] = array[:shift_index, :]
323
+ f_shift_array = self._freq_radio(shift_array, px_from - window, px_to + window)
324
+ else:
325
+ f_shift_array = f_array
326
+
327
+ for j, x in enumerate(px):
328
+ i = int(np.floor(x))
329
+ if x - i > 0.4: # TO DO : Specific to near_step = 0.5?
330
+ f_left = f_array[:, i - window : i]
331
+ f_right = f_shift_array[:, i + 1 : i + window + 1][:, ::-1]
332
+ else:
333
+ f_left = f_array[:, i - window : i]
334
+ f_right = f_shift_array[:, i : i + window][:, ::-1]
335
+ with np.errstate(divide="ignore", invalid="ignore"):
336
+ f_coef[j] = np.sum(np.abs(f_left - f_right))
337
+ return f_coef
338
+
339
+ def _cor_correlation(self, px, abs_pos, near_std, signal, near_weight, near_alpha):
340
+ if abs_pos is not None:
341
+ if signal == "sinlet":
342
+ coef = self.sinlet((abs_pos, near_std, -near_weight, 1), px)
343
+ elif signal == "gaussian":
344
+ coef = self.gaussian((abs_pos, near_std, -near_weight, 1), px)
345
+ elif signal == "tukey":
346
+ coef = self.tukey((abs_pos, near_std * 2, near_alpha, -near_weight, 1), px)
347
+ else:
348
+ raise ValueError("Shape unsupported")
349
+ else:
350
+ coef = np.ones_like(px)
351
+ return coef
352
+
353
+ def find_shift(
354
+ self,
355
+ sino,
356
+ angles=None,
357
+ side="center",
358
+ near_std=100,
359
+ near_width=20,
360
+ shift_sino=True,
361
+ crop_around_cor=False,
362
+ signal="tukey",
363
+ near_weight=0.1,
364
+ near_alpha=0.5,
365
+ near_step=0.5,
366
+ return_relative_to_middle=None,
367
+ ):
368
+ detector_width = sino.shape[1]
369
+
370
+ # COMPAT.
371
+ if return_relative_to_middle is None:
372
+ deprecation_warning(
373
+ "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'.",
374
+ do_print=True,
375
+ func_name="CenterOfRotationFourierAngles.find_shift",
376
+ )
377
+ return_relative_to_middle = True # the kwarg above will be False by default in a future release
378
+ # ---
379
+
380
+ if angles is None:
381
+ angles = np.linspace(0, 2 * np.pi, sino.shape[0], endpoint=True)
382
+ increment = np.abs(angles[0] - angles[1])
383
+ if np.abs(angles[0] - angles[-1]) < (360 - 0.5) * np.pi / 180 - increment:
384
+ raise ValueError("Not enough angles, estimator skipped")
385
+
386
+ if is_scalar(side):
387
+ abs_pos = side
388
+ # COMPAT.
389
+ elif side == "near":
390
+ deprecation_warning(
391
+ "side='near' is deprecated, please use side=<a scalar>", do_print=True, func_name="fourier_angles_near"
392
+ )
393
+ abs_pos = detector_width // 2
394
+ ##.
395
+ elif side == "center":
396
+ abs_pos = detector_width // 2
397
+ elif side == "left":
398
+ abs_pos = detector_width // 4
399
+ elif side == "right":
400
+ abs_pos = detector_width * 3 // 4
401
+ else:
402
+ raise ValueError(f"side '{side}' is not handled")
403
+
404
+ px = self._px(detector_width, abs_pos, near_width, near_std, crop_around_cor, near_step)
405
+
406
+ coef_f = self._symmetry_correlation(
407
+ px,
408
+ sino,
409
+ angles,
410
+ near_width,
411
+ shift_sino,
412
+ )
413
+ coef_p = self._cor_correlation(px, abs_pos, near_std, signal, near_weight, near_alpha)
414
+ coef = coef_f * coef_p
415
+
416
+ if len(px) > 0:
417
+ cor = px[np.argmin(coef)] - (detector_width - 1) / 2
418
+ else:
419
+ # raise ValueError ?
420
+ cor = None
421
+ if not (return_relative_to_middle):
422
+ cor += (detector_width - 1) / 2
423
+ return cor
424
+
425
+ __call__ = find_shift
426
+
427
+
428
+ class CenterOfRotationVo:
429
+ """
430
+ A wrapper around algotom 'find_center_vo' and 'find_center_360'.
431
+
432
+ Nghia T. Vo, Michael Drakopoulos, Robert C. Atwood, and Christina Reinhard,
433
+ "Reliable method for calculating the center of rotation in parallel-beam tomography,"
434
+ Opt. Express 22, 19078-19086 (2014)
435
+ """
436
+
437
+ default_extra_options = {}
438
+
439
+ def __init__(self, logger=None, verbose=False, extra_options=None):
440
+ if not (__have_algotom__):
441
+ raise ImportError("Need the 'algotom' package")
442
+ self.extra_options = self.default_extra_options.copy()
443
+ self.extra_options.update(extra_options or {})
444
+
445
+ def find_shift(
446
+ self,
447
+ sino,
448
+ halftomo=False,
449
+ is_360=False,
450
+ win_width=100,
451
+ side="center",
452
+ search_width_fraction=0.1,
453
+ step=0.25,
454
+ radius=4,
455
+ ratio=0.5,
456
+ dsp=True,
457
+ ncore=None,
458
+ hor_drop=None,
459
+ ver_drop=None,
460
+ denoise=True,
461
+ norm=True,
462
+ use_overlap=False,
463
+ return_relative_to_middle=None,
464
+ ):
465
+ # COMPAT.
466
+ if return_relative_to_middle is None:
467
+ deprecation_warning(
468
+ "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'.",
469
+ do_print=True,
470
+ func_name="CenterOfRotationVo.find_shift",
471
+ )
472
+ return_relative_to_middle = True # the kwarg above will be False by default in a future release
473
+ # ---
474
+
475
+ if halftomo:
476
+ side_algotom = {"left": 0, "right": 1}.get(side, None)
477
+ cor, _, _, _ = find_center_360(
478
+ sino, win_width, side=side_algotom, denoise=denoise, norm=norm, use_overlap=use_overlap, ncore=ncore
479
+ )
480
+ else:
481
+ if is_360 and not (halftomo):
482
+ # Take only one part of the sinogram and use "find_center_vo" - this works better in this case
483
+ sino = sino[: sino.shape[0] // 2]
484
+
485
+ sino_width = sino.shape[-1]
486
+ search_width = int(search_width_fraction * sino_width)
487
+
488
+ if side == "left":
489
+ start, stop = 0, search_width
490
+ elif side == "center":
491
+ start, stop = sino_width // 2 - search_width, sino_width // 2 + search_width
492
+ elif side == "right":
493
+ start, stop = sino_width - search_width, sino_width
494
+ elif is_scalar(side):
495
+ # side is passed as an offset from the middle of detector
496
+ side = side + (sino.shape[-1] - 1) / 2.0
497
+ start, stop = max(0, side - search_width), min(sino_width, side + search_width)
498
+ else:
499
+ raise ValueError("Expected 'side' to be 'left', 'center', 'right' or a scalar")
500
+
501
+ cor = find_center_vo(
502
+ sino,
503
+ start=start,
504
+ stop=stop,
505
+ step=step,
506
+ radius=radius,
507
+ ratio=ratio,
508
+ dsp=dsp,
509
+ ncore=ncore,
510
+ hor_drop=hor_drop,
511
+ ver_drop=ver_drop,
512
+ )
513
+ return cor if not (return_relative_to_middle) else cor - (sino.shape[1] - 1) / 2
nabu/estimation/focus.py CHANGED
@@ -1,10 +1,54 @@
1
1
  import numpy as np
2
2
 
3
+ from scipy.fft import fftn
4
+
5
+ from ..processing.azim import azimuthal_integration_skimage_stack, azimuthal_integration_imagej_stack, __have_skimage__
3
6
  from .alignment import plt
4
7
  from .cor import CenterOfRotation
5
8
 
6
9
 
7
10
  class CameraFocus(CenterOfRotation):
11
+
12
+ def _check_position_jitter(self, img_pos):
13
+ pos_diff = np.diff(img_pos)
14
+ if np.any(pos_diff <= 0):
15
+ self.logger.warning(
16
+ "Image position regressed throughout scan! (negative movement for some image positions)"
17
+ )
18
+
19
+ @staticmethod
20
+ def _gradient(x, axes):
21
+ d = [None] * len(axes)
22
+ for ii in range(len(axes)):
23
+ ind = -(ii + 1)
24
+ padding = [(0, 0)] * len(x.shape)
25
+ padding[ind] = (0, 1)
26
+ temp_x = np.pad(x, padding, mode="constant")
27
+ d[ind] = np.diff(temp_x, n=1, axis=ind)
28
+ return np.stack(d, axis=0)
29
+
30
+ @staticmethod
31
+ def _compute_metric_value(data, metric, axes=(-2, -1)):
32
+ if metric.lower() == "std":
33
+ return np.std(data, axis=axes) / np.mean(data, axis=axes)
34
+ elif metric.lower() == "grad":
35
+ grad_data = CameraFocus._gradient(data, axes=axes)
36
+ grad_mag = np.sqrt(np.sum(grad_data**2, axis=0))
37
+ return np.sum(grad_mag, axis=axes)
38
+ elif metric.lower() == "psd":
39
+ f_data = fftn(data, axes=axes, workers=4)
40
+ f_data = np.fft.fftshift(f_data, axes=(-2, -1))
41
+ # octave-fasttomo3 uses |.|^2, probably with scaled FFT (norm="forward" in python),
42
+ # but tests show that it's less accurate.
43
+ f_data = np.abs(f_data)
44
+ ai_func = azimuthal_integration_skimage_stack if __have_skimage__ else azimuthal_integration_imagej_stack
45
+ az_data = ai_func(f_data, n_threads=4)
46
+ max_vals = np.max(az_data, axis=0)
47
+ az_data /= max_vals[None, :]
48
+ return np.mean(az_data, axis=-1)
49
+ else:
50
+ raise ValueError("Unknown metric function %s" % metric)
51
+
8
52
  def find_distance(
9
53
  self,
10
54
  img_stack: np.ndarray,
@@ -76,6 +120,7 @@ class CameraFocus(CenterOfRotation):
76
120
  is the associated image position (starting from 1).
77
121
  """
78
122
  self._check_img_stack_size(img_stack, img_pos)
123
+ self._check_position_jitter(img_pos)
79
124
 
80
125
  if peak_fit_radius < 1:
81
126
  self.logger.warning("Parameter peak_fit_radius should be at least 1, given: %d instead." % peak_fit_radius)
@@ -93,19 +138,25 @@ class CameraFocus(CenterOfRotation):
93
138
  high_pass=high_pass,
94
139
  )
95
140
 
96
- img_stds = np.std(img_stack, axis=(-2, -1)) / np.mean(img_stack, axis=(-2, -1))
141
+ img_resp = self._compute_metric_value(img_stack, metric=metric, axes=(-2, -1))
97
142
 
98
- # assuming images are equispaced
143
+ # assuming images are equispaced!
144
+ # focus_step = np.mean(np.abs(np.diff(img_pos)))
99
145
  focus_step = (img_pos[-1] - img_pos[0]) / (num_imgs - 1)
100
146
 
101
147
  img_inds = np.arange(num_imgs)
102
- (f_vals, f_pos) = self.extract_peak_regions_1d(img_stds, peak_radius=peak_fit_radius, cc_coords=img_inds)
103
- focus_ind, img_std_max = self.refine_max_position_1d(f_vals, return_vertex_val=True)
148
+ (f_vals, f_pos) = self.extract_peak_regions_1d(img_resp, peak_radius=peak_fit_radius, cc_coords=img_inds)
149
+ focus_ind, img_resp_max = self.refine_max_position_1d(f_vals, return_vertex_val=True, return_all_coeffs=True)
104
150
  focus_ind += f_pos[1, :]
105
151
 
106
152
  focus_pos = img_pos[0] + focus_step * focus_ind
107
153
  focus_ind += 1
108
154
 
155
+ if focus_pos.size == 1:
156
+ focus_pos = focus_pos[0]
157
+ if focus_ind.size == 1:
158
+ focus_ind = focus_ind[0]
159
+
109
160
  if self.verbose:
110
161
  self.logger.info(
111
162
  "Fitted focus motor position:",
@@ -115,9 +166,9 @@ class CameraFocus(CenterOfRotation):
115
166
  )
116
167
  f, ax = plt.subplots(1, 1)
117
168
  self._add_plot_window(f, ax=ax)
118
- ax.stem(img_pos, img_stds)
119
- ax.stem(focus_pos, img_std_max, linefmt="C1-", markerfmt="C1o")
120
- ax.set_title("Images std")
169
+ ax.stem(img_pos, img_resp)
170
+ ax.stem(focus_pos, img_resp_max, linefmt="C1-", markerfmt="C1o")
171
+ ax.set_title("Images response (metric: %s)" % metric)
121
172
  plt.show(block=False)
122
173
 
123
174
  return focus_pos, focus_ind
@@ -272,6 +323,7 @@ class CameraFocus(CenterOfRotation):
272
323
  ... img_stack, img_pos, roi_yxhw=img_roi, regions_number=regions_number)
273
324
  """
274
325
  self._check_img_stack_size(img_stack, img_pos)
326
+ self._check_position_jitter(img_pos)
275
327
 
276
328
  if peak_fit_radius < 1:
277
329
  self.logger.warning("Parameter peak_fit_radius should be at least 1, given: %d instead." % peak_fit_radius)
@@ -306,15 +358,15 @@ class CameraFocus(CenterOfRotation):
306
358
  )
307
359
  img_stack = np.reshape(img_stack, block_stack_size)
308
360
 
309
- img_stds = np.std(img_stack, axis=(-3, -1)) / np.mean(img_stack, axis=(-3, -1))
310
- img_stds = np.reshape(img_stds, [num_imgs, -1]).transpose()
361
+ img_resp = self._compute_metric_value(img_stack, metric=metric, axes=(-3, -1))
362
+ img_resp = np.reshape(img_resp, [num_imgs, -1]).transpose()
311
363
 
312
364
  # assuming images are equispaced
313
365
  focus_step = (img_pos[-1] - img_pos[0]) / (num_imgs - 1)
314
366
 
315
367
  img_inds = np.arange(num_imgs)
316
- (f_vals, f_pos) = self.extract_peak_regions_1d(img_stds, peak_radius=peak_fit_radius, cc_coords=img_inds)
317
- focus_inds = self.refine_max_position_1d(f_vals)
368
+ (f_vals, f_pos) = self.extract_peak_regions_1d(img_resp, peak_radius=peak_fit_radius, cc_coords=img_inds)
369
+ focus_inds = self.refine_max_position_1d(f_vals, return_all_coeffs=True)
318
370
  focus_inds += f_pos[1, :]
319
371
 
320
372
  focus_poss = img_pos[0] + focus_step * focus_inds