Skip to content

Commit eb7fc5f

Browse files
authored
Merge pull request #23 from bionanoimaging/fourier_filtering
Fourier filtering
2 parents 6b3e63f + 5bad8f9 commit eb7fc5f

File tree

7 files changed

+412
-19
lines changed

7 files changed

+412
-19
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ChainRulesCore = "1, 1.0, 1.1"
1818
FFTW = "1.5"
1919
ImageTransformations = "0.9"
2020
IndexFunArrays = "0.2"
21-
NDTools = "0.5"
21+
NDTools = "0.5.1"
2222
NFFT = "0.11, 0.12, 0.13"
2323
PaddedViews = "0.5"
2424
ShiftedArrays = "2"

src/FourierTools.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ include("resampling.jl")
1515
include("custom_fourier_types.jl")
1616
include("fourier_resizing.jl")
1717
include("fourier_shifting.jl")
18+
include("fourier_filtering.jl")
1819
include("fourier_resample_1D_based.jl")
1920
include("fourier_rotate.jl")
2021
include("fourier_shear.jl")

src/fourier_filtering.jl

Lines changed: 333 additions & 0 deletions
Large diffs are not rendered by default.

src/fourier_shear.jl

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@ There is also `shear!` available.
1616
+ `fix_nyquist`: apply a fix to the highest frequency during the Fourier-space application of the exponential factor
1717
+ `adapt_size`: if true, pad the data prior to the shear. The result array will be larger
1818
+ `pad_value`: the value to pad with (only applies if `adapt_size=true`)
19+
+ `assign_wrap=assign_wrap`: replaces wrap-around areas by `pad_value` (only of `adapt_size` is `false`)
1920
2021
For complex arrays we use `fft`, for real array we use `rfft`.
2122
"""
22-
function shear(arr::AbstractArray, Δ, shear_dir_dim=1, shear_dim=2; fix_nyquist=false, adapt_size=false::Bool, pad_value=zero(eltype(arr)))
23+
function shear(arr::AbstractArray, Δ, shear_dir_dim=1, shear_dim=2; fix_nyquist=false, assign_wrap=false, adapt_size=false::Bool, pad_value=zero(eltype(arr)))
2324
if adapt_size
2425
ns = Tuple(d == shear_dir_dim ? size(arr,d)+ceil(Int,abs.(Δ)) : size(arr,d) for d in 1:ndims(arr))
2526
arr2 = collect(select_region(arr, new_size=ns, pad_value=pad_value))
2627
return shear!(arr2, Δ, shear_dir_dim, shear_dim, fix_nyquist=fix_nyquist)
2728
else
28-
return shear!(copy(arr), Δ, shear_dir_dim, shear_dim, fix_nyquist=fix_nyquist)
29+
return shear!(copy(arr), Δ, shear_dir_dim, shear_dim, fix_nyquist=fix_nyquist, assign_wrap=assign_wrap, pad_value=pad_value)
2930
end
3031
end
3132

@@ -40,11 +41,14 @@ For more details see `shear.`
4041
For complex arrays we can completely avoid large memory allocations.
4142
For real arrays, we need at least allocate on array in the fourier space.
4243
"""
43-
function shear!(arr::AbstractArray{<:Complex, N}, Δ, shear_dir_dim=1, shear_dim=2; fix_nyquist=false, assign_wrap=false, pad_value=zero(eltype(arr))) where N
44+
function shear!(arr::TA, Δ, shear_dir_dim=1, shear_dim=2; fix_nyquist=false, assign_wrap=false, pad_value=zero(eltype(arr))) where {N, TA<:AbstractArray{<:Complex, N}}
4445
fft!(arr, shear_dir_dim)
4546

4647
# stores the maximum amount of shift
47-
shift = reshape(fftfreq(size(arr, shear_dir_dim)), NDTools.select_sizes(arr, shear_dir_dim))
48+
# TR = real_arr_type(TA)
49+
shift = similar(arr, real(eltype(arr)), select_sizes(arr, shear_dir_dim))
50+
shift .= reshape(fftfreq(size(arr, shear_dir_dim)), NDTools.select_sizes(arr, shear_dir_dim))
51+
# shift = TR(reorient(fftfreq(size(arr, shear_dir_dim)), shear_dir_dim, Val(N)))
4852

4953
apply_shift_strength!(arr, arr, shift, shear_dir_dim, shear_dim, Δ, fix_nyquist)
5054

@@ -56,16 +60,19 @@ function shear!(arr::AbstractArray{<:Complex, N}, Δ, shear_dir_dim=1, shear_dim
5660
return arr
5761
end
5862

59-
function shear!(arr::AbstractArray{<:Real, N}, Δ, shear_dir_dim=1, shear_dim=2; fix_nyquist=false, assign_wrap=false, pad_value=zero(eltype(arr))) where N
63+
function shear!(arr::TA, Δ, shear_dir_dim=1, shear_dim=2; fix_nyquist=false, assign_wrap=false, pad_value=zero(eltype(arr))) where {N, TA<:AbstractArray{<:Real, N}}
6064
p = plan_rfft(arr, shear_dir_dim)
6165
arr_ft = p * arr
6266

6367
# stores the maximum amount of shift
64-
shift = reshape(rfftfreq(size(arr, shear_dir_dim)), NDTools.select_sizes(arr_ft, shear_dir_dim))
68+
# TR = real_arr_type(TA)
69+
shift = similar(arr, real(eltype(arr_ft)), select_sizes(arr_ft, shear_dir_dim))
70+
shift .= reshape(rfftfreq(size(arr, shear_dir_dim)), NDTools.select_sizes(arr_ft, shear_dir_dim))
71+
# shift = TR(reorient(rfftfreq(size(arr, shear_dir_dim)),shear_dir_dim, Val(N)))
6572

6673
apply_shift_strength!(arr_ft, arr, shift, shear_dir_dim, shear_dim, Δ, fix_nyquist)
6774
# go back to real space
68-
75+
6976
# overwrites arr in-place
7077
ldiv!(arr, p, arr_ft)
7178
if assign_wrap
@@ -103,10 +110,12 @@ function assign_shear_wrap!(arr, Δ, shear_dir_dim=1, shear_dim=2, pad_value=zer
103110
end
104111
end
105112

106-
function apply_shift_strength!(arr, arr_orig, shift, shear_dir_dim, shear_dim, Δ, fix_nyquist=false)
113+
function apply_shift_strength!(arr::TA, arr_orig, shift, shear_dir_dim, shear_dim, Δ, fix_nyquist=false) where {T, N, TA<:AbstractArray{T, N}}
107114
#applies the strength to each slice
108-
shift_strength = reshape(fftpos(1, size(arr, shear_dim), CenterFT), NDTools.select_sizes(arr, shear_dim))
109-
115+
# The TR trick does not seem to work for the code below due to a call with a PaddedArray.
116+
shift_strength = similar(arr, real(eltype(arr)), select_sizes(arr, shear_dim))
117+
shift_strength .= (real(eltype(TA))).(reorient(fftpos(1, size(arr, shear_dim), CenterFT), shear_dim, Val(N)))
118+
110119
# do the exp multiplication in place
111120
e = cispi.(2 .* Δ .* shift .* shift_strength)
112121
# for even arrays we need to fix real property of highest frequency

src/fourier_shifting.jl

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,26 @@ function soft_shift(freqs, shift, fraction=eltype(freqs)(0.1); corner=false)
8080
return cispi.(-freqs .* 2 .* (w .* shift + (1.0 .-w).* rounded_shift))
8181
end
8282

83-
function shift_by_1D_FT!(arr::AbstractArray{<:Complex, N}, shifts; soft_fraction=0, take_real=false, fix_nyquist_frequency=false) where {N}
83+
function shift_by_1D_FT!(arr::TA, shifts; soft_fraction=0, take_real=false, fix_nyquist_frequency=false) where {N, TA<:AbstractArray{<:Complex, N}}
8484
# iterates of the dimension d using the corresponding shift
8585
for (d, shift) in pairs(shifts)
8686
if iszero(shift)
8787
continue
8888
end
8989
# better use reorient from NDTools here?
90-
freqs = reshape(fftfreq(size(arr, d)), ntuple(i -> 1, Val(d-1))..., size(arr,d))
90+
# TR = real_arr_type(TA)
91+
92+
freqs = similar(arr, real(eltype(arr)), select_sizes(arr, d))
93+
# freqs = TR(reorient(fftfreq(size(arr, d)),d, Val(N)))
94+
freqs .= reorient(fftfreq(size(arr, d)),d, Val(N))
95+
# @show size(freqs)
9196
# allocates a 1D slice of exp values
9297
if iszero(soft_fraction)
9398
ϕ = cispi.(- freqs .* 2 .* shift)
9499
else
95100
ϕ = soft_shift(freqs, shift, soft_fraction)
96101
end
102+
# ϕ = exp_ikx_sep(complex_arr_type(TA), size(arr), dims=(d,), shift_by = shift)[1]
97103
# in even case, set one value to real
98104
if iseven(size(arr, d))
99105
s = size(arr, d) ÷ 2 + 1
@@ -126,14 +132,24 @@ end
126132
# the idea is the following:
127133
# rfft(x, 1) -> exp shift -> fft(x, 2) -> exp shift -> fft(x, 3) -> exp shift -> ifft(x, [2,3]) -> irfft(x, 1)
128134
# So once we did a rft to shift something we can call the routine for complex arrays to shift
129-
function shift_by_1D_RFT!(arr::AbstractArray{<:Real, N}, shifts; soft_fraction=0, fix_nyquist_frequency=false, take_real=true) where {T, N}
135+
function shift_by_1D_RFT!(arr::TA, shifts; soft_fraction=0, fix_nyquist_frequency=false, take_real=true) where {N, TA<:AbstractArray{<:Real, N}}
130136
for (d, shift) in pairs(shifts)
131137
if iszero(shift)
132138
continue
133139
end
134140

135-
s = size(arr, d) ÷ 2 + 1
136-
freqs = reshape(fftfreq(size(arr, d))[1:s], ntuple(i -> 1, d-1)..., s)
141+
p = plan_rfft(arr, d)
142+
arr_ft = p * arr
143+
144+
#s1 = select_sizes(arr_ft, d)
145+
s = size(arr_ft,d); # s1[d]
146+
# @show size(arr, d) ÷ 2 + 1
147+
# freqs = TR(reshape(fftfreq(size(arr, d))[1:s], ntuple(i -> 1, d-1)..., s1))
148+
149+
# TR = real_arr_type(TA)
150+
freqs = similar(arr, real(eltype(arr_ft)), select_sizes(arr_ft, d))
151+
# freqs = TR(reorient(fftfreq(size(arr, d))[1:s], d, Val(N)))
152+
freqs .= reorient(rfftfreq(size(arr, d)), d, Val(N))
137153
if iszero(soft_fraction)
138154
ϕ = cispi.(-freqs .* 2 .* shift)
139155
else
@@ -146,9 +162,6 @@ function shift_by_1D_RFT!(arr::AbstractArray{<:Real, N}, shifts; soft_fraction=0
146162
invr = isinf(invr) ? 0 : invr
147163
ϕ[s] = fix_nyquist_frequency ? invr : ϕ[s]
148164
end
149-
p = plan_rfft(arr, d)
150-
151-
arr_ft = p * arr
152165
arr_ft .*= ϕ
153166
# since we now did a single rfft dim, we can switch to the complex routine
154167
new_shifts = ntuple(i -> i d ? 0 : shifts[i], N)

test/fourier_filtering.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Random.seed!(42)
2+
3+
@testset "Fourier filtering" begin
4+
5+
@testset "Gaussian filter complex" begin
6+
sz = (21, 22)
7+
x = randn(ComplexF32, sz)
8+
sigma = (1.1,2.2)
9+
gf = filter_gaussian(x, sigma, real_space_kernel=false)
10+
# Note that this is not the same, since one kernel is generated in real space and one in Fourier space!
11+
# with sizes around 10, the difference is huge!
12+
k = gaussian(Float32, sz, sigma=sigma)
13+
k = k./sum(k) # different than "normal".
14+
gfc = conv_psf(x, k)
15+
@test (gf,gfc, rtol=1e-2) # it is realatively inaccurate due to the kernel being generated in different places
16+
gfr = filter_gaussian(x, sigma, real_space_kernel=true)
17+
@test (gfr,gfc) # it can be debated how to best normalize a Gaussian filter
18+
end
19+
20+
@testset "Gaussian filter real" begin
21+
sz = (21, 22)
22+
x = randn(Float32, sz)
23+
sigma = (1.1,2.2)
24+
gf = filter_gaussian(x, sigma, real_space_kernel=true)
25+
# Note that this is not the same, since one kernel is generated in real space and one in Fourier space!
26+
# with sizes around 10, the difference is huge!
27+
k = gaussian(sz, sigma=sigma)
28+
k = k./sum(k) # different than "normal".
29+
gf2 = conv_psf(x, k)
30+
@test (gf,gf2, rtol=1e-2) # it is realatively inaccurate due to the kernel being generated in different places
31+
end
32+
@testset "Other filters" begin
33+
@test filter_hamming(FourierTools.delta(Float32, (3,)), border_in=0.0, border_out=1.0) [0.23,0.54, 0.23]
34+
@test filter_hann(FourierTools.delta(Float32, (3,)), border_in=0.0, border_out=1.0) [0.25,0.5, 0.25]
35+
end
36+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ include("custom_fourier_types.jl")
2121
include("damping.jl")
2222
include("czt.jl")
2323
include("nfft_tests.jl")
24+
include("fourier_filtering.jl")
2425

2526
return

0 commit comments

Comments
 (0)