Skip to content

Commit ec3d043

Browse files
authored
implement LBP rotation-invariant version (#6)
1 parent 3713921 commit ec3d043

File tree

10 files changed

+142
-4
lines changed

10 files changed

+142
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
*.jl.mem
44
Manifest.toml
55
/docs/build/
6+
/benchmark/tune.json

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ version = "0.1.0"
55

66
[deps]
77
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
8+
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
89
TiledIteration = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
910

1011
[compat]
1112
ImageCore = "0.8, 0.9"
13+
StaticArrays = "0.12, 1"
1214
TiledIteration = "0.2, 0.3"
1315
julia = "1"
1416

benchmark/benchmarks.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ tst_types = (N0f8, Float32, Gray{N0f8}, Gray{Float32})
1616

1717
const SUITE = BenchmarkGroup()
1818

19-
alg_list = (( "Original", lbp_original),)
19+
alg_list = (( "Original", lbp_original),
20+
( "Rotation-Invariant", lbp_ri),
21+
)
2022

2123
function add_algorithm_benchmark!(suite, img, alg_name, alg;
2224
tst_sizes, tst_types)

src/LocalBinaryPatterns.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ module LocalBinaryPatterns
22

33
using TiledIteration: EdgeIterator
44
using ImageCore
5+
using ImageCore.OffsetArrays
6+
using StaticArrays
57

6-
export lbp_original, lbp_original!
8+
export
9+
lbp_original, lbp_original!,
10+
lbp_ri, lbp_ri!
711

812
include("lbp_original.jl")
13+
include("lbp_ri.jl")
14+
include("utils.jl")
15+
include("compat.jl")
916

1017
end

src/compat.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
if VERSION < v"1.5"
2+
# https://github.com/JuliaLang/julia/blob/70cc57cb36d839afe6ce56ea48ff6ed01bc262c4/base/int.jl#L524-L551
3+
function bitrotate(x::T, k::Integer) where {T <: Integer}
4+
(x << ((sizeof(T) << 3 - 1) & k)) | (x >>> ((sizeof(T) << 3 - 1) & -k))
5+
end
6+
end

src/lbp_original.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
lbp_original(X)
33
lbp_original!(out, X)
44
5-
Compute the local binary pattern, the original version [1], of gray image using 3x3
5+
Compute the local binary pattern, the original version, of gray image using 3x3
66
neighborhood matrix.
77
88
```text
@@ -12,6 +12,8 @@ neighborhood matrix.
1212
2 1 7 1 0 1 4 16 128 0 0 128
1313
```
1414
15+
# Examples
16+
1517
```jldoctest; setup=:(using LocalBinaryPatterns)
1618
julia> X = [6 7 9; 5 6 3; 2 1 7]
1719
3×3 $(Matrix{Int}):
@@ -32,7 +34,7 @@ julia> lbp_original(X)
3234
- [2] T. Ojala, M. Pietikäinen, and T. Mäenpää, “A Generalized Local Binary Pattern Operator for Multiresolution Gray Scale and Rotation Invariant Texture Classification,” in _Advances in Pattern Recognition — ICAPR 2001, vol. 2013, S. Singh, N. Murshed, and W. Kropatsch, Eds. Berlin, Heidelberg: Springer Berlin Heidelberg_, 2001, pp. 399–408. doi: 10.1007/3-540-44732-6_41.
3335
"""
3436
lbp_original(X::AbstractArray) = lbp_original!(similar(X, UInt8), X)
35-
function lbp_original!(out, X::AbstractMatrix{T}) where T<:Union{Real, Gray}
37+
function lbp_original!(out, X::AbstractMatrix{T}) where T<:Union{Real,Gray}
3638
# nearest interpolation, 3x3 neighborhood
3739

3840
# The original version [1] uses clockwise order; here we use anti-clockwise order

src/lbp_ri.jl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
lbp_ri(X)
3+
lbp_ri!(out, X)
4+
5+
Compute the local binary pattern, the rotation invariant version, of gray image using 3x3
6+
neighborhood matrix.
7+
8+
```text
9+
3x3 block center-thresholded weights multiplied by weights sum rotation invariant encoding
10+
6 7 9 1 1 1 1 8 32 1 8 32
11+
5 6 3 ==> 0 x 0 ==> 2 x 64 ==> 0 x 0 ==> 169 ==> 53
12+
2 1 7 1 0 1 4 16 128 0 0 128
13+
```
14+
15+
The rotation encoding is to map all elements in the bitrotation equivalent class to the
16+
minimal value of this class. For example, `0b11010000` and `0b01000011` belongs to the same
17+
class because `bitrotate(0b01000011, -2) == 0b11010000`, thus both values are mapped to
18+
`0b00001101`, which is `minimum(bitrotate.(0b11010000, 0:7))`.
19+
20+
# Examples
21+
22+
```jldoctest; setup=:(using LocalBinaryPatterns)
23+
julia> X = [6 7 9; 5 6 3; 2 1 7]
24+
3×3 $(Matrix{Int}):
25+
6 7 9
26+
5 6 3
27+
2 1 7
28+
29+
julia> lbp_ri(X)
30+
3×3 $(Matrix{UInt8}):
31+
0x03 0x01 0x00
32+
0x0d 0x35 0x1b
33+
0x05 0x5b 0x00
34+
```
35+
36+
# References
37+
38+
- [1] Pietikäinen, Matti, Timo Ojala, and Zelin Xu. "Rotation-invariant texture classification using feature distributions." _Pattern recognition_ 33.1 (2000): 43-52.
39+
- [2] T. Ojala, M. Pietikainen, and T. Maenpaa, “Multiresolution gray-scale and rotation invariant texture classification with local binary patterns,” _IEEE Trans. Pattern Anal. Machine Intell._, vol. 24, no. 7, pp. 971–987, Jul. 2002, doi: 10.1109/TPAMI.2002.1017623.
40+
"""
41+
lbp_ri(X::AbstractArray) = lbp_ri!(similar(X, UInt8), X)
42+
43+
function lbp_ri!(out, X::AbstractMatrix{T}) where T<:Union{Real, Gray}
44+
# nearest interpolation, 3x3 neighborhood, rotation invariant
45+
encoding_table = build_circular_invariant_encoding_table(UInt8)
46+
lbp_original!(out, X)
47+
@. out = encoding_table[out + 1]
48+
return out
49+
end

src/utils.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This implements the solver for Eq. (8) in the Ojala 2002 as lookup table.
2+
# Computing the encoding table using naive implementation is time-consuming,
3+
# since it's read-only, we simply cache the encoding table.
4+
const _circular_invariant_encoding_tables = Dict()
5+
const SupportedEncodingTypes = Union{UInt8, UInt16}
6+
function build_circular_invariant_encoding_table(::Type{T}) where T<:SupportedEncodingTypes
7+
d = _circular_invariant_encoding_tables
8+
haskey(d, T) && return d[T]
9+
d[T] = _freeze(T, _build_inverse_table(_bitrotate_quotation_space(T)))
10+
return d[T]
11+
end
12+
13+
# Mathematically, this is the quotation space under circular bitshift of the N-bits binary
14+
# pattern space. For instance, 0b00001101 and 0b01000011 belong to the same equivalance
15+
# class.
16+
# TODO(johnnychen94): maybe support UInt32 and beyond by providing a more efficient implementation
17+
function _bitrotate_quotation_space(::Type{T}) where T<:SupportedEncodingTypes
18+
s = Vector{T}[]
19+
for x in typemin(T):typemax(T)
20+
# without the following skipping mechanism actually runs faster
21+
# any(c->x in c, s) && continue
22+
push!(s, sort!(bitrotate.(x, 0:8sizeof(T)-1)))
23+
end
24+
Dict(minimum(c) => c for c in unique!(s))
25+
end
26+
function _build_inverse_table(d::Dict{T,<:AbstractVector{T}}) where {T}
27+
id = Vector{T}(undef, typemax(T)-typemin(T)+1)
28+
for k in keys(d)
29+
for q in d[k]
30+
id[q+1] = k
31+
end
32+
end
33+
return id
34+
end
35+
_freeze(::Type{T}, v::Vector) where T = SVector{typemax(T)-typemin(T)+1}(v)

test/lbp_ri.jl

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
@testset "lbp_ri" begin
2+
# reference result comes from [1]
3+
# - [1] T. Ojala, M. Pietikäinen, and D. Harwood, “A comparative study of texture measures with classification based on featured distributions,” _Pattern Recognition_, vol. 29, no. 1, pp. 51–59, Jan. 1996, doi: 10.1016/0031-3203(95)00067-4.
4+
X = [6 7 9; 5 6 3; 2 1 7]
5+
ref_out = [3 1 0; 13 53 27; 5 91 0]
6+
7+
out = lbp_ri(X)
8+
@test eltype(out) == UInt8
9+
@test size(out) == (3, 3)
10+
@test out == ref_out
11+
12+
fill!(out, 0)
13+
lbp_ri!(out, X)
14+
@test out == ref_out
15+
16+
# intensity-invariant
17+
@test lbp_ri(X./255) == out
18+
# Gray inputs
19+
@test lbp_ri(Gray.(X./255)) == out
20+
21+
# LBP is only defined for scalar values
22+
@test_throws MethodError lbp_ri(RGB.(Gray.(X./255)))
23+
24+
# not yet ready for N-dimensional array (although it's doable)
25+
@test_throws MethodError lbp_ri(rand(3, 3, 3))
26+
27+
@testset "OffsetArrays" begin
28+
Xo = OffsetArray(X, -1, -1)
29+
out = lbp_ri(Xo)
30+
@test axes(out) == axes(Xo)
31+
@test OffsetArrays.no_offset_view(out) == ref_out
32+
end
33+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ using Aqua, Documenter
1313
end
1414

1515
include("lbp_original.jl")
16+
include("lbp_ri.jl")
1617

1718
end

0 commit comments

Comments
 (0)