Skip to content

Commit

Permalink
refactor and enhance AdaptiveThreshold method (#30)
Browse files Browse the repository at this point in the history
* move implementations to AdaptiveThreshold functor

* Rewrite AdaptiveThreshold with CartesianIndices to support n-D images

Changes:
* fixes a keywords constructor bug introduced by incorrect order
  it should be
  `AdaptiveThreshold(percentage, window_size)`
  instead of
  `AdaptiveThreshold(window_size, percentage)`
* support n-D images with the usage of CartesianIndices
* generalize the type annotation of percentage from `Int` to `Float64`
* move argument validation of AdaptiveThreshold to its inner constructor
* correct the result of recommend_size from `floor` to `round` -- the closest integer

* update and simplify the docstring

* fix syntax error and test utils.jl

* enhance the test codes and fix several bugs

* add CartetianIndex compat to Julia 1.0

* update docstring and deprecate `window_size` and `recommend_size`

Changes:

* roll back to the previous docstring style
* `window_size` is no longer a field of `AdaptiveThreshold`, instead
  it's a keyword argument in `binarize`
* there's no need to export `recommend_size` since it's automatically
  called if not specified

* add Argument section to docstring
  • Loading branch information
johnnychen94 authored Jul 24, 2019
1 parent ca471b8 commit 5ab30c7
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 75 deletions.
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ HistogramThresholding = "2c695a8d-9458-5d45-9878-1b8a99cf7853"
ImageContrastAdjustment = "f332f351-ec65-5f6a-b3d1-319c6670881a"
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900"
Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

Expand Down
5 changes: 5 additions & 0 deletions src/ImageBinarization.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module ImageBinarization

using Base.Iterators: repeated
using MappedArrays

using ImageContrastAdjustment
using ColorTypes
using ColorVectorSpace
Expand All @@ -8,6 +11,7 @@ using HistogramThresholding
using Polynomials
using Statistics
using ImageCore
using ImageCore: GenericGrayImage

# TODO: port BinarizationAPI to ImagesAPI
include("BinarizationAPI/BinarizationAPI.jl")
Expand All @@ -16,6 +20,7 @@ import .BinarizationAPI: AbstractImageBinarizationAlgorithm,

include("integral_image.jl")
include("util.jl")
include("compat.jl")

# Concrete binarization algorithms

Expand Down
157 changes: 97 additions & 60 deletions src/adaptive_threshold.jl
Original file line number Diff line number Diff line change
@@ -1,100 +1,137 @@
struct AdaptiveThreshold <: AbstractImageBinarizationAlgorithm
window_size::Int
percentage::Int
end
@doc raw"""
AdaptiveThreshold <: AbstractImageBinarizationAlgorithm
AdaptiveThreshold(; percentage = 15)
"""
```
binarize(AdaptiveThreshold(; percentage = 15, window_size = 32), img)
```
Uses a binarization threshold that varies across the image according
to background illumination.
binarize([T,] img, f::AdaptiveThreshold; [window_size])
binarize!([out,] img, f::AdaptiveThreshold; [window_size])
Binarize `img` using a threshold that varies according to background
illumination.
# Output
Returns the binarized image as an `Array{Gray{Bool},2}`.
Return the binarized image as an `Array{Gray{T}}` of size `size(img)`. If
`T` is not specified, it is inferred from `out` and `img`.
# Details
If the value of a pixel is ``t`` percent less than the average of an ``s
\\times s`` window of pixels centered around the pixel, then the pixel is set
If the value of a pixel is `t` percent less than the average of an ``s
\times s`` window of pixels centered around the pixel, then the pixel is set
to black, otherwise it is set to white.
A computationally efficient method for computing the average of an ``s \\times s``
neighbourhood is achieved by using an *integral image*.
A computationally efficient method for computing the average of an ``s
\times s`` neighbourhood is achieved by using an *integral image*
[`integral_image`](@ref).
This algorithm works particularly well on images that have distinct contrast
between background and foreground. See [1] for more details.
# Arguments
The function argument is described in more detail below.
## `img::AbstractArray`
The image that need to be binarized. The image is automatically converted
to `Gray` in order to construct the requisite graylevel histogram..
# Options
Various options for the parameters of this function are described in more detail
below.
Various options for the parameters of `AdaptiveThreshold`, `binarize` and
`binarize!` are described in more detail below.
## Choices for `percentage`
You can specify an integer for the `percentage` (denoted by ``t`` in the
publication) which must be between 0 and 100. If left unspecified a default
value of 15 is utilised.
You can specify an integer for the `percentage` (denoted by `t` in [1])
which must be between 0 and 100. Default: 15
## Choices for `window_size`
The argument `window_size` (denoted by ``s`` in the publication) specifies the
size of pixel's square neighbourhood which must be greater than zero. A
recommended size is the integer value which is closest to 1/8 of the average of
the width and height. You can use the convenience function
`recommend_size(::AbstractArray{T,2})` to obtain this suggested value.
If left unspecified, a default value of 32 is utilised.
#Example
The argument `window_size` (denoted by `s` in [1]) specifies the size of
pixel's square neighbourhood which must be greater than zero.
The default value is the integer which is closest to 1/8 of the average of
the width and height of `img`.
!!! info
`window_size` is a keyword argument in [`binarize`](@ref) and [`binarize!`](@ref)
# Examples
```julia
using TestImages
img = testimage("cameraman")
s = recommend_size(img)
binarize(AdaptiveThreshold(percentage = 15, window_size = s), img)
f = AdaptiveThreshold()
img₀₁_1 = binarize(img, f)
img₀₁_2 = binarize(img, f, window_size=16)
```
See also [`binarize!`](@ref) for in-place operation.
# References
1. Bradley, D. (2007). Adaptive Thresholding using Integral Image. *Journal of Graphic Tools*, 12(2), pp.13-21. [doi:10.1080/2151237x.2007.10129236](https://doi.org/10.1080/2151237x.2007.10129236)
[1] Bradley, D. (2007). Adaptive Thresholding using Integral Image. *Journal of Graphic Tools*, 12(2), pp.13-21. [doi:10.1080/2151237x.2007.10129236](https://doi.org/10.1080/2151237x.2007.10129236)
"""
function binarize(algorithm::AdaptiveThreshold, img::AbstractArray{T,2}) where T <: Colorant
binarize(algorithm, Gray.(img))
struct AdaptiveThreshold <: AbstractImageBinarizationAlgorithm
percentage::Float64

function AdaptiveThreshold(percentage)
(percentage < 0 || percentage > 100) && throw(ArgumentError("percentage should be ∈ [0, 100]."))
new(percentage)
end
end

function AdaptiveThreshold(; percentage::Real = 15, window_size=nothing)
if window_size === nothing
return AdaptiveThreshold(percentage)
else
# deprecate window_size
return AdaptiveThreshold(window_size, percentage)
end
end

function binarize(algorithm::AdaptiveThreshold, img::AbstractArray{T,2}) where T <: Gray
s = algorithm.window_size
t = algorithm.percentage
if s < 0 || t < 0 || t > 100
return error("Percentage and window_size must be greater than or equal to 0. Percentage must be less than or equal to 100.")
function (f::AdaptiveThreshold)(out::GenericGrayImage,
img::GenericGrayImage;
window_size::Union{Nothing, Integer}=nothing)
if window_size === nothing
window_size = default_AdaptiveThreshold_window_size(img)
end
img₀₁ = zeros(Gray{Bool}, axes(img))

window_size < 0 && throw(ArgumentError("window_size should be non-negative."))
size(out) == size(img) || throw(ArgumentError("out and img should have the same shape, instead they are $(size(out)) and $(size(img))"))

t = f.percentage
rₛ = CartesianIndex(Tuple(repeated(window_size ÷ 2, ndims(img))))
R = CartesianIndices(img)
p_first, p_last = first(R), last(R)

integral_img = integral_image(img)
h, w = size(img)
for i in CartesianIndices(img)
j,k = i.I
y1 = max(1,j - div(s,2))
y2 = min(h,j + div(s,2))
x1 = max(1,k - div(s,2))
x2 = min(w,k + div(s,2))
total = boxdiff(integral_img, y1:y2, x1:x2)
count = (y2 - y1) * (x2 - x1)
if img[i] * count <= total * ((100 - t) / 100)
img₀₁[i] = 0
@simd for p in R
p_tl = max(p_first, p - rₛ)
p_br = min(p_last, p + rₛ)
# can we pre-calculate this before the for-loop?
total = boxdiff(integral_img, p_tl, p_br)
count = length(_colon(p_tl, p_br))
if img[p] * count <= total * ((100 - t) / 100)
out[p] = 0
else
img₀₁[i] = 1
out[p] = 1
end
end
img₀₁
out
end

AdaptiveThreshold(; percentage::Int = 15, window_size::Int = 32) = AdaptiveThreshold(percentage, window_size)
# first do Color3 to Gray conversion
(f::AdaptiveThreshold)(out::GenericGrayImage, img::AbstractArray{<:Color3}, args...; kwargs...) =
f(out, of_eltype(Gray, img), args...; kwargs...)

"""
```
recommend_size(img)
```
Helper function for `AdaptiveThreshold` algorithm which returns an integer value
which is closest to 1/8 of the average of the width and height of the image.
default_AdaptiveThreshold_window_size(img)::Int
Estimate the appropriate `window_size` for [`AdaptiveThreshold`](@ref) algorithm using `round(mean(size(img))/8)`
"""
function recommend_size(img::AbstractArray{T,2}) where T
s = div(div(sum(size(img)),2),8)
end
default_AdaptiveThreshold_window_size(img::AbstractArray) = round(Int, mean(size(img)) / 8)
10 changes: 10 additions & 0 deletions src/compat.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
_colon(I, J)
`_colon(I, J)` works equivelently to `I:J`, it's used to backward support julia v"1.0".
"""
_colon(I, J) = I:J
if v"1.0" <= VERSION < v"1.1"
_colon(I::CartesianIndex{N}, J::CartesianIndex{N}) where N =
CartesianIndices(map((i,j) -> i:j, Tuple(I), Tuple(J)))
end
13 changes: 13 additions & 0 deletions src/deprecations.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
using Base: depwarn

# issue #23: swap argument order
# Deprecated in ImageBinarization v0.3
function binarize(f::AbstractImageBinarizationAlgorithm, img::AbstractArray, args...; kwargs...)
depwarn("binarize(alg, img) is deprecated, use binarize(img, alg) instead", :binarize)
binarize(img, f, args...; kwargs...)
end

# move window_size out of AdaptiveThreshold and unexport recommend_size
# Deprecated in ImageBinarization v0.3
function AdaptiveThreshold(window_size, percentage)
depwarn("deprecated: window_size is no longer used as an `AdaptiveThreshold` field, instead, it's a keyword argument of `binarize`. Please check `AdaptiveThreshold` for more details.", :AdaptiveThreshold)
AdaptiveThreshold(percentage)
end

function recommend_size(img)
depwarn("deprecated: `binarize` automatically calls `recommend_size` now, it will be unexported in the future. Please check `AdaptiveThreshold` for more details.", :recommend_size)
default_AdaptiveThreshold_window_size(img)
end
Binary file added test/References/AdaptiveThreshold_Color3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/References/AdaptiveThreshold_Gray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
91 changes: 78 additions & 13 deletions test/adaptive_threshold.jl
Original file line number Diff line number Diff line change
@@ -1,29 +1,94 @@

using ImageBinarization: default_AdaptiveThreshold_window_size
@testset "adaptive_threshold" begin
original_image = testimage("lena")
for T in (Gray{N0f8}, Gray{N0f16}, Gray{Float32}, Gray{Float64})
img = T.(original_image)
img₀₁ = binarize(AdaptiveThreshold(), img)
@info "Test: AdaptiveThreshold"

@testset "API" begin
img_gray = imresize(testimage("lena_gray_256"); ratio=0.25)
img = copy(img_gray)

# AdaptiveThreshold
@test AdaptiveThreshold() == AdaptiveThreshold(15)
@test AdaptiveThreshold(15) == AdaptiveThreshold(percentage=15)

# window_size non-positive integer
f = AdaptiveThreshold()
@test_throws ArgumentError binarize(img, f, window_size = -10)
@test_throws TypeError binarize(img, f, window_size = 32.5)
# percentage ∈ [0, 100]
@test_throws ArgumentError AdaptiveThreshold(-10)
@test_throws ArgumentError AdaptiveThreshold(150)

# binarize
f = AdaptiveThreshold(percentage=15)
binarized_img_1 = binarize(img, f)
@test img == img_gray # img unchanged
@test eltype(binarized_img_1) == Gray{N0f8}
@test binarize(img, f,
window_size=default_AdaptiveThreshold_window_size(img)) == binarized_img_1

# Check original image is unchanged.
@test img == T.(testimage("lena"))
binarized_img_2 = binarize(Gray{Bool}, img, f)
@test img == img_gray # img unchanged
@test eltype(binarized_img_2) == Gray{Bool}

binarized_img_3 = similar(img, Bool)
binarize!(binarized_img_3, img, f)
@test img == img_gray # img unchanged
@test eltype(binarized_img_3) == Bool

binarized_img_4 = copy(img_gray)
binarize!(binarized_img_4, f)
@test eltype(binarized_img_4) == Gray{N0f8}

@test binarized_img_1 == binarized_img_2
@test binarized_img_1 == binarized_img_3
@test binarized_img_1 == binarized_img_4
end

@testset "Types" begin
# Gray
img_gray = imresize(float64.(testimage("lena_gray_256")); ratio=0.25)
f = AdaptiveThreshold(percentage=15)

type_list = generate_test_types([Float32, N0f8], [Gray])
for T in type_list
img = T.(img_gray)
@test_reference "References/AdaptiveThreshold_Gray.png" Gray.(binarize(img, f))
end

# Color3
img_color = imresize(float64.(testimage("lena_color_256")); ratio=0.25)
f = AdaptiveThreshold(percentage=15)

type_list = generate_test_types([Float32, N0f8], [RGB, Lab])
for T in type_list
img = T.(img_gray)
@test_reference "References/AdaptiveThreshold_Color3.png" Gray.(binarize(img, f))
end
end

@testset "Numerical" begin
# Check that the image only has ones or zeros.
img = imresize(float64.(testimage("lena_gray_256")); ratio=0.25)
f = AdaptiveThreshold(percentage=15)
img₀₁ = binarize(img, f)
non_zeros = findall(x -> x != 0.0 && x != 1.0, img₀₁)
@test length(non_zeros) == 0

# Check type of binarized image.
@test typeof(img₀₁) == Array{Gray{Bool},2}

# Check that ones and zeros have been assigned to the correct side of the threshold.
maxval, maxpos = findmax(Gray.(img))
@test img₀₁[maxpos] == 1
minval, minpos = findmin(Gray.(img))
@test img₀₁[minpos] == 0
end

binarize(AdaptiveThreshold(percentage = 10), original_image)
binarize(AdaptiveThreshold(percentage = 10, window_size = 32), original_image)
binarize(AdaptiveThreshold(percentage = 10, window_size = recommend_size(original_image)), original_image)
@testset "Miscellaneous" begin
img = testimage("lena_gray_256")
@test default_AdaptiveThreshold_window_size(img) == 32

# deprecations
@test (@test_deprecated AdaptiveThreshold(32, 15)) == AdaptiveThreshold(15)
@test (@test_deprecated AdaptiveThreshold(window_size=32, percentage=15)) == AdaptiveThreshold(percentage=15)
@test (@test_deprecated recommend_size(img)) == ImageBinarization.default_AdaptiveThreshold_window_size(img)
end

end
4 changes: 2 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ include("testutils.jl")

@testset "ImageBinarization.jl" begin
include("util.jl")

# include("adaptive_threshold.jl")
include("adaptive_threshold.jl")
# include("balanced.jl")
# include("entropy.jl")
# include("intermodes.jl")
Expand Down

0 comments on commit 5ab30c7

Please sign in to comment.