nabu 2024.1.10__py3-none-any.whl → 2024.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) 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/__init__.py +0 -0
  15. nabu/app/tests/test_reduce_dark_flat.py +4 -1
  16. nabu/cuda/kernel.py +11 -2
  17. nabu/cuda/processing.py +2 -2
  18. nabu/cuda/src/cone.cu +77 -0
  19. nabu/cuda/src/hierarchical_backproj.cu +271 -0
  20. nabu/cuda/utils.py +0 -6
  21. nabu/estimation/alignment.py +5 -19
  22. nabu/estimation/cor.py +173 -599
  23. nabu/estimation/cor_sino.py +356 -26
  24. nabu/estimation/focus.py +63 -11
  25. nabu/estimation/tests/test_cor.py +124 -58
  26. nabu/estimation/tests/test_focus.py +6 -6
  27. nabu/estimation/tilt.py +2 -1
  28. nabu/estimation/utils.py +5 -33
  29. nabu/io/__init__.py +1 -1
  30. nabu/io/cast_volume.py +1 -1
  31. nabu/io/reader.py +416 -21
  32. nabu/io/tests/test_readers.py +422 -0
  33. nabu/io/tests/test_writers.py +1 -102
  34. nabu/io/writer.py +4 -433
  35. nabu/opencl/kernel.py +14 -3
  36. nabu/opencl/processing.py +8 -0
  37. nabu/pipeline/config_validators.py +5 -2
  38. nabu/pipeline/datadump.py +12 -5
  39. nabu/pipeline/estimators.py +162 -188
  40. nabu/pipeline/fullfield/chunked.py +168 -92
  41. nabu/pipeline/fullfield/chunked_cuda.py +7 -3
  42. nabu/pipeline/fullfield/computations.py +2 -7
  43. nabu/pipeline/fullfield/dataset_validator.py +0 -4
  44. nabu/pipeline/fullfield/nabu_config.py +37 -13
  45. nabu/pipeline/fullfield/processconfig.py +22 -13
  46. nabu/pipeline/fullfield/reconstruction.py +13 -9
  47. nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
  48. nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
  49. nabu/pipeline/helical/helical_reconstruction.py +1 -1
  50. nabu/pipeline/params.py +21 -1
  51. nabu/pipeline/processconfig.py +1 -12
  52. nabu/pipeline/reader.py +146 -0
  53. nabu/pipeline/tests/test_estimators.py +44 -72
  54. nabu/pipeline/utils.py +4 -2
  55. nabu/pipeline/writer.py +10 -2
  56. nabu/preproc/ccd_cuda.py +1 -1
  57. nabu/preproc/ctf.py +14 -7
  58. nabu/preproc/ctf_cuda.py +2 -3
  59. nabu/preproc/double_flatfield.py +5 -12
  60. nabu/preproc/double_flatfield_cuda.py +2 -2
  61. nabu/preproc/flatfield.py +5 -1
  62. nabu/preproc/flatfield_cuda.py +5 -1
  63. nabu/preproc/phase.py +24 -73
  64. nabu/preproc/phase_cuda.py +5 -8
  65. nabu/preproc/tests/test_ctf.py +11 -7
  66. nabu/preproc/tests/test_flatfield.py +67 -122
  67. nabu/preproc/tests/test_paganin.py +54 -30
  68. nabu/processing/azim.py +206 -0
  69. nabu/processing/convolution_cuda.py +1 -1
  70. nabu/processing/fft_cuda.py +15 -17
  71. nabu/processing/histogram.py +2 -0
  72. nabu/processing/histogram_cuda.py +2 -1
  73. nabu/processing/kernel_base.py +3 -0
  74. nabu/processing/muladd_cuda.py +1 -0
  75. nabu/processing/padding_opencl.py +1 -1
  76. nabu/processing/roll_opencl.py +1 -0
  77. nabu/processing/rotation_cuda.py +2 -2
  78. nabu/processing/tests/test_fft.py +17 -10
  79. nabu/processing/unsharp_cuda.py +1 -1
  80. nabu/reconstruction/cone.py +104 -40
  81. nabu/reconstruction/fbp.py +3 -0
  82. nabu/reconstruction/fbp_base.py +7 -2
  83. nabu/reconstruction/filtering.py +20 -7
  84. nabu/reconstruction/filtering_cuda.py +7 -1
  85. nabu/reconstruction/hbp.py +424 -0
  86. nabu/reconstruction/mlem.py +99 -0
  87. nabu/reconstruction/reconstructor.py +2 -0
  88. nabu/reconstruction/rings_cuda.py +19 -19
  89. nabu/reconstruction/sinogram_cuda.py +1 -0
  90. nabu/reconstruction/sinogram_opencl.py +3 -1
  91. nabu/reconstruction/tests/test_cone.py +10 -5
  92. nabu/reconstruction/tests/test_deringer.py +7 -6
  93. nabu/reconstruction/tests/test_fbp.py +124 -10
  94. nabu/reconstruction/tests/test_filtering.py +13 -11
  95. nabu/reconstruction/tests/test_halftomo.py +30 -4
  96. nabu/reconstruction/tests/test_mlem.py +91 -0
  97. nabu/reconstruction/tests/test_reconstructor.py +8 -3
  98. nabu/resources/dataset_analyzer.py +142 -92
  99. nabu/resources/gpu.py +1 -0
  100. nabu/resources/nxflatfield.py +134 -125
  101. nabu/resources/templates/id16a_fluo.conf +42 -0
  102. nabu/resources/tests/test_extract.py +10 -0
  103. nabu/resources/tests/test_nxflatfield.py +2 -2
  104. nabu/stitching/alignment.py +80 -24
  105. nabu/stitching/config.py +105 -68
  106. nabu/stitching/definitions.py +1 -0
  107. nabu/stitching/frame_composition.py +68 -60
  108. nabu/stitching/overlap.py +91 -51
  109. nabu/stitching/single_axis_stitching.py +32 -0
  110. nabu/stitching/slurm_utils.py +6 -6
  111. nabu/stitching/stitcher/__init__.py +0 -0
  112. nabu/stitching/stitcher/base.py +124 -0
  113. nabu/stitching/stitcher/dumper/__init__.py +3 -0
  114. nabu/stitching/stitcher/dumper/base.py +94 -0
  115. nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
  116. nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
  117. nabu/stitching/stitcher/post_processing.py +555 -0
  118. nabu/stitching/stitcher/pre_processing.py +1068 -0
  119. nabu/stitching/stitcher/single_axis.py +484 -0
  120. nabu/stitching/stitcher/stitcher.py +0 -0
  121. nabu/stitching/stitcher/y_stitcher.py +13 -0
  122. nabu/stitching/stitcher/z_stitcher.py +45 -0
  123. nabu/stitching/stitcher_2D.py +278 -0
  124. nabu/stitching/tests/test_config.py +12 -37
  125. nabu/stitching/tests/test_frame_composition.py +33 -59
  126. nabu/stitching/tests/test_overlap.py +149 -7
  127. nabu/stitching/tests/test_utils.py +1 -1
  128. nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
  129. nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
  130. nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
  131. nabu/stitching/utils/__init__.py +1 -0
  132. nabu/stitching/utils/post_processing.py +281 -0
  133. nabu/stitching/utils/tests/test_post-processing.py +21 -0
  134. nabu/stitching/{utils.py → utils/utils.py} +79 -52
  135. nabu/stitching/y_stitching.py +27 -0
  136. nabu/stitching/z_stitching.py +32 -2281
  137. nabu/testutils.py +1 -152
  138. nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
  139. nabu/utils.py +158 -61
  140. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/METADATA +24 -17
  141. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/RECORD +145 -121
  142. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/WHEEL +1 -1
  143. nabu/io/tiffwriter_zmm.py +0 -99
  144. nabu/pipeline/fallback_utils.py +0 -149
  145. nabu/pipeline/helical/tests/test_accumulator.py +0 -158
  146. nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
  147. nabu/pipeline/helical/tests/test_strategy.py +0 -61
  148. nabu/pipeline/helical/utils.py +0 -51
  149. nabu/pipeline/tests/test_chunk_reader.py +0 -74
  150. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
  151. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
  152. {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/top_level.txt +0 -0
@@ -4,17 +4,16 @@ import pytest
4
4
  import scipy.ndimage
5
5
  import h5py
6
6
  from nabu.testutils import utilstest, __do_long_tests__
7
- from nabu.testutils import get_data as nabu_get_data
7
+ from nabu.testutils import get_data
8
8
 
9
9
  from nabu.estimation.cor import (
10
10
  CenterOfRotation,
11
11
  CenterOfRotationAdaptiveSearch,
12
12
  CenterOfRotationGrowingWindow,
13
13
  CenterOfRotationSlidingWindow,
14
- CenterOfRotationFourierAngles,
15
14
  CenterOfRotationOctaveAccurate,
16
15
  )
17
- from nabu.estimation.cor_sino import SinoCor
16
+ from nabu.estimation.cor_sino import SinoCor, CenterOfRotationFourierAngles, CenterOfRotationVo, __have_algotom__
18
17
 
19
18
 
20
19
  @pytest.fixture(scope="class")
@@ -51,7 +50,6 @@ def bootstrap_cor_fourier(request):
51
50
  cls.sinos = a["sinos"]
52
51
  cls.angles = a["angles"]
53
52
  cls.true_cor = a["true_cor"]
54
- cls.estimated_cor_from_motor = a["estimated_cor_from_motor"]
55
53
 
56
54
 
57
55
  def get_cor_data_h5(*dataset_path):
@@ -64,11 +62,9 @@ def get_cor_data_h5(*dataset_path):
64
62
  dataset_downloaded_path = utilstest.getfile(dataset_relpath)
65
63
  with h5py.File(dataset_downloaded_path, "r") as hf:
66
64
  data = hf["/entry/instrument/detector/data"][()]
67
-
68
- cor_global_pix = hf["/calibration/alignment/global/x_rotation_axis_pixel_position"][()]
69
-
70
- cor_highlow_pix = hf["/calibration/alignment/highlow/x_rotation_axis_pixel_position"][()]
71
- tilt_deg = hf["/calibration/alignment/highlow/z_camera_tilt"][()]
65
+ cor_global_pix = hf["/calibration/alignment/global/x_rotation_axis_pixel_position"][()][0]
66
+ cor_highlow_pix = hf["/calibration/alignment/highlow/x_rotation_axis_pixel_position"][()][0]
67
+ tilt_deg = hf["/calibration/alignment/highlow/z_camera_tilt"][()][0]
72
68
 
73
69
  return data, (cor_global_pix, cor_highlow_pix, tilt_deg)
74
70
 
@@ -109,14 +105,16 @@ class TestCor:
109
105
  radio2 = np.fliplr(self.data[1, :, :])
110
106
 
111
107
  CoR_calc = CenterOfRotation()
112
- cor_position = CoR_calc.find_shift(radio1, radio2)
108
+ cor_position = CoR_calc.find_shift(radio1, radio2, return_relative_to_middle=True)
113
109
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
114
110
 
115
111
  message = "Computed CoR %f " % cor_position + " and real CoR %f do not coincide" % self.cor_gl_pix
116
112
  assert np.isclose(self.cor_gl_pix, cor_position, atol=self.abs_tol), message
117
113
 
118
114
  # testing again with the validity return value
119
- cor_position, result_validity = CoR_calc.find_shift(radio1, radio2, return_validity=True)
115
+ cor_position, result_validity = CoR_calc.find_shift(
116
+ radio1, radio2, return_validity=True, return_relative_to_middle=True
117
+ )
120
118
  assert np.isscalar(cor_position)
121
119
 
122
120
  message = (
@@ -133,7 +131,7 @@ class TestCor:
133
131
  radio2 = np.random.poisson(np.fliplr(radio2) * 400)
134
132
 
135
133
  CoR_calc = CenterOfRotation()
136
- cor_position = CoR_calc.find_shift(radio1, radio2, median_filt_shape=(3, 3))
134
+ cor_position = CoR_calc.find_shift(radio1, radio2, median_filt_shape=(3, 3), return_relative_to_middle=True)
137
135
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
138
136
 
139
137
  message = "Computed CoR %f " % cor_position + " and real CoR %f do not coincide" % self.cor_gl_pix
@@ -157,8 +155,7 @@ class TestCor:
157
155
 
158
156
  CoR_calc = CenterOfRotation()
159
157
 
160
- # cor_position = CoR_calc.find_shift(radio1, radio2)
161
- cor_position = CoR_calc.find_shift(radio1, radio2, low_pass=(6.0, 0.3))
158
+ cor_position = CoR_calc.find_shift(radio1, radio2, low_pass=(6.0, 0.3), return_relative_to_middle=True)
162
159
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
163
160
 
164
161
  message = "Computed CoR %f " % cor_position + " and real CoR %f do not coincide" % self.cor_gl_pix
@@ -168,7 +165,7 @@ class TestCor:
168
165
  def test_half_tomo_cor_exp(self):
169
166
  """test the half_tomo algorithm on experimental data"""
170
167
 
171
- radios = nabu_get_data("ha_autocor_radios.npz")
168
+ radios = get_data("ha_autocor_radios.npz")
172
169
  radio1 = radios["radio1"]
173
170
  radio2 = radios["radio2"]
174
171
  cor_pos = radios["cor_pos"]
@@ -177,20 +174,22 @@ class TestCor:
177
174
 
178
175
  CoR_calc = CenterOfRotationAdaptiveSearch()
179
176
 
180
- cor_position = CoR_calc.find_shift(radio1, radio2, low_pass=1, high_pass=20, filtered_cost=True)
177
+ cor_position = CoR_calc.find_shift(
178
+ radio1, radio2, low_pass=1, high_pass=20, filtered_cost=True, return_relative_to_middle=True
179
+ )
181
180
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
182
181
 
183
182
  message = (
184
183
  "Computed CoR %f " % cor_position
185
- + " and real CoR %f should coincide when using the halftomo algorithm with hald tomo data" % cor_pos
184
+ + " and real CoR %f should coincide when using the halftomo algorithm with half tomo data" % cor_pos
186
185
  )
187
- assert np.isclose(cor_pos, cor_position, atol=self.abs_tol), message
186
+ assert np.isclose(cor_pos, cor_position, atol=self.abs_tol + 0.5), message
188
187
 
189
188
  @pytest.mark.skipif(not (__do_long_tests__), reason="need environment variable NABU_LONG_TESTS=1")
190
189
  def test_half_tomo_cor_exp_limited(self):
191
190
  """test the hal_tomo algorithm on experimental data and global search with limits"""
192
191
 
193
- radios = nabu_get_data("ha_autocor_radios.npz")
192
+ radios = get_data("ha_autocor_radios.npz")
194
193
  radio1 = radios["radio1"]
195
194
  radio2 = radios["radio2"]
196
195
  cor_pos = radios["cor_pos"]
@@ -200,15 +199,22 @@ class TestCor:
200
199
  CoR_calc = CenterOfRotationAdaptiveSearch()
201
200
 
202
201
  cor_position, result_validity = CoR_calc.find_shift(
203
- radio1, radio2, low_pass=1, high_pass=20, margins=(100, 10), filtered_cost=False, return_validity=True
202
+ radio1,
203
+ radio2,
204
+ low_pass=1,
205
+ high_pass=20,
206
+ margins=(100, 10),
207
+ filtered_cost=False,
208
+ return_validity=True,
209
+ return_relative_to_middle=True,
204
210
  )
205
211
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
206
212
 
207
213
  message = (
208
214
  "Computed CoR %f " % cor_position
209
- + " and real CoR %f should coincide when using the halftomo algorithm with hald tomo data" % cor_pos
215
+ + " and real CoR %f should coincide when using the halftomo algorithm with half tomo data" % cor_pos
210
216
  )
211
- assert np.isclose(cor_pos, cor_position, atol=self.abs_tol), message
217
+ assert np.isclose(cor_pos, cor_position, atol=self.abs_tol + 0.5), message
212
218
 
213
219
  message = "returned result_validity is %s " % result_validity + " while it should be sound"
214
220
 
@@ -219,7 +225,7 @@ class TestCor:
219
225
  radio2 = np.fliplr(self.data[1, :, :])
220
226
 
221
227
  CoR_calc = CenterOfRotation()
222
- cor_position = CoR_calc.find_shift(radio1, radio2, padding_mode="edge")
228
+ cor_position = CoR_calc.find_shift(radio1, radio2, padding_mode="edge", return_relative_to_middle=True)
223
229
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
224
230
 
225
231
  message = "Computed CoR %f " % cor_position + " and real CoR %f do not coincide" % self.cor_gl_pix
@@ -232,7 +238,7 @@ class TestCor:
232
238
  radio2 = self.data[1, :, :]
233
239
 
234
240
  with pytest.raises(ValueError) as ex:
235
- CoR_calc.find_shift(radio1, radio2)
241
+ CoR_calc.find_shift(radio1, radio2, return_relative_to_middle=True)
236
242
 
237
243
  message = "Error should have been raised about img #1 shape, other error raised instead:\n%s" % str(ex.value)
238
244
  assert "Images need to be 2-dimensional. Shape of image #1" in str(ex.value), message
@@ -244,7 +250,7 @@ class TestCor:
244
250
  radio2 = self.data
245
251
 
246
252
  with pytest.raises(ValueError) as ex:
247
- CoR_calc.find_shift(radio1, radio2)
253
+ CoR_calc.find_shift(radio1, radio2, return_relative_to_middle=True)
248
254
 
249
255
  message = "Error should have been raised about img #2 shape, other error raised instead:\n%s" % str(ex.value)
250
256
  assert "Images need to be 2-dimensional. Shape of image #2" in str(ex.value), message
@@ -256,7 +262,7 @@ class TestCor:
256
262
  radio2 = self.data[1, :, 0:10]
257
263
 
258
264
  with pytest.raises(ValueError) as ex:
259
- CoR_calc.find_shift(radio1, radio2)
265
+ CoR_calc.find_shift(radio1, radio2, return_relative_to_middle=True)
260
266
 
261
267
  message = (
262
268
  "Error should have been raised about different image shapes, "
@@ -274,7 +280,11 @@ class TestCorWindowSlide:
274
280
 
275
281
  CoR_calc = CenterOfRotationSlidingWindow()
276
282
  cor_position = CoR_calc.find_shift(
277
- radio1, radio2, side="left", window_width=round(radio1.shape[-1] / 4.0 * 3.0)
283
+ radio1,
284
+ radio2,
285
+ side="left",
286
+ window_width=round(radio1.shape[-1] / 4.0 * 3.0),
287
+ return_relative_to_middle=True,
278
288
  )
279
289
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
280
290
 
@@ -282,7 +292,12 @@ class TestCorWindowSlide:
282
292
  assert np.isclose(self.cor_gl_pix, cor_position, atol=self.abs_tol), message
283
293
 
284
294
  cor_position, result_validity = CoR_calc.find_shift(
285
- radio1, radio2, side="left", window_width=round(radio1.shape[-1] / 4.0 * 3.0), return_validity=True
295
+ radio1,
296
+ radio2,
297
+ side="left",
298
+ window_width=round(radio1.shape[-1] / 4.0 * 3.0),
299
+ return_validity=True,
300
+ return_relative_to_middle=True,
286
301
  )
287
302
 
288
303
  message = "returned result_validity is %s " % result_validity + " while it should be sound"
@@ -294,7 +309,7 @@ class TestCorWindowSlide:
294
309
  radio2 = np.fliplr(self.data[1, :, :])
295
310
 
296
311
  CoR_calc = CenterOfRotationSlidingWindow()
297
- cor_position = CoR_calc.find_shift(radio1, radio2, side="center")
312
+ cor_position = CoR_calc.find_shift(radio1, radio2, side="center", return_relative_to_middle=True)
298
313
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
299
314
 
300
315
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_gl_pix
@@ -305,7 +320,7 @@ class TestCorWindowSlide:
305
320
  radio2 = np.fliplr(self.data_ha_proj[1, :, :])
306
321
 
307
322
  CoR_calc = CenterOfRotationSlidingWindow()
308
- cor_position = CoR_calc.find_shift(radio1, radio2, side="right")
323
+ cor_position = CoR_calc.find_shift(radio1, radio2, side="right", return_relative_to_middle=True)
309
324
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
310
325
 
311
326
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_ha_pr_pix
@@ -316,7 +331,7 @@ class TestCorWindowSlide:
316
331
  radio2 = self.data_ha_proj[1, :, :]
317
332
 
318
333
  CoR_calc = CenterOfRotationSlidingWindow()
319
- cor_position = CoR_calc.find_shift(radio1, radio2, side="left")
334
+ cor_position = CoR_calc.find_shift(radio1, radio2, side="left", return_relative_to_middle=True)
320
335
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
321
336
 
322
337
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % -self.cor_ha_pr_pix
@@ -327,7 +342,7 @@ class TestCorWindowSlide:
327
342
  sino2 = np.fliplr(self.data_ha_sino[1, :, :])
328
343
 
329
344
  CoR_calc = CenterOfRotationSlidingWindow()
330
- cor_position = CoR_calc.find_shift(sino1, sino2, side="right")
345
+ cor_position = CoR_calc.find_shift(sino1, sino2, side="right", return_relative_to_middle=True)
331
346
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
332
347
 
333
348
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_ha_sn_pix
@@ -342,7 +357,7 @@ class TestCorWindowGrow:
342
357
  radio2 = np.fliplr(self.data[1, :, :])
343
358
 
344
359
  CoR_calc = CenterOfRotationGrowingWindow()
345
- cor_position = CoR_calc.find_shift(radio1, radio2, side="center")
360
+ cor_position = CoR_calc.find_shift(radio1, radio2, side="center", return_relative_to_middle=True)
346
361
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
347
362
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_gl_pix
348
363
  assert np.isclose(self.cor_gl_pix, cor_position, atol=self.abs_tol), message
@@ -352,7 +367,7 @@ class TestCorWindowGrow:
352
367
  radio2 = np.fliplr(self.data_ha_proj[1, :, :])
353
368
 
354
369
  CoR_calc = CenterOfRotationGrowingWindow()
355
- cor_position = CoR_calc.find_shift(radio1, radio2, side="right")
370
+ cor_position = CoR_calc.find_shift(radio1, radio2, side="right", return_relative_to_middle=True)
356
371
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
357
372
 
358
373
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_ha_pr_pix
@@ -363,13 +378,15 @@ class TestCorWindowGrow:
363
378
  radio2 = self.data_ha_proj[1, :, :]
364
379
 
365
380
  CoR_calc = CenterOfRotationGrowingWindow()
366
- cor_position = CoR_calc.find_shift(radio1, radio2, side="left")
381
+ cor_position = CoR_calc.find_shift(radio1, radio2, side="left", return_relative_to_middle=True)
367
382
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
368
383
 
369
384
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % -self.cor_ha_pr_pix
370
385
  assert np.isclose(-self.cor_ha_pr_pix, cor_position, atol=self.abs_tol), message
371
386
 
372
- cor_position, result_validity = CoR_calc.find_shift(radio1, radio2, side="left", return_validity=True)
387
+ cor_position, result_validity = CoR_calc.find_shift(
388
+ radio1, radio2, side="left", return_validity=True, return_relative_to_middle=True
389
+ )
373
390
 
374
391
  message = "returned result_validity is %s " % result_validity + " while it should be sound"
375
392
 
@@ -380,7 +397,7 @@ class TestCorWindowGrow:
380
397
  radio2 = np.fliplr(self.data_ha_proj[1, :, :])
381
398
 
382
399
  CoR_calc = CenterOfRotationGrowingWindow()
383
- cor_position = CoR_calc.find_shift(radio1, radio2, side="all")
400
+ cor_position = CoR_calc.find_shift(radio1, radio2, side="all", return_relative_to_middle=True)
384
401
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
385
402
 
386
403
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_ha_pr_pix
@@ -391,7 +408,7 @@ class TestCorWindowGrow:
391
408
  sino2 = np.fliplr(self.data_ha_sino[1, :, :])
392
409
 
393
410
  CoR_calc = CenterOfRotationGrowingWindow()
394
- cor_position = CoR_calc.find_shift(sino1, sino2, side="right")
411
+ cor_position = CoR_calc.find_shift(sino1, sino2, side="right", return_relative_to_middle=True)
395
412
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
396
413
 
397
414
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_ha_sn_pix
@@ -421,7 +438,9 @@ class TestCorOctaveAccurate:
421
438
  def test_cor_accurate_positive_shift(self):
422
439
  detector_width = self.image_pair_stylo[0].shape[1]
423
440
  CoR_calc = CenterOfRotationOctaveAccurate()
424
- cor_position = CoR_calc.find_shift(self.image_pair_stylo[0], np.fliplr(self.image_pair_stylo[1]), "center")
441
+ cor_position = CoR_calc.find_shift(
442
+ self.image_pair_stylo[0], np.fliplr(self.image_pair_stylo[1]), "center", return_relative_to_middle=True
443
+ )
425
444
  cor_position = cor_position + detector_width / 2
426
445
  assert np.isscalar(cor_position), f"cor_position expected to be a scalar, {type(cor_position)} returned"
427
446
  message = f"Computed CoR {cor_position} and expected CoR {self.cor_pos_abs_stylo} do not coincide."
@@ -431,7 +450,10 @@ class TestCorOctaveAccurate:
431
450
  detector_width = self.image_pair_blc12781[0].shape[1]
432
451
  CoR_calc = CenterOfRotationOctaveAccurate()
433
452
  cor_position = CoR_calc.find_shift(
434
- self.image_pair_blc12781[0], np.fliplr(self.image_pair_blc12781[1]), "center"
453
+ self.image_pair_blc12781[0],
454
+ np.fliplr(self.image_pair_blc12781[1]),
455
+ "center",
456
+ return_relative_to_middle=True,
435
457
  )
436
458
  cor_position = cor_position + detector_width / 2
437
459
  assert np.isscalar(cor_position), f"cor_position expected to be a scalar, {type(cor_position)} returned"
@@ -441,32 +463,76 @@ class TestCorOctaveAccurate:
441
463
 
442
464
  @pytest.mark.usefixtures("bootstrap_cor_fourier", "bootstrap_cor_win")
443
465
  class TestCorFourierAngle:
466
+ @pytest.mark.skip("Broken function")
444
467
  def test_sino_right_axis_with_near_pos(self):
445
- sino1 = self.data_ha_sino[0, :, :]
446
- sino2 = np.fliplr(self.data_ha_sino[1, :, :])
468
+ sino = np.vstack([self.data_ha_sino[0], self.data_ha_sino[1]])
447
469
  start_angle = np.pi / 4
448
- angles = np.linspace(start_angle, start_angle + 2 * np.pi, 2 * sino1.shape[0])
470
+ angles = np.linspace(start_angle, start_angle + 2 * np.pi, sino.shape[0])
449
471
 
450
- cor_options = {"side": 740, "refine": True}
451
-
452
- CoR_calc = CenterOfRotationFourierAngles(cor_options=cor_options)
453
- cor_position = CoR_calc.find_shift(sino1, sino2, angles, side="right")
472
+ CoR_calc = CenterOfRotationFourierAngles()
473
+ cor_position = CoR_calc.find_shift(
474
+ sino, angles, side="right", crop_around_cor=True, return_relative_to_middle=True
475
+ ) # side=sino.shape[1]/2+740)
454
476
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
455
477
 
456
478
  message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_ha_sn_pix
457
479
  assert np.isclose(self.cor_ha_sn_pix, cor_position, atol=self.abs_tol * 3), message
458
480
 
459
- def test_sino_right_axis_with_ignore(self):
460
- sino1 = self.data_ha_sino[0, :, :]
461
- sino2 = np.fliplr(self.data_ha_sino[1, :, :])
462
- start_angle = np.pi / 4
463
- angles = np.linspace(start_angle, start_angle + 2 * np.pi, 2 * sino1.shape[0])
464
-
465
- cor_options = {"side": "ignore", "refine": True}
481
+ def test_sino_right_axis_with_near_pos_jl(self):
466
482
 
467
- CoR_calc = CenterOfRotationFourierAngles(cor_options=cor_options)
468
- cor_position = CoR_calc.find_shift(sino1, sino2, angles, side="right")
483
+ CoR_calc = CenterOfRotationFourierAngles()
484
+ cor_position = CoR_calc.find_shift(
485
+ self.sinos, self.angles, side="right", crop_around_cor=True, return_relative_to_middle=True
486
+ ) # side=sino.shape[1]/2+740)
469
487
  assert np.isscalar(cor_position), f"cor_position expected to be a scale, {type(cor_position)} returned"
470
488
 
471
- message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.cor_ha_sn_pix
472
- assert np.isclose(self.cor_ha_sn_pix, cor_position, atol=self.abs_tol * 3), message
489
+ message = "Computed CoR %f " % cor_position + " and expected CoR %f do not coincide" % self.true_cor
490
+ assert np.isclose(self.true_cor, cor_position, atol=self.abs_tol * 3), message
491
+
492
+
493
+ @pytest.fixture(scope="class")
494
+ def bootstrap_vo_cor(request):
495
+ cls = request.cls
496
+ cls.tol = 1e-2
497
+ cls.test_sinograms = {name: get_data("sino_%s.npz" % name) for name in ["pencil", "coffee", "mousebrains"]}
498
+ sino_bamboo = get_data("sino_bamboo_hercules_for_test.npz")
499
+ cls.test_sinograms["bamboo_hercules"] = {
500
+ "data": sino_bamboo["sinos"],
501
+ # FIXME the test file needs to be re-generated, "true_cor" has an incorrect offset
502
+ "cor": sino_bamboo["true_cor"] + (2560) / 2,
503
+ }
504
+
505
+
506
+ @pytest.mark.skipif(not (__have_algotom__), reason="need algotom for this test")
507
+ @pytest.mark.usefixtures("bootstrap_vo_cor")
508
+ class TestVoCOR:
509
+
510
+ def _test_cor(self, dataset_name, tolerance=1e-2, **cor_options):
511
+ cor_finder = CenterOfRotationVo()
512
+ cor = cor_finder.find_shift(
513
+ self.test_sinograms[dataset_name]["data"], return_relative_to_middle=False, **cor_options
514
+ )
515
+ cor_ref = self.test_sinograms[dataset_name]["cor"]
516
+ assert (
517
+ np.abs(cor - cor_ref) < tolerance
518
+ ), "CoR estimation failed for %s: expected %.3f, got %.3f (tol = %.2e)" % (
519
+ dataset_name,
520
+ cor_ref,
521
+ cor,
522
+ tolerance,
523
+ )
524
+
525
+ def test_cor_180(self):
526
+ self._test_cor("pencil", tolerance=0.6)
527
+
528
+ def test_cor_180_more_complex(self): ...
529
+
530
+ def test_cor_360_halftomo(self):
531
+ self._test_cor("bamboo_hercules", tolerance=0.1, halftomo=True)
532
+
533
+ def test_cor_360_halftomo_hard(self):
534
+ # This one is difficult
535
+ self._test_cor("mousebrains", tolerance=2, halftomo=True)
536
+
537
+ def test_cor_360_not_halftomo(self):
538
+ self._test_cor("coffee", tolerance=0.5, halftomo=False, is_360=True)
@@ -46,13 +46,13 @@ def get_focus_data(*dataset_path):
46
46
  ]
47
47
  )
48
48
 
49
- angle_best_ind = hf["/calibration/focus/angle/best_img"][()]
50
- angle_best_pos = hf["/calibration/focus/angle/best_pos"][()]
51
- angle_tilt_v = hf["/calibration/focus/angle/tilt_v_rad"][()]
52
- angle_tilt_h = hf["/calibration/focus/angle/tilt_h_rad"][()]
49
+ angle_best_ind = hf["/calibration/focus/angle/best_img"][()][0]
50
+ angle_best_pos = hf["/calibration/focus/angle/best_pos"][()][0]
51
+ angle_tilt_v = hf["/calibration/focus/angle/tilt_v_rad"][()][0]
52
+ angle_tilt_h = hf["/calibration/focus/angle/tilt_h_rad"][()][0]
53
53
 
54
- std_best_ind = hf["/calibration/focus/std/best_img"][()]
55
- std_best_pos = hf["/calibration/focus/std/best_pos"][()]
54
+ std_best_ind = hf["/calibration/focus/std/best_img"][()][0]
55
+ std_best_pos = hf["/calibration/focus/std/best_pos"][()][0]
56
56
 
57
57
  calib_data_angle = (angle_best_ind, angle_best_pos, angle_tilt_v, angle_tilt_h)
58
58
  calib_data_std = (std_best_ind, std_best_pos)
nabu/estimation/tilt.py CHANGED
@@ -188,7 +188,7 @@ class CameraTilt(CenterOfRotation):
188
188
  img_fft_1 = img_fft_1[..., : img_fft_1.shape[-2] // 2, :]
189
189
  img_fft_2 = img_fft_2[..., : img_fft_2.shape[-2] // 2, :]
190
190
 
191
- tilt_pix = self.find_shift(img_fft_1, img_fft_2, shift_axis=-2)
191
+ tilt_pix = self.find_shift(img_fft_1, img_fft_2, shift_axis=-2, return_relative_to_middle=True)
192
192
  tilt_deg = -(360 / img_shape[0]) * tilt_pix
193
193
 
194
194
  img_1 = skt.rotate(img_1, tilt_deg)
@@ -201,6 +201,7 @@ class CameraTilt(CenterOfRotation):
201
201
  peak_fit_radius=peak_fit_radius,
202
202
  high_pass=high_pass,
203
203
  low_pass=low_pass,
204
+ return_relative_to_middle=True,
204
205
  )
205
206
 
206
207
  if self.verbose:
nabu/estimation/utils.py CHANGED
@@ -1,39 +1,11 @@
1
1
  import numpy as np
2
- from scipy.signal import find_peaks
3
2
 
4
3
 
5
- def is_fullturn_scan(angles):
4
+ def is_fullturn_scan(angles_rad, tol=None):
6
5
  """
7
6
  Return True if the angles correspond to a full-turn (360 degrees) scan.
8
7
  """
9
- angles = angles % (2 * np.pi)
10
- angles -= angles.min()
11
- sin_angles = np.sin(angles)
12
- min_dist = 5 # TODO find a more robust angles-based min distance, though this should cover most of the cases
13
- maxima = find_peaks(sin_angles, distance=min_dist)[0]
14
- minima = find_peaks(-sin_angles, distance=min_dist)[0]
15
- n_max = maxima.size
16
- n_min = minima.size
17
- # abs(n_max - n_min) actually means the following:
18
- # * 0: All turns are full (eg. 2pi, 4pi)
19
- # * 1: At least one half-turn remains (eg. pi, 3pi)
20
- if abs(n_max - n_min) == 0:
21
- return True
22
- else:
23
- return False
24
-
25
-
26
- def get_halfturn_indices(angles):
27
- angles = angles % (2 * np.pi)
28
- angles -= angles.min()
29
- sin_angles = np.sin(angles)
30
- min_dist = 5 # TODO find a more robust angles-based min distance, though this should cover most of the cases
31
- maxima = find_peaks(sin_angles, distance=min_dist)[0]
32
- minima = find_peaks(-sin_angles, distance=min_dist)[0]
33
- extrema = np.sort(np.hstack([maxima, minima]))
34
- extrema -= extrema.min()
35
- extrema = np.hstack([extrema, [angles.size - 1]])
36
- res = []
37
- for i in range(extrema.size - 1):
38
- res.append((extrema[i], extrema[i + 1]))
39
- return res
8
+ angles_rad = np.sort(angles_rad)
9
+ if tol is None:
10
+ tol = np.min(np.abs(np.diff(angles_rad))) * 1.1
11
+ return np.abs((angles_rad.max() - angles_rad.min()) - (2 * np.pi)) < tol
nabu/io/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  from .reader import NPReader, EDFReader, HDF5File, HDF5Loader, ChunkReader, Readers
2
- from .writer import NXProcessWriter, TIFFWriter, EDFWriter, JP2Writer, NPYWriter, NPZWriter
2
+ from .writer import NXProcessWriter
nabu/io/cast_volume.py CHANGED
@@ -66,7 +66,7 @@ def get_default_output_volume(
66
66
  Constructor = EDFVolume
67
67
  elif output_type == "jp2":
68
68
  Constructor = JP2KVolume
69
- return Constructor(
69
+ return Constructor( # pylint: disable=E0601
70
70
  folder=os.path.join(
71
71
  os.path.dirname(input_volume.data_url.file_path()),
72
72
  output_dir,