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,400 @@
|
|
|
1
|
+
"""CUDA kernels for 2D parallel beam projections.
|
|
2
|
+
|
|
3
|
+
This module contains CUDA kernels implementing the Siddon ray-tracing method
|
|
4
|
+
for 2D parallel beam forward projection and backprojection, plus a
|
|
5
|
+
dedicated voxel-driven FBP gather kernel for analytical reconstruction
|
|
6
|
+
pipelines.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numba import cuda
|
|
12
|
+
|
|
13
|
+
from ..constants import (
|
|
14
|
+
_FASTMATH_DECORATOR,
|
|
15
|
+
_FDK_ACCURACY_DECORATOR,
|
|
16
|
+
_BIG,
|
|
17
|
+
_TINY,
|
|
18
|
+
_ZERO,
|
|
19
|
+
_ONE,
|
|
20
|
+
_HALF,
|
|
21
|
+
_EPSILON,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# 2D Parallel Beam Forward Projection Kernel
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
@_FASTMATH_DECORATOR
|
|
30
|
+
def _parallel_2d_forward_kernel(
|
|
31
|
+
d_image, Nx, Ny,
|
|
32
|
+
d_sino, n_ang, n_det,
|
|
33
|
+
det_spacing, d_ray_dir, d_det_origin, d_det_u_vec, cx, cy, voxel_spacing
|
|
34
|
+
):
|
|
35
|
+
"""Compute the 2D parallel beam forward projection with arbitrary ray trajectories.
|
|
36
|
+
|
|
37
|
+
This CUDA kernel implements cell-constant Siddon ray tracing for
|
|
38
|
+
2D parallel beam forward projection. Supports arbitrary ray directions and detector
|
|
39
|
+
positions for each view, enabling non-circular trajectories.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
d_image : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
44
|
+
Input 2D image 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
|
+
d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
50
|
+
Output sinogram array on CUDA.
|
|
51
|
+
n_ang : int
|
|
52
|
+
Number of projection angles.
|
|
53
|
+
n_det : int
|
|
54
|
+
Number of detector elements.
|
|
55
|
+
det_spacing : float
|
|
56
|
+
Physical spacing between detector elements.
|
|
57
|
+
d_ray_dir : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
58
|
+
Ray direction unit vectors for each view, shape (n_ang, 2).
|
|
59
|
+
d_det_origin : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
60
|
+
Detector origin positions for each view, shape (n_ang, 2), in physical units.
|
|
61
|
+
d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
62
|
+
Detector u-direction unit vectors for each view, shape (n_ang, 2).
|
|
63
|
+
cx : float
|
|
64
|
+
Half of image width in voxels.
|
|
65
|
+
cy : float
|
|
66
|
+
Half of image height in voxels.
|
|
67
|
+
voxel_spacing : float
|
|
68
|
+
Physical size of one voxel (in same units as det_spacing).
|
|
69
|
+
|
|
70
|
+
Notes
|
|
71
|
+
-----
|
|
72
|
+
Supports arbitrary parallel beam geometries by specifying ray direction,
|
|
73
|
+
detector origin, and detector orientation vectors for each view.
|
|
74
|
+
Integrates each ray through piecewise-constant pixel cells.
|
|
75
|
+
"""
|
|
76
|
+
iang, idet = cuda.grid(2)
|
|
77
|
+
if iang >= n_ang or idet >= n_det:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# === 2D PARALLEL BEAM GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
|
|
81
|
+
# Read ray direction (parallel for all rays in this view)
|
|
82
|
+
dir_x = np.float64(d_ray_dir[iang, 0])
|
|
83
|
+
dir_y = np.float64(d_ray_dir[iang, 1])
|
|
84
|
+
|
|
85
|
+
# Read detector origin and orientation vector
|
|
86
|
+
det_ox = np.float64(d_det_origin[iang, 0]) / voxel_spacing
|
|
87
|
+
det_oy = np.float64(d_det_origin[iang, 1]) / voxel_spacing
|
|
88
|
+
|
|
89
|
+
u_vec_x = np.float64(d_det_u_vec[iang, 0])
|
|
90
|
+
u_vec_y = np.float64(d_det_u_vec[iang, 1])
|
|
91
|
+
|
|
92
|
+
# Calculate detector element offset from origin
|
|
93
|
+
u_offset = (np.float64(idet) + _HALF - np.float64(n_det) * _HALF) * det_spacing / voxel_spacing
|
|
94
|
+
|
|
95
|
+
# Ray starting point: detector origin + offset along u-direction
|
|
96
|
+
pnt_x = det_ox + u_offset * u_vec_x
|
|
97
|
+
pnt_y = det_oy + u_offset * u_vec_y
|
|
98
|
+
|
|
99
|
+
# === RAY-VOLUME INTERSECTION CALCULATION ===
|
|
100
|
+
# Compute parametric intersection points with volume boundaries using ray equation r(t) = pnt + t*dir
|
|
101
|
+
# Volume extends from [-cx, cx] x [-cy, cy] in voxel coordinate system
|
|
102
|
+
# Mathematical basis: For ray r(t) = origin + t*direction, solve r(t) = boundary for parameter t
|
|
103
|
+
t_min, t_max = -_BIG, _BIG # Initialize ray parameter range to unbounded
|
|
104
|
+
|
|
105
|
+
# X-direction boundary intersections
|
|
106
|
+
# Handle non-parallel rays: compute intersection parameters with left (-cx) and right (+cx) boundaries
|
|
107
|
+
if dir_x != 0.0: # Ray not parallel to x-axis (avoid division by zero)
|
|
108
|
+
tx1, tx2 = (-cx - pnt_x) / dir_x, (cx - pnt_x) / dir_x # Left and right boundary intersections
|
|
109
|
+
# Update valid parameter range: intersection of current range with x-boundary constraints
|
|
110
|
+
# min/max operations ensure we get the entry/exit points correctly regardless of ray direction
|
|
111
|
+
t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2)) # Update valid parameter range
|
|
112
|
+
elif pnt_x < -cx or pnt_x > cx: # Ray parallel to x-axis but outside volume bounds
|
|
113
|
+
# Edge case: ray never intersects volume if parallel and outside boundaries
|
|
114
|
+
d_sino[iang, idet] = _ZERO; return
|
|
115
|
+
|
|
116
|
+
# Y-direction boundary intersections (identical logic to x-direction)
|
|
117
|
+
# Handle non-parallel rays: compute intersection parameters with bottom (-cy) and top (+cy) boundaries
|
|
118
|
+
if dir_y != 0.0: # Ray not parallel to y-axis (avoid division by zero)
|
|
119
|
+
ty1, ty2 = (-cy - pnt_y) / dir_y, (cy - pnt_y) / dir_y # Bottom and top boundary intersections
|
|
120
|
+
# Intersect y-boundary constraints with existing parameter range from x-boundaries
|
|
121
|
+
t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2)) # Intersect with x-range
|
|
122
|
+
elif pnt_y < -cy or pnt_y > cy: # Ray parallel to y-axis but outside volume bounds
|
|
123
|
+
# Edge case: ray never intersects volume if parallel and outside boundaries
|
|
124
|
+
d_sino[iang, idet] = _ZERO; return
|
|
125
|
+
|
|
126
|
+
# Boundary intersection validation: check if ray actually intersects the volume
|
|
127
|
+
# If t_min >= t_max, the ray misses the volume entirely (no valid intersection interval)
|
|
128
|
+
if t_min >= t_max:
|
|
129
|
+
d_sino[iang, idet] = _ZERO; return
|
|
130
|
+
|
|
131
|
+
# The setup above runs in float64 because a distant source makes float32
|
|
132
|
+
# ray parameters too coarse, and only an exactly zero direction component
|
|
133
|
+
# is treated as axis-parallel there. The float32 traversal restarts at the
|
|
134
|
+
# entry point, so its parameters stay within the volume diameter.
|
|
135
|
+
ent_x = np.float32(pnt_x + t_min * dir_x)
|
|
136
|
+
ent_y = np.float32(pnt_y + t_min * dir_y)
|
|
137
|
+
ray_x = np.float32(dir_x)
|
|
138
|
+
ray_y = np.float32(dir_y)
|
|
139
|
+
t_end = np.float32(t_max - t_min)
|
|
140
|
+
|
|
141
|
+
# === SIDDON METHOD VOXEL TRAVERSAL INITIALIZATION ===
|
|
142
|
+
accum = _ZERO # Accumulated projection value along ray
|
|
143
|
+
t = _ZERO
|
|
144
|
+
|
|
145
|
+
# Convert ray entry point to voxel indices (image coordinate system)
|
|
146
|
+
ix = int(math.floor(ent_x + t * ray_x + cx)) # Current voxel x-index
|
|
147
|
+
iy = int(math.floor(ent_y + t * ray_y + cy)) # Current voxel y-index
|
|
148
|
+
|
|
149
|
+
# Determine traversal direction and step sizes for each axis
|
|
150
|
+
step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1) # Voxel stepping direction
|
|
151
|
+
# Hoist inverse directions to reduce divisions and branches
|
|
152
|
+
inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
|
|
153
|
+
inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
|
|
154
|
+
dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
|
|
155
|
+
dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
|
|
156
|
+
|
|
157
|
+
# Calculate parameter values for next voxel boundary crossings using inv_dir_*
|
|
158
|
+
next_ix = ix + (1 if step_x > 0 else 0)
|
|
159
|
+
next_iy = iy + (1 if step_y > 0 else 0)
|
|
160
|
+
tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
|
|
161
|
+
ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
|
|
162
|
+
|
|
163
|
+
# === MAIN RAY TRAVERSAL LOOP ===
|
|
164
|
+
# Step through voxels along ray path, accumulating cell-constant contributions.
|
|
165
|
+
while t < t_end:
|
|
166
|
+
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
167
|
+
# Determine next voxel boundary crossing (minimum of x, y boundaries or ray exit)
|
|
168
|
+
t_next = min(tx, ty, t_end)
|
|
169
|
+
seg_len = t_next - t # Length of ray segment within current voxel region
|
|
170
|
+
if seg_len > _ZERO:
|
|
171
|
+
accum += d_image[iy, ix] * seg_len
|
|
172
|
+
|
|
173
|
+
# === VOXEL BOUNDARY CROSSING LOGIC ===
|
|
174
|
+
# Advance to next voxel based on which boundary is crossed first
|
|
175
|
+
if tx <= ty: # X-boundary crossed first
|
|
176
|
+
t = tx
|
|
177
|
+
ix += step_x # Move to next voxel in x-direction
|
|
178
|
+
tx += dt_x # Update next x-boundary crossing parameter
|
|
179
|
+
else: # Y-boundary crossed first
|
|
180
|
+
t = ty
|
|
181
|
+
iy += step_y # Move to next voxel in y-direction
|
|
182
|
+
ty += dt_y # Update next y-boundary crossing parameter
|
|
183
|
+
|
|
184
|
+
# Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
|
|
185
|
+
d_sino[iang, idet] = accum * voxel_spacing
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ============================================================================
|
|
189
|
+
# 2D Parallel Beam Backprojection Kernel
|
|
190
|
+
# ============================================================================
|
|
191
|
+
|
|
192
|
+
@_FASTMATH_DECORATOR
|
|
193
|
+
def _parallel_2d_backward_kernel(
|
|
194
|
+
d_sino, n_ang, n_det,
|
|
195
|
+
d_image, Nx, Ny,
|
|
196
|
+
det_spacing, d_ray_dir, d_det_origin, d_det_u_vec, cx, cy, voxel_spacing
|
|
197
|
+
):
|
|
198
|
+
"""Compute the 2D parallel beam backprojection with arbitrary ray trajectories.
|
|
199
|
+
|
|
200
|
+
This CUDA kernel implements the adjoint of cell-constant Siddon ray tracing for
|
|
201
|
+
2D parallel beam backprojection. Supports arbitrary ray directions and detector
|
|
202
|
+
positions for each view, enabling non-circular trajectories.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
207
|
+
Input sinogram array on CUDA.
|
|
208
|
+
n_ang : int
|
|
209
|
+
Number of projection angles.
|
|
210
|
+
n_det : int
|
|
211
|
+
Number of detector elements.
|
|
212
|
+
d_image : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
213
|
+
Output image gradient array on CUDA.
|
|
214
|
+
Nx : int
|
|
215
|
+
Number of voxels along the x-axis.
|
|
216
|
+
Ny : int
|
|
217
|
+
Number of voxels along the y-axis.
|
|
218
|
+
det_spacing : float
|
|
219
|
+
Physical spacing between detector elements.
|
|
220
|
+
d_ray_dir : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
221
|
+
Ray direction unit vectors for each view, shape (n_ang, 2).
|
|
222
|
+
d_det_origin : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
223
|
+
Detector origin positions for each view, shape (n_ang, 2), in physical units.
|
|
224
|
+
d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
225
|
+
Detector u-direction unit vectors for each view, shape (n_ang, 2).
|
|
226
|
+
cx : float
|
|
227
|
+
Half of image width in voxels.
|
|
228
|
+
cy : float
|
|
229
|
+
Half of image height in voxels.
|
|
230
|
+
voxel_spacing : float
|
|
231
|
+
Physical size of one voxel (in same units as det_spacing).
|
|
232
|
+
|
|
233
|
+
Notes
|
|
234
|
+
-----
|
|
235
|
+
This operation is the adjoint of the forward projection. Sinogram values
|
|
236
|
+
are distributed back into the volume along identical ray paths using
|
|
237
|
+
atomic operations to ensure thread-safe accumulation.
|
|
238
|
+
Supports arbitrary parallel beam geometries.
|
|
239
|
+
"""
|
|
240
|
+
iang, idet = cuda.grid(2)
|
|
241
|
+
if iang >= n_ang or idet >= n_det:
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
# === 2D BACKPROJECTION VALUE AND GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
|
|
245
|
+
# Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
|
|
246
|
+
val = d_sino[iang, idet] * voxel_spacing
|
|
247
|
+
|
|
248
|
+
# Read ray direction (parallel for all rays in this view)
|
|
249
|
+
dir_x = np.float64(d_ray_dir[iang, 0])
|
|
250
|
+
dir_y = np.float64(d_ray_dir[iang, 1])
|
|
251
|
+
|
|
252
|
+
# Read detector origin and orientation vector
|
|
253
|
+
det_ox = np.float64(d_det_origin[iang, 0]) / voxel_spacing
|
|
254
|
+
det_oy = np.float64(d_det_origin[iang, 1]) / voxel_spacing
|
|
255
|
+
|
|
256
|
+
u_vec_x = np.float64(d_det_u_vec[iang, 0])
|
|
257
|
+
u_vec_y = np.float64(d_det_u_vec[iang, 1])
|
|
258
|
+
|
|
259
|
+
# Calculate detector element offset from origin
|
|
260
|
+
u_offset = (np.float64(idet) + _HALF - np.float64(n_det) * _HALF) * det_spacing / voxel_spacing
|
|
261
|
+
|
|
262
|
+
# Ray starting point: detector origin + offset along u-direction
|
|
263
|
+
pnt_x = det_ox + u_offset * u_vec_x
|
|
264
|
+
pnt_y = det_oy + u_offset * u_vec_y
|
|
265
|
+
|
|
266
|
+
# === RAY-VOLUME INTERSECTION CALCULATION (identical to forward) ===
|
|
267
|
+
t_min, t_max = -_BIG, _BIG
|
|
268
|
+
if dir_x != 0.0:
|
|
269
|
+
tx1, tx2 = (-cx - pnt_x) / dir_x, (cx - pnt_x) / dir_x
|
|
270
|
+
t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
|
|
271
|
+
elif pnt_x < -cx or pnt_x > cx: return
|
|
272
|
+
|
|
273
|
+
if dir_y != 0.0:
|
|
274
|
+
ty1, ty2 = (-cy - pnt_y) / dir_y, (cy - pnt_y) / dir_y
|
|
275
|
+
t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
|
|
276
|
+
elif pnt_y < -cy or pnt_y > cy: return
|
|
277
|
+
|
|
278
|
+
if t_min >= t_max: return
|
|
279
|
+
|
|
280
|
+
# The setup above runs in float64 because a distant source makes float32
|
|
281
|
+
# ray parameters too coarse, and only an exactly zero direction component
|
|
282
|
+
# is treated as axis-parallel there. The float32 traversal restarts at the
|
|
283
|
+
# entry point, so its parameters stay within the volume diameter.
|
|
284
|
+
ent_x = np.float32(pnt_x + t_min * dir_x)
|
|
285
|
+
ent_y = np.float32(pnt_y + t_min * dir_y)
|
|
286
|
+
ray_x = np.float32(dir_x)
|
|
287
|
+
ray_y = np.float32(dir_y)
|
|
288
|
+
t_end = np.float32(t_max - t_min)
|
|
289
|
+
|
|
290
|
+
# === SIDDON METHOD TRAVERSAL INITIALIZATION ===
|
|
291
|
+
t = _ZERO
|
|
292
|
+
ix = int(math.floor(ent_x + t * ray_x + cx))
|
|
293
|
+
iy = int(math.floor(ent_y + t * ray_y + cy))
|
|
294
|
+
|
|
295
|
+
step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1)
|
|
296
|
+
inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
|
|
297
|
+
inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
|
|
298
|
+
dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
|
|
299
|
+
dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
|
|
300
|
+
next_ix = ix + (1 if step_x > 0 else 0)
|
|
301
|
+
next_iy = iy + (1 if step_y > 0 else 0)
|
|
302
|
+
tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
|
|
303
|
+
ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
|
|
304
|
+
|
|
305
|
+
# === BACKPROJECTION TRAVERSAL LOOP ===
|
|
306
|
+
# Adjoint of the cell-constant Siddon forward projection.
|
|
307
|
+
while t < t_end:
|
|
308
|
+
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
309
|
+
t_next = min(tx, ty, t_end)
|
|
310
|
+
seg_len = t_next - t
|
|
311
|
+
if seg_len > _ZERO:
|
|
312
|
+
cuda.atomic.add(d_image, (iy, ix), val * seg_len)
|
|
313
|
+
|
|
314
|
+
# Advance to next voxel (identical logic to forward projection)
|
|
315
|
+
if tx <= ty:
|
|
316
|
+
t = tx
|
|
317
|
+
ix += step_x
|
|
318
|
+
tx += dt_x
|
|
319
|
+
else:
|
|
320
|
+
t = ty
|
|
321
|
+
iy += step_y
|
|
322
|
+
ty += dt_y
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ============================================================================
|
|
327
|
+
# 2D Parallel Beam Analytical FBP Backprojection Gather (voxel-driven)
|
|
328
|
+
# ============================================================================
|
|
329
|
+
#
|
|
330
|
+
# Distinct from ``_parallel_2d_backward_kernel`` above, which is the
|
|
331
|
+
# pure Siddon adjoint ``P^T`` used by autograd. This kernel is the
|
|
332
|
+
# classical voxel-driven FBP gather: for each output pixel it projects
|
|
333
|
+
# the pixel centre onto the detector for each view, linearly samples
|
|
334
|
+
# the filtered sinogram, and accumulates. Parallel beam has no source,
|
|
335
|
+
# so there is no ``(sid/U)^2`` distance weighting.
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@_FDK_ACCURACY_DECORATOR
|
|
339
|
+
def _parallel_2d_fbp_backproject_kernel(
|
|
340
|
+
d_sino, n_views, n_det,
|
|
341
|
+
d_image, Nx, Ny,
|
|
342
|
+
det_spacing, d_ray_dir, d_det_origin, d_det_u_vec,
|
|
343
|
+
cx, cy, voxel_spacing
|
|
344
|
+
):
|
|
345
|
+
"""Voxel-driven parallel-beam FBP backprojection gather (arbitrary trajectory).
|
|
346
|
+
|
|
347
|
+
For each output pixel ``(ix, iy)`` loops over views and accumulates
|
|
348
|
+
a linearly interpolated filtered-sinogram sample. The pixel's
|
|
349
|
+
detector coordinate is ``u = (P - det_origin) . det_u_vec`` (no
|
|
350
|
+
magnification because the rays are parallel).
|
|
351
|
+
"""
|
|
352
|
+
ix, iy = cuda.grid(2)
|
|
353
|
+
if ix >= Nx or iy >= Ny:
|
|
354
|
+
return
|
|
355
|
+
if n_det < 2:
|
|
356
|
+
d_image[iy, ix] = _ZERO
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
# Sample at voxel centres, matching the cell-constant Siddon projector.
|
|
360
|
+
x_v = np.float32(ix) + _HALF - cx
|
|
361
|
+
y_v = np.float32(iy) + _HALF - cy
|
|
362
|
+
|
|
363
|
+
det_spacing_v = det_spacing / voxel_spacing
|
|
364
|
+
half_u = (np.float32(n_det) - _ONE) * _HALF
|
|
365
|
+
|
|
366
|
+
accum = _ZERO
|
|
367
|
+
for iview in range(n_views):
|
|
368
|
+
dox = d_det_origin[iview, 0] / voxel_spacing
|
|
369
|
+
doy = d_det_origin[iview, 1] / voxel_spacing
|
|
370
|
+
|
|
371
|
+
ux = d_det_u_vec[iview, 0]
|
|
372
|
+
uy = d_det_u_vec[iview, 1]
|
|
373
|
+
|
|
374
|
+
rx = x_v - dox
|
|
375
|
+
ry = y_v - doy
|
|
376
|
+
|
|
377
|
+
u_det = rx * ux + ry * uy
|
|
378
|
+
|
|
379
|
+
fu = u_det / det_spacing_v + half_u
|
|
380
|
+
if fu < _ZERO or fu > (np.float32(n_det) - _ONE):
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
iu0 = int(math.floor(fu))
|
|
384
|
+
if iu0 >= n_det - 1:
|
|
385
|
+
iu0 = n_det - 2
|
|
386
|
+
if iu0 < 0:
|
|
387
|
+
iu0 = 0
|
|
388
|
+
tu = fu - np.float32(iu0)
|
|
389
|
+
if tu < _ZERO:
|
|
390
|
+
tu = _ZERO
|
|
391
|
+
elif tu > _ONE:
|
|
392
|
+
tu = _ONE
|
|
393
|
+
|
|
394
|
+
s0 = d_sino[iview, iu0]
|
|
395
|
+
s1 = d_sino[iview, iu0 + 1]
|
|
396
|
+
sample = s0 * (_ONE - tu) + s1 * tu
|
|
397
|
+
|
|
398
|
+
accum += sample
|
|
399
|
+
|
|
400
|
+
d_image[iy, ix] = accum
|