Skip to content

Commit 898c687

Browse files
implement the actual realign of the images, allow for complex-valued images (motion estimates happens on the absolute value), and improve tests.
1 parent d75cc6a commit 898c687

3 files changed

Lines changed: 71 additions & 42 deletions

File tree

src/MRIRealign.jl

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,45 +10,53 @@ using ImageFiltering
1010
using Statistics
1111
using OhMyThreads
1212

13-
export estimate_motion_parameters
13+
export realign!, create_rotation_matrix
1414

1515

1616
# --- Top-level functions ---
17-
function estimate_motion_parameters(img::AbstractArray{Tin,4};
17+
function realign!(img::AbstractArray{Tin,4};
1818
center=size(img)[1:3] 2,
1919
ref_mode=:consensus,
2020
mask=trues(size(img)[1:3]),
21-
fwhm=nothing::Union{Nothing,NTuple{3}}
22-
) where {Tin<:Real}
21+
fwhm=nothing::Union{Nothing,NTuple{3}},
22+
realign=true
23+
) where Tin
24+
25+
if Tin <: Complex
26+
_img = abs.(img)
27+
Treal = real(Tin)
28+
else
29+
_img = img
30+
Treal = Tin
31+
end
2332
T = Float64 # lower precision results in gradient inacurracies
2433

2534
if ref_mode == :consensus
26-
t_refs = axes(img, 4)
35+
t_refs = axes(_img, 4)
2736
elseif ref_mode == :mean
28-
img = cat(img, mean(img, dims=4); dims=4)
29-
t_refs = size(img, 4)
37+
_img = cat(_img, mean(_img, dims=4); dims=4)
38+
t_refs = size(_img, 4)
3039
elseif typeof(ref_mode) <: Integer
3140
t_refs = ref_mode
3241
else
3342
error("ref_mode must either be `:consensus`, `:mean`, or an integer")
3443
end
3544

36-
img_s = (fwhm === nothing || all(fwhm .== 0)) ? img : smooth_image(img, fwhm)
45+
img_s = (fwhm === nothing || all(fwhm .== 0)) ? _img : smooth_image(_img, fwhm)
3746

3847
# interpolate all time frames
39-
_interpolate(x) = extrapolate(interpolate(x, BSpline(Cubic())), Interpolations.Flat())
4048
@views Tint = typeof(_interpolate(img_s[:, :, :, 1]))
4149
img_itp = Vector{Tint}(undef, size(img_s, 4))
4250
@tasks for t eachindex(img_itp)
43-
vol = @view img_s[:, :, :, t]
51+
vol = img_s[:, :, :, t]
4452
vol ./= quantile(vec(vol), T(0.9))
4553
img_itp[t] = _interpolate(vol)
4654
end
4755

4856
# random shifts seem to help with the speed of convertion (cf. SPM)
4957
mask_inds = [Tuple(idx) .+ rand(NTuple{3,T}) .- T(0.5) for idx findall(mask)]
5058

51-
motion_params = Array{T}(undef, 6, length(img_itp), length(t_refs))
59+
_motion_params = Array{T}(undef, 6, length(img_itp), length(t_refs))
5260
for (i_ref, t_ref) enumerate(t_refs)
5361
reference = [img_itp[t_ref](idx[1], idx[2], idx[3]) for idx in mask_inds]
5462
grad_field = [gradient(img_itp[t_ref], idx[1], idx[2], idx[3]) for idx mask_inds]
@@ -61,34 +69,38 @@ function estimate_motion_parameters(img::AbstractArray{Tin,4};
6169

6270
fgh! = make_fgh_function(reference, img_itp[t], center, mask_inds, grad_field, hess_field, diff_vals)
6371
res = optimize(Optim.only_fgh!(fgh!), p0, NewtonTrustRegion())
64-
motion_params[:, t, i_ref] .= Optim.minimizer(res)
72+
_motion_params[:, t, i_ref] .= Optim.minimizer(res)
6573
end
6674
end
6775

68-
if ref_mode == :consensus
69-
return Tin.(weighted_mean_estimate!(motion_params; r=mean(size(img)[1:3])))
76+
motion_params = if ref_mode == :consensus
77+
weighted_mean_estimate!(_motion_params; r=mean(size(img)[1:3]))
7078
elseif ref_mode == :mean
71-
return Tin.(motion_params[:, 1:end-1, 1])
79+
_motion_params[:, 1:end-1, 1]
7280
else
73-
return Tin.(dropdims(motion_params, dims=3))
81+
dropdims(_motion_params, dims=3)
7482
end
75-
end
7683

84+
if realign
85+
realign!(img, motion_params; center)
86+
end
7787

78-
# TODO
79-
# function realign_volumes(img::Array{<:Real,4}; ref_mode=:first, subsample=4, mask=nothing, σ=3.0)
80-
# sx, sy, sz, nt = size(img)
81-
# center = voxelcenter(img[:,:,:,1])
82-
# aligned = Array{Float64,4}(undef, sx, sy, sz, nt)
88+
return Treal.(motion_params)
89+
end
8390

84-
# @tasks for t in 1:nt
85-
# println("Realigning volume $t / $nt ...")
86-
# A = create_affine_matrix(motion_params[t, :], center)
87-
# aligned[:,:,:,t] = warp_volume_itp(moving, A, (sx,sy,sz))
88-
# end
8991

90-
# return motion_params
91-
# end
92+
function realign!(img::AbstractArray{T,4}, motion_params; center=size(img)[1:3] 2) where T
93+
@tasks for t axes(img, 4)
94+
vol = @view img[:, :, :, t]
95+
img_itp = _interpolate(vol)
96+
A = create_affine_matrix(motion_params[:, t], center)
97+
@inbounds for idx CartesianIndices(vol)
98+
v = A * SVector{4,Float64}(idx[1], idx[2], idx[3], 1)
99+
vol[idx] = img_itp(v[1], v[2], v[3])
100+
end
101+
end
102+
return img
103+
end
92104

93105

94106
# --- Combined f, g, h! for Optim.only_fg! or Newton trust-region ---
@@ -166,6 +178,8 @@ function smooth_image(img::AbstractArray{T,4}, fwhm::NTuple{3}) where {T<:Real}
166178
return img_s
167179
end
168180

181+
_interpolate(x) = extrapolate(interpolate(x, BSpline(Cubic())), Interpolations.Flat())
182+
169183
function create_rotation_matrix(rx, ry, rz)
170184
Rx = @SMatrix [1 0 0; 0 cos(rx) -sin(rx); 0 sin(rx) cos(rx)]
171185
Ry = @SMatrix [cos(ry) 0 sin(ry); 0 1 0; -sin(ry) 0 cos(ry)]
@@ -233,9 +247,6 @@ function params_from_rigid_affine(A, center)
233247
return @SVector [rx, ry, rz, t[1], t[2], t[3]]
234248
end
235249

236-
237-
238-
239250
function weighted_mean_estimate!(estimates; r=1, max_iter=100, tol=1e-6)
240251
# bring all estimates in the same frame of reference (iframe = end÷2)
241252
for iref axes(estimates, 3)
@@ -253,7 +264,7 @@ function weighted_mean_estimate!(estimates; r=1, max_iter=100, tol=1e-6)
253264

254265
residuals = estimates .- consensus
255266
residuals[1:3, :, :] .*= r # weight rotations by the radius
256-
weights = 1 ./ (1 .+ sqrt.(sum(abs2, residuals; dims=1:2))) #! dims=1:2 calculates the weights for all 6 parameters jointly, while dims=2 calculates them for each parameter separately
267+
weights = 1 ./ (1 .+ sqrt.(sum(abs2, residuals; dims=1:2))) # dims=1:2 calculates the weights for all 6 parameters jointly, while dims=2 calculates them for each parameter separately
257268
consensus = sum((weights .* estimates); dims=3) ./ sum(weights; dims=3) # Compute new weighted mean
258269

259270
if norm(consensus .- consensus_old) < tol

test/Project.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
[deps]
2-
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
32
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
43
FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000"
54
ImagePhantoms = "71a99df6-f52c-4da1-bd2a-69d6f37f3252"

test/runtests.jl

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,28 +65,28 @@ fgh!(nothing, G, H, p0)
6565
# @test H ≈ H_fd rtol = 0.5
6666

6767

68-
## test translations
68+
## test translation estimates
6969
imgs = cat(image, circshift(image, (1, 0, 0)), circshift(image, (0, 1, 0)), circshift(image, (0, 0, 1)), dims=4)
7070

7171
p_ref = zeros(6, 4)
7272
p_ref[4, 2] = 1
7373
p_ref[5, 3] = 1
7474
p_ref[6, 4] = 1
7575

76-
@test estimate_motion_parameters(imgs; ref_mode=1) p_ref rtol = 1e-1
76+
@test realign!(imgs; ref_mode=1, realign=false) p_ref rtol = 1e-1
7777

7878
p_ref[4, :] .-= 1
79-
@test estimate_motion_parameters(imgs; ref_mode=2) p_ref rtol = 1e-1
79+
@test realign!(imgs; ref_mode=2, realign=false) p_ref rtol = 1e-1
8080

8181
p_ref[4, :] .+= 1
8282
p_ref[5, :] .-= 1
83-
@test estimate_motion_parameters(imgs; ref_mode=3) p_ref rtol = 1e-1
83+
@test realign!(imgs; ref_mode=3, realign=false) p_ref rtol = 1e-1
8484

8585
p_ref[5, :] .+= 1
8686
p_ref[6, :] .-= 1
87-
@test estimate_motion_parameters(imgs; ref_mode=4) p_ref rtol = 1e-1
87+
@test realign!(imgs; ref_mode=4, realign=false) p_ref rtol = 1e-1
8888

89-
## test free movement (rotaions and translations)
89+
## test rotation estimates (and translations)
9090
ps = [[0.1, 0, 0, 0, 0, 0],
9191
[0, 0.1, 0, 0, 0, 0],
9292
[0, 0, 0.1, 0, 0, 0],
@@ -103,5 +103,24 @@ for p ∈ ps
103103
v = A \ SVector{4,Float64}(i[1], i[2], i[3], 1)
104104
img_interpolated[i] = abs.(img_itp(v[1], v[2], v[3]))
105105
end
106-
@test estimate_motion_parameters(cat(image, img_interpolated; dims=4); ref_mode=1)[:,2] p atol = 1e-1
107-
end
106+
107+
img_series = cat(image, img_interpolated; dims=4)
108+
img_series_r = copy(img_series)
109+
110+
img_series_phase = exp.(1im .* axes(img_series, 1) .* π/size(img_series, 1))
111+
img_series_c = img_series .* img_series_phase
112+
113+
@test realign!(img_series_r; ref_mode=1, realign=true)[:,2] p atol = 1e-1
114+
@test realign!(img_series_c; ref_mode=1, realign=true)[:,2] p atol = 1e-1
115+
116+
# test if img_series are aligned after the first run
117+
@test realign!(img_series_r; ref_mode=1, realign=false)[:,2] 0 .* p atol = 1e-2
118+
@test realign!(img_series_c; ref_mode=1, realign=false)[:,2] 0 .* p atol = 1e-2
119+
120+
@test img_series_r[:,:,:,1] img_series[:,:,:,1] rtol = 1e-3
121+
@test img_series_r[:,:,:,2] img_series[:,:,:,1] rtol = 1e-1
122+
123+
img_series_c ./= img_series_phase
124+
@test img_series_c[:,:,:,1] img_series[:,:,:,1] rtol = 1e-3
125+
@test img_series_c[:,:,:,2] img_series[:,:,:,1] rtol = 1e-1
126+
end

0 commit comments

Comments
 (0)