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

[Morphology][Tasks] introduce generic fillholes algorithm #119

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
14 changes: 12 additions & 2 deletions benchmark/benchmarks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ end
SUITE["connected"] = BenchmarkGroup()
let grp = SUITE["connected"]
grp["label_components"] = @benchmarkable label_components($blobs)
grp["label_flatzones"] = @benchmarkable label_flatzones($cameraman, trues(3,3))
grp["label_lambdaflatzones"] = @benchmarkable label_lambdaflatzones($cameraman, trues(3,3),Gray{N0f8}(1.0/255.0))
grp["label_flatzones"] = @benchmarkable label_flatzones($cameraman, trues(3, 3))
grp["label_lambdaflatzones"] = @benchmarkable label_lambdaflatzones($cameraman, trues(3, 3), Gray{N0f8}(1.0 / 255.0))
end

SUITE["Maxtree"] = BenchmarkGroup()
Expand Down Expand Up @@ -136,3 +136,13 @@ let grp = SUITE["extremum"]
grp["regional_maxima"]["$sz×$sz"] = @benchmarkable regional_maxima($tst_img)
end
end

SUITE["clearborder"] = BenchmarkGroup()
let grp = SUITE["clearborder"]
grp["clearborder"] = @benchmarkable clearborder($blobs)
end

SUITE["fillhole"] = BenchmarkGroup()
let grp = SUITE["fillhole"]
grp["fillhole"] = @benchmarkable fillhole($blobs)
end
8 changes: 6 additions & 2 deletions src/ImageMorphology.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ include("connected.jl")
include("clearborder.jl")
include("extreme_filter.jl")
include("extremum.jl")
include("fillholes.jl")
include("ops/dilate.jl")
include("ops/erode.jl")
include("ops/closing.jl")
Expand Down Expand Up @@ -129,8 +130,10 @@ export

#feature_transform.jl
feature_transform,
distance_transform,
clearborder,
distance_transform, clearborder,

fillhole,
fillhole!,

#leveling
low_leveling,
Expand All @@ -139,6 +142,7 @@ export
high_leveling!,
leveling,
leveling!,

#extremum
hmaxima,
hmaxima!,
Expand Down
58 changes: 58 additions & 0 deletions src/fillholes.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
fillhole(img; [dims])
fillhole(img; se)

Fill the holes in image 'img'. Could be binary or grascale

The `dims` keyword is used to specify the dimension to process by constructing the box shape
structuring element [`strel_box(img; dims)`](@ref strel_box). For generic structuring
element, the half-size is expected to be either `0` or `1` along each dimension.

The output has the same type as input image
"""

function fillhole(img; dims=coords_spatial(img))
return fillhole(img, strel_box(img, dims))
end

function fillhole(img, se)
return fillhole!(similar(img), img, se)
end

function fillhole!(out, img; dims=coords_spatial(img))
return fillhole!(out, img, strel_box(img, dims))
end

function fillhole!(out, img, se)
return _fillhole!(out, img, se)
end

function _fillhole!(out, img, se)
N = ndims(img)

axes(out) == axes(img) || throw(DimensionMismatch("images should have the same axes"))

se_size = strel_size(se)
if length(se_size) != N
msg = "the input structuring element is not for $N dimensional array, instead it is for $(length(se_size)) dimensional array"
throw(DimensionMismatch(msg))
end
if !all(x -> in(x, (1, 3)), strel_size(se))
msg = "structuring element with half-size larger than 1 is invalid"
throw(DimensionMismatch(msg))
end

tmp = similar(img)

# fill marker image with max
fill!(tmp, typemax(eltype(img)))
# fill borders with 0
dimensions = size(tmp)
outerrange = CartesianIndices(map(i -> 1:i, dimensions))
innerrange = CartesianIndices(map(i -> (1 + 1):(i - 1), dimensions))
for i in EdgeIterator(outerrange, innerrange)
tmp[i] = 0
Copy link
Member

@johnnychen94 johnnychen94 Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For generic programming: tmp[i] = zero(T) where I assume T = eltype(img)

The difference is very small but noteworthy:

  • tmp[i] = 0 would be valid if there's a conversion from 0 (Int) to eltype(tmp)
  • tmp[i] = zero(T) would be valid as long as zero(T) is defined (which is true for almost all number-like types)

Why we should prefer the zero(T) version is that: not all types (should) support (implicit) conversion from Int. The following is an JuliaImages example, but you can quickly come up with many like it:

julia> using ImageCore

julia> zero(HSV)
HSV{Float32}(0.0f0,0.0f0,0.0f0)

julia> HSV(0)
ERROR: in ccolor, no automatic conversion from Int64 and HSV
Stacktrace:
 [1] ccolor(#unused#::Type{HSV}, #unused#::Type{Int64})
   @ ColorTypes ~/.julia/packages/ColorTypes/1dGw6/src/traits.jl:410
 [2] convert(#unused#::Type{HSV}, c::Int64)
   @ ColorTypes ~/.julia/packages/ColorTypes/1dGw6/src/conversions.jl:74
 [3] HSV(x::Int64)
   @ ColorTypes ~/.julia/packages/ColorTypes/1dGw6/src/types.jl:464
 [4] top-level scope
   @ REPL[3]:1

end

return mreconstruct!(erode, out, tmp, img, se)
end
135 changes: 135 additions & 0 deletions test/fillholes.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
@testset "fillhole" begin
#binary
img = Bool[
0 0 0 0 0 0 0
0 1 1 1 1 1 0
0 1 0 0 0 1 0
0 1 0 0 0 1 0
0 1 0 0 0 1 0
0 1 1 1 1 1 0
0 0 0 0 0 0 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test where the 0s at the border are not a single connected component? Also the case where there are no zeros on the border m. Also add a test for multiple interior holes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we could say nothing for "holes" in the border, anyway we don't have acess to underlyting image domain, are these really holes
So as other image processing framework we don't fill hole touching the borders

]

expected = Bool[
0 0 0 0 0 0 0
0 1 1 1 1 1 0
0 1 1 1 1 1 0
0 1 1 1 1 1 0
0 1 1 1 1 1 0
0 1 1 1 1 1 0
0 0 0 0 0 0 0
]

out = fillhole(img)
@test eltype(out) == Bool
@test out == expected

# in place
out = similar(img)
fillhole!(out, img)
@test out == expected

# in place diamond
out = similar(img)
fillhole!(out, img, strel_diamond((3, 3)))
@test out == expected

# more holes
#binary
img = Bool[
0 0 0 0 0 1 1 0
0 1 1 1 0 0 0 0
0 1 0 1 0 0 0 0
0 1 1 1 0 0 0 0
0 0 0 1 1 1 0 0
1 0 0 1 0 1 0 0
1 0 0 1 1 1 0 0
1 0 0 0 0 0 0 0
]

expected = Bool[
0 0 0 0 0 1 1 0
0 1 1 1 0 0 0 0
0 1 1 1 0 0 0 0
0 1 1 1 0 0 0 0
0 0 0 1 1 1 0 0
1 0 0 1 1 1 0 0
1 0 0 1 1 1 0 0
1 0 0 0 0 0 0 0
]

out = fillhole(img)
@test eltype(out) == Bool
@test out == expected

# in place
out = similar(img)
fillhole!(out, img)
@test out == expected


# "holes" touching the borders
# by definitions we can't say anything in this case
# because we have no acess to the underlying image domain
# so like other framework, leave these holes not filled

#binary
img = Bool[
0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1
0 0 0 0 0 0 1 0
0 0 0 0 0 0 1 0
1 1 1 1 0 0 1 1
1 0 0 1 0 0 0 0
1 1 1 1 0 0 0 0
1 0 0 0 0 0 0 0
]

expected = Bool[
0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1
0 0 0 0 0 0 1 0
0 0 0 0 0 0 1 0
1 1 1 1 0 0 1 1
1 1 1 1 0 0 0 0
1 1 1 1 0 0 0 0
1 0 0 0 0 0 0 0
]

out = fillhole(img)
@test eltype(out) == Bool
@test out == expected

# in place
out = similar(img)
fillhole!(out, img)
@test out == expected

#gray
img = [
3 3 3 3 3 3 3 3 3 3
3 4 4 4 3 3 4 4 4 3
3 4 1 4 3 3 4 1 4 3
3 4 4 4 3 3 4 4 4 3
3 3 3 3 3 3 3 3 3 3
]

expected = [
3 3 3 3 3 3 3 3 3 3
3 4 4 4 3 3 4 4 4 3
3 4 4 4 3 3 4 4 4 3
3 4 4 4 3 3 4 4 4 3
3 3 3 3 3 3 3 3 3 3
]

out = fillhole(img)
@test out == expected

msg = "the input structuring element is not for 1 dimensional array, instead it is for 2 dimensional array"
@test_throws DimensionMismatch(msg) fillhole(rand(10), strel_box((3, 3)))

se = strel_diamond((7, 7))
msg = "structuring element with half-size larger than 1 is invalid"
@test_throws DimensionMismatch(msg) fillhole(rand(10, 10), se)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for an OffsetArray. Either check for the appropriate exception or correct function.


end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ include("testutils.jl")
include("feature_transform.jl")
include("leveling.jl")
include("clearborder.jl")
include("fillholes.jl")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this indented?

@info "Beginning deprecation tests, warnings are expected"
include("deprecations.jl")
end
Expand Down