Iterative Reconstruction
In this section we implement a more complex iterative reconstruction algorithm. We will use iterative solvers provided by RegularizedLeastSquares.jl. These solver feature a variety of arguments, in this example we will just focus on the parameters for the number of iterations and regularization. Each solver requires either a matrix or a matrix-free linear operator which implements multiplication with a vector as input. We will use LinearOperatorCollection.jl, which implements a wrapper around RadonKA.
For this example, we will further assume that the construction of this operator is costly and should be done only once. This means our algorithm will be stateful and has to store the operator.
Parameters and Processing
We will start by defining the parameters for the algorithm and the processing steps. Afterwards we can implement the algorithm itself. Since we will use the same preprocessing as for the direct reconstruction, we can reuse the parameters and processing steps and jump directly to the iterative parameters:
using RegularizedLeastSquares, LinearOperatorCollection
abstract type AbstractIterativeRadonReconstructionParameters <: AbstractRadonReconstructionParameters end
Base.@kwdef struct IterativeRadonReconstructionParameters{S <: AbstractLinearSolver, R <: AbstractRegularization, N} <: AbstractIterativeRadonReconstructionParameters
solver::Type{S}
iterations::Int64
reg::Vector{R}
shape::NTuple{N, Int64}
angles::Vector{Float64}
end
The parameters of this struct can be grouped into three catergories. The solver type just specifies which solver to use. The number of iterations and the regularization term could be abstracted into a nested AbstractRadonParameter
which describe the parameters for the solver. Lastly the shape and angles are required to construct the linear operator.
Since we want to construct the linear operator only once, we will write the process
method with the operator as a given argument:
function AbstractImageReconstruction.process(::Type{<:AbstractIterativeRadonAlgorithm}, params::IterativeRadonReconstructionParameters, op, data::AbstractArray{T, 4}) where {T}
solver = createLinearSolver(params.solver, op; iterations = params.iterations, reg = params.reg)
result = similar(data, params.shape..., size(data, 4))
for i = 1:size(data, 4)
result[:, :, :, i] = solve!(solver, vec(data[:, :, :, i]))
end
return result
end
Later we need to define to create the operator and pass it to this process
method.
Algorithm
Similar to the direct reconstruction algorithm, we want our iterative algorithm to accept both preprocessing and reconstruction parameters. We will encode this in a new type:
Base.@kwdef struct IterativeRadonParameters{P<:AbstractRadonPreprocessingParameters, R<:AbstractIterativeRadonReconstructionParameters} <: AbstractRadonParameters
pre::P
reco::R
end
Instead of defining essentially the same struct again, we could also define a more generic one and specify the supported reconstruction parameter as type constraints in the algorithm constructor.
Unlike the direct reconstruction algorithm, the iterative algorithm has to store the linear operator. We will store it as a field in the algorithm type:
mutable struct IterativeRadonAlgorithm{D <: IterativeRadonParameters} <: AbstractIterativeRadonAlgorithm
parameter::D
op::Union{Nothing, AbstractLinearOperator}
output::Channel{Any}
end
We will set the operator to nothing
in the constructor:
function IterativeRadonAlgorithm(parameter::D) where D
return IterativeRadonAlgorithm{D}(parameter, nothing, Channel{Any}(Inf))
end
Main.IterativeRadonAlgorithm
Next we implement the process
method for our reconstruction parameters and an algorithm instance. This allows us to access the operator and pass it to the processing step:
function AbstractImageReconstruction.process(algo::IterativeRadonAlgorithm, params::IterativeRadonParameters{P, R}, data::AbstractArray{T, 4}) where {T, P<:AbstractRadonPreprocessingParameters, R<:AbstractIterativeRadonReconstructionParameters}
data = process(algo, params.pre, data)
return process(algo, params.reco, algo.op, data)
end
Note that initially the operator is nothing
and the processing step would fail as it stands. To "fix" this we define a process
method for the algorithm instance which creates the operator and stores it in the algorithm:
function AbstractImageReconstruction.process(algo::IterativeRadonAlgorithm, params::AbstractIterativeRadonReconstructionParameters, ::Nothing, data::AbstractArray{T, 4}) where {T}
op = RadonOp(T; shape = params.shape, angles = params.angles)
algo.op = op
return process(AbstractIterativeRadonAlgorithm, params, op, data)
end
Our algorithm is not type stable. To fix this, we would need to know the element type of the sinograms during construction. Which is possible with a different parameterization of the algorithm. We will not do this here. Often times the performance impact of this is negligible as the critical sections are in the preprocessing or the iterative solver, especially since we still dispatch on the operator.
To finish up the implementation we need to implement the remaining runtime related functions:
Base.take!(algo::IterativeRadonAlgorithm) = Base.take!(algo.output)
function Base.put!(algo::IterativeRadonAlgorithm, data::AbstractArray{T, 4}) where {T}
lock(algo.output) do
put!(algo.output, process(algo, algo.parameter, data))
end
end
Base.lock(algo::IterativeRadonAlgorithm) = lock(algo.output)
Base.unlock(algo::IterativeRadonAlgorithm) = unlock(algo.output)
Base.isready(algo::IterativeRadonAlgorithm) = isready(algo.output)
Base.wait(algo::IterativeRadonAlgorithm) = wait(algo.output)
AbstractImageReconstruction.parameter(algo::IterativeRadonAlgorithm) = algo.parameter
This page was generated using Literate.jl.