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.
Files changed (65) hide show
  1. doc/doc_config.py +32 -0
  2. nabu/__init__.py +1 -1
  3. nabu/app/cast_volume.py +9 -1
  4. nabu/app/cli_configs.py +80 -3
  5. nabu/app/estimate_motion.py +54 -0
  6. nabu/app/multicor.py +2 -4
  7. nabu/app/pcaflats.py +116 -0
  8. nabu/app/reconstruct.py +1 -7
  9. nabu/app/reduce_dark_flat.py +5 -2
  10. nabu/estimation/cor.py +1 -1
  11. nabu/estimation/motion.py +557 -0
  12. nabu/estimation/tests/test_motion_estimation.py +471 -0
  13. nabu/estimation/tilt.py +1 -1
  14. nabu/estimation/translation.py +47 -1
  15. nabu/io/cast_volume.py +100 -13
  16. nabu/io/reader.py +32 -1
  17. nabu/io/tests/test_remove_volume.py +152 -0
  18. nabu/pipeline/config_validators.py +42 -43
  19. nabu/pipeline/estimators.py +255 -0
  20. nabu/pipeline/fullfield/chunked.py +67 -43
  21. nabu/pipeline/fullfield/chunked_cuda.py +5 -2
  22. nabu/pipeline/fullfield/nabu_config.py +20 -14
  23. nabu/pipeline/fullfield/processconfig.py +17 -3
  24. nabu/pipeline/fullfield/reconstruction.py +4 -1
  25. nabu/pipeline/params.py +12 -0
  26. nabu/pipeline/tests/test_estimators.py +240 -3
  27. nabu/preproc/ccd.py +53 -3
  28. nabu/preproc/flatfield.py +306 -1
  29. nabu/preproc/shift.py +3 -1
  30. nabu/preproc/tests/test_pcaflats.py +154 -0
  31. nabu/processing/rotation_cuda.py +3 -1
  32. nabu/processing/tests/test_rotation.py +4 -2
  33. nabu/reconstruction/astra.py +245 -0
  34. nabu/reconstruction/fbp.py +7 -0
  35. nabu/reconstruction/fbp_base.py +31 -7
  36. nabu/reconstruction/fbp_opencl.py +8 -0
  37. nabu/reconstruction/filtering_opencl.py +2 -0
  38. nabu/reconstruction/mlem.py +47 -13
  39. nabu/reconstruction/tests/test_filtering.py +13 -2
  40. nabu/reconstruction/tests/test_mlem.py +91 -62
  41. nabu/resources/dataset_analyzer.py +144 -20
  42. nabu/resources/nxflatfield.py +101 -35
  43. nabu/resources/tests/test_nxflatfield.py +1 -1
  44. nabu/resources/utils.py +16 -10
  45. nabu/stitching/alignment.py +7 -7
  46. nabu/stitching/config.py +22 -20
  47. nabu/stitching/definitions.py +2 -2
  48. nabu/stitching/overlap.py +4 -4
  49. nabu/stitching/sample_normalization.py +5 -5
  50. nabu/stitching/stitcher/post_processing.py +5 -3
  51. nabu/stitching/stitcher/pre_processing.py +24 -20
  52. nabu/stitching/tests/test_config.py +3 -3
  53. nabu/stitching/tests/test_y_preprocessing_stitching.py +11 -8
  54. nabu/stitching/tests/test_z_postprocessing_stitching.py +2 -2
  55. nabu/stitching/tests/test_z_preprocessing_stitching.py +23 -20
  56. nabu/stitching/utils/utils.py +7 -7
  57. nabu/testutils.py +1 -4
  58. nabu/utils.py +13 -0
  59. {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/METADATA +3 -4
  60. {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/RECORD +64 -57
  61. {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/WHEEL +1 -1
  62. {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/entry_points.txt +2 -1
  63. nabu/app/correct_rot.py +0 -62
  64. {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/licenses/LICENSE +0 -0
  65. {nabu-2025.1.0.dev14.dist-info → nabu-2025.1.0rc2.dist-info}/top_level.txt +0 -0
@@ -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
- self._ff_options = self.processing_options["flatfield"].copy()
397
-
398
- # This won't work when resuming from a step (i.e before FF), because we rely on H5Loader()
399
- # which re-compacts the data. When data is re-compacted, we have to know the original radios positions.
400
- # These positions can be saved in the "file_dump" metadata, but it is not loaded for now
401
- # (the process_config object is re-built from scratch every time)
402
- self._ff_options["projs_indices"] = self.chunk_reader.get_frames_indices()
403
-
404
- if self._ff_options.get("normalize_srcurrent", False):
405
- a_start_idx, a_end_idx = self.sub_region[0]
406
- subs = self.process_config.subsampling_factor
407
- self._ff_options["radios_srcurrent"] = self._ff_options["radios_srcurrent"][a_start_idx:a_end_idx:subs]
408
-
409
- distortion_correction = None
410
- if self._ff_options["do_flat_distortion"]:
411
- self.logger.info("Flats distortion correction will be applied")
412
- self.FlatFieldClass = FlatField # no GPU implementation available, force this backend
413
- estimation_kwargs = {}
414
- estimation_kwargs.update(self._ff_options["flat_distortion_params"])
415
- estimation_kwargs["logger"] = self.logger
416
- distortion_correction = DistortionCorrection(
417
- estimation_method="fft-correlation", estimation_kwargs=estimation_kwargs, correction_method="interpn"
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
- # Reduced darks/flats are loaded, but we have to crop them on the current sub-region
421
- # and possibly do apply some pre-processing (binning, distortion correction, ...)
422
- darks_flats = load_darks_flats(
423
- self.dataset_info,
424
- self.sub_region[1:],
425
- processing_func=self._ff_processing_function,
426
- processing_func_args=self._ff_processing_function_args,
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
- # FlatField parameter "radios_indices" must account for subsampling
430
- self.flatfield = self.FlatFieldClass(
431
- self.radios_shape,
432
- flats=darks_flats["flats"],
433
- darks=darks_flats["darks"],
434
- radios_indices=self._ff_options["projs_indices"],
435
- interpolation="linear",
436
- distortion_correction=distortion_correction,
437
- radios_srcurrent=self._ff_options["radios_srcurrent"],
438
- flats_srcurrent=self._ff_options["flats_srcurrent"],
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 and self.flatfield.distortion_correction is not None:
79
- self._when_to_transfer_radios_on_gpu = "flatfield"
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": "1.4",
20
- "help": "Nexus version to use when browsing the HDF5 dataset. Default is 1.0.",
21
- "validator": float_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 to 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.",
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 'projection_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.",
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 - forced or 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.",
65
- "validator": flatfield_enabled_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 radio is correlated with its corresponding flat, in order to determine and correct the flat distortion.",
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": flatfield_enabled_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 radio, i.e ((Nx-1)/2.0, (Ny-1)/2.0).",
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 correspondig sinogram, and 'cor_options' can contain the parameter 'subsampling'.",
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": "0",
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 (ex. 'sinogram_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, ex. sinogram_mydataset.hdf5.",
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