Skip to content

Commit 1e3e2f3

Browse files
committed
introduce log gabor filter: LogGabor and LogGaborComplex
1 parent f25ddf9 commit 1e3e2f3

File tree

5 files changed

+335
-2
lines changed

5 files changed

+335
-2
lines changed

docs/demos/filters/gabor_filter.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
# ---
88

99
# This example shows how one can apply spatial space kernesl [`Gabor`](@ref Kernel.Gabor)
10-
# using fourier transformation and convolution theorem to extract image features.
10+
# using fourier transformation and convolution theorem to extract image features. A similar
11+
# example is [Log Gabor filter](@ref demo_log_gabor_filter).
1112

1213
using ImageCore, ImageShow, ImageFiltering # or you could just `using Images`
1314
using FFTW

docs/demos/filters/loggabor_filter.jl

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# ---
2+
# title: Log Gabor filter
3+
# id: demo_log_gabor_filter
4+
# cover: assets/log_gabor.png
5+
# author: Johnny Chen
6+
# date: 2021-11-01
7+
# ---
8+
9+
# This example shows how one can apply frequency space kernesl [`LogGabor`](@ref
10+
# Kernel.LogGabor) and [`LogGaborComplex`](@ref Kernel.LogGaborComplex) using fourier
11+
# transformation and convolution theorem to extract image features. A similar example
12+
# is the sptaial space kernel [Gabor filter](@ref demo_gabor_filter).
13+
14+
using ImageCore, ImageShow, ImageFiltering # or you could just `using Images`
15+
using FFTW
16+
using TestImages
17+
18+
# ## Definition
19+
#
20+
# Mathematically, log gabor filter is defined in spatial space as the composition of
21+
# its frequency component `r` and angular component `a`:
22+
#
23+
# ```math
24+
# r(\omega, \theta) = \exp(-\frac{(\log(\omega/\omega_0))^2}{2\sigma_\omega^2}) \\
25+
# a(\omega, \theta) = \exp(-\frac{(\theta - \theta_0)^2}{2\sigma_\theta^2})
26+
# ```
27+
#
28+
# `LogGaborComplex` provides a complex-valued matrix with value `Complex(r, a)`, while
29+
# `LogGabor` provides real-valued matrix with value `r * a`.
30+
31+
kern_c = Kernel.LogGaborComplex((10, 10), 1/6, 0)
32+
kern_r = Kernel.LogGabor((10, 10), 1/6, 0)
33+
kern_r == @. real(kern_c) * imag(kern_c)
34+
35+
# !!! note "Lazy array"
36+
# The `LogGabor` and `LogGaborComplex` types are lazy arrays, which means when you build
37+
# the Log Gabor kernel, you actually don't need to allocate any memories. The computation
38+
# does not happen until you request the value.
39+
#
40+
# ```julia
41+
# using BenchmarkTools
42+
# kern = @btime Kernel.LogGabor((64, 64), 1/6, 0); # 1.711 ns (0 allocations: 0 bytes)
43+
# @btime collect($kern); # 146.260 μs (2 allocations: 64.05 KiB)
44+
# ```
45+
46+
47+
# To explain the parameters of Log Gabor filter, let's introduce small helper functions to
48+
# display complex-valued kernels.
49+
show_phase(kern) = @. Gray(log(abs(imag(kern)) + 1))
50+
show_mag(kern) = @. Gray(log(abs(real(kern)) + 1))
51+
show_abs(kern) = @. Gray(log(abs(kern) + 1))
52+
nothing #hide
53+
54+
# From left to right are visualization of the kernel in frequency space: frequency `r`,
55+
# algular `a`, `sqrt(r^2 + a^2)`, `r * a`, and its spatial space kernel.
56+
kern = Kernel.LogGaborComplex((32, 32), 100, 0)
57+
mosaic(
58+
show_mag(kern),
59+
show_phase(kern),
60+
show_abs(kern),
61+
Gray.(Kernel.LogGabor(kern)),
62+
show_abs(centered(ifftshift(ifft(kern)))),
63+
nrow=1
64+
)
65+
66+
# ## Examples
67+
#
68+
# Because the filter is defined on frequency space, we can use [the convolution
69+
# theorem](https://en.wikipedia.org/wiki/Convolution_theorem):
70+
#
71+
# ```math
72+
# \mathcal{F}(x \circledast k) = \mathcal{F}(x) \odot \mathcal{F}(k)
73+
# ```
74+
# where ``\circledast`` is convolution, ``\odot`` is pointwise-multiplication, and
75+
# ``\mathcal{F}`` is the fourier transformation.
76+
#
77+
# Also, since Log Gabor kernel is defined around center point (0, 0), we have to apply
78+
# `ifftshift` first before we do pointwise-multiplication.
79+
80+
img = TestImages.shepp_logan(127)
81+
kern = Kernel.LogGaborComplex(size(img), 50, π/4)
82+
## we don't need to call `fft(kern)` here because it's already on frequency space
83+
out = ifft(centered(fft(channelview(img))) .* ifftshift(kern))
84+
mosaic(img, show_abs(kern), show_mag(out); nrow=1)
85+
86+
# A filter bank is just a list of filter kernels, applying the filter bank generates
87+
# multiple outputs:
88+
89+
X_freq = centered(fft(channelview(img)))
90+
filters = vcat(
91+
[Kernel.LogGaborComplex(size(img), 50, θ) for θ in -π/2:π/4:π/2],
92+
[Kernel.LogGabor(size(img), 50, θ) for θ in -π/2:π/4:π/2]
93+
)
94+
out = map(filters) do kern
95+
ifft(X_freq .* ifftshift(kern))
96+
end
97+
mosaic(
98+
map(show_abs, filters)...,
99+
map(show_abs, out)...;
100+
nrow=4, rowmajor=true
101+
)
102+
103+
## save covers #src
104+
using FileIO #src
105+
mkpath("assets") #src
106+
filters = [Kernel.LogGaborComplex((32, 32), 5, θ) for θ in range(-π/2, stop=π/2, length=9)] #src
107+
save("assets/log_gabor.png", mosaic(map(show_abs, filters); nrow=3, npad=2, fillvalue=Gray(1)); fps=2) #src

docs/src/function_reference.md

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Kernel.DoG
2424
Kernel.LoG
2525
Kernel.Laplacian
2626
Kernel.Gabor
27+
Kernel.LogGabor
28+
Kernel.LogGaborComplex
2729
Kernel.moffat
2830
```
2931

src/gaborkernels.jl

+142-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module GaborKernels
22

3-
export Gabor
3+
export Gabor, LogGabor, LogGaborComplex
44

55
"""
66
Gabor(size_or_axes, wavelength, orientation; kwargs...)
@@ -169,6 +169,147 @@ end
169169
return exp(-(xr^2 + yr^2)/(2σ^2)) * cis(xr*λ_scaled + ψ)
170170
end
171171

172+
173+
"""
174+
LogGaborComplex(size_or_axes, ω, θ; σω=1, σθ=1, normalize=true)
175+
176+
Generate the 2-D Log Gabor kernel in spatial space by `Complex(r, a)`, where `r` and `a`
177+
are the frequency and angular components, respectively.
178+
179+
More detailed documentation and example can be found in the `r * a` version
180+
[`LogGabor`](@ref).
181+
"""
182+
struct LogGaborComplex{T, TP,R<:AbstractUnitRange} <: AbstractMatrix{T}
183+
ax::Tuple{R,R}
184+
ω::TP
185+
θ::TP
186+
σω::TP
187+
σθ::TP
188+
normalize::Bool
189+
190+
# cache values
191+
freq_scale::Tuple{TP, TP} # only used when normalize is true
192+
ω_denom::TP # 1/(2(log(σω/ω))^2)
193+
θ_denom::TP # 1/(2σθ^2)
194+
function LogGaborComplex{T,TP,R}(ax::Tuple{R,R}, ω::TP, θ::TP, σω::TP, σθ::TP, normalize::Bool) where {T,TP,R}
195+
σω > 0 || throw(ArgumentError("`σω` should be positive: $σω"))
196+
σθ > 0 || throw(ArgumentError("`σθ` should be positive: $σθ"))
197+
ω_denom = 1/(2(log(σω/ω))^2)
198+
θ_denom = 1/(2σθ^2)
199+
freq_scale = map(r->1/length(r), ax)
200+
new{T,TP,R}(ax, ω, θ, σω, σθ, normalize, freq_scale, ω_denom, θ_denom)
201+
end
202+
end
203+
function LogGaborComplex(
204+
size_or_axes::Tuple, ω::Real, θ::Real;
205+
σω::Real=1, σθ::Real=1, normalize::Bool=true,
206+
)
207+
params = float.(promote(ω, θ, σω, σθ))
208+
T = typeof(params[1])
209+
ax = _to_axes(size_or_axes)
210+
LogGaborComplex{Complex{T}, T, typeof(first(ax))}(ax, params..., normalize)
211+
end
212+
213+
@inline Base.size(kern::LogGaborComplex) = map(length, kern.ax)
214+
@inline Base.axes(kern::LogGaborComplex) = kern.ax
215+
216+
@inline function Base.getindex(kern::LogGaborComplex, x::Int, y::Int)
217+
ω_denom, θ_denom = kern.ω_denom, kern.θ_denom
218+
# Although in `getindex`, the computation is heavy enough that this runtime if-branch is
219+
# harmless to the overall performance at all
220+
if kern.normalize
221+
# normalize: from reference [1] of LogGabor
222+
# By changing division to multiplication gives about 5-10% performance boost
223+
x, y = (x, y) .* kern.freq_scale
224+
end
225+
ω = sqrt(x^2 + y^2) # this is faster than hypot(x, y)
226+
θ = atan(y, x)
227+
r = exp((-(log/kern.ω))^2)*ω_denom) # radial component
228+
a = exp((--kern.θ)^2)*θ_denom) # angular component
229+
return Complex(r, a)
230+
end
231+
232+
233+
"""
234+
LogGabor(size_or_axes, ω, θ; σω=1, σθ=1, normalize=true)
235+
236+
Generate the 2-D Log Gabor kernel in spatial space by `r * a`, where `r` and `a` are the
237+
frequency and angular components, respectively.
238+
239+
See also [`LogGaborComplex`](@ref) for the `Complex(r, a)` version.
240+
241+
# Arguments
242+
243+
- `kernel_size::Dims{2}`: the Log Gabor kernel size. The axes at each dimension will be
244+
`-r:r` if the size is odd.
245+
- `kernel_axes::NTuple{2, <:AbstractUnitRange}`: the axes of the Log Gabor kernel.
246+
- `ω`: the center frequency.
247+
- `θ`: the center orientation.
248+
249+
# Keywords
250+
251+
- `σω=1`: scale component for `ω`. Larger `σω` makes the filter more sensitive to center
252+
region.
253+
- `σθ=1`: scale component for `θ`. Larger `σθ` makes the filter less sensitive to
254+
orientation.
255+
- `normalize=true`: whether to normalize the frequency domain into [-0.5, 0.5]x[-0.5, 0.5]
256+
domain via `inds = inds./size(kern)`. For image-related tasks where the [Weber–Fechner
257+
law](https://en.wikipedia.org/wiki/Weber%E2%80%93Fechner_law) applies, this usually
258+
provides more stable pipeline.
259+
260+
# Examples
261+
262+
To apply log gabor filter `g` on image `X`, one need to use convolution theorem, i.e.,
263+
`conv(A, K) == ifft(fft(A) .* fft(K))`. Because this `LogGabor` function generates Log Gabor
264+
kernel directly in spatial space, we don't need to apply `fft(K)` here:
265+
266+
```jldoctest
267+
julia> using ImageFiltering, FFTW, TestImages, ImageCore
268+
269+
julia> img = TestImages.shepp_logan(256);
270+
271+
julia> kern = Kernel.LogGabor(size(img), 1/6, 0);
272+
273+
julia> g_img = ifft(centered(fft(channelview(img))) .* ifftshift(kern)); # apply convolution theorem
274+
275+
julia> @. Gray(abs(g_img));
276+
277+
```
278+
279+
# Extended help
280+
281+
Mathematically, log gabor filter is defined in spatial space as the product of its frequency
282+
component `r` and angular component `a`:
283+
284+
```math
285+
r(\\omega, \\theta) = \\exp(-\\frac{(\\log(\\omega/\\omega_0))^2}{2\\sigma_\\omega^2}) \\
286+
a(\\omega, \\theta) = \\exp(-\\frac{(\\theta - \\theta_0)^2}{2\\sigma_\\theta^2})
287+
```
288+
289+
# References
290+
291+
- [1] [What Are Log-Gabor Filters and Why Are They
292+
Good?](https://www.peterkovesi.com/matlabfns/PhaseCongruency/Docs/convexpl.html)
293+
- [2] Kovesi, Peter. "Image features from phase congruency." _Videre: Journal of computer
294+
vision research_ 1.3 (1999): 1-26.
295+
- [3] Field, David J. "Relations between the statistics of natural images and the response
296+
properties of cortical cells." _Josa a_ 4.12 (1987): 2379-2394.
297+
"""
298+
struct LogGabor{T, AT<:LogGaborComplex} <: AbstractMatrix{T}
299+
complex_data::AT
300+
end
301+
LogGabor(complex_data::AT) where AT<:LogGaborComplex = LogGabor{real(eltype(AT)), AT}(complex_data)
302+
LogGabor(size_or_axes::Tuple, ω, θ; kwargs...) = LogGabor(LogGaborComplex(size_or_axes, ω, θ; kwargs...))
303+
304+
@inline Base.size(kern::LogGabor) = size(kern.complex_data)
305+
@inline Base.axes(kern::LogGabor) = axes(kern.complex_data)
306+
Base.@propagate_inbounds function Base.getindex(kern::LogGabor, inds::Int...)
307+
# cache the result to avoid repeated computation
308+
v = kern.complex_data[inds...]
309+
return real(v) * imag(v)
310+
end
311+
312+
172313
# Utils
173314

174315
function _to_axes(sz::Dims)

test/gaborkernels.jl

+82
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,86 @@ end
122122
end
123123
end
124124

125+
126+
@testset "LogGabor" begin
127+
@testset "API" begin
128+
# LogGabor: r * a
129+
# LogGaborComplex: Complex(r, a)
130+
kern = @inferred Kernel.LogGabor((11, 11), 1/6, 0)
131+
kern_c = Kernel.LogGaborComplex((11, 11), 1/6, 0)
132+
@test kern == @. real(kern_c) * imag(kern_c)
133+
134+
# Normally it just return a ComplexF64 matrix if users are careless
135+
kern = @inferred Kernel.LogGabor((11, 11), 2, 0)
136+
@test size(kern) == (11, 11)
137+
@test axes(kern) == (-5:5, -5:5)
138+
@test eltype(kern) == Float64
139+
# ensure that this is an efficient lazy array
140+
@test isbitstype(typeof(kern))
141+
142+
# but still allow construction of ComplexF32 matrix
143+
kern = @inferred Kernel.LogGabor((11, 11), 1.0f0/6, 0.0f0)
144+
@test eltype(kern) == Float32
145+
146+
# passing axes explicitly allows building a subregion of it
147+
kern1 = @inferred Kernel.LogGabor((1:10, 1:10), 1/6, 0)
148+
@test axes(kern1) == (1:10, 1:10)
149+
kern2 = @inferred Kernel.LogGabor((21, 21), 1/6, 0)
150+
# but they are not the same in normalize=true mode
151+
@test kern2[1:end, 1:end] != kern1
152+
153+
# when normalize=false, they're the same
154+
kern1 = @inferred Kernel.LogGabor((1:10, 1:10), 1/6, 0; normalize=false)
155+
kern2 = @inferred Kernel.LogGabor((21, 21), 1/6, 0; normalize=false)
156+
@test kern2[1:end, 1:end] == kern1
157+
158+
# test default keyword values
159+
kern1 = @inferred Kernel.LogGabor((11, 11), 2, 0)
160+
kern2 = @inferred Kernel.LogGabor((11, 11), 2, 0; σω=1, σθ=1, normalize=true)
161+
@test kern1 === kern2
162+
163+
@test_throws ArgumentError Kernel.LogGabor((11, 11), 2, 0; σω=-1)
164+
@test_throws ArgumentError Kernel.LogGabor((11, 11), 2, 0; σθ=-1)
165+
@test_throws ArgumentError Kernel.LogGaborComplex((11, 11), 2, 0; σω=-1)
166+
@test_throws ArgumentError Kernel.LogGaborComplex((11, 11), 2, 0; σθ=-1)
167+
end
168+
169+
@testset "symmetricity" begin
170+
kern = Kernel.LogGaborComplex((11, 11), 1/12, 0)
171+
# the real part is an even-symmetric function
172+
@test is_symmetric(real.(kern))
173+
# the imaginary part is a mirror along y-axis
174+
rows = [imag.(kern[i, :]) for i in axes(kern, 1)]
175+
@test all(map(is_symmetric, rows))
176+
177+
# NOTE(johnnychen94): I'm not sure the current implementation is the standard or
178+
# correct. This numerical references are generated only to make sure future changes
179+
# don't accidentally break it.
180+
# Use N0f8 to ignore "insignificant" numerical changes to keep unit test happy
181+
ref_real = N0f8[
182+
0.741 0.796 0.82 0.796 0.741
183+
0.796 0.886 0.941 0.886 0.796
184+
0.82 0.941 0.0 0.941 0.82
185+
0.796 0.886 0.941 0.886 0.796
186+
0.741 0.796 0.82 0.796 0.741
187+
]
188+
ref_imag = N0f8[
189+
0.063 0.027 0.008 0.027 0.063
190+
0.125 0.063 0.008 0.063 0.125
191+
0.29 0.29 1.0 0.29 0.29
192+
0.541 0.733 1.0 0.733 0.541
193+
0.733 0.898 1.0 0.898 0.733
194+
]
195+
kern = Kernel.LogGaborComplex((5, 5), 1/12, 0)
196+
@test collect(N0f8.(real(kern))) == ref_real
197+
@test collect(N0f8.(imag(kern))) == ref_imag
198+
end
199+
200+
@testset "applications" begin
201+
img = TestImages.shepp_logan(127)
202+
kern = Kernel.LogGabor(size(img), 1/100, π/4)
203+
out = @test_nowarn ifft(centered(fft(channelview(img))) .* ifftshift(kern))
204+
end
205+
end
206+
125207
end

0 commit comments

Comments
 (0)