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
@@ -0,0 +1,424 @@
1
+ import math
2
+ import numpy as np
3
+
4
+ from ..utils import get_cuda_srcfile
5
+ from ..cuda.processing import __has_pycuda__
6
+
7
+ if __has_pycuda__:
8
+ from ..cuda.kernel import CudaKernel
9
+ from .sinogram_cuda import CudaSinoMult
10
+
11
+ from .fbp import CudaBackprojector
12
+
13
+
14
+ try:
15
+ import pycuda.driver as cuda
16
+ from pycuda import gpuarray as garray
17
+
18
+ __have_hbp__ = True
19
+ except:
20
+ __have_hbp__ = False
21
+
22
+
23
+ def buildConebeamGeometry(
24
+ anglesRad, rotAxisProjectionFromLeftPixelUnits, sourceSampleDistanceVoxelUnits, opticalAxisFromLeftPixelUnits=None
25
+ ):
26
+ """Generate fanbeam/conebeam projection matrices (as required by the backprojector) based on geometry parameters"""
27
+ if opticalAxisFromLeftPixelUnits is None:
28
+ if hasattr(rotAxisProjectionFromLeftPixelUnits, "__iter__"):
29
+ opticalAxisFromLeftPixelUnits = rotAxisProjectionFromLeftPixelUnits[0]
30
+ else:
31
+ opticalAxisFromLeftPixelUnits = rotAxisProjectionFromLeftPixelUnits
32
+
33
+ t = opticalAxisFromLeftPixelUnits
34
+ d = sourceSampleDistanceVoxelUnits
35
+
36
+ if hasattr(rotAxisProjectionFromLeftPixelUnits, "__iter__"):
37
+ P_list = [
38
+ np.array([[0, -t / d, 1, a], [1, 0, 0, 0], [0, -1 / d, 0, 1]], dtype=np.float64) # pylint: disable=E1130
39
+ for a in rotAxisProjectionFromLeftPixelUnits
40
+ ]
41
+ else:
42
+ a = rotAxisProjectionFromLeftPixelUnits
43
+ P_list = [
44
+ np.array([[0, -t / d, 1, a], [1, 0, 0, 0], [0, -1 / d, 0, 1]], dtype=np.float64) # pylint: disable=E1130
45
+ ] * len(anglesRad)
46
+
47
+ R = lambda w: np.array(
48
+ [[1, 0, 0, 0], [0, np.cos(w), np.sin(w), 0], [0, -np.sin(w), np.cos(w), 0], [0, 0, 0, 1]], dtype=np.float64
49
+ )
50
+ return np.array([P @ R(-w) for P, w in zip(P_list, anglesRad)])
51
+
52
+
53
+ class HierarchicalBackprojector(CudaBackprojector):
54
+ kernel_filename = "hierarchical_backproj.cu"
55
+
56
+ def _init_geometry(self, sino_shape, slice_shape, angles, rot_center, halftomo, slice_roi):
57
+ super()._init_geometry(sino_shape, slice_shape, angles, rot_center, halftomo, slice_roi)
58
+ # pylint: disable=E1130 # -angles because different convention for the rotation direction
59
+ self.angles = -self.angles
60
+
61
+ # to do the reconstruction in reduction_steps steps
62
+ self.reduction_steps = self.extra_options.get("hbp_reduction_steps", 2)
63
+ reduction_factor = int(math.ceil((sino_shape[-2]) ** (1 / self.reduction_steps)))
64
+
65
+ # TODO customize
66
+ axis_source_meters = 1.0e9
67
+ voxel_size_microns = 1.0
68
+ #
69
+
70
+ axis_cor = self.extra_options.get("axis_correction", None)
71
+ if axis_cor is None:
72
+ axis_cor = 0
73
+ bpgeometry = buildConebeamGeometry(
74
+ self.angles, self.rot_center + axis_cor, 1.0e6 * axis_source_meters / voxel_size_microns
75
+ )
76
+ self.setup_hbp(bpgeometry, reductionFactor=reduction_factor, legs=self.extra_options.get("hbp_legs", 4))
77
+
78
+ def setup_hbp(
79
+ self,
80
+ bpgeometry,
81
+ reductionFactor=20,
82
+ grid_wh_factors=(1, 1),
83
+ fac=1,
84
+ legs=4,
85
+ ):
86
+
87
+ # This implementation seems not to use textures
88
+ self._use_textures = False
89
+
90
+ # for the non texture implementation, this big number will discard texture limitations
91
+ large_factor_for_non_texture_memory_access = 2**10
92
+ # TODO: read limits from device info.
93
+ self.GPU_MAX_GRIDSIZE = 2**15 * large_factor_for_non_texture_memory_access
94
+ self.GPU_MAX_GRIDS = 2**11 * large_factor_for_non_texture_memory_access
95
+
96
+ if self.sino_shape[0] != len(bpgeometry):
97
+ raise ValueError("self.sino_shape[0] != len(bpgeometry)")
98
+ if self.sino_shape[0] != len(self.angles):
99
+ raise ValueError("self.sino_shape[0] != len(self.angles)")
100
+
101
+ if self.sino_shape[1] > self.GPU_MAX_GRIDSIZE:
102
+ raise ValueError(f"self.sino_shape[1] > {self.GPU_MAX_GRIDSIZE} not supported by GPU")
103
+ if self.sino_shape[0] > self.GPU_MAX_GRIDSIZE:
104
+ raise ValueError(f"self.sino_shape[0] > {self.GPU_MAX_GRIDSIZE} currently not supported")
105
+
106
+ self.reductionFactor = reductionFactor
107
+ self.legs = legs
108
+
109
+ self.bpsetupsH = bpgeometry.astype(np.float32)
110
+ # self.bpsetupsD = cuda.mem_alloc(self.bpsetupsH.nbytes)
111
+ # cuda.memcpy_htod(self.bpsetupsD, self.bpsetupsH)
112
+ self.bpsetupsD = self._processing.to_device("bpsetupsD", self.bpsetupsH)
113
+
114
+ # if allocate_cuda_sinogram:
115
+ # self.sinogramD = cuda.mem_alloc(self.sino_shape[0] * self.sino_shape[1] * self.float_size)
116
+ # else:
117
+ # self.sinogramD = None
118
+ self.sinogramD = None
119
+
120
+ self.whf = grid_wh_factors
121
+ if self.sino_shape[1] * 2 * self.whf[0] * fac > self.GPU_MAX_GRIDSIZE:
122
+ print(f"WARNING: gridsampling limited to {self.GPU_MAX_GRIDSIZE}")
123
+ self.whf[0] = self.GPU_MAX_GRIDSIZE / (self.sino_shape[1] * 2 * fac)
124
+
125
+ ###############################################
126
+ ########## create intermediate grids ##########
127
+ ###############################################
128
+
129
+ self.reductionFactors = []
130
+ self.grids = [] # shapes
131
+ self.gridTransforms = [] # grid-to-world
132
+ self.gridInvTransforms = [] # world-to-grid
133
+ self.gridTransformsH = [] # host buffer
134
+ self.gridTransformsD = [] # device buffer
135
+
136
+ ### first level grid: will receive backprojections #
137
+ ####################################################
138
+
139
+ N = self.slice_shape[1] * fac
140
+
141
+ angularRange = abs(self.angles.ptp()) / self.sino_shape[0] * reductionFactor
142
+
143
+ ngrids = int(math.ceil(self.sino_shape[0] / reductionFactor))
144
+
145
+ grid_width = int(
146
+ np.rint(2 * N * self.whf[0])
147
+ ) # double sampling to account/compensate for diamond shaped grid of ray-intersections
148
+ grid_height = int(
149
+ math.ceil(angularRange * N * self.whf[1])
150
+ ) # small-angle approximation, generates as much "lines" as needed to account for all intersection levels
151
+
152
+ m = (len(self.angles) // reductionFactor) * reductionFactor
153
+ # TODO: improve angle calculation for more general cases
154
+ tmpangles = np.angle(
155
+ np.average(np.exp(1.0j * self.angles[:m].reshape(m // reductionFactor, reductionFactor)), axis=1)
156
+ )
157
+
158
+ tmpangles = np.concatenate((tmpangles, (np.angle(np.average(np.exp(1.0j * self.angles[m:]))),)))[:ngrids]
159
+ gridAinvT = self._getAinvT(N, grid_height, grid_width)
160
+ setupRs = self._getRotationMatrices(tmpangles)
161
+
162
+ pad = int(math.ceil(ngrids / legs) * legs - ngrids) # add nan-padding for inline-signaling of unused grids
163
+ self.gridTransforms += [
164
+ np.array(
165
+ [(R @ gridAinvT) for R in setupRs] + [np.ones((3, 3), np.float32) * math.nan] * pad, dtype=np.float32
166
+ )
167
+ ]
168
+ self.gridInvTransforms += [np.array([np.linalg.inv(t) for t in self.gridTransforms[-1]], dtype=np.float32)]
169
+ self.grids += [(grid_height, grid_width, int(math.ceil(ngrids / legs)))]
170
+ self.reductionFactors += [reductionFactor]
171
+
172
+ ### intermediate level grids: accumulation grids ###
173
+ ####################################################
174
+
175
+ # Actual iteration count typically within 1-5. Cf. break condition
176
+ for i in range(100):
177
+ # for a reasonable (with regard to memory requirement) grid-aspect ratio in the intermediate levels,
178
+ # the covered angular range per grid should not exceed 28.6°, i.e.,
179
+ # fewer than 7 (6.3) or 13 (12.6) grids for a 180° / 360° scan is not reasonable
180
+ if int(math.ceil(ngrids / reductionFactor)) < 20:
181
+ break
182
+ angularRange *= reductionFactor
183
+ ngrids = int(math.ceil(ngrids / reductionFactor))
184
+
185
+ grid_height = int(
186
+ math.ceil(angularRange * N * self.whf[1])
187
+ ) # implicit small angle approximation, whose validity is
188
+ # asserted by the preceding "break"
189
+ gridAinvT = self._getAinvT(N, grid_height, grid_width)
190
+
191
+ prevAngles = tmpangles
192
+ m = (len(prevAngles) // reductionFactor) * reductionFactor
193
+ # TODO: improve angle calculation for more general cases
194
+ tmpangles = np.angle(
195
+ np.average(np.exp(1.0j * prevAngles[:m].reshape(m // reductionFactor, reductionFactor)), axis=1)
196
+ )
197
+ tmpangles = np.concatenate((tmpangles, (np.angle(np.average(np.exp(1.0j * prevAngles[m:]))),)))[:ngrids]
198
+ setupRsRed = self._getRotationMatrices(tmpangles)
199
+
200
+ pad = int(math.ceil(ngrids / legs) * legs - ngrids)
201
+ self.gridTransforms += [
202
+ np.array(
203
+ [(R @ gridAinvT) for R in setupRsRed] + [np.ones((3, 3), np.float32) * math.nan] * pad,
204
+ dtype=np.float32,
205
+ )
206
+ ]
207
+ self.gridInvTransforms += [np.array([np.linalg.inv(t) for t in self.gridTransforms[-1]], dtype=np.float32)]
208
+ self.grids += [(grid_height, grid_width, int(math.ceil(ngrids / legs)))]
209
+ self.reductionFactors += [reductionFactor]
210
+
211
+ ##### final accumulation grid #################
212
+ ###############################################
213
+
214
+ reductionFactor = ngrids
215
+ ngrids = 1
216
+ grid_size = self.slice_shape[1]
217
+ grid_width = grid_size
218
+ grid_height = grid_size
219
+
220
+ # gridAinvT = self._getAinvT(N, grid_height, grid_width)
221
+ gridAinvT = self._getAinvT(N, grid_height, grid_width, 1 / fac)
222
+
223
+ self.gridTransforms += [
224
+ np.array([gridAinvT] * legs, dtype=np.float32)
225
+ ] # inflate transform list for convenience in reconstruction loop
226
+ self.gridInvTransforms += [np.array([np.linalg.inv(t) for t in self.gridTransforms[-1]], dtype=np.float32)]
227
+ self.grids += [(grid_height, grid_width, ngrids)]
228
+ self.reductionFactors += [reductionFactor]
229
+
230
+ #### accumulation grids #####
231
+ self.gridTransformsD = []
232
+ self.gridInvTransformsD = []
233
+ self.gridsD = []
234
+
235
+ max_grid_size = get_max_grid_size(self.grids)
236
+
237
+ for i in range(len(self.grids)):
238
+ gridTransformH = np.array(self.gridTransforms[i][:, :2, :3], dtype=np.float32, order="C").copy()
239
+ gridInvTransformH = np.array(self.gridInvTransforms[i][:, :2, :3], dtype=np.float32, order="C").copy()
240
+ self.gridTransformsD.append(self._processing.to_device("gridTransformsD%d " % i, gridTransformH.ravel()))
241
+ self.gridInvTransformsD.append(
242
+ self._processing.to_device("gridInvTransformsD%d" % i, gridInvTransformH.ravel())
243
+ )
244
+
245
+ if legs == 1 or i + 1 != (len(self.grids)):
246
+ if i < 2:
247
+ self.gridsD.append(self._processing.allocate_array("gridsD%d" % i, max_grid_size))
248
+ else:
249
+ self.gridsD.append(self.gridsD[i % 2])
250
+ else:
251
+ self.gridsD.append(self._processing.allocate_array("gridsD%d" % i, get_max_grid_size(self.grids[-1:])))
252
+
253
+ self.imageBufferShape = (grid_size, grid_size)
254
+ self.imageBufferD = self._processing.allocate_array(
255
+ "imageBufferD", self.imageBufferShape[0] * self.imageBufferShape[1]
256
+ )
257
+ self.imageBufferH = np.zeros(self.imageBufferShape, dtype=np.float32)
258
+
259
+ def _getAinvT(self, finalGridWidthAndHeight, currentGridHeight, currentGridWidth, scale=1):
260
+ N = finalGridWidthAndHeight
261
+ grid_height = currentGridHeight
262
+ grid_width = currentGridWidth
263
+
264
+ # shifts a texture coordinate from corner origin to center origin
265
+ T = np.array(((1, 0, -0.5 * (grid_height - 1)), (0, 1, -0.5 * (grid_width - 1)), (0, 0, 1)), dtype=np.float32)
266
+ # scales texture coordinates (of subsampled grid) into the unit/cooridnate system of a fully sampled grid
267
+ Ainv = np.array(
268
+ (((N - 1) / (grid_height - 1) * scale, 0, 0), (0, (N - 1) / (grid_width - 1) * scale, 0), (0, 0, 1)),
269
+ dtype=np.float32,
270
+ )
271
+ return Ainv @ T
272
+
273
+ def _getRotationMatrices(self, angles):
274
+ return [
275
+ np.array(((np.cos(a), np.sin(a), 0), (-np.sin(a), np.cos(a), 0), (0, 0, 1)), dtype=np.float32)
276
+ for a in angles
277
+ ]
278
+
279
+ def _compile_kernels(self):
280
+ # pylint: disable=E0606
281
+ self.backprojector = CudaKernel(
282
+ "backprojector",
283
+ filename=get_cuda_srcfile(self.kernel_filename),
284
+ signature="PPiiiiPiifPi",
285
+ )
286
+ self.aggregator = CudaKernel(
287
+ "aggregator", filename=get_cuda_srcfile(self.kernel_filename), signature="iPPiiiiPiiiP"
288
+ )
289
+ self.clip_outer_circle_kernel = CudaKernel(
290
+ "clip_outer_circle", filename=get_cuda_srcfile(self.kernel_filename), signature="Pii"
291
+ )
292
+ # Duplicate of fbp.py ...
293
+ if self.halftomo and self.rot_center < self.dwidth:
294
+ self.sino_mult = CudaSinoMult(self.sino_shape, self.rot_center, ctx=self._processing.ctx)
295
+ #
296
+
297
+ def _set_sino(self, sino, do_checks=True):
298
+ if do_checks and not (sino.flags.c_contiguous):
299
+ raise ValueError("Expected C-Contiguous array")
300
+ else:
301
+ self._d_sino = self._processing.allocate_array("_d_sino", self.sino_shape)
302
+ if id(self._d_sino) == id(sino):
303
+ return
304
+ self._d_sino[:] = sino[:]
305
+
306
+ def backproj(self, sino, output=None, do_checks=True, reference=False):
307
+ if self.halftomo and self.rot_center < self.dwidth:
308
+ self.sino_mult.prepare_sino(sino)
309
+ self._set_sino(sino)
310
+ lws = (64, 4, 4)
311
+
312
+ if reference:
313
+ gws = getGridSize(self.grids[-1], lws)
314
+ (grid_height, grid_width, ngrids) = self.grids[-1]
315
+
316
+ self.backprojector(
317
+ self.bpsetupsD,
318
+ self.gridTransformsD[-1].gpudata,
319
+ np.int32(self.sino_shape[0]),
320
+ np.int32(grid_width),
321
+ np.int32(grid_height),
322
+ np.int32(ngrids),
323
+ self.gridsD[-1],
324
+ np.int32(self.sino_shape[1]),
325
+ np.int32(self.sino_shape[0]),
326
+ np.float32(self._backproj_scale_factor),
327
+ self._d_sino,
328
+ np.int32(0), # offset
329
+ block=lws,
330
+ grid=gws,
331
+ )
332
+
333
+ else:
334
+ for leg in list(range(0, self.legs)):
335
+ gridOffset = leg * self.grids[0][2]
336
+ projOffset = gridOffset * self.reductionFactors[0]
337
+ gws = getGridSize(self.grids[0], lws)
338
+ (grid_height, grid_width, ngrids) = self.grids[0]
339
+
340
+ self.backprojector(
341
+ self.bpsetupsD,
342
+ self.gridTransformsD[0][6 * gridOffset :],
343
+ np.int32(self.reductionFactors[0]),
344
+ np.int32(grid_width),
345
+ np.int32(grid_height),
346
+ np.int32(ngrids),
347
+ self.gridsD[0],
348
+ np.int32(self.sino_shape[1]),
349
+ np.int32(self.sino_shape[0]),
350
+ np.float32(self._backproj_scale_factor),
351
+ self._d_sino,
352
+ np.int32(projOffset),
353
+ block=lws,
354
+ grid=gws,
355
+ )
356
+
357
+ for i in range(1, len(self.grids)):
358
+ if self.grids[i][2] >= 8:
359
+ lws = (16, 16, 4)
360
+ else:
361
+ lws = (32, 32, 1)
362
+
363
+ gws = getGridSize(self.grids[i], lws)
364
+
365
+ (new_grid_height, new_grid_width, new_ngrids) = self.grids[i]
366
+ (prev_grid_height, prev_grid_width, prev_ngrids) = self.grids[i - 1]
367
+
368
+ gridOffset = leg * self.grids[i][2]
369
+ prevGridOffset = leg * self.grids[i - 1][2]
370
+
371
+ self.aggregator(
372
+ np.int32((i + 1 == len(self.grids)) and (leg > 0)),
373
+ self.gridTransformsD[i][6 * gridOffset :],
374
+ self.gridInvTransformsD[i - 1][6 * prevGridOffset :],
375
+ np.int32(self.reductionFactors[i]),
376
+ np.int32(new_grid_width),
377
+ np.int32(new_grid_height),
378
+ np.int32(new_ngrids),
379
+ self.gridsD[i],
380
+ np.int32(prev_grid_width),
381
+ np.int32(prev_grid_height),
382
+ np.int32(prev_ngrids),
383
+ self.gridsD[i - 1],
384
+ block=lws,
385
+ grid=gws,
386
+ )
387
+
388
+ if self.extra_options.get("clip_outer_circle", False):
389
+ lws = (16, 16, 1)
390
+ ny, nx = self.slice_shape
391
+ gws = getGridSize((nx, ny, 1), lws)
392
+ self.clip_outer_circle_kernel(self.gridsD[-1], np.int32(ny), np.int32(nx), block=lws, grid=gws)
393
+
394
+ # FIXME pycuda fails to do a discontiguous memcpy for more than 2^31 bytes
395
+ if self.gridsD[-1].nbytes > 2**31:
396
+ r1d = self.gridsD[-1].get()
397
+ r2d = np.ascontiguousarray(r1d.reshape(self.slice_shape))
398
+ if output is not None:
399
+ output[:] = r2d[:]
400
+ return output
401
+ else:
402
+ return r2d
403
+ # --------
404
+
405
+ else:
406
+ return self.gridsD[-1].reshape(self.slice_shape).get(ary=output)
407
+
408
+
409
+ def get_max_grid_size(grids):
410
+ size_max = 0
411
+ for dims in grids:
412
+ size = 1
413
+ for d in dims:
414
+ size = size * d
415
+ if size > size_max:
416
+ size_max = size
417
+ return size_max
418
+
419
+
420
+ def getGridSize(minimum, local):
421
+ m, l = np.array(minimum), np.array(local)
422
+ new = (m // l) * l
423
+ new[new < m] += l[new < m]
424
+ return tuple(map(int, new // l))
@@ -0,0 +1,99 @@
1
+ import numpy as np
2
+
3
+
4
+ try:
5
+ import corrct as cct
6
+
7
+ __have_corrct__ = True
8
+ except ImportError:
9
+ __have_corrct__ = False
10
+
11
+
12
+ class MLEMReconstructor:
13
+ """
14
+ A reconstructor for MLEM reconstruction using the CorrCT toolbox.
15
+ """
16
+
17
+ default_extra_options = {
18
+ "compute_shifts": False,
19
+ "tomo_consistency": False,
20
+ "v_min_for_v_shifts": 0,
21
+ "v_max_for_v_shifts": None,
22
+ "v_min_for_u_shifts": 0,
23
+ "v_max_for_u_shifts": None,
24
+ }
25
+
26
+ def __init__(
27
+ self,
28
+ sinos_shape,
29
+ angles_rad,
30
+ shifts_uv=None,
31
+ cor=None,
32
+ n_iterations=50,
33
+ extra_options=None,
34
+ ):
35
+ """ """
36
+ if not (__have_corrct__):
37
+ raise ImportError("Need corrct package")
38
+ self.angles_rad = angles_rad
39
+ self.n_iterations = n_iterations
40
+
41
+ self._configure_extra_options(extra_options)
42
+ self._set_sino_shape(sinos_shape)
43
+ self._set_shifts(shifts_uv, cor)
44
+
45
+ def _configure_extra_options(self, extra_options):
46
+ self.extra_options = self.default_extra_options.copy()
47
+ self.extra_options.update(extra_options or {})
48
+
49
+ def _set_sino_shape(self, sinos_shape):
50
+ if len(sinos_shape) != 3:
51
+ raise ValueError("Expected a 3D shape")
52
+ self.sinos_shape = sinos_shape
53
+ self.n_sinos, self.n_angles, self.prj_width = sinos_shape
54
+ if self.n_angles != len(self.angles_rad):
55
+ raise ValueError(
56
+ f"Number of angles ({len(self.angles_rad)}) does not match size of sinograms ({self.n_angles})."
57
+ )
58
+
59
+ def _set_shifts(self, shifts_uv, cor):
60
+ if shifts_uv is None:
61
+ self.shifts_uv = np.zeros([self.n_angles, 2])
62
+ else:
63
+ if shifts_uv.shape[0] != self.n_angles:
64
+ raise ValueError(
65
+ f"Number of shifts given ({shifts_uv.shape[0]}) does not mathc the number of projections ({self.n_angles})."
66
+ )
67
+ self.shifts_uv = shifts_uv.copy()
68
+ self.cor = cor
69
+
70
+ def reconstruct(self, data_vwu):
71
+ """
72
+ data_align_vwu: numpy.ndarray or pycuda.gpuarray
73
+ Raw data, with shape (n_sinograms, n_angles, width)
74
+ output: optional
75
+ Output array. If not provided, a new numpy array is returned
76
+ """
77
+ if not isinstance(data_vwu, np.ndarray):
78
+ data_vwu = data_vwu.get()
79
+ data_vwu /= data_vwu.mean()
80
+
81
+ # MLEM recons
82
+ self.vol_geom_align = cct.models.VolumeGeometry.get_default_from_data(data_vwu)
83
+ self.prj_geom_align = cct.models.ProjectionGeometry.get_default_parallel()
84
+ # Vertical shifts were handled in pipeline. Set them to ZERO
85
+ self.shifts_uv[:, 1] = 0.0
86
+ self.prj_geom_align.set_detector_shifts_vu(self.shifts_uv.T[::-1])
87
+
88
+ variances_align = cct.processing.compute_variance_poisson(data_vwu)
89
+ self.weights_align = cct.processing.compute_variance_weight(variances_align, normalized=True) # , use_std=True
90
+ self.data_term_align = cct.data_terms.DataFidelity_wl2(self.weights_align)
91
+ solver = cct.solvers.MLEM(verbose=True, data_term=self.data_term_align)
92
+ self.solver_opts = dict(lower_limit=0) # , x_mask=cct.processing.circular_mask(vol_geom_align.shape_xyz[:-2])
93
+
94
+ with cct.projectors.ProjectorUncorrected(
95
+ self.vol_geom_align, self.angles_rad, rot_axis_shift_pix=self.cor, prj_geom=self.prj_geom_align
96
+ ) as A:
97
+ rec, _ = solver(A, data_vwu, iterations=self.n_iterations, **self.solver_opts)
98
+
99
+ return rec
@@ -153,6 +153,8 @@ class Reconstructor:
153
153
  if self.axis == "x":
154
154
  self.backprojector_roi = (self._idx_start, self._idx_end, start_u, end_u)
155
155
  start_z, end_z = start_v, end_v
156
+ else:
157
+ raise ValueError("Invalid axis")
156
158
  self._z_indices = np.arange(start_z, end_z)
157
159
  self.output_shape = (
158
160
  self._z_indices.size,
@@ -4,7 +4,6 @@ from ..cuda.processing import CudaProcessing, __has_pycuda__
4
4
  from ..processing.padding_cuda import CudaPadding
5
5
  from ..processing.fft_cuda import get_fft_class, get_available_fft_implems
6
6
  from ..processing.transpose import CudaTranspose
7
- from ..thirdparty.tomocupy_remove_stripe import remove_all_stripe_pycuda, __have_tomocupy_deringer__
8
7
  from .rings import MunchDeringer, SinoMeanDeringer, VoDeringer
9
8
 
10
9
  if __has_pycuda__:
@@ -18,6 +17,8 @@ try:
18
17
  except ImportError:
19
18
  __have_pycudwt__ = False
20
19
 
20
+ # pylint: disable=E0606
21
+
21
22
 
22
23
  class CudaMunchDeringer(MunchDeringer):
23
24
  def __init__(
@@ -28,7 +29,7 @@ class CudaMunchDeringer(MunchDeringer):
28
29
  wname="db15",
29
30
  padding=None,
30
31
  padding_mode="edge",
31
- fft_backend="skcuda",
32
+ fft_backend="vkfft",
32
33
  cuda_options=None,
33
34
  ):
34
35
  """
@@ -63,8 +64,8 @@ class CudaMunchDeringer(MunchDeringer):
63
64
  self._setup_fw_kernel()
64
65
 
65
66
  def _check_can_use_wavelets(self):
66
- if not (__have_pycudwt__):
67
- raise ValueError("Needs pycudwt to use this class")
67
+ if not (__have_pycudwt__ and __has_pycuda__):
68
+ raise ValueError("Needs pycuda and pycudwt to use this class")
68
69
 
69
70
  def _init_padding(self):
70
71
  if self.padding is None:
@@ -220,20 +221,22 @@ class CudaVoDeringer(VoDeringer):
220
221
  """
221
222
 
222
223
  def _check_requirement(self):
224
+ # Do it here, otherwise cupy shows warnings at import even if not used
225
+ from ..thirdparty.tomocupy_remove_stripe import remove_all_stripe_pycuda, __have_tomocupy_deringer__
226
+
223
227
  if not (__have_tomocupy_deringer__):
224
228
  raise ImportError("need cupy")
229
+ self._remove_all_stripe_pycuda = remove_all_stripe_pycuda
225
230
 
226
231
  def remove_rings_radios(self, radios):
227
- return remove_all_stripe_pycuda(radios, **self._remove_all_stripe_kwargs)
232
+ return self._remove_all_stripe_pycuda(radios, layout="radios", **self._remove_all_stripe_kwargs)
228
233
 
229
234
  def remove_rings_sinograms(self, sinos):
230
- radios = sinos.transpose(axes=(1, 0, 2)) # view, no copy
231
- self.remove_rings_radios(radios)
232
- return sinos
235
+ return self._remove_all_stripe_pycuda(sinos, layout="sinos", **self._remove_all_stripe_kwargs)
233
236
 
234
237
  def remove_rings_sinogram(self, sino):
235
- radios = sino.reshape(sino.shape[0], 1, sino.shape[1]) # no copy
236
- self.remove_rings_radios(radios)
238
+ sinos = sino.reshape((1, sino.shape[0], -1)) # no copy
239
+ self.remove_rings_sinograms(sinos)
237
240
  return sino
238
241
 
239
242
  remove_rings = remove_rings_sinograms
@@ -261,10 +264,9 @@ class CudaSinoMeanDeringer(SinoMeanDeringer):
261
264
  filename=get_cuda_srcfile("normalization.cu"),
262
265
  signature="PPiii",
263
266
  )
264
- self._mean_kernel_block = (32, 1, 1)
265
- self._mean_kernel_grid = [updiv(self.sinos_shape[-1], self._mean_kernel_block[0]), 1, 1]
266
- self._mean_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(1)]
267
-
267
+ self._mean_kernel_block = (32, 1, 32)
268
+ self._mean_kernel_grid = [updiv(a, b) for a, b in zip(self.sinos_shape[::-1], self._mean_kernel_block)]
269
+ self._mean_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(self.n_z)]
268
270
  self._mean_kernel_kwargs = {
269
271
  "grid": self._mean_kernel_grid,
270
272
  "block": self._mean_kernel_block,
@@ -276,11 +278,9 @@ class CudaSinoMeanDeringer(SinoMeanDeringer):
276
278
  signature="PPiii",
277
279
  options=["-DGENERIC_OP=%d" % (3 if self.mode == "divide" else 1)],
278
280
  )
279
- self._op_kernel_block = (16, 16, 1)
280
- self._op_kernel_grid = [updiv(a, b) for a, b in zip(self.sinos_shape[1:][::-1], self._op_kernel_block[:-1])] + [
281
- 1
282
- ]
283
- self._op_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(1)]
281
+ self._op_kernel_block = (16, 16, 4)
282
+ self._op_kernel_grid = [updiv(a, b) for a, b in zip(self.sinos_shape[::-1], self._op_kernel_block)]
283
+ self._op_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(self.n_z)]
284
284
  self._op_kernel_kwargs = {
285
285
  "grid": self._op_kernel_grid,
286
286
  "block": self._op_kernel_block,
@@ -188,6 +188,7 @@ class CudaSinoNormalization(SinoNormalization):
188
188
  self._d_normalization_array = self.cuda_processing.to_device(
189
189
  "_d_normalization_array", normalization_array.astype("f")
190
190
  )
191
+ # pylint: disable=E0606
191
192
  if self.normalization_kind == "subtraction":
192
193
  generic_op_val = 1
193
194
  elif self.normalization_kind == "division":
@@ -30,11 +30,13 @@ class OpenCLSinoMult(SinoMult):
30
30
 
31
31
  def prepare_sino(self, sino):
32
32
  sino = self.opencl_processing.set_array("d_sino", sino)
33
- self.halftomo_kernel(
33
+ ev = self.halftomo_kernel(
34
34
  self.opencl_processing.queue,
35
35
  sino,
36
36
  *self._halftomo_kernel_other_args,
37
37
  global_size=self._global_size,
38
38
  local_size=self._local_size,
39
39
  )
40
+ if self.opencl_processing.device_type == "cpu":
41
+ ev.wait()
40
42
  return sino