Skip to content

Commit 0031270

Browse files
authored
support up to 32 circular points (#12)
1 parent c554453 commit 0031270

File tree

10 files changed

+345
-137
lines changed

10 files changed

+345
-137
lines changed

docs/src/reference.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# API References
22

3-
```@autodocs
4-
Modules = [LocalBinaryPatterns]
3+
```@docs
4+
lbp_original
5+
```
6+
7+
## Internal utilities
8+
9+
```@docs
10+
LocalBinaryPatterns.rotation_encoding_table
11+
LocalBinaryPatterns.RotationEncodingTable
12+
LocalBinaryPatterns.uniform_encoding_table
513
```

src/LocalBinaryPatterns.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ using ImageCore
66
using ImageCore.OffsetArrays
77
using StaticArrays
88

9-
export
10-
lbp_original, lbp_original!
9+
export lbp_original
1110

1211
include("lbp_original.jl")
1312

13+
include("bitrotate_encoding.jl")
14+
include("uniform_encoding.jl")
1415
include("utils.jl")
1516
include("compat.jl")
1617

src/bitrotate_encoding.jl

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
const _RotationEncodingTables = Dict()
2+
3+
"""
4+
rotation_encoding_table(T, nbits)
5+
rotation_encoding_table(T, X::AbstractUnitRange)
6+
7+
Build the rotation-invariant encoding table. If global runtime cache exists, then reuse it.
8+
"""
9+
rotation_encoding_table(::Type{T}, nbits::Int) where T = rotation_encoding_table(T, 0:2^nbits-1)
10+
function rotation_encoding_table(::Type{T}, X::AbstractUnitRange) where T
11+
d = _RotationEncodingTables
12+
k = (T, minimum(X), maximum(X))
13+
haskey(d, k) && return d[k]
14+
lookup = d[k] = RotationEncodingTable{T}(X)
15+
return lookup
16+
end
17+
18+
"""
19+
RotationEncodingTable{T<:Unsigned}(X)
20+
21+
The lookup table for the quotient space constructed using `bitrotate` equivalance relation
22+
on bit representation of datatype `T`.
23+
24+
By definition, ``a`` is equivalant to ``b``(``a ~ b``) if there exists ``n`` such that
25+
`bitrotate(a, n) == b`.
26+
27+
This equivalance class gives a unique encoding map from ``x∈X`` to ``a``, where ``a`` is the
28+
minimal value of the equivalance class ``[x]``. For instance, the equivalance class of
29+
`0b10101001` (UInt8) is
30+
31+
```julia
32+
0b10101001 # 169
33+
0b01010011 # 83
34+
0b10100110 # 166
35+
0b01001101 # 77
36+
0b10011010 # 154
37+
0b00110101 # 53
38+
0b01101010 # 106
39+
0b11010100 # 212
40+
```
41+
42+
thus `f(169) == 0x35 == 53`.
43+
44+
```jldoctest
45+
julia> using LocalBinaryPatterns: RotationEncodingTable
46+
47+
julia> X = RotationEncodingTable{UInt8}(0:255);
48+
49+
julia> X[169] # 53
50+
0x35
51+
52+
julia> X = RotationEncodingTable{UInt8}(128:255);
53+
54+
julia> X[169] # 154
55+
0x9a
56+
```
57+
58+
The values in the lookup table is implemented in lazy cache manner so the real computation
59+
only happens once at the first retrieval.
60+
61+
This lookup table is used to build an efficient implementation of rotation-invariant local
62+
binary pattern [1].
63+
64+
# References
65+
66+
- [1] 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.
67+
"""
68+
struct RotationEncodingTable{T<:Unsigned,AT<:AbstractUnitRange,LAT} <: AbstractVector{T}
69+
X::AT
70+
Y::LAT
71+
end
72+
function RotationEncodingTable{T}(X::AbstractUnitRange) where T
73+
if !((maximum(X) <= typemax(T)) && (minimum(X) >= typemin(T)))
74+
throw(ArgumentError("range $(first(X)):$(last(X)) can't be uniquely represented by type $T."))
75+
end
76+
77+
if log2(length(X)) >= 26
78+
datasize = round(length(X)/1024/1024/1024, digits=3)
79+
@warn "$(datasize)GB data will be created."
80+
end
81+
82+
Y = Vector{Union{T,Missing}}(missing, length(X))
83+
RotationEncodingTable{T,typeof(X),typeof(Y)}(X, Y)
84+
end
85+
86+
@inline Base.axes(X::RotationEncodingTable) = (first(X.X):last(X.X), )
87+
@inline Base.size(X::RotationEncodingTable) = (length(X.X)-1)
88+
@inline function Base.getindex(X::RotationEncodingTable{T}, x::Int) where T
89+
i = x-first(axes(X, 1))+1
90+
v = X.Y[i]
91+
if ismissing(v)
92+
v = minimum_bitrotate(T(x), X.X)
93+
X.Y[i] = v
94+
end
95+
return T(v)
96+
end
97+
98+
function minimum_bitrotate(x::T, X::AbstractUnitRange) where T<:Unsigned
99+
function bit_rotate_project_to(x, n, X)
100+
v = bitrotate(x, n)
101+
return v in X ? v : x
102+
end
103+
minimum(bit_rotate_project_to(x, n, X) for n in 0:8sizeof(T)-1)
104+
end

src/lbp_original.jl

Lines changed: 94 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,13 @@
11
"""
2-
lbp_original(X; kwargs...)
3-
lbp_original(X, npoints, radius, interpolation=Linear(); kwargs...)
4-
lbp_original!(out, X, offsets; kwargs...)
2+
lbp_original(X; [rotation], [uniform_degree])
53
64
Compute the local binary pattern of gray image `X` using the original method.
75
86
# Arguments
97
108
- `X::AbstractMatrix`: the input image matrix. For colorful images, one can manually convert
11-
it to some monochrome space, e.g., `Gray` or the L-channel of `Lab`.
12-
13-
[2,4] proposes a generalized interpolation-based version using circular neighborhood matrix,
14-
it produces better result for `rotation=true` case but is usually much slower than the plain
15-
3x3 matrix version. The arguments for this version are:
16-
17-
- `npoints::Int`(4 ≤ npoints ≤ 8): the number of (uniform-spaced) neighborhood points.
18-
- `radius::Real`(radius ≥ 1.0): the radius of the circular.
19-
- `interpolation::Union{Degree, InterpolationType}=Linear()`: the interpolation method used
20-
to generate non-grid pixel value. In most cases, `Linear()` are good enough. One can also
21-
try other costly interpolation methods, e.g., `Cubic(Line(OnGrid()))`(also known as
22-
"bicubic"), `Lanczos()`. See also Interpolations.jl for more choices.
23-
24-
!!! note "neighborhood order differences"
25-
Different implementation might use different neighborhood orders; this will change the
26-
encoding result but will not change the overall distribution. For instance,
27-
`lbp_original(X)` differs from `lbp_original(X, 8, 1, Constant())` only by how `offsets`
28-
(see below) are ordered; the former uses column-major top-left to bottom-right 3x3 matrix
29-
order and the latter uses circular order.
30-
31-
Arguments for in-place version:
32-
33-
- `offsets::Tuple`: tuple of neighborhood matrix, e.g., `((0, 1), (0, -1), (1, 0), (-1, 0))`
34-
specifies the 4-neighborhood matrix. If `X isa Interpolations.AbstractInterpolation`
35-
holds, then the position values can be float numbers, e.g, `(0.7, 0.7)`.
9+
it to some monochrome space, e.g., `Gray`, the L-channel of `Lab`. One could also do
10+
channelwise LBP and then concatenate together.
3611
3712
# Parameters
3813
@@ -61,12 +36,6 @@ julia> lbp_original(X)
6136
0x68 0xa9 0x1b
6237
0x28 0x6b 0x00
6338
64-
julia> lbp_original(X, 4, 1) # 4-neighbor with circular radius 1
65-
3×3 $(Matrix{UInt8}):
66-
0x01 0x01 0x00
67-
0x03 0x02 0x0e
68-
0x02 0x07 0x00
69-
7039
julia> lbp_original(X; rotation=true)
7140
3×3 $(Matrix{UInt8}):
7241
0x03 0x01 0x00
@@ -128,21 +97,90 @@ unchanged because it only has ``2`` bit transitions.
12897
- [3] Pietikäinen, Matti, Timo Ojala, and Zelin Xu. "Rotation-invariant texture classification using feature distributions." _Pattern recognition_ 33.1 (2000): 43-52.
12998
- [4] 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.
13099
"""
131-
function lbp_original(X::AbstractArray; kwargs...)
100+
function lbp_original(X::AbstractArray; rotation::Bool=false, uniform_degree::Union{Nothing,Int}=nothing)
132101
# The original version [1] uses clockwise order; here we use anti-clockwise order
133102
# because Julia is column-major order. If we consider memory order differences then they
134103
# are equivalent.
135104
offsets = ((-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1))
136-
lbp_original!(similar(X, UInt8), X, offsets; kwargs...)
105+
lookups = _build_lbp_original_lookup(UInt8, 8; rotation=rotation, uniform_degree=uniform_degree)
106+
lbp_original!(similar(X, UInt8), X, offsets, lookups)
107+
end
108+
109+
"""
110+
lbp_original(X, npoints, radius, interpolation=Linear(); [rotation], [uniform_degree])
111+
112+
Compute the local binary pattern of gray image `X` using the interpolation-based original
113+
method with circular neighborhood matrix.
114+
115+
This produces better result for `rotation=true` case but is usually slower than the plain
116+
3x3 matrix version `lbp_original(X)`.
117+
118+
# Arguments
119+
120+
- `npoints::Int`(4 ≤ npoints ≤ 32): the number of (uniform-spaced) neighborhood points. It
121+
is recommended to use one of {4, 8, 12, 16, 24}.
122+
- `radius::Real`(radius ≥ 1.0): the radius of the circular. Larger radius computes the
123+
pattern of a larger local window/block.
124+
- `interpolation::Union{Degree, InterpolationType}=Linear()`: the interpolation method used
125+
to generate non-grid pixel value. In most cases, `Linear()` are good enough. One can
126+
also try other costly interpolation methods, e.g., `Cubic(Line(OnGrid()))`(also known as
127+
"bicubic"), `Lanczos()`. See also Interpolations.jl for more choices.
128+
129+
!!! note "neighborhood order differences"
130+
Different implementation might use different neighborhood orders; this will change the
131+
encoding result but will not change the overall distribution. For instance,
132+
`lbp_original(X)` differs from `lbp_original(X, 8, 1, Constant())` only by how
133+
`offsets` (see below) are ordered; the former uses column-major top-left to
134+
bottom-right 3x3 matrix order and the latter uses circular order.
135+
136+
# Examples
137+
138+
```jldoctest; setup=:(using LocalBinaryPatterns)
139+
julia> X = [6 7 9; 5 6 3; 2 1 7]
140+
3×3 $(Matrix{Int}):
141+
6 7 9
142+
5 6 3
143+
2 1 7
144+
145+
julia> lbp_original(X, 4, 1) # 4-neighbor with circular radius 1
146+
3×3 $(Matrix{UInt32}):
147+
0x00000001 0x00000001 0x00000000
148+
0x00000003 0x00000002 0x0000000e
149+
0x00000002 0x00000007 0x00000000
150+
151+
julia> lbp_original(X, 4, 1; rotation=true)
152+
3×3 $(Matrix{UInt32}):
153+
0x00000001 0x00000001 0x00000000
154+
0x00000003 0x00000001 0x00000007
155+
0x00000001 0x00000007 0x00000000
156+
```
157+
158+
"""
159+
function lbp_original(
160+
X::AbstractArray, npoints, radius, interpolation=Linear();
161+
rotation::Bool=false, uniform_degree::Union{Nothing,Int}=nothing)
162+
interpolation = wrap_BSpline(interpolation)
163+
offsets = _circular_neighbor_offsets(npoints, radius)
164+
if interpolation == BSpline(Constant())
165+
offsets = map(offsets) do o
166+
round.(Int, o, RoundToZero)
167+
end
168+
itp = X
169+
else
170+
itp = interpolate(X, interpolation)
171+
end
172+
# For the sake of type-stability, hardcode to UInt32 here.
173+
lookups = _build_lbp_original_lookup(UInt32, npoints, rotation=rotation, uniform_degree=uniform_degree)
174+
lbp_original!(similar(X, UInt32), itp, offsets, lookups)
137175
end
176+
138177
function lbp_original!(
139178
out,
140179
X::AbstractMatrix{T},
141-
offsets::Tuple;
142-
rotation::Bool=false,
143-
uniform_degree::Union{Nothing,Int}=nothing,
180+
offsets::Tuple,
181+
lookups::Vector
144182
) where T<:Union{Real,Gray}
145-
length(offsets) > 8 && throw(ArgumentError("length(offsets) >= 8 is not supported."))
183+
length(offsets) > 32 && throw(ArgumentError("length(offsets)=$(length(offsets)) is expected to be no larger than 32."))
146184
outerR = CartesianIndices(X)
147185
r = CartesianIndex(
148186
ceil(Int, maximum(abs.(extrema(first.(offsets))))),
@@ -175,28 +213,26 @@ function lbp_original!(
175213
out[I] = rst
176214
end
177215

178-
# The building is cached and chained(if there are multiple encoding passes) thus the
179-
# cost is decreased to one brocasting to `getindex` and `setindex!`.
180-
encoding_table = build_LBP_encoding_table(UInt8; rotation=rotation, uniform_degree=uniform_degree)
181-
if !isnothing(encoding_table)
182-
@. out = encoding_table[out + 1]
216+
for F in lookups
217+
@. out = F[out]
183218
end
184219

185220
return out
186221
end
187222

188-
function lbp_original(X::AbstractArray, npoints, radius, interpolation=Linear(); kwargs...)
189-
# TODO(johnnychen94): support npoints=24 as [2, 4] indicate
190-
4<= npoints <= 8 || throw(ArgumentError("currently only support 4<= npoints <=8, instead it is $(npoints)."))
191-
interpolation = wrap_BSpline(interpolation)
192-
offsets = _circular_neighbor_offsets(npoints, radius)
193-
if interpolation == BSpline(Constant())
194-
offsets = map(offsets) do o
195-
round.(Int, o, RoundToZero)
196-
end
197-
itp = X
198-
else
199-
itp = interpolate(X, interpolation)
223+
function _build_lbp_original_lookup(
224+
::Type{T}, nbits;
225+
rotation::Bool,
226+
uniform_degree::Union{Nothing,Int}
227+
) where T <: Unsigned
228+
# TODO(johnnychen94): fuse these operation into one broadcasting could potentially give
229+
# better performance.
230+
lookups = []
231+
if rotation
232+
push!(lookups, rotation_encoding_table(T, nbits))
233+
end
234+
if !isnothing(uniform_degree)
235+
push!(lookups, uniform_encoding_table(T, nbits, uniform_degree))
200236
end
201-
lbp_original!(similar(X, UInt8), itp, offsets; kwargs...)
237+
return lookups
202238
end

src/uniform_encoding.jl

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const _UniformEncodingTables = Dict()
2+
"""
3+
uniform_encoding_table(T, X::AbstractUnitRange, degree::Int)
4+
5+
Get the encoding table of `X` using uniform degree filter.
6+
7+
A uniform degree filter is defined as `f(x) = U(x) > degree ? P+1 : x`, where `P` is number
8+
of bits of `x`, and `U(x)` is the circular bit transition of `x` with bit representation of
9+
datatype `T`.
10+
11+
For example, `0b0000001` has ``2`` bit transitions thus is unchanged, while `0b00000101` has
12+
``4`` bit transitions. The transition is count in circular sense, i.e., the highest and
13+
lowest bits are also compared.
14+
15+
```jldoctest
16+
julia> using LocalBinaryPatterns: uniform_encoding_table
17+
18+
julia> X = uniform_encoding_table(UInt8, 0:255, 2);
19+
20+
julia> X[0b0000001]
21+
0x01
22+
23+
julia> X[0b0000101] # miscellaneous
24+
0x09
25+
```
26+
27+
This function is used to distinguish local binary patterns from texture patterns and
28+
miscellaneous patterns. See [1] for more information.
29+
30+
!!! info "Runtime caches"
31+
For better performance, this function uses a runtime cache to store the lookup and
32+
shared among all callers. The result is expected to be used in read-only mode.
33+
34+
# References
35+
36+
- [1] 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.
37+
"""
38+
function uniform_encoding_table(::Type{T}, X::AbstractUnitRange, degree::Int) where T
39+
d = _UniformEncodingTables
40+
k = (T, degree, minimum(X), maximum(X))
41+
haskey(d, k) && return d[k]
42+
lookup = d[k] = _uniform_encoding_table(T, X, degree)
43+
return lookup
44+
end
45+
uniform_encoding_table(::Type{T}, nbits::Int, degree::Int) where T = uniform_encoding_table(T, 0:2^nbits-1, degree)
46+
47+
function _uniform_encoding_table(::Type{T}, X::AbstractUnitRange, degree::Int) where T<:Unsigned
48+
if !((maximum(X) <= typemax(T)) && (minimum(X) >= typemin(T)))
49+
throw(ArgumentError("range $(first(X)):$(last(X)) can't be uniquely represented by type $T."))
50+
end
51+
52+
function _count_bit_transitions(x)
53+
# Eq. (10) in Ojala 2002
54+
count_ones(x << 7 & typemax(typeof(x))) + count_ones(x (x >> 1))
55+
end
56+
# Eq. (9) for Ojala 2002
57+
lookup = map(X) do x
58+
n = _count_bit_transitions(T(x))
59+
T(ifelse(n > degree, 8sizeof(T)+1, x))
60+
end
61+
OffsetVector(lookup, X)
62+
end

0 commit comments

Comments
 (0)