Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion benchmark/benchmarks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ tst_types = (N0f8, Float32, Gray{N0f8}, Gray{Float32})
const SUITE = BenchmarkGroup()

alg_list = (( "Original", lbp_original),
( "Rotation-Invariant", lbp_rotation_invariant),
)

function add_algorithm_benchmark!(suite, img, alg_name, alg;
Expand Down
4 changes: 4 additions & 0 deletions src/compat.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ if VERSION < v"1.5"
(x << ((sizeof(T) << 3 - 1) & k)) | (x >>> ((sizeof(T) << 3 - 1) & -k))
end
end

if VERSION < v"1.1"
isnothing(x) = x === nothing
end
39 changes: 34 additions & 5 deletions src/lbp_original.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ neighborhood matrix.

# Parameters

- `rotation=false`: set `true` to generate patterns that are invariant to rotation [3].
The parameters control whether and the degree additional encoding passses are used to get
patterns that are more robust to certain changes, e.g., rotation. The following lists are
ordered as encoding order. For example, if `rotation=true` and `uniform_degree=2`, then
rotation encoding will be applied first.

- `rotation=false`: set `true` to generate patterns that are invariant to rotation [3]. For
example, pattern `0b00001101` is equivalent to `0b01000011` when `rotation=true`.
- `uniform_degree`: the threshold number of pattern uniform degree. From [2] a typical
choice is `2`.If it is `nothing`(default value) then no uniform encoding is applied.

# Examples

Expand All @@ -29,6 +37,12 @@ julia> lbp_original(X; rotation=true)
0x03 0x01 0x00
0x0d 0x35 0x1b
0x05 0x5b 0x00

julia> lbp_original(X; uniform_degree=2)
3×3 $(Matrix{UInt8}):
0xc0 0x40 0x00
0x09 0x09 0x09
0x09 0x09 0x00
```

# Extended help
Expand All @@ -55,7 +69,19 @@ the same class because `bitrotate(0b01000011, -2) == 0b11010000`, thus both valu
mapped to `0b00001101`. See also Eq.(8) in [2].

For 3x3 neighborhood matrix, applying rotation-invariant encoding decreases the possible
number of binary patterns from 256 to 36.
number of binary patterns from ``256`` to ``36``.

## Uniform encoding

Authors of [2] states that certain local binary patterns are fundamental properties of
texture, providing the vast majority, sometimes over 90 percent, of all 3x3 patterns. Those
patterns are called "uniform" as they contain very few spatial transitions. Uniform degree
is an additional encoding pass that controls at what circumstances can we set the block to
miscellaneous class.

For example, if `uniform_degree=2`, then `0b00001101` will be encoded as `9` (type
miscellaneous) because it has ``3`` bit transitions, and `0b00001100` will be unchanged
because it only has ``2`` bit transitions.

# References

Expand All @@ -67,7 +93,8 @@ lbp_original(X::AbstractArray; kwargs...) = lbp_original!(similar(X, UInt8), X;
function lbp_original!(
out,
X::AbstractMatrix{T};
rotation=false
rotation::Bool=false,
uniform_degree::Union{Nothing,Int}=nothing,
) where T<:Union{Real,Gray}
# nearest interpolation, 3x3 neighborhood

Expand Down Expand Up @@ -104,8 +131,10 @@ function lbp_original!(
out[I] = rst
end

if rotation
encoding_table = build_rotation_invariant_encoding_table(UInt8)
# The building is cached and chained(if there are multiple encoding passes) thus the
# cost is decreased to one brocasting to `getindex` and `setindex!`.
encoding_table = build_LBP_encoding_table(UInt8; rotation=rotation, uniform_degree=uniform_degree)
if !isnothing(encoding_table)
@. out = encoding_table[out + 1]
end

Expand Down
51 changes: 38 additions & 13 deletions src/utils.jl
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
# This implements the solver for Eq. (8) in the Ojala 2002 as lookup table.
# Computing the encoding table using naive implementation is time-consuming,
# since it's read-only, we simply cache the encoding table.
const _rotation_invariant_encoding_tables = Dict()
const SupportedEncodingTypes = Union{UInt8, UInt16}
function build_rotation_invariant_encoding_table(::Type{T}) where T<:SupportedEncodingTypes
d = _rotation_invariant_encoding_tables
haskey(d, T) && return d[T]
d[T] = _freeze(T, _build_inverse_table(_bitrotate_quotation_space(T)))
return d[T]
const _LBP_encoding_table = Dict()
function build_LBP_encoding_table(::Type{T};
rotation::Bool,
uniform_degree::Union{Nothing,Int}=nothing
) where T<:SupportedEncodingTypes

d = _LBP_encoding_table
p = (T, rotation, uniform_degree)
haskey(d, p) && return d[p]

identity_lookup = identity.(typemin(T):typemax(T))
rot_lookup = rotation ? _build_inverse_table(_bitrotate_quotation_space(T)) : identity_lookup
uniform_lookup = !isnothing(uniform_degree) ? _uniform_encoding_table(T, uniform_degree) : identity_lookup

# Chaining multiple encoding passes into one lookup table so that we can move as
# much computation to warm-up phase as we can.
lookup = d[p] = _freeze(T, uniform_lookup[rot_lookup.+1])
return lookup == identity_lookup ? nothing : lookup
end

# Mathematically, this is the quotation space under circular bitshift of the N-bits binary
# pattern space. For instance, 0b00001101 and 0b01000011 belong to the same equivalance
# class.
# TODO(johnnychen94): maybe support UInt32 and beyond by providing a more efficient implementation
function _bitrotate_quotation_space(::Type{T}) where T<:SupportedEncodingTypes
# Mathematically, this is the quotation space under circular bitshift of the N-bits binary
# pattern space. For instance, 0b00001101 and 0b01000011 belong to the same equivalance
# class.
# TODO(johnnychen94): maybe support UInt32 and beyond by providing a more efficient implementation

# This implements the solver for Eq. (8) in the Ojala 2002 as lookup table.
# Computing the encoding table using naive implementation is time-consuming,
# since it's read-only, we simply cache the encoding table.
s = Vector{T}[]
for x in typemin(T):typemax(T)
# without the following skipping mechanism actually runs faster
Expand All @@ -23,6 +36,18 @@ function _bitrotate_quotation_space(::Type{T}) where T<:SupportedEncodingTypes
end
Dict(minimum(c) => c for c in unique!(s))
end

function _uniform_encoding_table(::Type{T}, degree::Int) where T<:SupportedEncodingTypes
function _count_bit_transitions(x)
count_ones(x << 7 & typemax(typeof(x))) + count_ones(x ⊻ (x >> 1))
end
# Eq. (9) for Ojala 2002
map(typemin(T):typemax(T)) do x
n = _count_bit_transitions(x)
ifelse(n > degree, 8sizeof(T)+1, x)
end
end

function _build_inverse_table(d::Dict{T,<:AbstractVector{T}}) where {T}
id = Vector{T}(undef, typemax(T)-typemin(T)+1)
for k in keys(d)
Expand Down
20 changes: 20 additions & 0 deletions test/lbp_original.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,24 @@
@test size(out) == (3, 3)
@test out == ref_out
end

@testset "Uniform encoding" begin
X = [6 7 9; 5 6 3; 2 1 7]
ref_out = [192 64 0; 9 9 9; 9 9 0]

out = lbp_original(X; rotation=false, uniform_degree=2)
@test eltype(out) == UInt8
@test size(out) == (3, 3)
@test out == ref_out
end

@testset "Rotation Invariant, Uniform encoding" begin
X = [6 7 9; 5 6 3; 2 1 7]
ref_out = [3 1 0; 9 9 9; 9 9 0]

out = lbp_original(X; rotation=true, uniform_degree=2)
@test eltype(out) == UInt8
@test size(out) == (3, 3)
@test out == ref_out
end
end