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