using DistanceTransforms: transform, boolean_indicator
using CairoMakie: Figure, Axis, heatmap!, hidedecorations!
# Create a random binary array
= rand([0, 1], 10, 10)
arr
# Apply distance transform
= transform(boolean_indicator(arr))
transformed
# Create visualization
= Figure(size = (800, 400))
fig = Axis(fig[1, 1], title = "Original")
ax1 = Axis(fig[1, 2], title = "Distance Transform")
ax2 heatmap!(ax1, arr, colormap = :grays)
heatmap!(ax2, transformed, colormap = :grays)
fig
User Guide
This guide covers both basic and advanced usage of DistanceTransforms.jl.
Getting Started
Installation
using Pkg
Pkg.add("DistanceTransforms")
Basic Usage
The primary function in DistanceTransforms.jl is transform
. This function processes an array of 0s and 1s, converting each background element (0) into a value representing its squared Euclidean distance to the nearest foreground element (1).
Real-World Example
Let’s apply a distance transform to a real image:
using Images
# Download and load example image
= load(download("http://docs.opencv.org/3.1.0/water_coins.jpg"))
img
# Convert to binary image
= Gray.(img) .> 0.5
img_bw
# Apply distance transform
= transform(boolean_indicator(img_bw))
img_tfm
# Visualize
= Figure(size = (900, 300))
fig = Axis(fig[1, 1], title = "Original Image")
ax1 = Axis(fig[1, 2], title = "Segmented Image")
ax2 = Axis(fig[1, 3], title = "Distance Transform")
ax3 heatmap!(ax1, rotr90(img), colormap = :grays)
heatmap!(ax2, rotr90(img_bw), colormap = :grays)
heatmap!(ax3, rotr90(img_tfm), colormap = :grays)
hidedecorations!.([ax1, ax2, ax3])
fig
Understanding Euclidean Distance
The library, by default, returns the squared Euclidean distance. If you need the true Euclidean distance, you can take the square root of each element:
# Create sample binary array
= [
array2 0 1 1 0 1
0 0 0 1 0
1 1 0 0 0
]
# Convert to boolean indicator
= boolean_indicator(array2)
array2_bool
# Apply squared Euclidean distance transform
= transform(array2_bool)
sq_euc_transform
# Convert to true Euclidean distance
= sqrt.(sq_euc_transform)
euc_transform
# Display results
println("Squared Euclidean Distance: $(sq_euc_transform)")
println("Euclidean Distance: $(euc_transform)")
Squared Euclidean Distance: Float32[1.0 0.0 0.0 1.0 0.0; 1.0 1.0 1.0 0.0 1.0; 0.0 0.0 1.0 1.0 2.0]
Euclidean Distance: Float32[1.0 0.0 0.0 1.0 0.0; 1.0 1.0 1.0 0.0 1.0; 0.0 0.0 1.0 1.0 1.4142135]
Comparison with ImageMorphology.jl
using ImageMorphology: distance_transform, feature_transform
# Apply ImageMorphology distance transform
= distance_transform(feature_transform(Bool.(array2)))
euc_transform2
# Compare results
println("Are the results approximately equal?")
isapprox(euc_transform2, euc_transform; rtol = 1e-2)
Are the results approximately equal?
true
Advanced Features
Multi-threading
DistanceTransforms.jl efficiently utilizes multi-threading, particularly in its Felzenszwalb distance transform algorithm.
⚠️ Julia might only load with 1 thread
which makes the actual benchmark below meaningless and potentially confusing. Use this for understanding how to use the threaded
kwarg ⚠️
using BenchmarkTools
using Base.Threads: nthreads
# Create a random binary array
= boolean_indicator(rand([0, 1], 1000, 1000))
x
# Single-threaded benchmark
= @benchmark transform($x; threaded = false)
single_threaded
# Multi-threaded benchmark
= @benchmark transform($x; threaded = true)
multi_threaded
# Display results
println("Number of threads: $(nthreads())")
println("Single-threaded minimum time: $(minimum(single_threaded).time / 1e6) ms")
println("Multi-threaded minimum time: $(minimum(multi_threaded).time / 1e6) ms")
println("Speedup factor: $(minimum(single_threaded).time / minimum(multi_threaded).time)")
Number of threads: 1
Single-threaded minimum time: 17.508084 ms
Multi-threaded minimum time: 17.558 ms
Speedup factor: 0.9971570793940084
GPU Acceleration
DistanceTransforms.jl extends its performance capabilities with GPU acceleration. The library uses Julia’s multiple dispatch to automatically leverage GPU resources when available.
CUDA Example
using CUDA
using DistanceTransforms
# Create a random array on GPU
= CUDA.rand([0, 1], 1000, 1000)
x_gpu = boolean_indicator(x_gpu)
x_gpu
# The transform function automatically uses GPU
= transform(x_gpu)
result_gpu
# Transfer result back to CPU if needed
= Array(result_gpu) result_cpu
Metal Example
using Metal
using DistanceTransforms
# Create a random array on GPU
= Metal.rand([0, 1], 1000, 1000)
x_gpu = boolean_indicator(x_gpu)
x_gpu
# The transform function automatically uses GPU
= transform(x_gpu)
result_gpu
# Transfer result back to CPU if needed
= Array(result_gpu) result_cpu
Performance Benchmarks
Performance comparison across different implementations for 2D and 3D distance transforms:
As shown in the graph, the GPU implementation demonstrates superior performance, especially for larger arrays.
Best Practices
- For small arrays (<100x100): CPU with multi-threading is often sufficient
- For medium arrays: Multi-threaded CPU may be faster than GPU due to lower overhead
- For large arrays (>1000x1000): GPU acceleration provides the best performance
- For 3D data: GPU acceleration is strongly recommended due to the computational complexity
Algorithm Details
On the CPU, DistanceTransforms.jl uses the squared Euclidean distance transform algorithm by Felzenszwalb and Huttenlocher, known for its accuracy and efficiency. On the GPU, thanks to the amazing AcceleratedKernels.jl and KernelAbstraction.jl packages, the algorithm is essentially identical.