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.

Projections of a patient volume on a cylindrical detector

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#

Native cylindrical detector operators#
 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()