Quickstart

This page shows the shortest path from a fresh install to a forward and back projection with parallelproj. It uses the built-in DemoPETScannerGeometry so that the scanner setup is a single line; for a real scanner you would replace it with RegularPolygonPETScannerGeometry (or build a custom geometry), but the rest of the workflow is identical.

Make sure parallelproj is installed first (see Installation).

A minimal PET sinogram projection

The example below uses the regular-polygon route, which is the right choice for scanners with cylindrical symmetry and a single layer of LOR endpoints:

scanner geometryMichelogram (axial plane layout) → RegularPolygonPETLORDescriptorRegularPolygonPETProjector.

(General block / panel scanners use the equal-block route instead – ModularizedPETScannerGeometryEqualBlockPETLORDescriptorEqualBlockPETProjector; see the API reference.)

import array_api_compat.numpy as xp  # the array backend (swap for .torch / .cupy)

import matplotlib.pyplot as plt

from parallelproj.pet_scanners import DemoPETScannerGeometry
from parallelproj.pet_lors import Michelogram, RegularPolygonPETLORDescriptor
from parallelproj.projectors import RegularPolygonPETProjector

# the device used for every array we create
dev = "cpu"  # or "cuda"

# demo cylindrical PET scanner (here trimmed to 4 rings)
# describes world coordinates of LOR endpoints
scanner = DemoPETScannerGeometry(xp, dev, num_rings=4)

# line of response descriptor that describes how order pairs of LOR endpoints
# including options for radial trimming and axial compression
lor_desc = RegularPolygonPETLORDescriptor(
    scanner,
    Michelogram(scanner.num_rings, max_ring_difference=3, span=1),
    radial_trim=50,
)

# non-TOF projector
proj = RegularPolygonPETProjector(
    lor_desc, img_shape=(100, 100, 8), voxel_size=(4.0, 4.0, 4.0)
)

# a simple test image: a hot box in the centre
img = xp.zeros(proj.in_shape, dtype=xp.float32, device=dev)
img[50:90, 10:40, :] = 1.0

img_fwd = proj(img)  # forward projection:  image  -> sinogram
back = proj.adjoint(img_fwd)  # back projection (adjoint);  proj.H(sino) works too

print("image shape:    ", img.shape)
print("sinogram shape: ", img_fwd.shape)

fig = plt.figure(figsize=(8, 8), tight_layout=True)
ax = fig.add_subplot(111, projection="3d")
proj.show_geometry(ax)
plt.show()

That is the whole core API: a projector is a LinearOperator, so proj(img) forward projects, proj.adjoint(sino) (equivalently proj.H(sino)) back projects, and the two are exact adjoints of each other – everything else in the library (reconstruction algorithms, priors, resolution and attenuation models) is built on top of this.

A few things worth knowing

  • Imports come from submodules. The top-level parallelproj namespace is intentionally minimal, so classes are imported from their submodule (from parallelproj.projectors import RegularPolygonPETProjector). The import map in the API reference lists where each public name lives.

  • Arrays are float32. Projectors expect and return single-precision arrays; create your images with dtype=xp.float32.

  • Same code on CPU and GPU. parallelproj is python array API compatible. To run the snippet above on a CUDA GPU, change only the import (import array_api_compat.torch as xp or import array_api_compat.cupy as xp) and set dev = "cuda". Use to_numpy_array() to bring a result back to NumPy (e.g. for plotting).

  • Time-of-flight. For a TOF projector, create a TOFParameters object and assign it to proj.tof_parameters; the forward projection then gains a trailing TOF-bin axis. See the 02_pet_sinogram_projections gallery examples.

Next steps