Skip to content

Commit 18ed6fb

Browse files
committed
feature_transform: add multithreading
If Julia is being run with multiple threads, this now will default to using all threads in `feature_transform`. The multithreaded implementation is relatively straightforward, although the recursive algorithm makes it a little hard to wrap your head around: - `computeft!` for dimension `d` is dependent only on slices of dimensions `1:d`, and indeed for all except the final call to `voronoift!` just on dimensions `1:d-1`. Hence you can divide the last dimension `N` into chunks and give each to a thread. - For the final `voronoift!` call along dimension `N`, it's a one-dimensional operation along this dimension. Hence you can just split the next-to-last dimension into chunks instead. This change was responsible for performance improvements in `distance_transform` noted in JuliaImages/image_benchmarks#1. Julia 1.3 is required for `@spawn`
1 parent 93aa1f8 commit 18ed6fb

File tree

6 files changed

+69
-31
lines changed

6 files changed

+69
-31
lines changed

.github/workflows/UnitTest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
fail-fast: false
1515
matrix:
1616
matrix:
17-
julia-version: ['1.0', '1', 'nightly']
17+
julia-version: ['1.3', '1', 'nightly']
1818
os: [ubuntu-latest]
1919
arch: [x64]
2020
include:

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ TiledIteration = "06e1c1a7-607b-532d-9fad-de7d9aa2abac"
1212
Documenter = "0.24, 0.25"
1313
ImageCore = "0.9"
1414
Requires = "1"
15-
TiledIteration = "0.2, 0.3"
16-
julia = "1"
15+
TiledIteration = "0.3.1"
16+
julia = "1.3"
1717

1818
[extras]
1919
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"

src/ImageMorphology.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ module ImageMorphology
33
using ImageCore
44
using ImageCore: GenericGrayImage
55
using LinearAlgebra
6-
using Base.Cartesian # TODO: delete this
7-
using TiledIteration: EdgeIterator
6+
using TiledIteration: EdgeIterator, SplitAxis, SplitAxes
87
using Requires
98

109
include("convexhull.jl")

src/bwdist.jl

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
module FeatureTransform
22

3+
using ..ImageMorphology: SplitAxis, SplitAxes
4+
using ..ImageCore
5+
36
export feature_transform, distance_transform
47

58
"""
6-
feature_transform(I::AbstractArray{Bool, N}, [w=nothing]) -> F
9+
feature_transform(img::AbstractArray{Bool, N};
10+
weights=nothing, nthreads=Threads.nthreads()) -> F
711
812
Compute the feature transform of a binary image `I`, finding the
913
closest "feature" (positions where `I` is `true`) for each location in
@@ -26,20 +30,45 @@ See also: [`distance_transform`](@ref).
2630
Transforms of Binary Images in Arbitrary Dimensions' [Maurer et al.,
2731
2003] (DOI: 10.1109/TPAMI.2003.1177156)
2832
"""
29-
function feature_transform(I::AbstractArray{Bool,N}, w::Union{Nothing,NTuple{N}}=nothing) where N
33+
function feature_transform(img::AbstractArray{<:Union{Bool,AbstractGray{Bool}},N};
34+
weights::Union{Nothing,NTuple{N}}=nothing,
35+
nthreads::Int = length(img) < 1000 ? 1 : Threads.nthreads()) where N
36+
nthreads > 0 || error("the number of threads must be positive")
37+
N == 0 && return reshape([CartesianIndex()])
38+
# Allocate the output
39+
F = similar(img, CartesianIndex{N})
40+
axsimg = axes(img)
3041
# To allocate temporary storage for voronoift!, compute one
3142
# element (so we have the proper type)
32-
fi = first(CartesianIndices(axes(I)))
33-
drft = DistRFT(fi, w, (), Base.tail(fi.I))
34-
tmp = Vector{typeof(drft)}()
35-
36-
# Allocate the output
37-
F = similar(I, CartesianIndex{N})
38-
39-
# Compute the feature transform (recursive algorithm)
40-
computeft!(F, I, CartesianIndex(()), w, tmp)
41-
42-
F
43+
fi = first(CartesianIndices(axsimg))
44+
drft = DistRFT(fi, weights, (), Base.tail(fi.I))
45+
if nthreads == 1
46+
tmp = typeof(drft)[]
47+
computeft!(F, img, axsimg, CartesianIndex(()), weights, tmp)
48+
# Finish the last dimension (for threading, we avoid doing it in computeft!)
49+
finishft!(F, img, axsimg, CartesianIndex(()), weights, tmp)
50+
else
51+
tasks = Task[]
52+
tmps = [typeof(drft)[] for _ = 1:nthreads]
53+
saxs = SplitAxes(axsimg, nthreads - 0.2) # give main thread less work since it also schedules the others
54+
for i = nthreads:-1:2
55+
push!(tasks, Threads.@spawn computeft!(F, img, saxs[i], CartesianIndex(()), weights, tmps[i]))
56+
end
57+
computeft!(F, img, saxs[1], CartesianIndex(()), weights, tmps[1])
58+
while !isempty(tasks)
59+
wait(pop!(tasks))
60+
end
61+
# Finish the last dimension
62+
saxs1 = SplitAxes(axsimg[1:N-1], nthreads - 0.2)
63+
for i = nthreads:-1:2
64+
push!(tasks, Threads.@spawn finishft!(F, img, (saxs1[i]..., axsimg[end]), CartesianIndex(()), weights, tmps[i]))
65+
end
66+
finishft!(F, img, (saxs1[1]..., axsimg[end]), CartesianIndex(()), weights, tmps[1])
67+
while !isempty(tasks)
68+
wait(pop!(tasks))
69+
end
70+
end
71+
return F
4372
end
4473

4574
"""
@@ -68,33 +97,38 @@ function distance_transform(F::AbstractArray{CartesianIndex{N},N}, w::Union{Noth
6897
D
6998
end
7099

71-
function computeft!(F, I, jpost::CartesianIndex{K}, pixelspacing, tmp) where K
100+
function computeft!(F, img, axsimg, jpost::CartesianIndex{K}, pixelspacing, tmp) where K
72101
# tmp is workspace for voronoift!
73102
= nullindex(F) # sentinel position
74-
if K == ndims(I)-1 # innermost loop (d=1 case, line 1)
103+
if K == ndims(img)-1 # innermost loop (d=1 case, line 1)
75104
# Fig. 2, lines 2-8
76-
@inbounds @simd for i1 in axes(I, 1)
77-
F[i1, jpost] = I[i1, jpost] ? CartesianIndex(i1, jpost) :
105+
@inbounds @simd for i1 in axes(img, 1)
106+
F[i1, jpost] = gray(img[i1, jpost]) ? CartesianIndex(i1, jpost) :
78107
end
79108
else # recursively handle trailing dimensions
80109
# Fig. 2, lines 10-12
81-
for i1 in axes(I, ndims(I) - K)
82-
computeft!(F, I, CartesianIndex(i1, jpost), pixelspacing, tmp)
110+
for i1 in axsimg[ndims(img) - K]
111+
computeft!(F, img, axsimg, CartesianIndex(i1, jpost), pixelspacing, tmp)
83112
end
84113
end
114+
K == 0 && return F # for the final axis, threads will be split across next-to-last axis
115+
return finishft!(F, img, axsimg, jpost, pixelspacing, tmp)
116+
end
117+
118+
function finishft!(F, img, axsimg, jpost, pixelspacing, tmp)
85119
# Fig. 2, lines 14-20
86-
axespre = truncatet(axes(F), jpost) # discard the trailing axes of F
120+
axespre = truncatet(axsimg, jpost) # first N-K-1 axes (these are "finished" within each K+1-dimensional slice)
87121
for jpre in CartesianIndices(axespre)
88-
voronoift!(F, I, jpre, jpost, pixelspacing, tmp)
122+
voronoift!(F, img, jpre, jpost, pixelspacing, tmp) # finish axis N-K in K-dimensional slice `jpost`
89123
end
90-
F
124+
return F
91125
end
92126

93-
function voronoift!(F, I, jpre, jpost, pixelspacing, tmp)
94-
d = length(jpre)+1
127+
function voronoift!(F, img, jpre, jpost, pixelspacing, tmp)
128+
d = length(jpre)+1 # axis to work along
95129
= nullindex(F)
96130
empty!(tmp)
97-
for i in axes(I, d)
131+
for i in axes(img, d)
98132
# Fig 3, lines 3-13
99133
xi = CartesianIndex(jpre, i, jpost)
100134
@inbounds fi = F[xi]
@@ -115,7 +149,7 @@ function voronoift!(F, I, jpre, jpost, pixelspacing, tmp)
115149
# Fig 3, lines 18-24
116150
l = 1
117151
@inbounds fthis = tmp[l].fi
118-
for i in axes(I, d)
152+
for i in axes(img, d)
119153
xi = CartesianIndex(jpre, i, jpost)
120154
d2this = wnorm2(xi-fthis, pixelspacing)
121155
while l < nS

src/deprecations.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
@deprecate label_components!(out::AbstractArray{Int,N}, A::AbstractArray{T,N}, connectivity::Array{Bool,N}, bkg) where {T,N} label_components!(out, A, connectivity; bkg=bkg)
55

66
@deprecate imfill(img::AbstractArray{Bool}, interval::Tuple{Real,Real}, dims::Union{Dims, AbstractVector{Int}}) imfill(img, interval; dims=dims)
7+
8+
import .FeatureTransform: feature_transform
9+
@deprecate feature_transform(img, weights; kwargs...) feature_transform(img; weights=weights, kwargs...)

test/bwdist.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
@test F == ind2cart([1 1 1; 2 2 13; 2 8 13; 4 9 14; 4 10 10])
6969
D = distance_transform(F)
7070
@test D == [0.0 1.0 2.0; 0.0 1.0 1.0; 1.0 0.0 0.0; 0.0 0.0 0.0; 1.0 0.0 1.0]
71+
72+
@test feature_transform(Gray.(A)) == F
7173
end
7274

7375
@testset "Corner Case Images" begin

0 commit comments

Comments
 (0)