Skip to content

Commit 22a75f7

Browse files
authored
Support fillval in flood_fill! (#77)
This also distinguishes `flood` (which always returns a "mask") from `flood_fill!` (which can be used to insert non-boolean values). Also adds docstrings and doctests
1 parent 82c79cd commit 22a75f7

File tree

4 files changed

+149
-25
lines changed

4 files changed

+149
-25
lines changed

Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
3939
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
4040
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
4141
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
42+
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
4243
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4344

4445
[targets]
45-
test = ["Documenter", "FileIO", "ImageMagick", "Images", "Test"]
46+
test = ["Documenter", "FileIO", "ImageMagick", "Images", "SparseArrays", "Test"]

src/ImageSegmentation.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export
3434
felzenszwalb,
3535
fast_scanning,
3636
fast_scanning!,
37-
flood_fill,
37+
flood,
3838
flood_fill!,
3939
watershed,
4040
hmin_transform,

src/flood_fill.jl

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,128 @@
1-
flood_fill(f, src::AbstractArray, idx::Union{Integer,CartesianIndex}, nbrhood_function=diamond_iterator(window_neighbors(src))) =
1+
"""
2+
mask = flood(f, src, idx, nbrhood_function=diamond_iterator((3,3,...)))
3+
4+
Return an array `mask` with the same axes as `src`, marked `true` for all elements of `src` that:
5+
6+
- satisfy `f(src[i]) == true` and
7+
- are connected by such elements to the starting point `idx` (an integer index or `CartesianIndex`).
8+
9+
This throws an error if `f` evaluates as `false` for the starting value `src[idx]`.
10+
The sense of connectivity is defined by `nbrhood_function`, with two choices being
11+
[`ImageSegmentation.diamond_iterator`](@ref) and [`ImageSegmentation.box_iterator`](@ref.)
12+
13+
# Examples
14+
15+
```jldoctest; setup=:(using ImageSegmentation, ImageCore)
16+
julia> mostly_red(c) = red(c) > green(c) && red(c) > blue(c)
17+
mostly_red (generic function with 1 method)
18+
19+
julia> img = repeat(LinRange(colorant"red", colorant"blue", 4), 1, 2) # red-to-blue
20+
4×2 Array{RGB{Float32},2} with eltype RGB{Float32}:
21+
RGB{Float32}(1.0,0.0,0.0) RGB{Float32}(1.0,0.0,0.0)
22+
RGB{Float32}(0.666667,0.0,0.333333) RGB{Float32}(0.666667,0.0,0.333333)
23+
RGB{Float32}(0.333333,0.0,0.666667) RGB{Float32}(0.333333,0.0,0.666667)
24+
RGB{Float32}(0.0,0.0,1.0) RGB{Float32}(0.0,0.0,1.0)
25+
26+
julia> flood(mostly_red, [img; img], 1) # only first copy of `img` is connected
27+
8×2 BitMatrix:
28+
1 1
29+
1 1
30+
0 0
31+
0 0
32+
0 0
33+
0 0
34+
0 0
35+
0 0
36+
```
37+
38+
See also [`flood_fill!`](@ref).
39+
"""
40+
flood(f, src::AbstractArray, idx::Union{Integer,CartesianIndex}, nbrhood_function=diamond_iterator(window_neighbors(src))) =
241
flood_fill!(f, falses(axes(src)) #=fill!(similar(src, Bool), false)=#, src, idx, nbrhood_function)
342

4-
function flood_fill(src::AbstractArray, idx::Union{Integer,CartesianIndex},
43+
function flood(src::AbstractArray, idx::Union{Integer,CartesianIndex},
544
nbrhood_function=diamond_iterator(window_neighbors(src)); thresh)
645
validx = src[idx]
746
validx = accum_type(typeof(validx))(validx)
847
return let validx=validx
9-
flood_fill(val -> default_diff_fn(val, validx) < thresh, src, idx, nbrhood_function)
48+
flood(val -> default_diff_fn(val, validx) < thresh, src, idx, nbrhood_function)
1049
end
1150
end
1251

13-
function flood_fill!(f, dest, src::AbstractArray, idx::Union{Int,CartesianIndex}, nbrhood_function=diamond_iterator(window_neighbors(src)))
52+
"""
53+
flood_fill!(f, dest, src, idx, nbrhood_function=diamond_iterator((3,3,...)); fillvalue=true, isfilled = isequal(fillvalue))
54+
55+
Set entries of `dest` to `fillvalue` for all elements of `src` that:
56+
57+
- satisfy `f(src[i]) == true` and
58+
- are connected by such elements to the starting point `idx` (an integer index or `CartesianIndex`).
59+
60+
This throws an error if `f` evaluates as `false` for the starting value `src[idx]`.
61+
The sense of connectivity is defined by `nbrhood_function`, with two choices being
62+
[`ImageSegmentation.diamond_iterator`](@ref) and [`ImageSegmentation.box_iterator`](@ref.)
63+
64+
You can optionally omit `dest`, in which case entries in `src` will be set to `fillvalue`.
65+
You may also supply `isfilled`, which should return `true` for any value in `dest`
66+
which does not need to be set or visited; one requirement is that `isfilled(fillvalue) == true`.
67+
68+
# Examples
69+
70+
```jldoctest; setup=:(using ImageSegmentation)
71+
julia> a = repeat([1:4; 1:4], 1, 3)
72+
8×3 Matrix{Int64}:
73+
1 1 1
74+
2 2 2
75+
3 3 3
76+
4 4 4
77+
1 1 1
78+
2 2 2
79+
3 3 3
80+
4 4 4
81+
82+
julia> flood_fill!(>=(3), a, CartesianIndex(3, 2); fillvalue = -1, isfilled = <(0))
83+
8×3 Matrix{Int64}:
84+
1 1 1
85+
2 2 2
86+
-1 -1 -1
87+
-1 -1 -1
88+
1 1 1
89+
2 2 2
90+
3 3 3
91+
4 4 4
92+
```
93+
94+
See also [`flood`](@ref).
95+
"""
96+
function flood_fill!(f,
97+
dest,
98+
src::AbstractArray,
99+
idx::Union{Int,CartesianIndex},
100+
nbrhood_function = diamond_iterator(window_neighbors(src));
101+
fillvalue = true,
102+
isfilled = (fillvalue === true) ? identity : isequal(fillvalue))
14103
R = CartesianIndices(src)
15-
axes(dest) == R.indices || throw(DimensionMismatch("$(axes(dest)) do not match $(Tuple(R))"))
16104
idx = R[idx] # ensure cartesian indexing
17105
f(src[idx]) || throw(ArgumentError("starting point fails to meet criterion"))
18106
q = [idx]
19-
_flood_fill!(f, dest, src, R, q, nbrhood_function)
107+
fillvalue = convert(eltype(dest), fillvalue)
108+
axes(dest) == R.indices || throw(DimensionMismatch("$(axes(dest)) do not match $(Tuple(R))"))
109+
_flood_fill!(f, dest, src, R, q, nbrhood_function, fillvalue, isfilled)
20110
return dest
21111
end
22-
flood_fill!(f, dest, src::AbstractArray, idx::Integer, nbrhood_function=diamond_iterator(window_neighbors(src))) =
23-
flood_fill!(f, dest, src, Int(idx)::Int, nbrhood_function)
112+
flood_fill!(f, dest, src::AbstractArray, idx::Integer, args...; kwargs...) =
113+
flood_fill!(f, dest, src, Int(idx)::Int, args...; kwargs...)
114+
flood_fill!(f, src::AbstractArray, idx::Union{Integer,CartesianIndex}, args...; kwargs...) =
115+
flood_fill!(f, src, src, idx, args...; kwargs...)
24116

25117
# This is a trivial implementation (just to get something working), better would be a raster implementation
26-
function _flood_fill!(f::F, dest, src, R::CartesianIndices{N}, q, nbrhood_function::FN) where {F,N,FN}
118+
function _flood_fill!(f::F, dest, src, R::CartesianIndices{N}, q, nbrhood_function::FN, fillvalue, isfilled::C) where {F,N,FN,C}
119+
isfilled(fillvalue) == true || throw(ArgumentError("`isfilled(fillvalue)` must return `true`"))
27120
while !isempty(q)
28121
idx = pop!(q)
29-
dest[idx] = true
122+
dest[idx] = fillvalue
30123
@inbounds for j in nbrhood_function(idx)
31124
j R || continue
32-
if f(src[j]) && !dest[j]
125+
if f(src[j]) && !isfilled(dest[j])
33126
push!(q, j)
34127
end
35128
end

test/flood_fill.jl

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ using ImageSegmentation.Colors
33
using ImageSegmentation.FixedPointNumbers
44
using FileIO
55
using Statistics
6+
using SparseArrays
67
using Test
78

89
@testset "flood_fill" begin
910
# 0d
1011
a = reshape([true])
11-
@test flood_fill(identity, a, CartesianIndex()) == a
12-
@test_throws ArgumentError flood_fill(!, a, CartesianIndex())
12+
@test flood(identity, a, CartesianIndex()) == a
13+
@test_throws ArgumentError flood(!, a, CartesianIndex())
1314
# 1d
1415
a = 1:7
15-
@test flood_fill(==(2), a, CartesianIndex(2)) == (a .== 2)
16-
@test_throws ArgumentError flood_fill(==(2), a, CartesianIndex(3))
17-
@test flood_fill(x -> 1 < x < 4, a, CartesianIndex(2)) == [false, true, true, false, false, false, false]
18-
@test flood_fill(isinteger, a, CartesianIndex(2)) == trues(7)
16+
@test flood(==(2), a, CartesianIndex(2)) == (a .== 2)
17+
@test_throws ArgumentError flood(==(2), a, CartesianIndex(3))
18+
@test flood(x -> 1 < x < 4, a, CartesianIndex(2)) == [false, true, true, false, false, false, false]
19+
@test flood(isinteger, a, CartesianIndex(2)) == trues(7)
1920
# 2d
2021
ab = [true false false false;
2122
true true false false;
@@ -26,13 +27,13 @@ using Test
2627
for (f, a) in ((identity, ab), (==(1), an0f8), (==(1), agray))
2728
for idx in CartesianIndices(a)
2829
if f(a[idx])
29-
@test flood_fill(f, a, idx) == a
30+
@test flood(f, a, idx) == a
3031
else
31-
@test_throws ArgumentError flood_fill(f, a, idx)
32+
@test_throws ArgumentError flood(f, a, idx)
3233
end
3334
end
3435
end
35-
@test flood_fill(identity, ab, Int16(1)) == ab
36+
@test flood(identity, ab, Int16(1)) == ab
3637
# 3d
3738
k = 10
3839
a = falses(k, k, k)
@@ -47,19 +48,48 @@ using Test
4748
end
4849
for idx in eachindex(a)
4950
if a[idx]
50-
@test flood_fill(identity, a, idx) == a
51+
@test flood(identity, a, idx) == a
5152
else
52-
@test_throws ArgumentError flood_fill(identity, a, idx)
53+
@test_throws ArgumentError flood(identity, a, idx)
5354
end
5455
end
5556
# Colors
5657
path = download("https://github.com/JuliaImages/juliaimages.github.io/raw/source/docs/src/pkgs/segmentation/assets/flower.jpg")
5758
img = load(path)
58-
seg = flood_fill(img, CartesianIndex(87,280); thresh=0.3)
59+
seg = flood(img, CartesianIndex(87,280); thresh=0.3)
5960
@test 0.2*length(seg) <= sum(seg) <= 0.25*length(seg)
6061
c = mean(img[seg])
6162
# N0f8 makes for easier approximate testing
6263
@test N0f8(red(c)) N0f8(0.855)
6364
@test N0f8(green(c)) N0f8(0.161)
6465
@test N0f8(blue(c)) N0f8(0.439)
66+
67+
# flood_fill!
68+
near3(x) = round(Int, x) == 3
69+
a0 = [range(2, 4, length=9);]
70+
a = copy(a0)
71+
idx = (length(a)+1)÷2
72+
dest = fill!(similar(a, Bool), false)
73+
@test flood_fill!(near3, dest, a, idx) == (round.(a) .== 3)
74+
a = copy(a0)
75+
flood_fill!(near3, a, idx; fillvalue=3)
76+
@test a == [near3(a0[i]) ? 3 : a[i] for i in eachindex(a)]
77+
a = copy(a0)
78+
flood_fill!(near3, a, idx; fillvalue=-1)
79+
@test a == [near3(a0[i]) ? -1 : a[i] for i in eachindex(a)]
80+
a = copy(a0)
81+
@test_throws ArgumentError flood_fill!(near3, a, idx; fillvalue=-1, isfilled=near3)
82+
83+
# This mimics a "big data" application in which we have several structures we want
84+
# to label with different segment numbers, and the `src` array is too big to fit
85+
# in memory.
86+
# It would be better to use a package like SparseArrayKit, which allows efficient
87+
# insertions and supports arbitrary dimensions.
88+
a = Bool[0 0 0 0 0 0 1 1;
89+
1 1 0 0 0 0 0 0]
90+
dest = spzeros(Int, size(a)...) # stores the nonzero indexes in a Dict
91+
flood_fill!(identity, dest, a, CartesianIndex(2, 1); fillvalue=1)
92+
flood_fill!(identity, dest, a, CartesianIndex(1, 7); fillvalue=2)
93+
@test dest == [0 0 0 0 0 0 2 2;
94+
1 1 0 0 0 0 0 0]
6595
end

0 commit comments

Comments
 (0)