nabu 2024.1.10__py3-none-any.whl → 2024.2.0rc1__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 +176 -597
  23. nabu/estimation/cor_sino.py +353 -25
  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 +125 -187
  40. nabu/pipeline/fullfield/chunked.py +162 -90
  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 +40 -72
  54. nabu/pipeline/utils.py +4 -2
  55. nabu/pipeline/writer.py +3 -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 +130 -85
  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.0rc1.dist-info}/METADATA +24 -17
  141. {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/RECORD +145 -121
  142. {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.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.0rc1.dist-info}/LICENSE +0 -0
  151. {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/entry_points.txt +0 -0
  152. {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/top_level.txt +0 -0
@@ -2,41 +2,35 @@
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):
40
34
  logger = LoggerOrPrint(logger)
41
35
  cor_options = cor_options or {}
42
36
  check_supported(method, list(cor_methods.keys()), "COR estimation method")
@@ -108,116 +102,41 @@ class CORFinderBase:
108
102
  Dataset information structure
109
103
  """
110
104
  check_supported(method, self.search_methods, "CoR estimation method")
105
+ self.method = method
106
+ self.cor_options = cor_options or {}
111
107
  self.logger = LoggerOrPrint(logger)
112
108
  self.dataset_info = dataset_info
113
109
  self.do_flatfield = do_flatfield
114
110
  self.shape = dataset_info.radio_dims[::-1]
115
- self._init_cor_finder(method, cor_options)
111
+ self._get_lookup_side()
112
+ self._init_cor_finder()
116
113
 
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)
126
-
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)
114
+ def _get_lookup_side(self):
115
+ """
116
+ Get the "initial guess" where the center-of-rotation (CoR) should be estimated.
117
+ For example 'center' means that CoR search will be done near the middle of the detector, i.e center column.
118
+ """
119
+ lookup_side = self.cor_options.get("side", None)
120
+ self._lookup_side = lookup_side
121
+ # User-provided scalar
122
+ if not (isinstance(lookup_side, str)) and np.isscalar(lookup_side):
123
+ return
129
124
 
130
- detector_width = self.dataset_info.radio_dims[0]
131
125
  default_lookup_side = "right" if self.dataset_info.is_halftomo else "center"
132
- near_init = self.cor_options.get("side", None)
133
126
 
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
-
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"
127
+ # By default in nabu config, side='from_file' meaning that we inspect the dataset information for CoR metadata
128
+ if lookup_side == "from_file":
129
+ initial_cor_pos = self.dataset_info.dataset_scanner.x_rotation_axis_pixel_position # relative pos in pixels
130
+ if initial_cor_pos is None or initial_cor_pos == 0:
131
+ self.logger.warning("Could not get an initial estimate for center of rotation in data file")
132
+ lookup_side = default_lookup_side
178
133
  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)
134
+ lookup_side = initial_cor_pos
135
+ self._lookup_side = initial_cor_pos
136
+
137
+ def _init_cor_finder(self):
138
+ cor_finder_cls = self.search_methods[self.method]["class"]
139
+ self.cor_finder = cor_finder_cls(verbose=False, logger=self.logger, extra_options=None)
221
140
 
222
141
 
223
142
  class CORFinder(CORFinderBase):
@@ -235,19 +154,17 @@ class CORFinder(CORFinderBase):
235
154
  },
236
155
  "sliding-window": {
237
156
  "class": CenterOfRotationSlidingWindow,
238
- "default_args": ["center"],
239
157
  },
240
158
  "growing-window": {
241
159
  "class": CenterOfRotationGrowingWindow,
242
160
  },
243
161
  "octave-accurate": {
244
162
  "class": CenterOfRotationOctaveAccurate,
245
- "default_args": ["center"],
246
163
  },
247
164
  }
248
165
 
249
166
  def __init__(
250
- self, method, dataset_info, do_flatfield=True, cor_options=None, logger=None, radio_angles: tuple = (0.0, np.pi)
167
+ self, method, dataset_info, do_flatfield=True, cor_options=None, logger=None, radio_angles=(0.0, np.pi)
251
168
  ):
252
169
  """
253
170
  Initialize a CORFinder object.
@@ -261,7 +178,6 @@ class CORFinder(CORFinderBase):
261
178
  super().__init__(method, dataset_info, do_flatfield=do_flatfield, cor_options=cor_options, logger=logger)
262
179
  self._radio_angles = radio_angles
263
180
  self._init_radios()
264
- self._init_flatfield()
265
181
  self._apply_flatfield()
266
182
  self._apply_tilt()
267
183
 
@@ -270,21 +186,16 @@ class CORFinder(CORFinderBase):
270
186
  self.dataset_info, radio_angles=self._radio_angles, return_indices=True
271
187
  )
272
188
 
273
- def _init_flatfield(self):
189
+ def _apply_flatfield(self):
274
190
  if not (self.do_flatfield):
275
191
  return
276
- self.flatfield = FlatFieldDataUrls(
192
+ self.flatfield = FlatField(
277
193
  self.radios.shape,
278
194
  flats=self.dataset_info.flats,
279
195
  darks=self.dataset_info.darks,
280
196
  radios_indices=self._radios_indices,
281
197
  interpolation="linear",
282
- convert_float=True,
283
198
  )
284
-
285
- def _apply_flatfield(self):
286
- if not (self.do_flatfield):
287
- return
288
199
  self.flatfield.normalize_radios(self.radios)
289
200
 
290
201
  def _apply_tilt(self):
@@ -306,11 +217,14 @@ class CORFinder(CORFinderBase):
306
217
  The estimated center of rotation for the current dataset.
307
218
  """
308
219
  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
220
+ # All find_shift() methods in self.search_methods have the same API with "img_1" and "img_2"
221
+ cor_exec_kwargs = update_func_kwargs(self.cor_finder.find_shift, self.cor_options)
222
+ cor_exec_kwargs["return_relative_to_middle"] = False
223
+ if self._lookup_side is not None:
224
+ cor_exec_kwargs["side"] = self._lookup_side
225
+ self.logger.debug("%s.find_shift(%s)" % (self.cor_finder.__class__.__name__, str(cor_exec_kwargs)))
226
+ shift = self.cor_finder.find_shift(self.radios[0], np.fliplr(self.radios[1]), **cor_exec_kwargs)
227
+ return shift
314
228
 
315
229
 
316
230
  # alias
@@ -329,16 +243,26 @@ class SinoCORFinder(CORFinderBase):
329
243
  },
330
244
  "sino-sliding-window": {
331
245
  "class": CenterOfRotationSlidingWindow,
332
- "default_args": ["right"],
333
246
  },
334
247
  "sino-growing-window": {
335
248
  "class": CenterOfRotationGrowingWindow,
336
249
  },
337
- "fourier-angles": {"class": CenterOfRotationFourierAngles, "default_args": [None, "center"]},
250
+ "fourier-angles": {"class": CenterOfRotationFourierAngles},
251
+ "vo": {
252
+ "class": CenterOfRotationVo,
253
+ },
338
254
  }
339
255
 
340
256
  def __init__(
341
- self, method, dataset_info, slice_idx="middle", subsampling=10, do_flatfield=True, cor_options=None, logger=None
257
+ self,
258
+ method,
259
+ dataset_info,
260
+ do_flatfield=True,
261
+ take_log=True,
262
+ cor_options=None,
263
+ logger=None,
264
+ slice_idx="middle",
265
+ subsampling=10,
342
266
  ):
343
267
  """
344
268
  Initialize a SinoCORFinder object.
@@ -355,20 +279,15 @@ class SinoCORFinder(CORFinderBase):
355
279
  subsampling strategy when building sinograms.
356
280
  As building the complete sinogram from raw projections might be tedious, the reading is done with subsampling.
357
281
  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
282
  """
361
283
  super().__init__(method, dataset_info, do_flatfield=do_flatfield, cor_options=cor_options, logger=logger)
362
- self._check_360()
363
284
  self._set_slice_idx(slice_idx)
364
285
  self._set_subsampling(subsampling)
365
286
  self._load_raw_sinogram()
366
287
  self._flatfield(do_flatfield)
367
- self._get_sinogram()
288
+ self._get_sinogram(take_log)
368
289
 
369
290
  def _check_360(self):
370
- if self.dataset_info.dataset_scanner.scan_range == 360:
371
- return
372
291
  if not is_fullturn_scan(self.dataset_info.rotation_angles):
373
292
  raise ValueError("Sinogram-based Center of Rotation estimation can only be used for 360 degrees scans")
374
293
 
@@ -382,50 +301,47 @@ class SinoCORFinder(CORFinderBase):
382
301
 
383
302
  def _set_subsampling(self, subsampling):
384
303
  projs_idx = sorted(self.dataset_info.projections.keys())
304
+ self.subsampling = None
385
305
  if is_int(subsampling):
386
306
  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
307
+ raise NotImplementedError
308
+ else:
391
309
  self.projs_indices = projs_idx[::subsampling]
392
310
  self.angles = self.dataset_info.rotation_angles[::subsampling]
311
+ self.subsampling = subsampling
393
312
  else: # Angular step
394
313
  raise NotImplementedError()
395
314
 
396
315
  def _load_raw_sinogram(self):
397
316
  if self.slice_idx is None:
398
317
  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
318
+ reader_kwargs = {
319
+ "sub_region": (slice(None, None, self.subsampling), slice(self.slice_idx, self.slice_idx + 1), slice(None))
320
+ }
321
+ if self.dataset_info.kind == "edf":
322
+ reader_kwargs = {"n_reading_threads": get_num_threads()}
323
+ self.data_reader = self.dataset_info.get_reader(**reader_kwargs)
324
+ self._radios = self.data_reader.load_data()
411
325
 
412
326
  def _flatfield(self, do_flatfield):
413
327
  self.do_flatfield = bool(do_flatfield)
414
328
  if not self.do_flatfield:
415
329
  return
416
- flatfield = FlatFieldDataUrls(
330
+ flats = {k: arr[self.slice_idx : self.slice_idx + 1, :] for k, arr in self.dataset_info.flats.items()}
331
+ darks = {k: arr[self.slice_idx : self.slice_idx + 1, :] for k, arr in self.dataset_info.darks.items()}
332
+ flatfield = FlatField(
417
333
  self._radios.shape,
418
- self.dataset_info.flats,
419
- self.dataset_info.darks,
334
+ flats,
335
+ darks,
420
336
  radios_indices=self.projs_indices,
421
- sub_region=(None, None, self.slice_idx, self.slice_idx + 1),
422
337
  )
423
338
  flatfield.normalize_radios(self._radios)
424
339
 
425
- def _get_sinogram(self):
426
- log = Log(self._radios.shape, clip_min=1e-6, clip_max=10.0)
340
+ def _get_sinogram(self, take_log):
427
341
  sinogram = self._radios[:, 0, :].copy()
428
- log.take_logarithm(sinogram)
342
+ if take_log:
343
+ log = Log(self._radios.shape, clip_min=1e-6, clip_max=10.0)
344
+ log.take_logarithm(sinogram)
429
345
  self.sinogram = sinogram
430
346
 
431
347
  @staticmethod
@@ -440,10 +356,29 @@ class SinoCORFinder(CORFinderBase):
440
356
 
441
357
  def find_cor(self):
442
358
  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
359
+
360
+ cor_exec_kwargs = update_func_kwargs(self.cor_finder.find_shift, self.cor_options)
361
+ cor_exec_kwargs["return_relative_to_middle"] = False
362
+
363
+ if self._lookup_side is not None:
364
+ cor_exec_kwargs["side"] = self._lookup_side
365
+
366
+ if self.method == "fourier-angles":
367
+ cor_exec_args = [self.sinogram]
368
+ cor_exec_kwargs["angles"] = self.dataset_info.rotation_angles
369
+ elif self.method == "vo":
370
+ cor_exec_args = [self.sinogram]
371
+ cor_exec_kwargs["halftomo"] = self.dataset_info.is_halftomo
372
+ cor_exec_kwargs["is_360"] = is_fullturn_scan(self.dataset_info.rotation_angles)
373
+ else:
374
+ # For these methods relying on find_shift() with two images, the sinogram needs to be split in two
375
+ img_1, img_2 = self._split_sinogram(self.sinogram)
376
+ cor_exec_args = [img_1, np.fliplr(img_2)]
377
+
378
+ self.logger.debug("%s.find_shift(%s)" % (self.cor_finder.__class__.__name__, str(cor_exec_kwargs)))
379
+ shift = self.cor_finder.find_shift(*cor_exec_args, **cor_exec_kwargs)
380
+
381
+ return shift
447
382
 
448
383
 
449
384
  # alias
@@ -472,14 +407,14 @@ class CompositeCORFinder(CORFinderBase):
472
407
  "class": CenterOfRotation, # Hack. Not used. Everything is done in the find_cor() func.
473
408
  }
474
409
  }
475
- _default_cor_options = {"low_pass": 0.4, "high_pass": 10, "side": "center", "near_pos": 0, "near_width": 20}
410
+ _default_cor_options = {"low_pass": 0.4, "high_pass": 10, "side": "near", "near_pos": 0, "near_width": 40}
476
411
 
477
412
  def __init__(
478
413
  self,
479
414
  dataset_info,
480
415
  oversampling=4,
481
416
  theta_interval=5,
482
- n_subsampling_y=10,
417
+ n_subsampling_y=40,
483
418
  take_log=True,
484
419
  cor_options=None,
485
420
  spike_threshold=0.04,
@@ -530,8 +465,6 @@ class CompositeCORFinder(CORFinderBase):
530
465
  if useful_span < np.pi:
531
466
  theta_interval = theta_interval * useful_span / np.pi
532
467
 
533
- # self._get_cor_options(cor_options)
534
-
535
468
  self.take_log = take_log
536
469
  self.ovs = oversampling
537
470
  self.theta_interval = theta_interval
@@ -566,16 +499,15 @@ class CompositeCORFinder(CORFinderBase):
566
499
 
567
500
  self.absolute_indices = sorted(self.dataset_info.projections.keys())
568
501
 
569
- my_flats = load_images_from_dataurl_dict(self.dataset_info.flats)
502
+ my_flats = self.dataset_info.flats
570
503
 
571
504
  if my_flats is not None and len(list(my_flats.keys())):
572
505
  self.use_flat = True
573
- self.flatfield = FlatFieldDataUrls(
506
+ self.flatfield = FlatField(
574
507
  (len(self.absolute_indices), self.sy, self.sx),
575
508
  self.dataset_info.flats,
576
509
  self.dataset_info.darks,
577
510
  radios_indices=self.absolute_indices,
578
- dtype=np.float64,
579
511
  )
580
512
  else:
581
513
  self.use_flat = False
@@ -678,7 +610,7 @@ class CompositeCORFinder(CORFinderBase):
678
610
  other_i = sorted_angle_indexes[0]
679
611
  elif insertion_point == len(sorted_all_angles):
680
612
  other_i = sorted_angle_indexes[insertion_point - 1]
681
- radio2 = self.get_radio(self.absolute_indices[other_i])
613
+ radio2 = self.get_radio(self.absolute_indices[other_i]) # pylint: disable=E0606
682
614
 
683
615
  self.sino[irad : irad + radio1.shape[0], :] = self._oversample(radio1)
684
616
  self.sino[
@@ -714,31 +646,38 @@ class CompositeCORFinder(CORFinderBase):
714
646
  tmp_sy, ovsd_sx = radio1.shape
715
647
  assert orig_sy == tmp_sy and orig_ovsd_sx == ovsd_sx, "this should not happen"
716
648
 
717
- if self.cor_options["side"] == "center":
649
+ cor_side = self.cor_options["side"]
650
+ if cor_side == "center":
718
651
  overlap_min = max(round(ovsd_sx - ovsd_sx / 3), 4)
719
652
  overlap_max = min(round(ovsd_sx + ovsd_sx / 3), 2 * ovsd_sx - 4)
720
- elif self.cor_options["side"] == "right":
653
+ elif cor_side == "right":
721
654
  overlap_min = max(4, self.ovs * self.high_pass * 3)
722
655
  overlap_max = ovsd_sx
723
- elif self.cor_options["side"] == "left":
656
+ elif cor_side == "left":
724
657
  overlap_min = ovsd_sx
725
658
  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":
659
+ elif cor_side == "all":
727
660
  overlap_min = max(4, self.ovs * self.high_pass * 3)
728
661
  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":
662
+ elif is_scalar(cor_side):
663
+ near_pos = cor_side
664
+ near_width = self.cor_options["near_width"]
665
+ overlap_min = max(4, ovsd_sx - 2 * self.ovs * (near_pos + near_width))
666
+ overlap_max = min(2 * ovsd_sx - 4, ovsd_sx - 2 * self.ovs * (near_pos - near_width))
667
+ # COMPAT.
668
+ elif cor_side == "near":
669
+ deprecation_warning(
670
+ "using side='near' is deprecated, use side=<a scalar> instead",
671
+ do_print=True,
672
+ func_name="composite_near_pos",
673
+ )
731
674
  near_pos = self.cor_options["near_pos"]
732
675
  near_width = self.cor_options["near_width"]
733
-
734
676
  overlap_min = max(4, ovsd_sx - 2 * self.ovs * (near_pos + near_width))
735
677
  overlap_max = min(2 * ovsd_sx - 4, ovsd_sx - 2 * self.ovs * (near_pos - near_width))
736
-
678
+ # ---
737
679
  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)
680
+ raise ValueError("Invalid option 'side=%s'" % self.cor_options["side"])
742
681
 
743
682
  if overlap_min > overlap_max:
744
683
  message = f""" There is no safe search range in find_cor once the margins corresponding to the high_pass filter are discarded.
@@ -938,13 +877,12 @@ class DetectorTiltEstimator:
938
877
  def _init_flatfield(self):
939
878
  if not (self.do_flatfield):
940
879
  return
941
- self.flatfield = FlatFieldDataUrls(
880
+ self.flatfield = FlatField(
942
881
  self.radios.shape,
943
882
  flats=self.dataset_info.flats,
944
883
  darks=self.dataset_info.darks,
945
884
  radios_indices=self.radios_indices,
946
885
  interpolation="linear",
947
- convert_float=True,
948
886
  )
949
887
 
950
888
  def _apply_flatfield(self):