nabu 2023.2.1__py3-none-any.whl → 2024.1.0rc3__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 (183) hide show
  1. doc/conf.py +1 -1
  2. doc/doc_config.py +32 -0
  3. nabu/__init__.py +2 -1
  4. nabu/app/bootstrap_stitching.py +1 -1
  5. nabu/app/cli_configs.py +122 -2
  6. nabu/app/composite_cor.py +27 -2
  7. nabu/app/correct_rot.py +70 -0
  8. nabu/app/create_distortion_map_from_poly.py +42 -18
  9. nabu/app/diag_to_pix.py +358 -0
  10. nabu/app/diag_to_rot.py +449 -0
  11. nabu/app/generate_header.py +4 -3
  12. nabu/app/histogram.py +2 -2
  13. nabu/app/multicor.py +6 -1
  14. nabu/app/parse_reconstruction_log.py +151 -0
  15. nabu/app/prepare_weights_double.py +83 -22
  16. nabu/app/reconstruct.py +5 -1
  17. nabu/app/reconstruct_helical.py +7 -0
  18. nabu/app/reduce_dark_flat.py +6 -3
  19. nabu/app/rotate.py +4 -4
  20. nabu/app/stitching.py +16 -2
  21. nabu/app/tests/test_reduce_dark_flat.py +18 -2
  22. nabu/app/validator.py +4 -4
  23. nabu/cuda/convolution.py +8 -376
  24. nabu/cuda/fft.py +4 -0
  25. nabu/cuda/kernel.py +4 -4
  26. nabu/cuda/medfilt.py +5 -158
  27. nabu/cuda/padding.py +5 -71
  28. nabu/cuda/processing.py +23 -2
  29. nabu/cuda/src/ElementOp.cu +78 -0
  30. nabu/cuda/src/backproj.cu +28 -2
  31. nabu/cuda/src/fourier_wavelets.cu +2 -2
  32. nabu/cuda/src/normalization.cu +23 -0
  33. nabu/cuda/src/padding.cu +2 -2
  34. nabu/cuda/src/transpose.cu +16 -0
  35. nabu/cuda/utils.py +39 -0
  36. nabu/estimation/alignment.py +10 -1
  37. nabu/estimation/cor.py +808 -38
  38. nabu/estimation/cor_sino.py +7 -9
  39. nabu/estimation/tests/test_cor.py +85 -3
  40. nabu/io/reader.py +26 -18
  41. nabu/io/tests/test_cast_volume.py +3 -3
  42. nabu/io/tests/test_detector_distortion.py +3 -3
  43. nabu/io/tiffwriter_zmm.py +2 -2
  44. nabu/io/utils.py +14 -4
  45. nabu/io/writer.py +5 -3
  46. nabu/misc/fftshift.py +6 -0
  47. nabu/misc/histogram.py +5 -285
  48. nabu/misc/histogram_cuda.py +8 -104
  49. nabu/misc/kernel_base.py +3 -121
  50. nabu/misc/padding_base.py +5 -69
  51. nabu/misc/processing_base.py +3 -107
  52. nabu/misc/rotation.py +5 -62
  53. nabu/misc/rotation_cuda.py +5 -65
  54. nabu/misc/transpose.py +6 -0
  55. nabu/misc/unsharp.py +3 -78
  56. nabu/misc/unsharp_cuda.py +5 -52
  57. nabu/misc/unsharp_opencl.py +8 -85
  58. nabu/opencl/fft.py +6 -0
  59. nabu/opencl/kernel.py +21 -6
  60. nabu/opencl/padding.py +5 -72
  61. nabu/opencl/processing.py +27 -5
  62. nabu/opencl/src/backproj.cl +3 -3
  63. nabu/opencl/src/fftshift.cl +65 -12
  64. nabu/opencl/src/padding.cl +2 -2
  65. nabu/opencl/src/roll.cl +96 -0
  66. nabu/opencl/src/transpose.cl +16 -0
  67. nabu/pipeline/config_validators.py +63 -3
  68. nabu/pipeline/dataset_validator.py +2 -2
  69. nabu/pipeline/estimators.py +193 -35
  70. nabu/pipeline/fullfield/chunked.py +34 -17
  71. nabu/pipeline/fullfield/chunked_cuda.py +7 -5
  72. nabu/pipeline/fullfield/computations.py +48 -13
  73. nabu/pipeline/fullfield/nabu_config.py +13 -13
  74. nabu/pipeline/fullfield/processconfig.py +10 -5
  75. nabu/pipeline/fullfield/reconstruction.py +1 -2
  76. nabu/pipeline/helical/fbp.py +5 -0
  77. nabu/pipeline/helical/filtering.py +12 -9
  78. nabu/pipeline/helical/gridded_accumulator.py +179 -33
  79. nabu/pipeline/helical/helical_chunked_regridded.py +262 -151
  80. nabu/pipeline/helical/helical_chunked_regridded_cuda.py +4 -11
  81. nabu/pipeline/helical/helical_reconstruction.py +56 -18
  82. nabu/pipeline/helical/span_strategy.py +1 -1
  83. nabu/pipeline/helical/tests/test_accumulator.py +4 -0
  84. nabu/pipeline/params.py +23 -2
  85. nabu/pipeline/processconfig.py +3 -8
  86. nabu/pipeline/tests/test_chunk_reader.py +78 -0
  87. nabu/pipeline/tests/test_estimators.py +120 -2
  88. nabu/pipeline/utils.py +25 -0
  89. nabu/pipeline/writer.py +2 -0
  90. nabu/preproc/ccd_cuda.py +9 -7
  91. nabu/preproc/ctf.py +21 -26
  92. nabu/preproc/ctf_cuda.py +25 -25
  93. nabu/preproc/double_flatfield.py +14 -2
  94. nabu/preproc/double_flatfield_cuda.py +7 -11
  95. nabu/preproc/flatfield_cuda.py +23 -27
  96. nabu/preproc/phase.py +19 -24
  97. nabu/preproc/phase_cuda.py +21 -21
  98. nabu/preproc/shift_cuda.py +58 -28
  99. nabu/preproc/tests/test_ctf.py +5 -5
  100. nabu/preproc/tests/test_double_flatfield.py +2 -2
  101. nabu/preproc/tests/test_vshift.py +13 -2
  102. nabu/processing/__init__.py +0 -0
  103. nabu/processing/convolution_cuda.py +375 -0
  104. nabu/processing/fft_base.py +163 -0
  105. nabu/processing/fft_cuda.py +256 -0
  106. nabu/processing/fft_opencl.py +54 -0
  107. nabu/processing/fftshift.py +134 -0
  108. nabu/processing/histogram.py +286 -0
  109. nabu/processing/histogram_cuda.py +103 -0
  110. nabu/processing/kernel_base.py +126 -0
  111. nabu/processing/medfilt_cuda.py +159 -0
  112. nabu/processing/muladd.py +29 -0
  113. nabu/processing/muladd_cuda.py +68 -0
  114. nabu/processing/padding_base.py +71 -0
  115. nabu/processing/padding_cuda.py +75 -0
  116. nabu/processing/padding_opencl.py +77 -0
  117. nabu/processing/processing_base.py +123 -0
  118. nabu/processing/roll_opencl.py +64 -0
  119. nabu/processing/rotation.py +63 -0
  120. nabu/processing/rotation_cuda.py +66 -0
  121. nabu/processing/tests/__init__.py +0 -0
  122. nabu/processing/tests/test_fft.py +268 -0
  123. nabu/processing/tests/test_fftshift.py +71 -0
  124. nabu/{misc → processing}/tests/test_histogram.py +2 -4
  125. nabu/{cuda → processing}/tests/test_medfilt.py +1 -1
  126. nabu/processing/tests/test_muladd.py +54 -0
  127. nabu/{cuda → processing}/tests/test_padding.py +119 -75
  128. nabu/processing/tests/test_roll.py +63 -0
  129. nabu/{misc → processing}/tests/test_rotation.py +3 -2
  130. nabu/processing/tests/test_transpose.py +72 -0
  131. nabu/{misc → processing}/tests/test_unsharp.py +41 -8
  132. nabu/processing/transpose.py +126 -0
  133. nabu/processing/unsharp.py +79 -0
  134. nabu/processing/unsharp_cuda.py +53 -0
  135. nabu/processing/unsharp_opencl.py +75 -0
  136. nabu/reconstruction/fbp.py +34 -10
  137. nabu/reconstruction/fbp_base.py +35 -16
  138. nabu/reconstruction/fbp_opencl.py +7 -12
  139. nabu/reconstruction/filtering.py +2 -2
  140. nabu/reconstruction/filtering_cuda.py +13 -14
  141. nabu/reconstruction/filtering_opencl.py +3 -4
  142. nabu/reconstruction/projection.py +2 -0
  143. nabu/reconstruction/rings.py +158 -1
  144. nabu/reconstruction/rings_cuda.py +218 -58
  145. nabu/reconstruction/sinogram_cuda.py +16 -12
  146. nabu/reconstruction/tests/test_deringer.py +116 -14
  147. nabu/reconstruction/tests/test_fbp.py +22 -31
  148. nabu/reconstruction/tests/test_filtering.py +11 -2
  149. nabu/resources/dataset_analyzer.py +89 -26
  150. nabu/resources/nxflatfield.py +2 -2
  151. nabu/resources/tests/test_nxflatfield.py +1 -1
  152. nabu/resources/utils.py +9 -2
  153. nabu/stitching/alignment.py +184 -0
  154. nabu/stitching/config.py +241 -39
  155. nabu/stitching/definitions.py +6 -0
  156. nabu/stitching/frame_composition.py +4 -2
  157. nabu/stitching/overlap.py +99 -3
  158. nabu/stitching/sample_normalization.py +60 -0
  159. nabu/stitching/slurm_utils.py +10 -10
  160. nabu/stitching/tests/test_alignment.py +99 -0
  161. nabu/stitching/tests/test_config.py +16 -1
  162. nabu/stitching/tests/test_overlap.py +68 -2
  163. nabu/stitching/tests/test_sample_normalization.py +49 -0
  164. nabu/stitching/tests/test_slurm_utils.py +5 -5
  165. nabu/stitching/tests/test_utils.py +3 -33
  166. nabu/stitching/tests/test_z_stitching.py +391 -22
  167. nabu/stitching/utils.py +144 -202
  168. nabu/stitching/z_stitching.py +309 -126
  169. nabu/testutils.py +18 -0
  170. nabu/thirdparty/tomocupy_remove_stripe.py +586 -0
  171. nabu/utils.py +32 -6
  172. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/LICENSE +1 -1
  173. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/METADATA +5 -5
  174. nabu-2024.1.0rc3.dist-info/RECORD +296 -0
  175. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/WHEEL +1 -1
  176. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/entry_points.txt +5 -1
  177. nabu/conftest.py +0 -14
  178. nabu/opencl/fftshift.py +0 -92
  179. nabu/opencl/tests/test_fftshift.py +0 -55
  180. nabu/opencl/tests/test_padding.py +0 -84
  181. nabu-2023.2.1.dist-info/RECORD +0 -252
  182. /nabu/cuda/src/{fftshift.cu → dfi_fftshift.cu} +0 -0
  183. {nabu-2023.2.1.dist-info → nabu-2024.1.0rc3.dist-info}/top_level.txt +0 -0
@@ -8,12 +8,17 @@ import scipy.fft # pylint: disable=E0611
8
8
  from silx.io import get_data
9
9
  from typing import Union, Optional
10
10
  import math
11
+ from numbers import Real
12
+ from scipy import ndimage as nd
13
+
11
14
  from ..preproc.flatfield import FlatFieldDataUrls
12
15
  from ..estimation.cor import (
13
16
  CenterOfRotation,
14
17
  CenterOfRotationAdaptiveSearch,
15
18
  CenterOfRotationSlidingWindow,
16
19
  CenterOfRotationGrowingWindow,
20
+ CenterOfRotationFourierAngles,
21
+ CenterOfRotationOctaveAccurate,
17
22
  )
18
23
  from ..estimation.cor_sino import SinoCorInterface
19
24
  from ..estimation.tilt import CameraTilt
@@ -23,7 +28,7 @@ from ..resources.utils import extract_parameters
23
28
  from ..utils import check_supported, is_int
24
29
  from .params import tilt_methods
25
30
  from ..resources.dataset_analyzer import get_0_180_radios
26
- from ..misc.rotation import Rotation
31
+ from ..processing.rotation import Rotation
27
32
  from ..io.reader import ChunkReader
28
33
  from ..preproc.ccd import Log, CCDFilter
29
34
  from ..misc import fourier_filters
@@ -50,9 +55,15 @@ def estimate_cor(method, dataset_info, do_flatfield=True, cor_options: Optional[
50
55
  else:
51
56
  raise TypeError(f"cor_options_str is expected to be a dict or a str. {type(cor_options)} provided")
52
57
 
53
- # Dispatch
58
+ # Dispatch. COR estimation is always expressed in absolute number of pixels (i.e. from the center of the first pixel column)
54
59
  if method in CORFinder.search_methods:
55
- cor_finder = CORFinder(method, dataset_info, do_flatfield=do_flatfield, cor_options=cor_options, logger=logger)
60
+ cor_finder = CORFinder(
61
+ method,
62
+ dataset_info,
63
+ do_flatfield=do_flatfield,
64
+ cor_options=cor_options,
65
+ logger=logger,
66
+ )
56
67
  estimated_cor = cor_finder.find_cor()
57
68
  elif method in SinoCORFinder.search_methods:
58
69
  cor_finder = SinoCORFinder(
@@ -109,21 +120,102 @@ class CORFinderBase:
109
120
  raise TypeError(
110
121
  f"cor_options is expected to be an optional instance of dict. Get {cor_options} ({type(cor_options)}) instead"
111
122
  )
112
- self.cor_options = cor_options or {}
123
+ self.cor_options = {}
124
+ if isinstance(cor_options, dict):
125
+ self.cor_options.update(cor_options)
113
126
 
114
- cor_class = self.search_methods[method]["class"]
115
- self.cor_finder = cor_class(logger=self.logger)
127
+ # tomotools internal meeting 07 feb 2024: Merge of options 'near_pos' and 'side'.
128
+ # See [minutes](https://gitlab.esrf.fr/tomotools/minutes/-/blob/master/minutes-20240207.md?ref_type=heads)
116
129
 
130
+ detector_width = self.dataset_info.radio_dims[0]
117
131
  default_lookup_side = "right" if self.dataset_info.is_halftomo else "center"
132
+ near_init = self.cor_options.get("side", None)
133
+
134
+ if near_init is None:
135
+ near_init = default_lookup_side
136
+
137
+ if near_init == "from_file":
138
+ try:
139
+ near_pos = self.dataset_info.dataset_scanner.estimated_cor_frm_motor # relative pos in pixels
140
+ if isinstance(near_pos, Real):
141
+ # near_pos += detector_width // 2 # Field in NX is relative.
142
+ self.cor_options.update({"near_pos": int(near_pos)})
143
+ else:
144
+ near_init = default_lookup_side
145
+ except:
146
+ self.logger.warning(
147
+ "COR estimation from motor position absent from NX file. Global search is performed."
148
+ )
149
+ near_init = default_lookup_side
150
+ elif isinstance(near_init, Real):
151
+ self.cor_options.update({"near_pos": near_init})
152
+ near_init = "near" # ???
153
+ elif near_init == "near": # Legacy
154
+ if not isinstance(self.cor_options["near_pos"], Real):
155
+ self.logger.warning("Side option set to 'near' but no 'near_pos' option set.")
156
+ self.logger.warning("Set side to right if HA, center otherwise.")
157
+ near_init = default_lookup_side
158
+ elif near_init in ("left", "right", "center", "all"):
159
+ pass
160
+ else:
161
+ self.logger.warning(
162
+ f"COR option 'side' received {near_init} and should be either 'from_file' (default), 'left', 'right', 'center', 'near' or a number."
163
+ )
164
+
165
+ if isinstance(self.cor_options.get("near_pos", None), Real):
166
+ # Check validity of near_pos
167
+ if np.abs(self.cor_options["near_pos"]) > detector_width / 2:
168
+ self.logger.warning(
169
+ f"Relative COR passed is greater than half the size of the detector. Did you enter a absolute COR position?"
170
+ )
171
+ self.logger.warning("Instead, the center of the detector is used.")
172
+ self.cor_options["near_pos"] = 0
173
+
174
+ # Set side from near_pos if passed.
175
+ if self.cor_options["near_pos"] < 0.0:
176
+ self.cor_options.update({"side": "left"})
177
+ near_init = "left"
178
+ else:
179
+ self.cor_options.update({"side": "right"})
180
+ near_init = "right"
181
+
182
+ self.cor_options.update({"side": near_init})
183
+
184
+ # At this stage : side is set to one of left, right, center near.
185
+ # and near_pos to a numeric value.
186
+
187
+ # if isinstance(self.cor_options["near_pos"], Real):
188
+ # # estimated_cor_frm_motor value is supposed to be relative. Since the config documentation expects the "near_pos" options
189
+ # # to be given as an absolute COR estimate, a conversion is needed.
190
+ # self.cor_options["near_pos"] += detector_width // 2 # converted in absolute nb of pixels.
191
+ # if not (isinstance(self.cor_options["near_pos"], Real) or self.cor_options["near_pos"] == "ignore"):
192
+ # self.cor_options.update({"near_pos": "ignore"})
193
+
194
+ # At this stage, cor_options["near_pos"] is either
195
+ # - 'ignore':
196
+ # - an (absolute) integer value (either the user-provided one if present or the NX one).
197
+
198
+ cor_class = self.search_methods[method]["class"]
199
+ self.cor_finder = cor_class(logger=self.logger, cor_options=self.cor_options)
200
+
118
201
  lookup_side = self.cor_options.get("side", default_lookup_side)
119
202
 
203
+ # OctaveAccurate
204
+ if cor_class == CenterOfRotationOctaveAccurate:
205
+ lookup_side = "center"
206
+ angles = self.dataset_info.rotation_angles
207
+
120
208
  self.cor_exec_args = []
121
209
  self.cor_exec_args.extend(self.search_methods[method].get("default_args", []))
122
210
 
123
211
  # CenterOfRotationSlidingWindow is the only class to have a mandatory argument ("side")
124
212
  # TODO - it would be more elegant to have it as a kwarg...
125
- if len(self.cor_exec_args) > 0 and cor_class is CenterOfRotationSlidingWindow:
126
- self.cor_exec_args[0] = lookup_side
213
+ if len(self.cor_exec_args) > 0:
214
+ if cor_class in (CenterOfRotationSlidingWindow, CenterOfRotationOctaveAccurate):
215
+ self.cor_exec_args[0] = lookup_side
216
+ elif cor_class in (CenterOfRotationFourierAngles,):
217
+ self.cor_exec_args[0] = angles
218
+ self.cor_exec_args[1] = lookup_side
127
219
  #
128
220
  self.cor_exec_kwargs = update_func_kwargs(self.cor_finder.find_shift, self.cor_options)
129
221
 
@@ -148,6 +240,10 @@ class CORFinder(CORFinderBase):
148
240
  "growing-window": {
149
241
  "class": CenterOfRotationGrowingWindow,
150
242
  },
243
+ "octave-accurate": {
244
+ "class": CenterOfRotationOctaveAccurate,
245
+ "default_args": ["center"],
246
+ },
151
247
  }
152
248
 
153
249
  def __init__(self, method, dataset_info, do_flatfield=True, cor_options=None, logger=None):
@@ -225,13 +321,14 @@ class SinoCORFinder(CORFinderBase):
225
321
  "sino-coarse-to-fine": {
226
322
  "class": SinoCorInterface,
227
323
  },
228
- "sliding-window": {
324
+ "sino-sliding-window": {
229
325
  "class": CenterOfRotationSlidingWindow,
230
326
  "default_args": ["right"],
231
327
  },
232
- "growing-window": {
328
+ "sino-growing-window": {
233
329
  "class": CenterOfRotationGrowingWindow,
234
330
  },
331
+ "fourier-angles": {"class": CenterOfRotationFourierAngles, "default_args": [None, "center"]},
235
332
  }
236
333
 
237
334
  def __init__(
@@ -286,6 +383,7 @@ class SinoCORFinder(CORFinderBase):
286
383
  self.projs_indices = np.round(indices_float).astype(np.int32).tolist()
287
384
  else: # Subsampling step
288
385
  self.projs_indices = projs_idx[::subsampling]
386
+ self.angles = self.dataset_info.rotation_angles[::subsampling]
289
387
  else: # Angular step
290
388
  raise NotImplementedError()
291
389
 
@@ -338,7 +436,7 @@ class SinoCORFinder(CORFinderBase):
338
436
  self.logger.info("Estimating center of rotation")
339
437
  self.logger.debug("%s.find_shift(%s)" % (self.cor_finder.__class__.__name__, str(self.cor_exec_kwargs)))
340
438
  img_1, img_2 = self._split_sinogram(self.sinogram)
341
- shift = self.cor_finder.find_shift(img_1, img_2, *self.cor_exec_args, **self.cor_exec_kwargs)
439
+ shift = self.cor_finder.find_shift(img_1, np.fliplr(img_2), *self.cor_exec_args, **self.cor_exec_kwargs)
342
440
  return self.shape[1] / 2 + shift
343
441
 
344
442
 
@@ -346,7 +444,7 @@ class SinoCORFinder(CORFinderBase):
346
444
  SinoCOREstimator = SinoCORFinder
347
445
 
348
446
 
349
- class CompositeCORFinder:
447
+ class CompositeCORFinder(CORFinderBase):
350
448
  """
351
449
  Class and method to prepare sinogram and calculate COR
352
450
  The pseudo sinogram is built with shrinked radios taken every theta_interval degres
@@ -363,6 +461,11 @@ class CompositeCORFinder:
363
461
  by several order of magnitude without modifing the final result
364
462
  """
365
463
 
464
+ search_methods = {
465
+ "composite-coarse-to-fine": {
466
+ "class": CenterOfRotation, # Hack. Not used. Everything is done in the find_cor() func.
467
+ }
468
+ }
366
469
  _default_cor_options = {"low_pass": 0.4, "high_pass": 10, "side": "center", "near_pos": 0, "near_width": 20}
367
470
 
368
471
  def __init__(
@@ -377,6 +480,9 @@ class CompositeCORFinder:
377
480
  logger=None,
378
481
  norm_order=1,
379
482
  ):
483
+ super().__init__(
484
+ "composite-coarse-to-fine", dataset_info, do_flatfield=True, cor_options=cor_options, logger=logger
485
+ )
380
486
  if norm_order not in [1, 2]:
381
487
  raise ValueError(
382
488
  f""" the norm order (nom_order parameter) must be either 1 or 2. You passed {norm_order}
@@ -388,6 +494,12 @@ class CompositeCORFinder:
388
494
  self.dataset_info = dataset_info
389
495
  self.logger = LoggerOrPrint(logger)
390
496
 
497
+ self.sx, self.sy = self.dataset_info.radio_dims
498
+
499
+ default_cor_options = self._default_cor_options.copy()
500
+ default_cor_options.update(self.cor_options)
501
+ self.cor_options = default_cor_options
502
+
391
503
  # the algorithm can work for angular ranges larger than 1.2*pi
392
504
  # up to an arbitrarily number of turns as it is the case in helical scans
393
505
  self.spike_threshold = spike_threshold
@@ -398,8 +510,6 @@ class CompositeCORFinder:
398
510
  self.angle_min = self.unwrapped_rotation_angles.min()
399
511
  self.angle_max = self.unwrapped_rotation_angles.max()
400
512
 
401
- self.sx, self.sy = self.dataset_info.radio_dims
402
-
403
513
  if (self.angle_max - self.angle_min) < 1.2 * np.pi:
404
514
  useful_span = None
405
515
  raise ValueError(
@@ -414,7 +524,7 @@ class CompositeCORFinder:
414
524
  if useful_span < np.pi:
415
525
  theta_interval = theta_interval * useful_span / np.pi
416
526
 
417
- self._get_cor_options(cor_options)
527
+ # self._get_cor_options(cor_options)
418
528
 
419
529
  self.take_log = take_log
420
530
  self.ovs = oversampling
@@ -474,7 +584,6 @@ class CompositeCORFinder:
474
584
  # initialize sinograms and radios arrays
475
585
  self.sino = np.zeros([2 * self.nprobed * n_subsampling_y, (self.sx - 1) * self.ovs + 1], "f")
476
586
  self._loaded = False
477
-
478
587
  self.high_pass = self.cor_options["high_pass"]
479
588
  img_filter = fourier_filters.get_bandpass_filter(
480
589
  (self.sino.shape[0] // 2, self.sino.shape[1]),
@@ -492,16 +601,9 @@ class CompositeCORFinder:
492
601
  """oversampling in the horizontal direction"""
493
602
  if self.ovs == 1:
494
603
  return radio
495
- result = np.zeros([radio.shape[0], (radio.shape[1] - 1) * self.ovs + 1], "f")
496
- result[:, :: self.ovs] = radio
497
-
498
- for i in range(1, self.ovs):
499
- f = i / self.ovs
500
- result[:, i :: self.ovs] = (1 - f) * result[:, 0 : -self.ovs : self.ovs] + f * result[
501
- :, self.ovs :: self.ovs
502
- ]
503
-
504
- return result
604
+ else:
605
+ ovs_2D = [1, self.ovs]
606
+ return oversample(radio, ovs_2D)
505
607
 
506
608
  def _get_cor_options(self, cor_options):
507
609
  default_dict = self._default_cor_options.copy()
@@ -644,45 +746,59 @@ class CompositeCORFinder:
644
746
 
645
747
  best_overlap = overlap_min
646
748
  best_error = np.inf
749
+
750
+ blurred_radio1 = nd.gaussian_filter(abs(radio1), [0, self.high_pass])
751
+ blurred_radio2 = nd.gaussian_filter(abs(radio2), [0, self.high_pass])
752
+
647
753
  for z in range(int(overlap_min), int(overlap_max) + 1):
648
754
  if z <= ovsd_sx:
649
755
  my_z = z
650
756
  my_radio1 = radio1
651
757
  my_radio2 = radio2
758
+ my_blurred_radio1 = blurred_radio1
759
+ my_blurred_radio2 = blurred_radio2
652
760
  else:
653
761
  my_z = ovsd_sx - (z - ovsd_sx)
654
762
  my_radio1 = np.fliplr(radio1)
655
763
  my_radio2 = np.fliplr(radio2)
764
+ my_blurred_radio1 = np.fliplr(blurred_radio1)
765
+ my_blurred_radio2 = np.fliplr(blurred_radio2)
656
766
 
657
767
  common_left = np.fliplr(my_radio1[:, ovsd_sx - my_z :])[:, : -int(math.ceil(self.ovs * self.high_pass * 2))]
658
768
  # adopt a 'safe' margin considering high_pass value (possibly float)
659
769
  common_right = my_radio2[:, ovsd_sx - my_z : -int(math.ceil(self.ovs * self.high_pass * 2))]
660
770
 
771
+ common_blurred_left = np.fliplr(my_blurred_radio1[:, ovsd_sx - my_z :])[
772
+ :, : -int(math.ceil(self.ovs * self.high_pass * 2))
773
+ ]
774
+ # adopt a 'safe' margin considering high_pass value (possibly float)
775
+ common_blurred_right = my_blurred_radio2[:, ovsd_sx - my_z : -int(math.ceil(self.ovs * self.high_pass * 2))]
776
+
661
777
  if common_right.size == 0:
662
778
  continue
663
779
 
664
- error = self.error_metric(common_right, common_left)
780
+ error = self.error_metric(common_right, common_left, common_blurred_right, common_blurred_left)
665
781
 
666
782
  min_error = min(best_error, error)
667
783
 
668
784
  if min_error == error:
669
785
  best_overlap = z
670
786
  best_error = min_error
671
- self.logger.debug(
672
- "testing an overlap of %.2f pixels, actual best overlap is %.2f pixels over %d\r"
673
- % (z / self.ovs, best_overlap / self.ovs, ovsd_sx / self.ovs),
674
- )
787
+ # self.logger.debug(
788
+ # "testing an overlap of %.2f pixels, actual best overlap is %.2f pixels over %d\r"
789
+ # % (z / self.ovs, best_overlap / self.ovs, ovsd_sx / self.ovs),
790
+ # )
675
791
 
676
792
  offset = (ovsd_sx - best_overlap) / self.ovs / 2
677
793
  cor_abs = (self.sx - 1) / 2 + offset
678
794
 
679
795
  return cor_abs
680
796
 
681
- def error_metric(self, common_right, common_left):
797
+ def error_metric(self, common_right, common_left, common_blurred_right, common_blurred_left):
682
798
  if self.norm_order == 2:
683
799
  return self.error_metric_l2(common_right, common_left)
684
800
  elif self.norm_order == 1:
685
- return self.error_metric_l1(common_right, common_left)
801
+ return self.error_metric_l1(common_right, common_left, common_blurred_right, common_blurred_left)
686
802
  else:
687
803
  assert False, "this cannot happen"
688
804
 
@@ -699,14 +815,56 @@ class CompositeCORFinder:
699
815
 
700
816
  return res
701
817
 
702
- def error_metric_l1(self, common_right, common_left):
703
- common = common_right - common_left
818
+ def error_metric_l1(self, common_right, common_left, common_blurred_right, common_blurred_left):
819
+ common = (common_right - common_left) / (common_blurred_right + common_blurred_left)
704
820
 
705
821
  res = abs(common).mean()
706
822
 
707
823
  return res
708
824
 
709
825
 
826
+ def oversample(radio, ovs_s):
827
+ """oversampling an image in arbitrary directions.
828
+ The first and last point of each axis will still remain as extremal points of the new axis.
829
+ """
830
+ result = np.zeros([(radio.shape[0] - 1) * ovs_s[0] + 1, (radio.shape[1] - 1) * ovs_s[1] + 1], "f")
831
+
832
+ # Pre-initialisation: The original data falls exactly on the following strided positions in the new data array.
833
+ result[:: ovs_s[0], :: ovs_s[1]] = radio
834
+
835
+ for k in range(0, ovs_s[0]):
836
+ # interpolation coefficient for axis 0
837
+ g = k / ovs_s[0]
838
+ for i in range(0, ovs_s[1]):
839
+ if i == 0 and k == 0:
840
+ # this case subset was already exactly matched from before the present double loop,
841
+ # in the pre-initialisation line.
842
+ continue
843
+ # interpolation coefficent for axis 1
844
+ f = i / ovs_s[1]
845
+
846
+ # stop just a bit before cause we are not extending beyond the limits.
847
+ # If we are exacly on a vertical or horizontal original line, then no shift will be applied,
848
+ # and we will exploit the equality f+(1-f)=g+(1-g)=1 adding twice the same contribution with
849
+ # interpolation factors which become dummies pour le coup.
850
+ stop0 = -ovs_s[0] if k else None
851
+ stop1 = -ovs_s[1] if i else None
852
+
853
+ # Once again, we exploit the g+(1-g)=1 equality
854
+ start0 = ovs_s[0] if k else 0
855
+ start1 = ovs_s[1] if i else 0
856
+
857
+ # and what is done below makes clear the corundum above.
858
+ result[k :: ovs_s[0], i :: ovs_s[1]] = (1 - g) * (
859
+ (1 - f) * result[0 : stop0 : ovs_s[0], 0 : stop1 : ovs_s[1]]
860
+ + f * result[0 : stop0 : ovs_s[0], start1 :: ovs_s[1]]
861
+ ) + g * (
862
+ (1 - f) * result[start0 :: ovs_s[0], 0 : stop1 : ovs_s[1]]
863
+ + f * result[start0 :: ovs_s[0], start1 :: ovs_s[1]]
864
+ )
865
+ return result
866
+
867
+
710
868
  # alias
711
869
  CompositeCOREstimator = CompositeCORFinder
712
870
 
@@ -16,10 +16,10 @@ from ...preproc.phase import PaganinPhaseRetrieval
16
16
  from ...preproc.ctf import CTFPhaseRetrieval, GeoPars
17
17
  from ...reconstruction.sinogram import SinoNormalization
18
18
  from ...reconstruction.filtering import SinoFilter
19
- from ...misc.rotation import Rotation
20
- from ...reconstruction.rings import MunchDeringer
21
- from ...misc.unsharp import UnsharpMask
22
- from ...misc.histogram import PartialHistogram, hist_as_2Darray
19
+ from ...processing.rotation import Rotation
20
+ from ...reconstruction.rings import MunchDeringer, SinoMeanDeringer, VoDeringer
21
+ from ...processing.unsharp import UnsharpMask
22
+ from ...processing.histogram import PartialHistogram, hist_as_2Darray
23
23
  from ..utils import use_options, pipeline_step, get_subregion
24
24
  from ..datadump import DataDumpManager
25
25
  from ..writer import WriterManager
@@ -49,7 +49,9 @@ class ChunkedPipeline:
49
49
  UnsharpMaskClass = UnsharpMask
50
50
  ImageRotationClass = Rotation
51
51
  VerticalShiftClass = VerticalShift
52
- SinoDeringerClass = MunchDeringer
52
+ MunchDeringerClass = MunchDeringer
53
+ SinoMeanDeringerClass = SinoMeanDeringer
54
+ VoDeringerClass = VoDeringer
53
55
  MLogClass = Log
54
56
  SinoNormalizationClass = SinoNormalization
55
57
  SinoFilterClass = SinoFilter
@@ -425,6 +427,8 @@ class ChunkedPipeline:
425
427
  output_is_mlog=False,
426
428
  average_is_on_log=avg_is_on_log,
427
429
  sigma_filter=options["sigma"],
430
+ log_clip_min=options["log_min_clip"],
431
+ log_clip_max=options["log_max_clip"],
428
432
  )
429
433
 
430
434
  @use_options("ccd_correction", "ccd_correction")
@@ -468,7 +472,7 @@ class ChunkedPipeline:
468
472
  lim1=options["ctf_lim1"],
469
473
  lim2=options["ctf_lim2"],
470
474
  logger=self.logger,
471
- fftw_num_threads=None, # TODO tune in advanced params of nabu config file
475
+ fft_num_threads=None, # TODO tune in advanced params of nabu config file
472
476
  use_rfft=True,
473
477
  normalize_by_mean=options["ctf_normalize_by_mean"],
474
478
  translation_vh=translations_vh,
@@ -482,12 +486,7 @@ class ChunkedPipeline:
482
486
  pixel_size=options["pixel_size_m"],
483
487
  padding=options["padding_type"],
484
488
  # TODO tune in advanced params of nabu config file
485
- fftw_num_threads=None,
486
- )
487
- if self.phase_retrieval.use_fftw:
488
- self.logger.debug(
489
- "%s using FFTW with %d threads"
490
- % (self.phase_retrieval.__class__.__name__, self.phase_retrieval.fftw.num_threads)
489
+ fft_num_threads=None,
491
490
  )
492
491
 
493
492
  @use_options("unsharp_mask", "unsharp_mask")
@@ -519,11 +518,25 @@ class ChunkedPipeline:
519
518
 
520
519
  @use_options("sino_rings_correction", "sino_deringer")
521
520
  def _init_sino_rings_correction(self):
522
- options = self.processing_options["sino_rings_correction"]
523
- fw_params = extract_parameters(options["user_options"])
524
- fw_sigma = fw_params.pop("sigma", 1.0)
525
521
  n_a, n_z, n_x = self.radios_cropped_shape
526
- self.sino_deringer = self.SinoDeringerClass(fw_sigma, (n_z, n_a, n_x), **fw_params)
522
+ sinos_shape = (n_z, n_a, n_x)
523
+ options = self.processing_options["sino_rings_correction"]
524
+
525
+ destriper_params = extract_parameters(options["user_options"])
526
+ if options["method"] == "munch":
527
+ # TODO MunchDeringer does not have an API consistent with the other deringers
528
+ fw_sigma = destriper_params.pop("sigma", 1.0)
529
+ self.sino_deringer = self.MunchDeringerClass(fw_sigma, sinos_shape, **destriper_params)
530
+ elif options["method"] == "vo":
531
+ self.sino_deringer = self.VoDeringerClass(sinos_shape, **destriper_params)
532
+ elif options["method"] == "mean-subtraction":
533
+ self.sino_deringer = self.SinoMeanDeringerClass(
534
+ sinos_shape, mode="subtract", fft_num_threads=None, **destriper_params
535
+ )
536
+ elif options["method"] == "mean-division":
537
+ self.sino_deringer = self.SinoMeanDeringerClass(
538
+ sinos_shape, mode="divide", fft_num_threads=None, **destriper_params
539
+ )
527
540
 
528
541
  @use_options("reconstruction", "reconstruction")
529
542
  def _init_reconstruction(self):
@@ -598,6 +611,7 @@ class ChunkedPipeline:
598
611
  "float_clip_values": options["float_clip_values"],
599
612
  "tiff_single_file": options.get("tiff_single_file", False),
600
613
  "single_output_file_initialized": getattr(self.process_config, "single_output_file_initialized", False),
614
+ "raw_vol_metadata": {"voxelSize": self.dataset_info.pixel_size}, # legacy...
601
615
  }
602
616
  writer_extra_options.update(extra_options)
603
617
  self.writer = WriterManager(
@@ -738,8 +752,11 @@ class ChunkedPipeline:
738
752
 
739
753
  @pipeline_step("writer", "Saving data")
740
754
  def _write_data(self, data=None):
741
- if data is None:
755
+ if data is None and self.reconstruction is not None:
742
756
  data = self.recs
757
+ if data is None:
758
+ self.logger.info("No data to write")
759
+ return
743
760
  self.writer.write_data(data)
744
761
  self.logger.info("Wrote %s" % self.writer.fname)
745
762
  self._write_histogram()
@@ -6,10 +6,10 @@ from ...preproc.phase_cuda import CudaPaganinPhaseRetrieval
6
6
  from ...preproc.ctf_cuda import CudaCTFPhaseRetrieval
7
7
  from ...reconstruction.sinogram_cuda import CudaSinoBuilder, CudaSinoNormalization
8
8
  from ...reconstruction.filtering_cuda import CudaSinoFilter
9
- from ...reconstruction.rings_cuda import CudaMunchDeringer
10
- from ...misc.unsharp_cuda import CudaUnsharpMask
11
- from ...misc.rotation_cuda import CudaRotation
12
- from ...misc.histogram_cuda import CudaPartialHistogram
9
+ from ...reconstruction.rings_cuda import CudaMunchDeringer, CudaSinoMeanDeringer, CudaVoDeringer
10
+ from ...processing.unsharp_cuda import CudaUnsharpMask
11
+ from ...processing.rotation_cuda import CudaRotation
12
+ from ...processing.histogram_cuda import CudaPartialHistogram
13
13
  from ...reconstruction.fbp import Backprojector
14
14
  from ...reconstruction.cone import __have_astra__, ConebeamReconstructor
15
15
  from ...cuda.utils import get_cuda_context, __has_pycuda__, __pycuda_error_msg__
@@ -36,7 +36,9 @@ class CudaChunkedPipeline(ChunkedPipeline):
36
36
  UnsharpMaskClass = CudaUnsharpMask
37
37
  ImageRotationClass = CudaRotation
38
38
  VerticalShiftClass = CudaVerticalShift
39
- SinoDeringerClass = CudaMunchDeringer
39
+ MunchDeringerClass = CudaMunchDeringer
40
+ SinoMeanDeringerClass = CudaSinoMeanDeringer
41
+ VoDeringerClass = CudaVoDeringer
40
42
  MLogClass = CudaLog
41
43
  SinoBuilderClass = CudaSinoBuilder
42
44
  SinoNormalizationClass = CudaSinoNormalization
@@ -3,9 +3,11 @@ from silx.image.tomography import get_next_power
3
3
  from ...utils import check_supported
4
4
 
5
5
 
6
- def estimate_required_memory(process_config, delta_z=None, delta_a=None, max_mem_allocation_GB=None, debug=False):
6
+ def estimate_required_memory(
7
+ process_config, delta_z=None, delta_a=None, max_mem_allocation_GB=None, fft_plans=True, debug=False
8
+ ):
7
9
  """
8
- Estimate the memory (RAM) needed for a reconstruction.
10
+ Estimate the memory (RAM) in Bytes needed for a reconstruction.
9
11
 
10
12
  Parameters
11
13
  -----------
@@ -87,20 +89,31 @@ def estimate_required_memory(process_config, delta_z=None, delta_a=None, max_mem
87
89
  # Phase retrieval
88
90
  # ---------------
89
91
  if "phase" in processing_steps:
90
- # Phase retrieval is done image-wise, so near in-place, but needs to
91
- # allocate some images, fft plans, and so on
92
+ # Phase retrieval is done image-wise, so near in-place, but needs to allocate some memory:
93
+ # filter with padded shape, radio_padded, radio_padded_fourier, and possibly FFT plan.
94
+ # CTF phase retrieval uses "2 filters" (num and denom) but let's neglect this.
92
95
  Nx_p = get_next_power(2 * Nx)
93
96
  Nz_p = get_next_power(2 * Nz)
94
- img_size_real = 2 * 4 * Nx_p * Nz_p
95
- img_size_cplx = 2 * 8 * ((Nx_p * Nz_p) // 2 + 1)
96
- total_memory_needed += 2 * img_size_real + 3 * img_size_cplx
97
+ img_size_real = Nx_p * Nz_p * 4
98
+ img_size_cplx = ((Nx_p * Nz_p) // 2 + 1) * 8 # assuming RFFT
99
+ factor = 1
100
+ if fft_plans:
101
+ factor = 2
102
+ total_memory_needed += (2 * img_size_real + img_size_cplx) * factor
97
103
 
98
104
  # Sinogram de-ringing
99
105
  # -------------------
100
106
  if "sino_rings_correction" in processing_steps:
101
- # Process is done image-wise.
102
- # Needs one Discrete Wavelets transform and one FFT/IFFT plan for each scale
103
- total_memory_needed += (Nx * Na * 4) * 5.5 # approx.
107
+ method = process_config.processing_options["sino_rings_correction"]["method"]
108
+ if method == "munch":
109
+ # Process is done image-wise.
110
+ # Needs one Discrete Wavelets transform and one FFT/IFFT plan for each scale
111
+ factor = 2 if not (fft_plans) else 5.5 # approx!
112
+ total_memory_needed += (Nx * Na * 4) * factor
113
+ elif method == "vo":
114
+ # cupy-based implementation makes many calls to "scipy-like" functions, where the memory usage is not under control
115
+ # TODO try to estimate this
116
+ pass
104
117
 
105
118
  # Reconstruction
106
119
  # ---------------
@@ -127,7 +140,14 @@ def estimate_required_memory(process_config, delta_z=None, delta_a=None, max_mem
127
140
 
128
141
 
129
142
  def estimate_max_chunk_size(
130
- available_memory_GB, process_config, pipeline_part="all", n_rows=None, step=10, max_mem_allocation_GB=None
143
+ available_memory_GB,
144
+ process_config,
145
+ pipeline_part="all",
146
+ n_rows=None,
147
+ step=10,
148
+ max_mem_allocation_GB=None,
149
+ fft_plans=True,
150
+ debug=False,
131
151
  ):
132
152
  """
133
153
  Estimate the maximum size of the data chunk that can be loaded in memory.
@@ -195,7 +215,12 @@ def estimate_max_chunk_size(
195
215
  while True:
196
216
  try:
197
217
  mem = estimate_required_memory(
198
- process_config, delta_z=delta_z, delta_a=delta_a, max_mem_allocation_GB=max_mem_allocation_GB
218
+ process_config,
219
+ delta_z=delta_z,
220
+ delta_a=delta_a,
221
+ max_mem_allocation_GB=max_mem_allocation_GB,
222
+ fft_plans=fft_plans,
223
+ debug=debug,
199
224
  )
200
225
  except ValueError:
201
226
  # For very big dataset this function might return "0".
@@ -215,7 +240,17 @@ def estimate_max_chunk_size(
215
240
 
216
241
  process_config.processing_steps = processing_steps_bak
217
242
 
218
- res = last_valid_delta_a if pipeline_part == "radios" else last_valid_delta_z
243
+ if pipeline_part != "radios":
244
+ if mem / 1e9 < available_memory_GB:
245
+ res = min(delta_z, process_config.radio_shape()[0])
246
+ else:
247
+ res = last_valid_delta_z
248
+ else:
249
+ if mem / 1e9 < available_memory_GB:
250
+ res = min(delta_a, process_config.n_angles())
251
+ else:
252
+ res = last_valid_delta_a
253
+
219
254
  # Really not ideal. For very large dataset, "step" should be very small.
220
255
  # Otherwise we go from 0 -> OK to 10 -> not OK, and then retain 0...
221
256
  if res == 0: