|
1 | 1 | module GaborKernels
|
2 | 2 |
|
3 |
| -export Gabor |
| 3 | +export Gabor, LogGabor, LogGaborComplex |
4 | 4 |
|
5 | 5 | """
|
6 | 6 | Gabor(size_or_axes, wavelength, orientation; kwargs...)
|
@@ -169,6 +169,147 @@ end
|
169 | 169 | return exp(-(xr^2 + yr^2)/(2σ^2)) * cis(xr*λ_scaled + ψ)
|
170 | 170 | end
|
171 | 171 |
|
| 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 | + |
172 | 313 | # Utils
|
173 | 314 |
|
174 | 315 | function _to_axes(sz::Dims)
|
|
0 commit comments