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.
- nabu/__init__.py +1 -1
- nabu/app/bootstrap.py +2 -3
- nabu/app/cast_volume.py +4 -2
- nabu/app/cli_configs.py +5 -0
- nabu/app/composite_cor.py +1 -1
- nabu/app/create_distortion_map_from_poly.py +5 -6
- nabu/app/diag_to_pix.py +7 -19
- nabu/app/diag_to_rot.py +14 -29
- nabu/app/double_flatfield.py +32 -44
- nabu/app/parse_reconstruction_log.py +3 -0
- nabu/app/reconstruct.py +53 -15
- nabu/app/reconstruct_helical.py +2 -2
- nabu/app/stitching.py +27 -13
- nabu/app/tests/__init__.py +0 -0
- nabu/app/tests/test_reduce_dark_flat.py +4 -1
- nabu/cuda/kernel.py +11 -2
- nabu/cuda/processing.py +2 -2
- nabu/cuda/src/cone.cu +77 -0
- nabu/cuda/src/hierarchical_backproj.cu +271 -0
- nabu/cuda/utils.py +0 -6
- nabu/estimation/alignment.py +5 -19
- nabu/estimation/cor.py +176 -597
- nabu/estimation/cor_sino.py +353 -25
- nabu/estimation/focus.py +63 -11
- nabu/estimation/tests/test_cor.py +124 -58
- nabu/estimation/tests/test_focus.py +6 -6
- nabu/estimation/tilt.py +2 -1
- nabu/estimation/utils.py +5 -33
- nabu/io/__init__.py +1 -1
- nabu/io/cast_volume.py +1 -1
- nabu/io/reader.py +416 -21
- nabu/io/tests/test_readers.py +422 -0
- nabu/io/tests/test_writers.py +1 -102
- nabu/io/writer.py +4 -433
- nabu/opencl/kernel.py +14 -3
- nabu/opencl/processing.py +8 -0
- nabu/pipeline/config_validators.py +5 -2
- nabu/pipeline/datadump.py +12 -5
- nabu/pipeline/estimators.py +125 -187
- nabu/pipeline/fullfield/chunked.py +162 -90
- nabu/pipeline/fullfield/chunked_cuda.py +7 -3
- nabu/pipeline/fullfield/computations.py +2 -7
- nabu/pipeline/fullfield/dataset_validator.py +0 -4
- nabu/pipeline/fullfield/nabu_config.py +37 -13
- nabu/pipeline/fullfield/processconfig.py +22 -13
- nabu/pipeline/fullfield/reconstruction.py +13 -9
- nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
- nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
- nabu/pipeline/helical/helical_reconstruction.py +1 -1
- nabu/pipeline/params.py +21 -1
- nabu/pipeline/processconfig.py +1 -12
- nabu/pipeline/reader.py +146 -0
- nabu/pipeline/tests/test_estimators.py +40 -72
- nabu/pipeline/utils.py +4 -2
- nabu/pipeline/writer.py +3 -2
- nabu/preproc/ccd_cuda.py +1 -1
- nabu/preproc/ctf.py +14 -7
- nabu/preproc/ctf_cuda.py +2 -3
- nabu/preproc/double_flatfield.py +5 -12
- nabu/preproc/double_flatfield_cuda.py +2 -2
- nabu/preproc/flatfield.py +5 -1
- nabu/preproc/flatfield_cuda.py +5 -1
- nabu/preproc/phase.py +24 -73
- nabu/preproc/phase_cuda.py +5 -8
- nabu/preproc/tests/test_ctf.py +11 -7
- nabu/preproc/tests/test_flatfield.py +67 -122
- nabu/preproc/tests/test_paganin.py +54 -30
- nabu/processing/azim.py +206 -0
- nabu/processing/convolution_cuda.py +1 -1
- nabu/processing/fft_cuda.py +15 -17
- nabu/processing/histogram.py +2 -0
- nabu/processing/histogram_cuda.py +2 -1
- nabu/processing/kernel_base.py +3 -0
- nabu/processing/muladd_cuda.py +1 -0
- nabu/processing/padding_opencl.py +1 -1
- nabu/processing/roll_opencl.py +1 -0
- nabu/processing/rotation_cuda.py +2 -2
- nabu/processing/tests/test_fft.py +17 -10
- nabu/processing/unsharp_cuda.py +1 -1
- nabu/reconstruction/cone.py +104 -40
- nabu/reconstruction/fbp.py +3 -0
- nabu/reconstruction/fbp_base.py +7 -2
- nabu/reconstruction/filtering.py +20 -7
- nabu/reconstruction/filtering_cuda.py +7 -1
- nabu/reconstruction/hbp.py +424 -0
- nabu/reconstruction/mlem.py +99 -0
- nabu/reconstruction/reconstructor.py +2 -0
- nabu/reconstruction/rings_cuda.py +19 -19
- nabu/reconstruction/sinogram_cuda.py +1 -0
- nabu/reconstruction/sinogram_opencl.py +3 -1
- nabu/reconstruction/tests/test_cone.py +10 -5
- nabu/reconstruction/tests/test_deringer.py +7 -6
- nabu/reconstruction/tests/test_fbp.py +124 -10
- nabu/reconstruction/tests/test_filtering.py +13 -11
- nabu/reconstruction/tests/test_halftomo.py +30 -4
- nabu/reconstruction/tests/test_mlem.py +91 -0
- nabu/reconstruction/tests/test_reconstructor.py +8 -3
- nabu/resources/dataset_analyzer.py +130 -85
- nabu/resources/gpu.py +1 -0
- nabu/resources/nxflatfield.py +134 -125
- nabu/resources/templates/id16a_fluo.conf +42 -0
- nabu/resources/tests/test_extract.py +10 -0
- nabu/resources/tests/test_nxflatfield.py +2 -2
- nabu/stitching/alignment.py +80 -24
- nabu/stitching/config.py +105 -68
- nabu/stitching/definitions.py +1 -0
- nabu/stitching/frame_composition.py +68 -60
- nabu/stitching/overlap.py +91 -51
- nabu/stitching/single_axis_stitching.py +32 -0
- nabu/stitching/slurm_utils.py +6 -6
- nabu/stitching/stitcher/__init__.py +0 -0
- nabu/stitching/stitcher/base.py +124 -0
- nabu/stitching/stitcher/dumper/__init__.py +3 -0
- nabu/stitching/stitcher/dumper/base.py +94 -0
- nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
- nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
- nabu/stitching/stitcher/post_processing.py +555 -0
- nabu/stitching/stitcher/pre_processing.py +1068 -0
- nabu/stitching/stitcher/single_axis.py +484 -0
- nabu/stitching/stitcher/stitcher.py +0 -0
- nabu/stitching/stitcher/y_stitcher.py +13 -0
- nabu/stitching/stitcher/z_stitcher.py +45 -0
- nabu/stitching/stitcher_2D.py +278 -0
- nabu/stitching/tests/test_config.py +12 -37
- nabu/stitching/tests/test_frame_composition.py +33 -59
- nabu/stitching/tests/test_overlap.py +149 -7
- nabu/stitching/tests/test_utils.py +1 -1
- nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
- nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
- nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
- nabu/stitching/utils/__init__.py +1 -0
- nabu/stitching/utils/post_processing.py +281 -0
- nabu/stitching/utils/tests/test_post-processing.py +21 -0
- nabu/stitching/{utils.py → utils/utils.py} +79 -52
- nabu/stitching/y_stitching.py +27 -0
- nabu/stitching/z_stitching.py +32 -2281
- nabu/testutils.py +1 -152
- nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
- nabu/utils.py +158 -61
- {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/METADATA +24 -17
- {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/RECORD +145 -121
- {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/WHEEL +1 -1
- nabu/io/tiffwriter_zmm.py +0 -99
- nabu/pipeline/fallback_utils.py +0 -149
- nabu/pipeline/helical/tests/test_accumulator.py +0 -158
- nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
- nabu/pipeline/helical/tests/test_strategy.py +0 -61
- nabu/pipeline/helical/utils.py +0 -51
- nabu/pipeline/tests/test_chunk_reader.py +0 -74
- {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/LICENSE +0 -0
- {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/entry_points.txt +0 -0
- {nabu-2024.1.10.dist-info → nabu-2024.2.0rc1.dist-info}/top_level.txt +0 -0
nabu/pipeline/estimators.py
CHANGED
@@ -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
|
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.
|
111
|
+
self._get_lookup_side()
|
112
|
+
self._init_cor_finder()
|
116
113
|
|
117
|
-
def
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
self.
|
124
|
-
|
125
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
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
|
189
|
+
def _apply_flatfield(self):
|
274
190
|
if not (self.do_flatfield):
|
275
191
|
return
|
276
|
-
self.flatfield =
|
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
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|
250
|
+
"fourier-angles": {"class": CenterOfRotationFourierAngles},
|
251
|
+
"vo": {
|
252
|
+
"class": CenterOfRotationVo,
|
253
|
+
},
|
338
254
|
}
|
339
255
|
|
340
256
|
def __init__(
|
341
|
-
self,
|
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
|
-
|
388
|
-
|
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
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
self.data_reader =
|
405
|
-
|
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
|
-
|
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
|
-
|
419
|
-
|
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
|
-
|
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
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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": "
|
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=
|
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 =
|
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 =
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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 =
|
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):
|