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
diffct/projectors.py
ADDED
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
"""PyTorch autograd functions for CT projections.
|
|
2
|
+
|
|
3
|
+
This module contains PyTorch autograd Function classes that wrap CUDA kernels
|
|
4
|
+
for differentiable CT forward projection and backprojection operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .constants import _DTYPE
|
|
11
|
+
from .utils import (
|
|
12
|
+
DeviceManager,
|
|
13
|
+
TorchCUDABridge,
|
|
14
|
+
_get_numba_external_stream_for,
|
|
15
|
+
_trig_tables,
|
|
16
|
+
_validate_3d_memory_layout,
|
|
17
|
+
_grid_2d,
|
|
18
|
+
_grid_3d,
|
|
19
|
+
)
|
|
20
|
+
from .kernels import (
|
|
21
|
+
_parallel_2d_forward_kernel,
|
|
22
|
+
_parallel_2d_backward_kernel,
|
|
23
|
+
_fan_2d_forward_kernel,
|
|
24
|
+
_fan_2d_backward_kernel,
|
|
25
|
+
_cone_3d_forward_kernel,
|
|
26
|
+
_cone_3d_backward_kernel,
|
|
27
|
+
_parallel_2d_geometry_vjp_kernel,
|
|
28
|
+
_fan_2d_geometry_vjp_kernel,
|
|
29
|
+
_cone_3d_geometry_vjp_kernel,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# Geometry gradients
|
|
35
|
+
# ============================================================================
|
|
36
|
+
|
|
37
|
+
def _geometry_vjp(beam, volume, grad_sino, geometry, detector_spacing, voxel_spacing):
|
|
38
|
+
"""Return d<grad_sino, A(geometry) volume>/d(geometry), one tensor per component.
|
|
39
|
+
|
|
40
|
+
``detector_spacing`` is a scalar for 2D beams and ``(du, dv)`` for cone
|
|
41
|
+
beams. Results take the dtype and device of each geometry component.
|
|
42
|
+
"""
|
|
43
|
+
device = volume.device
|
|
44
|
+
vol = volume.detach().to(device=device, dtype=torch.float32).contiguous()
|
|
45
|
+
cot = grad_sino.detach().to(device=device, dtype=torch.float32).contiguous()
|
|
46
|
+
geom = [g.detach().to(device=device, dtype=torch.float32).contiguous() for g in geometry]
|
|
47
|
+
grads = [torch.zeros_like(g) for g in geom]
|
|
48
|
+
d_geom = [TorchCUDABridge.tensor_to_cuda_array(g) for g in geom]
|
|
49
|
+
d_grads = [TorchCUDABridge.tensor_to_cuda_array(g) for g in grads]
|
|
50
|
+
numba_stream = _get_numba_external_stream_for(torch.cuda.current_stream())
|
|
51
|
+
if beam == "cone":
|
|
52
|
+
D, H, W = vol.shape
|
|
53
|
+
n_views, n_u, n_v = cot.shape
|
|
54
|
+
du, dv = detector_spacing
|
|
55
|
+
d_vol = TorchCUDABridge.tensor_to_cuda_array(vol.permute(2, 1, 0).contiguous())
|
|
56
|
+
grid, tpb = _grid_3d(n_v, n_u, n_views)
|
|
57
|
+
_cone_3d_geometry_vjp_kernel[grid, tpb, numba_stream](
|
|
58
|
+
d_vol, W, H, D, TorchCUDABridge.tensor_to_cuda_array(cot), n_views, n_u, n_v,
|
|
59
|
+
_DTYPE(du), _DTYPE(dv), *d_geom,
|
|
60
|
+
_DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(D * 0.5), _DTYPE(voxel_spacing), *d_grads,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
Ny, Nx = vol.shape
|
|
64
|
+
n_views, n_det = cot.shape
|
|
65
|
+
kernel = _parallel_2d_geometry_vjp_kernel if beam == "parallel" else _fan_2d_geometry_vjp_kernel
|
|
66
|
+
grid, tpb = _grid_2d(n_views, n_det)
|
|
67
|
+
kernel[grid, tpb, numba_stream](
|
|
68
|
+
TorchCUDABridge.tensor_to_cuda_array(vol), Nx, Ny,
|
|
69
|
+
TorchCUDABridge.tensor_to_cuda_array(cot), n_views, n_det,
|
|
70
|
+
_DTYPE(detector_spacing), *d_geom,
|
|
71
|
+
_DTYPE(Nx * 0.5), _DTYPE(Ny * 0.5), _DTYPE(voxel_spacing), *d_grads,
|
|
72
|
+
)
|
|
73
|
+
return tuple(grad.to(dtype=g.dtype, device=g.device) for grad, g in zip(grads, geometry))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _GeometryVJPFunction(torch.autograd.Function):
|
|
77
|
+
"""First-order geometry gradient; differentiating it again raises."""
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def forward(ctx, beam, volume, grad_sino, detector_spacing, voxel_spacing, *geometry):
|
|
81
|
+
return _geometry_vjp(beam, volume, grad_sino, geometry, detector_spacing, voxel_spacing)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def backward(ctx, *grad_outputs):
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
"diffct does not support second derivatives with respect to the geometry"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _geometry_grads(needs_grad, beam, volume, grad_sino, geometry, detector_spacing, voxel_spacing):
|
|
91
|
+
"""Geometry gradients for the components in ``needs_grad``; None for the rest."""
|
|
92
|
+
if not any(needs_grad):
|
|
93
|
+
return (None,) * len(geometry)
|
|
94
|
+
grads = _GeometryVJPFunction.apply(
|
|
95
|
+
beam, volume, grad_sino, detector_spacing, voxel_spacing, *geometry
|
|
96
|
+
)
|
|
97
|
+
return tuple(grad if needed else None for grad, needed in zip(grads, needs_grad))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ============================================================================
|
|
101
|
+
# PyTorch Autograd Functions
|
|
102
|
+
# ============================================================================
|
|
103
|
+
|
|
104
|
+
class ParallelProjectorFunction(torch.autograd.Function):
|
|
105
|
+
"""
|
|
106
|
+
Summary
|
|
107
|
+
-------
|
|
108
|
+
PyTorch autograd function for differentiable 2D parallel beam forward projection.
|
|
109
|
+
|
|
110
|
+
Notes
|
|
111
|
+
-----
|
|
112
|
+
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
113
|
+
method with a cell-constant image basis for parallel beam CT geometry. The forward pass computes
|
|
114
|
+
the sinogram from a 2D image using parallel beam geometry. The backward pass
|
|
115
|
+
computes gradients using the adjoint backprojection operation. Requires
|
|
116
|
+
CUDA-capable hardware and a properly configured CUDA environment; all input
|
|
117
|
+
tensors must reside on the same CUDA device.
|
|
118
|
+
|
|
119
|
+
Examples
|
|
120
|
+
--------
|
|
121
|
+
>>> import torch
|
|
122
|
+
>>> from diffct.differentiable import ParallelProjectorFunction
|
|
123
|
+
>>>
|
|
124
|
+
>>> # Create a 2D image with gradient tracking
|
|
125
|
+
>>> image = torch.randn(128, 128, device='cuda', requires_grad=True)
|
|
126
|
+
>>> # Define projection parameters
|
|
127
|
+
>>> angles = torch.linspace(0, torch.pi, 180, device='cuda')
|
|
128
|
+
>>> num_detectors = 128
|
|
129
|
+
>>> detector_spacing = 1.0
|
|
130
|
+
>>> # Compute forward projection
|
|
131
|
+
>>> projector = ParallelProjectorFunction.apply
|
|
132
|
+
>>> sinogram = projector(image, angles, num_detectors, detector_spacing)
|
|
133
|
+
>>> # Compute loss and gradients
|
|
134
|
+
>>> loss = sinogram.sum()
|
|
135
|
+
>>> loss.backward()
|
|
136
|
+
>>> print(f"Gradient shape: {image.grad.shape}") # (128, 128)
|
|
137
|
+
"""
|
|
138
|
+
@staticmethod
|
|
139
|
+
def forward(ctx, image, ray_dir, det_origin, det_u_vec, num_detectors, detector_spacing=1.0, voxel_spacing=1.0):
|
|
140
|
+
"""Compute the 2D parallel beam forward projection with arbitrary trajectories using CUDA acceleration.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
image : torch.Tensor
|
|
145
|
+
2D input image tensor of shape (H, W), must be on a CUDA device and of type float32.
|
|
146
|
+
ray_dir : torch.Tensor
|
|
147
|
+
Ray direction unit vectors for each view, shape (n_views, 2).
|
|
148
|
+
det_origin : torch.Tensor
|
|
149
|
+
Detector origin positions for each view, shape (n_views, 2), in physical units.
|
|
150
|
+
det_u_vec : torch.Tensor
|
|
151
|
+
Detector u-direction unit vectors for each view, shape (n_views, 2).
|
|
152
|
+
num_detectors : int
|
|
153
|
+
Number of detector elements in the sinogram (columns).
|
|
154
|
+
detector_spacing : float, optional
|
|
155
|
+
Physical spacing between detector elements (default: 1.0).
|
|
156
|
+
voxel_spacing : float, optional
|
|
157
|
+
Physical size of one voxel (in same units as detector_spacing, default: 1.0).
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
sinogram : torch.Tensor
|
|
162
|
+
2D tensor of shape (n_views, num_detectors) containing the forward projection (sinogram) on the same device as `image`.
|
|
163
|
+
|
|
164
|
+
Notes
|
|
165
|
+
-----
|
|
166
|
+
- All input tensors must be on the same CUDA device.
|
|
167
|
+
- The operation is fully differentiable and supports autograd.
|
|
168
|
+
- Supports arbitrary parallel beam geometries.
|
|
169
|
+
- Uses cell-constant Siddon ray tracing.
|
|
170
|
+
|
|
171
|
+
Examples
|
|
172
|
+
--------
|
|
173
|
+
>>> image = torch.randn(128, 128, device='cuda', requires_grad=True)
|
|
174
|
+
>>> ray_dir, det_origin, det_u_vec = circular_trajectory_2d_parallel(180, device='cuda')
|
|
175
|
+
>>> sinogram = ParallelProjectorFunction.apply(
|
|
176
|
+
... image, ray_dir, det_origin, det_u_vec, 128, 1.0
|
|
177
|
+
... )
|
|
178
|
+
"""
|
|
179
|
+
# Original inputs: backward builds a differentiable graph through them.
|
|
180
|
+
ctx.save_for_backward(
|
|
181
|
+
image if any(ctx.needs_input_grad[1:4]) else None,
|
|
182
|
+
ray_dir, det_origin, det_u_vec,
|
|
183
|
+
)
|
|
184
|
+
device = DeviceManager.get_device(image)
|
|
185
|
+
image = DeviceManager.ensure_device(image, device)
|
|
186
|
+
ray_dir = DeviceManager.ensure_device(ray_dir, device)
|
|
187
|
+
det_origin = DeviceManager.ensure_device(det_origin, device)
|
|
188
|
+
det_u_vec = DeviceManager.ensure_device(det_u_vec, device)
|
|
189
|
+
|
|
190
|
+
# Ensure input is float32 for kernel compatibility
|
|
191
|
+
image = image.to(dtype=torch.float32).contiguous()
|
|
192
|
+
ray_dir = ray_dir.to(dtype=torch.float32).contiguous()
|
|
193
|
+
det_origin = det_origin.to(dtype=torch.float32).contiguous()
|
|
194
|
+
det_u_vec = det_u_vec.to(dtype=torch.float32).contiguous()
|
|
195
|
+
|
|
196
|
+
Ny, Nx = image.shape
|
|
197
|
+
n_views = ray_dir.shape[0]
|
|
198
|
+
|
|
199
|
+
# Allocate output tensor on the same device
|
|
200
|
+
sinogram = torch.zeros((n_views, num_detectors), dtype=image.dtype, device=device)
|
|
201
|
+
|
|
202
|
+
# Get Numba CUDA array views for kernel
|
|
203
|
+
d_image = TorchCUDABridge.tensor_to_cuda_array(image)
|
|
204
|
+
d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
|
|
205
|
+
d_ray_dir_arr = TorchCUDABridge.tensor_to_cuda_array(ray_dir)
|
|
206
|
+
d_det_origin_arr = TorchCUDABridge.tensor_to_cuda_array(det_origin)
|
|
207
|
+
d_det_u_vec_arr = TorchCUDABridge.tensor_to_cuda_array(det_u_vec)
|
|
208
|
+
|
|
209
|
+
grid, tpb = _grid_2d(n_views, num_detectors)
|
|
210
|
+
cx, cy = _DTYPE(Nx * 0.5), _DTYPE(Ny * 0.5)
|
|
211
|
+
|
|
212
|
+
pt_stream = torch.cuda.current_stream()
|
|
213
|
+
numba_stream = _get_numba_external_stream_for(pt_stream)
|
|
214
|
+
_parallel_2d_forward_kernel[grid, tpb, numba_stream](
|
|
215
|
+
d_image, Nx, Ny, d_sino, n_views, num_detectors,
|
|
216
|
+
_DTYPE(detector_spacing), d_ray_dir_arr, d_det_origin_arr, d_det_u_vec_arr, cx, cy, _DTYPE(voxel_spacing)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
ctx.intermediate = (num_detectors, detector_spacing, Ny, Nx, voxel_spacing)
|
|
220
|
+
return sinogram
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def backward(ctx, grad_sinogram):
|
|
224
|
+
image, ray_dir, det_origin, det_u_vec = ctx.saved_tensors
|
|
225
|
+
num_detectors, detector_spacing, Ny, Nx, voxel_spacing = ctx.intermediate
|
|
226
|
+
grad_image = None
|
|
227
|
+
if ctx.needs_input_grad[0]:
|
|
228
|
+
# The adjoint is an autograd Function too, so second derivatives work.
|
|
229
|
+
grad_image = ParallelBackprojectorFunction.apply(
|
|
230
|
+
grad_sinogram, ray_dir, det_origin, det_u_vec, detector_spacing, Ny, Nx, voxel_spacing
|
|
231
|
+
)
|
|
232
|
+
geometry_grads = _geometry_grads(
|
|
233
|
+
ctx.needs_input_grad[1:4], "parallel", image, grad_sinogram,
|
|
234
|
+
(ray_dir, det_origin, det_u_vec), detector_spacing, voxel_spacing,
|
|
235
|
+
)
|
|
236
|
+
return (grad_image, *geometry_grads, None, None, None)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ParallelBackprojectorFunction(torch.autograd.Function):
|
|
240
|
+
"""
|
|
241
|
+
Summary
|
|
242
|
+
-------
|
|
243
|
+
PyTorch autograd function for differentiable 2D parallel beam backprojection.
|
|
244
|
+
|
|
245
|
+
Notes
|
|
246
|
+
-----
|
|
247
|
+
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
248
|
+
method with a cell-constant image basis for parallel beam backprojection. The forward pass computes a 2D
|
|
249
|
+
reconstruction from sinogram data using parallel beam backprojection, and the
|
|
250
|
+
backward pass computes gradients via forward projection as the adjoint operation.
|
|
251
|
+
Requires CUDA-capable hardware and consistent device placements.
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
Examples
|
|
255
|
+
--------
|
|
256
|
+
>>> import torch
|
|
257
|
+
>>> from diffct.differentiable import ParallelBackprojectorFunction
|
|
258
|
+
>>>
|
|
259
|
+
>>> sinogram = torch.randn(180, 128, device='cuda', requires_grad=True)
|
|
260
|
+
>>> angles = torch.linspace(0, torch.pi, 180, device='cuda')
|
|
261
|
+
>>> recon = ParallelBackprojectorFunction.apply(sinogram, angles, 1.0, 128, 128)
|
|
262
|
+
>>> loss = recon.sum()
|
|
263
|
+
>>> loss.backward()
|
|
264
|
+
>>> print(sinogram.grad.shape) # (180, 128)
|
|
265
|
+
"""
|
|
266
|
+
@staticmethod
|
|
267
|
+
def forward(ctx, sinogram, ray_dir, det_origin, det_u_vec, detector_spacing=1.0, H=128, W=128, voxel_spacing=1.0):
|
|
268
|
+
"""Compute the 2D parallel beam backprojection with arbitrary trajectories using CUDA acceleration.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
sinogram : torch.Tensor
|
|
273
|
+
2D input sinogram tensor of shape (n_views, num_detectors), must be on a CUDA device and of type float32.
|
|
274
|
+
ray_dir : torch.Tensor
|
|
275
|
+
Ray direction unit vectors for each view, shape (n_views, 2).
|
|
276
|
+
det_origin : torch.Tensor
|
|
277
|
+
Detector origin positions for each view, shape (n_views, 2), in physical units.
|
|
278
|
+
det_u_vec : torch.Tensor
|
|
279
|
+
Detector u-direction unit vectors for each view, shape (n_views, 2).
|
|
280
|
+
detector_spacing : float, optional
|
|
281
|
+
Physical spacing between detector elements (default: 1.0).
|
|
282
|
+
H : int, optional
|
|
283
|
+
Height of the output reconstruction image (default: 128).
|
|
284
|
+
W : int, optional
|
|
285
|
+
Width of the output reconstruction image (default: 128).
|
|
286
|
+
voxel_spacing : float, optional
|
|
287
|
+
Physical size of one voxel (in same units as detector_spacing, default: 1.0).
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
reco : torch.Tensor
|
|
292
|
+
2D tensor of shape (H, W) containing the reconstructed image on the same device as `sinogram`.
|
|
293
|
+
|
|
294
|
+
Notes
|
|
295
|
+
-----
|
|
296
|
+
- All input tensors must be on the same CUDA device.
|
|
297
|
+
- The operation is fully differentiable and supports autograd.
|
|
298
|
+
- Supports arbitrary parallel beam geometries.
|
|
299
|
+
- Uses the adjoint of cell-constant Siddon ray tracing.
|
|
300
|
+
|
|
301
|
+
Examples
|
|
302
|
+
--------
|
|
303
|
+
>>> sinogram = torch.randn(180, 128, device='cuda', requires_grad=True)
|
|
304
|
+
>>> ray_dir, det_origin, det_u_vec = circular_trajectory_2d_parallel(180, device='cuda')
|
|
305
|
+
>>> reco = ParallelBackprojectorFunction.apply(
|
|
306
|
+
... sinogram, ray_dir, det_origin, det_u_vec, 1.0, 128, 128
|
|
307
|
+
... )
|
|
308
|
+
"""
|
|
309
|
+
# Original inputs: backward builds a differentiable graph through them.
|
|
310
|
+
ctx.save_for_backward(
|
|
311
|
+
sinogram if any(ctx.needs_input_grad[1:4]) else None,
|
|
312
|
+
ray_dir, det_origin, det_u_vec,
|
|
313
|
+
)
|
|
314
|
+
device = DeviceManager.get_device(sinogram)
|
|
315
|
+
sinogram = DeviceManager.ensure_device(sinogram, device)
|
|
316
|
+
ray_dir = DeviceManager.ensure_device(ray_dir, device)
|
|
317
|
+
det_origin = DeviceManager.ensure_device(det_origin, device)
|
|
318
|
+
det_u_vec = DeviceManager.ensure_device(det_u_vec, device)
|
|
319
|
+
|
|
320
|
+
# Ensure input is float32 for kernel compatibility
|
|
321
|
+
sinogram = sinogram.to(dtype=torch.float32).contiguous()
|
|
322
|
+
ray_dir = ray_dir.to(dtype=torch.float32).contiguous()
|
|
323
|
+
det_origin = det_origin.to(dtype=torch.float32).contiguous()
|
|
324
|
+
det_u_vec = det_u_vec.to(dtype=torch.float32).contiguous()
|
|
325
|
+
|
|
326
|
+
n_views, n_det = sinogram.shape
|
|
327
|
+
Ny, Nx = H, W
|
|
328
|
+
|
|
329
|
+
# Allocate output tensor on the same device
|
|
330
|
+
reco = torch.zeros((Ny, Nx), dtype=sinogram.dtype, device=device)
|
|
331
|
+
|
|
332
|
+
# Get Numba CUDA array views for kernel
|
|
333
|
+
d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
|
|
334
|
+
d_reco = TorchCUDABridge.tensor_to_cuda_array(reco)
|
|
335
|
+
d_ray_dir_arr = TorchCUDABridge.tensor_to_cuda_array(ray_dir)
|
|
336
|
+
d_det_origin_arr = TorchCUDABridge.tensor_to_cuda_array(det_origin)
|
|
337
|
+
d_det_u_vec_arr = TorchCUDABridge.tensor_to_cuda_array(det_u_vec)
|
|
338
|
+
|
|
339
|
+
grid, tpb = _grid_2d(n_views, n_det)
|
|
340
|
+
cx, cy = _DTYPE(Nx * 0.5), _DTYPE(Ny * 0.5)
|
|
341
|
+
|
|
342
|
+
pt_stream = torch.cuda.current_stream()
|
|
343
|
+
numba_stream = _get_numba_external_stream_for(pt_stream)
|
|
344
|
+
_parallel_2d_backward_kernel[grid, tpb, numba_stream](
|
|
345
|
+
d_sino, n_views, n_det, d_reco, Nx, Ny,
|
|
346
|
+
_DTYPE(detector_spacing), d_ray_dir_arr, d_det_origin_arr, d_det_u_vec_arr, cx, cy, _DTYPE(voxel_spacing)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
ctx.intermediate = (H, W, detector_spacing, sinogram.shape[0], sinogram.shape[1], voxel_spacing)
|
|
350
|
+
return reco
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def backward(ctx, grad_output):
|
|
354
|
+
sinogram, ray_dir, det_origin, det_u_vec = ctx.saved_tensors
|
|
355
|
+
H, W, detector_spacing, n_views, n_det, voxel_spacing = ctx.intermediate
|
|
356
|
+
grad_sino = None
|
|
357
|
+
if ctx.needs_input_grad[0]:
|
|
358
|
+
grad_sino = ParallelProjectorFunction.apply(
|
|
359
|
+
grad_output, ray_dir, det_origin, det_u_vec, n_det, detector_spacing, voxel_spacing
|
|
360
|
+
)
|
|
361
|
+
# <grad_output, A^T y> = <A grad_output, y>, so the projector's VJP applies.
|
|
362
|
+
geometry_grads = _geometry_grads(
|
|
363
|
+
ctx.needs_input_grad[1:4], "parallel", grad_output, sinogram,
|
|
364
|
+
(ray_dir, det_origin, det_u_vec), detector_spacing, voxel_spacing,
|
|
365
|
+
)
|
|
366
|
+
return (grad_sino, *geometry_grads, None, None, None, None)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class FanProjectorFunction(torch.autograd.Function):
|
|
370
|
+
"""
|
|
371
|
+
Summary
|
|
372
|
+
-------
|
|
373
|
+
PyTorch autograd function for differentiable 2D fan beam forward projection.
|
|
374
|
+
|
|
375
|
+
Notes
|
|
376
|
+
-----
|
|
377
|
+
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
378
|
+
method with a cell-constant image basis for fan beam geometry, where rays diverge from a point
|
|
379
|
+
X-ray source to a linear detector array. The forward pass computes sinograms
|
|
380
|
+
using divergent beam geometry, and the backward pass computes gradients via
|
|
381
|
+
adjoint backprojection.
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
Examples
|
|
385
|
+
--------
|
|
386
|
+
>>> import torch
|
|
387
|
+
>>> from diffct.differentiable import FanProjectorFunction
|
|
388
|
+
>>>
|
|
389
|
+
>>> image = torch.randn(256, 256, device='cuda', requires_grad=True)
|
|
390
|
+
>>> angles = torch.linspace(0, 2 * torch.pi, 360, device='cuda')
|
|
391
|
+
>>> sinogram = FanProjectorFunction.apply(image, angles, 512, 1.0, 1500.0, 1000.0)
|
|
392
|
+
>>> loss = sinogram.sum()
|
|
393
|
+
>>> loss.backward()
|
|
394
|
+
>>> print(image.grad.shape) # (256, 256)
|
|
395
|
+
"""
|
|
396
|
+
@staticmethod
|
|
397
|
+
def forward(ctx, image, src_pos, det_center, det_u_vec, num_detectors, detector_spacing, voxel_spacing=1.0):
|
|
398
|
+
"""Compute the 2D fan beam forward projection with arbitrary trajectories using CUDA acceleration.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
image : torch.Tensor
|
|
403
|
+
2D input image tensor of shape (H, W), must be on a CUDA device and of type float32.
|
|
404
|
+
src_pos : torch.Tensor
|
|
405
|
+
Source positions for each view, shape (n_views, 2), in physical units.
|
|
406
|
+
det_center : torch.Tensor
|
|
407
|
+
Detector center positions for each view, shape (n_views, 2), in physical units.
|
|
408
|
+
det_u_vec : torch.Tensor
|
|
409
|
+
Detector u-direction unit vectors for each view, shape (n_views, 2).
|
|
410
|
+
num_detectors : int
|
|
411
|
+
Number of detector elements in the sinogram (columns).
|
|
412
|
+
detector_spacing : float
|
|
413
|
+
Physical spacing between detector elements.
|
|
414
|
+
voxel_spacing : float, optional
|
|
415
|
+
Physical size of one voxel (in same units as detector_spacing, default: 1.0).
|
|
416
|
+
|
|
417
|
+
Returns
|
|
418
|
+
-------
|
|
419
|
+
sinogram : torch.Tensor
|
|
420
|
+
2D tensor of shape (n_views, num_detectors) containing the fan beam sinogram on the same device as `image`.
|
|
421
|
+
|
|
422
|
+
Notes
|
|
423
|
+
-----
|
|
424
|
+
- All input tensors must be on the same CUDA device.
|
|
425
|
+
- The operation is fully differentiable and supports autograd.
|
|
426
|
+
- Supports arbitrary fan beam geometries.
|
|
427
|
+
- Uses cell-constant Siddon ray tracing.
|
|
428
|
+
|
|
429
|
+
Examples
|
|
430
|
+
--------
|
|
431
|
+
>>> image = torch.randn(256, 256, device='cuda', requires_grad=True)
|
|
432
|
+
>>> src_pos, det_center, det_u_vec = circular_trajectory_2d_fan(360, 1000.0, 1500.0, device='cuda')
|
|
433
|
+
>>> sinogram = FanProjectorFunction.apply(
|
|
434
|
+
... image, src_pos, det_center, det_u_vec, 512, 1.0
|
|
435
|
+
... )
|
|
436
|
+
"""
|
|
437
|
+
# Original inputs: backward builds a differentiable graph through them.
|
|
438
|
+
ctx.save_for_backward(
|
|
439
|
+
image if any(ctx.needs_input_grad[1:4]) else None,
|
|
440
|
+
src_pos, det_center, det_u_vec,
|
|
441
|
+
)
|
|
442
|
+
device = DeviceManager.get_device(image)
|
|
443
|
+
image = DeviceManager.ensure_device(image, device)
|
|
444
|
+
src_pos = DeviceManager.ensure_device(src_pos, device)
|
|
445
|
+
det_center = DeviceManager.ensure_device(det_center, device)
|
|
446
|
+
det_u_vec = DeviceManager.ensure_device(det_u_vec, device)
|
|
447
|
+
|
|
448
|
+
image = image.to(dtype=torch.float32).contiguous()
|
|
449
|
+
src_pos = src_pos.to(dtype=torch.float32).contiguous()
|
|
450
|
+
det_center = det_center.to(dtype=torch.float32).contiguous()
|
|
451
|
+
det_u_vec = det_u_vec.to(dtype=torch.float32).contiguous()
|
|
452
|
+
|
|
453
|
+
Ny, Nx = image.shape
|
|
454
|
+
n_views = src_pos.shape[0]
|
|
455
|
+
|
|
456
|
+
sinogram = torch.zeros((n_views, num_detectors), dtype=image.dtype, device=device)
|
|
457
|
+
|
|
458
|
+
d_image = TorchCUDABridge.tensor_to_cuda_array(image)
|
|
459
|
+
d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
|
|
460
|
+
d_src_pos_arr = TorchCUDABridge.tensor_to_cuda_array(src_pos)
|
|
461
|
+
d_det_center_arr = TorchCUDABridge.tensor_to_cuda_array(det_center)
|
|
462
|
+
d_det_u_vec_arr = TorchCUDABridge.tensor_to_cuda_array(det_u_vec)
|
|
463
|
+
|
|
464
|
+
grid, tpb = _grid_2d(n_views, num_detectors)
|
|
465
|
+
cx, cy = _DTYPE(Nx * 0.5), _DTYPE(Ny * 0.5)
|
|
466
|
+
|
|
467
|
+
pt_stream = torch.cuda.current_stream()
|
|
468
|
+
numba_stream = _get_numba_external_stream_for(pt_stream)
|
|
469
|
+
_fan_2d_forward_kernel[grid, tpb, numba_stream](
|
|
470
|
+
d_image, Nx, Ny, d_sino, n_views, num_detectors,
|
|
471
|
+
_DTYPE(detector_spacing), d_src_pos_arr, d_det_center_arr, d_det_u_vec_arr,
|
|
472
|
+
cx, cy, _DTYPE(voxel_spacing)
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
ctx.intermediate = (num_detectors, detector_spacing, Ny, Nx, voxel_spacing)
|
|
476
|
+
return sinogram
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def backward(ctx, grad_sinogram):
|
|
480
|
+
image, src_pos, det_center, det_u_vec = ctx.saved_tensors
|
|
481
|
+
num_detectors, detector_spacing, Ny, Nx, voxel_spacing = ctx.intermediate
|
|
482
|
+
grad_image = None
|
|
483
|
+
if ctx.needs_input_grad[0]:
|
|
484
|
+
# The adjoint is an autograd Function too, so second derivatives work.
|
|
485
|
+
grad_image = FanBackprojectorFunction.apply(
|
|
486
|
+
grad_sinogram, src_pos, det_center, det_u_vec, detector_spacing, Ny, Nx, voxel_spacing
|
|
487
|
+
)
|
|
488
|
+
geometry_grads = _geometry_grads(
|
|
489
|
+
ctx.needs_input_grad[1:4], "fan", image, grad_sinogram,
|
|
490
|
+
(src_pos, det_center, det_u_vec), detector_spacing, voxel_spacing,
|
|
491
|
+
)
|
|
492
|
+
return (grad_image, *geometry_grads, None, None, None)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class FanBackprojectorFunction(torch.autograd.Function):
|
|
496
|
+
"""
|
|
497
|
+
Summary
|
|
498
|
+
-------
|
|
499
|
+
PyTorch autograd function for differentiable 2D fan beam backprojection.
|
|
500
|
+
|
|
501
|
+
Notes
|
|
502
|
+
-----
|
|
503
|
+
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
504
|
+
method with a cell-constant image basis for fan beam backprojection. Implements the adjoint
|
|
505
|
+
of the fan beam projection operator, distributing sinogram values back into
|
|
506
|
+
the reconstruction volume along divergent ray paths. The forward pass
|
|
507
|
+
computes reconstruction from sinogram data, and the backward pass computes
|
|
508
|
+
gradients via forward projection.
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
Examples
|
|
512
|
+
--------
|
|
513
|
+
>>> import torch
|
|
514
|
+
>>> from diffct.differentiable import FanBackprojectorFunction
|
|
515
|
+
>>>
|
|
516
|
+
>>> sinogram = torch.randn(360, 512, device='cuda', requires_grad=True)
|
|
517
|
+
>>> angles = torch.linspace(0, 2 * torch.pi, 360, device='cuda')
|
|
518
|
+
>>> recon = FanBackprojectorFunction.apply(sinogram, angles, 1.0, 256, 256, 1500.0, 1000.0)
|
|
519
|
+
>>> loss = recon.sum()
|
|
520
|
+
>>> loss.backward()
|
|
521
|
+
>>> print(sinogram.grad.shape) # (360, 512)
|
|
522
|
+
"""
|
|
523
|
+
@staticmethod
|
|
524
|
+
def forward(ctx, sinogram, src_pos, det_center, det_u_vec, detector_spacing, H, W, voxel_spacing=1.0):
|
|
525
|
+
"""Compute the 2D fan beam backprojection with arbitrary trajectories using CUDA acceleration.
|
|
526
|
+
|
|
527
|
+
Parameters
|
|
528
|
+
----------
|
|
529
|
+
sinogram : torch.Tensor
|
|
530
|
+
2D input fan beam sinogram tensor of shape (n_views, num_detectors), must be on a CUDA device and of type float32.
|
|
531
|
+
src_pos : torch.Tensor
|
|
532
|
+
Source positions for each view, shape (n_views, 2), in physical units.
|
|
533
|
+
det_center : torch.Tensor
|
|
534
|
+
Detector center positions for each view, shape (n_views, 2), in physical units.
|
|
535
|
+
det_u_vec : torch.Tensor
|
|
536
|
+
Detector u-direction unit vectors for each view, shape (n_views, 2).
|
|
537
|
+
detector_spacing : float
|
|
538
|
+
Physical spacing between detector elements.
|
|
539
|
+
H : int
|
|
540
|
+
Height of the output reconstruction image.
|
|
541
|
+
W : int
|
|
542
|
+
Width of the output reconstruction image.
|
|
543
|
+
voxel_spacing : float, optional
|
|
544
|
+
Physical size of one voxel (in same units as detector_spacing, default: 1.0).
|
|
545
|
+
|
|
546
|
+
Returns
|
|
547
|
+
-------
|
|
548
|
+
reco : torch.Tensor
|
|
549
|
+
2D tensor of shape (H, W) containing the reconstructed image on the same device as `sinogram`.
|
|
550
|
+
|
|
551
|
+
Notes
|
|
552
|
+
-----
|
|
553
|
+
- All input tensors must be on the same CUDA device.
|
|
554
|
+
- The operation is fully differentiable and supports autograd.
|
|
555
|
+
- Supports arbitrary fan beam geometries.
|
|
556
|
+
- Uses the adjoint of cell-constant Siddon ray tracing.
|
|
557
|
+
|
|
558
|
+
Examples
|
|
559
|
+
--------
|
|
560
|
+
>>> sinogram = torch.randn(360, 512, device='cuda', requires_grad=True)
|
|
561
|
+
>>> src_pos, det_center, det_u_vec = circular_trajectory_2d_fan(360, 1000.0, 1500.0, device='cuda')
|
|
562
|
+
>>> reco = FanBackprojectorFunction.apply(
|
|
563
|
+
... sinogram, src_pos, det_center, det_u_vec, 1.0, 256, 256
|
|
564
|
+
... )
|
|
565
|
+
"""
|
|
566
|
+
# Original inputs: backward builds a differentiable graph through them.
|
|
567
|
+
ctx.save_for_backward(
|
|
568
|
+
sinogram if any(ctx.needs_input_grad[1:4]) else None,
|
|
569
|
+
src_pos, det_center, det_u_vec,
|
|
570
|
+
)
|
|
571
|
+
device = DeviceManager.get_device(sinogram)
|
|
572
|
+
sinogram = DeviceManager.ensure_device(sinogram, device)
|
|
573
|
+
src_pos = DeviceManager.ensure_device(src_pos, device)
|
|
574
|
+
det_center = DeviceManager.ensure_device(det_center, device)
|
|
575
|
+
det_u_vec = DeviceManager.ensure_device(det_u_vec, device)
|
|
576
|
+
|
|
577
|
+
sinogram = sinogram.to(dtype=torch.float32).contiguous()
|
|
578
|
+
src_pos = src_pos.to(dtype=torch.float32).contiguous()
|
|
579
|
+
det_center = det_center.to(dtype=torch.float32).contiguous()
|
|
580
|
+
det_u_vec = det_u_vec.to(dtype=torch.float32).contiguous()
|
|
581
|
+
|
|
582
|
+
n_views, n_det = sinogram.shape
|
|
583
|
+
Ny, Nx = H, W
|
|
584
|
+
|
|
585
|
+
reco = torch.zeros((Ny, Nx), dtype=sinogram.dtype, device=device)
|
|
586
|
+
|
|
587
|
+
d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
|
|
588
|
+
d_reco = TorchCUDABridge.tensor_to_cuda_array(reco)
|
|
589
|
+
d_src_pos_arr = TorchCUDABridge.tensor_to_cuda_array(src_pos)
|
|
590
|
+
d_det_center_arr = TorchCUDABridge.tensor_to_cuda_array(det_center)
|
|
591
|
+
d_det_u_vec_arr = TorchCUDABridge.tensor_to_cuda_array(det_u_vec)
|
|
592
|
+
|
|
593
|
+
grid, tpb = _grid_2d(n_views, n_det)
|
|
594
|
+
cx, cy = _DTYPE(Nx * 0.5), _DTYPE(Ny * 0.5)
|
|
595
|
+
|
|
596
|
+
pt_stream = torch.cuda.current_stream()
|
|
597
|
+
numba_stream = _get_numba_external_stream_for(pt_stream)
|
|
598
|
+
_fan_2d_backward_kernel[grid, tpb, numba_stream](
|
|
599
|
+
d_sino, n_views, n_det, d_reco, Nx, Ny,
|
|
600
|
+
_DTYPE(detector_spacing), d_src_pos_arr, d_det_center_arr, d_det_u_vec_arr,
|
|
601
|
+
cx, cy, _DTYPE(voxel_spacing)
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
ctx.intermediate = (H, W, detector_spacing, n_views, n_det, voxel_spacing)
|
|
605
|
+
return reco
|
|
606
|
+
|
|
607
|
+
@staticmethod
|
|
608
|
+
def backward(ctx, grad_output):
|
|
609
|
+
sinogram, src_pos, det_center, det_u_vec = ctx.saved_tensors
|
|
610
|
+
H, W, detector_spacing, n_views, n_det, voxel_spacing = ctx.intermediate
|
|
611
|
+
grad_sino = None
|
|
612
|
+
if ctx.needs_input_grad[0]:
|
|
613
|
+
grad_sino = FanProjectorFunction.apply(
|
|
614
|
+
grad_output, src_pos, det_center, det_u_vec, n_det, detector_spacing, voxel_spacing
|
|
615
|
+
)
|
|
616
|
+
# <grad_output, A^T y> = <A grad_output, y>, so the projector's VJP applies.
|
|
617
|
+
geometry_grads = _geometry_grads(
|
|
618
|
+
ctx.needs_input_grad[1:4], "fan", grad_output, sinogram,
|
|
619
|
+
(src_pos, det_center, det_u_vec), detector_spacing, voxel_spacing,
|
|
620
|
+
)
|
|
621
|
+
return (grad_sino, *geometry_grads, None, None, None, None)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class ConeProjectorFunction(torch.autograd.Function):
|
|
625
|
+
"""
|
|
626
|
+
Summary
|
|
627
|
+
-------
|
|
628
|
+
PyTorch autograd function for differentiable 3D cone beam forward projection.
|
|
629
|
+
|
|
630
|
+
Notes
|
|
631
|
+
-----
|
|
632
|
+
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
633
|
+
method with a cell-constant voxel basis for 3D cone beam geometry. Rays emanate from a point
|
|
634
|
+
X-ray source to a 2D detector array capturing volumetric projection data.
|
|
635
|
+
The forward pass computes 3D projections, and the backward pass computes
|
|
636
|
+
gradients via adjoint 3D backprojection. Requires significant GPU memory.
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
Examples
|
|
640
|
+
--------
|
|
641
|
+
>>> import torch
|
|
642
|
+
>>> from diffct.differentiable import ConeProjectorFunction
|
|
643
|
+
>>>
|
|
644
|
+
>>> volume = torch.randn(128, 128, 128, device='cuda', requires_grad=True)
|
|
645
|
+
>>> angles = torch.linspace(0, 2 * torch.pi, 360, device='cuda')
|
|
646
|
+
>>> projections = ConeProjectorFunction.apply(volume, angles, 256, 256, 1.0, 1.0, 1500.0, 1000.0)
|
|
647
|
+
>>> loss = projections.sum()
|
|
648
|
+
>>> loss.backward()
|
|
649
|
+
>>> print(volume.grad.shape) # (128, 128, 128)
|
|
650
|
+
"""
|
|
651
|
+
@staticmethod
|
|
652
|
+
def forward(ctx, volume, src_pos, det_center, det_u_vec, det_v_vec, det_u, det_v, du, dv, voxel_spacing=1.0):
|
|
653
|
+
"""Compute the 3D cone beam forward projection with arbitrary trajectories using CUDA acceleration.
|
|
654
|
+
|
|
655
|
+
Parameters
|
|
656
|
+
----------
|
|
657
|
+
volume : torch.Tensor
|
|
658
|
+
3D input volume tensor of shape (D, H, W), must be on a CUDA device and of type float32.
|
|
659
|
+
src_pos : torch.Tensor
|
|
660
|
+
Source positions for each view, shape (n_views, 3), in physical units.
|
|
661
|
+
det_center : torch.Tensor
|
|
662
|
+
Detector center positions for each view, shape (n_views, 3), in physical units.
|
|
663
|
+
det_u_vec : torch.Tensor
|
|
664
|
+
Detector u-direction unit vectors for each view, shape (n_views, 3).
|
|
665
|
+
det_v_vec : torch.Tensor
|
|
666
|
+
Detector v-direction unit vectors for each view, shape (n_views, 3).
|
|
667
|
+
det_u : int
|
|
668
|
+
Number of detector elements along the u-axis (width).
|
|
669
|
+
det_v : int
|
|
670
|
+
Number of detector elements along the v-axis (height).
|
|
671
|
+
du : float
|
|
672
|
+
Physical spacing between detector elements along the u-axis.
|
|
673
|
+
dv : float
|
|
674
|
+
Physical spacing between detector elements along the v-axis.
|
|
675
|
+
voxel_spacing : float, optional
|
|
676
|
+
Physical size of one voxel (in same units as positions, default: 1.0).
|
|
677
|
+
|
|
678
|
+
Returns
|
|
679
|
+
-------
|
|
680
|
+
sino : torch.Tensor
|
|
681
|
+
3D tensor of shape (n_views, det_u, det_v) containing the cone beam projections on the same device as `volume`.
|
|
682
|
+
|
|
683
|
+
Notes
|
|
684
|
+
-----
|
|
685
|
+
- All input tensors must be on the same CUDA device.
|
|
686
|
+
- The operation is fully differentiable and supports autograd.
|
|
687
|
+
- Supports arbitrary source and detector trajectories, not limited to circular orbits.
|
|
688
|
+
- Uses cell-constant Siddon ray tracing.
|
|
689
|
+
|
|
690
|
+
Examples
|
|
691
|
+
--------
|
|
692
|
+
>>> volume = torch.randn(128, 128, 128, device='cuda', requires_grad=True)
|
|
693
|
+
>>> # Create circular trajectory
|
|
694
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = circular_trajectory_3d(360, 1000.0, 1500.0, device='cuda')
|
|
695
|
+
>>> sino = ConeProjectorFunction.apply(
|
|
696
|
+
... volume, src_pos, det_center, det_u_vec, det_v_vec, 256, 256, 1.0, 1.0
|
|
697
|
+
... )
|
|
698
|
+
"""
|
|
699
|
+
# Original inputs: backward builds a differentiable graph through them.
|
|
700
|
+
ctx.save_for_backward(
|
|
701
|
+
volume if any(ctx.needs_input_grad[1:5]) else None,
|
|
702
|
+
src_pos, det_center, det_u_vec, det_v_vec,
|
|
703
|
+
)
|
|
704
|
+
device = DeviceManager.get_device(volume)
|
|
705
|
+
volume = DeviceManager.ensure_device(volume, device)
|
|
706
|
+
src_pos = DeviceManager.ensure_device(src_pos, device)
|
|
707
|
+
det_center = DeviceManager.ensure_device(det_center, device)
|
|
708
|
+
det_u_vec = DeviceManager.ensure_device(det_u_vec, device)
|
|
709
|
+
det_v_vec = DeviceManager.ensure_device(det_v_vec, device)
|
|
710
|
+
|
|
711
|
+
volume = volume.to(dtype=torch.float32).contiguous()
|
|
712
|
+
src_pos = src_pos.to(dtype=torch.float32).contiguous()
|
|
713
|
+
det_center = det_center.to(dtype=torch.float32).contiguous()
|
|
714
|
+
det_u_vec = det_u_vec.to(dtype=torch.float32).contiguous()
|
|
715
|
+
det_v_vec = det_v_vec.to(dtype=torch.float32).contiguous()
|
|
716
|
+
|
|
717
|
+
D, H, W = volume.shape
|
|
718
|
+
n_views = src_pos.shape[0]
|
|
719
|
+
|
|
720
|
+
# Validate memory layout to prevent coordinate system inconsistencies
|
|
721
|
+
_validate_3d_memory_layout(volume, expected_order='DHW')
|
|
722
|
+
|
|
723
|
+
sino = torch.zeros((n_views, det_u, det_v), dtype=volume.dtype, device=device)
|
|
724
|
+
|
|
725
|
+
volume_perm = volume.permute(2, 1, 0).contiguous()
|
|
726
|
+
d_vol = TorchCUDABridge.tensor_to_cuda_array(volume_perm)
|
|
727
|
+
d_sino = TorchCUDABridge.tensor_to_cuda_array(sino)
|
|
728
|
+
d_src_pos_arr = TorchCUDABridge.tensor_to_cuda_array(src_pos)
|
|
729
|
+
d_det_center_arr = TorchCUDABridge.tensor_to_cuda_array(det_center)
|
|
730
|
+
d_det_u_vec_arr = TorchCUDABridge.tensor_to_cuda_array(det_u_vec)
|
|
731
|
+
d_det_v_vec_arr = TorchCUDABridge.tensor_to_cuda_array(det_v_vec)
|
|
732
|
+
|
|
733
|
+
grid, tpb = _grid_3d(det_v, det_u, n_views)
|
|
734
|
+
cx, cy, cz = _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(D * 0.5)
|
|
735
|
+
|
|
736
|
+
pt_stream = torch.cuda.current_stream()
|
|
737
|
+
numba_stream = _get_numba_external_stream_for(pt_stream)
|
|
738
|
+
_cone_3d_forward_kernel[grid, tpb, numba_stream](
|
|
739
|
+
d_vol, W, H, D, d_sino, n_views, det_u, det_v,
|
|
740
|
+
_DTYPE(du), _DTYPE(dv), d_src_pos_arr, d_det_center_arr, d_det_u_vec_arr, d_det_v_vec_arr,
|
|
741
|
+
cx, cy, cz, _DTYPE(voxel_spacing)
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
ctx.intermediate = (D, H, W, det_u, det_v, du, dv, voxel_spacing)
|
|
745
|
+
return sino
|
|
746
|
+
|
|
747
|
+
@staticmethod
|
|
748
|
+
def backward(ctx, grad_sinogram):
|
|
749
|
+
volume, src_pos, det_center, det_u_vec, det_v_vec = ctx.saved_tensors
|
|
750
|
+
(D, H, W, det_u, det_v, du, dv, voxel_spacing) = ctx.intermediate
|
|
751
|
+
grad_vol = None
|
|
752
|
+
if ctx.needs_input_grad[0]:
|
|
753
|
+
# The adjoint is an autograd Function too, so second derivatives work.
|
|
754
|
+
grad_vol = ConeBackprojectorFunction.apply(
|
|
755
|
+
grad_sinogram, src_pos, det_center, det_u_vec, det_v_vec, D, H, W, du, dv, voxel_spacing
|
|
756
|
+
)
|
|
757
|
+
geometry_grads = _geometry_grads(
|
|
758
|
+
ctx.needs_input_grad[1:5], "cone", volume, grad_sinogram,
|
|
759
|
+
(src_pos, det_center, det_u_vec, det_v_vec), (du, dv), voxel_spacing,
|
|
760
|
+
)
|
|
761
|
+
return (grad_vol, *geometry_grads, None, None, None, None, None)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class ConeBackprojectorFunction(torch.autograd.Function):
|
|
765
|
+
"""
|
|
766
|
+
Summary
|
|
767
|
+
-------
|
|
768
|
+
PyTorch autograd function for differentiable 3D cone beam backprojection.
|
|
769
|
+
|
|
770
|
+
Notes
|
|
771
|
+
-----
|
|
772
|
+
Provides a differentiable interface to the CUDA-accelerated Siddon ray-tracing
|
|
773
|
+
method with a cell-constant voxel basis for 3D cone beam backprojection. The forward pass
|
|
774
|
+
computes a 3D reconstruction from cone beam projection data using
|
|
775
|
+
backprojection as the adjoint operation. The backward pass computes gradients
|
|
776
|
+
via 3D cone beam forward projection. Requires CUDA-capable hardware and
|
|
777
|
+
consistent device placements.
|
|
778
|
+
|
|
779
|
+
This operation may be memory- and computationally-intensive due to 3D geometry.
|
|
780
|
+
Consider using gradient checkpointing, smaller volumes, or distributed computing
|
|
781
|
+
for large-scale applications, and ensure sufficient GPU memory is available.
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
Examples
|
|
785
|
+
--------
|
|
786
|
+
>>> import torch
|
|
787
|
+
>>> from diffct.differentiable import ConeBackprojectorFunction
|
|
788
|
+
>>>
|
|
789
|
+
>>> projections = torch.randn(360, 256, 256, device='cuda', requires_grad=True)
|
|
790
|
+
>>> angles = torch.linspace(0, 2 * torch.pi, 360, device='cuda')
|
|
791
|
+
>>> D, H, W = 128, 128, 128
|
|
792
|
+
>>> du, dv = 1.0, 1.0
|
|
793
|
+
>>> sdd, sid = 1500.0, 1000.0
|
|
794
|
+
>>> backprojector = ConeBackprojectorFunction.apply
|
|
795
|
+
>>> volume = backprojector(projections, angles, D, H, W, du, dv, sdd, sid)
|
|
796
|
+
>>> loss = volume.sum()
|
|
797
|
+
>>> loss.backward()
|
|
798
|
+
>>> print(f"Projection gradient shape: {projections.grad.shape}") # (360, 256, 256)
|
|
799
|
+
"""
|
|
800
|
+
@staticmethod
|
|
801
|
+
def forward(ctx, sinogram, src_pos, det_center, det_u_vec, det_v_vec, D, H, W, du, dv, voxel_spacing=1.0):
|
|
802
|
+
"""Compute the 3D cone beam backprojection with arbitrary trajectories using CUDA acceleration.
|
|
803
|
+
|
|
804
|
+
Parameters
|
|
805
|
+
----------
|
|
806
|
+
sinogram : torch.Tensor
|
|
807
|
+
3D input cone beam projection tensor of shape (n_views, det_u, det_v), must be on a CUDA device and of type float32.
|
|
808
|
+
src_pos : torch.Tensor
|
|
809
|
+
Source positions for each view, shape (n_views, 3), in physical units.
|
|
810
|
+
det_center : torch.Tensor
|
|
811
|
+
Detector center positions for each view, shape (n_views, 3), in physical units.
|
|
812
|
+
det_u_vec : torch.Tensor
|
|
813
|
+
Detector u-direction unit vectors for each view, shape (n_views, 3).
|
|
814
|
+
det_v_vec : torch.Tensor
|
|
815
|
+
Detector v-direction unit vectors for each view, shape (n_views, 3).
|
|
816
|
+
D : int
|
|
817
|
+
Depth (z-dimension) of the output reconstruction volume.
|
|
818
|
+
H : int
|
|
819
|
+
Height (y-dimension) of the output reconstruction volume.
|
|
820
|
+
W : int
|
|
821
|
+
Width (x-dimension) of the output reconstruction volume.
|
|
822
|
+
du : float
|
|
823
|
+
Physical spacing between detector elements along the u-axis.
|
|
824
|
+
dv : float
|
|
825
|
+
Physical spacing between detector elements along the v-axis.
|
|
826
|
+
voxel_spacing : float, optional
|
|
827
|
+
Physical size of one voxel (in same units as positions, default: 1.0).
|
|
828
|
+
|
|
829
|
+
Returns
|
|
830
|
+
-------
|
|
831
|
+
vol : torch.Tensor
|
|
832
|
+
3D tensor of shape (D, H, W) containing the reconstructed volume on the same device as `sinogram`.
|
|
833
|
+
|
|
834
|
+
Notes
|
|
835
|
+
-----
|
|
836
|
+
- All input tensors must be on the same CUDA device.
|
|
837
|
+
- The operation is fully differentiable and supports autograd.
|
|
838
|
+
- Supports arbitrary source and detector trajectories.
|
|
839
|
+
- Uses the adjoint of cell-constant Siddon ray tracing.
|
|
840
|
+
|
|
841
|
+
Examples
|
|
842
|
+
--------
|
|
843
|
+
>>> projections = torch.randn(360, 256, 256, device='cuda', requires_grad=True)
|
|
844
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = circular_trajectory_3d(360, 1000.0, 1500.0, device='cuda')
|
|
845
|
+
>>> vol = ConeBackprojectorFunction.apply(
|
|
846
|
+
... projections, src_pos, det_center, det_u_vec, det_v_vec, 128, 128, 128, 1.0, 1.0
|
|
847
|
+
... )
|
|
848
|
+
"""
|
|
849
|
+
# Original inputs: backward builds a differentiable graph through them.
|
|
850
|
+
ctx.save_for_backward(
|
|
851
|
+
sinogram if any(ctx.needs_input_grad[1:5]) else None,
|
|
852
|
+
src_pos, det_center, det_u_vec, det_v_vec,
|
|
853
|
+
)
|
|
854
|
+
device = DeviceManager.get_device(sinogram)
|
|
855
|
+
sinogram = DeviceManager.ensure_device(sinogram, device)
|
|
856
|
+
src_pos = DeviceManager.ensure_device(src_pos, device)
|
|
857
|
+
det_center = DeviceManager.ensure_device(det_center, device)
|
|
858
|
+
det_u_vec = DeviceManager.ensure_device(det_u_vec, device)
|
|
859
|
+
det_v_vec = DeviceManager.ensure_device(det_v_vec, device)
|
|
860
|
+
|
|
861
|
+
sinogram = sinogram.to(dtype=torch.float32).contiguous()
|
|
862
|
+
src_pos = src_pos.to(dtype=torch.float32).contiguous()
|
|
863
|
+
det_center = det_center.to(dtype=torch.float32).contiguous()
|
|
864
|
+
det_u_vec = det_u_vec.to(dtype=torch.float32).contiguous()
|
|
865
|
+
det_v_vec = det_v_vec.to(dtype=torch.float32).contiguous()
|
|
866
|
+
|
|
867
|
+
n_views, n_u, n_v = sinogram.shape
|
|
868
|
+
|
|
869
|
+
# Validate memory layout to prevent coordinate system inconsistencies
|
|
870
|
+
_validate_3d_memory_layout(sinogram, expected_order='VHW')
|
|
871
|
+
|
|
872
|
+
vol_perm = torch.zeros((W, H, D), dtype=sinogram.dtype, device=device)
|
|
873
|
+
|
|
874
|
+
d_sino = TorchCUDABridge.tensor_to_cuda_array(sinogram)
|
|
875
|
+
d_reco = TorchCUDABridge.tensor_to_cuda_array(vol_perm)
|
|
876
|
+
d_src_pos_arr = TorchCUDABridge.tensor_to_cuda_array(src_pos)
|
|
877
|
+
d_det_center_arr = TorchCUDABridge.tensor_to_cuda_array(det_center)
|
|
878
|
+
d_det_u_vec_arr = TorchCUDABridge.tensor_to_cuda_array(det_u_vec)
|
|
879
|
+
d_det_v_vec_arr = TorchCUDABridge.tensor_to_cuda_array(det_v_vec)
|
|
880
|
+
|
|
881
|
+
grid, tpb = _grid_3d(n_v, n_u, n_views)
|
|
882
|
+
cx, cy, cz = _DTYPE(W * 0.5), _DTYPE(H * 0.5), _DTYPE(D * 0.5)
|
|
883
|
+
|
|
884
|
+
pt_stream = torch.cuda.current_stream()
|
|
885
|
+
numba_stream = _get_numba_external_stream_for(pt_stream)
|
|
886
|
+
_cone_3d_backward_kernel[grid, tpb, numba_stream](
|
|
887
|
+
d_sino, n_views, n_u, n_v, d_reco, W, H, D,
|
|
888
|
+
_DTYPE(du), _DTYPE(dv), d_src_pos_arr, d_det_center_arr, d_det_u_vec_arr, d_det_v_vec_arr,
|
|
889
|
+
cx, cy, cz, _DTYPE(voxel_spacing)
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
ctx.intermediate = (D, H, W, n_u, n_v, du, dv, voxel_spacing)
|
|
893
|
+
vol = vol_perm.permute(2, 1, 0).contiguous()
|
|
894
|
+
return vol
|
|
895
|
+
|
|
896
|
+
@staticmethod
|
|
897
|
+
def backward(ctx, grad_output):
|
|
898
|
+
sinogram, src_pos, det_center, det_u_vec, det_v_vec = ctx.saved_tensors
|
|
899
|
+
(D, H, W, n_u, n_v, du, dv, voxel_spacing) = ctx.intermediate
|
|
900
|
+
grad_sino = None
|
|
901
|
+
if ctx.needs_input_grad[0]:
|
|
902
|
+
grad_sino = ConeProjectorFunction.apply(
|
|
903
|
+
grad_output, src_pos, det_center, det_u_vec, det_v_vec, n_u, n_v, du, dv, voxel_spacing
|
|
904
|
+
)
|
|
905
|
+
# <grad_output, A^T y> = <A grad_output, y>, so the projector's VJP applies.
|
|
906
|
+
geometry_grads = _geometry_grads(
|
|
907
|
+
ctx.needs_input_grad[1:5], "cone", grad_output, sinogram,
|
|
908
|
+
(src_pos, det_center, det_u_vec, det_v_vec), (du, dv), voxel_spacing,
|
|
909
|
+
)
|
|
910
|
+
return (grad_sino, *geometry_grads, None, None, None, None, None, None)
|