From 1a72d8fbd6c5075fbc139e8395d86ebf6c5e03d9 Mon Sep 17 00:00:00 2001 From: Thomas Retornaz Date: Wed, 4 Jan 2023 14:37:16 +0100 Subject: [PATCH 1/2] [Morphology][Tasks] introduce generic fillholes algorithm Todos: * speedup binary implem --- benchmark/benchmarks.jl | 18 +++++++++--- src/ImageMorphology.jl | 8 ++++++ src/filholes.jl | 58 +++++++++++++++++++++++++++++++++++++ test/fillholes.jl | 64 +++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 5 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 src/filholes.jl create mode 100644 test/fillholes.jl diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index eda471d..d0fa48c 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -1,5 +1,5 @@ -# Usage: -# julia benchmark/run_benchmarks.jl +Usage: + julia benchmark/run_benchmarks.jl using BenchmarkTools using ImageCore using ImageMorphology @@ -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() @@ -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 \ No newline at end of file diff --git a/src/ImageMorphology.jl b/src/ImageMorphology.jl index 862362c..17d1a85 100644 --- a/src/ImageMorphology.jl +++ b/src/ImageMorphology.jl @@ -24,6 +24,7 @@ include("connected.jl") include("clearborder.jl") include("extreme_filter.jl") include("extremum.jl") +include("filholes.jl") include("ops/dilate.jl") include("ops/erode.jl") include("ops/closing.jl") @@ -130,8 +131,14 @@ export #feature_transform.jl feature_transform, distance_transform, + + #clearborder clearborder, + #fillhole + fillhole, + fillhole!, + #leveling low_leveling, low_leveling!, @@ -139,6 +146,7 @@ export high_leveling!, leveling, leveling!, + #extremum hmaxima, hmaxima!, diff --git a/src/filholes.jl b/src/filholes.jl new file mode 100644 index 0000000..75cc06f --- /dev/null +++ b/src/filholes.jl @@ -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 + end + + return mreconstruct!(erode, out, tmp, img, se) +end \ No newline at end of file diff --git a/test/fillholes.jl b/test/fillholes.jl new file mode 100644 index 0000000..f62f3bc --- /dev/null +++ b/test/fillholes.jl @@ -0,0 +1,64 @@ +@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 + ] + + 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) + out = fillhole!(out, img) + @test out == expected + + # in place diamond + out = similar(img) + out = fillhole!(out, img, strel_diamond((3, 3))) + @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) + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index b74eaa1..6a70bc1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -37,6 +37,7 @@ include("testutils.jl") include("feature_transform.jl") include("leveling.jl") include("clearborder.jl") + include("fillholes.jl") @info "Beginning deprecation tests, warnings are expected" include("deprecations.jl") end From 773a632cffbe6e48307bb9603f2765df0353b02e Mon Sep 17 00:00:00 2001 From: Thomas Retornaz Date: Sun, 12 Feb 2023 04:07:42 +0100 Subject: [PATCH 2/2] [Morphology][fillholes] follow review request * fix naming * add more tests --- benchmark/benchmarks.jl | 4 +- src/ImageMorphology.jl | 10 ++--- src/{filholes.jl => fillholes.jl} | 0 test/fillholes.jl | 75 ++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 11 deletions(-) rename src/{filholes.jl => fillholes.jl} (100%) diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl index d0fa48c..8f5af9c 100644 --- a/benchmark/benchmarks.jl +++ b/benchmark/benchmarks.jl @@ -1,5 +1,5 @@ -Usage: - julia benchmark/run_benchmarks.jl +# Usage: +# julia benchmark/run_benchmarks.jl using BenchmarkTools using ImageCore using ImageMorphology diff --git a/src/ImageMorphology.jl b/src/ImageMorphology.jl index 17d1a85..dffa6f8 100644 --- a/src/ImageMorphology.jl +++ b/src/ImageMorphology.jl @@ -24,7 +24,7 @@ include("connected.jl") include("clearborder.jl") include("extreme_filter.jl") include("extremum.jl") -include("filholes.jl") +include("fillholes.jl") include("ops/dilate.jl") include("ops/erode.jl") include("ops/closing.jl") @@ -130,12 +130,8 @@ export #feature_transform.jl feature_transform, - distance_transform, - - #clearborder - clearborder, - - #fillhole + distance_transform, clearborder, + fillhole, fillhole!, diff --git a/src/filholes.jl b/src/fillholes.jl similarity index 100% rename from src/filholes.jl rename to src/fillholes.jl diff --git a/test/fillholes.jl b/test/fillholes.jl index f62f3bc..06b963f 100644 --- a/test/fillholes.jl +++ b/test/fillholes.jl @@ -26,12 +26,83 @@ # in place out = similar(img) - out = fillhole!(out, img) + fillhole!(out, img) @test out == expected # in place diamond out = similar(img) - out = fillhole!(out, img, strel_diamond((3, 3))) + 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