|
| 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 |
0 commit comments