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