Source code for toupy.tomo.radon

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Forward Radon transform (projection).

GPU path  : CuPy + the CUDA kernel defined in iradon.py
CPU path  : skimage.transform.radon (unchanged fallback).

Public API is unchanged:  projector()
"""

# third party packages
import numpy as np
from skimage.transform import radon

# local libraries
from ..utils.plot_utils import isnotebook
from .iradon import CUDA_AVAILABLE

if CUDA_AVAILABLE:
    import cupy as cp
    from .iradon import _radon_kernel

__all__ = ["radon_cuda", "projector"]


[docs] def radon_cuda(recons, theta): """ Forward Radon transform — CUDA/GPU path. Falls back to ``skimage.transform.radon`` automatically if CuPy is unavailable. Parameters ---------- recons : ndarray shape (N, N) The tomographic slice to project. Converted to float32 internally. theta : ndarray Projection angles in degrees. Returns ------- sinogram : ndarray shape (N, nangles) The computed sinogram, in the same layout as skimage's radon output (each *column* = one projection). """ if not CUDA_AVAILABLE: return np.squeeze(radon(recons, theta, circle=True)) N = recons.shape[0] nangles = len(theta) nbins = N # detector bins == image side length nsteps = int(np.ceil(N * np.sqrt(2))) # enough steps to cross the diagonal th = np.deg2rad(np.asarray(theta, dtype=np.float32)) cos_th = np.cos(th).astype(np.float32) sin_th = np.sin(th).astype(np.float32) # Upload image and trig tables img_gpu = cp.asarray(recons.astype(np.float32)) cos_th_gpu = cp.asarray(cos_th) sin_th_gpu = cp.asarray(sin_th) sino_gpu = cp.zeros(nbins * nangles, dtype=cp.float32) # Launch kernel — each thread = one (bin, angle) sinogram entry block = (16, 16, 1) grid = ( int(np.ceil(nbins / block[0])), int(np.ceil(nangles / block[1])), 1, ) _radon_kernel( grid, block, (img_gpu.ravel(), sino_gpu, cos_th_gpu, sin_th_gpu, np.int32(N), np.int32(nbins), np.int32(nangles), np.int32(nsteps)), ) cp.cuda.stream.get_current_stream().synchronize() # Bring back to CPU; reshape to (nbins, nangles) — same layout as skimage sinogram = cp.asnumpy(sino_gpu).reshape(nangles, nbins).T return np.squeeze(sinogram)
[docs] def projector(recons, theta, **params): """ Wrapper to choose between CUDA and CPU forward Radon transform. Parameters ---------- recons : ndarray shape (N, N) Tomographic slice to project. theta : ndarray Projection angles in degrees. params : dict ``params["cuda"]`` — True → use GPU path. ``params["opencl"]`` — accepted as alias for backwards compatibility. Returns ------- sinogram : ndarray shape (N, nangles) """ use_cuda = params.get("cuda", params.get("opencl", False)) if use_cuda: return radon_cuda(recons, theta) else: return np.squeeze(radon(recons, theta, circle=True))