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,451 @@
|
|
|
1
|
+
"""CUDA kernels for 2D fan beam projections.
|
|
2
|
+
|
|
3
|
+
This module contains CUDA kernels implementing the Siddon ray-tracing method
|
|
4
|
+
for 2D fan beam forward projection and backprojection, plus a dedicated
|
|
5
|
+
voxel-driven FBP 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
|
+
# 2D Fan Beam Forward Projection Kernel
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
@_FASTMATH_DECORATOR
|
|
29
|
+
def _fan_2d_forward_kernel(
|
|
30
|
+
d_image, Nx, Ny,
|
|
31
|
+
d_sino, n_ang, n_det,
|
|
32
|
+
det_spacing, d_src_pos, d_det_center, d_det_u_vec,
|
|
33
|
+
cx, cy, voxel_spacing
|
|
34
|
+
):
|
|
35
|
+
"""Compute the 2D fan beam forward projection with arbitrary source-detector trajectories.
|
|
36
|
+
|
|
37
|
+
This CUDA kernel implements cell-constant Siddon ray tracing for
|
|
38
|
+
2D fan beam forward projection. Supports arbitrary source and detector positions
|
|
39
|
+
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 fan beam 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_src_pos : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
58
|
+
Source positions for each view, shape (n_ang, 2), in physical units.
|
|
59
|
+
d_det_center : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
60
|
+
Detector center 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 fan beam geometries by specifying source position,
|
|
73
|
+
detector center, 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 FAN BEAM GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
|
|
81
|
+
# Read source position from position matrix (in physical units)
|
|
82
|
+
src_x = d_src_pos[iang, 0] / voxel_spacing
|
|
83
|
+
src_y = d_src_pos[iang, 1] / voxel_spacing
|
|
84
|
+
|
|
85
|
+
# Read detector center and orientation vector
|
|
86
|
+
det_cx = d_det_center[iang, 0] / voxel_spacing
|
|
87
|
+
det_cy = d_det_center[iang, 1] / voxel_spacing
|
|
88
|
+
|
|
89
|
+
u_vec_x = d_det_u_vec[iang, 0]
|
|
90
|
+
u_vec_y = d_det_u_vec[iang, 1]
|
|
91
|
+
|
|
92
|
+
# Calculate detector element offset from center
|
|
93
|
+
u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
|
|
94
|
+
|
|
95
|
+
# Calculate 2D detector element position using center + u*u_vec
|
|
96
|
+
det_x = det_cx + u_offset * u_vec_x
|
|
97
|
+
det_y = det_cy + u_offset * u_vec_y
|
|
98
|
+
|
|
99
|
+
# === RAY DIRECTION CALCULATION ===
|
|
100
|
+
# Ray direction vector from source to detector element
|
|
101
|
+
dir_x, dir_y = det_x - src_x, det_y - src_y
|
|
102
|
+
length = math.sqrt(dir_x * dir_x + dir_y * dir_y) # Ray length
|
|
103
|
+
if length < _EPSILON: # Degenerate ray case
|
|
104
|
+
d_sino[iang, idet] = _ZERO; return
|
|
105
|
+
|
|
106
|
+
# Normalize ray direction vector for parametric traversal
|
|
107
|
+
inv_len = _ONE / length
|
|
108
|
+
dir_x, dir_y = dir_x * inv_len, dir_y * inv_len
|
|
109
|
+
|
|
110
|
+
# === RAY-VOLUME INTERSECTION CALCULATION ===
|
|
111
|
+
# Measure the ray parameter from the endpoint nearer the volume centre, so a
|
|
112
|
+
# distant source does not make the float32 parameters coarse. Only the
|
|
113
|
+
# source-to-detector segment is integrated.
|
|
114
|
+
if src_x * src_x + src_y * src_y <= det_x * det_x + det_y * det_y:
|
|
115
|
+
org_x = src_x
|
|
116
|
+
org_y = src_y
|
|
117
|
+
t_min, t_max = _ZERO, length
|
|
118
|
+
else:
|
|
119
|
+
org_x = det_x
|
|
120
|
+
org_y = det_y
|
|
121
|
+
t_min, t_max = -length, _ZERO
|
|
122
|
+
if abs(dir_x) > _TINY:
|
|
123
|
+
tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x # Volume boundary intersections
|
|
124
|
+
t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
|
|
125
|
+
elif org_x < -cx or org_x > cx: # Ray origin outside volume bounds
|
|
126
|
+
d_sino[iang, idet] = _ZERO; return
|
|
127
|
+
|
|
128
|
+
if abs(dir_y) > _TINY:
|
|
129
|
+
ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
|
|
130
|
+
t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
|
|
131
|
+
elif org_y < -cy or org_y > cy:
|
|
132
|
+
d_sino[iang, idet] = _ZERO; return
|
|
133
|
+
|
|
134
|
+
if t_min >= t_max: # No valid intersection
|
|
135
|
+
d_sino[iang, idet] = _ZERO; return
|
|
136
|
+
|
|
137
|
+
# The traversal restarts at the entry point, so its parameters stay
|
|
138
|
+
# within the volume diameter.
|
|
139
|
+
ent_x = org_x + t_min * dir_x
|
|
140
|
+
ent_y = org_y + t_min * dir_y
|
|
141
|
+
ray_x = dir_x
|
|
142
|
+
ray_y = dir_y
|
|
143
|
+
t_end = t_max - t_min
|
|
144
|
+
|
|
145
|
+
# === SIDDON METHOD TRAVERSAL (same algorithm as parallel beam) ===
|
|
146
|
+
accum = _ZERO # Accumulated projection value
|
|
147
|
+
t = _ZERO
|
|
148
|
+
|
|
149
|
+
# Convert ray entry point to voxel indices
|
|
150
|
+
ix = int(math.floor(ent_x + t * ray_x + cx))
|
|
151
|
+
iy = int(math.floor(ent_y + t * ray_y + cy))
|
|
152
|
+
|
|
153
|
+
# Traversal parameters (identical to parallel beam implementation)
|
|
154
|
+
step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1)
|
|
155
|
+
inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
|
|
156
|
+
inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
|
|
157
|
+
dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
|
|
158
|
+
dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
|
|
159
|
+
next_ix = ix + (1 if step_x > 0 else 0)
|
|
160
|
+
next_iy = iy + (1 if step_y > 0 else 0)
|
|
161
|
+
tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
|
|
162
|
+
ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
|
|
163
|
+
|
|
164
|
+
# Main traversal loop with cell-constant Siddon integration.
|
|
165
|
+
while t < t_end:
|
|
166
|
+
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
167
|
+
t_next = min(tx, ty, t_end)
|
|
168
|
+
seg_len = t_next - t
|
|
169
|
+
if seg_len > _ZERO:
|
|
170
|
+
accum += d_image[iy, ix] * seg_len
|
|
171
|
+
|
|
172
|
+
# Voxel boundary crossing logic (identical to parallel beam)
|
|
173
|
+
if tx <= ty:
|
|
174
|
+
t = tx
|
|
175
|
+
ix += step_x
|
|
176
|
+
tx += dt_x
|
|
177
|
+
else:
|
|
178
|
+
t = ty
|
|
179
|
+
iy += step_y
|
|
180
|
+
ty += dt_y
|
|
181
|
+
|
|
182
|
+
# Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
|
|
183
|
+
d_sino[iang, idet] = accum * voxel_spacing
|
|
184
|
+
|
|
185
|
+
@_FASTMATH_DECORATOR
|
|
186
|
+
|
|
187
|
+
# ============================================================================
|
|
188
|
+
# 2D Fan Beam Backprojection Kernel
|
|
189
|
+
# ============================================================================
|
|
190
|
+
|
|
191
|
+
def _fan_2d_backward_kernel(
|
|
192
|
+
d_sino, n_ang, n_det,
|
|
193
|
+
d_image, Nx, Ny,
|
|
194
|
+
det_spacing, d_src_pos, d_det_center, d_det_u_vec,
|
|
195
|
+
cx, cy, voxel_spacing
|
|
196
|
+
):
|
|
197
|
+
"""Compute the 2D fan beam backprojection with arbitrary source-detector trajectories.
|
|
198
|
+
|
|
199
|
+
This CUDA kernel implements the adjoint of cell-constant Siddon ray tracing for
|
|
200
|
+
2D fan beam backprojection. Supports arbitrary source and detector positions
|
|
201
|
+
for each view, enabling non-circular trajectories.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
d_sino : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
206
|
+
Input fan beam sinogram array on CUDA.
|
|
207
|
+
n_ang : int
|
|
208
|
+
Number of projection angles.
|
|
209
|
+
n_det : int
|
|
210
|
+
Number of detector elements.
|
|
211
|
+
d_image : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
212
|
+
Output image gradient array on CUDA.
|
|
213
|
+
Nx : int
|
|
214
|
+
Number of voxels along the x-axis.
|
|
215
|
+
Ny : int
|
|
216
|
+
Number of voxels along the y-axis.
|
|
217
|
+
det_spacing : float
|
|
218
|
+
Physical spacing between detector elements.
|
|
219
|
+
d_src_pos : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
220
|
+
Source positions for each view, shape (n_ang, 2), in physical units.
|
|
221
|
+
d_det_center : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
222
|
+
Detector center positions for each view, shape (n_ang, 2), in physical units.
|
|
223
|
+
d_det_u_vec : numba.cuda.cudadrv.devicearray.DeviceNDArray
|
|
224
|
+
Detector u-direction unit vectors for each view, shape (n_ang, 2).
|
|
225
|
+
cx : float
|
|
226
|
+
Half of image width in voxels.
|
|
227
|
+
cy : float
|
|
228
|
+
Half of image height in voxels.
|
|
229
|
+
voxel_spacing : float
|
|
230
|
+
Physical size of one voxel (in same units as det_spacing).
|
|
231
|
+
|
|
232
|
+
Notes
|
|
233
|
+
-----
|
|
234
|
+
As the adjoint to the fan beam forward projection, this operation
|
|
235
|
+
distributes sinogram values back into the volume along divergent ray
|
|
236
|
+
paths using atomic operations for thread-safe accumulation.
|
|
237
|
+
Supports arbitrary fan beam geometries.
|
|
238
|
+
"""
|
|
239
|
+
iang, idet = cuda.grid(2)
|
|
240
|
+
if iang >= n_ang or idet >= n_det:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# === 2D BACKPROJECTION VALUE AND GEOMETRY SETUP (ARBITRARY TRAJECTORY) ===
|
|
244
|
+
# Siddon segment lengths are in voxel units; voxel_spacing makes them physical lengths.
|
|
245
|
+
val = d_sino[iang, idet] * voxel_spacing
|
|
246
|
+
|
|
247
|
+
# Read source position from position matrix (in physical units)
|
|
248
|
+
src_x = d_src_pos[iang, 0] / voxel_spacing
|
|
249
|
+
src_y = d_src_pos[iang, 1] / voxel_spacing
|
|
250
|
+
|
|
251
|
+
# Read detector center and orientation vector
|
|
252
|
+
det_cx = d_det_center[iang, 0] / voxel_spacing
|
|
253
|
+
det_cy = d_det_center[iang, 1] / voxel_spacing
|
|
254
|
+
|
|
255
|
+
u_vec_x = d_det_u_vec[iang, 0]
|
|
256
|
+
u_vec_y = d_det_u_vec[iang, 1]
|
|
257
|
+
|
|
258
|
+
# Calculate detector element offset from center
|
|
259
|
+
u_offset = (np.float32(idet) + _HALF - np.float32(n_det) * _HALF) * det_spacing / voxel_spacing
|
|
260
|
+
|
|
261
|
+
# Calculate 2D detector element position using center + u*u_vec
|
|
262
|
+
det_x = det_cx + u_offset * u_vec_x
|
|
263
|
+
det_y = det_cy + u_offset * u_vec_y
|
|
264
|
+
|
|
265
|
+
# === RAY DIRECTION CALCULATION ===
|
|
266
|
+
# Ray direction vector from source to detector element
|
|
267
|
+
dir_x, dir_y = det_x - src_x, det_y - src_y
|
|
268
|
+
length = math.sqrt(dir_x * dir_x + dir_y * dir_y) # Ray length
|
|
269
|
+
if length < _EPSILON: return # Skip degenerate rays
|
|
270
|
+
inv_len = _ONE / length # Normalization factor for ray direction
|
|
271
|
+
dir_x, dir_y = dir_x * inv_len, dir_y * inv_len # Normalized ray direction vector
|
|
272
|
+
|
|
273
|
+
# === RAY-VOLUME INTERSECTION CALCULATION ===
|
|
274
|
+
# Measure the ray parameter from the endpoint nearer the volume centre, so a
|
|
275
|
+
# distant source does not make the float32 parameters coarse. Only the
|
|
276
|
+
# source-to-detector segment is integrated.
|
|
277
|
+
if src_x * src_x + src_y * src_y <= det_x * det_x + det_y * det_y:
|
|
278
|
+
org_x = src_x
|
|
279
|
+
org_y = src_y
|
|
280
|
+
t_min, t_max = _ZERO, length
|
|
281
|
+
else:
|
|
282
|
+
org_x = det_x
|
|
283
|
+
org_y = det_y
|
|
284
|
+
t_min, t_max = -length, _ZERO
|
|
285
|
+
if abs(dir_x) > _TINY:
|
|
286
|
+
tx1, tx2 = (-cx - org_x) / dir_x, (cx - org_x) / dir_x
|
|
287
|
+
t_min, t_max = max(t_min, min(tx1, tx2)), min(t_max, max(tx1, tx2))
|
|
288
|
+
elif org_x < -cx or org_x > cx: return
|
|
289
|
+
|
|
290
|
+
if abs(dir_y) > _TINY:
|
|
291
|
+
ty1, ty2 = (-cy - org_y) / dir_y, (cy - org_y) / dir_y
|
|
292
|
+
t_min, t_max = max(t_min, min(ty1, ty2)), min(t_max, max(ty1, ty2))
|
|
293
|
+
elif org_y < -cy or org_y > cy: return
|
|
294
|
+
|
|
295
|
+
if t_min >= t_max: return
|
|
296
|
+
|
|
297
|
+
# The traversal restarts at the entry point, so its parameters stay
|
|
298
|
+
# within the volume diameter.
|
|
299
|
+
ent_x = org_x + t_min * dir_x
|
|
300
|
+
ent_y = org_y + t_min * dir_y
|
|
301
|
+
ray_x = dir_x
|
|
302
|
+
ray_y = dir_y
|
|
303
|
+
t_end = t_max - t_min
|
|
304
|
+
|
|
305
|
+
# === SIDDON METHOD TRAVERSAL INITIALIZATION ===
|
|
306
|
+
t = _ZERO
|
|
307
|
+
ix = int(math.floor(ent_x + t * ray_x + cx))
|
|
308
|
+
iy = int(math.floor(ent_y + t * ray_y + cy))
|
|
309
|
+
|
|
310
|
+
step_x, step_y = (1 if ray_x >= 0 else -1), (1 if ray_y >= 0 else -1)
|
|
311
|
+
inv_dir_x = (_ONE / ray_x) if abs(ray_x) > _TINY else _ZERO
|
|
312
|
+
inv_dir_y = (_ONE / ray_y) if abs(ray_y) > _TINY else _ZERO
|
|
313
|
+
dt_x = abs(inv_dir_x) if abs(ray_x) > _TINY else _BIG
|
|
314
|
+
dt_y = abs(inv_dir_y) if abs(ray_y) > _TINY else _BIG
|
|
315
|
+
next_ix = ix + (1 if step_x > 0 else 0)
|
|
316
|
+
next_iy = iy + (1 if step_y > 0 else 0)
|
|
317
|
+
tx = (np.float32(next_ix) - cx - ent_x) * inv_dir_x if abs(ray_x) > _TINY else _BIG
|
|
318
|
+
ty = (np.float32(next_iy) - cy - ent_y) * inv_dir_y if abs(ray_y) > _TINY else _BIG
|
|
319
|
+
|
|
320
|
+
# === FAN BEAM BACKPROJECTION TRAVERSAL LOOP ===
|
|
321
|
+
# Adjoint of the cell-constant Siddon forward projection.
|
|
322
|
+
while t < t_end:
|
|
323
|
+
if 0 <= ix < Nx and 0 <= iy < Ny:
|
|
324
|
+
t_next = min(tx, ty, t_end)
|
|
325
|
+
seg_len = t_next - t
|
|
326
|
+
if seg_len > _ZERO:
|
|
327
|
+
cuda.atomic.add(d_image, (iy, ix), val * seg_len)
|
|
328
|
+
|
|
329
|
+
# === VOXEL BOUNDARY CROSSING LOGIC ===
|
|
330
|
+
# Advance to next voxel based on which boundary is crossed first
|
|
331
|
+
if tx <= ty:
|
|
332
|
+
t = tx
|
|
333
|
+
ix += step_x
|
|
334
|
+
tx += dt_x
|
|
335
|
+
else:
|
|
336
|
+
t = ty
|
|
337
|
+
iy += step_y
|
|
338
|
+
ty += dt_y
|
|
339
|
+
|
|
340
|
+
# ============================================================================
|
|
341
|
+
# 2D Fan Beam Analytical FBP Backprojection Gather (voxel-driven)
|
|
342
|
+
# ============================================================================
|
|
343
|
+
#
|
|
344
|
+
# Distinct from ``_fan_2d_backward_kernel`` above, which is the pure
|
|
345
|
+
# Siddon adjoint ``P^T`` used by autograd. This kernel is the classical
|
|
346
|
+
# voxel-driven FBP gather: for each output pixel it projects the pixel
|
|
347
|
+
# centre onto the detector for each view, linearly samples the filtered
|
|
348
|
+
# sinogram, and accumulates ``(sid_v / U_n)^2 * sample``.
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@_FDK_ACCURACY_DECORATOR
|
|
352
|
+
def _fan_2d_fbp_backproject_kernel(
|
|
353
|
+
d_sino, n_views, n_det,
|
|
354
|
+
d_image, Nx, Ny,
|
|
355
|
+
det_spacing, d_src_pos, d_det_center, d_det_u_vec,
|
|
356
|
+
cx, cy, voxel_spacing, iso_x, iso_y
|
|
357
|
+
):
|
|
358
|
+
"""Voxel-driven fan-beam FBP backprojection gather (arbitrary trajectory).
|
|
359
|
+
|
|
360
|
+
For each pixel ``(ix, iy)`` loops over views and accumulates a linearly
|
|
361
|
+
interpolated filtered-sinogram sample weighted by ``(sid_n/U_n)^2``,
|
|
362
|
+
where ``U_n`` is the signed distance from the source ``S_v`` to the
|
|
363
|
+
pixel along the detector normal.
|
|
364
|
+
"""
|
|
365
|
+
ix, iy = cuda.grid(2)
|
|
366
|
+
if ix >= Nx or iy >= Ny:
|
|
367
|
+
return
|
|
368
|
+
if n_det < 2:
|
|
369
|
+
d_image[iy, ix] = _ZERO
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
# Sample at voxel centres, matching the cell-constant Siddon projector.
|
|
373
|
+
x_v = np.float32(ix) + _HALF - cx
|
|
374
|
+
y_v = np.float32(iy) + _HALF - cy
|
|
375
|
+
|
|
376
|
+
det_spacing_v = det_spacing / voxel_spacing
|
|
377
|
+
half_u = (np.float32(n_det) - _ONE) * _HALF
|
|
378
|
+
|
|
379
|
+
accum = _ZERO
|
|
380
|
+
for iview in range(n_views):
|
|
381
|
+
sx = d_src_pos[iview, 0] / voxel_spacing
|
|
382
|
+
sy = d_src_pos[iview, 1] / voxel_spacing
|
|
383
|
+
|
|
384
|
+
dcx = d_det_center[iview, 0] / voxel_spacing
|
|
385
|
+
dcy = d_det_center[iview, 1] / voxel_spacing
|
|
386
|
+
|
|
387
|
+
ux = d_det_u_vec[iview, 0]
|
|
388
|
+
uy = d_det_u_vec[iview, 1]
|
|
389
|
+
|
|
390
|
+
# Detector normal in 2D: rotate u_vec 90 degrees. Choose the
|
|
391
|
+
# orientation that points from the source toward the detector,
|
|
392
|
+
# so that ``U_n`` is positive for voxels on the detector side.
|
|
393
|
+
nx = -uy
|
|
394
|
+
ny = ux
|
|
395
|
+
# Align normal so that (det_center - src) . n > 0.
|
|
396
|
+
align = (dcx - sx) * nx + (dcy - sy) * ny
|
|
397
|
+
if align < _ZERO:
|
|
398
|
+
nx = -nx
|
|
399
|
+
ny = -ny
|
|
400
|
+
|
|
401
|
+
# Perpendicular distance from source to voxel along normal.
|
|
402
|
+
px = x_v - sx
|
|
403
|
+
py = y_v - sy
|
|
404
|
+
U_n = px * nx + py * ny
|
|
405
|
+
if U_n <= _EPSILON:
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
sdd_n = (dcx - sx) * nx + (dcy - sy) * ny
|
|
409
|
+
if sdd_n <= _EPSILON:
|
|
410
|
+
continue
|
|
411
|
+
mag = sdd_n / U_n
|
|
412
|
+
|
|
413
|
+
# Source-to-isocenter distance, independent of coordinate origin.
|
|
414
|
+
sid_n = (iso_x - sx) * nx + (iso_y - sy) * ny
|
|
415
|
+
if sid_n <= _EPSILON:
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
hx = sx + mag * px
|
|
419
|
+
hy = sy + mag * py
|
|
420
|
+
|
|
421
|
+
rx = hx - dcx
|
|
422
|
+
ry = hy - dcy
|
|
423
|
+
u_det = rx * ux + ry * uy
|
|
424
|
+
|
|
425
|
+
fu = u_det / det_spacing_v + half_u
|
|
426
|
+
if fu < _ZERO or fu > (np.float32(n_det) - _ONE):
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
iu0 = int(math.floor(fu))
|
|
430
|
+
if iu0 >= n_det - 1:
|
|
431
|
+
iu0 = n_det - 2
|
|
432
|
+
if iu0 < 0:
|
|
433
|
+
iu0 = 0
|
|
434
|
+
tu = fu - np.float32(iu0)
|
|
435
|
+
if tu < _ZERO:
|
|
436
|
+
tu = _ZERO
|
|
437
|
+
elif tu > _ONE:
|
|
438
|
+
tu = _ONE
|
|
439
|
+
|
|
440
|
+
s0 = d_sino[iview, iu0]
|
|
441
|
+
s1 = d_sino[iview, iu0 + 1]
|
|
442
|
+
sample = s0 * (_ONE - tu) + s1 * tu
|
|
443
|
+
|
|
444
|
+
w_ratio = sid_n / U_n
|
|
445
|
+
w = w_ratio * w_ratio
|
|
446
|
+
|
|
447
|
+
accum += w * sample
|
|
448
|
+
|
|
449
|
+
d_image[iy, ix] = accum
|
|
450
|
+
|
|
451
|
+
|