nabu 2025.1.0.dev14__py3-none-any.whl → 2025.1.0rc2__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/doc_config.py +32 -0
- nabu/__init__.py +1 -1
- nabu/app/cast_volume.py +9 -1
- nabu/app/cli_configs.py +80 -3
- nabu/app/estimate_motion.py +54 -0
- nabu/app/multicor.py +2 -4
- nabu/app/pcaflats.py +116 -0
- nabu/app/reconstruct.py +1 -7
- nabu/app/reduce_dark_flat.py +5 -2
- nabu/estimation/cor.py +1 -1
- nabu/estimation/motion.py +557 -0
- nabu/estimation/tests/test_motion_estimation.py +471 -0
- nabu/estimation/tilt.py +1 -1
- nabu/estimation/translation.py +47 -1
- nabu/io/cast_volume.py +100 -13
- nabu/io/reader.py +32 -1
- nabu/io/tests/test_remove_volume.py +152 -0
- nabu/pipeline/config_validators.py +42 -43
- nabu/pipeline/estimators.py +255 -0
- nabu/pipeline/fullfield/chunked.py +67 -43
- nabu/pipeline/fullfield/chunked_cuda.py +5 -2
- nabu/pipeline/fullfield/nabu_config.py +20 -14
- nabu/pipeline/fullfield/processconfig.py +17 -3
- nabu/pipeline/fullfield/reconstruction.py +4 -1
- nabu/pipeline/params.py +12 -0
- nabu/pipeline/tests/test_estimators.py +240 -3
- nabu/preproc/ccd.py +53 -3
- nabu/preproc/flatfield.py +306 -1
- nabu/preproc/shift.py +3 -1
- nabu/preproc/tests/test_pcaflats.py +154 -0
- nabu/processing/rotation_cuda.py +3 -1
- nabu/processing/tests/test_rotation.py +4 -2
- nabu/reconstruction/astra.py +245 -0
- nabu/reconstruction/fbp.py +7 -0
- nabu/reconstruction/fbp_base.py +31 -7
- nabu/reconstruction/fbp_opencl.py +8 -0
- nabu/reconstruction/filtering_opencl.py +2 -0
- nabu/reconstruction/mlem.py +47 -13
- nabu/reconstruction/tests/test_filtering.py +13 -2
- nabu/reconstruction/tests/test_mlem.py +91 -62
- nabu/resources/dataset_analyzer.py +144 -20
- nabu/resources/nxflatfield.py +101 -35
- nabu/resources/tests/test_nxflatfield.py +1 -1
- nabu/resources/utils.py +16 -10
- nabu/stitching/alignment.py +7 -7
- nabu/stitching/config.py +22 -20
- nabu/stitching/definitions.py +2 -2
- nabu/stitching/overlap.py +4 -4
- nabu/stitching/sample_normalization.py +5 -5
- nabu/stitching/stitcher/post_processing.py +5 -3
- nabu/stitching/stitcher/pre_processing.py +24 -20
- nabu/stitching/tests/test_config.py +3 -3
- nabu/stitching/tests/test_y_preprocessing_stitching.py +11 -8
- nabu/stitching/tests/test_z_postprocessing_stitching.py +2 -2
- nabu/stitching/tests/test_z_preprocessing_stitching.py +23 -20
- nabu/stitching/utils/utils.py +7 -7
- nabu/testutils.py +1 -4
- nabu/utils.py +13 -0
- {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/METADATA +3 -4
- {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/RECORD +64 -57
- {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/WHEEL +1 -1
- {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/entry_points.txt +2 -1
- nabu/app/correct_rot.py +0 -62
- {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/licenses/LICENSE +0 -0
- {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/top_level.txt +0 -0
nabu/pipeline/estimators.py
CHANGED
@@ -9,6 +9,7 @@ import scipy.fft # pylint: disable=E0611
|
|
9
9
|
from silx.io import get_data
|
10
10
|
import math
|
11
11
|
from scipy import ndimage as nd
|
12
|
+
|
12
13
|
from ..preproc.flatfield import FlatField
|
13
14
|
from ..estimation.cor import (
|
14
15
|
CenterOfRotation,
|
@@ -17,8 +18,10 @@ from ..estimation.cor import (
|
|
17
18
|
CenterOfRotationGrowingWindow,
|
18
19
|
CenterOfRotationOctaveAccurate,
|
19
20
|
)
|
21
|
+
from .. import version as nabu_version
|
20
22
|
from ..estimation.cor_sino import SinoCorInterface, CenterOfRotationFourierAngles, CenterOfRotationVo
|
21
23
|
from ..estimation.tilt import CameraTilt
|
24
|
+
from ..estimation.motion import MotionEstimation
|
22
25
|
from ..estimation.utils import is_fullturn_scan
|
23
26
|
from ..resources.logger import LoggerOrPrint
|
24
27
|
from ..resources.utils import extract_parameters
|
@@ -989,3 +992,255 @@ class DetectorTiltEstimator:
|
|
989
992
|
|
990
993
|
# alias
|
991
994
|
TiltFinder = DetectorTiltEstimator
|
995
|
+
|
996
|
+
|
997
|
+
def estimate_translations(dataset_info, do_flatfield=True): ...
|
998
|
+
|
999
|
+
|
1000
|
+
class TranslationsEstimator:
|
1001
|
+
|
1002
|
+
_default_extra_options = {
|
1003
|
+
"window_size": 200,
|
1004
|
+
}
|
1005
|
+
|
1006
|
+
def __init__(
|
1007
|
+
self,
|
1008
|
+
dataset_info,
|
1009
|
+
do_flatfield=True,
|
1010
|
+
rot_center=None,
|
1011
|
+
halftomo_side=None,
|
1012
|
+
angular_subsampling=10,
|
1013
|
+
deg_xy=2,
|
1014
|
+
deg_z=2,
|
1015
|
+
shifts_estimator="phase_cross_correlation",
|
1016
|
+
extra_options=None,
|
1017
|
+
):
|
1018
|
+
self._configure_extra_options(extra_options)
|
1019
|
+
self.logger = LoggerOrPrint(dataset_info.logger)
|
1020
|
+
self.dataset_info = dataset_info
|
1021
|
+
self.angular_subsampling = angular_subsampling
|
1022
|
+
self.do_360 = self.dataset_info.is_360
|
1023
|
+
self.do_flatfield = do_flatfield
|
1024
|
+
self.radios = None
|
1025
|
+
self._deg_xy = deg_xy
|
1026
|
+
self._deg_z = deg_z
|
1027
|
+
self._shifts_estimator = shifts_estimator
|
1028
|
+
self._shifts_estimator_kwargs = {}
|
1029
|
+
self._cor = rot_center
|
1030
|
+
self._configure_halftomo(halftomo_side)
|
1031
|
+
self._estimate_cor = self._cor is None
|
1032
|
+
self.sample_shifts_xy = None
|
1033
|
+
self.sample_shifts_z = None
|
1034
|
+
|
1035
|
+
def _configure_extra_options(self, extra_options):
|
1036
|
+
self.extra_options = self._default_extra_options.copy()
|
1037
|
+
self.extra_options.update(extra_options or {})
|
1038
|
+
|
1039
|
+
def _configure_halftomo(self, halftomo_side):
|
1040
|
+
if halftomo_side is False:
|
1041
|
+
# Force disable halftomo
|
1042
|
+
self.halftomo_side = False
|
1043
|
+
return
|
1044
|
+
self._start_x = None
|
1045
|
+
self._end_x = None
|
1046
|
+
if (halftomo_side is not None) and not (self.do_360):
|
1047
|
+
raise ValueError(
|
1048
|
+
"Expected 360° dataset for half-tomography, but this dataset does not look like a 360° dataset"
|
1049
|
+
)
|
1050
|
+
if halftomo_side is None:
|
1051
|
+
if self.dataset_info.is_halftomo:
|
1052
|
+
halftomo_side = "right"
|
1053
|
+
else:
|
1054
|
+
self.halftomo_side = False
|
1055
|
+
return
|
1056
|
+
self.halftomo_side = halftomo_side
|
1057
|
+
window_size = self.extra_options["window_size"]
|
1058
|
+
if self._cor is not None:
|
1059
|
+
# In this case we look for shifts around the CoR
|
1060
|
+
self._start_x = int(self._cor - window_size / 2)
|
1061
|
+
self._end_x = int(self._cor + window_size / 2)
|
1062
|
+
elif halftomo_side == "right":
|
1063
|
+
self._start_x = -window_size
|
1064
|
+
self._end_x = None
|
1065
|
+
elif halftomo_side == "left":
|
1066
|
+
self._start_x = 0
|
1067
|
+
self._end_x = window_size
|
1068
|
+
elif is_scalar(halftomo_side):
|
1069
|
+
# Expect approximate location of CoR, relative to left-most column
|
1070
|
+
self._start_x = int(halftomo_side - window_size / 2)
|
1071
|
+
self._end_x = int(halftomo_side + window_size / 2)
|
1072
|
+
else:
|
1073
|
+
raise ValueError(
|
1074
|
+
f"Expected 'halftomo_side' to be either 'left', 'right', or an integer (got {halftomo_side})"
|
1075
|
+
)
|
1076
|
+
self.logger.debug(f"[MotionEstimation] Half-tomo looking at [{self._start_x}:{self._end_x}]")
|
1077
|
+
# For half-tomo, skimage.registration.phase_cross_correlation might look a bit too far away
|
1078
|
+
if (
|
1079
|
+
self._shifts_estimator == "phase_cross_correlation"
|
1080
|
+
and self._shifts_estimator_kwargs.get("overlap_ratio", 0.3) >= 0.3
|
1081
|
+
):
|
1082
|
+
self._shifts_estimator_kwargs.update({"overlap_ratio": 0.2})
|
1083
|
+
#
|
1084
|
+
|
1085
|
+
def _load_data(self):
|
1086
|
+
self.logger.debug("[MotionEstimation] reading data")
|
1087
|
+
if self.do_360:
|
1088
|
+
"""
|
1089
|
+
In this case we compare pair of opposite projections.
|
1090
|
+
If rotation angles are arbitrary, we should do something like
|
1091
|
+
for angle in dataset_info.rotation_angles:
|
1092
|
+
img, angle_deg, idx = dataset_info.get_image_at_angle(
|
1093
|
+
np.degrees(angle)+180, return_angle_and_index=True
|
1094
|
+
)
|
1095
|
+
Most of the time (always ?), the dataset was acquired with a circular trajectory,
|
1096
|
+
so we can use angles:
|
1097
|
+
dataset_info.rotation_angles[::self.angular_subsampling]
|
1098
|
+
which amounts to reading one radio out of "angular_subsampling"
|
1099
|
+
"""
|
1100
|
+
|
1101
|
+
# TODO account for more general rotation angles. The following will only work for circular trajectory and ordered angles
|
1102
|
+
self._reader = self.dataset_info.get_reader(
|
1103
|
+
sub_region=(slice(None, None, self.angular_subsampling), slice(None), slice(None))
|
1104
|
+
)
|
1105
|
+
self.radios = self._reader.load_data()
|
1106
|
+
self.angles = self.dataset_info.rotation_angles[:: self.angular_subsampling]
|
1107
|
+
self._radios_idx = self._reader.get_frames_indices()
|
1108
|
+
self.logger.debug("[MotionEstimation] This is a 360° scan, will use pairs of opposite projections")
|
1109
|
+
else:
|
1110
|
+
"""
|
1111
|
+
In this case we use the "return projections", i.e special projections acquired at several angles
|
1112
|
+
(eg. [180, 90, 0]) before ending the scan
|
1113
|
+
"""
|
1114
|
+
return_projs, return_angles_deg, return_idx = self.dataset_info.get_alignment_projections()
|
1115
|
+
self._angles_return = np.radians(return_angles_deg)
|
1116
|
+
self._radios_return = return_projs
|
1117
|
+
self._radios_idx_return = return_idx
|
1118
|
+
|
1119
|
+
projs = []
|
1120
|
+
angles_rad = []
|
1121
|
+
projs_idx = []
|
1122
|
+
for angle_deg in return_angles_deg:
|
1123
|
+
proj, rot_angle_deg, proj_idx = self.dataset_info.get_image_at_angle(
|
1124
|
+
angle_deg, image_type="projection", return_angle_and_index=True
|
1125
|
+
)
|
1126
|
+
projs.append(proj)
|
1127
|
+
angles_rad.append(np.radians(rot_angle_deg))
|
1128
|
+
projs_idx.append(proj_idx)
|
1129
|
+
self._radios_outwards = np.array(projs)
|
1130
|
+
self._angles_outward = np.array(angles_rad)
|
1131
|
+
self._radios_idx_outwards = np.array(projs_idx)
|
1132
|
+
self.logger.debug("[MotionEstimation] This is a 180° scan, will use 'return projections'")
|
1133
|
+
|
1134
|
+
def _apply_flatfield(self):
|
1135
|
+
if not (self.do_flatfield):
|
1136
|
+
return
|
1137
|
+
self.logger.debug("[MotionEstimation] flatfield")
|
1138
|
+
if self.do_360:
|
1139
|
+
self._flatfield = FlatField(
|
1140
|
+
self.radios.shape,
|
1141
|
+
flats=self.dataset_info.flats,
|
1142
|
+
darks=self.dataset_info.darks,
|
1143
|
+
radios_indices=self._radios_idx,
|
1144
|
+
)
|
1145
|
+
self._flatfield.normalize_radios(self.radios)
|
1146
|
+
else:
|
1147
|
+
# 180 + return projs
|
1148
|
+
self._flatfield_outwards = FlatField(
|
1149
|
+
self._radios_outwards.shape,
|
1150
|
+
flats=self.dataset_info.flats,
|
1151
|
+
darks=self.dataset_info.darks,
|
1152
|
+
radios_indices=self._radios_idx_outwards,
|
1153
|
+
)
|
1154
|
+
self._flatfield_outwards.normalize_radios(self._radios_outwards)
|
1155
|
+
self._flatfield_return = FlatField(
|
1156
|
+
self._radios_return.shape,
|
1157
|
+
flats=self.dataset_info.flats,
|
1158
|
+
darks=self.dataset_info.darks,
|
1159
|
+
radios_indices=self._radios_idx_return,
|
1160
|
+
)
|
1161
|
+
self._flatfield_outwards.normalize_radios(self._radios_return)
|
1162
|
+
|
1163
|
+
def estimate_motion(self):
|
1164
|
+
self._load_data()
|
1165
|
+
self._apply_flatfield()
|
1166
|
+
|
1167
|
+
n_projs_tot = self.dataset_info.n_angles
|
1168
|
+
if self.do_360:
|
1169
|
+
n_a = self.radios.shape[0]
|
1170
|
+
# See notes above - this works only for circular trajectory / ordered angles
|
1171
|
+
projs_stack1 = self.radios[: n_a // 2]
|
1172
|
+
projs_stack2 = self.radios[n_a // 2 :]
|
1173
|
+
angles1 = self.angles[: n_a // 2]
|
1174
|
+
angles2 = self.angles[n_a // 2 :]
|
1175
|
+
indices1 = (self._radios_idx - self._radios_idx[0])[: n_a // 2]
|
1176
|
+
indices2 = (self._radios_idx - self._radios_idx[0])[n_a // 2 :]
|
1177
|
+
else:
|
1178
|
+
projs_stack1 = self._radios_outwards
|
1179
|
+
projs_stack2 = self._radios_return
|
1180
|
+
angles1 = self._angles_outward
|
1181
|
+
angles2 = self._angles_return
|
1182
|
+
indices1 = self._radios_idx_outwards - self._radios_idx_outwards.min()
|
1183
|
+
indices2 = self._radios_idx_return - self._radios_idx_outwards.min()
|
1184
|
+
|
1185
|
+
if self._start_x is not None:
|
1186
|
+
# Compute Motion Estimation on subset of images (eg. for half-tomo)
|
1187
|
+
projs_stack1 = projs_stack1[..., self._start_x : self._end_x]
|
1188
|
+
projs_stack2 = projs_stack2[..., self._start_x : self._end_x]
|
1189
|
+
|
1190
|
+
self.motion_estimator = MotionEstimation(
|
1191
|
+
projs_stack1,
|
1192
|
+
projs_stack2,
|
1193
|
+
angles1,
|
1194
|
+
angles2,
|
1195
|
+
indices1,
|
1196
|
+
indices2,
|
1197
|
+
n_projs_tot,
|
1198
|
+
shifts_estimator=self._shifts_estimator,
|
1199
|
+
shifts_estimator_kwargs=self._shifts_estimator_kwargs,
|
1200
|
+
)
|
1201
|
+
|
1202
|
+
self.logger.debug("[MotionEstimation] estimating shifts")
|
1203
|
+
|
1204
|
+
estimated_shifts_v = self.motion_estimator.estimate_vertical_motion(degree=self._deg_z)
|
1205
|
+
estimated_shifts_h, cor = self.motion_estimator.estimate_horizontal_motion(degree=self._deg_xy, cor=self._cor)
|
1206
|
+
if self._start_x is not None:
|
1207
|
+
cor += (self._start_x % self.radios.shape[-1]) + (projs_stack1.shape[-1] - 1) / 2.0
|
1208
|
+
|
1209
|
+
self.sample_shifts_xy = estimated_shifts_h
|
1210
|
+
self.sample_shifts_z = estimated_shifts_v
|
1211
|
+
if self._cor is None:
|
1212
|
+
self.logger.info(
|
1213
|
+
"[MotionEstimation] Estimated center of rotation (relative to middle of detector): %.2f" % cor
|
1214
|
+
)
|
1215
|
+
return estimated_shifts_h, estimated_shifts_v, cor
|
1216
|
+
|
1217
|
+
def generate_translations_movements_file(self, filename, fmt="%.3f", only=None):
|
1218
|
+
if self.sample_shifts_xy is None:
|
1219
|
+
raise RuntimeError("Need to run estimate_motion() first")
|
1220
|
+
|
1221
|
+
angles = self.dataset_info.rotation_angles
|
1222
|
+
cor = self._cor or 0
|
1223
|
+
txy_est_all_angles = self.motion_estimator.apply_fit_horiz(angles=angles)
|
1224
|
+
tz_est_all_angles = self.motion_estimator.apply_fit_vertic(angles=angles)
|
1225
|
+
estimated_shifts_vu_all_angles = self.motion_estimator.convert_sample_motion_to_detector_shifts(
|
1226
|
+
txy_est_all_angles, tz_est_all_angles, angles, cor=cor
|
1227
|
+
)
|
1228
|
+
estimated_shifts_vu_all_angles[:, 1] -= cor
|
1229
|
+
correct_shifts_uv = -estimated_shifts_vu_all_angles[:, ::-1]
|
1230
|
+
|
1231
|
+
if only is not None:
|
1232
|
+
if only == "horizontal":
|
1233
|
+
correct_shifts_uv[:, 1] = 0
|
1234
|
+
elif only == "vertical":
|
1235
|
+
correct_shifts_uv[:, 0] = 0
|
1236
|
+
else:
|
1237
|
+
raise ValueError("Expected 'only' to be either None, 'horizontal' or 'vertical'")
|
1238
|
+
|
1239
|
+
header = f"Generated by nabu {nabu_version} : {str(self)}"
|
1240
|
+
np.savetxt(filename, correct_shifts_uv, fmt=fmt, header=header)
|
1241
|
+
|
1242
|
+
def __str__(self):
|
1243
|
+
ret = f"{self.__class__.__name__}(do_flatfield={self.do_flatfield}, rot_center={self._cor}, angular_subsampling={self.angular_subsampling})"
|
1244
|
+
if self.sample_shifts_xy is not None:
|
1245
|
+
ret += f", shifts_estimator={self.motion_estimator.shifts_estimator}"
|
1246
|
+
return ret
|
@@ -10,7 +10,7 @@ from ...resources.utils import extract_parameters
|
|
10
10
|
from ...misc.binning import binning as image_binning
|
11
11
|
from ...io.reader import EDFStackReader, HDF5Loader, NXTomoReader
|
12
12
|
from ...preproc.ccd import Log, CCDFilter
|
13
|
-
from ...preproc.flatfield import FlatField
|
13
|
+
from ...preproc.flatfield import FlatField, PCAFlatsNormalizer
|
14
14
|
from ...preproc.distortion import DistortionCorrection
|
15
15
|
from ...preproc.shift import VerticalShift
|
16
16
|
from ...preproc.double_flatfield import DoubleFlatField
|
@@ -45,6 +45,7 @@ class ChunkedPipeline:
|
|
45
45
|
|
46
46
|
backend = "numpy"
|
47
47
|
FlatFieldClass = FlatField
|
48
|
+
PCAFlatFieldClass = PCAFlatsNormalizer
|
48
49
|
DoubleFlatFieldClass = DoubleFlatField
|
49
50
|
CCDCorrectionClass = CCDFilter
|
50
51
|
PaganinPhaseRetrievalClass = PaganinPhaseRetrieval
|
@@ -393,50 +394,68 @@ class ChunkedPipeline:
|
|
393
394
|
|
394
395
|
@use_options("flatfield", "flatfield")
|
395
396
|
def _init_flatfield(self):
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
self.
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
397
|
+
if self.processing_options["flatfield"]:
|
398
|
+
self._ff_options = self.processing_options["flatfield"].copy()
|
399
|
+
|
400
|
+
# This won't work when resuming from a step (i.e before FF), because we rely on H5Loader()
|
401
|
+
# which re-compacts the data. When data is re-compacted, we have to know the original radios positions.
|
402
|
+
# These positions can be saved in the "file_dump" metadata, but it is not loaded for now
|
403
|
+
# (the process_config object is re-built from scratch every time)
|
404
|
+
self._ff_options["projs_indices"] = self.chunk_reader.get_frames_indices()
|
405
|
+
|
406
|
+
if self._ff_options.get("normalize_srcurrent", False):
|
407
|
+
a_start_idx, a_end_idx = self.sub_region[0]
|
408
|
+
subs = self.process_config.subsampling_factor
|
409
|
+
self._ff_options["radios_srcurrent"] = self._ff_options["radios_srcurrent"][a_start_idx:a_end_idx:subs]
|
410
|
+
|
411
|
+
distortion_correction = None
|
412
|
+
if self._ff_options["do_flat_distortion"]:
|
413
|
+
self.logger.info("Flats distortion correction will be applied")
|
414
|
+
self.FlatFieldClass = FlatField # no GPU implementation available, force this backend
|
415
|
+
estimation_kwargs = {}
|
416
|
+
estimation_kwargs.update(self._ff_options["flat_distortion_params"])
|
417
|
+
estimation_kwargs["logger"] = self.logger
|
418
|
+
distortion_correction = DistortionCorrection(
|
419
|
+
estimation_method="fft-correlation",
|
420
|
+
estimation_kwargs=estimation_kwargs,
|
421
|
+
correction_method="interpn",
|
422
|
+
)
|
419
423
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
424
|
+
if self.processing_options["flatfield"]["method"].lower() != "pca":
|
425
|
+
# Reduced darks/flats are loaded, but we have to crop them on the current sub-region
|
426
|
+
# and possibly do apply some pre-processing (binning, distortion correction, ...)
|
427
|
+
darks_flats = load_darks_flats(
|
428
|
+
self.dataset_info,
|
429
|
+
self.sub_region[1:],
|
430
|
+
processing_func=self._ff_processing_function,
|
431
|
+
processing_func_args=self._ff_processing_function_args,
|
432
|
+
)
|
428
433
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
434
|
+
# FlatField parameter "radios_indices" must account for subsampling
|
435
|
+
self.flatfield = self.FlatFieldClass(
|
436
|
+
self.radios_shape,
|
437
|
+
flats=darks_flats["flats"],
|
438
|
+
darks=darks_flats["darks"],
|
439
|
+
radios_indices=self._ff_options["projs_indices"],
|
440
|
+
interpolation="linear",
|
441
|
+
distortion_correction=distortion_correction,
|
442
|
+
radios_srcurrent=self._ff_options["radios_srcurrent"],
|
443
|
+
flats_srcurrent=self._ff_options["flats_srcurrent"],
|
444
|
+
)
|
445
|
+
else:
|
446
|
+
flats = self.process_config.dataset_info.flats
|
447
|
+
darks = self.process_config.dataset_info.darks
|
448
|
+
if len(darks) != 1:
|
449
|
+
raise ValueError(f"There should be only one reduced dark. Found {len(darks)}.")
|
450
|
+
else:
|
451
|
+
dark_key = list(darks.keys())[0]
|
452
|
+
nb_pca_components = len(flats) - 1
|
453
|
+
img_subregion = tuple(slice(*sr) for sr in self.sub_region[1:])
|
454
|
+
self.flatfield = self.PCAFlatFieldClass(
|
455
|
+
np.array([flats[k][img_subregion] for k in range(1, nb_pca_components)]),
|
456
|
+
darks[dark_key][img_subregion],
|
457
|
+
flats[0][img_subregion], # Mean
|
458
|
+
)
|
440
459
|
|
441
460
|
@use_options("double_flatfield", "double_flatfield")
|
442
461
|
def _init_double_flatfield(self):
|
@@ -647,6 +666,11 @@ class ChunkedPipeline:
|
|
647
666
|
"v_max_for_v_shifts": None,
|
648
667
|
"v_min_for_u_shifts": 0,
|
649
668
|
"v_max_for_u_shifts": None,
|
669
|
+
"scale_factor": 1.0 / options["voxel_size_cm"][0],
|
670
|
+
"clip_outer_circle": options["clip_outer_circle"],
|
671
|
+
"outer_circle_value": options["outer_circle_value"],
|
672
|
+
"filter_cutoff": options["fbp_filter_cutoff"],
|
673
|
+
"crop_filtered_data": options["crop_filtered_data"],
|
650
674
|
},
|
651
675
|
)
|
652
676
|
|
@@ -75,8 +75,11 @@ class CudaChunkedPipeline(ChunkedPipeline):
|
|
75
75
|
# Decide when to transfer data to GPU. Normally it's right after reading the data,
|
76
76
|
# But sometimes a part of the processing is done on CPU.
|
77
77
|
self._when_to_transfer_radios_on_gpu = "read_data"
|
78
|
-
if self.flatfield is not None
|
79
|
-
|
78
|
+
if self.flatfield is not None:
|
79
|
+
use_flats_distortion = getattr(self.flatfield, "distortion_correction", None) is not None
|
80
|
+
use_pca_flats = self.processing_options["flatfield"]["method"].lower() == "pca"
|
81
|
+
if use_flats_distortion or use_pca_flats:
|
82
|
+
self._when_to_transfer_radios_on_gpu = "flatfield"
|
80
83
|
|
81
84
|
def _init_cuda(self, cuda_options):
|
82
85
|
if not (__has_pycuda__):
|
@@ -16,14 +16,14 @@ nabu_config = {
|
|
16
16
|
"type": "advanced",
|
17
17
|
},
|
18
18
|
"nexus_version": {
|
19
|
-
"default": "
|
20
|
-
"help": "Nexus version to use when browsing the HDF5 dataset.
|
21
|
-
"validator":
|
19
|
+
"default": "",
|
20
|
+
"help": "Specify a Nexus version to use when browsing the HDF5 dataset.",
|
21
|
+
"validator": optional_float_validator,
|
22
22
|
"type": "advanced",
|
23
23
|
},
|
24
24
|
"darks_flats_dir": {
|
25
25
|
"default": "",
|
26
|
-
"help": "Path to a directory where XXX_flats.h5 and XXX_darks.h5 are to be found, where 'XXX' denotes the dataset basename. If these files are found, then reduced flats/darks will be loaded from them. Otherwise, reduced flats/darks will be saved
|
26
|
+
"help": "Path to a directory where XXX_flats.h5 and XXX_darks.h5 are to be found, where 'XXX' denotes the dataset basename. If these files are found, then reduced flats/darks will be loaded from them. Otherwise, reduced flats/darks will be saved there once computed, either in the .nx directory, or in the output directory. Mind that the HDF5 entry corresponds to the one of the dataset.",
|
27
27
|
"validator": optional_directory_location_validator,
|
28
28
|
"type": "optional",
|
29
29
|
},
|
@@ -41,7 +41,7 @@ nabu_config = {
|
|
41
41
|
},
|
42
42
|
"projections_subsampling": {
|
43
43
|
"default": "1",
|
44
|
-
"help": "Projections subsampling factor: take one projection out of '
|
44
|
+
"help": "Projections subsampling factor: take one projection out of 'projections_subsampling'. The format can be an integer (take 1 projection out of N), or N:M (take 1 projection out of N, start with the projection number M)\nFor example: 2 (or 2:0) to reconstruct from even projections, 2:1 to reconstruct from odd projections.",
|
45
45
|
"validator": projections_subsampling_validator,
|
46
46
|
"type": "advanced",
|
47
47
|
},
|
@@ -61,13 +61,19 @@ nabu_config = {
|
|
61
61
|
"preproc": {
|
62
62
|
"flatfield": {
|
63
63
|
"default": "1",
|
64
|
-
"help": "How to perform flat-field normalization. The parameter value can be:\n - 1 or True: enabled.\n - 0 or False: disabled\n -
|
65
|
-
"validator":
|
64
|
+
"help": "How to perform flat-field normalization. The parameter value can be:\n - 1 or True: enabled.\n - 0 or False: disabled\n - pca: perform a normalization via Principal Component Analysis decomposition PCA-flat-field normalization",
|
65
|
+
"validator": flatfield_validator,
|
66
66
|
"type": "required",
|
67
67
|
},
|
68
|
+
"flatfield_loading_mode": {
|
69
|
+
"default": "load_if_present",
|
70
|
+
"help": "How to load/compute flat-field. This parameter can be:\n - load_if_present (default) or empty string: Use the existing flatfield files, if existing.\n - force-load: perform flatfield regardless of the dataset by attempting to load darks/flats\n - force-compute: perform flatfield, ignore all .h5 files containing already computed darks/flats.",
|
71
|
+
"validator": flatfield_loading_mode_validator,
|
72
|
+
"type": "optional",
|
73
|
+
},
|
68
74
|
"flat_distortion_correction_enabled": {
|
69
75
|
"default": "0",
|
70
|
-
"help": "Whether to correct for flat distortion. If activated, each
|
76
|
+
"help": "Whether to correct for flat distortion. If activated, each radiograph is correlated with its corresponding flat, in order to determine and correct the flat distortion.",
|
71
77
|
"validator": boolean_validator,
|
72
78
|
"type": "advanced",
|
73
79
|
},
|
@@ -113,7 +119,7 @@ nabu_config = {
|
|
113
119
|
"double_flatfield": {
|
114
120
|
"default": "0",
|
115
121
|
"help": "Whether to perform 'double flat-field' filtering (this can help to remove rings artefacts). Possible values:\n - 1 or True: enabled.\n - 0 or False: disabled\n - force-load: use an existing DFF file regardless of the dataset\n - force-compute: re-compute the DFF, ignore all existing .h5 files containing already computed DFF",
|
116
|
-
"validator":
|
122
|
+
"validator": flatfield_validator,
|
117
123
|
"type": "optional",
|
118
124
|
},
|
119
125
|
"dff_sigma": {
|
@@ -172,7 +178,7 @@ nabu_config = {
|
|
172
178
|
},
|
173
179
|
"rotate_projections_center": {
|
174
180
|
"default": "",
|
175
|
-
"help": "Center of rotation when 'tilt_correction' is non-empty. By default the center of rotation is the middle of each
|
181
|
+
"help": "Center of rotation when 'tilt_correction' is non-empty. By default the center of rotation is the middle of each radiograph, i.e ((Nx-1)/2.0, (Ny-1)/2.0).",
|
176
182
|
"validator": optional_tuple_of_floats_validator,
|
177
183
|
"type": "advanced",
|
178
184
|
},
|
@@ -272,7 +278,7 @@ nabu_config = {
|
|
272
278
|
},
|
273
279
|
"cor_slice": {
|
274
280
|
"default": "",
|
275
|
-
"help": "Which slice to use for estimating the Center of Rotation (CoR). This parameter can be an integer or 'top', 'middle', 'bottom'.\nIf provided, the CoR will be estimated from the
|
281
|
+
"help": "Which slice to use for estimating the Center of Rotation (CoR). This parameter can be an integer or 'top', 'middle', 'bottom'.\nIf provided, the CoR will be estimated from the corresponding sinogram, and 'cor_options' can contain the parameter 'subsampling'.",
|
276
282
|
"validator": cor_slice_validator,
|
277
283
|
"type": "advanced",
|
278
284
|
},
|
@@ -479,7 +485,7 @@ nabu_config = {
|
|
479
485
|
},
|
480
486
|
"postproc": {
|
481
487
|
"output_histogram": {
|
482
|
-
"default": "
|
488
|
+
"default": "1",
|
483
489
|
"help": "Whether to compute a histogram of the volume.",
|
484
490
|
"validator": boolean_validator,
|
485
491
|
"type": "optional",
|
@@ -544,7 +550,7 @@ nabu_config = {
|
|
544
550
|
"pipeline": {
|
545
551
|
"save_steps": {
|
546
552
|
"default": "",
|
547
|
-
"help": "Save intermediate results. This is a list of comma-separated processing steps, for ex: flatfield, phase, sinogram.\nEach step generates a HDF5 file in the form name_file_prefix.hdf5 (
|
553
|
+
"help": "Save intermediate results. This is a list of comma-separated processing steps, for ex: flatfield, phase, sinogram.\nEach step generates a HDF5 file in the form name_file_prefix.hdf5 (e.g. 'sinogram_file_prefix.hdf5')",
|
548
554
|
"validator": optional_string_validator,
|
549
555
|
"type": "optional",
|
550
556
|
},
|
@@ -556,7 +562,7 @@ nabu_config = {
|
|
556
562
|
},
|
557
563
|
"steps_file": {
|
558
564
|
"default": "",
|
559
|
-
"help": "File where the intermediate processing steps are written. By default it is empty, and intermediate processing steps are written in the same directory as the reconstructions, with a file prefix,
|
565
|
+
"help": "File where the intermediate processing steps are written. By default it is empty, and intermediate processing steps are written in the same directory as the reconstructions, with a file prefix, e.g. sinogram_mydataset.hdf5.",
|
560
566
|
"validator": optional_output_file_path_validator,
|
561
567
|
"type": "advanced",
|
562
568
|
},
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
4
4
|
from .get_double_flatfield import get_double_flatfield
|
5
5
|
from silx.io import get_data
|
6
6
|
from silx.io.url import DataUrl
|
7
|
-
from ...utils import copy_dict_items, compare_dicts
|
7
|
+
from ...utils import copy_dict_items, compare_dicts, deprecation_warning
|
8
8
|
from ...io.utils import hdf5_entry_exists, get_h5_value
|
9
9
|
from ...io.reader import import_h5_to_dict
|
10
10
|
from ...resources.utils import extract_parameters, get_values_from_file
|
@@ -75,16 +75,19 @@ class ProcessConfig(ProcessConfigBase):
|
|
75
75
|
Update the 'dataset_info' (DatasetAnalyzer class instance) data structure with options from user configuration.
|
76
76
|
"""
|
77
77
|
self.logger.debug("Updating dataset information with user configuration")
|
78
|
-
if self.dataset_info.kind == "nx":
|
78
|
+
if self.dataset_info.kind == "nx" and self.nabu_config["preproc"]["flatfield"]:
|
79
79
|
update_dataset_info_flats_darks(
|
80
80
|
self.dataset_info,
|
81
81
|
self.nabu_config["preproc"]["flatfield"],
|
82
|
+
loading_mode=self.nabu_config["preproc"]["flatfield_loading_mode"],
|
82
83
|
output_dir=self.nabu_config["output"]["location"],
|
83
84
|
darks_flats_dir=self.nabu_config["dataset"]["darks_flats_dir"],
|
84
85
|
)
|
85
86
|
elif self.dataset_info.kind == "edf":
|
86
87
|
self.dataset_info.flats = self.dataset_info.get_reduced_flats()
|
87
88
|
self.dataset_info.darks = self.dataset_info.get_reduced_darks()
|
89
|
+
else:
|
90
|
+
raise TypeError("Unknown dataset format")
|
88
91
|
self.rec_params = self.nabu_config["reconstruction"]
|
89
92
|
|
90
93
|
subsampling_factor, subsampling_start = self.nabu_config["dataset"]["projections_subsampling"]
|
@@ -425,8 +428,10 @@ class ProcessConfig(ProcessConfigBase):
|
|
425
428
|
# Flat-field
|
426
429
|
#
|
427
430
|
if nabu_config["preproc"]["flatfield"]:
|
431
|
+
ff_method = "pca" if nabu_config["preproc"]["flatfield"] == "pca" else "default"
|
428
432
|
tasks.append("flatfield")
|
429
433
|
options["flatfield"] = {
|
434
|
+
"method": ff_method,
|
430
435
|
# Data reader handles binning/subsampling by itself,
|
431
436
|
# but FlatField needs "real" indices (after binning/subsampling)
|
432
437
|
"projs_indices": self.projs_indices(subsampling=False),
|
@@ -434,7 +439,7 @@ class ProcessConfig(ProcessConfigBase):
|
|
434
439
|
"do_flat_distortion": nabu_config["preproc"]["flat_distortion_correction_enabled"],
|
435
440
|
"flat_distortion_params": extract_parameters(nabu_config["preproc"]["flat_distortion_params"]),
|
436
441
|
}
|
437
|
-
normalize_srcurrent = nabu_config["preproc"]["normalize_srcurrent"]
|
442
|
+
normalize_srcurrent = nabu_config["preproc"]["normalize_srcurrent"] and ff_method == "default"
|
438
443
|
radios_srcurrent = None
|
439
444
|
flats_srcurrent = None
|
440
445
|
if normalize_srcurrent:
|
@@ -458,6 +463,7 @@ class ProcessConfig(ProcessConfigBase):
|
|
458
463
|
if len(dataset_info.darks) > 1:
|
459
464
|
self.logger.warning("Cannot do flat-field with more than one reduced dark. Taking the first one.")
|
460
465
|
dataset_info.darks = dataset_info.darks[sorted(dataset_info.darks.keys())[0]]
|
466
|
+
|
461
467
|
#
|
462
468
|
# Spikes filter
|
463
469
|
#
|
@@ -470,6 +476,14 @@ class ProcessConfig(ProcessConfigBase):
|
|
470
476
|
#
|
471
477
|
# Double flat field
|
472
478
|
#
|
479
|
+
# ---- COMPAT ----
|
480
|
+
if nabu_config["preproc"].get("double_flatfield_enabled", False):
|
481
|
+
deprecation_warning(
|
482
|
+
"'double_flatfield_enabled' has been renamed to 'double_flatfield'. Please update your configuration file"
|
483
|
+
)
|
484
|
+
nabu_config["preproc"]["double_flatfield"] = True
|
485
|
+
|
486
|
+
# -------------
|
473
487
|
if nabu_config["preproc"]["double_flatfield"]:
|
474
488
|
tasks.append("double_flatfield")
|
475
489
|
options["double_flatfield"] = {
|
@@ -261,6 +261,9 @@ class FullFieldReconstructor:
|
|
261
261
|
if (self.process_config.dataset_info.detector_tilt or 0) > 15:
|
262
262
|
force_grouped_mode = True
|
263
263
|
msg = "Radios rotation with a large angle needs to process full radios"
|
264
|
+
if self.process_config.processing_options.get("flatfield", {}).get("method", "default") == "pca":
|
265
|
+
force_grouped_mode = True
|
266
|
+
msg = "PCA-Flatfield normalization needs to process full radios"
|
264
267
|
if self.process_config.resume_from_step == "sinogram" and force_grouped_mode:
|
265
268
|
self.logger.warning("Cannot use grouped-radios processing when resuming from sinogram")
|
266
269
|
force_grouped_mode = False
|
@@ -483,7 +486,7 @@ class FullFieldReconstructor:
|
|
483
486
|
{
|
484
487
|
"sub_region": (
|
485
488
|
(0, self.n_angles),
|
486
|
-
(curr_z_min - margin_up, curr_z_max + margin_down),
|
489
|
+
(int(curr_z_min - margin_up), int(curr_z_max + margin_down)),
|
487
490
|
(0, self.chunk_shape[-1]),
|
488
491
|
),
|
489
492
|
"margin": ((margin_up, margin_down), (0, 0)),
|
nabu/pipeline/params.py
CHANGED
@@ -3,6 +3,17 @@ flatfield_modes = {
|
|
3
3
|
"1": True,
|
4
4
|
"false": False,
|
5
5
|
"0": False,
|
6
|
+
# These three should be removed after a while (moved to 'flatfield_loading_mode')
|
7
|
+
"forced": "force-load",
|
8
|
+
"force-load": "force-load",
|
9
|
+
"force-compute": "force-compute",
|
10
|
+
#
|
11
|
+
"pca": "pca",
|
12
|
+
}
|
13
|
+
|
14
|
+
flatfield_loading_mode = {
|
15
|
+
"": "load_if_present",
|
16
|
+
"load_if_present": "load_if_present",
|
6
17
|
"forced": "force-load",
|
7
18
|
"force-load": "force-load",
|
8
19
|
"force-compute": "force-compute",
|
@@ -77,6 +88,7 @@ iterative_methods = {
|
|
77
88
|
optim_algorithms = {
|
78
89
|
"chambolle": "chambolle-pock",
|
79
90
|
"chambollepock": "chambolle-pock",
|
91
|
+
"chambolle-pock": "chambolle-pock",
|
80
92
|
"fista": "fista",
|
81
93
|
}
|
82
94
|
|