diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index cecde69..6cd85b9 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -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; diff --git a/src/compat.jl b/src/compat.jl index 12fd57a..7a451a9 100644 --- a/src/compat.jl +++ b/src/compat.jl @@ -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 diff --git a/src/lbp_original.jl b/src/lbp_original.jl index dd82f47..bd5ed22 100644 --- a/src/lbp_original.jl +++ b/src/lbp_original.jl @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/src/utils.jl b/src/utils.jl index 38ed93b..b13a556 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -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 @@ -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) diff --git a/test/lbp_original.jl b/test/lbp_original.jl index d480d56..99aa865 100644 --- a/test/lbp_original.jl +++ b/test/lbp_original.jl @@ -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