Skip to content

Commit f25ddf9

Browse files
committed
new gabor filter: lazy Gabor array
This commit introduces an lazy array interface for the Gabor filter, benchmark shows that it's 2-3x faster than the previous version. The documentation is heavly rewrote to explain this filter and all its parameters.
1 parent 424523c commit f25ddf9

File tree

7 files changed

+494
-3
lines changed

7 files changed

+494
-3
lines changed

docs/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[deps]
22
DemoCards = "311a05b2-6137-4a5a-b473-18580a3d38b5"
33
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
4+
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
5+
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
46
ImageContrastAdjustment = "f332f351-ec65-5f6a-b3d1-319c6670881a"
57
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
68
ImageDistances = "51556ac3-7006-55f5-8cb3-34580c88182d"

docs/demos/filters/gabor_filter.jl

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# ---
2+
# title: Gabor filter
3+
# id: demo_gabor_filter
4+
# cover: assets/gabor.png
5+
# author: Johnny Chen
6+
# date: 2021-11-01
7+
# ---
8+
9+
# 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.
11+
12+
using ImageCore, ImageShow, ImageFiltering # or you could just `using Images`
13+
using FFTW
14+
using TestImages
15+
16+
# ## Definition
17+
#
18+
# Mathematically, Gabor kernel is defined in spatial space:
19+
#
20+
# ```math
21+
# g(x, y) = \exp(-\frac{x'^2 + \gamma^2y'^2}{2\sigma^2})\exp(i(2\pi\frac{x'}{\lambda} + \psi))
22+
# ```
23+
# where ``i`` is imaginary unit `Complex(0, 1)`, and
24+
# ```math
25+
# x' = x\cos\theta + x\sin\theta \\
26+
# y' = -x\sin\theta + y\cos\theta
27+
# ```
28+
#
29+
30+
# First of all, Gabor kernel is a complex-valued matrix:
31+
32+
kern = Kernel.Gabor((10, 10), 2, 0.1)
33+
34+
# !!! tip "Lazy array"
35+
# The `Gabor` type is a lazy array, which means when you build the Gabor kernel, you
36+
# actually don't need to allocate any memories.
37+
#
38+
# ```julia
39+
# using BenchmarkTools
40+
# kern = @btime Kernel.Gabor((64, 64), 5, 0); # 36.481 ns (0 allocations: 0 bytes)
41+
# @btime collect($kern); # 75.278 μs (2 allocations: 64.05 KiB)
42+
# ```
43+
44+
# To explain the parameters of Gabor filter, let's introduce some small helpers function to
45+
# display complex-valued kernels.
46+
show_phase(kern) = @. Gray(log(abs(imag(kern)) + 1))
47+
show_mag(kern) = @. Gray(log(abs(real(kern)) + 1))
48+
show_abs(kern) = @. Gray(log(abs(kern) + 1))
49+
nothing #hide
50+
51+
# ## Keywords
52+
#
53+
# ### `wavelength` (λ)
54+
# λ specifies the wavelength of the sinusoidal factor.
55+
56+
bandwidth, orientation, phase_offset, aspect_ratio = 1, 0, 0, 0.5
57+
f(wavelength) = show_abs(Kernel.Gabor((100, 100); wavelength, bandwidth, orientation, aspect_ratio, phase_offset))
58+
mosaic(f.((5, 10, 15)), nrow=1)
59+
60+
# ### `orientation` (θ)
61+
# θ specifies the orientation of the normal to the parallel stripes of a Gabor function.
62+
63+
wavelength, bandwidth, phase_offset, aspect_ratio = 10, 1, 0, 0.5
64+
f(orientation) = show_abs(Kernel.Gabor((100, 100); wavelength, bandwidth, orientation, aspect_ratio, phase_offset))
65+
mosaic(f.((0, π/4, π/2)), nrow=1)
66+
67+
# ### `phase_offset` (ψ)
68+
69+
wavelength, bandwidth, orientation, aspect_ratio = 10, 1, 0, 0.5
70+
f(phase_offset) = show_phase(Kernel.Gabor((100, 100); wavelength, bandwidth, orientation, aspect_ratio, phase_offset))
71+
mosaic(f.((-π/2, 0, π/2, π)), nrow=1)
72+
73+
# ### `aspect_ratio` (γ)
74+
# γ specifies the ellipticity of the support of the Gabor function.
75+
76+
wavelength, bandwidth, orientation, phase_offset = 10, 1, 0, 0
77+
f(aspect_ratio) = show_abs(Kernel.Gabor((100, 100); wavelength, bandwidth, orientation, aspect_ratio, phase_offset))
78+
mosaic(f.((0.5, 1, 2)), nrow=1)
79+
80+
# ### `bandwidth` (b)
81+
# The half-response spatial frequency bandwidth (b) of a Gabor filter is related to the
82+
# ratio σ / λ, where σ and λ are the standard deviation of the Gaussian factor of the Gabor
83+
# function and the preferred wavelength, respectively, as follows:
84+
#
85+
# ```math
86+
# b = \log_2\frac{\frac{\sigma}{\lambda}\pi + \sqrt{\frac{\ln 2}{2}}}{\frac{\sigma}{\lambda}\pi - \sqrt{\frac{\ln 2}{2}}}
87+
# ```
88+
89+
wavelength, orientation, phase_offset, aspect_ratio = 10, 0, 0, 0.5
90+
f(bandwidth) = show_abs(Kernel.Gabor((100, 100); wavelength, bandwidth, orientation, aspect_ratio, phase_offset))
91+
mosaic(f.((0.5, 1, 2)), nrow=1)
92+
93+
# ## Examples
94+
#
95+
# There are two options to apply a spatial space kernel: 1) via `imfilter`, and 2) via the
96+
# convolution theorem.
97+
98+
# ### `imfilter`
99+
#
100+
# [`imfilter`](@ref) does not require the kernel size to be the same as the image size.
101+
# Usually kernel size is at least 5 times larger than the wavelength.
102+
103+
img = TestImages.shepp_logan(127)
104+
kern = Kernel.Gabor((19, 19), 3, 0)
105+
out = imfilter(img, real.(kern))
106+
mosaic(img, show_abs(kern), show_mag(out); nrow=1)
107+
108+
# ### convolution theorem
109+
110+
# The [convolution theorem](https://en.wikipedia.org/wiki/Convolution_theorem) tells us
111+
# that `fft(conv(X, K))` is equivalent to `fft(X) .* fft(K)`. Because Gabor kernel is
112+
# defined with regard to its center point (0, 0), we need to do `ifftshift` first so that
113+
# the frequency centers of both `fft(X)` and `fft(kern)` align well.
114+
115+
kern = Kernel.Gabor(size(img), 3, 0)
116+
out = ifft(fft(channelview(img)) .* ifftshift(fft(kern)))
117+
mosaic(img, show_abs(kern), show_mag(out); nrow=1)
118+
119+
# As you may have notice, using convolution theorem generates different results. This is
120+
# simply because the kernel size are different. If we create a smaller kernel, we then need
121+
# to apply [`freqkernel`](@ref) first so that we can do element-wise multiplication.
122+
123+
## freqkernel = zero padding + fftshift + fft
124+
kern = Kernel.Gabor((19, 19), 3, 0)
125+
kern_freq = freqkernel(real.(kern), size(img))
126+
out = ifft(fft(channelview(img)) .* kern_freq)
127+
mosaic(img, show_abs(kern), show_mag(out); nrow=1)
128+
129+
# !!! note "Performance on different kernel size"
130+
# When the kernel size is small, `imfilter` works more efficient than fft-based
131+
# convolution. This benchmark isn't backed by CI so the result might be outdated, but
132+
# you get the idea.
133+
#
134+
# ```julia
135+
# using BenchmarkTools
136+
# img = TestImages.shepp_logan(127);
137+
#
138+
# kern = Kernel.Gabor((19, 19), 3, 0);
139+
# fft_conv(img, kern) = ifft(fft(channelview(img)) .* freqkernel(real.(kern), size(img)))
140+
# @btime imfilter($img, real.($kern)); # 236.813 μs (118 allocations: 418.91 KiB)
141+
# @btime fft_conv($img, $kern) # 1.777 ms (127 allocations: 1.61 MiB)
142+
#
143+
# kern = Kernel.Gabor(size(img), 3, 0)
144+
# fft_conv(img, kern) = ifft(fft(channelview(img)) .* ifftshift(fft(kern)))
145+
# @btime imfilter($img, real.($kern)); # 5.318 ms (163 allocations: 5.28 MiB)
146+
# @btime fft_conv($img, $kern); # 2.218 ms (120 allocations: 1.73 MiB)
147+
# ```
148+
#
149+
150+
## Filter bank
151+
152+
# A filter bank is just a list of filter kernels, applying the filter bank generates
153+
# multiple outputs:
154+
155+
filters = [Kernel.Gabor(size(img), 3, θ) for θ in -π/2:π/4:π/2];
156+
X_freq = fft(channelview(img))
157+
out = map(filters) do kern
158+
ifft(X_freq .* ifftshift(fft(kern)))
159+
end
160+
mosaic(
161+
map(show_abs, filters)...,
162+
map(show_abs, out)...;
163+
nrow=2, rowmajor=true
164+
)
165+
166+
## save covers #src
167+
using FileIO #src
168+
mkpath("assets") #src
169+
filters = [Kernel.Gabor((32, 32), 5, θ) for θ in range(-π/2, stop=π/2, length=9)] #src
170+
save("assets/gabor.png", mosaic(map(show_abs, filters); nrow=3, npad=2, fillvalue=Gray(1)); fps=2) #src
171+
172+
# # References
173+
#
174+
# - [1] [Wikipedia: Gabor filter](https://en.wikipedia.org/wiki/Gabor_filter)
175+
# - [2] [Wikipedia: Gabor transformation](https://en.wikipedia.org/wiki/Gabor_transform)
176+
# - [3] [Wikipedia: Gabor atom](https://en.wikipedia.org/wiki/Gabor_atom)
177+
# - [4] [Gabor filter for image processing and computer vision](http://matlabserver.cs.rug.nl/edgedetectionweb/web/edgedetection_params.html)

docs/src/function_reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Kernel.gaussian
2323
Kernel.DoG
2424
Kernel.LoG
2525
Kernel.Laplacian
26-
Kernel.gabor
26+
Kernel.Gabor
2727
Kernel.moffat
2828
```
2929

src/gaborkernels.jl

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
module GaborKernels
2+
3+
export Gabor
4+
5+
"""
6+
Gabor(size_or_axes, wavelength, orientation; kwargs...)
7+
Gabor(size_or_axes; wavelength, orientation, kwargs...)
8+
9+
Generate the 2-D Gabor kernel in the spatial space.
10+
11+
# Arguments
12+
13+
## `kernel_size::Dims{2}` or `kernel_axes::NTuple{2,<:AbstractUnitRange}`
14+
15+
Specifies either the size or axes of the output kernel. The axes at each dimension will be
16+
`-r:r` if the size is odd.
17+
18+
## `wavelength::Real`(λ>=2)
19+
20+
This is the wavelength of the sinusoidal factor of the Gabor filter kernel and herewith the
21+
preferred wavelength of this filter. Its value is specified in pixels.
22+
23+
The value `λ=2` should not be used in combination with phase offset `ψ=-π/2` or `ψ=π/2`
24+
because in these cases the Gabor function is sampled in its zero crossings.
25+
26+
In order to prevent the occurence of undesired effects at the image borders, the wavelength
27+
value should be smaller than one fifth of the input image size.
28+
29+
## `orientation::Real`(θ∈[0, 2pi]):
30+
31+
This parameter specifies the orientation of the normal to the parallel stripes of a Gabor
32+
function [3].
33+
34+
# Keywords
35+
36+
## `bandwidth=1` (b>0)
37+
38+
The half-response spatial frequency bandwidth b (in octaves) of a Gabor filter is related to
39+
the ratio σ / λ, where σ and λ are the standard deviation of the Gaussian factor of the
40+
Gabor function and the preferred wavelength, respectively, as follows:
41+
42+
```math
43+
b = \\log_2\\frac{\\frac{\\sigma}{\\lambda}\\pi + \\sqrt{\\frac{\\ln 2}{2}}}{\\frac{\\sigma}{\\lambda}\\pi - \\sqrt{\\frac{\\ln 2}{2}}}
44+
```
45+
46+
## `aspect_ratio=0.5`(γ>0)
47+
48+
This parameter, called more precisely the spatial aspect ratio, specifies the ellipticity of
49+
the support of the Gabor function [3]. For `γ = 1`, the support is circular. For γ < 1 the
50+
support is elongated in orientation of the parallel stripes of the function.
51+
52+
## `phase_offset=0` (ψ∈[-π, π])
53+
54+
The values `0` and `π` correspond to center-symmetric 'center-on' and 'center-off'
55+
functions, respectively, while -π/2 and π/2 correspond to anti-symmetric functions. All
56+
other cases correspond to asymmetric functions.
57+
58+
# Examples
59+
60+
There are two ways to use gabor filters: 1) via [`imfilter`](@ref), 2) via `fft` and
61+
convolution theorem. Usually `imfilter` requires a small kernel to work efficiently, while
62+
using `fft` can be more efficient when the kernel has the same size of the image.
63+
64+
## imfilter
65+
66+
```jldoctest gabor_example
67+
julia> using ImageFiltering, FFTW, TestImages, ImageCore
68+
69+
julia> img = TestImages.shepp_logan(256);
70+
71+
julia> kern = Kernel.Gabor((19, 19), 3, 0); # usually a small kernel size
72+
73+
julia> g_img = imfilter(img, real.(kern));
74+
75+
```
76+
77+
## convolution theorem
78+
79+
The convolution theorem is stated as `fft(conv(A, K)) == fft(A) .* fft(K)`:
80+
81+
```jldoctest gabor_example
82+
julia> kern = Kernel.Gabor(size(img), 3, 0);
83+
84+
julia> g_img = ifft(fft(channelview(img)) .* ifftshift(fft(kern))); # apply convolution theorem
85+
86+
julia> @. Gray(abs(g_img));
87+
88+
```
89+
90+
See the [gabor filter demo](@ref demo_gabor_filter) for more examples with images.
91+
92+
# Extended help
93+
94+
Mathematically, gabor filter is defined in spatial space:
95+
96+
```math
97+
g(x, y) = \\exp(-\\frac{x'^2 + \\gamma^2y'^2}{2\\sigma^2})\\exp(i(2\\pi\\frac{x'}{\\lambda} + \\psi))
98+
```
99+
100+
where ``x' = x\\cos\\theta + y\\sin\\theta`` and ``y' = -x\\sin\\theta + y\\cos\\theta``.
101+
102+
# References
103+
104+
- [1] [Wikipedia: Gabor filter](https://en.wikipedia.org/wiki/Gabor_filter)
105+
- [2] [Wikipedia: Gabor transformation](https://en.wikipedia.org/wiki/Gabor_transform)
106+
- [3] [Wikipedia: Gabor atom](https://en.wikipedia.org/wiki/Gabor_atom)
107+
- [4] [Gabor filter for image processing and computer vision](http://matlabserver.cs.rug.nl/edgedetectionweb/web/edgedetection_params.html)
108+
"""
109+
struct Gabor{T<:Complex, TP<:Real, R<:AbstractUnitRange} <: AbstractMatrix{T}
110+
ax::Tuple{R,R}
111+
λ::TP
112+
θ::TP
113+
ψ::TP
114+
σ::TP
115+
γ::TP
116+
117+
# cache values
118+
sc::Tuple{TP, TP} # sincos(θ)
119+
λ_scaled::TP # 2π/λ
120+
function Gabor{T,TP,R}(ax::Tuple{R,R}, λ::TP, θ::TP, ψ::TP, σ::TP, γ::TP) where {T,TP,R}
121+
λ > 0 || throw(ArgumentError("`λ` should be positive: "))
122+
new{T,TP,R}(ax, λ, θ, ψ, σ, γ, sincos(θ), 2π/λ)
123+
end
124+
end
125+
function Gabor(size_or_axes::Tuple; wavelength, orientation, kwargs...)
126+
Gabor(size_or_axes, wavelength, orientation; kwargs...)
127+
end
128+
function Gabor(
129+
size_or_axes::Tuple,
130+
wavelength::Real,
131+
orientation::Real;
132+
bandwidth::Real=1.0f0,
133+
phase_offset::Real=0.0f0,
134+
aspect_ratio::Real=0.5f0,
135+
)
136+
bandwidth > 0 || throw(ArgumentError("`bandwidth` should be positive: $bandwidth"))
137+
aspect_ratio > 0 || throw(ArgumentError("`aspect_ratio` should be positive: $aspect_ratio"))
138+
wavelength >= 2 || @warn "`wavelength` should be equal to or greater than 2" wavelength
139+
# we still follow the math symbols in the implementation
140+
λ, γ, ψ = wavelength, aspect_ratio, phase_offset
141+
# for orientation: Julia follow column-major order, thus here we compensate the orientation by π/2
142+
θ = convert(float(typeof(orientation)), mod2pi(orientation+π/2))
143+
# follows reference [4]
144+
σ = convert(float(typeof(λ)), (λ/π)*sqrt(0.5log(2)) * (2^bandwidth+1)/(2^bandwidth-1))
145+
146+
params = float.(promote(λ, θ, ψ, σ, γ))
147+
T = typeof(params[1])
148+
149+
ax = _to_axes(size_or_axes)
150+
all(map(length, ax) .> 0) || throw(ArgumentError("Kernel size should be positive: $size_or_axes"))
151+
if any(r->5λ > length(r), ax)
152+
expected_size = @. 5λ * length(ax)
153+
@warn "for wavelength `λ=` the expected kernel size is expected to be larger than $expected_size."
154+
end
155+
156+
Gabor{Complex{T}, T, typeof(first(ax))}(ax, params...)
157+
end
158+
159+
@inline Base.size(kern::Gabor) = map(length, kern.ax)
160+
@inline Base.axes(kern::Gabor) = kern.ax
161+
@inline function Base.getindex(kern::Gabor, x::Int, y::Int)
162+
ψ, σ, γ = kern.ψ, kern.σ, kern.γ
163+
s, c = kern.sc # sincos(θ)
164+
λ_scaled = kern.λ_scaled # 2π/λ
165+
166+
xr = x*c + y*s
167+
yr = -x*s + y*c
168+
yr = γ * yr
169+
return exp(-(xr^2 + yr^2)/(2σ^2)) * cis(xr*λ_scaled + ψ)
170+
end
171+
172+
# Utils
173+
174+
function _to_axes(sz::Dims)
175+
map(sz) do n
176+
r=n÷2
177+
isodd(n) ? UnitRange(-r, n-r-1) : UnitRange(-r+1, n-r)
178+
end
179+
end
180+
_to_axes(ax::NTuple{N, T}) where {N, T<:AbstractUnitRange} = ax
181+
182+
end

src/kernel.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dimensionality. The following kernels are supported:
1111
- `DoG` (Difference-of-Gaussian)
1212
- `LoG` (Laplacian-of-Gaussian)
1313
- `Laplacian`
14-
- `gabor`
14+
- `Gabor`
1515
- `moffat`
1616
1717
See also: [`KernelFactors`](@ref).
@@ -23,6 +23,9 @@ using ..ImageFiltering
2323
using ..ImageFiltering.KernelFactors
2424
import ..ImageFiltering: _reshape, IdentityUnitRange
2525

26+
include("gaborkernels.jl")
27+
using .GaborKernels
28+
2629
# We would like to do `using ..ImageFiltering.imgradients` so that that
2730
# Documenter.jl (the documentation system) can parse a reference such as `See
2831
# also: [`ImageFiltering.imgradients`](@ref)`. However, imgradients is not yet

0 commit comments

Comments
 (0)