A CUDA-based library for computed tomography (CT) projection and reconstruction with differentiable operators
Project description
diffct: Differentiable Computed Tomography Operators
A high-performance, CUDA-accelerated library for circular orbits CT reconstruction with end-to-end differentiable operators, amplitude- calibrated analytical FBP / FDK, and a separable-footprint projector family for cell-integrated forward models. Built for optimization and deep-learning integration.
โญ Please star this project if you find it is useful!
๐ Branches
Main Branch (Stable, PyPI)
This is the stable release branch supporting circular-orbit CT
reconstruction. Every versioned release on
PyPI comes from main. See
CHANGELOG.md for the full release history.
Dev Branch (arbitrary trajectories)
The dev branch is the arbitrary-trajectory evolution of the library.
Kernels take per-view (src_pos, det_center, det_u_vec[, det_v_vec])
arrays instead of closed-form sdd / sid / beta scalars, so you can
reconstruct along spiral, saddle, sinusoidal, or any user-supplied
trajectory. It is kept in sync with main's 1.2.11 analytical
reconstruction overhaul (ramp filter, weighted backproject, tests,
benchmark suite); the only thing currently deferred from main is the
1.3.0 separable-footprint (SF) backends, because generalising the
trapezoidal footprint math to arbitrary trajectories is a separate
research effort.
โ ๏ธ Note: The dev branch is under active development and is not published to PyPI. If you find any bugs please raise an issue.
โจ Features
- Fast: CUDA-accelerated forward and backward projectors with Numba CUDA kernels, plus dedicated voxel-driven FBP / FDK gather kernels with coalesced memory writes.
- Differentiable: End-to-end gradient propagation via
torch.autograd. Every projector / backprojector pair is a byte-accurate adjoint, verified bytests/test_adjoint_inner_product.pyandtorch.autograd.gradcheckintests/test_gradcheck.py. - Analytical reconstruction: Amplitude-calibrated FBP / FDK
pipelines via
ramp_filter_1d(Ram-Lak / Hann / Hamming / cosine / Shepp-Logan windows, configurable padding and physicalsample_spacing),fan_cosine_weights/cone_cosine_weights,parker_weights,angular_integration_weights, andparallel_weighted_backproject/fan_weighted_backproject/cone_weighted_backproject. A unit-density phantom reconstructs back to amplitude 1 without any manual scaling. - Separable-footprint projectors: Optional
backend="sf"(fan) andbackend="sf_tr"/"sf_tt"(cone) selectors on everyFanProjectorFunction/ConeProjectorFunctioncall expose voxel-driven SF projectors (Long-Fessler-Balter, IEEE TMI 2010). The matched SF forward model is a cell-integrated alternative to ray-sampled Siddon and is useful when an iterative or learned pipeline wants a mass-conserving forward operator. The analytical FBP / FDK gather (backend="sf"onfan_weighted_backproject/cone_weighted_backproject) uses LEAP's chord-weighted matched-adjoint form (projectors_SF.cu); on standard Shepp-Logan phantoms it reaches Siddon-VD parity in both amplitude and MSE (within ~1 %). See the Core Algorithm section below for when SF is worth the extra ~2-3x forward cost. - Tested: 66 pytest tests covering adjoint identity, gradcheck,
smoke, FBP / FDK accuracy per geometry, detector / center offsets,
and 29 ramp-filter window cases. Opt-in 27-case
pytest-benchmarkperf suite undertests/benchmarks/for before/after regression tracking.
๐ Supported Geometries
- Parallel Beam: 2D parallel-beam geometry
- Fan Beam: 2D fan-beam geometry
- Cone Beam: 3D cone-beam geometry
๐ฌ Core Algorithm
At the heart of every diffct projector / backprojector pair is
Siddon's algorithm (Siddon 1985)
โ a ray-driven integer DDA that walks each ray through the voxel
grid, stepping forward only at voxel boundaries and giving exact
parametric intersection lengths in O(N) per ray.
The classical Siddon samples the cell value at each step
(nearest-neighbor). diffct instead uses bilinear (2D) /
trilinear (3D) interpolation of the surrounding voxel corners at
every sample point. The same interpolation appears twice in the
analytical pipeline, both in the Siddon forward projector walking
the image, and in the voxel-driven FBP / FDK gather backprojector
(*_weighted_backproject) sampling the filtered sinogram at each
voxel's projected detector footprint. The matched autograd adjoint
uses exactly the same weights as a scatter-add, giving a byte-
accurate adjoint verified by <Ax, y> โ <x, A^T y> at float32
precision (see tests/test_adjoint_inner_product.py).
Why this choice. Interpolation weights are continuous in the
voxel values, so โsinogram / โvoxel is well-defined everywhere
and torch.autograd flows gradients back through the projector
without surrogate tricks or straight-through estimators. Nearest-
neighbor Siddon would give piecewise-constant outputs with zero
gradient on the interior of each cell โ fine for one-shot FBP /
FDK, useless for iterative or learned reconstruction. A single
integer-DDA kernel also keeps the forward and adjoint code
structurally identical across parallel, fan, and cone, which is
what makes the adjoint byte-accurate in the first place.
The trade-off: slight blur. Bi / trilinear interpolation behaves like a mild low-pass filter on top of the voxel grid, so the effective reconstruction MTF rolls off a bit earlier than a cell- integrated projector would give. To recover that resolution the sharpest knob is the ramp filter window:
- Ramp filter window:
ramp_filter_1d(window=...)picks the frequency-domain apodization applied on top of the ramp. In order of sharpness:"ram-lak">"hamming">"hann". Sharper windows recover more high frequency content at the cost of more visible ringing / noise. This is by far the biggest knob on the reconstruction MTF at typical CBCT geometries.
Separable-footprint (SF) backend โ honest scope. The optional
backend="sf" (fan) and "sf_tr" / "sf_tt" (cone) selectors on
fan_weighted_backproject and cone_weighted_backproject replace
the default bilinear voxel-driven gather with a gather that
integrates the filtered sinogram over each voxel's projected
trapezoidal footprint, in LEAP's chord-weighted matched-adjoint
form (projectors_SF.cu). What you actually get, verified on
Shepp-Logan and the walnut dataset:
- Amplitude: matches Siddon VD to within ~1 % at nominal,
sub-nominal (
voxel = 0.5 * detector_pitch * sid / sdd) and mildly supra-nominal voxel grids. Both backends are consistent with the same analytical FBP / FDK scale constant on unit- density phantoms. - MSE / SSIM: SF is typically a whisker better than VD (fractions of a percent) on standard phantoms; do not expect a noticeable MSE win from the backend alone.
- Visible MTF: at the ~1.5-3x CBCT magnifications used in fan / cone examples, SF and VD produce visually indistinguishable edge profiles. Plot a line through a sharp edge and the two curves trace each other. The "SF is sharper at sub-nominal" claim you see in the SF literature is real in the extreme-sub-nominal regime (voxel much smaller than one detector bin) but invisible at the geometries in the shipped examples.
So why ship the SF backend at all? Because the forward side is the actually interesting one:
- SF forward is mass-conserving per voxel โ a single voxel's contribution is spread across the correct multi-bin footprint instead of concentrated at one bin, which matters for iterative reconstruction, learned priors, and any loss that compares sinograms directly. Siddon-bilinear forward is a point sampler and only conserves mass statistically.
- SF's matched adjoint is byte-accurate (verified by
tests/test_adjoint_inner_product.py), so gradients flow correctly through the cell-integrated forward model. - The SF-matched autograd adjoint and the LEAP chord-weighted FBP
gather live in different kernels: the first is used by
FanProjectorFunction/ConeProjectorFunctionon the backward pass, the second is used byfan_weighted_backproject/cone_weighted_backprojectwhen you pick the SF backend. Both are exposed; pickingbackend="sf"on both sides gives you a consistent cell-integrated pipeline.
Bottom line. If you just want the cleanest FBP / FDK of a
Shepp-Logan or a walnut on typical CBCT geometry, stay on
backend="siddon" and tune the ramp window. Switch to
backend="sf" / "sf_tr" / "sf_tt" when you care about the
forward model being cell-integrated โ iterative reconstruction,
learned priors, sinogram-level losses, or comparing against a LEAP
baseline. Concrete usage of both paths is in examples/fbp_fan.py,
examples/fdk_cone.py and examples/realdata_walnut_fdk.py.
๐ฅ Real-data Example
Cone-beam FDK reconstruction of a real walnut from the Helsinki
industrial CBCT scanner (Meaney 2022, Zenodo
10.5281/zenodo.6986012,
CC-BY 4.0). The shipped sinogram at
examples/data/walnut_cone.npz
is a 241-view, 256x256-per-view, flat-field-normalised and logged
subset of the original 721 x 2368 x 2240 uint16 acquisition (binned
8x, cropped 256x256, float16, ~25 MB). A sample reconstruction
montage is shipped at
examples/data/walnut_reco.png.
Run the full analytical FDK pipeline with
python examples/realdata_walnut_fdk.py
The same analytical wrappers (cone_cosine_weights, ramp_filter_1d,
angular_integration_weights, cone_weighted_backproject) that
reconstruct Shepp-Logan phantoms in fdk_cone.py are used here with
no algorithmic changes โ only the geometry is swapped for the one
stored in the .npz. The example reconstructs at half the nominal
voxel size on a 512ยณ grid with backend="sf_tr" and a Hamming
ramp window; you can switch to backend="siddon" and get a
visually equivalent result (the backend choice is a forward-model
preference here, not a reconstruction-sharpness knob). See
examples/data/NOTICE for the full
attribution and the regeneration procedure.
๐งฉ Code Structure
diffct/
โโโ diffct/
โ โโโ __init__.py # public API re-exports
โ โโโ differentiable.py # CUDA kernels, autograd Functions,
โ # analytical helpers, SF backends
โโโ examples/ # circular-orbit example scripts
โ โโโ fbp_parallel.py
โ โโโ fbp_fan.py # with Parker short-scan switch
โ โโโ fdk_cone.py # with Parker short-scan switch
โ โโโ iterative_reco_parallel.py
โ โโโ iterative_reco_fan.py
โ โโโ iterative_reco_cone.py
โ โโโ realdata_fbp_parallel.py # synthetic real-data pipeline
โ โโโ realdata_fbp_fan.py # (Beer-Lambert + Poisson + -log)
โ โโโ realdata_fdk_cone.py
โ โโโ realdata_walnut_fdk.py # real Helsinki walnut CBCT data
โ โโโ data/
โ โโโ walnut_cone.npz # ~25 MB preprocessed walnut sinogram
โ โโโ walnut_reco.png # sample FDK reconstruction montage
โ โโโ preprocess_walnut.py # regenerator from Zenodo source
โ โโโ NOTICE # CC-BY 4.0 attribution
โโโ tests/
โ โโโ test_*.py # adjoint / gradcheck / accuracy /
โ โ # offsets / weights / ramp-filter
โ โโโ benchmarks/ # opt-in pytest-benchmark perf suite
โโโ docs/ # Sphinx documentation sources
โโโ pyproject.toml # project metadata
โโโ pytest.ini
โโโ CHANGELOG.md # Keep-a-Changelog release notes
โโโ README.md
โโโ LICENSE
๐ Quick Start
Prerequisites
Installation
CUDA 12 (Recommended):
# Create and activate conda environment
conda create -n diffct python=3.12
conda activate diffct
# Install CUDA (here 12.8.1 as example) PyTorch, and Numba
conda install nvidia/label/cuda-12.8.1::cuda-toolkit
# Install Pytorch, you can find the commend here: https://pytorch.org/get-started/locally/
# Install Numba with CUDA 12
pip install numba-cuda[cu12]
# Install diffct
pip install diffct
CUDA 13 Installation
# Create and activate conda environment
conda create -n diffct python=3.12
conda activate diffct
# Install CUDA (here 13.0.2 as example) PyTorch, and Numba
conda install nvidia/label/cuda-13.0.2::cuda-toolkit
# Install Pytorch, you can find the commend here: https://pytorch.org/get-started/locally/
# Install Numba with CUDA 13
pip install numba-cuda[cu13]
# Install diffct
pip install diffct
CUDA 11 Installation
# Create and activate conda environment
conda create -n diffct python=3.12
conda activate diffct
# Install CUDA (here 11.8.0 as example) PyTorch, and Numba
conda install nvidia/label/cuda-11.8.0::cuda-toolkit
# Install Pytorch, you can find the commend here: https://pytorch.org/get-started/locally/
# Install Numba with CUDA 11
pip install numba-cuda[cu11]
# Install diffct
pip install diffct
Running the tests
pytest tests/ -q # 66 tests, ~15 s
pytest tests/benchmarks/ --benchmark-only # opt-in perf suite, requires pytest-benchmark
๐ Citation
If you use this library in your research, please cite:
@software{diffct2025,
author = {Yipeng Sun},
title = {diffct: Differentiable Computed Tomography
Reconstruction with CUDA},
year = 2025,
publisher = {Zenodo},
doi = {10.5281/zenodo.14999333},
url = {https://doi.org/10.5281/zenodo.14999333}
}
๐ License
This project is licensed under the Apache 2.0 - see the LICENSE file for details.
๐ Acknowledgements
This project was highly inspired by:
- PYRO-NN
- geometry_gradients_CT
- LEAP (LLNL / Hyojin Kim et al.)
โ the LEAP chord-weighted matched-adjoint form in
projectors_SF.cuis the reference implementation that the 1.3.1 analytical SF backprojection kernels (_fan_2d_sf_fbp_backproject_kernel,_cone_3d_sf_tr_fdk_backproject_kernel,_cone_3d_sf_tt_fdk_backproject_kernel) in diffct are ported from. Apache 2.0-licensed, big thanks to the LEAP team.
Issues and contributions are welcome!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file diffct-1.3.1.tar.gz.
File metadata
- Download URL: diffct-1.3.1.tar.gz
- Upload date:
- Size: 26.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
288d22e3575d59772082e5ef6e458e54e1d6673c7394deee83404a6ba2977c4d
|
|
| MD5 |
825af294eda8a8be5ef1b5989f143259
|
|
| BLAKE2b-256 |
8a875f741648b712902df3bc9132a9088f9dbf86f18d83b929490a3ee275e71e
|
File details
Details for the file diffct-1.3.1-py2.py3-none-any.whl.
File metadata
- Download URL: diffct-1.3.1-py2.py3-none-any.whl
- Upload date:
- Size: 44.9 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4ba9087ce8c9611e62b5ac301d764d3fedcef9e8f9a6a77442e27d122e0751f
|
|
| MD5 |
043b26445cde3eecd56cb5e7418a73f0
|
|
| BLAKE2b-256 |
dd886562b02311bfbcd89dbb9eff26749af270c96ee51faf5915b13e6b317d5f
|