Skip to content

Commit c93ce87

Browse files
authored
Add support for compact and masked watersheds (#33)
1 parent 71a6ede commit c93ce87

File tree

3 files changed

+130
-29
lines changed

3 files changed

+130
-29
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ImageSegmentation"
22
uuid = "80713f31-8817-5129-9cf8-209ff8fb23e1"
3-
version = "1.4.2"
3+
version = "1.4.3"
44

55
[deps]
66
Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5"

src/watershed.jl

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import Base.isless
22

3-
struct PixelKey{CT}
3+
struct PixelKey{CT, N}
44
val::CT
55
time_step::Int
6+
source::CartesianIndex{N}
67
end
7-
isless(a::PixelKey{T}, b::PixelKey{T}) where {T} = (a.val < b.val) || (a.val == b.val && a.time_step < b.time_step)
8+
isless(a::PixelKey, b::PixelKey) = (a.val < b.val) || (a.val == b.val && a.time_step < b.time_step)
9+
10+
"""Calculate the euclidean distance between two `CartesianIndex` structs"""
11+
@inline _euclidean(a::CartesianIndex{N}, b::CartesianIndex{N}) where {N} = sqrt(sum(Tuple(a - b) .^ 2))
812

913
"""
1014
```
11-
segments = watershed(img, markers)
15+
segments = watershed(img, markers; compactness, mask)
1216
```
1317
Segments the image using watershed transform. Each basin formed by watershed transform corresponds to a segment.
1418
If you are using image local minimas as markers, consider using [`hmin_transform`](@ref) to avoid oversegmentation.
@@ -18,18 +22,42 @@ Parameters:
1822
- markers = An array (same size as img) with each region's marker assigned a index starting from 1. Zero means not a marker.
1923
If two markers have the same index, their regions will be merged into a single region.
2024
If you have markers as a boolean array, use `label_components`.
25+
- compactness = Use the compact watershed algorithm with the given compactness parameter. Larger values lead to more regularly
26+
shaped watershed basins.[^1]
27+
- mask = Only segment pixels where the value of `mask` is true, used to restrict segmentation to only areas of interest
28+
29+
30+
[^1]: https://www.tu-chemnitz.de/etit/proaut/publications/cws_pSLIC_ICPR.pdf
31+
32+
# Example
33+
34+
```jldoctest; setup = :(using Images, ImageSegmentation)
35+
julia> seeds = falses(100, 100); seeds[50, 25] = true; seeds[50, 75] = true;
2136
37+
julia> dists = distance_transform(feature_transform(seeds)); # calculate distances from seeds
2238
39+
julia> markers = label_components(seeds); # give each seed a unique integer id
2340
41+
julia> results = watershed(dists, markers);
42+
43+
julia> labels_map(result); # labels of segmented image
44+
```
2445
"""
25-
function watershed(img::AbstractArray{T, N}, markers::AbstractArray{S,N}) where {T<:Images.NumberLike, S<:Integer, N}
46+
function watershed(img::AbstractArray{T, N},
47+
markers::AbstractArray{S,N};
48+
mask::AbstractArray{Bool, N}=fill(true, axes(img)),
49+
compactness::Real = 0.0) where {T<:Images.NumberLike, S<:Integer, N}
2650

2751
if axes(img) != axes(markers)
2852
error("image size doesn't match marker image size")
53+
elseif axes(img) != axes(mask)
54+
error("image size doesn't match mask size")
2955
end
3056

57+
compact = compactness > 0.0
3158
segments = copy(markers)
32-
pq = PriorityQueue{CartesianIndex{N}, PixelKey{T}}()
59+
PK = PixelKey{compact ? floattype(T) : T, N}
60+
pq = PriorityQueue{CartesianIndex{N}, PK}()
3361
time_step = 0
3462

3563
R = CartesianIndices(axes(img))
@@ -39,22 +67,63 @@ function watershed(img::AbstractArray{T, N}, markers::AbstractArray{S,N}) where
3967
for j in CartesianIndices(_colon(max(Istart,i-one(i)), min(i+one(i),Iend)))
4068
if segments[j] == 0
4169
segments[j] = markers[i]
42-
enqueue!(pq, j, PixelKey(img[i], time_step))
70+
enqueue!(pq, j, PK(img[i], time_step, j))
4371
time_step += 1
4472
end
4573
end
4674
end
4775
end
4876

4977
while !isempty(pq)
50-
current = dequeue!(pq)
51-
segments_current = segments[current]
52-
img_current = img[current]
53-
for j in CartesianIndices(_colon(max(Istart,current-one(current)), min(current+one(current),Iend)))
78+
curr_idx, curr_elem = dequeue_pair!(pq)
79+
segments_current = segments[curr_idx]
80+
81+
# If we're using the compact algorithm, we need assign grouping for a given location
82+
# when it comes off the queue since we could find a better suited watershed later.
83+
if compact
84+
if segments_current > 0 && curr_idx != curr_elem.source
85+
# this is a non-marker location that we've already assigned
86+
continue
87+
end
88+
# group this location with its watershed
89+
segments[curr_idx] = segments[curr_elem.source]
90+
end
91+
92+
img_current = img[curr_idx]
93+
for j in CartesianIndices(_colon(max(Istart,curr_idx-one(curr_idx)), min(curr_idx+one(curr_idx),Iend)))
94+
95+
# if this location is false in the mask, we skip it
96+
(!mask[j]) && continue
97+
# only continue if this is a position that we haven't assigned yet
5498
if segments[j] == 0
55-
segments[j] = segments_current
56-
enqueue!(pq, j, PixelKey(img_current, time_step))
57-
time_step += 1
99+
# if we're doing a simple watershed, we can go ahead and set the final grouping for a new
100+
# ungrouped position the moment we first encounter it
101+
if !compact
102+
segments[j] = segments_current
103+
new_value = img_current
104+
else
105+
# in the compact algorithm case, we don't set the grouping
106+
# at push-time and instead calculate a temporary value based
107+
# on the weighted sum of the intensity and distance to the
108+
# current source marker.
109+
new_value = floattype(T)(img_current + compactness * _euclidean(j, curr_elem.source))
110+
end
111+
112+
# if this position is in the queue and we're using the compact algorithm, we need to replace
113+
# its watershed if we find one that it better belongs to
114+
if compact && j in keys(pq)
115+
elem = pq[j]
116+
new_elem = PK(new_value, time_step, curr_elem.source)
117+
118+
if new_elem < elem
119+
pq[j] = new_elem # update the watershed
120+
time_step += 1
121+
end
122+
else
123+
124+
pq[j] = PK(new_value, time_step, curr_elem.source)
125+
time_step += 1
126+
end
58127
end
59128
end
60129
end

test/watershed.jl

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,56 @@ using ImageFiltering
1111
markers[1, 1] = 1
1212
markers[50, 50] = 2
1313

14-
result = watershed(img, markers)
14+
@testset "classic watershed" begin
15+
# with classic watershed, we expect the top left label should spread around
16+
# the center label.
17+
result = watershed(img, markers)
1518

16-
@test length(result.segment_labels) == 2
17-
@test result.segment_labels == collect(1:2)
18-
@test all(label->(label==result.image_indexmap[50,50]), result.image_indexmap[26:74,26:74])
19+
@test length(result.segment_labels) == 2
20+
@test result.segment_labels == collect(1:2)
21+
@test all(label->(label==result.image_indexmap[50,50]), result.image_indexmap[26:74,26:74])
22+
end
1923

24+
@testset "masked watershed" begin
25+
mask = trues(size(img))
26+
mask[60:70, 60:70] .= false
2027

21-
img = ones(15, 15)
22-
#minima of depth 0.2
23-
img[3:5, 3:5] .= 0.9
24-
img[4,4] = 0.8
25-
#minima of depth 0.7
26-
img[9:13, 9:13] .= 0.8
27-
img[10:12, 10:12] .= 0.7
28-
img[11,11] = 0.3
28+
result = watershed(img, markers, mask=mask)
29+
labels = labels_map(result)
2930

30-
out = hmin_transform(img, 0.25)
31+
# where the mask is false, no label should be assigned
32+
@test sum(labels[.~ mask]) == 0
3133

32-
@test findlocalminima(img) == [CartesianIndex(4, 4), CartesianIndex(11, 11)]
33-
@test findlocalminima(out) == [CartesianIndex(11, 11)]
34+
result = watershed(img, markers, compactness=10.0, mask=mask)
35+
labels = labels_map(result)
36+
37+
# where the mask is false, no label should be assigned
38+
@test sum(labels[.~ mask]) == 0
39+
end
40+
41+
@testset "compact watershed" begin
42+
result = watershed(img, markers, compactness=10.0)
43+
labels = labels_map(result)
44+
45+
# since this is using the compact algorithm with a high value for
46+
# compactness, the boundary between labels 1 and 2 should occur halfway
47+
# between the two markers
48+
@test sum(labels .== 1) == sum(1:50) - 2
49+
end
50+
51+
@testset "h-minima transform" begin
52+
img = ones(15, 15)
53+
#minima of depth 0.2
54+
img[3:5, 3:5] .= 0.9
55+
img[4,4] = 0.8
56+
#minima of depth 0.7
57+
img[9:13, 9:13] .= 0.8
58+
img[10:12, 10:12] .= 0.7
59+
img[11,11] = 0.3
60+
61+
out = hmin_transform(img, 0.25)
62+
63+
@test findlocalminima(img) == [CartesianIndex(4, 4), CartesianIndex(11, 11)]
64+
@test findlocalminima(out) == [CartesianIndex(11, 11)]
65+
end
3466
end

0 commit comments

Comments
 (0)