Skip to content

Commit 2142d8e

Browse files
Initial implementation and benchmarks for distance transform (#35)
* Initial implementation and benchmarks for distance transform * Optimize distance transform
1 parent 48260ef commit 2142d8e

11 files changed

Lines changed: 1756 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ nanobind_add_module(_core
1313
NB_STATIC
1414
src/bindings/affinities.cxx
1515
src/bindings/blocking.cxx
16+
src/bindings/distance.cxx
1617
src/bindings/module.cxx
1718
src/bindings/filters.cxx
1819
src/bindings/flow.cxx

MIGRATION_GUIDE.md

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1547,7 +1547,7 @@ Important differences from vigra and fastfilters:
15471547
largest → smallest. This matches `fastfilters`. To get vigra's
15481548
ascending order, reverse with `result[..., ::-1]`.
15491549
- No IIR / recursive Gaussian, no `convolve` / `recursiveFilter2D`, no
1550-
morphology, no distance transforms, no nonlinear diffusion, and no
1550+
morphology, no nonlinear diffusion, and no
15511551
non-local means in v1. Use `scipy.ndimage`, `skimage`, or the original
15521552
vigra/fastfilters bindings if you need those.
15531553

@@ -1562,6 +1562,81 @@ Implementation notes:
15621562
`detail/threading.hxx::parallel_for_chunks` without changing the
15631563
public API.
15641564

1565+
### Distance Transforms
1566+
1567+
`bioimage-cpp` exposes exact binary Euclidean distance transforms under
1568+
`bic.distance`. The implementation uses the separable
1569+
Felzenszwalb–Huttenlocher algorithm, complexity O(N · ndim), with optional
1570+
multithreading across the orthogonal lines of each axis sweep.
1571+
1572+
SciPy / vigra:
1573+
1574+
```python
1575+
from scipy import ndimage
1576+
dist = ndimage.distance_transform_edt(mask, sampling=(2.0, 1.0))
1577+
1578+
import vigra.filters as vf
1579+
vec = vf.vectorDistanceTransform(mask)
1580+
```
1581+
1582+
bioimage-cpp:
1583+
1584+
```python
1585+
import bioimage_cpp as bic
1586+
1587+
# One call can return any combination of distances, feature indices, and
1588+
# difference vectors — the C++ kernel computes them in a single sweep.
1589+
dist = bic.distance.distance_transform(mask, sampling=(2.0, 1.0))
1590+
dist, idx, vec = bic.distance.distance_transform(
1591+
mask,
1592+
sampling=(2.0, 1.0),
1593+
return_distances=True,
1594+
return_indices=True,
1595+
return_vectors=True,
1596+
)
1597+
1598+
# Short alias kept for parity with vigra; equivalent to the call above with
1599+
# return_distances=False, return_indices=False, return_vectors=True.
1600+
vec = bic.distance.vector_difference_transform(mask, sampling=(2.0, 1.0))
1601+
```
1602+
1603+
Name mapping:
1604+
1605+
| scipy / vigra name | bioimage-cpp name |
1606+
| --- | --- |
1607+
| `scipy.ndimage.distance_transform_edt` | `distance_transform` |
1608+
| `vigra.filters.vectorDistanceTransform` | `vector_difference_transform` |
1609+
1610+
Important differences:
1611+
1612+
- Distance-valued outputs are `float32`, not SciPy's `float64`. Indices are
1613+
`int32` with shape `(ndim, *mask.shape)` (matches SciPy's layout). Vectors
1614+
are `float32` with shape `(*mask.shape, ndim)`; components are sampled
1615+
displacements `(feature_coord - pixel_coord) * sampling[ax]` per axis.
1616+
- `distance_transform` follows SciPy's binary convention: nonzero values are
1617+
foreground and distances are measured to the nearest zero-valued element.
1618+
`bool` and `uint8` C-contiguous inputs are fast-pathed without a copy;
1619+
other dtypes are converted via `array != 0`.
1620+
- A single `distance_transform` call can return any non-empty subset of
1621+
`distances`, `indices`, and `vectors` via the corresponding
1622+
`return_distances` / `return_indices` / `return_vectors` flags. The result
1623+
is the array itself when only one output is requested, otherwise a tuple
1624+
in `(distances, indices, vectors)` order with omitted entries skipped.
1625+
- Pre-allocated output buffers are supported via the `distances=`,
1626+
`indices=`, and `vectors=` keyword arguments. They must be C-contiguous,
1627+
writable, of the documented shape and dtype, and are written into in
1628+
place. Pre-allocated outputs are excluded from the return value (matching
1629+
SciPy's convention); the call returns `None` if every requested output
1630+
was preallocated.
1631+
- `number_of_threads` selects the thread count for the per-axis sweep.
1632+
`1` (the default) is single-threaded; `0` uses
1633+
`std::thread::hardware_concurrency()`; positive values pin an explicit
1634+
count. Output is deterministic and bitwise identical across thread counts.
1635+
- For an all-foreground input (no zero-valued elements), the result matches
1636+
SciPy: distances and indices report a virtual background point at
1637+
axis-0 coordinate `-1` and `0` on all other axes. The first row of
1638+
`indices` will then contain `-1` everywhere.
1639+
15651640
## I/O and Build Dependencies
15661641

15671642
`bioimage-cpp` intentionally does not replace nifty or affogato I/O helpers.

0 commit comments

Comments
 (0)