SPECTrecon PSF
This page explains the PSF portion of the Julia package SPECTrecon.jl
.
This page comes from a single Julia file: 3-psf.jl
.
You can access the source code for such Julia documentation using the 'Edit on GitHub' link in the top right. You can view the corresponding notebook in nbviewer here: 3-psf.ipynb
, or open it in binder here: 3-psf.ipynb
.
Setup
Packages needed here.
using SPECTrecon: psf_gauss, plan_psf, fft_conv!, fft_conv_adj!
using MIRTjim: jim, prompt
using Plots: scatter, scatter!, plot!, default
default(markerstrokecolor=:auto, markersize=3)
The following line is helpful when running this example.jl file as a script; this way it will prompt user to hit a key after each figure is displayed.
isinteractive() ? jim(:prompt, true) : prompt(:draw);
Overview
After rotating the image and the attenuation map, second step in SPECT image forward projection is to apply depth-dependent point spread function (PSF). Each (rotated) image plane is a certain distance from the SPECT detector and must be convolved with the 2D PSF appropriate for that plane.
Because SPECT has relatively poor spatial resolution, the PSF is usually fairly wide, so convolution using FFT operations is typically more efficient than direct spatial convolution.
Following other libraries like FFTW.jl, the PSF operations herein start with a plan
where work arrays are preallocated for subsequent use. The plan
is a Vector
of PlanPSF
objects: one for each thread. (Parallelism is across planes for a 3D image volume.) The number of threads defaults to Threads.nthreads()
.
Example
Start with a 3D image volume.
T = Float32 # work with single precision to save memory
nx = 32
nz = 30
image = zeros(T, nx, nx, nz) # ny = nx required
image[1nx÷4, 1nx÷4, 3nz÷4] = 1
image[2nx÷4, 2nx÷4, 2nz÷4] = 1
image[3nx÷4, 3nx÷4, 1nz÷4] = 1
jim(image, "Original image")
Create a synthetic gaussian depth-dependent PSF for a single view
px = 11
nview = 1 # for simplicity in this illustration
psf = repeat(psf_gauss( ; ny=nx, px), 1, 1, 1, nview)
jim(psf, "PSF for each of $nx planes")
Now plan the PSF modeling by specifying
- the image size (must be square)
- the PSF size: must be
px × pz × ny × nview
- the
Type
used for the work arrays.
plan = plan_psf( ; nx, nz, px, T)
1-element Vector{PlanPSF{Float32, FFTW.cFFTWPlan{ComplexF32, -1, true, 2, Tuple{Int64, Int64}}, AbstractFFTs.ScaledPlan{ComplexF32, FFTW.cFFTWPlan{ComplexF32, 1, true, 2, UnitRange{Int64}}, Float32}}} with N=32
Here are the internals for the plan for the first thread:
plan[1]
PlanPSF{Float32, FFTW.cFFTWPlan{ComplexF32, -1, true, 2, Tuple{Int64, Int64}}, AbstractFFTs.ScaledPlan{ComplexF32, FFTW.cFFTWPlan{ComplexF32, 1, true, 2, UnitRange{Int64}}, Float32}}
nx::Int64 32
nz::Int64 30
px::Int64 11
pz::Int64 11
padsize::NTuple{4, Int64} (16, 16, 17, 17)
workmat: 64×64 Matrix{Float32}
workvecx: 64-element Vector{Float32}
workvecz: 64-element Vector{Float32}
img_compl: 64×64 Matrix{ComplexF32}
ker_compl: 64×64 Matrix{ComplexF32}
fft_plan: FFTW.cFFTWPlan{ComplexF32, -1, true, 2, Tuple{Int64, Int64}}
ifft_plan: 0.00024414062 * FFTW.cFFTWPlan{ComplexF32, 1, true, 2, UnitRange{Int64}}
(82624 bytes)
With this plan
pre-allocated, now we can apply the depth-dependent PSF to the image volume (assumed already rotated here).
result = similar(image) # allocate memory for the result
fft_conv!(result, image, psf[:,:,:,1], plan) # mutates the first argument
jim(result, "After applying PSF")
Adjoint
To ensure adjoint consistency between SPECT forward- and back-projection, there is also an adjoint routine:
adj = similar(result)
fft_conv_adj!(adj, result, psf[:,:,:,1], plan)
jim(adj, "Adjoint of PSF modeling")
The adjoint is not the same as the inverse so one does not expect the output here to match the original image!
LinearMap
One can form a linear map corresponding to PSF modeling using LinearMapAA
. Perhaps the main purpose is simply for verifying adjoint correctness.
using LinearMapsAA: LinearMapAA
nx, nz, px = 10, 7, 5 # small size for illustration
psf3 = psf_gauss( ; ny=nx, px)
plan = plan_psf( ; nx, nz, px, T)
idim = (nx,nx,nz)
odim = (nx,nx,nz)
forw! = (y,x) -> fft_conv!(y, x, psf3, plan)
back! = (x,y) -> fft_conv_adj!(x, y, psf3, plan)
A = LinearMapAA(forw!, back!, (prod(odim),prod(idim)); T, odim, idim)
Afull = Matrix(A)
Aadj = Matrix(A')
jim(cat(dims=3, Afull, Aadj'), "Linear map for PSF modeling and its adjoint")
The following check verifies adjoint consistency:
@assert Afull ≈ Aadj'
This page was generated using Literate.jl.