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,36 +1,33 @@
1
- from copy import copy
2
1
  from dataclasses import dataclass
3
2
  import numpy
4
3
  from math import ceil
5
4
 
6
- from nabu.stitching.overlap import ZStichOverlapKernel
7
-
8
5
 
9
6
  @dataclass
10
- class _FrameCompositionBase:
11
- def compose(self, output_frame: numpy.ndarray, input_frames: tuple):
12
- raise NotImplementedError("Base class")
13
-
14
-
15
- @dataclass
16
- class ZFrameComposition(_FrameCompositionBase):
7
+ class FrameComposition:
17
8
  """
18
9
  class used to define intervals to know where to dump raw data or stitched data according to requested policy.
19
10
  The idea is to create this once for all for one stitching operation and reuse it for each frame.
20
11
  """
21
12
 
22
- local_start_y: tuple
23
- local_end_y: tuple
24
- global_start_y: tuple
25
- global_end_y: tuple
13
+ composed_axis: int
14
+ """axis along which the composition is done"""
15
+ local_start: tuple
16
+ """tuple of indices on the input frames ref to know where each region start (along the composed axis)"""
17
+ local_end: tuple
18
+ """tuple of indices on the input frames ref to know where each region end (along the composed axis)"""
19
+ global_start: tuple
20
+ """tuple of indices on the output frame ref to know where each region start (along the composed axis)"""
21
+ global_end: tuple
22
+ """tuple of indices on the output frame ref to know where each region end (along the composed axis)"""
26
23
 
27
24
  def browse(self):
28
- for i in range(len(self.local_start_y)):
25
+ for i in range(len(self.local_start)):
29
26
  yield (
30
- self.local_start_y[i],
31
- self.local_end_y[i],
32
- self.global_start_y[i],
33
- self.global_end_y[i],
27
+ self.local_start[i],
28
+ self.local_end[i],
29
+ self.global_start[i],
30
+ self.global_end[i],
34
31
  )
35
32
 
36
33
  def compose(self, output_frame: numpy.ndarray, input_frames: tuple):
@@ -39,20 +36,25 @@ class ZFrameComposition(_FrameCompositionBase):
39
36
  f"output_frame is expected to be 2D (gray scale) or 3D (RGB(A)) and not {output_frame.ndim}"
40
37
  )
41
38
  for (
42
- global_start_y,
43
- global_end_y,
44
- local_start_y,
45
- local_end_y,
39
+ global_start,
40
+ global_end,
41
+ local_start,
42
+ local_end,
46
43
  input_frame,
47
44
  ) in zip(
48
- self.global_start_y,
49
- self.global_end_y,
50
- self.local_start_y,
51
- self.local_end_y,
45
+ self.global_start,
46
+ self.global_end,
47
+ self.local_start,
48
+ self.local_end,
52
49
  input_frames,
53
50
  ):
54
51
  if input_frame is not None:
55
- output_frame[global_start_y:global_end_y] = input_frame[local_start_y:local_end_y]
52
+ if self.composed_axis == 0:
53
+ output_frame[global_start:global_end] = input_frame[local_start:local_end]
54
+ elif self.composed_axis == 1:
55
+ output_frame[:, global_start:global_end] = input_frame[:, local_start:local_end]
56
+ else:
57
+ raise ValueError(f"composed axis must be in (0, 1). Get {self.composed_axis}")
56
58
 
57
59
  @staticmethod
58
60
  def compute_raw_frame_compositions(frames: tuple, key_lines: tuple, overlap_kernels: tuple, stitching_axis):
@@ -63,37 +65,42 @@ class ZFrameComposition(_FrameCompositionBase):
63
65
  """
64
66
  assert len(frames) == len(overlap_kernels) + 1 == len(key_lines) + 1
65
67
 
66
- global_start_ys = [0]
68
+ global_start_indices = [0]
67
69
 
68
70
  # extend shifts and kernels to have a first shift of 0 and two overlaps values at 0 to
69
71
  # generalize processing
70
- local_start_ys = [0]
72
+ local_start_indices = [0]
71
73
 
72
- local_start_ys.extend(
74
+ local_start_indices.extend(
73
75
  [ceil(key_line[1] + kernel.overlap_size / 2) for (key_line, kernel) in zip(key_lines, overlap_kernels)]
74
76
  )
75
- local_end_ys = list(
77
+ local_end_indices = list(
76
78
  [ceil(key_line[0] - kernel.overlap_size / 2) for (key_line, kernel) in zip(key_lines, overlap_kernels)]
77
79
  )
78
- local_end_ys.append(frames[-1].shape[stitching_axis])
80
+ local_end_indices.append(frames[-1].shape[stitching_axis])
79
81
 
80
82
  for (
81
- new_local_start_y,
82
- new_local_end_y,
83
+ new_local_start_index,
84
+ new_local_end_index,
83
85
  kernel,
84
- ) in zip(local_start_ys, local_end_ys, overlap_kernels):
85
- global_start_ys.append(global_start_ys[-1] + (new_local_end_y - new_local_start_y) + kernel.overlap_size)
86
+ ) in zip(local_start_indices, local_end_indices, overlap_kernels):
87
+ global_start_indices.append(
88
+ global_start_indices[-1] + (new_local_end_index - new_local_start_index) + kernel.overlap_size
89
+ )
86
90
 
87
91
  # global end can be easily found from global start + local start and end
88
- global_end_ys = []
89
- for global_start_y, new_local_start_y, new_local_end_y in zip(global_start_ys, local_start_ys, local_end_ys):
90
- global_end_ys.append(global_start_y + new_local_end_y - new_local_start_y)
91
-
92
- return ZFrameComposition(
93
- local_start_y=tuple(local_start_ys),
94
- local_end_y=tuple(local_end_ys),
95
- global_start_y=tuple(global_start_ys),
96
- global_end_y=tuple(global_end_ys),
92
+ global_end_indices = []
93
+ for global_start_index, new_local_start_index, new_local_end_index in zip(
94
+ global_start_indices, local_start_indices, local_end_indices
95
+ ):
96
+ global_end_indices.append(global_start_index + new_local_end_index - new_local_start_index)
97
+
98
+ return FrameComposition(
99
+ composed_axis=stitching_axis,
100
+ local_start=tuple(local_start_indices),
101
+ local_end=tuple(local_end_indices),
102
+ global_start=tuple(global_start_indices),
103
+ global_end=tuple(global_end_indices),
97
104
  )
98
105
 
99
106
  @staticmethod
@@ -102,33 +109,34 @@ class ZFrameComposition(_FrameCompositionBase):
102
109
  compute frame composition for stiching.
103
110
  """
104
111
  assert len(frames) == len(overlap_kernels) + 1 == len(key_lines) + 1
105
- assert stitching_axis in (0, 1, 2)
112
+ assert stitching_axis in (0, 1)
106
113
 
107
114
  # position in the stitched frame;
108
- local_start_ys = [0] * len(overlap_kernels)
109
- local_end_ys = [kernel.overlap_size for kernel in overlap_kernels]
115
+ local_start_indices = [0] * len(overlap_kernels)
116
+ local_end_indices = [kernel.overlap_size for kernel in overlap_kernels]
110
117
 
111
118
  # position in the global frame. For this one it is simpler to rely on the raw frame composition
112
- composition_raw = ZFrameComposition.compute_raw_frame_compositions(
119
+ composition_raw = FrameComposition.compute_raw_frame_compositions(
113
120
  frames=frames,
114
121
  key_lines=key_lines,
115
122
  overlap_kernels=overlap_kernels,
116
123
  stitching_axis=stitching_axis,
117
124
  )
118
- global_start_ys = composition_raw.global_end_y[:-1]
119
- global_end_ys = composition_raw.global_start_y[1:]
120
-
121
- return ZFrameComposition(
122
- local_start_y=tuple(local_start_ys),
123
- local_end_y=tuple(local_end_ys),
124
- global_start_y=tuple(global_start_ys),
125
- global_end_y=tuple(global_end_ys),
125
+ global_start_indices = composition_raw.global_end[:-1]
126
+ global_end_indices = composition_raw.global_start[1:]
127
+
128
+ return FrameComposition(
129
+ composed_axis=stitching_axis,
130
+ local_start=tuple(local_start_indices),
131
+ local_end=tuple(local_end_indices),
132
+ global_start=tuple(global_start_indices),
133
+ global_end=tuple(global_end_indices),
126
134
  )
127
135
 
128
136
  @staticmethod
129
- def pprint_z_stitching(raw_composition, stitch_composition):
137
+ def pprint_composition(raw_composition, stitch_composition):
130
138
  """
131
- util to display what the output of the z stitch will looks like from composition
139
+ util to display what the output of the composition will looks like from composition
132
140
  """
133
141
  for i_frame, (raw_comp, stitch_comp) in enumerate(zip(raw_composition.browse(), stitch_composition.browse())):
134
142
  raw_local_start, raw_local_end, raw_global_start, raw_global_end = raw_comp
nabu/stitching/overlap.py CHANGED
@@ -1,33 +1,3 @@
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__ = "10/05/2022"
29
-
30
-
31
1
  import numpy
32
2
  import logging
33
3
  from typing import Optional, Union
@@ -59,34 +29,49 @@ class OverlapKernelBase:
59
29
  pass
60
30
 
61
31
 
62
- class ZStichOverlapKernel(OverlapKernelBase):
32
+ class ImageStichOverlapKernel(OverlapKernelBase):
63
33
  """
64
- class used to define overlap between two scans and create stitch between frames (`stitch` function)
34
+ Stitch two images along Y (axis 0 in image space)
65
35
  """
66
36
 
67
37
  DEFAULT_HIGH_FREQUENCY_THRESHOLD = 2
68
38
 
69
39
  def __init__(
70
40
  self,
71
- frame_width: int,
41
+ stitching_axis: int,
42
+ frame_unstitched_axis_size: tuple,
72
43
  stitching_strategy: OverlapStitchingStrategy = DEFAULT_OVERLAP_STRATEGY,
73
44
  overlap_size: int = DEFAULT_OVERLAP_SIZE,
74
45
  extra_params: Optional[dict] = None,
75
46
  ) -> None:
76
- """ """
47
+ """
48
+ :param stitching_axis: axis along which stitching is operate. Must be in '0', '1'
49
+ :param frame_unstitched_axis_size: according to the stitching axis the stitched framed will always have a constant size:
50
+ * If stitching_axis == 0 then it will be the frame width
51
+ * If stitching_axis == 1 then it will be the frame height
52
+ :param stitching_strategy: stategy / algorithm to use in order to generate the stitching
53
+ :param overlap_size: size (int) of the overlap (stitching) between the two images
54
+ :param extra_params: possibly extra parameters to operate the stitching
55
+ """
77
56
  from nabu.stitching.config import KEY_THRESHOLD_FREQUENCY # avoid acylic import
78
57
 
79
58
  if not isinstance(overlap_size, int) and overlap_size > 0:
80
59
  raise TypeError(
81
60
  f"overlap_size is expected to be a positive int, {overlap_size} - not {overlap_size} ({type(overlap_size)})"
82
61
  )
83
- if not isinstance(frame_width, int) or not frame_width > 0:
62
+ if not isinstance(frame_unstitched_axis_size, int) or not frame_unstitched_axis_size > 0:
84
63
  raise TypeError(
85
- f"frame_width is expected to be a positive int, {frame_width} - not {frame_width} ({type(frame_width)})"
64
+ f"frame_width is expected to be a positive int, {frame_unstitched_axis_size} - not {frame_unstitched_axis_size} ({type(frame_unstitched_axis_size)})"
65
+ )
66
+
67
+ if not stitching_axis in (0, 1):
68
+ raise ValueError(
69
+ "stitching_axis is expected to be the axis along which stitching must be done. It should be '0' or '1'"
86
70
  )
87
71
 
72
+ self._stitching_axis = stitching_axis
88
73
  self._overlap_size = abs(overlap_size)
89
- self._frame_width = frame_width
74
+ self._frame_unstitched_axis_size = frame_unstitched_axis_size
90
75
  self._stitching_strategy = OverlapStitchingStrategy.from_value(stitching_strategy)
91
76
  self._weights_img_1 = None
92
77
  self._weights_img_2 = None
@@ -97,13 +82,28 @@ class ZStichOverlapKernel(OverlapKernelBase):
97
82
  )
98
83
 
99
84
  def __str__(self) -> str:
100
- return f"z-stitching kernel (policy={self.stitching_strategy.value}, overlap_size={self.overlap_size}, frame={self._frame_width})"
85
+ return f"z-stitching kernel (policy={self.stitching_strategy.value}, overlap_size={self.overlap_size}, frame={self._frame_unstitched_axis_size})"
101
86
 
102
87
  @staticmethod
103
88
  def __check_img(img, name):
104
89
  if not isinstance(img, numpy.ndarray) and img.ndim == 2:
105
90
  raise ValueError(f"{name} is expected to be 2D numpy array")
106
91
 
92
+ @property
93
+ def stitched_axis(self) -> int:
94
+ return self._stitching_axis
95
+
96
+ @property
97
+ def unstitched_axis(self) -> int:
98
+ """
99
+ util function. The kernel is operating stitching on images along a single axis (`stitching_axis`).
100
+ This property is returning the other axis.
101
+ """
102
+ if self.stitched_axis == 0:
103
+ return 1
104
+ else:
105
+ return 0
106
+
107
107
  @property
108
108
  def overlap_size(self) -> int:
109
109
  return self._overlap_size
@@ -169,8 +169,22 @@ class ZStichOverlapKernel(OverlapKernelBase):
169
169
  else:
170
170
  raise NotImplementedError(f"{self.stitching_strategy} not implemented")
171
171
 
172
- self._weights_img_1 = weights_img_1.reshape(-1, 1) * numpy.ones(self._frame_width).reshape(1, -1)
173
- self._weights_img_2 = weights_img_2.reshape(-1, 1) * numpy.ones(self._frame_width).reshape(1, -1)
172
+ if self._stitching_axis == 0:
173
+ self._weights_img_1 = weights_img_1.reshape(-1, 1) * numpy.ones(self._frame_unstitched_axis_size).reshape(
174
+ 1, -1
175
+ )
176
+ self._weights_img_2 = weights_img_2.reshape(-1, 1) * numpy.ones(self._frame_unstitched_axis_size).reshape(
177
+ 1, -1
178
+ )
179
+ elif self._stitching_axis == 1:
180
+ self._weights_img_1 = weights_img_1.reshape(1, -1) * numpy.ones(self._frame_unstitched_axis_size).reshape(
181
+ -1, 1
182
+ )
183
+ self._weights_img_2 = weights_img_2.reshape(1, -1) * numpy.ones(self._frame_unstitched_axis_size).reshape(
184
+ -1, 1
185
+ )
186
+ else:
187
+ raise ValueError(f"stitching_axis should be in (0, 1). {self._stitching_axis} provided")
174
188
 
175
189
  def stitch(self, img_1, img_2, check_input=True) -> tuple:
176
190
  """Compute overlap region from the defined strategy"""
@@ -186,14 +200,20 @@ class ZStichOverlapKernel(OverlapKernelBase):
186
200
  if self._stitching_strategy is OverlapStitchingStrategy.IMAGE_MINIMUM_DIVERGENCE:
187
201
  return (
188
202
  compute_image_minimum_divergence(
189
- img_1=img_1, img_2=img_2, high_frequency_threshold=self._high_frequency_threshold
203
+ img_1=img_1,
204
+ img_2=img_2,
205
+ high_frequency_threshold=self._high_frequency_threshold,
206
+ stitching_axis=self.stitched_axis,
190
207
  ),
191
208
  None,
192
209
  None,
193
210
  )
194
211
  elif self._stitching_strategy is OverlapStitchingStrategy.HIGHER_SIGNAL:
195
212
  return (
196
- compute_image_higher_signal(img_1=img_1, img_2=img_2),
213
+ compute_image_higher_signal(
214
+ img_1=img_1,
215
+ img_2=img_2,
216
+ ),
197
217
  None,
198
218
  None,
199
219
  )
@@ -208,7 +228,9 @@ class ZStichOverlapKernel(OverlapKernelBase):
208
228
  )
209
229
 
210
230
 
211
- def compute_image_minimum_divergence(img_1: numpy.ndarray, img_2: numpy.ndarray, high_frequency_threshold):
231
+ def compute_image_minimum_divergence(
232
+ img_1: numpy.ndarray, img_2: numpy.ndarray, high_frequency_threshold, stitching_axis: int
233
+ ):
212
234
  """
213
235
  Algorithm to improve treatment of high frequency.
214
236
 
@@ -242,10 +264,20 @@ def compute_image_minimum_divergence(img_1: numpy.ndarray, img_2: numpy.ndarray,
242
264
  low_freq_img_2, high_freq_img_2 = split_image(img_2, threshold=high_frequency_threshold)
243
265
 
244
266
  # handle low frequency
245
- low_freq_stitching_kernel = ZStichOverlapKernel(
246
- frame_width=img_1.shape[1],
267
+ if stitching_axis == 0:
268
+ frame_cst_size = img_1.shape[1]
269
+ overlap_size = img_1.shape[0]
270
+ elif stitching_axis == 1:
271
+ frame_cst_size = img_1.shape[0]
272
+ overlap_size = img_1.shape[1]
273
+ else:
274
+ raise ValueError("")
275
+
276
+ low_freq_stitching_kernel = ImageStichOverlapKernel(
277
+ frame_unstitched_axis_size=frame_cst_size,
247
278
  stitching_strategy=OverlapStitchingStrategy.COSINUS_WEIGHTS,
248
- overlap_size=img_1.shape[0],
279
+ overlap_size=overlap_size,
280
+ stitching_axis=stitching_axis,
249
281
  )
250
282
  low_freq_stitched = low_freq_stitching_kernel.stitch(
251
283
  img_1=low_freq_img_1,
@@ -292,8 +324,8 @@ def check_overlaps(frames: Union[tuple, numpy.ndarray], positions: tuple, axis:
292
324
  """
293
325
  if not isinstance(frames, (tuple, numpy.ndarray)):
294
326
  raise TypeError(f"frames is expected to be a tuple or a numpy array. Get {type(frames)} instead")
295
- if not isinstance(positions, tuple):
296
- raise TypeError(f"positions is expected to be a tuple. Get {type(positions)} instead")
327
+ if not isinstance(positions, tuple) and len(positions) == 3:
328
+ raise TypeError(f"positions is expected to be a tuple of 3 elements. Get {type(positions)} instead")
297
329
  assert isinstance(axis, int), "axis is expected to be an int"
298
330
  assert isinstance(raise_error, bool), "raise_error is expected to be a bool"
299
331
 
@@ -301,14 +333,22 @@ def check_overlaps(frames: Union[tuple, numpy.ndarray], positions: tuple, axis:
301
333
  if raise_error:
302
334
  raise ValueError(error_msg)
303
335
  else:
304
- _logger.error(error_msg)
336
+ _logger.error(raise_error)
337
+
338
+ if axis == 0:
339
+ axis_frame_space = 0
340
+ elif axis == 2:
341
+ raise NotImplementedError(f"overlap check along axis {axis_frame_space}")
342
+ elif axis == 1:
343
+ axis_frame_space = 1
305
344
 
306
345
  # convert each frame to appropriate bounding box according to the axis
307
346
  def convert_to_bb(frame: numpy.ndarray, position: tuple, axis: int):
308
347
  assert isinstance(axis, int)
309
348
  assert isinstance(position, tuple), f"position expected a tuple. Get {type(position)} instead"
310
- start_frame = position[axis] - frame.shape[axis] // 2
311
- end_frame = start_frame + frame.shape[axis]
349
+ assert len(position) == 3, f"Expect to have three items for the position. Get {len(position)}"
350
+ start_frame = position[axis] - frame.shape[axis_frame_space] // 2
351
+ end_frame = start_frame + frame.shape[axis_frame_space]
312
352
  return BoundingBox1D(start_frame, end_frame)
313
353
 
314
354
  bounding_boxes = {
@@ -0,0 +1,32 @@
1
+ from .y_stitching import y_stitching
2
+ from .z_stitching import z_stitching
3
+ from tomoscan.identifier import BaseIdentifier
4
+ from nabu.stitching.config import (
5
+ SingleAxisStitchingConfiguration,
6
+ PreProcessedYStitchingConfiguration,
7
+ PreProcessedZStitchingConfiguration,
8
+ PostProcessedZStitchingConfiguration,
9
+ )
10
+
11
+
12
+ def stitching(configuration: SingleAxisStitchingConfiguration, progress=None) -> BaseIdentifier:
13
+ """
14
+ Apply stitching from provided configuration.
15
+ Stitching will be applied along a single axis at the moment.
16
+
17
+ like:
18
+ axis 0
19
+ ^
20
+ |
21
+ x-ray |
22
+ --------> ------> axis 2
23
+ /
24
+ /
25
+ axis 1
26
+ """
27
+ if isinstance(configuration, (PreProcessedYStitchingConfiguration,)):
28
+ return y_stitching(configuration=configuration, progress=progress)
29
+ elif isinstance(configuration, (PreProcessedZStitchingConfiguration, PostProcessedZStitchingConfiguration)):
30
+ return z_stitching(configuration=configuration, progress=progress)
31
+ else:
32
+ raise NotImplementedError(f"configuration type ({type(configuration)}) is not handled")
@@ -13,8 +13,8 @@ from ..pipeline.config import generate_nabu_configfile
13
13
  from .config import (
14
14
  StitchingConfiguration,
15
15
  get_default_stitching_config,
16
- PreProcessedZStitchingConfiguration,
17
- PostProcessedZStitchingConfiguration,
16
+ PreProcessedSingleAxisStitchingConfiguration,
17
+ PostProcessedSingleAxisStitchingConfiguration,
18
18
  SLURM_SECTION,
19
19
  )
20
20
 
@@ -64,11 +64,11 @@ def split_stitching_configuration_to_slurm_job(
64
64
  configuration.settle_inputs()
65
65
 
66
66
  slice_sub_parts = split_slices(slices=configuration.slices, n_parts=n_jobs)
67
- if isinstance(configuration, PreProcessedZStitchingConfiguration):
67
+ if isinstance(configuration, PreProcessedSingleAxisStitchingConfiguration):
68
68
  stitch_prefix = os.path.basename(os.path.splitext(configuration.output_file_path)[0])
69
69
  configuration.output_file_path = os.path.abspath(configuration.output_file_path)
70
70
 
71
- elif isinstance(configuration, PostProcessedZStitchingConfiguration):
71
+ elif isinstance(configuration, PostProcessedSingleAxisStitchingConfiguration):
72
72
  stitch_prefix = os.path.basename(os.path.splitext(configuration.output_volume.file_path)[0])
73
73
  configuration.output_volume.file_path = os.path.abspath(configuration.output_volume.file_path)
74
74
  else:
@@ -81,7 +81,7 @@ def split_stitching_configuration_to_slurm_job(
81
81
  # remove slurm configuration because once on the partition we run it manually
82
82
  sub_configuration.slurm_config = None
83
83
 
84
- if isinstance(sub_configuration, PreProcessedZStitchingConfiguration):
84
+ if isinstance(sub_configuration, PreProcessedSingleAxisStitchingConfiguration):
85
85
  original_output_file_path, file_extension = os.path.splitext(sub_configuration.output_file_path)
86
86
  sub_configuration.output_file_path = os.path.join(
87
87
  original_output_file_path,
@@ -91,7 +91,7 @@ def split_stitching_configuration_to_slurm_job(
91
91
  scan=sub_configuration.output_file_path,
92
92
  entry=sub_configuration.output_data_path,
93
93
  )
94
- elif isinstance(sub_configuration, PostProcessedZStitchingConfiguration):
94
+ elif isinstance(sub_configuration, PostProcessedSingleAxisStitchingConfiguration):
95
95
  if isinstance(sub_configuration.output_volume, (HDF5Volume, MultiTIFFVolume)):
96
96
  original_output_file_path, file_extension = os.path.splitext(sub_configuration.output_volume.file_path)
97
97
  sub_configuration.output_volume.file_path = os.path.join(
File without changes
@@ -0,0 +1,124 @@
1
+ from copy import copy
2
+ from typing import Union
3
+ from nabu.stitching.config import SingleAxisStitchingConfiguration
4
+ from tomoscan.esrf import NXtomoScan
5
+ from tomoscan.volumebase import VolumeBase
6
+ from tomoscan.identifier import BaseIdentifier
7
+
8
+
9
+ def get_obj_constant_side_length(obj: Union[NXtomoScan, VolumeBase], axis: int) -> int:
10
+ """
11
+ return tomo object lenght that will be constant over 1D stitching.
12
+ In the case of a stitching along axis 0 this will be:
13
+ * the projection width for pre-processing
14
+ * volume.shape[2] for post-processing
15
+
16
+ In the case of a stitching along axis 1 this will be:
17
+ * the projection height for pre-processing
18
+ """
19
+ if isinstance(obj, NXtomoScan):
20
+ if axis == 0:
21
+ return obj.dim_1
22
+ elif axis in (1, 2):
23
+ return obj.dim_2
24
+ elif isinstance(obj, VolumeBase) and axis == 0:
25
+ return obj.get_volume_shape()[-1]
26
+ else:
27
+ raise TypeError(f"obj type ({type(obj)}) and axis == {axis} is not handled")
28
+
29
+
30
+ class _StitcherBase:
31
+ """
32
+ Any stitcher base class
33
+ """
34
+
35
+ def __init__(self, configuration, progress=None) -> None:
36
+ if not isinstance(configuration, SingleAxisStitchingConfiguration):
37
+ raise TypeError
38
+
39
+ # flag to check if the serie has been ordered yet or not
40
+ self._configuration = copy(configuration)
41
+ # copy configuration because we will edit it
42
+ self._frame_composition = None
43
+ self._progress = progress
44
+ self._overlap_kernels = []
45
+ # kernels to create the stitching on overlaps.
46
+
47
+ @property
48
+ def serie_label(self) -> str:
49
+ """return serie name for logs"""
50
+ raise NotImplementedError("Base class")
51
+
52
+ @property
53
+ def reading_orders(self):
54
+ """
55
+ as scan can be take on one direction or the order (rotation goes from X to Y then from Y to X)
56
+ we might need to read data from one direction or another
57
+ """
58
+ return self._reading_orders
59
+
60
+ def order_input_tomo_objects(self):
61
+ """
62
+ order inputs tomo objects
63
+ """
64
+ raise NotImplementedError("Base class")
65
+
66
+ def check_inputs(self):
67
+ """
68
+ order inputs tomo objects
69
+ """
70
+ raise NotImplementedError("Base class")
71
+
72
+ def pre_processing_computation(self):
73
+ """
74
+ some specific pre-processing that can be call before retrieving the data
75
+ """
76
+ pass
77
+
78
+ @staticmethod
79
+ def param_is_auto(param):
80
+ return param in ("auto", ("auto",))
81
+
82
+ def stitch(self, store_composition: bool = True) -> BaseIdentifier:
83
+ """
84
+ Apply expected stitch from configuration and return the DataUrl of the object created
85
+
86
+ :param bool store_composition: if True then store the composition used for stitching in frame_composition.
87
+ So it can be reused by third part (like tomwer) to display composition made
88
+ """
89
+ raise NotImplementedError("base class")
90
+
91
+ @property
92
+ def frame_composition(self):
93
+ return self._frame_composition
94
+
95
+ @staticmethod
96
+ def from_abs_pos_to_rel_pos(abs_position: tuple):
97
+ """
98
+ return relative position from on object to the other but in relative this time
99
+ :param tuple abs_position: tuple containing the absolute positions
100
+ :return: len(abs_position) - 1 relative position
101
+ :rtype: tuple
102
+ """
103
+ return tuple([pos_obj_b - pos_obj_a for (pos_obj_a, pos_obj_b) in zip(abs_position[:-1], abs_position[1:])])
104
+
105
+ @staticmethod
106
+ def from_rel_pos_to_abs_pos(rel_positions: tuple, init_pos: int):
107
+ """
108
+ return absolute positions from a tuple of relative position and an initial position
109
+ :param tuple rel_positions: tuple containing the absolute positions
110
+ :return: len(rel_positions) + 1 relative position
111
+ :rtype: tuple
112
+ """
113
+ abs_pos = [
114
+ init_pos,
115
+ ]
116
+ for rel_pos in rel_positions:
117
+ abs_pos.append(abs_pos[-1] + rel_pos)
118
+ return abs_pos
119
+
120
+ def _compute_shifts(self):
121
+ """
122
+ after this stage the final shifts must be determine
123
+ """
124
+ raise NotImplementedError("base class")
@@ -0,0 +1,3 @@
1
+ from .postprocessing import PostProcessingStitchingDumper
2
+ from .postprocessing import PostProcessingStitchingDumperNoDD
3
+ from .preprocessing import PreProcessingStitchingDumper