Skip to content

Commit a18e2f3

Browse files
committed
WIP: introduce log gabor filter
1 parent 4fc85e4 commit a18e2f3

File tree

4 files changed

+235
-2
lines changed

4 files changed

+235
-2
lines changed

docs/demos/filters/gabor_filter.jl

Lines changed: 2 additions & 1 deletion
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
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 one small helper function
48+
# to 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+
# To apply the filter, we need to use [the convolution
69+
# theorem](https://en.wikipedia.org/wiki/Convolution_theorem):
70+
# ```math
71+
# \mathcal{F}(x \circledast k) = \mathcal{F}(x) \odot \mathcal{F}(k)
72+
# ```
73+
# where ``\circledast`` is convolution, ``\odot`` is pointwise-multiplication, and
74+
# ``\mathcal{F}`` is the fourier transformation.
75+
#
76+
# Because Log Gabor kernel is defined around center point (0, 0), we have to apply
77+
# `ifftshift` first before we do pointwise-multiplication. Because Log Gabor kernel
78+
# is defined on frequency space, we don't need to call `fft(kern)`.
79+
80+
img = TestImages.shepp_logan(127)
81+
kern = Kernel.LogGaborComplex(size(img), 50, π/4)
82+
out = ifft(centered(fft(channelview(img))) .* ifftshift(kern))
83+
mosaic(img, show_abs(kern), show_mag(out); nrow=1)
84+
85+
# A filter bank is just a list of filter kernels, applying the filter bank generates
86+
# multiple outputs:
87+
88+
X_freq = centered(fft(channelview(img)))
89+
filters = vcat(
90+
[Kernel.LogGaborComplex(size(img), 50, θ) for θ in -π/2:π/4:π/2],
91+
[Kernel.LogGabor(size(img), 50, θ) for θ in -π/2:π/4:π/2]
92+
)
93+
out = map(filters) do kern
94+
ifft(X_freq .* ifftshift(kern))
95+
end
96+
mosaic(
97+
map(show_abs, filters)...,
98+
map(show_abs, out)...;
99+
nrow=4, rowmajor=true
100+
)
101+
102+
## save covers #src
103+
using FileIO #src
104+
mkpath("assets") #src
105+
filters = [Kernel.LogGaborComplex((32, 32), 5, θ) for θ in range(-π/2, stop=π/2, length=9)] #src
106+
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 125 additions & 1 deletion
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...)
@@ -147,6 +147,130 @@ end
147147
return exp(-(xr^2 + yr^2)/(2σ^2)) * cis(xr*λ_scaled + ψ)
148148
end
149149

150+
151+
"""
152+
LogGaborComplex(size_or_axes, ω, θ; σω=1, σθ=1)
153+
LogGaborComplex(size_or_axes; ω, θ, σω=1, σθ=1)
154+
155+
Generates the 2-D Log Gabor kernel in spatial space by `Complex(r, a)`, where `r` and `a`
156+
are the frequency and angular components, respectively.
157+
158+
More detailed documentation can be found in the `r * a` version [`LogGabor`](@ref).
159+
"""
160+
struct LogGaborComplex{T, TP,R<:AbstractUnitRange} <: AbstractMatrix{T}
161+
ax::Tuple{R,R}
162+
ω::TP
163+
θ::TP
164+
σω::TP
165+
σθ::TP
166+
167+
# cache values
168+
ω_denom::TP # 1/(2(log(σω/ω))^2)
169+
θ_denom::TP # 1/(2σθ^2)
170+
function LogGaborComplex{T,TP,R}(ax::Tuple{R,R}, ω::TP, θ::TP, σω::TP, σθ::TP) where {T,TP,R}
171+
σω > 0 || throw(ArgumentError("`σω` should be positive: $σω"))
172+
σθ > 0 || throw(ArgumentError("`σθ` should be positive: $σθ"))
173+
new{T,TP,R}(ax, ω, θ, σω, σθ, 1/(2(log(σω/ω))^2), 1/(2σθ^2))
174+
end
175+
end
176+
LogGaborComplex(size_or_axes::Tuple, ω, θ; kwargs...) = LogGaborComplex(size_or_axes; ω, θ, kwargs...)
177+
function LogGaborComplex(size_or_axes::Tuple; ω::Real, θ::Real, σω::Real=1, σθ::Real=1)
178+
params = float.(promote(ω, θ, σω, σθ))
179+
T = typeof(params[1])
180+
ax = _to_axes(size_or_axes)
181+
LogGaborComplex{Complex{T}, T, typeof(first(ax))}(ax, params...)
182+
end
183+
184+
@inline Base.size(kern::LogGaborComplex) = map(length, kern.ax)
185+
@inline Base.axes(kern::LogGaborComplex) = kern.ax
186+
187+
@inline function Base.getindex(kern::LogGaborComplex, x::Int, y::Int)
188+
ω_denom, θ_denom = kern.ω_denom, kern.θ_denom
189+
# normalize: from reference [1]
190+
x, y = (x, y)./size(kern)
191+
ω = sqrt(x^2 + y^2) # this is faster than hypot(x, y)
192+
θ = atan(y, x)
193+
r = exp((-(log/kern.ω))^2)*ω_denom) # radial component
194+
a = exp((--kern.θ).^2)*θ_denom) # angular component
195+
return r + a*im
196+
end
197+
198+
199+
"""
200+
LogGabor(size_or_axes, ω, θ; σω=1, σθ=1)
201+
LogGabor(size_or_axes; ω, θ, σω=1, σθ=1)
202+
203+
Generates the 2-D Log Gabor kernel in spatial space by `r * a`, where `r` and `a` are the
204+
frequency and angular components, respectively.
205+
206+
See also [`LogGaborComplex`](@ref) for the `Complex(r, a)` version.
207+
208+
# Arguments
209+
210+
- `kernel_size::Dims{2}`: the Log Gabor kernel size. The axes at each dimension will be
211+
`-r:r` if the size is odd.
212+
- `kernel_axes::NTuple{2, <:AbstractUnitRange}`: the axes of the Log Gabor kernel.
213+
- `ω`: the center frequency.
214+
- `θ`: the center orientation.
215+
216+
# Keywords
217+
218+
- `σω=1`: scale component for `ω`. Larger `σω` makes the filter more sensitive to center region.
219+
- `σθ=1`: scale component for `θ`. Larger `σθ` makes the filter less sensitive to orientation.
220+
221+
# Examples
222+
223+
To apply log gabor filter `g` on image `X`, one need to use convolution theorem, i.e.,
224+
`conv(A, K) == ifft(fft(A) .* fft(K))`. Because this `LogGabor` function generates Log Gabor
225+
kernel directly in spatial space, we don't need to apply `fft(K)` here:
226+
227+
```jldoctest
228+
julia> using ImageFiltering, FFTW, TestImages, ImageCore
229+
230+
julia> img = TestImages.shepp_logan(256);
231+
232+
julia> kern = Kernel.LogGabor(size(img), 1/6, 0);
233+
234+
julia> g_img = ifft(fft(channelview(img)) .* ifftshift(fft(kern))); # apply convolution theorem
235+
236+
julia> @. Gray(abs(g_img));
237+
238+
```
239+
240+
# Extended help
241+
242+
Mathematically, log gabor filter is defined in spatial space as the product of
243+
its frequency component `r` and angular component `a`:
244+
245+
```math
246+
r(\\omega, \\theta) = \\exp(-\\frac{(\\log(\\omega/\\omega_0))^2}{2\\sigma_\\omega^2}) \\
247+
a(\\omega, \\theta) = \\exp(-\\frac{(\\theta - \\theta_0)^2}{2\\sigma_\\theta^2})
248+
```
249+
250+
# References
251+
252+
- [1] [What Are Log-Gabor Filters and Why Are They
253+
Good?](https://www.peterkovesi.com/matlabfns/PhaseCongruency/Docs/convexpl.html)
254+
- [2] Kovesi, Peter. "Image features from phase congruency." _Videre: Journal of computer
255+
vision research_ 1.3 (1999): 1-26.
256+
- [3] Field, David J. "Relations between the statistics of natural images and the response
257+
properties of cortical cells." _Josa a_ 4.12 (1987): 2379-2394.
258+
"""
259+
struct LogGabor{T, AT<:LogGaborComplex} <: AbstractMatrix{T}
260+
complex_data::AT
261+
end
262+
LogGabor(complex_data::AT) where AT<:LogGaborComplex = LogGabor{eltype(AT), AT}(complex_data)
263+
LogGabor(size_or_axes::Tuple, ω, θ; kwargs...) = LogGabor(size_or_axes; ω, θ, kwargs...)
264+
LogGabor(size_or_axes::Tuple; kwargs...) = LogGabor(LogGaborComplex(size_or_axes; kwargs...))
265+
266+
@inline Base.size(kern::LogGabor) = size(kern.complex_data)
267+
@inline Base.axes(kern::LogGabor) = axes(kern.complex_data)
268+
Base.@propagate_inbounds function Base.getindex(kern::LogGabor, inds::Int...)
269+
v = kern.complex_data[inds...]
270+
return real(v) * imag(v)
271+
end
272+
273+
150274
# Utils
151275

152276
function _to_axes(sz::Dims)

0 commit comments

Comments
 (0)