nabu 2024.1.10__py3-none-any.whl → 2024.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) 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/__init__.py +0 -0
  15. nabu/app/tests/test_reduce_dark_flat.py +4 -1
  16. nabu/cuda/kernel.py +11 -2
  17. nabu/cuda/processing.py +2 -2
  18. nabu/cuda/src/cone.cu +77 -0
  19. nabu/cuda/src/hierarchical_backproj.cu +271 -0
  20. nabu/cuda/utils.py +0 -6
  21. nabu/estimation/alignment.py +5 -19
  22. nabu/estimation/cor.py +173 -599
  23. nabu/estimation/cor_sino.py +356 -26
  24. nabu/estimation/focus.py +63 -11
  25. nabu/estimation/tests/test_cor.py +124 -58
  26. nabu/estimation/tests/test_focus.py +6 -6
  27. nabu/estimation/tilt.py +2 -1
  28. nabu/estimation/utils.py +5 -33
  29. nabu/io/__init__.py +1 -1
  30. nabu/io/cast_volume.py +1 -1
  31. nabu/io/reader.py +416 -21
  32. nabu/io/tests/test_readers.py +422 -0
  33. nabu/io/tests/test_writers.py +1 -102
  34. nabu/io/writer.py +4 -433
  35. nabu/opencl/kernel.py +14 -3
  36. nabu/opencl/processing.py +8 -0
  37. nabu/pipeline/config_validators.py +5 -2
  38. nabu/pipeline/datadump.py +12 -5
  39. nabu/pipeline/estimators.py +162 -188
  40. nabu/pipeline/fullfield/chunked.py +168 -92
  41. nabu/pipeline/fullfield/chunked_cuda.py +7 -3
  42. nabu/pipeline/fullfield/computations.py +2 -7
  43. nabu/pipeline/fullfield/dataset_validator.py +0 -4
  44. nabu/pipeline/fullfield/nabu_config.py +37 -13
  45. nabu/pipeline/fullfield/processconfig.py +22 -13
  46. nabu/pipeline/fullfield/reconstruction.py +13 -9
  47. nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
  48. nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
  49. nabu/pipeline/helical/helical_reconstruction.py +1 -1
  50. nabu/pipeline/params.py +21 -1
  51. nabu/pipeline/processconfig.py +1 -12
  52. nabu/pipeline/reader.py +146 -0
  53. nabu/pipeline/tests/test_estimators.py +44 -72
  54. nabu/pipeline/utils.py +4 -2
  55. nabu/pipeline/writer.py +10 -2
  56. nabu/preproc/ccd_cuda.py +1 -1
  57. nabu/preproc/ctf.py +14 -7
  58. nabu/preproc/ctf_cuda.py +2 -3
  59. nabu/preproc/double_flatfield.py +5 -12
  60. nabu/preproc/double_flatfield_cuda.py +2 -2
  61. nabu/preproc/flatfield.py +5 -1
  62. nabu/preproc/flatfield_cuda.py +5 -1
  63. nabu/preproc/phase.py +24 -73
  64. nabu/preproc/phase_cuda.py +5 -8
  65. nabu/preproc/tests/test_ctf.py +11 -7
  66. nabu/preproc/tests/test_flatfield.py +67 -122
  67. nabu/preproc/tests/test_paganin.py +54 -30
  68. nabu/processing/azim.py +206 -0
  69. nabu/processing/convolution_cuda.py +1 -1
  70. nabu/processing/fft_cuda.py +15 -17
  71. nabu/processing/histogram.py +2 -0
  72. nabu/processing/histogram_cuda.py +2 -1
  73. nabu/processing/kernel_base.py +3 -0
  74. nabu/processing/muladd_cuda.py +1 -0
  75. nabu/processing/padding_opencl.py +1 -1
  76. nabu/processing/roll_opencl.py +1 -0
  77. nabu/processing/rotation_cuda.py +2 -2
  78. nabu/processing/tests/test_fft.py +17 -10
  79. nabu/processing/unsharp_cuda.py +1 -1
  80. nabu/reconstruction/cone.py +104 -40
  81. nabu/reconstruction/fbp.py +3 -0
  82. nabu/reconstruction/fbp_base.py +7 -2
  83. nabu/reconstruction/filtering.py +20 -7
  84. nabu/reconstruction/filtering_cuda.py +7 -1
  85. nabu/reconstruction/hbp.py +424 -0
  86. nabu/reconstruction/mlem.py +99 -0
  87. nabu/reconstruction/reconstructor.py +2 -0
  88. nabu/reconstruction/rings_cuda.py +19 -19
  89. nabu/reconstruction/sinogram_cuda.py +1 -0
  90. nabu/reconstruction/sinogram_opencl.py +3 -1
  91. nabu/reconstruction/tests/test_cone.py +10 -5
  92. nabu/reconstruction/tests/test_deringer.py +7 -6
  93. nabu/reconstruction/tests/test_fbp.py +124 -10
  94. nabu/reconstruction/tests/test_filtering.py +13 -11
  95. nabu/reconstruction/tests/test_halftomo.py +30 -4
  96. nabu/reconstruction/tests/test_mlem.py +91 -0
  97. nabu/reconstruction/tests/test_reconstructor.py +8 -3
  98. nabu/resources/dataset_analyzer.py +142 -92
  99. nabu/resources/gpu.py +1 -0
  100. nabu/resources/nxflatfield.py +134 -125
  101. nabu/resources/templates/id16a_fluo.conf +42 -0
  102. nabu/resources/tests/test_extract.py +10 -0
  103. nabu/resources/tests/test_nxflatfield.py +2 -2
  104. nabu/stitching/alignment.py +80 -24
  105. nabu/stitching/config.py +105 -68
  106. nabu/stitching/definitions.py +1 -0
  107. nabu/stitching/frame_composition.py +68 -60
  108. nabu/stitching/overlap.py +91 -51
  109. nabu/stitching/single_axis_stitching.py +32 -0
  110. nabu/stitching/slurm_utils.py +6 -6
  111. nabu/stitching/stitcher/__init__.py +0 -0
  112. nabu/stitching/stitcher/base.py +124 -0
  113. nabu/stitching/stitcher/dumper/__init__.py +3 -0
  114. nabu/stitching/stitcher/dumper/base.py +94 -0
  115. nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
  116. nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
  117. nabu/stitching/stitcher/post_processing.py +555 -0
  118. nabu/stitching/stitcher/pre_processing.py +1068 -0
  119. nabu/stitching/stitcher/single_axis.py +484 -0
  120. nabu/stitching/stitcher/stitcher.py +0 -0
  121. nabu/stitching/stitcher/y_stitcher.py +13 -0
  122. nabu/stitching/stitcher/z_stitcher.py +45 -0
  123. nabu/stitching/stitcher_2D.py +278 -0
  124. nabu/stitching/tests/test_config.py +12 -37
  125. nabu/stitching/tests/test_frame_composition.py +33 -59
  126. nabu/stitching/tests/test_overlap.py +149 -7
  127. nabu/stitching/tests/test_utils.py +1 -1
  128. nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
  129. nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
  130. nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
  131. nabu/stitching/utils/__init__.py +1 -0
  132. nabu/stitching/utils/post_processing.py +281 -0
  133. nabu/stitching/utils/tests/test_post-processing.py +21 -0
  134. nabu/stitching/{utils.py → utils/utils.py} +79 -52
  135. nabu/stitching/y_stitching.py +27 -0
  136. nabu/stitching/z_stitching.py +32 -2281
  137. nabu/testutils.py +1 -152
  138. nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
  139. nabu/utils.py +158 -61
  140. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/METADATA +24 -17
  141. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/RECORD +145 -121
  142. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/WHEEL +1 -1
  143. nabu/io/tiffwriter_zmm.py +0 -99
  144. nabu/pipeline/fallback_utils.py +0 -149
  145. nabu/pipeline/helical/tests/test_accumulator.py +0 -158
  146. nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
  147. nabu/pipeline/helical/tests/test_strategy.py +0 -61
  148. nabu/pipeline/helical/utils.py +0 -51
  149. nabu/pipeline/tests/test_chunk_reader.py +0 -74
  150. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
  151. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
  152. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/top_level.txt +0 -0
@@ -2,41 +2,50 @@
2
2
  nabu.pipeline.estimators: helper classes/functions to estimate parameters of a dataset
3
3
  (center of rotation, detector tilt, etc).
4
4
  """
5
+
5
6
  import inspect
6
7
  import numpy as np
7
8
  import scipy.fft # pylint: disable=E0611
8
9
  from silx.io import get_data
9
- from typing import Union, Optional
10
10
  import math
11
- from numbers import Real
12
11
  from scipy import ndimage as nd
13
-
14
- from ..preproc.flatfield import FlatFieldDataUrls
12
+ from ..preproc.flatfield import FlatField
15
13
  from ..estimation.cor import (
16
14
  CenterOfRotation,
17
15
  CenterOfRotationAdaptiveSearch,
18
16
  CenterOfRotationSlidingWindow,
19
17
  CenterOfRotationGrowingWindow,
20
- CenterOfRotationFourierAngles,
21
18
  CenterOfRotationOctaveAccurate,
22
19
  )
23
- from ..estimation.cor_sino import SinoCorInterface
20
+ from ..estimation.cor_sino import SinoCorInterface, CenterOfRotationFourierAngles, CenterOfRotationVo
24
21
  from ..estimation.tilt import CameraTilt
25
22
  from ..estimation.utils import is_fullturn_scan
26
23
  from ..resources.logger import LoggerOrPrint
27
24
  from ..resources.utils import extract_parameters
28
- from ..utils import check_supported, is_int
29
- from .params import tilt_methods
25
+ from ..utils import check_supported, deprecation_warning, get_num_threads, is_int, is_scalar
30
26
  from ..resources.dataset_analyzer import get_radio_pair
31
27
  from ..processing.rotation import Rotation
32
- from ..io.reader import ChunkReader
33
28
  from ..preproc.ccd import Log, CCDFilter
34
29
  from ..misc import fourier_filters
35
- from .params import cor_methods
36
- from ..io.reader import load_images_from_dataurl_dict
30
+ from .params import cor_methods, tilt_methods
37
31
 
38
32
 
39
- def estimate_cor(method, dataset_info, do_flatfield=True, cor_options: Optional[Union[str, dict]] = None, logger=None):
33
+ def estimate_cor(method, dataset_info, do_flatfield=True, cor_options=None, logger=None):
34
+ """
35
+ High level function to compute the center of rotation (COR)
36
+
37
+ Parameters
38
+ ----------
39
+ method: name of the method to be used for computing the center of rotation
40
+ dataset_info: `nabu.resources.dataset_analyzer.DatasetAnalyzer`
41
+ Dataset information structure
42
+ do_flatfield: If True apply flat field to compute the center of rotation
43
+ cor_options: optional dictionary that can contain the following keys:
44
+ * slice_idx: index of the slice to use for computing the sinogram (for sinogram based algorithms)
45
+ * subsampling subsampling
46
+ * radio_angles: angles of the radios to use (for radio based algorithms)
47
+ logger: logging object
48
+ """
40
49
  logger = LoggerOrPrint(logger)
41
50
  cor_options = cor_options or {}
42
51
  check_supported(method, list(cor_methods.keys()), "COR estimation method")
@@ -62,6 +71,7 @@ def estimate_cor(method, dataset_info, do_flatfield=True, cor_options: Optional[
62
71
  dataset_info,
63
72
  do_flatfield=do_flatfield,
64
73
  cor_options=cor_options,
74
+ radio_angles=cor_options.get("radio_angles", (0.0, np.pi)),
65
75
  logger=logger,
66
76
  )
67
77
  estimated_cor = cor_finder.find_cor()
@@ -72,6 +82,7 @@ def estimate_cor(method, dataset_info, do_flatfield=True, cor_options: Optional[
72
82
  slice_idx=cor_options.get("slice_idx", "middle"),
73
83
  subsampling=cor_options.get("subsampling", 10),
74
84
  do_flatfield=do_flatfield,
85
+ take_log=cor_options.get("take_log", True),
75
86
  cor_options=cor_options,
76
87
  logger=logger,
77
88
  )
@@ -108,116 +119,41 @@ class CORFinderBase:
108
119
  Dataset information structure
109
120
  """
110
121
  check_supported(method, self.search_methods, "CoR estimation method")
122
+ self.method = method
123
+ self.cor_options = cor_options or {}
111
124
  self.logger = LoggerOrPrint(logger)
112
125
  self.dataset_info = dataset_info
113
126
  self.do_flatfield = do_flatfield
114
127
  self.shape = dataset_info.radio_dims[::-1]
115
- self._init_cor_finder(method, cor_options)
116
-
117
- def _init_cor_finder(self, method, cor_options):
118
- self.method = method
119
- if not isinstance(cor_options, (type(None), dict)):
120
- raise TypeError(
121
- f"cor_options is expected to be an optional instance of dict. Get {cor_options} ({type(cor_options)}) instead"
122
- )
123
- self.cor_options = {}
124
- if isinstance(cor_options, dict):
125
- self.cor_options.update(cor_options)
128
+ self._get_lookup_side()
129
+ self._init_cor_finder()
126
130
 
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)
131
+ def _get_lookup_side(self):
132
+ """
133
+ Get the "initial guess" where the center-of-rotation (CoR) should be estimated.
134
+ For example 'center' means that CoR search will be done near the middle of the detector, i.e center column.
135
+ """
136
+ lookup_side = self.cor_options.get("side", None)
137
+ self._lookup_side = lookup_side
138
+ # User-provided scalar
139
+ if not (isinstance(lookup_side, str)) and np.isscalar(lookup_side):
140
+ return
129
141
 
130
- detector_width = self.dataset_info.radio_dims[0]
131
142
  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": int(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
143
 
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"
144
+ # By default in nabu config, side='from_file' meaning that we inspect the dataset information for CoR metadata
145
+ if lookup_side == "from_file":
146
+ initial_cor_pos = self.dataset_info.dataset_scanner.x_rotation_axis_pixel_position # relative pos in pixels
147
+ if initial_cor_pos is None or initial_cor_pos == 0:
148
+ self.logger.warning("Could not get an initial estimate for center of rotation in data file")
149
+ lookup_side = default_lookup_side
178
150
  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
-
201
- lookup_side = self.cor_options.get("side", default_lookup_side)
202
-
203
- # OctaveAccurate
204
- # if cor_class == CenterOfRotationOctaveAccurate:
205
- # lookup_side = "center"
206
- angles = self.dataset_info.rotation_angles
207
-
208
- self.cor_exec_args = []
209
- self.cor_exec_args.extend(self.search_methods[method].get("default_args", []))
210
-
211
- # CenterOfRotationSlidingWindow is the only class to have a mandatory argument ("side")
212
- # TODO - it would be more elegant to have it as a kwarg...
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
219
- #
220
- self.cor_exec_kwargs = update_func_kwargs(self.cor_finder.find_shift, self.cor_options)
151
+ lookup_side = initial_cor_pos
152
+ self._lookup_side = initial_cor_pos
153
+
154
+ def _init_cor_finder(self):
155
+ cor_finder_cls = self.search_methods[self.method]["class"]
156
+ self.cor_finder = cor_finder_cls(verbose=False, logger=self.logger, extra_options=None)
221
157
 
222
158
 
223
159
  class CORFinder(CORFinderBase):
@@ -235,19 +171,17 @@ class CORFinder(CORFinderBase):
235
171
  },
236
172
  "sliding-window": {
237
173
  "class": CenterOfRotationSlidingWindow,
238
- "default_args": ["center"],
239
174
  },
240
175
  "growing-window": {
241
176
  "class": CenterOfRotationGrowingWindow,
242
177
  },
243
178
  "octave-accurate": {
244
179
  "class": CenterOfRotationOctaveAccurate,
245
- "default_args": ["center"],
246
180
  },
247
181
  }
248
182
 
249
183
  def __init__(
250
- self, method, dataset_info, do_flatfield=True, cor_options=None, logger=None, radio_angles: tuple = (0.0, np.pi)
184
+ self, method, dataset_info, do_flatfield=True, cor_options=None, logger=None, radio_angles=(0.0, np.pi)
251
185
  ):
252
186
  """
253
187
  Initialize a CORFinder object.
@@ -261,30 +195,30 @@ class CORFinder(CORFinderBase):
261
195
  super().__init__(method, dataset_info, do_flatfield=do_flatfield, cor_options=cor_options, logger=logger)
262
196
  self._radio_angles = radio_angles
263
197
  self._init_radios()
264
- self._init_flatfield()
265
198
  self._apply_flatfield()
266
199
  self._apply_tilt()
200
+ # octave-accurate does not support half-acquisition scans,
201
+ # but information on field of view is only known here with the "dataset_info" object.
202
+ # Do the check here.
203
+ if self.dataset_info.is_halftomo and method == "octave-accurate":
204
+ raise ValueError("The CoR estimator 'octave-accurate' does not support half-acquisition scans")
205
+ #
267
206
 
268
207
  def _init_radios(self):
269
208
  self.radios, self._radios_indices = get_radio_pair(
270
209
  self.dataset_info, radio_angles=self._radio_angles, return_indices=True
271
210
  )
272
211
 
273
- def _init_flatfield(self):
212
+ def _apply_flatfield(self):
274
213
  if not (self.do_flatfield):
275
214
  return
276
- self.flatfield = FlatFieldDataUrls(
215
+ self.flatfield = FlatField(
277
216
  self.radios.shape,
278
217
  flats=self.dataset_info.flats,
279
218
  darks=self.dataset_info.darks,
280
219
  radios_indices=self._radios_indices,
281
220
  interpolation="linear",
282
- convert_float=True,
283
221
  )
284
-
285
- def _apply_flatfield(self):
286
- if not (self.do_flatfield):
287
- return
288
222
  self.flatfield.normalize_radios(self.radios)
289
223
 
290
224
  def _apply_tilt(self):
@@ -306,11 +240,21 @@ class CORFinder(CORFinderBase):
306
240
  The estimated center of rotation for the current dataset.
307
241
  """
308
242
  self.logger.info("Estimating center of rotation")
309
- self.logger.debug("%s.find_shift(%s)" % (self.cor_finder.__class__.__name__, str(self.cor_exec_kwargs)))
310
- shift = self.cor_finder.find_shift(
311
- self.radios[0], np.fliplr(self.radios[1]), *self.cor_exec_args, **self.cor_exec_kwargs
312
- )
313
- return self.shape[1] / 2 + shift
243
+ # All find_shift() methods in self.search_methods have the same API with "img_1" and "img_2"
244
+ cor_exec_kwargs = update_func_kwargs(self.cor_finder.find_shift, self.cor_options)
245
+ cor_exec_kwargs["return_relative_to_middle"] = False
246
+ # ----- FIXME -----
247
+ # 'self.cor_options' can contain 'side="from_file"', and we should not modify it directly
248
+ # because it's entered by the user.
249
+ # Either make a copy of self.cor_options, or change the inspect() mechanism
250
+ if cor_exec_kwargs.get("side", None) == "from_file":
251
+ cor_exec_kwargs["side"] = self._lookup_side or "center"
252
+ # ------
253
+ if self._lookup_side is not None:
254
+ cor_exec_kwargs["side"] = self._lookup_side
255
+ self.logger.debug("%s.find_shift(%s)" % (self.cor_finder.__class__.__name__, str(cor_exec_kwargs)))
256
+ shift = self.cor_finder.find_shift(self.radios[0], np.fliplr(self.radios[1]), **cor_exec_kwargs)
257
+ return shift
314
258
 
315
259
 
316
260
  # alias
@@ -329,16 +273,26 @@ class SinoCORFinder(CORFinderBase):
329
273
  },
330
274
  "sino-sliding-window": {
331
275
  "class": CenterOfRotationSlidingWindow,
332
- "default_args": ["right"],
333
276
  },
334
277
  "sino-growing-window": {
335
278
  "class": CenterOfRotationGrowingWindow,
336
279
  },
337
- "fourier-angles": {"class": CenterOfRotationFourierAngles, "default_args": [None, "center"]},
280
+ "fourier-angles": {"class": CenterOfRotationFourierAngles},
281
+ "vo": {
282
+ "class": CenterOfRotationVo,
283
+ },
338
284
  }
339
285
 
340
286
  def __init__(
341
- self, method, dataset_info, slice_idx="middle", subsampling=10, do_flatfield=True, cor_options=None, logger=None
287
+ self,
288
+ method,
289
+ dataset_info,
290
+ do_flatfield=True,
291
+ take_log=True,
292
+ cor_options=None,
293
+ logger=None,
294
+ slice_idx="middle",
295
+ subsampling=10,
342
296
  ):
343
297
  """
344
298
  Initialize a SinoCORFinder object.
@@ -355,20 +309,15 @@ class SinoCORFinder(CORFinderBase):
355
309
  subsampling strategy when building sinograms.
356
310
  As building the complete sinogram from raw projections might be tedious, the reading is done with subsampling.
357
311
  A positive integer value means the subsampling step (i.e `projections[::subsampling]`).
358
- A negative integer value means we take -subsampling projections in total.
359
- A float value indicates the angular step in DEGREES.
360
312
  """
361
313
  super().__init__(method, dataset_info, do_flatfield=do_flatfield, cor_options=cor_options, logger=logger)
362
- self._check_360()
363
314
  self._set_slice_idx(slice_idx)
364
315
  self._set_subsampling(subsampling)
365
316
  self._load_raw_sinogram()
366
317
  self._flatfield(do_flatfield)
367
- self._get_sinogram()
318
+ self._get_sinogram(take_log)
368
319
 
369
320
  def _check_360(self):
370
- if self.dataset_info.dataset_scanner.scan_range == 360:
371
- return
372
321
  if not is_fullturn_scan(self.dataset_info.rotation_angles):
373
322
  raise ValueError("Sinogram-based Center of Rotation estimation can only be used for 360 degrees scans")
374
323
 
@@ -382,50 +331,47 @@ class SinoCORFinder(CORFinderBase):
382
331
 
383
332
  def _set_subsampling(self, subsampling):
384
333
  projs_idx = sorted(self.dataset_info.projections.keys())
334
+ self.subsampling = None
385
335
  if is_int(subsampling):
386
336
  if subsampling < 0: # Total number of angles
387
- n_angles = -subsampling
388
- indices_float = np.linspace(projs_idx[0], projs_idx[-1], n_angles, endpoint=True)
389
- self.projs_indices = np.round(indices_float).astype(np.int32).tolist()
390
- else: # Subsampling step
337
+ raise NotImplementedError
338
+ else:
391
339
  self.projs_indices = projs_idx[::subsampling]
392
340
  self.angles = self.dataset_info.rotation_angles[::subsampling]
341
+ self.subsampling = subsampling
393
342
  else: # Angular step
394
343
  raise NotImplementedError()
395
344
 
396
345
  def _load_raw_sinogram(self):
397
346
  if self.slice_idx is None:
398
347
  raise ValueError("Unknow slice index")
399
- # Subsample projections
400
- files = {}
401
- for idx in self.projs_indices:
402
- files[idx] = self.dataset_info.projections[idx]
403
- self.files = files
404
- self.data_reader = ChunkReader(
405
- self.files,
406
- sub_region=(None, None, self.slice_idx, self.slice_idx + 1),
407
- convert_float=True,
408
- )
409
- self.data_reader.load_files()
410
- self._radios = self.data_reader.files_data
348
+ reader_kwargs = {
349
+ "sub_region": (slice(None, None, self.subsampling), slice(self.slice_idx, self.slice_idx + 1), slice(None))
350
+ }
351
+ if self.dataset_info.kind == "edf":
352
+ reader_kwargs = {"n_reading_threads": get_num_threads()}
353
+ self.data_reader = self.dataset_info.get_reader(**reader_kwargs)
354
+ self._radios = self.data_reader.load_data()
411
355
 
412
356
  def _flatfield(self, do_flatfield):
413
357
  self.do_flatfield = bool(do_flatfield)
414
358
  if not self.do_flatfield:
415
359
  return
416
- flatfield = FlatFieldDataUrls(
360
+ flats = {k: arr[self.slice_idx : self.slice_idx + 1, :] for k, arr in self.dataset_info.flats.items()}
361
+ darks = {k: arr[self.slice_idx : self.slice_idx + 1, :] for k, arr in self.dataset_info.darks.items()}
362
+ flatfield = FlatField(
417
363
  self._radios.shape,
418
- self.dataset_info.flats,
419
- self.dataset_info.darks,
364
+ flats,
365
+ darks,
420
366
  radios_indices=self.projs_indices,
421
- sub_region=(None, None, self.slice_idx, self.slice_idx + 1),
422
367
  )
423
368
  flatfield.normalize_radios(self._radios)
424
369
 
425
- def _get_sinogram(self):
426
- log = Log(self._radios.shape, clip_min=1e-6, clip_max=10.0)
370
+ def _get_sinogram(self, take_log):
427
371
  sinogram = self._radios[:, 0, :].copy()
428
- log.take_logarithm(sinogram)
372
+ if take_log:
373
+ log = Log(self._radios.shape, clip_min=1e-6, clip_max=10.0)
374
+ log.take_logarithm(sinogram)
429
375
  self.sinogram = sinogram
430
376
 
431
377
  @staticmethod
@@ -440,10 +386,35 @@ class SinoCORFinder(CORFinderBase):
440
386
 
441
387
  def find_cor(self):
442
388
  self.logger.info("Estimating center of rotation")
443
- self.logger.debug("%s.find_shift(%s)" % (self.cor_finder.__class__.__name__, str(self.cor_exec_kwargs)))
444
- img_1, img_2 = self._split_sinogram(self.sinogram)
445
- shift = self.cor_finder.find_shift(img_1, np.fliplr(img_2), *self.cor_exec_args, **self.cor_exec_kwargs)
446
- return self.shape[1] / 2 + shift
389
+
390
+ cor_exec_kwargs = update_func_kwargs(self.cor_finder.find_shift, self.cor_options)
391
+ cor_exec_kwargs["return_relative_to_middle"] = False
392
+ # FIXME
393
+ # 'self.cor_options' can contain 'side="from_file"', and we should not modify it directly
394
+ # because it's entered by the user.
395
+ # Either make a copy of self.cor_options, or change the inspect() mechanism
396
+ if cor_exec_kwargs["side"] == "from_file":
397
+ cor_exec_kwargs["side"] = self._lookup_side or "center"
398
+ #
399
+ if self._lookup_side is not None:
400
+ cor_exec_kwargs["side"] = self._lookup_side
401
+
402
+ if self.method == "fourier-angles":
403
+ cor_exec_args = [self.sinogram]
404
+ cor_exec_kwargs["angles"] = self.dataset_info.rotation_angles
405
+ elif self.method == "vo":
406
+ cor_exec_args = [self.sinogram]
407
+ cor_exec_kwargs["halftomo"] = self.dataset_info.is_halftomo
408
+ cor_exec_kwargs["is_360"] = is_fullturn_scan(self.dataset_info.rotation_angles)
409
+ else:
410
+ # For these methods relying on find_shift() with two images, the sinogram needs to be split in two
411
+ img_1, img_2 = self._split_sinogram(self.sinogram)
412
+ cor_exec_args = [img_1, np.fliplr(img_2)]
413
+
414
+ self.logger.debug("%s.find_shift(%s)" % (self.cor_finder.__class__.__name__, str(cor_exec_kwargs)))
415
+ shift = self.cor_finder.find_shift(*cor_exec_args, **cor_exec_kwargs)
416
+
417
+ return shift
447
418
 
448
419
 
449
420
  # alias
@@ -472,14 +443,14 @@ class CompositeCORFinder(CORFinderBase):
472
443
  "class": CenterOfRotation, # Hack. Not used. Everything is done in the find_cor() func.
473
444
  }
474
445
  }
475
- _default_cor_options = {"low_pass": 0.4, "high_pass": 10, "side": "center", "near_pos": 0, "near_width": 20}
446
+ _default_cor_options = {"low_pass": 0.4, "high_pass": 10, "side": "near", "near_pos": 0, "near_width": 40}
476
447
 
477
448
  def __init__(
478
449
  self,
479
450
  dataset_info,
480
451
  oversampling=4,
481
452
  theta_interval=5,
482
- n_subsampling_y=10,
453
+ n_subsampling_y=40,
483
454
  take_log=True,
484
455
  cor_options=None,
485
456
  spike_threshold=0.04,
@@ -519,7 +490,7 @@ class CompositeCORFinder(CORFinderBase):
519
490
  if (self.angle_max - self.angle_min) < 1.2 * np.pi:
520
491
  useful_span = None
521
492
  raise ValueError(
522
- f"""Sinogram-based Center of Rotation estimation can only be used for scans over more than 180 degrees.
493
+ f"""Sinogram-based Center of Rotation estimation can only be used for scans over more than 180 degrees.
523
494
  Your angular span was barely above 180 degrees, it was in fact {((self.angle_max - self.angle_min)/np.pi):.2f} x 180
524
495
  and it is not considered to be enough by the discriminating condition which requires at least 1.2 half-turns
525
496
  """
@@ -530,8 +501,6 @@ class CompositeCORFinder(CORFinderBase):
530
501
  if useful_span < np.pi:
531
502
  theta_interval = theta_interval * useful_span / np.pi
532
503
 
533
- # self._get_cor_options(cor_options)
534
-
535
504
  self.take_log = take_log
536
505
  self.ovs = oversampling
537
506
  self.theta_interval = theta_interval
@@ -566,16 +535,15 @@ class CompositeCORFinder(CORFinderBase):
566
535
 
567
536
  self.absolute_indices = sorted(self.dataset_info.projections.keys())
568
537
 
569
- my_flats = load_images_from_dataurl_dict(self.dataset_info.flats)
538
+ my_flats = self.dataset_info.flats
570
539
 
571
540
  if my_flats is not None and len(list(my_flats.keys())):
572
541
  self.use_flat = True
573
- self.flatfield = FlatFieldDataUrls(
542
+ self.flatfield = FlatField(
574
543
  (len(self.absolute_indices), self.sy, self.sx),
575
544
  self.dataset_info.flats,
576
545
  self.dataset_info.darks,
577
546
  radios_indices=self.absolute_indices,
578
- dtype=np.float64,
579
547
  )
580
548
  else:
581
549
  self.use_flat = False
@@ -678,7 +646,7 @@ class CompositeCORFinder(CORFinderBase):
678
646
  other_i = sorted_angle_indexes[0]
679
647
  elif insertion_point == len(sorted_all_angles):
680
648
  other_i = sorted_angle_indexes[insertion_point - 1]
681
- radio2 = self.get_radio(self.absolute_indices[other_i])
649
+ radio2 = self.get_radio(self.absolute_indices[other_i]) # pylint: disable=E0606
682
650
 
683
651
  self.sino[irad : irad + radio1.shape[0], :] = self._oversample(radio1)
684
652
  self.sino[
@@ -714,31 +682,38 @@ class CompositeCORFinder(CORFinderBase):
714
682
  tmp_sy, ovsd_sx = radio1.shape
715
683
  assert orig_sy == tmp_sy and orig_ovsd_sx == ovsd_sx, "this should not happen"
716
684
 
717
- if self.cor_options["side"] == "center":
685
+ cor_side = self.cor_options["side"]
686
+ if cor_side == "center":
718
687
  overlap_min = max(round(ovsd_sx - ovsd_sx / 3), 4)
719
688
  overlap_max = min(round(ovsd_sx + ovsd_sx / 3), 2 * ovsd_sx - 4)
720
- elif self.cor_options["side"] == "right":
689
+ elif cor_side == "right":
721
690
  overlap_min = max(4, self.ovs * self.high_pass * 3)
722
691
  overlap_max = ovsd_sx
723
- elif self.cor_options["side"] == "left":
692
+ elif cor_side == "left":
724
693
  overlap_min = ovsd_sx
725
694
  overlap_max = min(2 * ovsd_sx - 4, 2 * ovsd_sx - self.ovs * self.ovs * self.high_pass * 3)
726
- elif self.cor_options["side"] == "all":
695
+ elif cor_side == "all":
727
696
  overlap_min = max(4, self.ovs * self.high_pass * 3)
728
697
  overlap_max = min(2 * ovsd_sx - 4, 2 * ovsd_sx - self.ovs * self.ovs * self.high_pass * 3)
729
-
730
- elif self.cor_options["side"] == "near":
698
+ elif is_scalar(cor_side):
699
+ near_pos = cor_side
700
+ near_width = self.cor_options["near_width"]
701
+ overlap_min = max(4, ovsd_sx - 2 * self.ovs * (near_pos + near_width))
702
+ overlap_max = min(2 * ovsd_sx - 4, ovsd_sx - 2 * self.ovs * (near_pos - near_width))
703
+ # COMPAT.
704
+ elif cor_side == "near":
705
+ deprecation_warning(
706
+ "using side='near' is deprecated, use side=<a scalar> instead",
707
+ do_print=True,
708
+ func_name="composite_near_pos",
709
+ )
731
710
  near_pos = self.cor_options["near_pos"]
732
711
  near_width = self.cor_options["near_width"]
733
-
734
712
  overlap_min = max(4, ovsd_sx - 2 * self.ovs * (near_pos + near_width))
735
713
  overlap_max = min(2 * ovsd_sx - 4, ovsd_sx - 2 * self.ovs * (near_pos - near_width))
736
-
714
+ # ---
737
715
  else:
738
- message = f""" The cor options "side" can only have one of the three possible values ["","",""].
739
- But it has the value "{self.cor_options["side"]}" instead
740
- """
741
- raise ValueError(message)
716
+ raise ValueError("Invalid option 'side=%s'" % self.cor_options["side"])
742
717
 
743
718
  if overlap_min > overlap_max:
744
719
  message = f""" There is no safe search range in find_cor once the margins corresponding to the high_pass filter are discarded.
@@ -938,13 +913,12 @@ class DetectorTiltEstimator:
938
913
  def _init_flatfield(self):
939
914
  if not (self.do_flatfield):
940
915
  return
941
- self.flatfield = FlatFieldDataUrls(
916
+ self.flatfield = FlatField(
942
917
  self.radios.shape,
943
918
  flats=self.dataset_info.flats,
944
919
  darks=self.dataset_info.darks,
945
920
  radios_indices=self.radios_indices,
946
921
  interpolation="linear",
947
- convert_float=True,
948
922
  )
949
923
 
950
924
  def _apply_flatfield(self):