Skip to content

Commit 4e5eeb6

Browse files
authored
iradon: threading, interpolations, and tests (#3)
add threading to iradon. add tests.
1 parent 5426d6b commit 4e5eeb6

File tree

3 files changed

+71
-54
lines changed

3 files changed

+71
-54
lines changed

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ version = "0.1.0"
44

55
[deps]
66
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
7+
Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
78

89
[compat]
910
julia = "1"
1011

1112
[extras]
13+
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
1214
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1315

1416
[targets]
15-
test = ["Test"]
17+
test = ["Images", "Test"]

src/ImageReconstruction.jl

Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,104 @@
11
module ImageReconstruction
22

3+
using Base.Threads
4+
using Interpolations
35
using FFTW
46

57
export radon, iradon
68

79
"""
8-
radon transform
10+
radon(I::AbstractMatrix, θ::AbstractRange, t::AbstractRange)
911
10-
https://en.wikipedia.org/wiki/Radon_transform
12+
Radon transform of a image `I` producing a sinogram from view angles `θ` in
13+
radians and detector sampling `t`.
1114
"""
12-
function radon end
13-
14-
"""
15-
inverse radon transform
16-
17-
https://en.wikipedia.org/wiki/Radon_transform
18-
"""
19-
function iradon end
20-
21-
22-
"""
23-
Radon transform of an image using a pixel-driven algorithm.
24-
"""
25-
function radon(image::AbstractMatrix, θ::AbstractRange, t::AbstractRange)
26-
P = zeros(eltype(image), length(t), length(θ))
27-
nr, nc = size(image)
28-
29-
for i in 1:nr, j in 1:nc
30-
x = j - nc / 2 + 0.5
31-
y = i - nr / 2 + 0.5
15+
function radon(I::AbstractMatrix, θ::AbstractRange, t::AbstractRange)
16+
P = zeros(eltype(I), length(t), length(θ))
17+
ax1, ax2 = axes(I)
18+
19+
nax1, nax2 = length(ax1), length(ax2)
20+
for j in ax2, i in ax1
21+
x = j - nax2 / 2 + 0.5
22+
y = i - nax1 / 2 + 0.5
3223
@inbounds for (k, θₖ) in enumerate(θ)
3324
t′ = x * cos(θₖ) + y * sin(θₖ)
3425

3526
a = convert(Int, round((t′ - minimum(t)) / step(t) + 1))
3627

3728
(a < 1 || a > length(t)) && continue
3829
α = abs(t′ - t[a])
39-
P[a, k] += (1 - α) * image[i, j]
30+
31+
I′ = I[i, j]
32+
P[a, k] += (1 - α) * I′
4033

4134
(a > length(t) + 1) && continue
42-
P[a + 1, k] += α * image[i, j]
35+
P[a+1, k] += α * I′
4336
end
4437
end
4538

4639
P
4740
end
4841

49-
function _ramp_spatial(N::Int, τ)
50-
h = zeros(N)
42+
function _ramp_spatial(N::Int, τ, Npad::Int = N)
43+
@assert Npad N
5144
N2 = N ÷ 2
52-
for i in eachindex(h)
53-
n = i - N2 - 1
54-
if mod(n, 2) != 0
55-
h[i] = -1 /* n * τ)^2
56-
elseif n == 0
57-
h[i] = 1 / (4 * τ^2)
58-
end
59-
end
60-
h
45+
hval(n) = n == 0 ? 1 / (4*τ^2) : - mod(n, 2)/* n * τ)^2
46+
[i N ? hval(i - N2 - 1) : 0. for i = 1:Npad]
6147
end
6248

63-
_zero_pad(p::AbstractVector, N::Int) = vcat(p, zeros(N))
64-
6549
"""
66-
Inverse radon transform using a ramp filter and a pixel-driven algorithm.
50+
iradon(P::AbstractMatrix, θ::AbstractRange, t::AbstractRange)
51+
52+
Inverse radon transform of a sinogram `P` with view angles `θ` in radians and
53+
detector sampling `t` producing an image on a 128x128 matrix.
6754
"""
68-
function iradon(sinogram::AbstractMatrix, θ::AbstractRange, t::AbstractRange)
55+
function iradon(P::AbstractMatrix, θ::AbstractRange, t::AbstractRange)
6956
pixels = 128
7057

7158
N = length(t)
7259
K = length(θ)
7360
Npad = nextpow(2, 2 * N - 1)
7461
τ = step(t)
75-
ramp = fft(_zero_pad(_ramp_spatial(N, τ), Npad - N))
76-
i = div(N, 2) + 1
62+
ramp = fft(_ramp_spatial(N, τ, Npad))
63+
i = N ÷ 2 + 1
7764
j = i + N - 1
7865

79-
image = zeros(eltype(sinogram), pixels, pixels)
80-
Q = Vector{eltype(sinogram)}(undef, N)
81-
for (k, θₖ) in enumerate(θ)
66+
T = eltype(P)
67+
I = [zeros(T, pixels, pixels) for _ = 1:nthreads()]
68+
P′ = [Vector{Complex{T}}(undef, Npad) for _ = 1:nthreads()]
69+
Q = [Vector{T}(undef, N) for _ = 1:nthreads()]
70+
71+
l = SpinLock()
72+
@inbounds @threads for (k, θₖ) in collect(enumerate(θ))
73+
id = threadid()
74+
Pid, Qid, Iid = P′[id], Q[id], I[id]
75+
8276
# filter projection
83-
Q[:] .= τ .* real.(ifft(fft(_zero_pad(sinogram[:, k], Npad - N)) .* ramp)[i:j])
77+
Pid[1:N] .= P[:, k]
78+
Pid[N+1:end] .= 0
79+
80+
# Need to prevent multiple thread execution during fft/ifft.
81+
# double free occurs otherwise.
82+
# https://github.com/JuliaMath/FFTW.jl/issues/134
83+
lock(l)
84+
fft!(Pid)
85+
Pid .*= ramp
86+
ifft!(Pid)
87+
unlock(l)
88+
Qid .= τ .* real.(@view Pid[i:j])
89+
90+
Qₖ = LinearInterpolation(t, Qid)
8491

8592
# backproject
86-
for c in CartesianIndices(image)
93+
for c in CartesianIndices(first(I))
8794
x = c.I[2] - pixels ÷ 2 + 0.5
8895
y = c.I[1] - pixels ÷ 2 + 0.5
96+
x^2 + y^2 pixels^2 / 4 && continue
8997
t′ = x * cos(θₖ) + y * sin(θₖ)
90-
# linear interpolation
91-
# image pixel (x, y) at θₖ is projected on t[z] and t[z+1]
92-
z = convert(Int, round((t′ - minimum(t)) / step(t) + 1))
93-
α = abs(t′ - t[z])
94-
image[c] += (1 - α) * Q[z] + α * Q[z + 1]
98+
Iid[c] += Qₖ(t′)
9599
end
96100
end
97-
@. image * π / K
101+
sum(I) .* π ./ K
98102
end
99103

100104
end # module

test/runtests.jl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using ImageReconstruction
22
using Test
33

4-
@testset "radon: pixel-driven" begin
4+
@testset "radon - centered impulse response" begin
55
pixels = 129
66
views = 200
77
I = zeros(pixels, pixels)
@@ -13,3 +13,14 @@ using Test
1313

1414
@test all(P[101, :] .== 1)
1515
end
16+
17+
using Images: shepp_logan, sad
18+
@testset "iradon - shepp logan" begin
19+
Igt = shepp_logan(128)
20+
views = 200
21+
θ = range(0, 2π, length=views)
22+
t = -150:150
23+
P = radon(Igt, θ, t)
24+
I = iradon(P, θ, t)
25+
@test sad(I, Igt) < 500
26+
end

0 commit comments

Comments
 (0)