Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor Niblack using the functor API #42

Merged
merged 2 commits into from
Jul 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/deprecations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ 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

# move window_size out of Niblack
# Deprecated in ImageBinarization v0.3
function Niblack(window_size, bias)
depwarn("deprecated: window_size is no longer used as an `Niblac` field, instead, it's a keyword argument of `binarize`. Please check `Niblack` for more details.", :Niblack)
Niblack(bias)
end
86 changes: 56 additions & 30 deletions src/niblack.jl
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
struct Niblack <: AbstractImageBinarizationAlgorithm
window_size::Int
bias::Float32
end
@doc raw"""
Niblack <: AbstractImageBinarizationAlgorithm
Niblack(; bias = 0.2)

"""
```
binarize(Niblack(; window_size = 7, bias = 0.2), img)
```
binarize([T,] img, f::Niblack; [window_size])
binarize!([out,] img, f::Niblack; [window_size])

Applies Niblack adaptive thresholding [1] under the assumption that the input
image is textual.

# 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

Expand All @@ -24,16 +22,16 @@ intensities of neighboring pixels in a window around it. This threshold is given
by

```math
T(x,y) = m(x,y) + k \\cdot s(x,y),
T(x,y) = m(x,y) + k \cdot s(x,y),
```

where ``k`` is a user-defined parameter weighting the influence of the standard
deviation on the value of ``T``.

Note that Niblack's algorithm is highly sensitive to variations in the gray
values of background pixels, which often exceed local thresholds and appear as
artifacts in the binarized image. The Sauvola algorithm included in this package
implements an attempt to address this issue [2].
artifacts in the binarized image. The [`Sauvola`](@ref) algorithm included in
this package implements an attempt to address this issue [2].

# Arguments

Expand All @@ -42,15 +40,22 @@ implements an attempt to address this issue [2].
An image which is binarized according to a per-pixel adaptive
threshold into background (0) and foreground (1) pixel values.

## `window_size` (denoted by ``w`` in the publication)
## `bias::Real` (denoted by ``k`` in the publication)

A user-defined biasing parameter on threshold. This can take negative values.
Larger `bias` encourages more black pixels in the output.

## `window_size::Integer` (denoted by ``w`` in the publication)

The threshold for each pixel is a function of the distribution of the intensities
of all neighboring pixels in a square window around it. The side length of this
window is ``2w + 1``, with the target pixel in the center position.

## `bias` (denoted by ``k`` in the publication)
If not specified, `window_size` is `7`.

A user-defined biasing parameter. This can take negative values.
!!! info

`window_size` is a keyword argument in [`binarize`](@ref) and [`binarize!`](@ref)

# Example

Expand All @@ -60,38 +65,59 @@ Binarize the "cameraman" image in the `TestImages` package.
using TestImages, ImageBinarization

img = testimage("cameraman")
img_binary = binarize(Niblack(window_size = 9, bias = 0.2), img)
img₀₁ = binarize(img, Niblack())
```

# References

1. Wayne Niblack (1986). *An Introduction to Image Processing*. Prentice-Hall, Englewood Cliffs, NJ: 115-16.
2. J. Sauvola and M. Pietikäinen (2000). "Adaptive document image binarization". *Pattern Recognition* 33 (2): 225-236. [doi:10.1016/S0031-3203(99)00055-2](https://doi.org/10.1016/S0031-3203(99)00055-2)
[1] Wayne Niblack (1986). *An Introduction to Image Processing*. Prentice-Hall, Englewood Cliffs, NJ: 115-16.
[2] J. Sauvola and M. Pietikäinen (2000). "Adaptive document image binarization". *Pattern Recognition* 33 (2): 225-236. [doi:10.1016/S0031-3203(99)00055-2](https://doi.org/10.1016/S0031-3203(99)00055-2)
"""
function binarize(algorithm::Niblack, img::AbstractArray{T,2}) where T <: Colorant
binarize(algorithm, Gray.(img))
struct Niblack <: AbstractImageBinarizationAlgorithm
bias::Float32
end

function binarize(algorithm::Niblack, img::AbstractArray{T,2}) where T <: AbstractGray
w = algorithm.window_size
k = algorithm.bias
img₀₁ = zeros(Gray{Bool}, axes(img))
function Niblack(; window_size = nothing, bias::Real = 0.2)
if window_size !== nothing
# deprecate window_size
return Niblack(window_size, bias)
else
return Niblack(bias)
end
end

function (f::Niblack)(out::GenericGrayImage,
img::GenericGrayImage;
window_size::Union{Integer, Nothing} = nothing)

if window_size === nothing
window_size = default_Niblack_window_size(img)
end

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))"))

k = f.bias
img_raw = channelview(img)
I = integral_image(img_raw)
I² = integral_image(img_raw.^2)

function threshold(pixel::CartesianIndex{2})
row₀, col₀, row₁, col₁ = get_window_bounds(img, pixel, w)
row₀, col₀, row₁, col₁ = get_window_bounds(img, pixel, window_size)
m = μ_in_window(I, row₀, col₀, row₁, col₁)
s = σ_in_window(I², m, row₀, col₀, row₁, col₁)
return m + (k * s)
end

for pixel in CartesianIndices(img)
img₀₁[pixel] = img[pixel] <= threshold(pixel) ? 0 : 1
@simd for pixel in CartesianIndices(img)
out[pixel] = img[pixel] <= threshold(pixel) ? 0 : 1
end

return img₀₁
end

Niblack(; window_size::Int = 7, bias::Real = 0.2) = Niblack(window_size, bias)
(f::Niblack)(out::GenericGrayImage, img::AbstractArray{<:Color3},
args...; kwargs...) =
f(out, of_eltype(Gray, img), args...; kwargs...)

# keep consistent to default_AdaptiveThreshold_window_size
# TODO: infer a better window_size from `img` rather than using fixed number
default_Niblack_window_size(img) = 7
2 changes: 1 addition & 1 deletion src/util.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function get_window_bounds(img::AbstractArray{T,2}, pixel::CartesianIndex{2}, w::Integer) where T <: Color
function get_window_bounds(img::GenericGrayImage, pixel::CartesianIndex{2}, w::Integer)
row, col = pixel.I
min_row, min_col = first.(axes(img))
max_row, max_col = last.(axes(img))
Expand Down
Binary file added test/References/Niblack_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/Niblack_Gray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 104 additions & 16 deletions test/niblack.jl
Original file line number Diff line number Diff line change
@@ -1,20 +1,108 @@
@testset "Niblack" begin

for T in (Gray{N0f8}, Gray{N0f16}, Gray{Float32}, Gray{Float64})
img = T.([i <= 25 && j <= 25 ? 0.8 : 1.0 for i = 1:50, j = 1:50])
target_row = target_col = 13
img[target_row,target_col] = 0

for i in 0:10:50, j in 0:10:50
img₀ = circshift(img, (i,j))
target_row₀ = (target_row + i) % 50
target_col₀ = (target_col + j) % 50

img_bin = binarize(Niblack(window_size = 25, bias = -6), img₀)
@test eltype(img_bin) == Gray{Bool}
@test sum(img_bin .== 0) == 1
@test img_bin[target_row₀, target_col₀] == 0
using ImageBinarization: default_Niblack_window_size

@testset "niblack" begin
@info "Test: Niblack"

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

# AdaptiveThreshold
@test Niblack() == Niblack(0.2)
@test Niblack(0.2) == Niblack(bias=0.2)

# window_size non-positive integer
f = Niblack()
@test_throws ArgumentError binarize(img, f, window_size = -10)
@test_throws TypeError binarize(img, f, window_size = 32.5)

# binarize
f = Niblack(bias=0.2)
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_Niblack_window_size(img)) == binarized_img_1

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(testimage("lena_gray_256"); ratio=0.25)
f = Niblack(bias=0.2)

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

# Color3
img_color = imresize(testimage("lena_color_256"); ratio=0.25)
f = Niblack(bias=0.2)

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

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

# 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


for T in (Gray{N0f8}, Gray{N0f16}, Gray{Float32}, Gray{Float64})
img = T.([i <= 25 && j <= 25 ? 0.8 : 1.0 for i = 1:50, j = 1:50])
target_row = target_col = 13
img[target_row,target_col] = 0

for i in 0:10:50, j in 0:10:50
img₀ = circshift(img, (i,j))
target_row₀ = (target_row + i) % 50
target_col₀ = (target_col + j) % 50

img_bin = binarize(img₀, Niblack(bias = -6), window_size=25)
@test sum(img_bin .== 0) == 1
@test img_bin[target_row₀, target_col₀] == 0
end
end
end

@testset "Miscellaneous" begin
img = testimage("lena_gray_256")
@test default_Niblack_window_size(img) == 7

# deprecations
@test (@test_deprecated Niblack(7, 0.2)) == Niblack(0.2)
@test (@test_deprecated Niblack(window_size=7, bias=0.2)) == Niblack(bias=0.2)
end

end
2 changes: 1 addition & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ include("testutils.jl")
include("minimum.jl")
include("minimum_error.jl")
include("moments.jl")
# include("niblack.jl")
include("niblack.jl")
include("otsu.jl")
# include("polysegment.jl")
# include("sauvola.jl")
Expand Down