Skip to content

Commit 82c79cd

Browse files
authored
Initial (trivial) implementation of flood_fill (#75)
Also adds window-iterators to traverse different neighborhood patterns.
1 parent f6cbc8e commit 82c79cd

File tree

10 files changed

+216
-38
lines changed

10 files changed

+216
-38
lines changed

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ julia = "1"
3636

3737
[extras]
3838
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
39+
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
40+
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
3941
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
4042
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4143

4244
[targets]
43-
test = ["Documenter", "Images", "Test"]
45+
test = ["Documenter", "FileIO", "ImageMagick", "Images", "Test"]

src/ImageSegmentation.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ include("core.jl")
1313
include("region_growing.jl")
1414
include("felzenszwalb.jl")
1515
include("fast_scanning.jl")
16+
include("flood_fill.jl")
1617
include("watershed.jl")
1718
include("region_merging.jl")
1819
include("meanshift.jl")
1920
include("clustering.jl")
2021
include("merge_segments.jl")
22+
include("deprecations.jl")
2123

2224
export
2325
#accessor methods
@@ -31,6 +33,9 @@ export
3133
unseeded_region_growing,
3234
felzenszwalb,
3335
fast_scanning,
36+
fast_scanning!,
37+
flood_fill,
38+
flood_fill!,
3439
watershed,
3540
hmin_transform,
3641
region_adjacency_graph,
@@ -43,7 +48,7 @@ export
4348
kmeans,
4449
fuzzy_cmeans,
4550
merge_segments,
46-
51+
4752
# types
4853
SegmentedImage,
4954
ImageEdge

src/compat.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@
55
else
66
const _oneunit = Base.oneunit
77
end
8+
9+
# Once Base has colon defined here we can replace this
10+
_colon(I::CartesianIndex{N}, J::CartesianIndex{N}) where N =
11+
CartesianIndices(map((i,j) -> i:j, Tuple(I), Tuple(J)))

src/core.jl

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
accum_type(::Type{T}) where {T<:Integer} = Int
2-
accum_type(::Type{Float32}) = Float32
3-
accum_type(::Type{T}) where {T<:Real} = Float64
4-
accum_type(::Type{C}) where {C<:Colorant} = base_colorant_type(C){accum_type(eltype(C))}
1+
accum_type(::Type{T}) where {T<:Integer} = Int
2+
accum_type(::Type{Float32}) = Float32
3+
accum_type(::Type{T}) where {T<:Real} = Float64
4+
accum_type(::Type{T}) where {T<:FixedPoint} = floattype(T)
5+
accum_type(::Type{C}) where {C<:Colorant} = base_colorant_type(C){accum_type(eltype(C))}
56

67
"""
78
`SegmentedImage` type contains the index-label mapping, assigned labels,
@@ -266,10 +267,68 @@ function prune_segments(s::SegmentedImage, is_rem::Function, diff_fn::Function)
266267

267268
end
268269

269-
# Once Base has colon defined here we can replace this
270-
_colon(I::CartesianIndex{N}, J::CartesianIndex{N}) where N =
271-
CartesianIndices(map((i,j) -> i:j, Tuple(I), Tuple(J)))
272270

271+
"""
272+
box_iterator(window)
273+
274+
Return a function that constructs a box-shaped iterable region.
275+
276+
# Examples
277+
```jldoctest; setup=:(using ImageSegmentation), filter=r"#\\d+"
278+
julia> fiter = ImageSegmentation.box_iterator((3, 3))
279+
#17 (generic function with 1 method)
280+
281+
julia> center = CartesianIndex(17, 24)
282+
CartesianIndex(17, 24)
283+
284+
julia> fiter(center)
285+
3×3 CartesianIndices{2, Tuple{UnitRange{$Int}, UnitRange{$Int}}}:
286+
CartesianIndex(16, 23) CartesianIndex(16, 24) CartesianIndex(16, 25)
287+
CartesianIndex(17, 23) CartesianIndex(17, 24) CartesianIndex(17, 25)
288+
CartesianIndex(18, 23) CartesianIndex(18, 24) CartesianIndex(18, 25)
289+
```
290+
"""
291+
function box_iterator(window::Dims{N}) where N
292+
for dim in window
293+
dim > 0 || error("Dimensions of the window must be positive")
294+
isodd(dim) || error("Dimensions of the window must be odd")
295+
end
296+
halfwindow = CartesianIndex(map(x -> x ÷ 2, window))
297+
return function(center::CartesianIndex{N})
298+
_colon(center-halfwindow, center+halfwindow)
299+
end
300+
end
301+
302+
"""
303+
diamond_iterator(window)
304+
305+
Return a function that constructs a diamond-shaped iterable region.
306+
307+
# Examples
308+
```jldoctest; setup=:(using ImageSegmentation), filter=r"#\\d+"
309+
julia> fiter = ImageSegmentation.diamond_iterator((3, 3))
310+
#18 (generic function with 1 method)
311+
312+
julia> center = CartesianIndex(17, 24)
313+
CartesianIndex(17, 24)
314+
315+
julia> fiter(center)
316+
(CartesianIndex(18, 24), CartesianIndex(17, 25), CartesianIndex(16, 24), CartesianIndex(17, 23))
317+
```
318+
"""
319+
function diamond_iterator(window::Dims{N}) where N
320+
for dim in window
321+
dim > 0 || error("Dimensions of the window must be positive")
322+
isodd(dim) || error("Dimensions of the window must be odd")
323+
end
324+
halfwindow = CartesianIndex(map(x -> x ÷ 2, window))
325+
return function(center::CartesianIndex{N})
326+
(ntuple(i -> center + CartesianIndex(ntuple(j -> i == j, N)), N)...,
327+
ntuple(i -> center - CartesianIndex(ntuple(j -> i == j, N)), N)...)
328+
end
329+
end
330+
331+
window_neighbors(img::AbstractArray{T,N}) where {T,N} = ntuple(_ -> 3, N)
273332

274333
"""
275334
G, vertex2cartesian = region_adjacency_graph(img, weight_fn, R)

src/deprecations.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@deprecate seeded_region_growing(
2+
img::AbstractArray,
3+
seeds::AbstractVector,
4+
kernel_dim::Vector{Int},
5+
diff_fn::Function = default_diff_fn) seeded_region_growing(img, seeds, (kernel_dim...,), diff_fn)
6+
7+
@deprecate unseeded_region_growing(
8+
img::AbstractArray,
9+
threshold::Real,
10+
kernel_dim::Vector{Int},
11+
diff_fn::Function = default_diff_fn) unseeded_region_growing(img, threshold, (kernel_dim...,), diff_fn)

src/flood_fill.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
flood_fill(f, src::AbstractArray, idx::Union{Integer,CartesianIndex}, nbrhood_function=diamond_iterator(window_neighbors(src))) =
2+
flood_fill!(f, falses(axes(src)) #=fill!(similar(src, Bool), false)=#, src, idx, nbrhood_function)
3+
4+
function flood_fill(src::AbstractArray, idx::Union{Integer,CartesianIndex},
5+
nbrhood_function=diamond_iterator(window_neighbors(src)); thresh)
6+
validx = src[idx]
7+
validx = accum_type(typeof(validx))(validx)
8+
return let validx=validx
9+
flood_fill(val -> default_diff_fn(val, validx) < thresh, src, idx, nbrhood_function)
10+
end
11+
end
12+
13+
function flood_fill!(f, dest, src::AbstractArray, idx::Union{Int,CartesianIndex}, nbrhood_function=diamond_iterator(window_neighbors(src)))
14+
R = CartesianIndices(src)
15+
axes(dest) == R.indices || throw(DimensionMismatch("$(axes(dest)) do not match $(Tuple(R))"))
16+
idx = R[idx] # ensure cartesian indexing
17+
f(src[idx]) || throw(ArgumentError("starting point fails to meet criterion"))
18+
q = [idx]
19+
_flood_fill!(f, dest, src, R, q, nbrhood_function)
20+
return dest
21+
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)
24+
25+
# 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}
27+
while !isempty(q)
28+
idx = pop!(q)
29+
dest[idx] = true
30+
@inbounds for j in nbrhood_function(idx)
31+
j R || continue
32+
if f(src[j]) && !dest[j]
33+
push!(q, j)
34+
end
35+
end
36+
end
37+
end

src/region_growing.jl

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

2-
default_diff_fn(c1::CT1,c2::CT2) where {CT1<:Union{Colorant,Real}, CT2<:Union{Colorant,Real}} = sqrt(sum(abs2,(c1)-accum_type(CT2)(c2)))
2+
default_diff_fn(c1::CT1,c2::CT2) where {CT1<:Union{Colorant,Real}, CT2<:Union{Colorant,Real}} = sqrt(_abs2((c1)-accum_type(CT2)(c2)))
3+
_abs2(c) = mapreducec(v->float(v)^2, +, 0, c)/length(c)
34

45
"""
56
seg_img = seeded_region_growing(img, seeds, [kernel_dim], [diff_fn])
@@ -53,19 +54,12 @@ Albert Mehnert, Paul Jackaway (1997), "An improved seeded region growing algorit
5354
Pattern Recognition Letters 18 (1997), 1065-1071
5455
"""
5556
function seeded_region_growing(img::AbstractArray{CT,N}, seeds::AbstractVector{<:PairOrTuple{CartesianIndex{N},Int}},
56-
kernel_dim::Union{Vector{Int}, NTuple{N, Int}} = ntuple(i->3,N), diff_fn::Function = default_diff_fn) where {CT<:Union{Colorant,Real}, N}
57-
length(kernel_dim) == N || error("Dimension count of image and kernel_dim do not match")
58-
for dim in kernel_dim
59-
dim > 0 || error("Dimensions of the kernel must be positive")
60-
isodd(dim) || error("Dimensions of the kernel must be odd")
61-
end
62-
pt = CartesianIndex(ntuple(i->kernel_dim[i]÷2, N))
63-
neighbourhood_gen(t) = c->_colon(c-t,c+t)
64-
seeded_region_growing(img, seeds, neighbourhood_gen(pt), diff_fn)
57+
kernel_dim::Dims{N} = ntuple(i->3,N), diff_fn::Function = default_diff_fn) where {CT<:Union{Colorant,Real}, N}
58+
seeded_region_growing(img, seeds, box_iterator(kernel_dim), diff_fn)
6559
end
6660

6761
function seeded_region_growing(img::AbstractArray{CT,N}, seeds::AbstractVector{<:PairOrTuple{CartesianIndex{N},Int}},
68-
neighbourhood::NF, diff_fn::DF = default_diff_fn) where {CT<:Union{Colorant,Real}, N, NF, DF}
62+
neighbourhood::NF, diff_fn::DF = default_diff_fn) where {CT<:Union{Colorant,Real}, N, NF<:Function, DF<:Function}
6963

7064
# Check if labels are positive integers
7165
for seed in seeds
@@ -243,18 +237,11 @@ julia> labels_map(seg)
243237
244238
"""
245239
function unseeded_region_growing(img::AbstractArray{CT,N}, threshold::Real,
246-
kernel_dim::Union{Vector{Int}, NTuple{N, Int}} = ntuple(i->3,N), diff_fn::Function = default_diff_fn) where {CT<:Colorant, N}
247-
length(kernel_dim) == N || error("Dimension count of image and kernel_dim do not match")
248-
for dim in kernel_dim
249-
dim > 0 || error("Dimensions of the kernel must be positive")
250-
isodd(dim) || error("Dimensions of the kernel must be odd")
251-
end
252-
pt = CartesianIndex(ntuple(i->kernel_dim[i]÷2, N))
253-
neighbourhood_gen(t) = c->_colon(c-t,c+t)
254-
unseeded_region_growing(img, threshold, neighbourhood_gen(pt), diff_fn)
240+
kernel_dim::Dims{N} = ntuple(i->3,N), diff_fn::Function = default_diff_fn) where {CT<:Colorant, N}
241+
unseeded_region_growing(img, threshold, box_iterator(kernel_dim), diff_fn)
255242
end
256243

257-
function unseeded_region_growing(img::AbstractArray{CT,N}, threshold::Real, neighbourhood::Function, diff_fn = default_diff_fn) where {CT<:Colorant,N}
244+
function unseeded_region_growing(img::AbstractArray{CT,N}, threshold::Real, neighbourhood::Function, diff_fn::Function = default_diff_fn) where {CT<:Colorant,N}
258245
TM = meantype(CT)
259246

260247
# Fast linear<->cartesian indexing lookup

test/flood_fill.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using ImageSegmentation
2+
using ImageSegmentation.Colors
3+
using ImageSegmentation.FixedPointNumbers
4+
using FileIO
5+
using Statistics
6+
using Test
7+
8+
@testset "flood_fill" begin
9+
# 0d
10+
a = reshape([true])
11+
@test flood_fill(identity, a, CartesianIndex()) == a
12+
@test_throws ArgumentError flood_fill(!, a, CartesianIndex())
13+
# 1d
14+
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)
19+
# 2d
20+
ab = [true false false false;
21+
true true false false;
22+
true false false true;
23+
true true true true]
24+
an0f8 = N0f8.(ab)
25+
agray = Gray.(an0f8)
26+
for (f, a) in ((identity, ab), (==(1), an0f8), (==(1), agray))
27+
for idx in CartesianIndices(a)
28+
if f(a[idx])
29+
@test flood_fill(f, a, idx) == a
30+
else
31+
@test_throws ArgumentError flood_fill(f, a, idx)
32+
end
33+
end
34+
end
35+
@test flood_fill(identity, ab, Int16(1)) == ab
36+
# 3d
37+
k = 10
38+
a = falses(k, k, k)
39+
idx = CartesianIndex(1,1,1)
40+
incs = [CartesianIndex(1,0,0), CartesianIndex(0,1,0), CartesianIndex(0,0,1)]
41+
a[idx] = true
42+
while any(<(k), Tuple(idx))
43+
d = rand(1:3)
44+
idx += incs[d]
45+
idx = min(idx, CartesianIndex(k,k,k))
46+
a[idx] = true
47+
end
48+
for idx in eachindex(a)
49+
if a[idx]
50+
@test flood_fill(identity, a, idx) == a
51+
else
52+
@test_throws ArgumentError flood_fill(identity, a, idx)
53+
end
54+
end
55+
# Colors
56+
path = download("https://github.com/JuliaImages/juliaimages.github.io/raw/source/docs/src/pkgs/segmentation/assets/flower.jpg")
57+
img = load(path)
58+
seg = flood_fill(img, CartesianIndex(87,280); thresh=0.3)
59+
@test 0.2*length(seg) <= sum(seg) <= 0.25*length(seg)
60+
c = mean(img[seg])
61+
# N0f8 makes for easier approximate testing
62+
@test N0f8(red(c)) N0f8(0.855)
63+
@test N0f8(green(c)) N0f8(0.161)
64+
@test N0f8(blue(c)) N0f8(0.439)
65+
end

test/region_growing.jl

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,29 +156,33 @@ of_mean_type(p::Colorant) = ImageSegmentation.meantype(typeof(p))(p)
156156
expected = ones(Int, (3,3))
157157
expected[1:3,3] .= 2
158158
expected_labels = [1,2]
159-
expected_means = Dict(1=>RGB{Float64}(0.3,1.0,0.0), 2=>RGB{Float64}(0.0,0.0,0.0))
159+
expected_means = Dict(1=>RGB{Float32}(0.3,1.0,0.0), 2=>RGB{Float32}(0.0,0.0,0.0))
160160
expected_count = Dict(1=>6, 2=>3)
161161

162162
seg = seeded_region_growing(img, seeds)
163163
@test all(label->(label in expected_labels), seg.segment_labels)
164164
@test all(label->(label in seg.segment_labels), expected_labels)
165165
@test expected_count == seg.segment_pixel_count
166-
@test expected_means == seg.segment_means
166+
@test expected_means[1] seg.segment_means[1]
167+
@test expected_means[2] seg.segment_means[2]
167168
@test seg.image_indexmap == expected
168169

169170
expected = ones(Int, (3,3))
170171
expected[1:3,2] .= 0
171172
expected[1:3,3] .= 2
172173
expected_labels = [0,1,2]
173-
expected_means = Dict(1=>RGB{Float64}(0.4,1.0,0.0), 2=>RGB{Float64}(0.0,0.0,0.0))
174+
expected_means = Dict(1=>RGB{Float32}(0.4,1.0,0.0), 2=>RGB{Float32}(0.0,0.0,0.0))
174175
expected_count = Dict(0=>3, 1=>3, 2=>3)
175176

176-
seg = seeded_region_growing(img, seeds, [3,3], (c1,c2)->abs(of_mean_type(c1).r - of_mean_type(c2).r))
177+
seg = seeded_region_growing(img, seeds, (3,3), (c1,c2)->abs(of_mean_type(c1).r - of_mean_type(c2).r))
177178
@test all(label->(label in expected_labels), seg.segment_labels)
178179
@test all(label->(label in seg.segment_labels), expected_labels)
179180
@test expected_count == seg.segment_pixel_count
180181
@test expected_means == seg.segment_means
181182
@test seg.image_indexmap == expected
183+
@info "The deprecation warning below is expected" # but can be deleted eventually!
184+
segd = seeded_region_growing(img, seeds, [3,3], (c1,c2)->abs(of_mean_type(c1).r - of_mean_type(c2).r))
185+
@test labels_map(segd) == labels_map(seg)
182186
end
183187

184188
@testset "Unseeded Region Growing" begin
@@ -296,11 +300,13 @@ end
296300
expected_means = Dict(1=>of_mean_type(img[1,1]), 3=>of_mean_type(img[1,3]), 2=>of_mean_type(img[1,2]))
297301
expected_count = Dict(1=>3, 2=>3, 3=>3)
298302

299-
seg = unseeded_region_growing(img, 0.2, [3,3], (c1,c2)->abs(of_mean_type(c1).r - of_mean_type(c2).r))
303+
seg = unseeded_region_growing(img, 0.2, (3,3), (c1,c2)->abs(of_mean_type(c1).r - of_mean_type(c2).r))
300304
@test all(label->(label in expected_labels), seg.segment_labels)
301305
@test all(label->(label in seg.segment_labels), expected_labels)
302306
@test expected_count == seg.segment_pixel_count
303307
@test expected_means == seg.segment_means
304308
@test seg.image_indexmap == expected
305-
309+
@info "The deprecation warning below is expected" # but can be deleted eventually!
310+
segd = unseeded_region_growing(img, 0.2, [3,3], (c1,c2)->abs(of_mean_type(c1).r - of_mean_type(c2).r))
311+
@test labels_map(segd) == labels_map(seg)
306312
end

test/runtests.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
using ImageSegmentation, Images, Test, SimpleWeightedGraphs, LightGraphs, StaticArrays, DataStructures
2-
using RegionTrees: isleaf, Cell, split!
2+
using RegionTrees: isleaf, Cell, split!
33
using MetaGraphs: MetaGraph, clear_props!, get_prop, has_prop, set_prop!, props, vertices
44

55
using Documenter
6-
doctest(ImageSegmentation, manual = false)
6+
Base.VERSION >= v"1.6" && doctest(ImageSegmentation, manual = false)
7+
@test isempty(detect_ambiguities(ImageSegmentation))
78

89
include("core.jl")
910
include("region_growing.jl")
1011
include("felzenszwalb.jl")
1112
include("fast_scanning.jl")
13+
include("flood_fill.jl")
1214
include("watershed.jl")
1315
include("region_merging.jl")
1416
include("meanshift.jl")

0 commit comments

Comments
 (0)