-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor and enhance AdaptiveThreshold method (#30)
* 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
1 parent
ca471b8
commit 5ab30c7
Showing
9 changed files
with
206 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters