nabu 2022.3.0a1__py3-none-any.whl → 2023.1.0a2__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 (96) hide show
  1. nabu/__init__.py +1 -1
  2. nabu/app/bootstrap.py +7 -1
  3. nabu/app/cast_volume.py +8 -2
  4. nabu/app/cli_configs.py +69 -0
  5. nabu/app/composite_cor.py +97 -0
  6. nabu/app/create_distortion_map_from_poly.py +118 -0
  7. nabu/app/nx_z_splitter.py +1 -1
  8. nabu/app/prepare_weights_double.py +21 -16
  9. nabu/app/reconstruct_helical.py +0 -1
  10. nabu/app/utils.py +10 -5
  11. nabu/cuda/processing.py +1 -0
  12. nabu/cuda/tests/test_padding.py +1 -0
  13. nabu/cuda/utils.py +1 -0
  14. nabu/distributed/__init__.py +0 -0
  15. nabu/distributed/utils.py +57 -0
  16. nabu/distributed/worker.py +543 -0
  17. nabu/estimation/cor.py +3 -7
  18. nabu/estimation/cor_sino.py +2 -1
  19. nabu/estimation/distortion.py +6 -4
  20. nabu/io/cast_volume.py +10 -1
  21. nabu/io/detector_distortion.py +305 -0
  22. nabu/io/reader.py +37 -7
  23. nabu/io/reader_helical.py +0 -3
  24. nabu/io/tests/test_cast_volume.py +16 -4
  25. nabu/io/tests/test_detector_distortion.py +178 -0
  26. nabu/io/tests/test_writers.py +2 -2
  27. nabu/io/tiffwriter_zmm.py +2 -3
  28. nabu/io/writer.py +84 -1
  29. nabu/io/writer_BACKUP_193259.py +556 -0
  30. nabu/io/writer_BACKUP_193381.py +556 -0
  31. nabu/io/writer_BASE_193259.py +548 -0
  32. nabu/io/writer_BASE_193381.py +548 -0
  33. nabu/io/writer_LOCAL_193259.py +550 -0
  34. nabu/io/writer_LOCAL_193381.py +550 -0
  35. nabu/io/writer_REMOTE_193259.py +557 -0
  36. nabu/io/writer_REMOTE_193381.py +557 -0
  37. nabu/misc/fourier_filters.py +2 -0
  38. nabu/misc/rotation.py +0 -1
  39. nabu/misc/tests/test_rotation.py +1 -0
  40. nabu/pipeline/config_validators.py +10 -0
  41. nabu/pipeline/datadump.py +1 -1
  42. nabu/pipeline/dataset_validator.py +0 -1
  43. nabu/pipeline/detector_distortion_provider.py +20 -0
  44. nabu/pipeline/estimators.py +35 -21
  45. nabu/pipeline/fallback_utils.py +1 -1
  46. nabu/pipeline/fullfield/chunked.py +30 -15
  47. nabu/pipeline/fullfield/chunked_black.py +881 -0
  48. nabu/pipeline/fullfield/chunked_cuda.py +34 -4
  49. nabu/pipeline/fullfield/chunked_fb.py +966 -0
  50. nabu/pipeline/fullfield/chunked_google.py +921 -0
  51. nabu/pipeline/fullfield/chunked_pep8.py +920 -0
  52. nabu/pipeline/fullfield/computations.py +7 -6
  53. nabu/pipeline/fullfield/dataset_validator.py +1 -1
  54. nabu/pipeline/fullfield/grouped_cuda.py +6 -0
  55. nabu/pipeline/fullfield/nabu_config.py +15 -3
  56. nabu/pipeline/fullfield/processconfig.py +5 -0
  57. nabu/pipeline/fullfield/reconstruction.py +1 -2
  58. nabu/pipeline/helical/gridded_accumulator.py +1 -8
  59. nabu/pipeline/helical/helical_chunked_regridded.py +48 -33
  60. nabu/pipeline/helical/helical_reconstruction.py +1 -9
  61. nabu/pipeline/helical/nabu_config.py +11 -14
  62. nabu/pipeline/helical/span_strategy.py +11 -4
  63. nabu/pipeline/helical/tests/test_accumulator.py +0 -3
  64. nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -6
  65. nabu/pipeline/helical/tests/test_strategy.py +0 -1
  66. nabu/pipeline/helical/weight_balancer.py +0 -1
  67. nabu/pipeline/params.py +4 -0
  68. nabu/pipeline/processconfig.py +6 -2
  69. nabu/pipeline/writer.py +9 -4
  70. nabu/preproc/distortion.py +4 -3
  71. nabu/preproc/double_flatfield.py +16 -4
  72. nabu/preproc/double_flatfield_cuda.py +3 -2
  73. nabu/preproc/double_flatfield_variable_region.py +13 -4
  74. nabu/preproc/flatfield.py +29 -7
  75. nabu/preproc/flatfield_cuda.py +0 -1
  76. nabu/preproc/flatfield_variable_region.py +5 -2
  77. nabu/preproc/phase.py +0 -1
  78. nabu/preproc/phase_cuda.py +0 -1
  79. nabu/preproc/tests/test_ctf.py +4 -3
  80. nabu/preproc/tests/test_flatfield.py +6 -7
  81. nabu/reconstruction/fbp_opencl.py +1 -1
  82. nabu/reconstruction/filtering.py +0 -1
  83. nabu/reconstruction/tests/test_fbp.py +1 -0
  84. nabu/resources/dataset_analyzer.py +0 -1
  85. nabu/resources/templates/bm05_pag.conf +34 -0
  86. nabu/resources/templates/id16_ctf.conf +2 -1
  87. nabu/resources/tests/test_nxflatfield.py +0 -1
  88. nabu/resources/tests/test_units.py +0 -1
  89. nabu/stitching/frame_composition.py +7 -1
  90. {nabu-2022.3.0a1.dist-info → nabu-2023.1.0a2.dist-info}/METADATA +2 -7
  91. {nabu-2022.3.0a1.dist-info → nabu-2023.1.0a2.dist-info}/RECORD +96 -75
  92. {nabu-2022.3.0a1.dist-info → nabu-2023.1.0a2.dist-info}/WHEEL +1 -1
  93. {nabu-2022.3.0a1.dist-info → nabu-2023.1.0a2.dist-info}/entry_points.txt +2 -1
  94. {nabu-2022.3.0a1.dist-info → nabu-2023.1.0a2.dist-info}/LICENSE +0 -0
  95. {nabu-2022.3.0a1.dist-info → nabu-2023.1.0a2.dist-info}/top_level.txt +0 -0
  96. {nabu-2022.3.0a1.dist-info → nabu-2023.1.0a2.dist-info}/zip-safe +0 -0
@@ -0,0 +1,921 @@
1
+ from os import path
2
+ from time import time
3
+ import numpy as np
4
+ from silx.io.url import DataUrl
5
+ from ...resources.logger import LoggerOrPrint
6
+ from ...resources.utils import is_hdf5_extension, extract_parameters
7
+ from ...io.reader import ChunkReader, HDF5Loader, get_hdf5_dataset_shape
8
+ from ...preproc.ccd import Log, CCDFilter
9
+ from ...preproc.flatfield import FlatFieldDataUrls
10
+ from ...preproc.distortion import DistortionCorrection
11
+ from ...preproc.shift import VerticalShift
12
+ from ...preproc.double_flatfield import DoubleFlatField
13
+ from ...preproc.phase import PaganinPhaseRetrieval
14
+ from ...reconstruction.sinogram import SinoBuilder, SinoNormalization
15
+ from ...misc.rotation import Rotation
16
+ from ...preproc.rings import MunchDeringer
17
+ from ...misc.unsharp import UnsharpMask
18
+ from ...misc.histogram import PartialHistogram, hist_as_2Darray
19
+ from ..utils import use_options, pipeline_step, WriterConfigurator
20
+ # For now we don't have a plain python/numpy backend for reconstruction
21
+ try:
22
+ from ...reconstruction.fbp_opencl import Backprojector
23
+ except:
24
+ Backprojector = None
25
+
26
+
27
+ class ChunkedPipeline:
28
+ """
29
+ Pipeline for "regular" full-field tomography.
30
+ Data is processed by chunks. A chunk consists in K contiguous lines of all the radios.
31
+ In parallel geometry, a chunk of K radios lines gives K sinograms,
32
+ and equivalently K reconstructed slices.
33
+ """
34
+
35
+ backend = "numpy"
36
+ FlatFieldClass = FlatFieldDataUrls
37
+ DoubleFlatFieldClass = DoubleFlatField
38
+ CCDCorrectionClass = CCDFilter
39
+ PaganinPhaseRetrievalClass = PaganinPhaseRetrieval
40
+ UnsharpMaskClass = UnsharpMask
41
+ ImageRotationClass = Rotation
42
+ VerticalShiftClass = VerticalShift
43
+ SinoBuilderClass = SinoBuilder
44
+ SinoDeringerClass = MunchDeringer
45
+ MLogClass = Log
46
+ SinoNormalizationClass = SinoNormalization
47
+ FBPClass = Backprojector
48
+ HistogramClass = PartialHistogram
49
+
50
+ def __init__(self,
51
+ process_config,
52
+ sub_region,
53
+ logger=None,
54
+ extra_options=None,
55
+ phase_margin=None):
56
+ """
57
+ Initialize a "Chunked" pipeline.
58
+
59
+ Parameters
60
+ ----------
61
+ processing_config: `ProcessConfig`
62
+ Process configuration.
63
+ sub_region: tuple
64
+ Sub-region to process in the volume for this worker, in the format
65
+ `(start_x, end_x, start_z, end_z)`.
66
+ logger: `nabu.app.logger.Logger`, optional
67
+ Logger class
68
+ extra_options: dict, optional
69
+ Advanced extra options.
70
+ phase_margin: tuple, optional
71
+ Margin to use when performing phase retrieval, in the form ((up, down), (left, right)).
72
+ See also the documentation of PaganinPhaseRetrieval.
73
+ If not provided, no margin is applied.
74
+
75
+
76
+ Notes
77
+ ------
78
+ Using a `phase_margin` results in a lesser number of reconstructed slices.
79
+ More specifically, if `phase_margin = (V, H)`, then there will be `delta_z - 2*V`
80
+ reconstructed slices (if the sub-region is in the middle of the volume)
81
+ or `delta_z - V` reconstructed slices (if the sub-region is on top or bottom
82
+ of the volume).
83
+ """
84
+ self.logger = LoggerOrPrint(logger)
85
+ self._set_params(process_config, sub_region, extra_options,
86
+ phase_margin)
87
+ self.set_subregion(sub_region)
88
+ self._init_pipeline()
89
+
90
+ def _set_params(self, process_config, sub_region, extra_options,
91
+ phase_margin):
92
+ self.process_config = process_config
93
+ self.dataset_info = self.process_config.dataset_info
94
+ self.dataset_infos = self.process_config.dataset_info # shorthand - deprecated
95
+ self.processing_steps = self.process_config.processing_steps.copy()
96
+ self.processing_options = self.process_config.processing_options
97
+ self.sub_region = self._check_subregion(sub_region)
98
+ self.delta_z = sub_region[-1] - sub_region[-2]
99
+ self.chunk_size = self.delta_z
100
+ self._set_phase_margin(phase_margin)
101
+ self._set_extra_options(extra_options)
102
+ self._callbacks = {}
103
+ self._steps_name2component = {}
104
+ self._steps_component2name = {}
105
+ self._data_dump = {}
106
+ self._resume_from_step = None
107
+
108
+ @staticmethod
109
+ def _check_subregion(sub_region):
110
+ if len(sub_region) < 4:
111
+ assert len(sub_region) == 2
112
+ sub_region = (None, None) + sub_region
113
+ if None in sub_region[-2:]:
114
+ raise ValueError("Cannot set z_min or z_max to None")
115
+ return sub_region
116
+
117
+ def _set_extra_options(self, extra_options):
118
+ if extra_options is None:
119
+ extra_options = {}
120
+ advanced_options = {}
121
+ advanced_options.update(extra_options)
122
+ self.extra_options = advanced_options
123
+
124
+ def _set_phase_margin(self, phase_margin):
125
+ if phase_margin is None:
126
+ phase_margin = ((0, 0), (0, 0))
127
+ self._phase_margin_up = phase_margin[0][0]
128
+ self._phase_margin_down = phase_margin[0][1]
129
+ self._phase_margin_left = phase_margin[1][0]
130
+ self._phase_margin_right = phase_margin[1][1]
131
+
132
+ def set_subregion(self, sub_region):
133
+ """
134
+ Set a sub-region to process.
135
+
136
+ Parameters
137
+ ----------
138
+ sub_region: tuple
139
+ Sub-region to process in the volume, in the format
140
+ `(start_x, end_x, start_z, end_z)` or `(start_z, end_z)`.
141
+ """
142
+ sub_region = self._check_subregion(sub_region)
143
+ dz = sub_region[-1] - sub_region[-2]
144
+ if dz != self.delta_z:
145
+ raise ValueError(
146
+ "Class was initialized for delta_z = %d but provided sub_region has delta_z = %d"
147
+ % (self.delta_z, dz))
148
+ self.sub_region = sub_region
149
+ self.z_min = sub_region[-2]
150
+ self.z_max = sub_region[-1]
151
+
152
+ def _compute_phase_kernel_margin(self):
153
+ """
154
+ Get the "margin" to pass to classes like PaganinPhaseRetrieval.
155
+ In order to have a good accuracy for filter-based phase retrieval methods,
156
+ we need to load extra data around the edges of each image. Otherwise,
157
+ a default padding type is applied.
158
+ """
159
+ if not (self.use_radio_processing_margin):
160
+ self._phase_margin = None
161
+ return
162
+ up_margin = self._phase_margin_up
163
+ down_margin = self._phase_margin_down
164
+ # Horizontal margin is not implemented
165
+ left_margin, right_margin = (0, 0)
166
+ self._phase_margin = ((up_margin, down_margin), (left_margin,
167
+ right_margin))
168
+
169
+ @property
170
+ def use_radio_processing_margin(self):
171
+ return ("phase" in self.processing_steps) or ("unsharp_mask"
172
+ in self.processing_steps)
173
+
174
+ def _get_phase_margin(self):
175
+ if not (self.use_radio_processing_margin):
176
+ return ((0, 0), (0, 0))
177
+ return self._phase_margin
178
+
179
+ def _get_cropped_radios(self):
180
+ ((up_margin, down_margin), (left_margin,
181
+ right_margin)) = self._phase_margin
182
+ zslice = slice(up_margin or None, -down_margin or None)
183
+ xslice = slice(left_margin or None, -right_margin or None)
184
+ self._radios_cropped = self.radios[:, zslice, xslice]
185
+ return self._radios_cropped
186
+
187
+ @property
188
+ def phase_margin(self):
189
+ """
190
+ Return the margin for phase retrieval in the form ((up, down), (left, right))
191
+ """
192
+ return self._get_phase_margin()
193
+
194
+ @property
195
+ def n_recs(self):
196
+ """
197
+ Return the final number of reconstructed slices.
198
+ """
199
+ n_recs = self.delta_z
200
+ n_recs -= sum(self._get_phase_margin()[0])
201
+ return n_recs
202
+
203
+ def _get_process_name(self, kind="reconstruction"):
204
+ # In the future, might be something like "reconstruction-<ID>"
205
+ if kind == "reconstruction":
206
+ return "reconstruction"
207
+ elif kind == "histogram":
208
+ return "histogram"
209
+ return kind
210
+
211
+ def _configure_dump(self, step_name):
212
+ if step_name not in self.processing_steps:
213
+ if step_name == "sinogram" and self.process_config._dump_sinogram:
214
+ fname_full = self.process_config._dump_sinogram_file
215
+ else:
216
+ return
217
+ else:
218
+ if not self.processing_options[step_name].get("save", False):
219
+ return
220
+ fname_full = self.processing_options[step_name]["save_steps_file"]
221
+
222
+ fname, ext = path.splitext(fname_full)
223
+ dirname, file_prefix = path.split(fname)
224
+ output_dir = path.join(dirname, file_prefix)
225
+ file_prefix += str("_%04d" % self._get_image_start_index())
226
+
227
+ self._data_dump[step_name] = WriterConfigurator(
228
+ output_dir,
229
+ file_prefix,
230
+ file_format="hdf5",
231
+ overwrite=True,
232
+ logger=self.logger,
233
+ nx_info={
234
+ "process_name":
235
+ step_name,
236
+ "processing_index":
237
+ 0, # TODO
238
+ "config": {
239
+ "processing_options": self.processing_options,
240
+ "nabu_config": self.process_config.nabu_config
241
+ },
242
+ "entry":
243
+ getattr(self.dataset_info.dataset_scanner, "entry", None),
244
+ })
245
+
246
+ def _configure_data_dumps(self):
247
+ for step_name in self.processing_steps:
248
+ self._configure_dump(step_name)
249
+ # sinogram is a special keyword: not in processing_steps, but guaranteed to be before sinogram generation
250
+ if self.process_config._dump_sinogram:
251
+ self._configure_dump("sinogram")
252
+
253
+ #
254
+ # Callbacks
255
+ #
256
+
257
+ def register_callback(self, step_name, callback):
258
+ """
259
+ Register a callback for a pipeline processing step.
260
+
261
+ Parameters
262
+ ----------
263
+ step_name: str
264
+ processing step name
265
+ callback: callable
266
+ A function. It will be executed once the processing step `step_name`
267
+ is finished. The function takes only one argument: the class instance.
268
+ """
269
+ if step_name not in self.processing_steps:
270
+ raise ValueError("'%s' is not in processing steps %s" %
271
+ (step_name, self.processing_steps))
272
+ if step_name in self._callbacks:
273
+ self._callbacks[step_name].append(callback)
274
+ else:
275
+ self._callbacks[step_name] = [callback]
276
+
277
+ def _reshape_radios_after_phase(self):
278
+ """
279
+ Callback executed after phase retrieval, if margin != (0, 0).
280
+ It modifies self.radios so that further processing will be done
281
+ on the "inner part".
282
+ """
283
+ if sum(self._get_phase_margin()[0]) <= 0:
284
+ return
285
+ self._orig_radios = self.radios
286
+ self.logger.debug(
287
+ "Reshaping radios from %s to %s" %
288
+ (str(self.radios.shape), str(self._radios_cropped.shape)))
289
+ self.radios = self._radios_cropped
290
+
291
+ #
292
+ # Overwritten in inheriting classes
293
+ #
294
+
295
+ def _get_shape(self, step_name):
296
+ """
297
+ Get the shape to provide to the class corresponding to step_name.
298
+ """
299
+ if step_name == "flatfield":
300
+ shape = self.radios.shape
301
+ elif step_name == "double_flatfield":
302
+ shape = self.radios.shape
303
+ elif step_name == "rotate_projections":
304
+ shape = self.radios.shape[1:]
305
+ elif step_name == "phase":
306
+ shape = self.radios.shape[1:]
307
+ elif step_name == "ccd_correction":
308
+ shape = self.radios.shape[1:]
309
+ elif step_name == "unsharp_mask":
310
+ shape = self.radios.shape[1:]
311
+ elif step_name == "take_log":
312
+ shape = self._radios_cropped_shape
313
+ elif step_name == "radios_movements":
314
+ shape = self._radios_cropped_shape
315
+ elif step_name == "sino_normalization":
316
+ shape = self._radios_cropped_shape
317
+ elif step_name == "build_sino":
318
+ shape = self._radios_cropped_shape
319
+ elif step_name == "sino_rings_correction":
320
+ shape = self.sino_builder.output_shape
321
+ elif step_name == "reconstruction":
322
+ shape = self.sino_builder.output_shape[1:]
323
+ else:
324
+ raise ValueError("Unknown processing step %s" % step_name)
325
+ self.logger.debug("Data shape for %s is %s" % (step_name, str(shape)))
326
+ return shape
327
+
328
+ def _get_phase_output_shape(self):
329
+ if not (self.use_radio_processing_margin):
330
+ self._radios_cropped_shape = self.radios.shape
331
+ return
332
+ ((up_margin, down_margin), (left_margin,
333
+ right_margin)) = self._phase_margin
334
+ self._radios_cropped_shape = (self.radios.shape[0],
335
+ self.radios.shape[1] -
336
+ (up_margin + down_margin),
337
+ self.radios.shape[2] -
338
+ (left_margin + right_margin))
339
+
340
+ def _allocate_array(self, shape, dtype, name=None):
341
+ return np.zeros(shape, dtype=dtype)
342
+
343
+ def _allocate_sinobuilder_output(self):
344
+ return self._allocate_array(self.sino_builder.output_shape,
345
+ "f",
346
+ name="sinos")
347
+
348
+ def _allocate_recs(self, ny, nx):
349
+ self.n_slices = self.radios.shape[1] # TODO modify with vertical shifts
350
+ if self.use_radio_processing_margin:
351
+ self.n_slices -= sum(self.phase_margin[0])
352
+ self.recs = self._allocate_array((self.n_slices, ny, nx),
353
+ "f",
354
+ name="recs")
355
+
356
+ def _reset_memory(self):
357
+ pass
358
+
359
+ def _get_read_dump_subregion(self):
360
+ read_opts = self.processing_options["read_chunk"]
361
+ if read_opts.get("process_file", None) is None:
362
+ return None
363
+ dump_start_z, dump_end_z = read_opts["dump_start_z"], read_opts[
364
+ "dump_end_z"]
365
+ relative_start_z = self.z_min - dump_start_z
366
+ relative_end_z = relative_start_z + self.delta_z
367
+ # (n_angles, n_z, n_x)
368
+ subregion = (None, None, relative_start_z, relative_end_z, None, None)
369
+ return subregion
370
+
371
+ def _check_resume_from_step(self):
372
+ if self._resume_from_step is None:
373
+ return
374
+ read_opts = self.processing_options["read_chunk"]
375
+ expected_radios_shape = get_hdf5_dataset_shape(
376
+ read_opts["process_file"],
377
+ read_opts["process_h5_path"],
378
+ sub_region=self._get_read_dump_subregion(),
379
+ )
380
+ # TODO check
381
+
382
+ def _init_reader_finalize(self):
383
+ """
384
+ Method called after _init_reader
385
+ """
386
+ self._check_resume_from_step()
387
+ self.radios = self.chunk_reader.data
388
+ self._compute_phase_kernel_margin()
389
+ self._get_phase_output_shape()
390
+
391
+ def _process_finalize(self):
392
+ """
393
+ Method called once the pipeline has been executed
394
+ """
395
+ if sum(self._get_phase_margin()[0]) > 0:
396
+ self.radios = self._orig_radios
397
+
398
+ def _get_slice_start_index(self):
399
+ return self.z_min + self._phase_margin_up
400
+
401
+ _get_image_start_index = _get_slice_start_index
402
+
403
+ #
404
+ # Pipeline initialization
405
+ #
406
+
407
+ def _init_pipeline(self):
408
+ self._init_reader()
409
+ self._init_flatfield()
410
+ self._init_double_flatfield()
411
+ self._init_ccd_corrections()
412
+ self._init_radios_rotation()
413
+ self._init_phase()
414
+ self._init_unsharp()
415
+ self._init_radios_movements()
416
+ self._init_mlog()
417
+ self._init_sino_normalization()
418
+ self._init_sino_builder()
419
+ self._init_sino_rings_correction()
420
+ self._prepare_reconstruction()
421
+ self._init_reconstruction()
422
+ self._init_histogram()
423
+ self._init_writer()
424
+ self._configure_data_dumps()
425
+
426
+ @use_options("read_chunk", "chunk_reader")
427
+ def _init_reader(self):
428
+ if "read_chunk" not in self.processing_steps:
429
+ raise ValueError("Cannot proceed without reading data")
430
+ options = self.processing_options["read_chunk"]
431
+ process_file = options.get("process_file", None)
432
+ if process_file is None:
433
+ # Standard case - start pipeline from raw data
434
+ # ChunkReader always take a non-subsampled dictionary "files"
435
+ self.chunk_reader = ChunkReader(
436
+ options["files"],
437
+ sub_region=self.sub_region,
438
+ convert_float=True,
439
+ binning=options["binning"],
440
+ dataset_subsampling=options["dataset_subsampling"])
441
+ else:
442
+ # Resume pipeline from dumped intermediate step
443
+ self.chunk_reader = HDF5Loader(
444
+ process_file,
445
+ options["process_h5_path"],
446
+ sub_region=self._get_read_dump_subregion())
447
+ self._resume_from_step = options["step_name"]
448
+ self.logger.debug(
449
+ "Load subregion %s from file %s" %
450
+ (str(self.chunk_reader.sub_region), self.chunk_reader.fname))
451
+ self._init_reader_finalize()
452
+
453
+ @use_options("flatfield", "flatfield")
454
+ def _init_flatfield(self, shape=None):
455
+ if shape is None:
456
+ shape = self._get_shape("flatfield")
457
+ options = self.processing_options["flatfield"]
458
+
459
+ distortion_correction = None
460
+ if options["do_flat_distortion"]:
461
+ self.logger.info("Flats distortion correction will be applied")
462
+ estimation_kwargs = {}
463
+ estimation_kwargs.update(options["flat_distortion_params"])
464
+ estimation_kwargs["logger"] = self.logger
465
+ distortion_correction = DistortionCorrection(
466
+ estimation_method="fft-correlation",
467
+ estimation_kwargs=estimation_kwargs,
468
+ correction_method="interpn")
469
+
470
+ # FlatField parameter "radios_indices" must account for subsampling
471
+ self.flatfield = self.FlatFieldClass(
472
+ shape,
473
+ flats=self.dataset_info.flats,
474
+ darks=self.dataset_info.darks,
475
+ radios_indices=options["projs_indices"],
476
+ interpolation="linear",
477
+ distortion_correction=distortion_correction,
478
+ sub_region=self.sub_region,
479
+ binning=options["binning"],
480
+ convert_float=True)
481
+
482
+ @use_options("double_flatfield", "double_flatfield")
483
+ def _init_double_flatfield(self):
484
+ options = self.processing_options["double_flatfield"]
485
+ avg_is_on_log = (options["sigma"] is not None)
486
+ result_url = None
487
+ if options["processes_file"] not in (None, ""):
488
+ result_url = DataUrl(
489
+ file_path=options["processes_file"],
490
+ data_path=(self.dataset_info.hdf5_entry or "entry") +
491
+ "/double_flatfield/results/data",
492
+ )
493
+ self.logger.info("Loading double flatfield from %s" %
494
+ result_url.file_path())
495
+ self.double_flatfield = self.DoubleFlatFieldClass(
496
+ self._get_shape("double_flatfield"),
497
+ result_url=result_url,
498
+ sub_region=self.sub_region,
499
+ input_is_mlog=False,
500
+ output_is_mlog=False,
501
+ average_is_on_log=avg_is_on_log,
502
+ sigma_filter=options["sigma"])
503
+
504
+ @use_options("ccd_correction", "ccd_correction")
505
+ def _init_ccd_corrections(self):
506
+ options = self.processing_options["ccd_correction"]
507
+ self.ccd_correction = self.CCDCorrectionClass(
508
+ self._get_shape("ccd_correction"),
509
+ median_clip_thresh=options["median_clip_thresh"])
510
+
511
+ @use_options("phase", "phase_retrieval")
512
+ def _init_phase(self):
513
+ options = self.processing_options["phase"]
514
+ # If unsharp mask follows phase retrieval, then it should be done
515
+ # before cropping to the "inner part".
516
+ # Otherwise, crop the data just after phase retrieval.
517
+ if "unsharp_mask" in self.processing_steps:
518
+ margin = None
519
+ else:
520
+ margin = self._phase_margin
521
+ self.phase_retrieval = self.PaganinPhaseRetrievalClass(
522
+ self._get_shape("phase"),
523
+ distance=options["distance_m"],
524
+ energy=options["energy_kev"],
525
+ delta_beta=options["delta_beta"],
526
+ pixel_size=options["pixel_size_m"],
527
+ padding=options["padding_type"],
528
+ margin=margin,
529
+ fftw_num_threads=
530
+ 0, # TODO tune in advanced params of nabu config file
531
+ )
532
+ if self.phase_retrieval.use_fftw:
533
+ self.logger.debug(
534
+ "PaganinPhaseRetrieval using FFTW with %d threads" %
535
+ self.phase_retrieval.fftw.num_threads)
536
+ if "unsharp_mask" not in self.processing_steps:
537
+ self.register_callback("phase",
538
+ ChunkedPipeline._reshape_radios_after_phase)
539
+
540
+ @use_options("unsharp_mask", "unsharp_mask")
541
+ def _init_unsharp(self):
542
+ options = self.processing_options["unsharp_mask"]
543
+ self.unsharp_mask = self.UnsharpMaskClass(
544
+ self._get_shape("unsharp_mask"),
545
+ options["unsharp_sigma"],
546
+ options["unsharp_coeff"],
547
+ mode="reflect",
548
+ method=options["unsharp_method"])
549
+ self.register_callback("unsharp_mask",
550
+ ChunkedPipeline._reshape_radios_after_phase)
551
+
552
+ @use_options("take_log", "mlog")
553
+ def _init_mlog(self):
554
+ options = self.processing_options["take_log"]
555
+ self.mlog = self.MLogClass(self._get_shape("take_log"),
556
+ clip_min=options["log_min_clip"],
557
+ clip_max=options["log_max_clip"])
558
+
559
+ @use_options("rotate_projections", "projs_rot")
560
+ def _init_radios_rotation(self):
561
+ options = self.processing_options["rotate_projections"]
562
+ center = options["center"]
563
+ if center is None:
564
+ nx, ny = self.dataset_info.radio_dims
565
+ center = (nx / 2 - 0.5, ny / 2 - 0.5)
566
+ center = (center[0], center[1] - self.z_min)
567
+ self.projs_rot = self.ImageRotationClass(
568
+ self._get_shape("rotate_projections"),
569
+ options["angle"],
570
+ center=center,
571
+ mode="edge",
572
+ reshape=False)
573
+ self._tmp_rotated_radio = self._allocate_array(
574
+ self._get_shape("rotate_projections"),
575
+ "f",
576
+ name="tmp_rotated_radio")
577
+
578
+ @use_options("radios_movements", "radios_movements")
579
+ def _init_radios_movements(self):
580
+ options = self.processing_options["radios_movements"]
581
+ self._vertical_shifts = options["translation_movements"][:, 1]
582
+ self.radios_movements = self.VerticalShiftClass(
583
+ self._get_shape("radios_movements"), self._vertical_shifts)
584
+
585
+ @use_options("sino_normalization", "sino_normalization")
586
+ def _init_sino_normalization(self):
587
+ options = self.processing_options["sino_normalization"]
588
+ self.sino_normalization = self.SinoNormalizationClass(
589
+ kind=options["method"],
590
+ radios_shape=self._get_shape("sino_normalization"),
591
+ normalization_array=options["normalization_array"])
592
+
593
+ @use_options("build_sino", "sino_builder")
594
+ def _init_sino_builder(self):
595
+ options = self.processing_options["build_sino"]
596
+ self.sino_builder = self.SinoBuilderClass(
597
+ radios_shape=self._get_shape("build_sino"),
598
+ rot_center=options["rotation_axis_position"],
599
+ halftomo=options["enable_halftomo"],
600
+ )
601
+ if not (options["enable_halftomo"]):
602
+ self._sinobuilder_copy = False
603
+ self._sinobuilder_output = None
604
+ self.sinos = None
605
+ else:
606
+ self._sinobuilder_copy = True
607
+ self.sinos = self._allocate_sinobuilder_output()
608
+ self._sinobuilder_output = self.sinos
609
+
610
+ @use_options("sino_rings_correction", "sino_deringer")
611
+ def _init_sino_rings_correction(self):
612
+ options = self.processing_options["sino_rings_correction"]
613
+ fw_params = extract_parameters(options["user_options"])
614
+ fw_sigma = fw_params.pop("sigma", 1.)
615
+ self.sino_deringer = self.SinoDeringerClass(
616
+ fw_sigma,
617
+ sinos_shape=self._get_shape("sino_rings_correction"),
618
+ **fw_params)
619
+
620
+ # this should be renamed, as it could be confused with _init_reconstruction. What about _get_reconstruction_array ?
621
+ @use_options("reconstruction", "reconstruction")
622
+ def _prepare_reconstruction(self):
623
+ options = self.processing_options["reconstruction"]
624
+ x_s, x_e = options["start_x"], options["end_x"]
625
+ y_s, y_e = options["start_y"], options["end_y"]
626
+ self._rec_roi = (x_s, x_e + 1, y_s, y_e + 1)
627
+ self._allocate_recs(y_e - y_s + 1, x_e - x_s + 1)
628
+
629
+ @use_options("reconstruction", "reconstruction")
630
+ def _init_reconstruction(self):
631
+ options = self.processing_options["reconstruction"]
632
+ if self.sino_builder is None:
633
+ raise ValueError("Reconstruction cannot be done without build_sino")
634
+ if self.FBPClass is None:
635
+ raise ValueError("No usable FBP module was found")
636
+
637
+ if options["enable_halftomo"]:
638
+ rot_center = options["rotation_axis_position_halftomo"]
639
+ else:
640
+ rot_center = options["rotation_axis_position"]
641
+ if self.sino_builder._halftomo_flip:
642
+ rot_center = self.sino_builder.rot_center
643
+
644
+ self.reconstruction = self.FBPClass(
645
+ self._get_shape("reconstruction"),
646
+ angles=options["angles"],
647
+ rot_center=rot_center,
648
+ filter_name=options["fbp_filter_type"],
649
+ slice_roi=self._rec_roi,
650
+ padding_mode=options["padding_type"],
651
+ extra_options={
652
+ "scale_factor": 1. / options["pixel_size_cm"],
653
+ "axis_correction": options["axis_correction"],
654
+ "centered_axis": options["centered_axis"],
655
+ "clip_outer_circle": options["clip_outer_circle"],
656
+ "filter_cutoff": options["fbp_filter_cutoff"],
657
+ })
658
+ if options["fbp_filter_type"] is None:
659
+ self.reconstruction.fbp = self.reconstruction.backproj
660
+
661
+ @use_options("histogram", "histogram")
662
+ def _init_histogram(self):
663
+ options = self.processing_options["histogram"]
664
+ self.histogram = self.HistogramClass(method="fixed_bins_number",
665
+ num_bins=options["histogram_bins"])
666
+
667
+ @use_options("save", "writer")
668
+ def _init_writer(self):
669
+ # TODO: henri: have a look to simplify
670
+ options = self.processing_options["save"]
671
+ file_prefix = options["file_prefix"]
672
+ output_dir = path.join(options["location"], file_prefix)
673
+ nx_info = None
674
+ self._hdf5_output = is_hdf5_extension(options["file_format"],
675
+ errors=False)
676
+ if self._hdf5_output:
677
+ fname_start_index = None
678
+ file_prefix += str("_%04d" % self._get_slice_start_index())
679
+ entry = getattr(self.dataset_info.dataset_scanner, "entry", None)
680
+ nx_info = {
681
+ "process_name": self._get_process_name(),
682
+ "processing_index": 0,
683
+ "config": {
684
+ # "processing_options": self.processing_options, # Takes too much time to write, not useful for partial files
685
+ "nabu_config": self.process_config.nabu_config,
686
+ },
687
+ "entry": entry,
688
+ }
689
+ self._histogram_processing_index = nx_info["processing_index"] + 1
690
+ else:
691
+ fname_start_index = self._get_slice_start_index()
692
+ self._histogram_processing_index = 1
693
+ writer_options = {}
694
+ if options["tiff_single_file"]:
695
+ writer_options = {
696
+ "tiff_single_file":
697
+ options["tiff_single_file"],
698
+ "single_tiff_initialized":
699
+ getattr(self.process_config, "single_tiff_initialized",
700
+ False),
701
+ }
702
+ self.process_config.single_tiff_initialized = True
703
+ self._writer_configurator = WriterConfigurator(
704
+ output_dir,
705
+ file_prefix,
706
+ file_format=options["file_format"],
707
+ overwrite=options["overwrite"],
708
+ start_index=fname_start_index,
709
+ logger=self.logger,
710
+ nx_info=nx_info,
711
+ write_histogram=("histogram" in self.processing_steps),
712
+ histogram_entry=getattr(self.dataset_info.dataset_scanner, "entry",
713
+ "entry"),
714
+ writer_options=writer_options,
715
+ extra_options={
716
+ "jpeg2000_compression_ratio":
717
+ options["jpeg2000_compression_ratio"],
718
+ "float_clip_values":
719
+ options["float_clip_values"],
720
+ })
721
+ self.writer = self._writer_configurator.writer
722
+ self._writer_exec_args = self._writer_configurator._writer_exec_args
723
+ self._writer_exec_kwargs = self._writer_configurator._writer_exec_kwargs
724
+ self.histogram_writer = self._writer_configurator.get_histogram_writer()
725
+
726
+ #
727
+ # Pipeline re-initialization
728
+ #
729
+
730
+ def _reset_sub_region(self, sub_region):
731
+ self.set_subregion(sub_region)
732
+ self._reset_reader_subregion()
733
+ self._reset_flatfield()
734
+
735
+ def _reset_flatfield(self):
736
+ self._init_flatfield()
737
+
738
+ #
739
+ # Pipeline execution
740
+ #
741
+
742
+ @pipeline_step("chunk_reader", "Reading data")
743
+ def _read_data(self):
744
+ self.logger.debug("Region = %s" % str(self.sub_region))
745
+ t0 = time()
746
+ self.chunk_reader.load_data()
747
+ el = time() - t0
748
+
749
+ shp = self.chunk_reader.data.shape
750
+ itemsize = self.chunk_reader.dtype.itemsize if hasattr(
751
+ self.chunk_reader, "dtype") else 4
752
+ GB = np.prod(shp) * itemsize / 1e9
753
+ self.logger.info("Read subvolume %s in %.2f s" % (str(shp), el))
754
+
755
+ def _reset_reader_subregion(self):
756
+ if self._resume_from_step is None:
757
+ self.chunk_reader._set_subregion(self.sub_region)
758
+ self.chunk_reader._init_reader()
759
+ self.chunk_reader._loaded = False
760
+ else:
761
+ self.chunk_reader._set_subregion(self._get_read_dump_subregion())
762
+ self.chunk_reader._loaded = False
763
+
764
+ @pipeline_step("flatfield", "Applying flat-field")
765
+ def _flatfield(self):
766
+ self.flatfield.normalize_radios(self.radios)
767
+
768
+ @pipeline_step("double_flatfield", "Applying double flat-field")
769
+ def _double_flatfield(self, radios=None):
770
+ if radios is None:
771
+ radios = self.radios
772
+ self.double_flatfield.apply_double_flatfield(radios)
773
+
774
+ @pipeline_step("ccd_correction", "Applying CCD corrections")
775
+ def _ccd_corrections(self, radios=None):
776
+ if radios is None:
777
+ radios = self.radios
778
+ _tmp_radio = self._allocate_array(radios.shape[1:],
779
+ "f",
780
+ name="tmp_ccdcorr_radio")
781
+ for i in range(radios.shape[0]):
782
+ self.ccd_correction.median_clip_correction(radios[i],
783
+ output=_tmp_radio)
784
+ radios[i][:] = _tmp_radio[:]
785
+
786
+ @pipeline_step("projs_rot", "Rotating projections")
787
+ def _rotate_projections(self, radios=None):
788
+ if radios is None:
789
+ radios = self.radios
790
+ tmp_radio = self._tmp_rotated_radio
791
+ for i in range(radios.shape[0]):
792
+ self.projs_rot.rotate(radios[i], output=tmp_radio)
793
+ radios[i][:] = tmp_radio[:]
794
+
795
+ @pipeline_step("phase_retrieval", "Performing phase retrieval")
796
+ def _retrieve_phase(self):
797
+ if "unsharp_mask" in self.processing_steps:
798
+ output = self.radios
799
+ else:
800
+ self._get_cropped_radios()
801
+ output = self._radios_cropped
802
+ for i in range(self.radios.shape[0]):
803
+ self.phase_retrieval.apply_filter(self.radios[i], output=output[i])
804
+
805
+ @pipeline_step("unsharp_mask", "Performing unsharp mask")
806
+ def _apply_unsharp(self):
807
+ for i in range(self.radios.shape[0]):
808
+ self.radios[i] = self.unsharp_mask.unsharp(self.radios[i])
809
+ self._get_cropped_radios()
810
+
811
+ @pipeline_step("mlog", "Taking logarithm")
812
+ def _take_log(self):
813
+ self.mlog.take_logarithm(self.radios)
814
+
815
+ @pipeline_step("radios_movements", "Applying radios movements")
816
+ def _radios_movements(self, radios=None):
817
+ if radios is None:
818
+ radios = self.radios
819
+ self.radios_movements.apply_vertical_shifts(
820
+ radios, list(range(radios.shape[0])))
821
+
822
+ @pipeline_step("sino_normalization", "Normalizing sinograms")
823
+ def _normalize_sinos(self, radios=None):
824
+ if radios is None:
825
+ radios = self.radios
826
+ sinos = radios.transpose((1, 0, 2))
827
+ self.sino_normalization.normalize(sinos)
828
+
829
+ def _dump_sinogram(self, radios=None):
830
+ if radios is None:
831
+ radios = self.radios
832
+ self._dump_data_to_file("sinogram", data=radios)
833
+
834
+ @pipeline_step("sino_builder", "Building sinograms")
835
+ def _build_sino(self, radios=None):
836
+ if radios is None:
837
+ radios = self.radios
838
+ # Either a new array (previously allocated in "_sinobuilder_output"),
839
+ # or a view of "radios"
840
+ self.sinos = self.sino_builder.radios_to_sinos(
841
+ radios,
842
+ output=self._sinobuilder_output,
843
+ copy=self._sinobuilder_copy)
844
+
845
+ @pipeline_step("sino_deringer", "Removing rings on sinograms")
846
+ def _destripe_sinos(self, sinos=None):
847
+ if sinos is None:
848
+ sinos = self.sinos
849
+ self.sino_deringer.remove_rings(sinos)
850
+
851
+ @pipeline_step("reconstruction", "Reconstruction")
852
+ def _reconstruct(self, sinos=None):
853
+ if sinos is None:
854
+ sinos = self.sinos
855
+ for i in range(sinos.shape[0]):
856
+ self.reconstruction.fbp(sinos[i], output=self.recs[i])
857
+
858
+ @pipeline_step("histogram", "Computing histogram")
859
+ def _compute_histogram(self, data=None):
860
+ if data is None:
861
+ data = self.recs
862
+ self.recs_histogram = self.histogram.compute_histogram(data)
863
+
864
+ @pipeline_step("writer", "Saving data")
865
+ def _write_data(self, data=None):
866
+ if data is None:
867
+ data = self.recs
868
+ self.writer.write(data, *self._writer_exec_args,
869
+ **self._writer_exec_kwargs)
870
+ self.logger.info("Wrote %s" % self.writer.get_filename())
871
+ self._write_histogram()
872
+
873
+ def _write_histogram(self):
874
+ if "histogram" not in self.processing_steps:
875
+ return
876
+ self.logger.info("Saving histogram")
877
+ self.histogram_writer.write(
878
+ hist_as_2Darray(self.recs_histogram),
879
+ self._get_process_name(kind="histogram"),
880
+ processing_index=self._histogram_processing_index,
881
+ config={
882
+ "file": path.basename(self.writer.get_filename()),
883
+ "bins": self.processing_options["histogram"]["histogram_bins"],
884
+ })
885
+
886
+ def _dump_data_to_file(self, step_name, data=None):
887
+ if step_name not in self._data_dump:
888
+ return
889
+ if data is None:
890
+ data = self.radios
891
+ writer = self._data_dump[step_name]
892
+ self.logger.info("Dumping data to %s" % writer.fname)
893
+ writer.write_data(data)
894
+
895
+ def _process_chunk(self):
896
+ self._flatfield()
897
+ self._double_flatfield()
898
+ self._ccd_corrections()
899
+ self._rotate_projections()
900
+ self._retrieve_phase()
901
+ self._apply_unsharp()
902
+ self._take_log()
903
+ self._radios_movements()
904
+ self._normalize_sinos()
905
+ self._dump_sinogram()
906
+ self._build_sino()
907
+ self._destripe_sinos()
908
+ self._reconstruct()
909
+ self._compute_histogram()
910
+ self._write_data()
911
+ self._process_finalize()
912
+
913
+ def process_chunk(self, sub_region=None):
914
+ if sub_region is not None:
915
+ self._reset_sub_region(sub_region)
916
+ self._reset_memory()
917
+ self._init_writer()
918
+ self._init_double_flatfield()
919
+ self._configure_data_dumps()
920
+ self._read_data()
921
+ self._process_chunk()