Native Curved Detector#
examples/curved_detector.py projects a phantom onto a native cylindrical cone
detector, checks the matched backprojection and computes volume and surface
gradients on a circular scan.
Vendor images of TCIA case L213 (see Data sources) as a volume, projected
onto a cylindrical detector centred on the source: 888 × 625 pixels, radius
949 mm, source 541 mm from the isocentre. Made by the volumes command of
docs/assets/gallery/compute.py; the script below uses a smaller geometry.#
Run it#
Run the commands from the root of a source checkout; pip install diffct does not
install the examples/ directory. See the “From source” tab in
Installation.
python examples/curved_detector.py
This small CUDA example uses the native curved operators directly, without resampling or FDK. See Parameterized detector surfaces for the callback contract.
Source#
1"""Native cylindrical detector: projection, matched adjoint and gradients.
2
3 python examples/curved_detector.py
4
5Uses a circular cone-beam trajectory and one ray per detector pixel. This
6example uses the native curved operators directly; it does not run FDK.
7"""
8
9import torch
10
11from diffct import Projector, circular_trajectory_3d
12from _common import shepp_logan_3d
13
14
15def main():
16 if not torch.cuda.is_available():
17 raise RuntimeError("diffct needs a CUDA GPU")
18 torch.manual_seed(0)
19 trajectory = circular_trajectory_3d(90, sid=80.0, sdd=128.0, device="cpu")
20 # Radius equals SDD: this cylinder is initially centred on each view's source.
21 radius = torch.tensor(128.0, dtype=torch.float64, requires_grad=True)
22
23 def cylinder(u, v):
24 u, v = u.to(radius.device), v.to(radius.device)
25 angle = u / radius
26 # The generated detector normal points away from the source.
27 return torch.stack((radius * angle.sin(), v,
28 radius * (angle.cos() - 1)), dim=-1)
29
30 operator = Projector(trajectory, (32, 32, 32), (96, 64),
31 detector_spacing=(0.8, 1.0), detector_surface=cylinder)
32 volume = torch.from_numpy(shepp_logan_3d(32)).cuda().requires_grad_()
33 sinogram = operator.project(volume)
34 y = torch.rand_like(sinogram)
35 adjoint = operator.backproject(y)
36 lhs = torch.sum(sinogram.double() * y.double())
37 rhs = torch.sum(volume.double() * adjoint.double())
38 torch.testing.assert_close(lhs, rhs, rtol=3e-5, atol=3e-5)
39
40 # d(sum(A x))/dx = A^T 1, with the same native curved geometry.
41 (volume_gradient,) = torch.autograd.grad(sinogram.sum(), volume, retain_graph=True)
42 expected = operator.backproject(torch.ones_like(sinogram))
43 torch.testing.assert_close(volume_gradient, expected, rtol=3e-5, atol=3e-5)
44 (radius_gradient,) = torch.autograd.grad(sinogram.square().sum(), radius)
45 assert torch.isfinite(radius_gradient) and radius_gradient != 0
46
47 print(f"volume {tuple(volume.shape)} -> curved sinogram {tuple(sinogram.shape)}")
48 print(f"matched adjoint {tuple(adjoint.shape)}, "
49 f"relative mismatch {(abs(lhs - rhs) / abs(lhs)).item():.3e}")
50 print(f"volume gradient matches native A^T 1; "
51 f"radius gradient {radius_gradient.item():.6g}")
52
53
54if __name__ == "__main__":
55 main()