diffct 2.0.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.
- diffct/__init__.py +83 -0
- diffct/analytical.py +569 -0
- diffct/constants.py +72 -0
- diffct/differentiable.py +74 -0
- diffct/geometry.py +991 -0
- diffct/kernels/__init__.py +43 -0
- diffct/kernels/cone_beam.py +617 -0
- diffct/kernels/fan_beam.py +451 -0
- diffct/kernels/geometry_vjp.py +562 -0
- diffct/kernels/parallel_beam.py +400 -0
- diffct/operators.py +546 -0
- diffct/projectors.py +910 -0
- diffct/utils.py +357 -0
- diffct-2.0.0.dist-info/METADATA +240 -0
- diffct-2.0.0.dist-info/RECORD +18 -0
- diffct-2.0.0.dist-info/WHEEL +4 -0
- diffct-2.0.0.dist-info/licenses/LICENSE +201 -0
- diffct-2.0.0.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""CUDA kernels for CT projections.
|
|
2
|
+
|
|
3
|
+
This subpackage contains CUDA kernels for 2D and 3D CT forward projection
|
|
4
|
+
and backprojection operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .parallel_beam import (
|
|
8
|
+
_parallel_2d_forward_kernel,
|
|
9
|
+
_parallel_2d_backward_kernel,
|
|
10
|
+
_parallel_2d_fbp_backproject_kernel,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .fan_beam import (
|
|
14
|
+
_fan_2d_forward_kernel,
|
|
15
|
+
_fan_2d_backward_kernel,
|
|
16
|
+
_fan_2d_fbp_backproject_kernel,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .cone_beam import (
|
|
20
|
+
_cone_3d_forward_kernel,
|
|
21
|
+
_cone_3d_backward_kernel,
|
|
22
|
+
_cone_3d_fdk_backproject_kernel,
|
|
23
|
+
)
|
|
24
|
+
from .geometry_vjp import (
|
|
25
|
+
_parallel_2d_geometry_vjp_kernel,
|
|
26
|
+
_fan_2d_geometry_vjp_kernel,
|
|
27
|
+
_cone_3d_geometry_vjp_kernel,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
'_parallel_2d_forward_kernel',
|
|
32
|
+
'_parallel_2d_backward_kernel',
|
|
33
|
+
'_parallel_2d_fbp_backproject_kernel',
|
|
34
|
+
'_fan_2d_forward_kernel',
|
|
35
|
+
'_fan_2d_backward_kernel',
|
|
36
|
+
'_fan_2d_fbp_backproject_kernel',
|
|
37
|
+
'_cone_3d_forward_kernel',
|
|
38
|
+
'_cone_3d_backward_kernel',
|
|
39
|
+
'_cone_3d_fdk_backproject_kernel',
|
|
40
|
+
'_parallel_2d_geometry_vjp_kernel',
|
|
41
|
+
'_fan_2d_geometry_vjp_kernel',
|
|
42
|
+
'_cone_3d_geometry_vjp_kernel',
|
|
43
|
+
]
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""CUDA kernels for 3D cone beam projections.
|
|
2
|
+
|
|
3
|
+
This module contains CUDA kernels implementing the Siddon ray-tracing method
|
|
4
|
+
for 3D cone beam forward projection and backprojection, plus a dedicated
|
|
5
|
+
voxel-driven FDK gather kernel for analytical reconstruction pipelines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numba import cuda
|
|
11
|
+
|
|
12
|
+
from ..constants import (
|
|
13
|
+
_FASTMATH_DECORATOR,
|
|
14
|
+
_FDK_ACCURACY_DECORATOR,
|
|
15
|
+
_BIG,
|
|
16
|
+
_TINY,
|
|
17
|
+
_ZERO,
|
|
18
|
+
_ONE,
|
|
19
|
+
_HALF,
|
|
20
|
+
_EPSILON,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# 3D Cone Beam Forward Projection Kernel
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
@_FASTMATH_DECORATOR
|
|
29
|
+
def _cone_3d_forward_kernel(
|
|
30
|
+
d_vol, Nx, Ny, Nz,
|
|
31
|
+
d_sino, n_views, n_u, n_v,
|
|
32
|
+
du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
|
|
33
|
+
cx, cy, cz, voxel_spacing
|
|
34
|
+
):
|
|
35
|
+
"""Compute the 3D cone-beam forward projection with arbitrary source-detector trajectories.
|
|
36
|
+
|
|
37
|
+
This CUDA kernel implements cell-constant Siddon ray tracing for
|
|
38
|
+
3D cone-beam forward projection. Supports arbitrary source and detector positions
|
|
39
|
+
for each view, enabling non-circular trajectories.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
d_vol : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
44
|
+
Input 3D volume array on CUDA.
|
|
45
|
+
Nx : int
|
|
46
|
+
Number of voxels along the x-axis.
|
|
47
|
+
Ny : int
|
|
48
|
+
Number of voxels along the y-axis.
|
|
49
|
+
Nz : int
|
|
50
|
+
Number of voxels along the z-axis.
|
|
51
|
+
d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
52
|
+
Output cone-beam sinogram array on CUDA.
|
|
53
|
+
n_views : int
|
|
54
|
+
Number of projection views.
|
|
55
|
+
n_u : int
|
|
56
|
+
Number of detector elements along the u-axis.
|
|
57
|
+
n_v : int
|
|
58
|
+
Number of detector elements along the v-axis.
|
|
59
|
+
du : float
|
|
60
|
+
Physical spacing between detector elements along the u-axis.
|
|
61
|
+
dv : float
|
|
62
|
+
Physical spacing between detector elements along the v-axis.
|
|
63
|
+
d_src_pos : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
64
|
+
Source positions for each view, shape (n_views, 3), in physical units.
|
|
65
|
+
d_det_center : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
66
|
+
Detector center positions for each view, shape (n_views, 3), in physical units.
|
|
67
|
+
d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
68
|
+
Detector u-direction unit vectors for each view, shape (n_views, 3).
|
|
69
|
+
d_det_v_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
70
|
+
Detector v-direction unit vectors for each view, shape (n_views, 3).
|
|
71
|
+
cx : float
|
|
72
|
+
Half of volume width along x-axis (in voxels).
|
|
73
|
+
cy : float
|
|
74
|
+
Half of volume height along y-axis (in voxels).
|
|
75
|
+
cz : float
|
|
76
|
+
Half of volume depth along z-axis (in voxels).
|
|
77
|
+
voxel_spacing : float
|
|
78
|
+
Physical size of one voxel (in same units as source/detector positions).
|
|
79
|
+
|
|
80
|
+
Notes
|
|
81
|
+
-----
|
|
82
|
+
Supports arbitrary cone-beam geometries by specifying source position,
|
|
83
|
+
detector center, and detector orientation vectors for each view.
|
|
84
|
+
Integrates each ray through piecewise-constant voxel cells.
|
|
85
|
+
"""
|
|
86
|
+
iv, iu, iview = cuda.grid(3)
|
|
87
|
+
if iview >= n_views or iu >= n_u or iv >= n_v:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# === 3D CONE BEAM GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
|
|
91
|
+
# Read source position from position matrix (in physical units)
|
|
92
|
+
src_x = d_src_pos[iview, 0] / voxel_spacing
|
|
93
|
+
src_y = d_src_pos[iview, 1] / voxel_spacing
|
|
94
|
+
src_z = d_src_pos[iview, 2] / voxel_spacing
|
|
95
|
+
|
|
96
|
+
# Read detector center and orientation vectors
|
|
97
|
+
det_cx = d_det_center[iview, 0] / voxel_spacing
|
|
98
|
+
det_cy = d_det_center[iview, 1] / voxel_spacing
|
|
99
|
+
det_cz = d_det_center[iview, 2] / voxel_spacing
|
|
100
|
+
|
|
101
|
+
u_vec_x = d_det_u_vec[iview, 0]
|
|
102
|
+
u_vec_y = d_det_u_vec[iview, 1]
|
|
103
|
+
u_vec_z = d_det_u_vec[iview, 2]
|
|
104
|
+
|
|
105
|
+
v_vec_x = d_det_v_vec[iview, 0]
|
|
106
|
+
v_vec_y = d_det_v_vec[iview, 1]
|
|
107
|
+
v_vec_z = d_det_v_vec[iview, 2]
|
|
108
|
+
|
|
109
|
+
# Calculate detector element offset from center
|
|
110
|
+
u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
|
|
111
|
+
v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
|
|
112
|
+
|
|
113
|
+
# Calculate 3D detector element position using center + u*u_vec + v*v_vec
|
|
114
|
+
det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
|
|
115
|
+
det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
|
|
116
|
+
det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
|
|
117
|
+
|
|
118
|
+
# === 3D RAY DIRECTION CALCULATION ===
|
|
119
|
+
# Ray direction vector from source to detector element in 3D space
|
|
120
|
+
dir_x, dir_y, dir_z = det_x - src_x, det_y - src_y, det_z - src_z
|
|
121
|
+
length = math.sqrt(dir_x*dir_x + dir_y*dir_y + dir_z*dir_z) # 3D ray length
|
|
122
|
+
if length < _EPSILON: # Degenerate ray case
|
|
123
|
+
d_sino[iview, iu, iv] = _ZERO; return
|
|
124
|
+
|
|
125
|
+
# Normalize 3D ray direction vector for parametric traversal
|
|
126
|
+
inv_len = _ONE / length
|
|
127
|
+
dir_x, dir_y, dir_z = dir_x*inv_len, dir_y*inv_len, dir_z*inv_len
|
|
128
|
+
|
|
129
|
+
# === 3D RAY-VOLUME INTERSECTION CALCULATION ===
|
|
130
|
+
# Measure the ray parameter from the endpoint nearer the volume centre, so a
|
|
131
|
+
# distant source does not make the float32 parameters coarse. Only the
|
|
132
|
+
# source-to-detector segment is integrated.
|
|
133
|
+
if src_x * src_x + src_y * src_y + src_z * src_z <= det_x * det_x + det_y * det_y + det_z * det_z:
|
|
134
|
+
org_x = src_x
|
|
135
|
+
org_y = src_y
|
|
136
|
+
org_z = src_z
|
|
137
|
+
t_min, t_max = _ZERO, length
|
|
138
|
+
else:
|
|
139
|
+
org_x = det_x
|
|
140
|
+
org_y = det_y
|
|
141
|
+
org_z = det_z
|
|
142
|
+
t_min, t_max = -length, _ZERO
|
|
143
|
+
|
|
144
|
+
# X-direction boundary intersections
|
|
145
|
+
if abs(dir_x) > _TINY:
|
|
146
|
+
tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x
|
|
147
|
+
t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
|
|
148
|
+
elif org_x < -cx or org_x > cx: # Ray origin outside x-bounds
|
|
149
|
+
d_sino[iview, iu, iv] = _ZERO; return
|
|
150
|
+
|
|
151
|
+
# Y-direction boundary intersections
|
|
152
|
+
if abs(dir_y) > _TINY:
|
|
153
|
+
ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
|
|
154
|
+
t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
|
|
155
|
+
elif org_y < -cy or org_y > cy: # Ray origin outside y-bounds
|
|
156
|
+
d_sino[iview, iu, iv] = _ZERO; return
|
|
157
|
+
|
|
158
|
+
# Z-direction boundary intersections (extends 2D algorithm to 3D)
|
|
159
|
+
if abs(dir_z) > _TINY:
|
|
160
|
+
tz1, tz2 = (-cz - org_z) / dir_z, (cz - org_z) / dir_z
|
|
161
|
+
t_min, t_max = max(t_min, min(tz1, tz2)), min(t_max, max(tz1, tz2))
|
|
162
|
+
elif org_z < -cz or org_z > cz: # Ray origin outside z-bounds
|
|
163
|
+
d_sino[iview, iu, iv] = _ZERO; return
|
|
164
|
+
|
|
165
|
+
if t_min >= t_max: # No valid 3D intersection
|
|
166
|
+
d_sino[iview, iu, iv] = _ZERO; return
|
|
167
|
+
|
|
168
|
+
# The traversal restarts at the entry point, so its parameters stay
|
|
169
|
+
# within the volume diameter.
|
|
170
|
+
ent_x = org_x + t_min * dir_x
|
|
171
|
+
ent_y = org_y + t_min * dir_y
|
|
172
|
+
ent_z = org_z + t_min * dir_z
|
|
173
|
+
ray_x = dir_x
|
|
174
|
+
ray_y = dir_y
|
|
175
|
+
ray_z = dir_z
|
|
176
|
+
t_end = t_max - t_min
|
|
177
|
+
|
|
178
|
+
# === 3D SIDDON METHOD TRAVERSAL INITIALIZATION ===
|
|
179
|
+
accum = _ZERO # Accumulated projection value
|
|
180
|
+
t = _ZERO
|
|
181
|
+
|
|
182
|
+
# Convert 3D ray entry point to voxel indices
|
|
183
|
+
ix = int(math.floor(ent_x + t * ray_x + cx)) # Current voxel x-index
|
|
184
|
+
iy = int(math.floor(ent_y + t * ray_y + cy)) # Current voxel y-index
|
|
185
|
+
iz = int(math.floor(ent_z + t * ray_z + cz)) # Current voxel z-index
|
|
186
|
+
|
|
187
|
+
# 3D traversal parameters (extends 2D algorithm)
|
|
188
|
+
step_x, step_y, step_z = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1), (1 if ray_z >= 0 else -1)
|
|
189
|
+
inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
|
|
190
|
+
inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
|
|
191
|
+
inv_dir_z = (_ONE / ray_z) if abs(ray_z) > _TINY else _ZERO
|
|
192
|
+
dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG # Parameter increment per x-voxel
|
|
193
|
+
dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG # Parameter increment per y-voxel
|
|
194
|
+
dt_z = abs(inv_dir_z) if abs(ray_z) > _TINY else _BIG # Parameter increment per z-voxel
|
|
195
|
+
|
|
196
|
+
# Calculate parameter values for next 3D voxel boundary crossings
|
|
197
|
+
next_ix = ix + (1 if step_x > 0 else 0)
|
|
198
|
+
next_iy = iy + (1 if step_y > 0 else 0)
|
|
199
|
+
next_iz = iz + (1 if step_z > 0 else 0)
|
|
200
|
+
tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
|
|
201
|
+
ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
|
|
202
|
+
tz = (np.float32(next_iz) - cz - ent_z) * inv_dir_z if abs(ray_z) > _TINY else _BIG
|
|
203
|
+
|
|
204
|
+
# === 3D TRAVERSAL LOOP WITH CELL-CONSTANT SIDDON INTEGRATION ===
|
|
205
|
+
while t < t_end:
|
|
206
|
+
if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
|
|
207
|
+
# Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
|
|
208
|
+
t_next = min(tx, ty, tz, t_end)
|
|
209
|
+
seg_len = t_next - t
|
|
210
|
+
if seg_len > _ZERO:
|
|
211
|
+
accum += d_vol[ix, iy, iz] * seg_len
|
|
212
|
+
|
|
213
|
+
# === 3D VOXEL BOUNDARY CROSSING LOGIC ===
|
|
214
|
+
# Advance to next voxel based on which boundary is crossed first in 3D
|
|
215
|
+
if tx <= ty and tx <= tz: # X-boundary crossed first
|
|
216
|
+
t = tx
|
|
217
|
+
ix += step_x
|
|
218
|
+
tx += dt_x
|
|
219
|
+
elif ty <= tx and ty <= tz: # Y-boundary crossed first
|
|
220
|
+
t = ty
|
|
221
|
+
iy += step_y
|
|
222
|
+
ty += dt_y
|
|
223
|
+
else: # Z-boundary crossed first
|
|
224
|
+
t = tz
|
|
225
|
+
iz += step_z
|
|
226
|
+
tz += dt_z
|
|
227
|
+
|
|
228
|
+
# Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
|
|
229
|
+
d_sino[iview, iu, iv] = accum * voxel_spacing
|
|
230
|
+
|
|
231
|
+
@_FASTMATH_DECORATOR
|
|
232
|
+
|
|
233
|
+
# ============================================================================
|
|
234
|
+
# 3D Cone Beam Backprojection Kernel
|
|
235
|
+
# ============================================================================
|
|
236
|
+
|
|
237
|
+
def _cone_3d_backward_kernel(
|
|
238
|
+
d_sino, n_views, n_u, n_v,
|
|
239
|
+
d_vol, Nx, Ny, Nz,
|
|
240
|
+
du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
|
|
241
|
+
cx, cy, cz, voxel_spacing
|
|
242
|
+
):
|
|
243
|
+
"""Compute the 3D cone-beam backprojection with arbitrary source-detector trajectories.
|
|
244
|
+
|
|
245
|
+
This CUDA kernel implements the adjoint of cell-constant Siddon ray tracing for
|
|
246
|
+
3D cone-beam backprojection. Supports arbitrary source and detector positions
|
|
247
|
+
for each view, enabling non-circular trajectories.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
252
|
+
Input cone-beam sinogram array on CUDA.
|
|
253
|
+
n_views : int
|
|
254
|
+
Number of projection views.
|
|
255
|
+
n_u : int
|
|
256
|
+
Number of detector elements along the u-axis.
|
|
257
|
+
n_v : int
|
|
258
|
+
Number of detector elements along the v-axis.
|
|
259
|
+
d_vol : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
260
|
+
Output 3D volume gradient array on CUDA.
|
|
261
|
+
Nx : int
|
|
262
|
+
Number of voxels along the x-axis.
|
|
263
|
+
Ny : int
|
|
264
|
+
Number of voxels along the y-axis.
|
|
265
|
+
Nz : int
|
|
266
|
+
Number of voxels along the z-axis.
|
|
267
|
+
du : float
|
|
268
|
+
Physical spacing between detector elements along the u-axis.
|
|
269
|
+
dv : float
|
|
270
|
+
Physical spacing between detector elements along the v-axis.
|
|
271
|
+
d_src_pos : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
272
|
+
Source positions for each view, shape (n_views, 3), in physical units.
|
|
273
|
+
d_det_center : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
274
|
+
Detector center positions for each view, shape (n_views, 3), in physical units.
|
|
275
|
+
d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
276
|
+
Detector u-direction unit vectors for each view, shape (n_views, 3).
|
|
277
|
+
d_det_v_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
278
|
+
Detector v-direction unit vectors for each view, shape (n_views, 3).
|
|
279
|
+
cx : float
|
|
280
|
+
Half of volume width along x-axis (in voxels).
|
|
281
|
+
cy : float
|
|
282
|
+
Half of volume height along y-axis (in voxels).
|
|
283
|
+
cz : float
|
|
284
|
+
Half of volume depth along z-axis (in voxels).
|
|
285
|
+
voxel_spacing : float
|
|
286
|
+
Physical size of one voxel (in same units as source/detector positions).
|
|
287
|
+
|
|
288
|
+
Notes
|
|
289
|
+
-----
|
|
290
|
+
As the adjoint to the cone-beam forward projection, this operation
|
|
291
|
+
distributes sinogram values back into the 3D volume along ray paths using
|
|
292
|
+
atomic operations for thread-safe accumulation.
|
|
293
|
+
Supports arbitrary cone-beam geometries.
|
|
294
|
+
"""
|
|
295
|
+
iv, iu, iview = cuda.grid(3)
|
|
296
|
+
if iview >= n_views or iu >= n_u or iv >= n_v:
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# === 3D BACKPROJECTION VALUE AND GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
|
|
300
|
+
# Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
|
|
301
|
+
g = d_sino[iview, iu, iv] * voxel_spacing
|
|
302
|
+
|
|
303
|
+
# Read source position from position matrix (in physical units)
|
|
304
|
+
src_x = d_src_pos[iview, 0] / voxel_spacing
|
|
305
|
+
src_y = d_src_pos[iview, 1] / voxel_spacing
|
|
306
|
+
src_z = d_src_pos[iview, 2] / voxel_spacing
|
|
307
|
+
|
|
308
|
+
# Read detector center and orientation vectors
|
|
309
|
+
det_cx = d_det_center[iview, 0] / voxel_spacing
|
|
310
|
+
det_cy = d_det_center[iview, 1] / voxel_spacing
|
|
311
|
+
det_cz = d_det_center[iview, 2] / voxel_spacing
|
|
312
|
+
|
|
313
|
+
u_vec_x = d_det_u_vec[iview, 0]
|
|
314
|
+
u_vec_y = d_det_u_vec[iview, 1]
|
|
315
|
+
u_vec_z = d_det_u_vec[iview, 2]
|
|
316
|
+
|
|
317
|
+
v_vec_x = d_det_v_vec[iview, 0]
|
|
318
|
+
v_vec_y = d_det_v_vec[iview, 1]
|
|
319
|
+
v_vec_z = d_det_v_vec[iview, 2]
|
|
320
|
+
|
|
321
|
+
# Calculate detector element offset from center
|
|
322
|
+
u_offset = (np.float32(iu) + _HALF - np.float32(n_u) * _HALF) * du / voxel_spacing
|
|
323
|
+
v_offset = (np.float32(iv) + _HALF - np.float32(n_v) * _HALF) * dv / voxel_spacing
|
|
324
|
+
|
|
325
|
+
# Calculate 3D detector element position using center + u*u_vec + v*v_vec
|
|
326
|
+
det_x = det_cx + u_offset * u_vec_x + v_offset * v_vec_x
|
|
327
|
+
det_y = det_cy + u_offset * u_vec_y + v_offset * v_vec_y
|
|
328
|
+
det_z = det_cz + u_offset * u_vec_z + v_offset * v_vec_z
|
|
329
|
+
|
|
330
|
+
# === 3D RAY DIRECTION CALCULATION ===
|
|
331
|
+
# Ray direction vector from source to detector element in 3D space
|
|
332
|
+
dir_x, dir_y, dir_z = det_x - src_x, det_y - src_y, det_z - src_z
|
|
333
|
+
length = math.sqrt(dir_x*dir_x + dir_y*dir_y + dir_z*dir_z) # 3D ray length
|
|
334
|
+
if length < _EPSILON: return # Skip degenerate rays
|
|
335
|
+
inv_len = _ONE / length # Normalization factor for ray direction
|
|
336
|
+
dir_x, dir_y, dir_z = dir_x*inv_len, dir_y*inv_len, dir_z*inv_len # Normalized 3D ray direction vector
|
|
337
|
+
|
|
338
|
+
# === 3D RAY-VOLUME INTERSECTION CALCULATION ===
|
|
339
|
+
# Measure the ray parameter from the endpoint nearer the volume centre, so a
|
|
340
|
+
# distant source does not make the float32 parameters coarse. Only the
|
|
341
|
+
# source-to-detector segment is integrated.
|
|
342
|
+
if src_x * src_x + src_y * src_y + src_z * src_z <= det_x * det_x + det_y * det_y + det_z * det_z:
|
|
343
|
+
org_x = src_x
|
|
344
|
+
org_y = src_y
|
|
345
|
+
org_z = src_z
|
|
346
|
+
t_min, t_max = _ZERO, length
|
|
347
|
+
else:
|
|
348
|
+
org_x = det_x
|
|
349
|
+
org_y = det_y
|
|
350
|
+
org_z = det_z
|
|
351
|
+
t_min, t_max = -length, _ZERO
|
|
352
|
+
|
|
353
|
+
# X-direction boundary intersections
|
|
354
|
+
if abs(dir_x) > _TINY:
|
|
355
|
+
tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x
|
|
356
|
+
t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
|
|
357
|
+
elif org_x < -cx or org_x > cx: return
|
|
358
|
+
|
|
359
|
+
# Y-direction boundary intersections
|
|
360
|
+
if abs(dir_y) > _TINY:
|
|
361
|
+
ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
|
|
362
|
+
t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
|
|
363
|
+
elif org_y < -cy or org_y > cy: return
|
|
364
|
+
|
|
365
|
+
# Z-direction boundary intersections (extends 2D algorithm to 3D)
|
|
366
|
+
if abs(dir_z) > _TINY:
|
|
367
|
+
tz1, tz2 = (-cz - org_z) / dir_z, (cz - org_z) / dir_z
|
|
368
|
+
t_min, t_max = max(t_min, min(tz1, tz2)), min(t_max, max(tz1, tz2))
|
|
369
|
+
elif org_z < -cz or org_z > cz: return
|
|
370
|
+
|
|
371
|
+
if t_min >= t_max: return
|
|
372
|
+
|
|
373
|
+
# The traversal restarts at the entry point, so its parameters stay
|
|
374
|
+
# within the volume diameter.
|
|
375
|
+
ent_x = org_x + t_min * dir_x
|
|
376
|
+
ent_y = org_y + t_min * dir_y
|
|
377
|
+
ent_z = org_z + t_min * dir_z
|
|
378
|
+
ray_x = dir_x
|
|
379
|
+
ray_y = dir_y
|
|
380
|
+
ray_z = dir_z
|
|
381
|
+
t_end = t_max - t_min
|
|
382
|
+
|
|
383
|
+
# === 3D SIDDON METHOD TRAVERSAL INITIALIZATION ===
|
|
384
|
+
t = _ZERO
|
|
385
|
+
ix = int(math.floor(ent_x + t * ray_x + cx)) # Current voxel x-index
|
|
386
|
+
iy = int(math.floor(ent_y + t * ray_y + cy)) # Current voxel y-index
|
|
387
|
+
iz = int(math.floor(ent_z + t * ray_z + cz)) # Current voxel z-index
|
|
388
|
+
|
|
389
|
+
# 3D traversal parameters (extends 2D algorithm)
|
|
390
|
+
step_x, step_y, step_z = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1), (1 if ray_z >= 0 else -1)
|
|
391
|
+
inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
|
|
392
|
+
inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
|
|
393
|
+
inv_dir_z = (_ONE / ray_z) if abs(ray_z) > _TINY else _ZERO
|
|
394
|
+
dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG # Parameter increment per x-voxel
|
|
395
|
+
dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG # Parameter increment per y-voxel
|
|
396
|
+
dt_z = abs(inv_dir_z) if abs(ray_z) > _TINY else _BIG # Parameter increment per z-voxel
|
|
397
|
+
|
|
398
|
+
# Calculate parameter values for next 3D voxel boundary crossings
|
|
399
|
+
next_ix = ix + (1 if step_x > 0 else 0)
|
|
400
|
+
next_iy = iy + (1 if step_y > 0 else 0)
|
|
401
|
+
next_iz = iz + (1 if step_z > 0 else 0)
|
|
402
|
+
tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
|
|
403
|
+
ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
|
|
404
|
+
tz = (np.float32(next_iz) - cz - ent_z) * inv_dir_z if abs(ray_z) > _TINY else _BIG
|
|
405
|
+
|
|
406
|
+
# === 3D CONE BEAM BACKPROJECTION TRAVERSAL LOOP ===
|
|
407
|
+
# Adjoint of the cell-constant Siddon forward projection.
|
|
408
|
+
while t < t_end:
|
|
409
|
+
if 0 <= ix < Nx and 0 <= iy < Ny and 0 <= iz < Nz:
|
|
410
|
+
# Determine next 3D voxel boundary crossing (minimum of x, y, z boundaries or ray exit)
|
|
411
|
+
t_next = min(tx, ty, tz, t_end)
|
|
412
|
+
seg_len = t_next - t
|
|
413
|
+
if seg_len > _ZERO:
|
|
414
|
+
cuda.atomic.add(d_vol, (ix, iy, iz), g * seg_len)
|
|
415
|
+
|
|
416
|
+
# === 3D VOXEL BOUNDARY CROSSING LOGIC ===
|
|
417
|
+
# Advance to next voxel based on which boundary is crossed first in 3D
|
|
418
|
+
if tx <= ty and tx <= tz: # X-boundary crossed first
|
|
419
|
+
t = tx
|
|
420
|
+
ix += step_x
|
|
421
|
+
tx += dt_x
|
|
422
|
+
elif ty <= tx and ty <= tz: # Y-boundary crossed first
|
|
423
|
+
t = ty
|
|
424
|
+
iy += step_y
|
|
425
|
+
ty += dt_y
|
|
426
|
+
else: # Z-boundary crossed first
|
|
427
|
+
t = tz
|
|
428
|
+
iz += step_z
|
|
429
|
+
tz += dt_z
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# ============================================================================
|
|
433
|
+
# 3D Cone Beam Analytical FDK Backprojection Gather (voxel-driven)
|
|
434
|
+
# ============================================================================
|
|
435
|
+
#
|
|
436
|
+
# Distinct from ``_cone_3d_backward_kernel`` above, which is the pure
|
|
437
|
+
# Siddon adjoint ``P^T`` used by autograd. This kernel is the classical
|
|
438
|
+
# voxel-driven FDK gather: for each output voxel it projects the voxel
|
|
439
|
+
# centre onto the detector for each view, bilinearly samples the
|
|
440
|
+
# already-filtered sinogram, and accumulates ``(sid/U_n)^2 * sample``.
|
|
441
|
+
# Both forward and adjoint autograd paths deliberately stay on the
|
|
442
|
+
# Siddon pair; only ``cone_weighted_backproject`` dispatches here.
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@_FDK_ACCURACY_DECORATOR
|
|
446
|
+
def _cone_3d_fdk_backproject_kernel(
|
|
447
|
+
d_sino, n_views, n_u, n_v,
|
|
448
|
+
d_vol, Nx, Ny, Nz,
|
|
449
|
+
du, dv, d_src_pos, d_det_center, d_det_u_vec, d_det_v_vec,
|
|
450
|
+
cx, cy, cz, voxel_spacing, iso_x, iso_y, iso_z
|
|
451
|
+
):
|
|
452
|
+
"""Voxel-driven FDK backprojection gather (arbitrary trajectory).
|
|
453
|
+
|
|
454
|
+
For each volume voxel, loops over all views and accumulates a
|
|
455
|
+
bilinearly-interpolated sinogram sample weighted by ``(sid_v/U_n)^2``,
|
|
456
|
+
where:
|
|
457
|
+
|
|
458
|
+
* ``sid_v = (isocenter - S_v) . n_v`` is the source-to-isocenter
|
|
459
|
+
distance along the detector normal,
|
|
460
|
+
* ``U_n = (P - S_v) . n_v`` is the signed distance from the source
|
|
461
|
+
to the voxel along the detector normal ``n_v = u_v x v_v``.
|
|
462
|
+
|
|
463
|
+
For a canonical circular orbit this reduces to the classical
|
|
464
|
+
``(sid/U)^2`` FDK weight with ``U = sid + x*sin(b) - y*cos(b)``.
|
|
465
|
+
|
|
466
|
+
Coalescing layout: ``d_vol`` has WHD layout ``(Nx, Ny, Nz)`` so its
|
|
467
|
+
innermost stride-1 axis is ``Nz``. We therefore unpack
|
|
468
|
+
``(iz, iy, ix) = cuda.grid(3)`` and have the Python wrapper launch
|
|
469
|
+
the grid as ``_grid_3d(Nz, Ny, Nx)`` so ``iz`` is warp-adjacent and
|
|
470
|
+
the per-voxel writes are coalesced.
|
|
471
|
+
"""
|
|
472
|
+
iz, iy, ix = cuda.grid(3)
|
|
473
|
+
if ix >= Nx or iy >= Ny or iz >= Nz:
|
|
474
|
+
return
|
|
475
|
+
if n_u < 2 or n_v < 2:
|
|
476
|
+
d_vol[ix, iy, iz] = _ZERO
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# Voxel position in voxel-unit, origin-centred coordinates.
|
|
480
|
+
# Sample at voxel centres, matching the cell-constant Siddon projector.
|
|
481
|
+
x_v = np.float32(ix) + _HALF - cx
|
|
482
|
+
y_v = np.float32(iy) + _HALF - cy
|
|
483
|
+
z_v = np.float32(iz) + _HALF - cz
|
|
484
|
+
|
|
485
|
+
du_v = du / voxel_spacing
|
|
486
|
+
dv_v = dv / voxel_spacing
|
|
487
|
+
|
|
488
|
+
half_u = (np.float32(n_u) - _ONE) * _HALF
|
|
489
|
+
half_v = (np.float32(n_v) - _ONE) * _HALF
|
|
490
|
+
|
|
491
|
+
accum = _ZERO
|
|
492
|
+
for iview in range(n_views):
|
|
493
|
+
# Source position in voxel units.
|
|
494
|
+
sx = d_src_pos[iview, 0] / voxel_spacing
|
|
495
|
+
sy = d_src_pos[iview, 1] / voxel_spacing
|
|
496
|
+
sz = d_src_pos[iview, 2] / voxel_spacing
|
|
497
|
+
|
|
498
|
+
# Detector centre in voxel units.
|
|
499
|
+
dcx = d_det_center[iview, 0] / voxel_spacing
|
|
500
|
+
dcy = d_det_center[iview, 1] / voxel_spacing
|
|
501
|
+
dcz = d_det_center[iview, 2] / voxel_spacing
|
|
502
|
+
|
|
503
|
+
# Detector orientation (unit vectors, ratio-free).
|
|
504
|
+
ux = d_det_u_vec[iview, 0]
|
|
505
|
+
uy = d_det_u_vec[iview, 1]
|
|
506
|
+
uz = d_det_u_vec[iview, 2]
|
|
507
|
+
vx = d_det_v_vec[iview, 0]
|
|
508
|
+
vy = d_det_v_vec[iview, 1]
|
|
509
|
+
vz = d_det_v_vec[iview, 2]
|
|
510
|
+
|
|
511
|
+
# Detector normal n = u x v. Normalized defensively in case the
|
|
512
|
+
# caller supplied non-orthonormal u/v axes.
|
|
513
|
+
nx = uy * vz - uz * vy
|
|
514
|
+
ny = uz * vx - ux * vz
|
|
515
|
+
nz = ux * vy - uy * vx
|
|
516
|
+
n_len = math.sqrt(nx * nx + ny * ny + nz * nz)
|
|
517
|
+
if n_len < _EPSILON:
|
|
518
|
+
continue
|
|
519
|
+
nx /= n_len
|
|
520
|
+
ny /= n_len
|
|
521
|
+
nz /= n_len
|
|
522
|
+
|
|
523
|
+
# Orient the normal so it points from the source toward the
|
|
524
|
+
# detector. Without this flip, user-supplied (u_vec, v_vec) pairs
|
|
525
|
+
# whose cross product happens to point the other way would make
|
|
526
|
+
# every voxel's U_n negative and get silently skipped, producing
|
|
527
|
+
# a zero reconstruction. The circular_trajectory_3d helper already
|
|
528
|
+
# produces the right-handed convention so this is a no-op there.
|
|
529
|
+
align = (dcx - sx) * nx + (dcy - sy) * ny + (dcz - sz) * nz
|
|
530
|
+
if align < _ZERO:
|
|
531
|
+
nx = -nx
|
|
532
|
+
ny = -ny
|
|
533
|
+
nz = -nz
|
|
534
|
+
|
|
535
|
+
# Signed distance from source to voxel along the detector normal
|
|
536
|
+
# (this is the U in the (sid/U)^2 FDK weight).
|
|
537
|
+
px = x_v - sx
|
|
538
|
+
py = y_v - sy
|
|
539
|
+
pz = z_v - sz
|
|
540
|
+
U_n = px * nx + py * ny + pz * nz
|
|
541
|
+
if U_n <= _EPSILON:
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
# Source-to-detector distance along the normal.
|
|
545
|
+
sdd_n = (dcx - sx) * nx + (dcy - sy) * ny + (dcz - sz) * nz
|
|
546
|
+
if sdd_n <= _EPSILON:
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
# Source-to-isocenter distance, independent of coordinate origin.
|
|
550
|
+
sid_n = (iso_x - sx) * nx + (iso_y - sy) * ny + (iso_z - sz) * nz
|
|
551
|
+
if sid_n <= _EPSILON:
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
mag = sdd_n / U_n
|
|
555
|
+
|
|
556
|
+
# Hit point on the detector plane.
|
|
557
|
+
hx = sx + mag * px
|
|
558
|
+
hy = sy + mag * py
|
|
559
|
+
hz = sz + mag * pz
|
|
560
|
+
|
|
561
|
+
# Detector coordinates relative to detector centre.
|
|
562
|
+
rx = hx - dcx
|
|
563
|
+
ry = hy - dcy
|
|
564
|
+
rz = hz - dcz
|
|
565
|
+
|
|
566
|
+
u_det = rx * ux + ry * uy + rz * uz
|
|
567
|
+
v_det = rx * vx + ry * vy + rz * vz
|
|
568
|
+
|
|
569
|
+
# Convert (u, v) to fractional bin index.
|
|
570
|
+
fu = u_det / du_v + half_u
|
|
571
|
+
fv = v_det / dv_v + half_v
|
|
572
|
+
|
|
573
|
+
if fu < _ZERO or fu > (np.float32(n_u) - _ONE) or fv < _ZERO or fv > (np.float32(n_v) - _ONE):
|
|
574
|
+
continue
|
|
575
|
+
|
|
576
|
+
iu0 = int(math.floor(fu))
|
|
577
|
+
iv0 = int(math.floor(fv))
|
|
578
|
+
if iu0 >= n_u - 1:
|
|
579
|
+
iu0 = n_u - 2
|
|
580
|
+
if iv0 >= n_v - 1:
|
|
581
|
+
iv0 = n_v - 2
|
|
582
|
+
if iu0 < 0:
|
|
583
|
+
iu0 = 0
|
|
584
|
+
if iv0 < 0:
|
|
585
|
+
iv0 = 0
|
|
586
|
+
tu = fu - np.float32(iu0)
|
|
587
|
+
tv = fv - np.float32(iv0)
|
|
588
|
+
if tu < _ZERO:
|
|
589
|
+
tu = _ZERO
|
|
590
|
+
elif tu > _ONE:
|
|
591
|
+
tu = _ONE
|
|
592
|
+
if tv < _ZERO:
|
|
593
|
+
tv = _ZERO
|
|
594
|
+
elif tv > _ONE:
|
|
595
|
+
tv = _ONE
|
|
596
|
+
|
|
597
|
+
s00 = d_sino[iview, iu0, iv0 ]
|
|
598
|
+
s10 = d_sino[iview, iu0 + 1, iv0 ]
|
|
599
|
+
s01 = d_sino[iview, iu0, iv0 + 1]
|
|
600
|
+
s11 = d_sino[iview, iu0 + 1, iv0 + 1]
|
|
601
|
+
|
|
602
|
+
omtu = _ONE - tu
|
|
603
|
+
omtv = _ONE - tv
|
|
604
|
+
sample = (
|
|
605
|
+
s00 * omtu * omtv +
|
|
606
|
+
s10 * tu * omtv +
|
|
607
|
+
s01 * omtu * tv +
|
|
608
|
+
s11 * tu * tv
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
# FDK weight (sid_n / U_n)^2.
|
|
612
|
+
w_ratio = sid_n / U_n
|
|
613
|
+
w = w_ratio * w_ratio
|
|
614
|
+
|
|
615
|
+
accum += w * sample
|
|
616
|
+
|
|
617
|
+
d_vol[ix, iy, iz] = accum
|