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.
- nabu/__init__.py +1 -1
- nabu/app/bootstrap.py +2 -3
- nabu/app/cast_volume.py +4 -2
- nabu/app/cli_configs.py +5 -0
- nabu/app/composite_cor.py +1 -1
- nabu/app/create_distortion_map_from_poly.py +5 -6
- nabu/app/diag_to_pix.py +7 -19
- nabu/app/diag_to_rot.py +14 -29
- nabu/app/double_flatfield.py +32 -44
- nabu/app/parse_reconstruction_log.py +3 -0
- nabu/app/reconstruct.py +53 -15
- nabu/app/reconstruct_helical.py +2 -2
- nabu/app/stitching.py +27 -13
- nabu/app/tests/__init__.py +0 -0
- nabu/app/tests/test_reduce_dark_flat.py +4 -1
- nabu/cuda/kernel.py +11 -2
- nabu/cuda/processing.py +2 -2
- nabu/cuda/src/cone.cu +77 -0
- nabu/cuda/src/hierarchical_backproj.cu +271 -0
- nabu/cuda/utils.py +0 -6
- nabu/estimation/alignment.py +5 -19
- nabu/estimation/cor.py +173 -599
- nabu/estimation/cor_sino.py +356 -26
- nabu/estimation/focus.py +63 -11
- nabu/estimation/tests/test_cor.py +124 -58
- nabu/estimation/tests/test_focus.py +6 -6
- nabu/estimation/tilt.py +2 -1
- nabu/estimation/utils.py +5 -33
- nabu/io/__init__.py +1 -1
- nabu/io/cast_volume.py +1 -1
- nabu/io/reader.py +416 -21
- nabu/io/tests/test_readers.py +422 -0
- nabu/io/tests/test_writers.py +1 -102
- nabu/io/writer.py +4 -433
- nabu/opencl/kernel.py +14 -3
- nabu/opencl/processing.py +8 -0
- nabu/pipeline/config_validators.py +5 -2
- nabu/pipeline/datadump.py +12 -5
- nabu/pipeline/estimators.py +162 -188
- nabu/pipeline/fullfield/chunked.py +168 -92
- nabu/pipeline/fullfield/chunked_cuda.py +7 -3
- nabu/pipeline/fullfield/computations.py +2 -7
- nabu/pipeline/fullfield/dataset_validator.py +0 -4
- nabu/pipeline/fullfield/nabu_config.py +37 -13
- nabu/pipeline/fullfield/processconfig.py +22 -13
- nabu/pipeline/fullfield/reconstruction.py +13 -9
- nabu/pipeline/helical/helical_chunked_regridded.py +1 -1
- nabu/pipeline/helical/helical_chunked_regridded_cuda.py +1 -0
- nabu/pipeline/helical/helical_reconstruction.py +1 -1
- nabu/pipeline/params.py +21 -1
- nabu/pipeline/processconfig.py +1 -12
- nabu/pipeline/reader.py +146 -0
- nabu/pipeline/tests/test_estimators.py +44 -72
- nabu/pipeline/utils.py +4 -2
- nabu/pipeline/writer.py +10 -2
- nabu/preproc/ccd_cuda.py +1 -1
- nabu/preproc/ctf.py +14 -7
- nabu/preproc/ctf_cuda.py +2 -3
- nabu/preproc/double_flatfield.py +5 -12
- nabu/preproc/double_flatfield_cuda.py +2 -2
- nabu/preproc/flatfield.py +5 -1
- nabu/preproc/flatfield_cuda.py +5 -1
- nabu/preproc/phase.py +24 -73
- nabu/preproc/phase_cuda.py +5 -8
- nabu/preproc/tests/test_ctf.py +11 -7
- nabu/preproc/tests/test_flatfield.py +67 -122
- nabu/preproc/tests/test_paganin.py +54 -30
- nabu/processing/azim.py +206 -0
- nabu/processing/convolution_cuda.py +1 -1
- nabu/processing/fft_cuda.py +15 -17
- nabu/processing/histogram.py +2 -0
- nabu/processing/histogram_cuda.py +2 -1
- nabu/processing/kernel_base.py +3 -0
- nabu/processing/muladd_cuda.py +1 -0
- nabu/processing/padding_opencl.py +1 -1
- nabu/processing/roll_opencl.py +1 -0
- nabu/processing/rotation_cuda.py +2 -2
- nabu/processing/tests/test_fft.py +17 -10
- nabu/processing/unsharp_cuda.py +1 -1
- nabu/reconstruction/cone.py +104 -40
- nabu/reconstruction/fbp.py +3 -0
- nabu/reconstruction/fbp_base.py +7 -2
- nabu/reconstruction/filtering.py +20 -7
- nabu/reconstruction/filtering_cuda.py +7 -1
- nabu/reconstruction/hbp.py +424 -0
- nabu/reconstruction/mlem.py +99 -0
- nabu/reconstruction/reconstructor.py +2 -0
- nabu/reconstruction/rings_cuda.py +19 -19
- nabu/reconstruction/sinogram_cuda.py +1 -0
- nabu/reconstruction/sinogram_opencl.py +3 -1
- nabu/reconstruction/tests/test_cone.py +10 -5
- nabu/reconstruction/tests/test_deringer.py +7 -6
- nabu/reconstruction/tests/test_fbp.py +124 -10
- nabu/reconstruction/tests/test_filtering.py +13 -11
- nabu/reconstruction/tests/test_halftomo.py +30 -4
- nabu/reconstruction/tests/test_mlem.py +91 -0
- nabu/reconstruction/tests/test_reconstructor.py +8 -3
- nabu/resources/dataset_analyzer.py +142 -92
- nabu/resources/gpu.py +1 -0
- nabu/resources/nxflatfield.py +134 -125
- nabu/resources/templates/id16a_fluo.conf +42 -0
- nabu/resources/tests/test_extract.py +10 -0
- nabu/resources/tests/test_nxflatfield.py +2 -2
- nabu/stitching/alignment.py +80 -24
- nabu/stitching/config.py +105 -68
- nabu/stitching/definitions.py +1 -0
- nabu/stitching/frame_composition.py +68 -60
- nabu/stitching/overlap.py +91 -51
- nabu/stitching/single_axis_stitching.py +32 -0
- nabu/stitching/slurm_utils.py +6 -6
- nabu/stitching/stitcher/__init__.py +0 -0
- nabu/stitching/stitcher/base.py +124 -0
- nabu/stitching/stitcher/dumper/__init__.py +3 -0
- nabu/stitching/stitcher/dumper/base.py +94 -0
- nabu/stitching/stitcher/dumper/postprocessing.py +356 -0
- nabu/stitching/stitcher/dumper/preprocessing.py +60 -0
- nabu/stitching/stitcher/post_processing.py +555 -0
- nabu/stitching/stitcher/pre_processing.py +1068 -0
- nabu/stitching/stitcher/single_axis.py +484 -0
- nabu/stitching/stitcher/stitcher.py +0 -0
- nabu/stitching/stitcher/y_stitcher.py +13 -0
- nabu/stitching/stitcher/z_stitcher.py +45 -0
- nabu/stitching/stitcher_2D.py +278 -0
- nabu/stitching/tests/test_config.py +12 -37
- nabu/stitching/tests/test_frame_composition.py +33 -59
- nabu/stitching/tests/test_overlap.py +149 -7
- nabu/stitching/tests/test_utils.py +1 -1
- nabu/stitching/tests/test_y_preprocessing_stitching.py +132 -0
- nabu/stitching/tests/{test_z_stitching.py → test_z_postprocessing_stitching.py} +167 -561
- nabu/stitching/tests/test_z_preprocessing_stitching.py +431 -0
- nabu/stitching/utils/__init__.py +1 -0
- nabu/stitching/utils/post_processing.py +281 -0
- nabu/stitching/utils/tests/test_post-processing.py +21 -0
- nabu/stitching/{utils.py → utils/utils.py} +79 -52
- nabu/stitching/y_stitching.py +27 -0
- nabu/stitching/z_stitching.py +32 -2281
- nabu/testutils.py +1 -152
- nabu/thirdparty/tomocupy_remove_stripe.py +43 -9
- nabu/utils.py +158 -61
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/METADATA +24 -17
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/RECORD +145 -121
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/WHEEL +1 -1
- nabu/io/tiffwriter_zmm.py +0 -99
- nabu/pipeline/fallback_utils.py +0 -149
- nabu/pipeline/helical/tests/test_accumulator.py +0 -158
- nabu/pipeline/helical/tests/test_pipeline_elements_full.py +0 -355
- nabu/pipeline/helical/tests/test_strategy.py +0 -61
- nabu/pipeline/helical/utils.py +0 -51
- nabu/pipeline/tests/test_chunk_reader.py +0 -74
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/LICENSE +0 -0
- {nabu-2024.1.10.dist-info → nabu-2024.2.0.dist-info}/entry_points.txt +0 -0
- {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="
|
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
|
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
|
-
|
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
|
-
|
236
|
-
self.
|
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,
|
265
|
-
self._mean_kernel_grid = [updiv(self.sinos_shape[
|
266
|
-
self._mean_kernel_args = [self.d_sino_profile, np.int32(self.n_x), np.int32(self.n_angles), np.int32(
|
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,
|
280
|
-
self._op_kernel_grid = [updiv(a, b) for a, b in zip(self.sinos_shape[
|
281
|
-
|
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
|