nabu 2024.1.9__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.
Files changed (151) hide show
  1. nabu/__init__.py +1 -1
  2. nabu/app/bootstrap.py +2 -3
  3. nabu/app/cast_volume.py +4 -2
  4. nabu/app/cli_configs.py +5 -0
  5. nabu/app/composite_cor.py +1 -1
  6. nabu/app/create_distortion_map_from_poly.py +5 -6
  7. nabu/app/diag_to_pix.py +7 -19
  8. nabu/app/diag_to_rot.py +14 -29
  9. nabu/app/double_flatfield.py +32 -44
  10. nabu/app/parse_reconstruction_log.py +3 -0
  11. nabu/app/reconstruct.py +53 -15
  12. nabu/app/reconstruct_helical.py +2 -2
  13. nabu/app/stitching.py +27 -13
  14. nabu/app/tests/test_reduce_dark_flat.py +4 -1
  15. nabu/cuda/kernel.py +11 -2
  16. nabu/cuda/processing.py +2 -2
  17. nabu/cuda/src/cone.cu +77 -0
  18. nabu/cuda/src/hierarchical_backproj.cu +271 -0
  19. nabu/cuda/utils.py +0 -6
  20. nabu/estimation/alignment.py +5 -19
  21. nabu/estimation/cor.py +173 -599
  22. nabu/estimation/cor_sino.py +356 -26
  23. nabu/estimation/focus.py +63 -11
  24. nabu/estimation/tests/test_cor.py +124 -58
  25. nabu/estimation/tests/test_focus.py +6 -6
  26. nabu/estimation/tilt.py +2 -1
  27. nabu/estimation/utils.py +5 -33
  28. nabu/io/__init__.py +1 -1
  29. nabu/io/cast_volume.py +1 -1
  30. nabu/io/reader.py +416 -21
  31. nabu/io/tests/test_readers.py +422 -0
  32. nabu/io/tests/test_writers.py +1 -102
  33. nabu/io/writer.py +4 -433
  34. nabu/opencl/kernel.py +14 -3
  35. nabu/opencl/processing.py +8 -0
  36. nabu/pipeline/config_validators.py +5 -2
  37. nabu/pipeline/datadump.py +12 -5
  38. nabu/pipeline/estimators.py +162 -188
  39. nabu/pipeline/fullfield/chunked.py +168 -92
  40. nabu/pipeline/fullfield/chunked_cuda.py +7 -3
  41. nabu/pipeline/fullfield/computations.py +2 -7
  42. nabu/pipeline/fullfield/dataset_validator.py +0 -4
  43. nabu/pipeline/fullfield/nabu_config.py +37 -13
  44. nabu/pipeline/fullfield/processconfig.py +22 -13
  45. nabu/pipeline/fullfield/reconstruction.py +13 -9
  46. nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
  47. nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
  48. nabu/pipeline/helical/helical_reconstruction.py +1 -1
  49. nabu/pipeline/params.py +21 -1
  50. nabu/pipeline/processconfig.py +1 -12
  51. nabu/pipeline/reader.py +146 -0
  52. nabu/pipeline/tests/test_estimators.py +44 -72
  53. nabu/pipeline/utils.py +4 -2
  54. nabu/pipeline/writer.py +10 -2
  55. nabu/preproc/ccd_cuda.py +1 -1
  56. nabu/preproc/ctf.py +14 -7
  57. nabu/preproc/ctf_cuda.py +2 -3
  58. nabu/preproc/double_flatfield.py +5 -12
  59. nabu/preproc/double_flatfield_cuda.py +2 -2
  60. nabu/preproc/flatfield.py +5 -1
  61. nabu/preproc/flatfield_cuda.py +5 -1
  62. nabu/preproc/phase.py +24 -73
  63. nabu/preproc/phase_cuda.py +5 -8
  64. nabu/preproc/tests/test_ctf.py +11 -7
  65. nabu/preproc/tests/test_flatfield.py +67 -122
  66. nabu/preproc/tests/test_paganin.py +54 -30
  67. nabu/processing/azim.py +206 -0
  68. nabu/processing/convolution_cuda.py +1 -1
  69. nabu/processing/fft_cuda.py +15 -17
  70. nabu/processing/histogram.py +2 -0
  71. nabu/processing/histogram_cuda.py +2 -1
  72. nabu/processing/kernel_base.py +3 -0
  73. nabu/processing/muladd_cuda.py +1 -0
  74. nabu/processing/padding_opencl.py +1 -1
  75. nabu/processing/roll_opencl.py +1 -0
  76. nabu/processing/rotation_cuda.py +2 -2
  77. nabu/processing/tests/test_fft.py +17 -10
  78. nabu/processing/unsharp_cuda.py +1 -1
  79. nabu/reconstruction/cone.py +104 -40
  80. nabu/reconstruction/fbp.py +3 -0
  81. nabu/reconstruction/fbp_base.py +7 -2
  82. nabu/reconstruction/filtering.py +20 -7
  83. nabu/reconstruction/filtering_cuda.py +7 -1
  84. nabu/reconstruction/hbp.py +424 -0
  85. nabu/reconstruction/mlem.py +99 -0
  86. nabu/reconstruction/reconstructor.py +2 -0
  87. nabu/reconstruction/rings_cuda.py +19 -19
  88. nabu/reconstruction/sinogram_cuda.py +1 -0
  89. nabu/reconstruction/sinogram_opencl.py +3 -1
  90. nabu/reconstruction/tests/test_cone.py +10 -5
  91. nabu/reconstruction/tests/test_deringer.py +7 -6
  92. nabu/reconstruction/tests/test_fbp.py +124 -10
  93. nabu/reconstruction/tests/test_filtering.py +13 -11
  94. nabu/reconstruction/tests/test_halftomo.py +30 -4
  95. nabu/reconstruction/tests/test_mlem.py +91 -0
  96. nabu/reconstruction/tests/test_reconstructor.py +8 -3
  97. nabu/resources/dataset_analyzer.py +142 -92
  98. nabu/resources/gpu.py +1 -0
  99. nabu/resources/nxflatfield.py +134 -125
  100. nabu/resources/templates/id16a_fluo.conf +42 -0
  101. nabu/resources/tests/test_extract.py +10 -0
  102. nabu/resources/tests/test_nxflatfield.py +2 -2
  103. nabu/stitching/alignment.py +80 -24
  104. nabu/stitching/config.py +105 -68
  105. nabu/stitching/definitions.py +1 -0
  106. nabu/stitching/frame_composition.py +68 -60
  107. nabu/stitching/overlap.py +91 -51
  108. nabu/stitching/single_axis_stitching.py +32 -0
  109. nabu/stitching/slurm_utils.py +6 -6
  110. nabu/stitching/stitcher/__init__.py +0 -0
  111. nabu/stitching/stitcher/base.py +124 -0
  112. nabu/stitching/stitcher/dumper/__init__.py +3 -0
  113. nabu/stitching/stitcher/dumper/base.py +94 -0
  114. nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
  115. nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
  116. nabu/stitching/stitcher/post_processing.py +555 -0
  117. nabu/stitching/stitcher/pre_processing.py +1068 -0
  118. nabu/stitching/stitcher/single_axis.py +484 -0
  119. nabu/stitching/stitcher/stitcher.py +0 -0
  120. nabu/stitching/stitcher/y_stitcher.py +13 -0
  121. nabu/stitching/stitcher/z_stitcher.py +45 -0
  122. nabu/stitching/stitcher_2D.py +278 -0
  123. nabu/stitching/tests/test_config.py +12 -37
  124. nabu/stitching/tests/test_frame_composition.py +33 -59
  125. nabu/stitching/tests/test_overlap.py +149 -7
  126. nabu/stitching/tests/test_utils.py +1 -1
  127. nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
  128. nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
  129. nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
  130. nabu/stitching/utils/__init__.py +1 -0
  131. nabu/stitching/utils/post_processing.py +281 -0
  132. nabu/stitching/utils/tests/test_post-processing.py +21 -0
  133. nabu/stitching/{utils.py → utils/utils.py} +79 -52
  134. nabu/stitching/y_stitching.py +27 -0
  135. nabu/stitching/z_stitching.py +32 -2263
  136. nabu/testutils.py +1 -152
  137. nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
  138. nabu/utils.py +158 -61
  139. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/METADATA +10 -3
  140. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/RECORD +144 -121
  141. nabu/io/tiffwriter_zmm.py +0 -99
  142. nabu/pipeline/fallback_utils.py +0 -149
  143. nabu/pipeline/helical/tests/test_accumulator.py +0 -158
  144. nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
  145. nabu/pipeline/helical/tests/test_strategy.py +0 -61
  146. nabu/pipeline/helical/utils.py +0 -51
  147. nabu/pipeline/tests/test_chunk_reader.py +0 -74
  148. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
  149. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/WHEEL +0 -0
  150. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
  151. {nabu-2024.1.9.dist-info → nabu-2024.2.0.dist-info}/top_level.txt +0 -0
@@ -1,63 +1,20 @@
1
- # coding: utf-8
2
- # /*##########################################################################
3
- #
4
- # Copyright (c) 2016-2017 European Synchrotron Radiation Facility
5
- #
6
- # Permission is hereby granted, free of charge, to any person obtaining a copy
7
- # of this software and associated documentation files (the "Software"), to deal
8
- # in the Software without restriction, including without limitation the rights
9
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- # copies of the Software, and to permit persons to whom the Software is
11
- # furnished to do so, subject to the following conditions:
12
- #
13
- # The above copyright notice and this permission notice shall be included in
14
- # all copies or substantial portions of the Software.
15
- #
16
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
- # THE SOFTWARE.
23
- #
24
- # ###########################################################################*/
25
-
26
- __authors__ = ["H. Payno"]
27
- __license__ = "MIT"
28
- __date__ = "13/05/2022"
29
-
30
-
31
1
  import os
32
- from silx.image.phantomgenerator import PhantomGenerator
33
- from scipy.ndimage import shift as scipy_shift
2
+
3
+ import h5py
34
4
  import numpy
35
5
  import pytest
36
- from nabu.stitching.config import (
37
- PreProcessedZStitchingConfiguration,
38
- PostProcessedZStitchingConfiguration,
39
- )
40
- from nabu.utils import Progress
41
- from nabu.stitching.config import KEY_IMG_REG_METHOD, NormalizationBySample
42
- from nabu.stitching.overlap import ZStichOverlapKernel, OverlapStitchingStrategy
43
- from nabu.stitching.z_stitching import (
44
- PostProcessZStitcher,
45
- PreProcessZStitcher,
46
- stitch_vertically_raw_frames,
47
- ZStitcher,
48
- )
49
- from nxtomo.nxobject.nxdetector import ImageKey
50
- from nxtomo.utils.transformation import UDDetTransformation, LRDetTransformation
51
- from nxtomo.application.nxtomo import NXtomo
52
- from nabu.stitching.alignment import AlignmentAxis1, AlignmentAxis2
6
+ from tqdm import tqdm
7
+ from silx.image.phantomgenerator import PhantomGenerator
8
+ from tomoscan.esrf.volume import EDFVolume, HDF5Volume
9
+ from tomoscan.esrf.volume.tiffvolume import TIFFVolume, has_tifffile
53
10
  from tomoscan.factory import Factory as TomoscanFactory
54
11
  from tomoscan.utils.volume import concatenate as concatenate_volumes
55
- from tomoscan.esrf.scan.nxtomoscan import NXtomoScan
56
- from tomoscan.esrf.volume import HDF5Volume, EDFVolume
57
- from tomoscan.esrf.volume.jp2kvolume import JP2KVolume, has_minimal_openjpeg
58
- from tomoscan.esrf.volume.tiffvolume import TIFFVolume, has_tifffile
12
+
13
+ from nabu.stitching.alignment import AlignmentAxis1, AlignmentAxis2
14
+ from nabu.stitching.config import NormalizationBySample, PostProcessedZStitchingConfiguration
15
+ from nabu.stitching.overlap import OverlapStitchingStrategy
59
16
  from nabu.stitching.utils import ShiftAlgorithm
60
- import h5py
17
+ from nabu.stitching.z_stitching import PostProcessZStitcher, PostProcessZStitcherNoDD
61
18
 
62
19
 
63
20
  strategies_to_test_weights = (
@@ -84,365 +41,6 @@ def build_raw_volume():
84
41
  return raw_volume
85
42
 
86
43
 
87
- @pytest.mark.parametrize("strategy", strategies_to_test_weights)
88
- def test_overlap_z_stitcher(strategy):
89
- frame_width = 128
90
- frame_height = frame_width
91
- frame_1 = PhantomGenerator.get2DPhantomSheppLogan(n=frame_width)
92
- stitcher = ZStichOverlapKernel(
93
- stitching_strategy=strategy,
94
- overlap_size=frame_height,
95
- frame_width=128,
96
- )
97
- stitched_frame = stitcher.stitch(frame_1, frame_1)[0]
98
- assert stitched_frame.shape == (frame_height, frame_width)
99
- # check result is close to the expected one
100
- numpy.testing.assert_allclose(frame_1, stitched_frame, atol=10e-10)
101
-
102
- # check sum of weights ~ 1.0
103
- numpy.testing.assert_allclose(
104
- stitcher.weights_img_1 + stitcher.weights_img_2,
105
- numpy.ones_like(stitcher.weights_img_1),
106
- )
107
-
108
-
109
- @pytest.mark.parametrize("dtype", (numpy.float16, numpy.float32))
110
- def test_z_stitch_raw_frames(dtype):
111
- """
112
- test z_stitch_raw_frames: insure a stitching with 3 frames and different overlap can be done
113
- """
114
- ref_frame_width = 256
115
- frame_ref = PhantomGenerator.get2DPhantomSheppLogan(n=ref_frame_width).astype(dtype)
116
-
117
- # split the frame into several part
118
- frame_1 = frame_ref[0:100]
119
- frame_2 = frame_ref[80:164]
120
- frame_3 = frame_ref[154:]
121
-
122
- kernel_1 = ZStichOverlapKernel(frame_width=ref_frame_width, overlap_size=20)
123
- kernel_2 = ZStichOverlapKernel(frame_width=ref_frame_width, overlap_size=10)
124
-
125
- stitched = stitch_vertically_raw_frames(
126
- frames=(frame_1, frame_2, frame_3),
127
- output_dtype=dtype,
128
- overlap_kernels=(kernel_1, kernel_2),
129
- raw_frames_compositions=None,
130
- overlap_frames_compositions=None,
131
- key_lines=(
132
- (
133
- 90, # frame_1 height - kernel_1 height / 2.0
134
- 10, # kernel_1 height / 2.0
135
- ),
136
- (
137
- 79, # frame_2 height - kernel_2 height / 2.0 ou 102-20 ?
138
- 5, # kernel_2 height / 2.0
139
- ),
140
- ),
141
- )
142
-
143
- assert stitched.shape == frame_ref.shape
144
- numpy.testing.assert_array_almost_equal(frame_ref, stitched)
145
-
146
-
147
- def test_z_stitch_raw_frames_2():
148
- """
149
- test z_stitch_raw_frames: insure a stitching with 3 frames and different overlap can be done
150
- """
151
- ref_frame_width = 256
152
- frame_ref = PhantomGenerator.get2DPhantomSheppLogan(n=ref_frame_width).astype(numpy.float32)
153
-
154
- # split the frame into several part
155
- frame_1 = frame_ref.copy()
156
- frame_2 = frame_ref.copy()
157
- frame_3 = frame_ref.copy()
158
-
159
- kernel_1 = ZStichOverlapKernel(frame_width=ref_frame_width, overlap_size=10)
160
- kernel_2 = ZStichOverlapKernel(frame_width=ref_frame_width, overlap_size=10)
161
-
162
- stitched = stitch_vertically_raw_frames(
163
- frames=(frame_1, frame_2, frame_3),
164
- output_dtype=numpy.float32,
165
- overlap_kernels=(kernel_1, kernel_2),
166
- raw_frames_compositions=None,
167
- overlap_frames_compositions=None,
168
- key_lines=((20, 20), (105, 105)),
169
- )
170
-
171
- assert stitched.shape == frame_ref.shape
172
- numpy.testing.assert_array_almost_equal(frame_ref, stitched)
173
-
174
-
175
- _stitching_configurations = (
176
- # simple case where shifts are provided
177
- {
178
- "n_proj": 4,
179
- "raw_pos": ((0, 0, 0), (-90, 0, 0), (-180, 0, 0)), # requested shift to
180
- "input_pos": ((0, 0, 0), (-90, 0, 0), (-180, 0, 0)), # requested shift to
181
- "raw_shifts": ((0, 0), (-90, 0), (-180, 0)),
182
- },
183
- # simple case where shift is found from z position
184
- {
185
- "n_proj": 4,
186
- "raw_pos": ((90, 0, 0), (0, 0, 0), (-90, 0, 0)),
187
- "input_pos": ((90, 0, 0), (0, 0, 0), (-90, 0, 0)),
188
- "check_bb": ((40, 140), (-50, 50), (-140, -40)),
189
- "axis_0_params": {
190
- KEY_IMG_REG_METHOD: ShiftAlgorithm.NONE,
191
- },
192
- "axis_2_params": {
193
- KEY_IMG_REG_METHOD: ShiftAlgorithm.NONE,
194
- },
195
- "raw_shifts": ((0, 0), (-90, 0), (-180, 0)),
196
- },
197
- )
198
-
199
-
200
- @pytest.mark.parametrize("configuration", _stitching_configurations)
201
- @pytest.mark.parametrize("dtype", (numpy.float32, numpy.int16))
202
- def test_PreProcessZStitcher(tmp_path, dtype, configuration):
203
- """
204
- test PreProcessZStitcher class and insure a full stitching can be done automatically.
205
- """
206
- n_proj = configuration["n_proj"]
207
- ref_frame_width = 280
208
- raw_frame_height = 100
209
- ref_frame = PhantomGenerator.get2DPhantomSheppLogan(n=ref_frame_width).astype(dtype) * 256.0
210
-
211
- # add some mark for image registration
212
- ref_frame[:, 96] = -3.2
213
- ref_frame[:, 125] = 9.1
214
- ref_frame[:, 165] = 4.4
215
- ref_frame[:, 200] = -2.5
216
- # create raw data
217
- frame_0_shift, frame_1_shift, frame_2_shift = configuration["raw_shifts"]
218
- frame_0 = scipy_shift(ref_frame, shift=frame_0_shift)[:raw_frame_height]
219
- frame_1 = scipy_shift(ref_frame, shift=frame_1_shift)[:raw_frame_height]
220
- frame_2 = scipy_shift(ref_frame, shift=frame_2_shift)[:raw_frame_height]
221
-
222
- frames = frame_0, frame_1, frame_2
223
- frame_0_input_pos, frame_1_input_pos, frame_2_input_pos = configuration["input_pos"]
224
- frame_0_raw_pos, frame_1_raw_pos, frame_2_raw_pos = configuration["raw_pos"]
225
-
226
- # create a Nxtomo for each of those raw data
227
- raw_data_dir = tmp_path / "raw_data"
228
- raw_data_dir.mkdir()
229
- output_dir = tmp_path / "output_dir"
230
- output_dir.mkdir()
231
- z_position = (
232
- frame_0_raw_pos[0],
233
- frame_1_raw_pos[0],
234
- frame_2_raw_pos[0],
235
- )
236
- scans = []
237
- for (i_frame, frame), z_pos in zip(enumerate(frames), z_position):
238
- nx_tomo = NXtomo()
239
- nx_tomo.sample.z_translation = [z_pos] * n_proj
240
- nx_tomo.sample.rotation_angle = numpy.linspace(0, 180, num=n_proj, endpoint=False)
241
- nx_tomo.instrument.detector.image_key_control = [ImageKey.PROJECTION] * n_proj
242
- nx_tomo.instrument.detector.x_pixel_size = 1.0
243
- nx_tomo.instrument.detector.y_pixel_size = 1.0
244
- nx_tomo.instrument.detector.distance = 2.3
245
- nx_tomo.energy = 19.2
246
- nx_tomo.instrument.detector.data = numpy.asarray([frame] * n_proj)
247
-
248
- file_path = os.path.join(raw_data_dir, f"nxtomo_{i_frame}.nx")
249
- entry = f"entry000{i_frame}"
250
- nx_tomo.save(file_path=file_path, data_path=entry)
251
- scans.append(NXtomoScan(scan=file_path, entry=entry))
252
-
253
- # if requested: check bounding box
254
- check_bb = configuration.get("check_bb", None)
255
- if check_bb is not None:
256
- for scan, expected_bb in zip(scans, check_bb):
257
- assert scan.get_bounding_box(axis="z") == expected_bb
258
- output_file_path = os.path.join(output_dir, "stitched.nx")
259
- output_data_path = "stitched"
260
- z_stich_config = PreProcessedZStitchingConfiguration(
261
- stitching_strategy=OverlapStitchingStrategy.LINEAR_WEIGHTS,
262
- overwrite_results=True,
263
- axis_0_pos_px=(
264
- frame_0_input_pos[0],
265
- frame_1_input_pos[0],
266
- frame_2_input_pos[0],
267
- ),
268
- axis_1_pos_px=(
269
- frame_0_input_pos[1],
270
- frame_1_input_pos[1],
271
- frame_2_input_pos[1],
272
- ),
273
- axis_2_pos_px=(
274
- frame_0_input_pos[2],
275
- frame_1_input_pos[2],
276
- frame_2_input_pos[2],
277
- ),
278
- axis_0_pos_mm=None,
279
- axis_1_pos_mm=None,
280
- axis_2_pos_mm=None,
281
- input_scans=scans,
282
- output_file_path=output_file_path,
283
- output_data_path=output_data_path,
284
- axis_0_params=configuration.get("axis_0_params", {}),
285
- axis_1_params=configuration.get("axis_1_params", {}),
286
- axis_2_params=configuration.get("axis_2_params", {}),
287
- output_nexus_version=None,
288
- slices=None,
289
- slurm_config=None,
290
- slice_for_cross_correlation="middle",
291
- pixel_size=None,
292
- )
293
- stitcher = PreProcessZStitcher(z_stich_config)
294
- output_identifier = stitcher.stitch()
295
- assert output_identifier.file_path == output_file_path
296
- assert output_identifier.data_path == output_data_path
297
-
298
- created_nx_tomo = NXtomo().load(
299
- file_path=output_identifier.file_path,
300
- data_path=output_identifier.data_path,
301
- detector_data_as="as_numpy_array",
302
- )
303
-
304
- assert created_nx_tomo.instrument.detector.data.ndim == 3
305
- mean_abs_error = configuration.get("mean_abs_error", None)
306
- if mean_abs_error is not None:
307
- assert (
308
- numpy.mean(numpy.abs(ref_frame - created_nx_tomo.instrument.detector.data[0, :ref_frame_width, :]))
309
- < mean_abs_error
310
- )
311
- else:
312
- numpy.testing.assert_array_almost_equal(
313
- ref_frame, created_nx_tomo.instrument.detector.data[0, :ref_frame_width, :]
314
- )
315
-
316
- # check also other metadata are here
317
- assert created_nx_tomo.instrument.detector.distance.value == 2.3
318
- assert created_nx_tomo.energy.value == 19.2
319
- numpy.testing.assert_array_equal(
320
- created_nx_tomo.instrument.detector.image_key_control,
321
- numpy.asarray([ImageKey.PROJECTION.PROJECTION] * n_proj),
322
- )
323
-
324
- # check configuration has been saved
325
- with h5py.File(output_identifier.file_path, mode="r") as h5f:
326
- assert "stitching_configuration" in h5f[output_identifier.data_path]
327
-
328
-
329
- slices_to_test_pre = (
330
- {
331
- "slices": (None,),
332
- "complete": True,
333
- },
334
- {
335
- "slices": (("first",), ("middle",), ("last",)),
336
- "complete": False,
337
- },
338
- {
339
- "slices": ((0, 1, 2), slice(3, -1, 1)),
340
- "complete": True,
341
- },
342
- )
343
-
344
-
345
- @pytest.mark.parametrize("configuration_dist", slices_to_test_pre)
346
- def test_DistributePreProcessZStitcher(tmp_path, configuration_dist):
347
- slices = configuration_dist["slices"]
348
- complete = configuration_dist["complete"]
349
-
350
- n_projs = 100
351
- raw_data = numpy.arange(100 * 128 * 128).reshape((100, 128, 128))
352
-
353
- # create raw data
354
- frame_0 = raw_data[:, 60:]
355
- assert frame_0.ndim == 3
356
- frame_0_pos = 40
357
- frame_1 = raw_data[:, 0:80]
358
- assert frame_1.ndim == 3
359
- frame_1_pos = 94
360
- frames = (frame_0, frame_1)
361
- z_positions = (frame_0_pos, frame_1_pos)
362
-
363
- # create a Nxtomo for each of those raw data
364
- raw_data_dir = tmp_path / "raw_data"
365
- raw_data_dir.mkdir()
366
- output_dir = tmp_path / "output_dir"
367
- output_dir.mkdir()
368
-
369
- scans = []
370
- for (i_frame, frame), z_pos in zip(enumerate(frames), z_positions):
371
- nx_tomo = NXtomo()
372
- nx_tomo.sample.z_translation = [z_pos] * n_projs
373
- nx_tomo.sample.rotation_angle = numpy.linspace(0, 180, num=n_projs, endpoint=False)
374
- nx_tomo.instrument.detector.image_key_control = [ImageKey.PROJECTION] * n_projs
375
- nx_tomo.instrument.detector.x_pixel_size = 1.0
376
- nx_tomo.instrument.detector.y_pixel_size = 1.0
377
- nx_tomo.instrument.detector.distance = 2.3
378
- nx_tomo.energy = 19.2
379
- nx_tomo.instrument.detector.data = frame
380
-
381
- file_path = os.path.join(raw_data_dir, f"nxtomo_{i_frame}.nx")
382
- entry = f"entry000{i_frame}"
383
- nx_tomo.save(file_path=file_path, data_path=entry)
384
- scans.append(NXtomoScan(scan=file_path, entry=entry))
385
-
386
- stitched_nx_tomo = []
387
- for s in slices:
388
- output_file_path = os.path.join(output_dir, "stitched_section.nx")
389
- output_data_path = f"stitched_{s}"
390
- z_stich_config = PreProcessedZStitchingConfiguration(
391
- axis_0_pos_px=z_positions,
392
- axis_1_pos_px=None,
393
- axis_2_pos_px=(0, 0),
394
- axis_0_pos_mm=None,
395
- axis_1_pos_mm=None,
396
- axis_2_pos_mm=None,
397
- axis_0_params={},
398
- axis_1_params={},
399
- axis_2_params={},
400
- stitching_strategy=OverlapStitchingStrategy.CLOSEST,
401
- overwrite_results=True,
402
- input_scans=scans,
403
- output_file_path=output_file_path,
404
- output_data_path=output_data_path,
405
- output_nexus_version=None,
406
- slices=s,
407
- slurm_config=None,
408
- slice_for_cross_correlation="middle",
409
- pixel_size=None,
410
- )
411
- stitcher = PreProcessZStitcher(z_stich_config)
412
- output_identifier = stitcher.stitch()
413
- assert output_identifier.file_path == output_file_path
414
- assert output_identifier.data_path == output_data_path
415
-
416
- created_nx_tomo = NXtomo().load(
417
- file_path=output_identifier.file_path,
418
- data_path=output_identifier.data_path,
419
- detector_data_as="as_numpy_array",
420
- )
421
- stitched_nx_tomo.append(created_nx_tomo)
422
- assert len(stitched_nx_tomo) == len(slices)
423
- final_nx_tomo = NXtomo.concatenate(stitched_nx_tomo)
424
- assert isinstance(final_nx_tomo.instrument.detector.data, numpy.ndarray)
425
- final_nx_tomo.save(
426
- file_path=os.path.join(output_dir, "final_stitched.nx"),
427
- data_path="entry0000",
428
- )
429
-
430
- if complete:
431
- len(final_nx_tomo.instrument.detector.data) == 128
432
- # test middle
433
- numpy.testing.assert_array_almost_equal(raw_data[1], final_nx_tomo.instrument.detector.data[1, :, :])
434
- else:
435
- len(final_nx_tomo.instrument.detector.data) == 3
436
- # test middle
437
- numpy.testing.assert_array_almost_equal(raw_data[49], final_nx_tomo.instrument.detector.data[1, :, :])
438
- # in the case of first, middle and last frames
439
- # test first
440
- numpy.testing.assert_array_almost_equal(raw_data[0], final_nx_tomo.instrument.detector.data[0, :, :])
441
-
442
- # test last
443
- numpy.testing.assert_array_almost_equal(raw_data[-1], final_nx_tomo.instrument.detector.data[-1, :, :])
444
-
445
-
446
44
  _VOL_CLASSES_TO_TEST_FOR_POSTPROC_STITCHING = [HDF5Volume, EDFVolume]
447
45
  # avoid testing glymur because doesn't handle float
448
46
  # if has_minimal_openjpeg:
@@ -451,29 +49,10 @@ if has_tifffile:
451
49
  _VOL_CLASSES_TO_TEST_FOR_POSTPROC_STITCHING.append(TIFFVolume)
452
50
 
453
51
 
454
- @pytest.mark.parametrize("progress", (None, Progress("z-stitching")))
455
- @pytest.mark.parametrize("volume_class", (_VOL_CLASSES_TO_TEST_FOR_POSTPROC_STITCHING))
456
- def test_PostProcessZStitcher(
457
- tmp_path,
458
- volume_class,
459
- progress,
460
- ):
461
- """
462
- test PreProcessZStitcher class and insure a full stitching can be done automatically.
463
-
464
- :param bool clear_input_volumes_data: if True save the volume then clear volume.data (used to check internal management of loading volumes - used to check behavior with HDF5)
465
- :param volume_class: class to be used (same class for input and output for now)
466
- :param axis_0_pos: position of the different TomoObj along axis 0 (Also know as z axis)
467
- """
52
+ def build_volumes(output_dir: str, volume_class):
468
53
  # create some random data.
469
54
  raw_volume = build_raw_volume()
470
55
 
471
- # create folder to save data (and debug)
472
- raw_data_dir = tmp_path / "raw_data"
473
- raw_data_dir.mkdir()
474
- output_dir = tmp_path / "output_dir"
475
- output_dir.mkdir()
476
-
477
56
  # create a simple case where the volume have 10 voxel of overlap and a height (z) of 30 Voxels, 40 and 30 Voxels
478
57
  vol_1_constructor_params = {
479
58
  "data": raw_volume[0:30, :, :],
@@ -511,29 +90,55 @@ def test_PostProcessZStitcher(
511
90
  },
512
91
  }
513
92
 
514
- raw_volumes = []
93
+ volumes = []
515
94
  axis_0_positions = []
516
95
  for i_vol, vol_params in enumerate([vol_1_constructor_params, vol_2_constructor_params, vol_3_constructor_params]):
517
96
  if volume_class == HDF5Volume:
518
97
  vol_params.update(
519
98
  {
520
- "file_path": os.path.join(raw_data_dir, f"raw_volume_{i_vol}.hdf5"),
99
+ "file_path": os.path.join(output_dir, f"raw_volume_{i_vol}.hdf5"),
521
100
  "data_path": "volume",
522
101
  }
523
102
  )
524
103
  else:
525
104
  vol_params.update(
526
105
  {
527
- "folder": os.path.join(raw_data_dir, f"raw_volume_{i_vol}"),
106
+ "folder": os.path.join(output_dir, f"raw_volume_{i_vol}"),
528
107
  }
529
108
  )
530
109
  axis_0_positions.append(vol_params["metadata"]["processing_options"]["reconstruction"]["position"][0])
531
110
 
532
111
  volume = volume_class(**vol_params)
533
112
  volume.save()
534
- raw_volumes.append(volume)
113
+ volumes.append(volume)
114
+ return volumes, axis_0_positions, raw_volume
535
115
 
536
- volume_1, volume_2, volume_3 = raw_volumes
116
+
117
+ @pytest.mark.parametrize("progress", (None, "with_tqdm"))
118
+ @pytest.mark.parametrize("volume_class", (_VOL_CLASSES_TO_TEST_FOR_POSTPROC_STITCHING))
119
+ def test_PostProcessZStitcher(
120
+ tmp_path,
121
+ volume_class,
122
+ progress,
123
+ ):
124
+ """
125
+ test PreProcessZStitcher class and insure a full stitching can be done automatically.
126
+
127
+ :param bool clear_input_volumes_data: if True save the volume then clear volume.data (used to check internal management of loading volumes - used to check behavior with HDF5)
128
+ :param volume_class: class to be used (same class for input and output for now)
129
+ :param axis_0_pos: position of the different TomoObj along axis 0 (Also know as z axis)
130
+ """
131
+ if progress == "with_tqdm":
132
+ progress = tqdm(total=100)
133
+
134
+ # create folder to save data (and debug)
135
+ raw_data_dir = tmp_path / "raw_data"
136
+ raw_data_dir.mkdir()
137
+ output_dir = tmp_path / "output_dir"
138
+ output_dir.mkdir()
139
+
140
+ volumes, axis_0_positions, raw_volume = build_volumes(output_dir=raw_data_dir, volume_class=volume_class)
141
+ volume_1, volume_2, volume_3 = volumes
537
142
 
538
143
  output_volume = HDF5Volume(
539
144
  file_path=os.path.join(output_dir, "stitched_volume.hdf5"),
@@ -575,7 +180,7 @@ def test_PostProcessZStitcher(
575
180
  numpy.testing.assert_array_almost_equal(raw_volume, output_volume.data)
576
181
 
577
182
  metadata = output_volume.metadata
578
- assert metadata["program"] == "nabu-stitching"
183
+ assert "about" in metadata
579
184
  assert "configuration" in metadata
580
185
  assert output_volume.position[0] == -60.0
581
186
  assert output_volume.pixel_size == (1.0, 1.0, 1.0)
@@ -668,10 +273,10 @@ def test_DistributePostProcessZStitcher(tmp_path, configuration_dist, flip_ud):
668
273
  ),
669
274
  axis_0_pos_mm=None,
670
275
  axis_0_params={},
671
- axis_1_pos_px=None,
276
+ axis_1_pos_px=(0, 0),
672
277
  axis_1_pos_mm=None,
673
278
  axis_1_params={},
674
- axis_2_pos_px=(0, 0),
279
+ axis_2_pos_px=None,
675
280
  axis_2_pos_mm=None,
676
281
  axis_2_params={},
677
282
  overwrite_results=True,
@@ -701,123 +306,6 @@ def test_DistributePostProcessZStitcher(tmp_path, configuration_dist, flip_ud):
701
306
  )
702
307
 
703
308
 
704
- def test_get_overlap_areas():
705
- """test get_overlap_areas function"""
706
- f_upper = numpy.linspace(7, 15, num=9, endpoint=True)
707
- f_lower = numpy.linspace(0, 12, num=13, endpoint=True)
708
-
709
- o_1, o_2 = ZStitcher.get_overlap_areas(
710
- upper_frame=f_upper,
711
- lower_frame=f_lower,
712
- upper_frame_key_line=3,
713
- lower_frame_key_line=10,
714
- overlap_size=4,
715
- stitching_axis=0,
716
- )
717
-
718
- numpy.testing.assert_array_equal(o_1, o_2)
719
- numpy.testing.assert_array_equal(o_1, numpy.linspace(8, 11, num=4, endpoint=True))
720
-
721
-
722
- def test_frame_flip(tmp_path):
723
- """check it with some NXtomo fliped"""
724
- ref_frame_width = 280
725
- n_proj = 10
726
- raw_frame_width = 100
727
- ref_frame = PhantomGenerator.get2DPhantomSheppLogan(n=ref_frame_width).astype(numpy.float32) * 256.0
728
- # create raw data
729
- frame_0_shift = (0, 0)
730
- frame_1_shift = (-90, 0)
731
- frame_2_shift = (-180, 0)
732
-
733
- frame_0 = scipy_shift(ref_frame, shift=frame_0_shift)[:raw_frame_width]
734
- frame_1 = scipy_shift(ref_frame, shift=frame_1_shift)[:raw_frame_width]
735
- frame_2 = scipy_shift(ref_frame, shift=frame_2_shift)[:raw_frame_width]
736
- frames = frame_0, frame_1, frame_2
737
-
738
- x_flips = [False, True, True]
739
- y_flips = [False, False, True]
740
-
741
- def apply_flip(args):
742
- frame, flip_x, flip_y = args
743
- if flip_x:
744
- frame = numpy.fliplr(frame)
745
- if flip_y:
746
- frame = numpy.flipud(frame)
747
- return frame
748
-
749
- frames = map(apply_flip, zip(frames, x_flips, y_flips))
750
-
751
- # create a Nxtomo for each of those raw data
752
- raw_data_dir = tmp_path / "raw_data"
753
- raw_data_dir.mkdir()
754
- output_dir = tmp_path / "output_dir"
755
- output_dir.mkdir()
756
- z_position = (90, 0, -90)
757
-
758
- scans = []
759
- for (i_frame, frame), z_pos, x_flip, y_flip in zip(enumerate(frames), z_position, x_flips, y_flips):
760
- nx_tomo = NXtomo()
761
- nx_tomo.sample.z_translation = [z_pos] * n_proj
762
- nx_tomo.sample.rotation_angle = numpy.linspace(0, 180, num=n_proj, endpoint=False)
763
- nx_tomo.instrument.detector.image_key_control = [ImageKey.PROJECTION] * n_proj
764
- nx_tomo.instrument.detector.x_pixel_size = 1.0
765
- nx_tomo.instrument.detector.y_pixel_size = 1.0
766
- nx_tomo.instrument.detector.distance = 2.3
767
- if x_flip:
768
- nx_tomo.instrument.detector.transformations.add_transformation(LRDetTransformation())
769
- if y_flip:
770
- nx_tomo.instrument.detector.transformations.add_transformation(UDDetTransformation())
771
- nx_tomo.energy = 19.2
772
- nx_tomo.instrument.detector.data = numpy.asarray([frame] * n_proj)
773
-
774
- file_path = os.path.join(raw_data_dir, f"nxtomo_{i_frame}.nx")
775
- entry = f"entry000{i_frame}"
776
- nx_tomo.save(file_path=file_path, data_path=entry)
777
- scans.append(NXtomoScan(scan=file_path, entry=entry))
778
-
779
- output_file_path = os.path.join(output_dir, "stitched.nx")
780
- output_data_path = "stitched"
781
- assert len(scans) == 3
782
- z_stich_config = PreProcessedZStitchingConfiguration(
783
- axis_0_pos_px=(0, -90, -180),
784
- axis_1_pos_px=None,
785
- axis_2_pos_px=(0, 0, 0),
786
- axis_0_pos_mm=None,
787
- axis_1_pos_mm=None,
788
- axis_2_pos_mm=None,
789
- axis_0_params={},
790
- axis_1_params={},
791
- axis_2_params={},
792
- stitching_strategy=OverlapStitchingStrategy.LINEAR_WEIGHTS,
793
- overwrite_results=True,
794
- input_scans=scans,
795
- output_file_path=output_file_path,
796
- output_data_path=output_data_path,
797
- output_nexus_version=None,
798
- slices=None,
799
- slurm_config=None,
800
- slice_for_cross_correlation="middle",
801
- pixel_size=None,
802
- )
803
- stitcher = PreProcessZStitcher(z_stich_config)
804
- output_identifier = stitcher.stitch()
805
- assert output_identifier.file_path == output_file_path
806
- assert output_identifier.data_path == output_data_path
807
-
808
- created_nx_tomo = NXtomo().load(
809
- file_path=output_identifier.file_path,
810
- data_path=output_identifier.data_path,
811
- detector_data_as="as_numpy_array",
812
- )
813
-
814
- assert created_nx_tomo.instrument.detector.data.ndim == 3
815
- # insure flipping has been taking into account
816
- numpy.testing.assert_array_almost_equal(ref_frame, created_nx_tomo.instrument.detector.data[0, :ref_frame_width, :])
817
-
818
- assert len(created_nx_tomo.instrument.detector.transformations) == 0
819
-
820
-
821
309
  @pytest.mark.parametrize("alignment_axis_2", ("left", "right", "center"))
822
310
  def test_vol_z_stitching_with_alignment_axis_2(tmp_path, alignment_axis_2):
823
311
  """
@@ -1050,8 +538,6 @@ def test_normalization_by_sample(tmp_path):
1050
538
  simple test of a volume stitching.
1051
539
  Raw volumes have 'extra' values (+2, +5, +9) that must be removed at the end thanks to the normalization
1052
540
  """
1053
- from copy import deepcopy
1054
-
1055
541
  raw_volume = build_raw_volume()
1056
542
  # create folder to save data (and debug)
1057
543
  raw_data_dir = tmp_path / "raw_data"
@@ -1163,7 +649,127 @@ def test_normalization_by_sample(tmp_path):
1163
649
  numpy.testing.assert_array_almost_equal(raw_volume, output_volume.data)
1164
650
 
1165
651
  metadata = output_volume.metadata
1166
- assert metadata["program"] == "nabu-stitching"
1167
652
  assert "configuration" in metadata
653
+ assert "about" in metadata
654
+ assert metadata["about"]["program"] == "nabu-stitching"
1168
655
  assert output_volume.position[0] == -60.0
1169
656
  assert output_volume.pixel_size == (1.0, 1.0, 1.0)
657
+
658
+
659
+ @pytest.mark.parametrize("data_duplication", (True, False))
660
+ def test_data_duplication(tmp_path, data_duplication):
661
+ """
662
+ Test that the post-processing stitching can be done without duplicating data.
663
+ And also making sure avoid data duplication can handle frame flips
664
+ """
665
+ raw_volume = build_raw_volume()
666
+
667
+ # create folder to save data (and debug)
668
+ raw_data_dir = tmp_path / "raw_data"
669
+ raw_data_dir.mkdir()
670
+ output_dir = tmp_path / "output_dir"
671
+ output_dir.mkdir()
672
+
673
+ volume_1 = HDF5Volume(
674
+ data=raw_volume[0:30],
675
+ metadata={
676
+ "processing_options": {
677
+ "reconstruction": {
678
+ "position": (-15.0, 0.0, 0.0),
679
+ "voxel_size_cm": (100.0, 100.0, 100.0),
680
+ }
681
+ },
682
+ },
683
+ file_path=os.path.join(raw_data_dir, f"raw_volume_1.hdf5"),
684
+ data_path="volume",
685
+ )
686
+
687
+ volume_2 = HDF5Volume(
688
+ data=raw_volume[20:80],
689
+ metadata={
690
+ "processing_options": {
691
+ "reconstruction": {
692
+ "position": (-50.0, 0.0, 0.0),
693
+ "voxel_size_cm": (100.0, 100.0, 100.0),
694
+ }
695
+ },
696
+ },
697
+ file_path=os.path.join(raw_data_dir, f"raw_volume_2.hdf5"),
698
+ data_path="volume",
699
+ )
700
+
701
+ volume_3 = HDF5Volume(
702
+ data=raw_volume[60:],
703
+ metadata={
704
+ "processing_options": {
705
+ "reconstruction": {
706
+ "position": (-90.0, 0.0, 0.0),
707
+ "voxel_size_cm": (100.0, 100.0, 100.0),
708
+ }
709
+ },
710
+ },
711
+ file_path=os.path.join(raw_data_dir, f"raw_volume_3.hdf5"),
712
+ data_path="volume",
713
+ )
714
+
715
+ for volume in (volume_1, volume_2, volume_3):
716
+ volume.save()
717
+ volume.clear_cache()
718
+
719
+ output_volume = HDF5Volume(
720
+ file_path=os.path.join(output_dir, "stitched_volume.hdf5"),
721
+ data_path="stitched_volume",
722
+ )
723
+
724
+ z_stich_config = PostProcessedZStitchingConfiguration(
725
+ stitching_strategy=OverlapStitchingStrategy.CLOSEST,
726
+ overwrite_results=True,
727
+ input_volumes=(volume_1, volume_2, volume_3),
728
+ output_volume=output_volume,
729
+ slices=None,
730
+ slurm_config=None,
731
+ axis_0_pos_px=None,
732
+ axis_0_pos_mm=None,
733
+ axis_0_params={"img_reg_method": ShiftAlgorithm.NONE},
734
+ axis_1_pos_px=None,
735
+ axis_1_pos_mm=None,
736
+ axis_1_params={"img_reg_method": ShiftAlgorithm.NONE},
737
+ axis_2_pos_px=None,
738
+ axis_2_pos_mm=None,
739
+ axis_2_params={"img_reg_method": ShiftAlgorithm.NONE},
740
+ slice_for_cross_correlation="middle",
741
+ voxel_size=None,
742
+ duplicate_data=data_duplication,
743
+ )
744
+
745
+ if data_duplication:
746
+ stitcher = PostProcessZStitcher(z_stich_config, progress=None)
747
+ else:
748
+ stitcher = PostProcessZStitcherNoDD(z_stich_config, progress=None)
749
+ output_identifier = stitcher.stitch()
750
+
751
+ assert output_identifier.file_path == output_volume.file_path
752
+ assert output_identifier.data_path == output_volume.data_path
753
+
754
+ output_volume.data = None
755
+ output_volume.metadata = None
756
+ output_volume.load_data(store=True)
757
+ output_volume.load_metadata(store=True)
758
+
759
+ assert raw_volume.shape == output_volume.data.shape
760
+ numpy.testing.assert_almost_equal(raw_volume.data, output_volume.data)
761
+
762
+ with h5py.File(output_volume.file_path, mode="r") as h5f:
763
+ if data_duplication:
764
+ assert f"{output_volume.data_path}/stitching_regions" not in h5f
765
+ assert not h5f[f"{output_volume.data_path}/results/data"].is_virtual
766
+ else:
767
+ assert f"{output_volume.data_path}/stitching_regions" in h5f
768
+ assert h5f[f"{output_volume.data_path}/results/data"].is_virtual
769
+
770
+ if not data_duplication:
771
+ # make sure an error is raised if we try to ask for no data duplication and if we get some flips
772
+ z_stich_config.flip_ud = (False, True, False)
773
+ with pytest.raises(ValueError):
774
+ stitcher = PostProcessZStitcherNoDD(z_stich_config, progress=None)
775
+ stitcher.stitch()