Skip to content

Commit 3cd82ea

Browse files
authored
Add imerode|dilate, fillsinks Float versions. Add bwconncomp (#1834)
* Add imerode|dilate, fillsinks Float versions. Add bwconncomp * graydist, cc2bw and bwdist functions.
1 parent d90d76d commit 3cd82ea

9 files changed

Lines changed: 1199 additions & 41 deletions

File tree

src/GMT.jl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,11 @@ export
177177

178178
VSdisp, mad, info, kmeans, pca, mosaic, quadbounds, quadkey, geocoder, getprovider, zscores,
179179

180-
bwhitmiss, binarize, bwareaopen, bwperim, bwskell, isodata, padarray, rgb2gray, rgb2lab, rgb2YCbCr, rgb2ycbcr, grid2img,
181-
img2grid, grays2cube, grays2rgb, imclose, imcomplement, imcomplement!, imdilate, imerode, imfilter, imopen, imsegment,
182-
imsobel, imtophat, imbothat, imhdome, imhmin, imhmax, immorphgrad, imrankfilter, strel,
183-
imfill, imreconstruct, fillsinks, fillsinks!, imregionalmin, imregionalmax, imclearborder,
180+
bwhitmiss, binarize, bwareaopen, bwconncomp, bwdist, bwlabel, bwperim, bwskell, cc2bw, graydist, isodata,
181+
padarray, rgb2gray, rgb2lab, rgb2YCbCr, rgb2ycbcr, grid2img, img2grid, grays2cube, grays2rgb, imclose,
182+
imcomplement, imcomplement!, imdilate, imerode, imfilter, imopen, imsegment, imsobel, imtophat, imbothat,
183+
imhdome, imhmin, imhmax, immorphgrad, imrankfilter, strel, imfill, imreconstruct, fillsinks, fillsinks!,
184+
imregionalmin, imregionalmax, imclearborder,
184185

185186
findpeaks, makeDCWs, mksymbol, circfit,
186187

@@ -360,6 +361,8 @@ include("seis/psmeca.jl")
360361
include("seis/gmtisf.jl")
361362
include("geodesy/psvelo.jl")
362363
include("geodesy/earthtide.jl")
364+
include("imgmorph/bwdist.jl")
365+
include("imgmorph/graydist.jl")
363366
include("MB/mbimport.jl")
364367
include("MB/mbgetdata.jl")
365368
include("MB/mbsvplist.jl")
@@ -453,6 +456,7 @@ end
453456

454457
#Base.precompile(Tuple{typeof(upGMT),Bool, Bool}) # Here it doesn't print anything.
455458
#Base.precompile(Tuple{Dict{Symbol, Any}, Vector{String}}) # Here it doesn't print anything.
459+
#Base.precompile(Tuple{typeof(Base.vect), Array{String, 1}, Vararg{Array{String, 1}}})
456460

457461
function __init__(test::Bool=false)
458462
clear_sessions(3600) # Delete stray sessions dirs older than 1 hour

src/imgmorph/bwdist.jl

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
# These functions are from the ImageMorphology.jl package, adapted here
2+
# for use in GMT.jl without requiring the entire package as a dependency.
3+
4+
function bwdist(img::AbstractArray{Bool, N}; weights=nothing, nthreads=Threads.nthreads()) where {N}
5+
ft = feature_transform(img; weights=weights, nthreads=nthreads)
6+
distance_transform(ft, weights)
7+
end
8+
function bwdist_idx(img::AbstractArray{Bool, N}; weights=nothing, nthreads=Threads.nthreads()) where {N}
9+
ft = feature_transform(img; weights=weights, nthreads=nthreads)
10+
end
11+
12+
bwdist(mat::AbstractArray{T, N}; weights=nothing, nthreads=Threads.nthreads()) where {T,N} =
13+
bwdist(mat .!= zero(T); weights=weights, nthreads=nthreads)
14+
bwdist_idx(mat::AbstractArray{T, N}; weights=nothing, nthreads=Threads.nthreads()) where {T,N} =
15+
bwdist_idx(mat .!= zero(T); weights=weights, nthreads=nthreads)
16+
17+
struct SplitAxis <: AbstractVector{UnitRange{Int}}
18+
splits::Vector{Int}
19+
end
20+
21+
"""
22+
SplitAxis(ax::AbstractUnitRange, n::Real)
23+
24+
Split `ax` into `ceil(Int, n)` approximately equal-sized chunks. The first chunk is no larger than any other chunk,
25+
and any fractional "deficit" in `n` will further shrink the first chunk.
26+
27+
This can be useful in splitting work across threads. When the first thread is responsible for assigning work to the others,
28+
it's often useful to assign less work to it to account for the time spent scheduling.
29+
30+
# Examples
31+
32+
```jldoctest; setup=:(using TiledIteration)
33+
julia> collect(SplitAxis(1:16, 4))
34+
4-element Vector{UnitRange{$Int}}:
35+
1:4
36+
5:8
37+
9:12
38+
13:16
39+
40+
julia> collect(SplitAxis(1:16, 3.5))
41+
4-element Vector{UnitRange{$Int}}:
42+
1:1
43+
2:6
44+
7:11
45+
12:16
46+
```
47+
48+
In the latter case all the ranges except the first have length 5; consequently, only one element remains for the first chunk.
49+
"""
50+
function SplitAxis(ax::AbstractUnitRange{<:Integer}, n::Real)
51+
step = ceil(Int, length(ax)/n)
52+
# Give the smallest amount of work to thread 1, since often it is also scheduling the work for all
53+
# the other threads.
54+
SplitAxis(max.(first(ax)-1, collect(reverse(range(last(ax), step=-step, length=ceil(Int, n)+1)))))
55+
end
56+
57+
Base.@propagate_inbounds Base.getindex(sax::SplitAxis, i::Int) = sax.splits[i]+1:sax.splits[i+1]
58+
59+
Base.size(sax::SplitAxis) = (length(sax.splits)-1,)
60+
61+
struct SplitAxes{N} <: AbstractVector{Tuple{UnitRange{Int},Vararg{UnitRange{Int},N}}}
62+
inner::NTuple{N,UnitRange{Int}}
63+
splitax::SplitAxis
64+
end
65+
66+
"""
67+
SplitAxes(axs::NTuple{N,AbstractUnitRange}, n::Real)
68+
69+
Split `axs` into `ceil(Int, n)` approximately equal-sized chunks along the final dimension represented by `axs`.
70+
71+
See [`SplitAxis`](@ref) for further details.
72+
73+
# Examples
74+
75+
```jldoctest; setup=:(using TiledIteration)
76+
julia> A = rand(3, 16);
77+
78+
julia> collect(SplitAxes(axes(A), 4))
79+
4-element Vector{Tuple{UnitRange{$Int}, UnitRange{$Int}}}:
80+
(1:3, 1:4)
81+
(1:3, 5:8)
82+
(1:3, 9:12)
83+
(1:3, 13:16)
84+
```
85+
"""
86+
SplitAxes(axs::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}}, n::Real) = SplitAxes{length(axs)-1}(Base.front(axs), SplitAxis(axs[end], n))
87+
88+
Base.@propagate_inbounds Base.getindex(saxs::SplitAxes, i::Int) = (saxs.inner..., saxs.splitax[i])
89+
90+
Base.size(saxs::SplitAxes) = size(saxs.splitax)
91+
92+
93+
"""
94+
feature_transform(img::AbstractArray{Bool, N}; weights=nothing, nthreads=Threads.nthreads()) -> F
95+
96+
Compute the feature transform of a binary image `I`, finding the
97+
closest "feature" (positions where `I` is `true`) for each location in
98+
`I`. Specifically, `F[i]` is a `CartesianIndex` encoding the position
99+
closest to `i` for which `I[F[i]]` is `true`. In cases where two or
100+
more features in `I` have the same distance from `i`, an arbitrary
101+
feature is chosen. If `I` has no `true` values, then all locations are
102+
mapped to an index where each coordinate is `typemin(Int)`.
103+
104+
Optionally specify the weight `w` assigned to each coordinate. For
105+
example, if `I` corresponds to an image where voxels are anisotropic,
106+
`w` could be the voxel spacing along each coordinate axis. The default
107+
value of `nothing` is equivalent to `w=(1,1,...)`.
108+
109+
See also: [`distance_transform`](@ref).
110+
111+
# Citation
112+
113+
- [1] Maurer, Calvin R., Rensheng Qi, and Vijay Raghavan. "A linear time algorithm for
114+
computing exact Euclidean distance transforms of binary images in arbitrary dimensions."
115+
_IEEE Transactions on Pattern Analysis and Machine Intelligence_ 25.2 (2003): 265-270.
116+
"""
117+
function feature_transform(img::AbstractArray{Bool, N}; weights::Union{Nothing,NTuple{N}}=nothing,
118+
nthreads::Int=length(img) < 1000 ? 1 : Threads.nthreads(),) where {N}
119+
nthreads > 0 || error("the number of threads must be positive, got $nthreads")
120+
N == 0 && return reshape([CartesianIndex()])
121+
# Allocate the output
122+
F = similar(img, CartesianIndex{N})
123+
axsimg = axes(img)
124+
# To allocate temporary storage for voronoift!, compute one
125+
# element (so we have the proper type)
126+
fi = first(CartesianIndices(axsimg))
127+
drft = DistRFT(fi, weights, (), Base.tail(fi.I))
128+
if nthreads == 1 || N == 1
129+
tmp = typeof(drft)[]
130+
computeft!(F, img, axsimg, CartesianIndex(), weights, tmp)
131+
# Finish the last dimension (for multithreading, we avoid doing it in computeft!)
132+
finishft!(F, img, axsimg, CartesianIndex(), weights, tmp)
133+
else
134+
tmps = [typeof(drft)[] for _ in 1:nthreads] # temporary storage (one per thread)
135+
saxs = SplitAxes(axsimg, nthreads - 0.2) # give main thread less work since it also schedules the others
136+
tasks = [Threads.@spawn computeft!(F, img, saxs[i], CartesianIndex(), weights, tmps[i]) for i in 2:nthreads]
137+
computeft!(F, img, saxs[1], CartesianIndex(), weights, tmps[1])
138+
foreach(wait, tasks)
139+
# Finish the last dimension
140+
saxs1 = SplitAxes(axsimg[1:(N - 1)], nthreads - 0.2)
141+
tasks = [Threads.@spawn finishft!(F, img, (saxs1[i]..., axsimg[end]), CartesianIndex(), weights, tmps[i]) for i in 2:nthreads]
142+
finishft!(F, img, (saxs1[1]..., axsimg[end]), CartesianIndex(), weights, tmps[1])
143+
foreach(wait, tasks)
144+
end
145+
return F
146+
end
147+
148+
"""
149+
distance_transform(F::AbstractArray{CartesianIndex}, [w=nothing]) -> D
150+
151+
Compute the distance transform of `F`, where each element `F[i]`
152+
represents a "target" or "feature" location assigned to `i`.
153+
Specifically, `D[i]` is the distance between `i` and `F[i]`.
154+
Optionally specify the weight `w` assigned to each coordinate; the
155+
default value of `nothing` is equivalent to `w=(1,1,...)`.
156+
157+
See also: [`feature_transform`](@ref).
158+
"""
159+
function distance_transform(F::AbstractArray{CartesianIndex{N},N}, w::Union{Nothing,NTuple{N}}=nothing) where {N}
160+
# To allocate the proper output type, compute the distance for one element
161+
R = CartesianIndices(axes(F))
162+
dst = wnorm2(zero(eltype(R)), w)
163+
D = similar(F, typeof(sqrt(dst)))
164+
165+
= nullindex(F)
166+
@inbounds for i in R
167+
fi = F[i]
168+
D[i] = fi ==? Inf : sqrt(wnorm2(fi - i, w))
169+
end
170+
171+
return D
172+
end
173+
174+
# This recursive implementation computes the feature transform, other than for finishing the
175+
# work along the final axis (axis `N` for an `N` dimensional array).
176+
# Omission of the final axis makes it easy to implement multithreading.
177+
# You can finish the final axis with a call to `finishft!` with `jpost = CartesianIndex()`.
178+
function computeft!(F, img, axsimg, jpost::CartesianIndex{K}, pixelspacing, tmp) where {K}
179+
# tmp is workspace for voronoift!
180+
= nullindex(F) # sentinel position
181+
if K == ndims(img) - 1 # innermost loop (d=1 case, line 1)
182+
# Fig. 2, lines 2-8
183+
@inbounds @simd for i1 in axes(img, 1)
184+
F[i1, jpost] = img[i1, jpost] ? CartesianIndex(i1, jpost) :
185+
end
186+
else # recursively handle trailing dimensions
187+
# Fig. 2, lines 10-12
188+
for i1 in axsimg[ndims(img) - K]
189+
computeft!(F, img, axsimg, CartesianIndex(i1, jpost), pixelspacing, tmp)
190+
end
191+
end
192+
K == 0 && return F # defer the final axis, where threads will be split across next-to-last axis
193+
return finishft!(F, img, axsimg, jpost, pixelspacing, tmp)
194+
end
195+
196+
function finishft!(F, img, axsimg, jpost, pixelspacing, tmp)
197+
# Fig. 2, lines 14-20
198+
axespre = truncatet(axsimg, jpost) # first N-K-1 axes (these are "finished" within each K+1-dimensional slice)
199+
for jpre in CartesianIndices(axespre)
200+
voronoift!(F, img, jpre, jpost, pixelspacing, tmp) # finish axis N-K in K-dimensional slice `jpost`
201+
end
202+
return F
203+
end
204+
205+
function voronoift!(F, img, jpre, jpost, pixelspacing, tmp)
206+
d = length(jpre) + 1 # axis to work along
207+
= nullindex(F)
208+
empty!(tmp)
209+
for i in axes(img, d)
210+
# Fig 3, lines 3-13
211+
xi = CartesianIndex(jpre, i, jpost)
212+
@inbounds fi = F[xi]
213+
if fi !=
214+
fidist = DistRFT(fi, pixelspacing, jpre, jpost)
215+
if length(tmp) < 2
216+
push!(tmp, fidist)
217+
else
218+
@inbounds while length(tmp) >= 2 && removeft(tmp[end - 1], tmp[end], fidist)
219+
pop!(tmp)
220+
end
221+
push!(tmp, fidist)
222+
end
223+
end
224+
end
225+
nS = length(tmp)
226+
nS == 0 && return F
227+
# Fig 3, lines 18-24
228+
l = 1
229+
@inbounds fthis = tmp[l].fi
230+
for i in axes(img, d)
231+
xi = CartesianIndex(jpre, i, jpost)
232+
d2this = wnorm2(xi - fthis, pixelspacing)
233+
while l < nS
234+
@inbounds fnext = tmp[l + 1].fi
235+
d2next = wnorm2(xi - fnext, pixelspacing)
236+
if d2this > d2next
237+
d2this, fthis = d2next, fnext
238+
l += 1
239+
else
240+
break
241+
end
242+
end
243+
@inbounds F[xi] = fthis
244+
end
245+
return F
246+
end
247+
248+
## Utilities
249+
250+
# Stores a feature location and its distance from the hyperplane Rd
251+
struct DistRFT{N,T}
252+
fi::CartesianIndex{N}
253+
dist2::T
254+
d::Int # the coordinate in dimension d
255+
end
256+
257+
"""
258+
DistRFT(fi::CartesianIndex, w, jpre, jpost)
259+
260+
Bundles a feature `fi` together with its distance from the line Rd,
261+
where Rd is specified by `(jpre..., :, jpost...)`. `w` is the
262+
weighting applied to each coordinate, and must be `nothing` or be a
263+
tuple with the same number of coordiantes as `fi`.
264+
265+
"""
266+
function DistRFT(fi::CartesianIndex, w, jpre::CartesianIndex, jpost::CartesianIndex)
267+
d2pre, ipost, wpost = dist2pre(fi.I, w, jpre.I)
268+
d2post = wnorm2(CartesianIndex(ipost) - jpost, wpost)
269+
@inbounds fid = fi[length(jpre) + 1]
270+
return DistRFT(fi, d2pre + d2post, fid)
271+
end
272+
function DistRFT(fi::CartesianIndex, w, jpre::Tuple, jpost::Tuple)
273+
return DistRFT(fi, w, CartesianIndex(jpre), CartesianIndex(jpost))
274+
end
275+
276+
@inline function removeft(u, v, w)
277+
a, b, c = v.d - u.d, w.d - v.d, w.d - u.d
278+
return c * v.dist2 - b * u.dist2 - a * w.dist2 > a * b * c
279+
end
280+
281+
"""
282+
truncatet(inds, j::CartesianIndex{K})
283+
284+
Discard the last `K+1` elements of the tuple `inds`.
285+
"""
286+
truncatet(inds, j::CartesianIndex) = _truncatet((), inds, j)
287+
_truncatet(out, inds::NTuple{N}, j::CartesianIndex{N}) where {N} = Base.front(out)
288+
@inline _truncatet(out, inds, j) = _truncatet((out..., inds[1]), Base.tail(inds), j)
289+
290+
nullindex(A::AbstractArray{T,N}) where {T,N} = typemin(Int) * oneunit(CartesianIndex{N})
291+
292+
"""
293+
wnorm2(x::CartesianIndex, w)
294+
295+
Compute `∑ (w[i]*x[i])^2`. Specifying `nothing` for `w` is equivalent to `w = (1,1,...)`.
296+
"""
297+
wnorm2(x::CartesianIndex, w) = _wnorm2(0, x.I, w)
298+
_wnorm2(s, ::Tuple{}, ::Nothing) = s
299+
_wnorm2(s, ::Tuple{}, ::Tuple{}) = s
300+
@inline _wnorm2(s, x, w::Nothing) = _wnorm2(s + sqr(x[1]), Base.tail(x), w)
301+
@inline _wnorm2(s, x, w) = _wnorm2(s + sqr(w[1] * x[1]), Base.tail(x), Base.tail(w))
302+
303+
"""
304+
dist2pre(x, w, jpre) -> s, xpost, wpost
305+
306+
`s` is equivalent to `wnorm2(x[1:length(jpre)]-jpre, w)`. `xpost` and
307+
`wpost` contain the trailing indices of `x` and `w` (skipping the
308+
element `length(jpre)+1`).
309+
"""
310+
dist2pre(x::Tuple, w, jpre) = _dist2pre(0, x, w, jpre)
311+
_dist2pre(s, x, w::Nothing, ::Tuple{}) = s, Base.tail(x), w
312+
_dist2pre(s, x, w, ::Tuple{}) = s, Base.tail(x), Base.tail(w)
313+
@inline function _dist2pre(s, x, w::Nothing, jpre)
314+
return _dist2pre(s + sqr(x[1] - jpre[1]), Base.tail(x), w, Base.tail(jpre))
315+
end
316+
@inline function _dist2pre(s, x, w, jpre)
317+
return _dist2pre(s + sqr(w[1] * (x[1] - jpre[1])), Base.tail(x), Base.tail(w), Base.tail(jpre))
318+
end
319+
320+
@inline sqr(x) = x * x

src/imgmorph/cc2bw.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
"""
3+
function cc2bw(cc::GMTConComp)
4+
# Create a binary image of the same size as the original
5+
I = mat2img(zeros(Bool, cc.image_size), x=cc.x, y=cc.y, inc=cc.inc, layout=cc.layout,
6+
is_transposed=(cc.layout[2] == 'R'), proj4=cc.proj4, wkt=cc.wkt, epsg=cc.epsg)
7+
8+
# Mark pixels belonging to any connected component as true
9+
for pixel_list in cc.pixel_list
10+
for rc in pixel_list
11+
I[rc] = true
12+
end
13+
end
14+
I.range[6] = 1.0
15+
return I
16+
end
17+

0 commit comments

Comments
 (0)