|
| 1 | +#= |
| 2 | +# [DCF-based "preconditioning" in MRI](@id 6-nufft) |
| 3 | +
|
| 4 | +This example illustrates |
| 5 | +how "preconditioning" based on sampling |
| 6 | +density compensation factors (DCFs) |
| 7 | +affects image reconstruction in MRI |
| 8 | +using the Julia language. |
| 9 | +=# |
| 10 | + |
| 11 | +#srcURL |
| 12 | + |
| 13 | +#= |
| 14 | +The bottom line here |
| 15 | +is that DCF-based preconditioning |
| 16 | +gives an apparent speed-up |
| 17 | +when staring from a zero initial image, |
| 18 | +but leads to increased noise (worse error). |
| 19 | +Choosing a smart starting image, |
| 20 | +like an appropriately scaled gridding image, |
| 21 | +provides comparable speed-up |
| 22 | +without compromising noise. |
| 23 | +=# |
| 24 | + |
| 25 | + |
| 26 | +#= |
| 27 | +First we add the Julia packages that are need for these examples. |
| 28 | +Change `false` to `true` in the following code block |
| 29 | +if you are using any of the following packages for the first time. |
| 30 | +=# |
| 31 | + |
| 32 | +if false |
| 33 | + import Pkg |
| 34 | + Pkg.add([ |
| 35 | + "ImagePhantoms" |
| 36 | + "Unitful" |
| 37 | + "Plots" |
| 38 | + "LaTeXStrings" |
| 39 | + "MIRTjim" |
| 40 | + "MIRT" |
| 41 | + "InteractiveUtils" |
| 42 | + ]) |
| 43 | +end |
| 44 | + |
| 45 | + |
| 46 | +# Now tell this Julia session to use the following packages. |
| 47 | +# Run `Pkg.add()` in the preceding code block first, if needed. |
| 48 | + |
| 49 | +using ImagePhantoms: shepp_logan, SheppLoganEmis, spectrum, phantom #, Gauss2 |
| 50 | +using LaTeXStrings |
| 51 | +using LinearAlgebra: I, norm |
| 52 | +using MIRTjim: jim, prompt # jiffy image display |
| 53 | +using MIRT: Anufft, diffl_map, ncg |
| 54 | +using Plots; default(label="", markerstrokecolor=:auto) |
| 55 | +using Random: seed!; seed!(0) |
| 56 | +using Unitful: mm # physical units (mm here) |
| 57 | +using InteractiveUtils: versioninfo |
| 58 | + |
| 59 | + |
| 60 | +# The following line is helpful when running this file as a script; |
| 61 | +# this way it will prompt user to hit a key after each image is displayed. |
| 62 | + |
| 63 | +isinteractive() && jim(:prompt, true); |
| 64 | + |
| 65 | + |
| 66 | +#= |
| 67 | +## Radial k-space sampling |
| 68 | +
|
| 69 | +We consider radial sampling |
| 70 | +as a simple representative non-Cartesian case. |
| 71 | +Consider imaging a 256mm × 256mm field of FOV |
| 72 | +with the goal of reconstructing a 128 × 128 pixel image. |
| 73 | +The following radial and angular k-space sampling is reasonable. |
| 74 | +=# |
| 75 | + |
| 76 | +N = 128 |
| 77 | +FOV = 256mm # physical units! |
| 78 | +Δx = FOV / N # pixel size |
| 79 | +kmax = 1 / 2Δx |
| 80 | +kr = ((-N÷2):(N÷2)) / (N÷2) * kmax # radial sampling in k-space |
| 81 | +Nr = length(kr) # N+1 |
| 82 | +Nϕ = N-2 # theoretically should be about π/2 * N |
| 83 | +kϕ = (0:Nϕ-1)/Nϕ * π # angular samples |
| 84 | +νx = kr * cos.(kϕ)' # N × Nϕ k-space sampling in cycles/mm |
| 85 | +νy = kr * sin.(kϕ)' |
| 86 | +plot(νx, νy, |
| 87 | + xaxis = (L"\nu_x", (-1,1) .* (1.1 * kmax), (-1:1)*kmax), |
| 88 | + yaxis = (L"\nu_y", (-1,1) .* (1.1 * kmax), (-1:1)*kmax), |
| 89 | + aspect_ratio = 1, |
| 90 | + title = "Radial k-space sampling", |
| 91 | +) |
| 92 | + |
| 93 | +# |
| 94 | +isinteractive() && prompt(); |
| 95 | + |
| 96 | +#= |
| 97 | +For the NUFFT routines considered here, |
| 98 | +the sampling arguments must be "Ω" values: |
| 99 | +digital frequencies have pseudo-units of radians / pixel. |
| 100 | +=# |
| 101 | + |
| 102 | +Ωx = (2π * Δx) * νx # N × Nϕ grid of k-space sample locations |
| 103 | +Ωy = (2π * Δx) * νy # in pseudo-units of radians / sample |
| 104 | + |
| 105 | +scatter(Ωx, Ωy, |
| 106 | + xaxis = (L"\Omega_x", (-1,1) .* 1.1π, ((-1:1)*π, ["-π", "0", "π"])), |
| 107 | + yaxis = (L"\Omega_y", (-1,1) .* 1.1π, ((-1:1)*π, ["-π", "0", "π"])), |
| 108 | + aspect_ratio = 1, markersize = 0.5, |
| 109 | + title = "Radial k-space sampling", |
| 110 | +) |
| 111 | + |
| 112 | +# |
| 113 | +isinteractive() && prompt(); |
| 114 | + |
| 115 | + |
| 116 | +#= |
| 117 | +## Radial k-space data for Shepp-Logan phantom |
| 118 | +
|
| 119 | +Get the ellipse parameters for a MRI-suitable version of the Shepp-Logan phantom |
| 120 | +and calculate (analytically) the radial k-space data. |
| 121 | +Then display in polar coordinates. |
| 122 | +=# |
| 123 | + |
| 124 | +object = shepp_logan(SheppLoganEmis(); fovs=(FOV,FOV)) |
| 125 | +#src object = [Gauss2(18mm, 0mm, 100mm, 70mm, 0, 1)] # useful for validating DCF |
| 126 | +data = spectrum(object).(νx,νy) |
| 127 | +data = data / oneunit(eltype(data)) # abandon units at this point |
| 128 | +dscale = 10000 |
| 129 | +jimk = (args...; kwargs...) -> jim(kr, kϕ, args...; |
| 130 | + xaxis = (L"k_r", (-1,1) .* kmax, (-1:1) .* maximum(abs, kr)), |
| 131 | + yaxis = (L"k_{\phi}", (0,π), (0,π)), |
| 132 | + aspect_ratio = :none, |
| 133 | + kwargs... |
| 134 | +) |
| 135 | +pk = jimk(abs.(data) / dscale; title="k-space data magnitude / $dscale") |
| 136 | + |
| 137 | + |
| 138 | +#= |
| 139 | +Here is what the phantom should look like ideally. |
| 140 | +=# |
| 141 | + |
| 142 | +x = (-(N÷2):(N÷2-1)) * Δx |
| 143 | +y = (-(N÷2):(N÷2-1)) * Δx |
| 144 | +ideal = phantom(x, y, object, 2) |
| 145 | +clim = (0, 9) |
| 146 | +p0 = jim(x, y, ideal; title="True Shepp-Logan phantom", clim) |
| 147 | + |
| 148 | +# |
| 149 | +isinteractive() && prompt(); |
| 150 | + |
| 151 | +#= |
| 152 | +## NUFFT operator |
| 153 | +with sanity check |
| 154 | +=# |
| 155 | + |
| 156 | +A = Anufft([vec(Ωx) vec(Ωy)], (N,N); n_shift = [N/2,N/2]) # todo: odim=(Nr,Nϕ) |
| 157 | +dx = FOV / N # pixel size |
| 158 | +dx = dx / oneunit(dx) # abandon units for now |
| 159 | +Ax_to_y = Ax -> dx^2 * reshape(Ax, Nr, Nϕ) # trick |
| 160 | +pj1 = jimk(abs.(Ax_to_y(A * ideal)) / dscale, "|A*x|/$dscale") |
| 161 | +pj2 = jimk(abs.(Ax_to_y(A * ideal) - data) / dscale, "|A*x-y|/$dscale") |
| 162 | +plot(pk, pj1, pj2) |
| 163 | + |
| 164 | +# |
| 165 | +isinteractive() && prompt(); |
| 166 | + |
| 167 | + |
| 168 | +#= |
| 169 | +## Density compensation |
| 170 | +
|
| 171 | +Radial sampling needs |
| 172 | +(N+1) DSF weights along k-space polar coordinate. |
| 173 | +
|
| 174 | +We use the improved method of |
| 175 | +[Lauzon&Rutt, 1996](https://doi.org/10.1002/mrm.1910360617) |
| 176 | +and |
| 177 | +[Joseph, 1998](https://doi.org/10.1002/mrm.1910400317). |
| 178 | +=# |
| 179 | + |
| 180 | +dν = 1/FOV # k-space radial sample spacing |
| 181 | +dcf = π / Nϕ * dν * abs.(kr) # see lauzon:96:eop, joseph:98:sei |
| 182 | +dcf[kr .== 0/mm] .= π * (dν/2)^2 / Nϕ # area of center disk |
| 183 | +dcf = dcf / oneunit(eltype(dcf)) # kludge: units not working with LinearMap now |
| 184 | +gridded4 = A' * vec(dcf .* data) |
| 185 | +p4 = jim(x, y, gridded4, title="NUFFT gridding with better ramp-filter DCF"; clim) |
| 186 | +
|
| 187 | +# |
| 188 | +isinteractive() && prompt(); |
| 189 | +
|
| 190 | +
|
| 191 | +# A profile shows it is "decent" but not amazing. |
| 192 | +
|
| 193 | +pp = plot(x, real(gridded4[:,N÷2]), label="Modified ramp DCF") |
| 194 | +plot!(x, real(ideal[:,N÷2]), label="Ideal", xlabel=L"x", ylabel="middle profile") |
| 195 | +
|
| 196 | +# |
| 197 | +isinteractive() && prompt(); |
| 198 | +
|
| 199 | +
|
| 200 | +#= |
| 201 | +## Unregularized iterative MRI reconstruction |
| 202 | +
|
| 203 | +Apply a few iterations of conjugate gradient (CG) |
| 204 | +to approach a minimizer of the least-squares cost function: |
| 205 | +```math |
| 206 | +\arg \min_{x} \frac{1}{2} ‖ A x - y ‖₂². |
| 207 | +``` |
| 208 | +
|
| 209 | +Using a zero-image as the starting point ``x₀`` |
| 210 | +leads to slow convergence. |
| 211 | +
|
| 212 | +Using "preconditioning" by including DCF values |
| 213 | +in the cost function |
| 214 | +(a kind of weighted LS) |
| 215 | +appears to give much faster convergence, |
| 216 | +but leads to suboptimal image quality |
| 217 | +especially in the presence of noise. |
| 218 | +
|
| 219 | +Using a decent initial image |
| 220 | +(e.g., a gridded image with appropriate scaling) |
| 221 | +is preferable. |
| 222 | +
|
| 223 | +There is a subtle point here about `dx` |
| 224 | +when converting the Fourier integral to a sum. |
| 225 | +Here `yideal` is `data/dx^2`. |
| 226 | +=# |
| 227 | +
|
| 228 | +nrmse = x -> norm(x - ideal) / norm(ideal) * 100 |
| 229 | +fun = (x, iter) -> nrmse(x) |
| 230 | +
|
| 231 | +snr2sigma(db, y) = 10^(-db/20) * norm(y) / sqrt(length(y)) |
| 232 | +yideal = vec(data/dx^2) |
| 233 | +σnoise = snr2sigma(40, yideal) |
| 234 | +ydata = yideal + σnoise * randn(eltype(yideal), size(yideal)) |
| 235 | +snr = 20 * log10(norm(yideal) / norm(ydata - yideal)) # check |
| 236 | +gradf = u -> u - ydata # gradient of f(u) = 1/2 ‖ u - y ‖² |
| 237 | +curvf = u -> 1 # curvature of f(u) |
| 238 | +
|
| 239 | +x0 = 0 * ideal |
| 240 | +niter = 15 |
| 241 | +xls0, out0 = ncg([A], [gradf], [curvf], x0; niter, fun) |
| 242 | +
|
| 243 | +default(linewidth = 2) |
| 244 | +ppr = plot( |
| 245 | + xaxis = ("iteration", (0, niter)), |
| 246 | + yaxis = ("NRMSE", (0, 100), 0:20:100), |
| 247 | + widen = true, |
| 248 | +) |
| 249 | +plot!(ppr, 0:niter, out0, |
| 250 | + label = "Non-preconditioned, 0 init", color = :red, marker = :circle, |
| 251 | +); |
| 252 | +
|
| 253 | +# "Preconditioned" version |
| 254 | +precon = vec(repeat(dcf, Nϕ)); |
| 255 | +
|
| 256 | +# gradient of fp(u) = 1/2 ‖ P^{1/2} (u - y) ‖² |
| 257 | +gradfp = u -> precon .* (u - ydata) |
| 258 | +curvfp = u -> precon # curvature of fp(u) |
| 259 | +
|
| 260 | +xlsp, out2 = ncg([A], [gradfp], [curvfp], x0; niter, fun) |
| 261 | +plot!(ppr, 0:niter, out2, |
| 262 | + label = "DCF preconditioned, 0 init", color = :blue, marker = :x, |
| 263 | +) |
| 264 | +#src savefig(ppr, "precon0.pdf") |
| 265 | +
|
| 266 | +# Smart start with gridded image |
| 267 | +x0 = gridded4 # initial guess: decent gridding reconstruction |
| 268 | +xls1, out1 = ncg([A], [gradf], [curvf], x0; niter, fun) |
| 269 | +plot!(ppr, 0:niter, out1, |
| 270 | + label = "Non-preconditioned, gridding init", color=:green, marker=:+, |
| 271 | +) |
| 272 | +
|
| 273 | +#src savefig(ppr, "precon1.pdf") |
| 274 | +
|
| 275 | +# |
| 276 | +isinteractive() && prompt(); |
| 277 | +
|
| 278 | +# The images look very similar at 15 iterations |
| 279 | +
|
| 280 | +elim = (0, 1) |
| 281 | +tmp = stack((ideal, xls0, xlsp, xls1)) |
| 282 | +err = stack((ideal, xls0, xlsp, xls1)) .- ideal |
| 283 | +p5 = plot( |
| 284 | + jim(x, y, tmp; title = "Ideal | 0 init | precon | grid init", |
| 285 | + clim, nrow=1, size = (600, 200)), |
| 286 | + jim(x, y, err; title = "Error", color=:cividis, |
| 287 | + clim = elim, nrow=1, size = (600, 200)), |
| 288 | + layout = (2, 1), |
| 289 | + size = (650, 450), |
| 290 | +) |
| 291 | +
|
| 292 | +# |
| 293 | +isinteractive() && prompt(); |
| 294 | +
|
| 295 | +
|
| 296 | +include("../../inc/reproduce.jl") |
0 commit comments