Skip to content

Commit

Permalink
Merge pull request #2 from seung-lab/wms_multilabel_dilate
Browse files Browse the repository at this point in the history
feat: multilabel dilation, erosion, opening, and closing
  • Loading branch information
william-silversmith committed Dec 18, 2023
2 parents 2fc0435 + 1306a6f commit bbdc368
Show file tree
Hide file tree
Showing 11 changed files with 883 additions and 4 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Build Wheels

on:
workflow_dispatch:
push:
tags:
- '*'
env:
CIBW_SKIP: cp27-* cp33-* cp34-* cp35-* pp27* pp36* pp37* pp38* pp39* pp310* *-musllinux* cp312-manylinux_i686

jobs:
build_wheels:
name: Build wheels on ${{matrix.arch}} for ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-2019, macos-latest]
arch: [auto]
include:
- os: ubuntu-latest
arch: aarch64

steps:
- uses: actions/checkout@v2

- name: Set up QEMU
if: ${{ matrix.arch == 'aarch64' }}
uses: docker/setup-qemu-action@v1

- name: Build wheels
uses: pypa/cibuildwheel@v2.16.2
# to supply options, put them in 'env', like:
env:
CIBW_ARCHS_LINUX: ${{matrix.arch}}
CIBW_BEFORE_BUILD: pip install oldest-supported-numpy pybind11 wheel

- uses: actions/upload-artifact@v2
with:
path: ./wheelhouse/*.whl
2 changes: 1 addition & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
python -m pip install pytest pybind11
- name: Compile
run: python setup.py develop
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ import fastmorph
# may be binary or unsigned integer 2D or 3D image
labels = np.load("my_labels.npy")


# multi-label capable morphological operators
# they use a 3x3x3 all on structuring element
# dilate picks the mode of surrounding labels

# by default only background (0) labels are filled
morphed = fastmorph.dilate(labels, parallel=2)
# processes every voxel
morphed = fastmorph.dilate(labels, background_only=False, parallel=2)

morphed = fastmorph.erode(labels)
morphed = fastmorph.opening(labels, parallel=2)
morphed = fastmorph.closing(labels, parallel=2)

# Dilate only supports binary images at this time.
# Radius is specified in physical units, but
# by default anisotropy = (1,1,1) so it is the
Expand Down
54 changes: 54 additions & 0 deletions automated_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,57 @@ def test_spherical_close():
res = fastmorph.spherical_close(labels, radius=1)
assert res[5,5,5] == True


def test_dilate():
labels = np.zeros((3,3,3), dtype=bool)

out = fastmorph.dilate(labels)
assert not np.any(out)

labels[1,1,1] = True

out = fastmorph.dilate(labels)
assert np.all(out)

labels = np.zeros((3,3,3), dtype=bool)
labels[0,0,0] = True
out = fastmorph.dilate(labels)

ans = np.zeros((3,3,3), dtype=bool)
ans[:2,:2,:2] = True

assert np.all(out == ans)

labels = np.zeros((3,3,3), dtype=int)
labels[0,1,1] = 1
labels[2,1,1] = 2

out = fastmorph.dilate(labels)
ans = np.ones((3,3,3), dtype=int)
ans[2,:,:] = 2
assert np.all(ans == out)

labels = np.zeros((3,3,3), dtype=int, order="F")
labels[0,1,1] = 1
labels[1,1,1] = 2
labels[2,1,1] = 2

out = fastmorph.dilate(labels)
ans = np.ones((3,3,3), dtype=int, order="F")
ans[1:,:,:] = 2
assert np.all(ans == out)


def test_erode():
labels = np.ones((3,3,3), dtype=bool)
out = fastmorph.erode(labels)
assert np.sum(out) == 1 and out[1,1,1] == True

out = fastmorph.erode(out)
assert not np.any(out)

out = fastmorph.erode(out)
assert not np.any(out)



16 changes: 16 additions & 0 deletions buildmac.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/zsh

source ~/.zprofile

function compile_wheel {
workon fm$1
pip install oldest-supported-numpy pybind11 setuptools wheel
pip install edt fastremap fill-voids connected-components-3d
python setup.py bdist_wheel
}

compile_wheel 38
compile_wheel 39
compile_wheel 310
compile_wheel 311
compile_wheel 312
Binary file added connectomics.npy.ckl.gz
Binary file not shown.
80 changes: 79 additions & 1 deletion fastmorph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,87 @@
import fill_voids
import cc3d
import fastremap
import multiprocessing as mp

import fastmorphops

AnisotropyType = Optional[Sequence[int]]

def dilate(
labels:np.ndarray,
background_only:bool = True,
parallel:int = 1,
) -> np.ndarray:
"""
Dilate forground labels using a 3x3x3 stencil with
all elements "on".
The mode of the voxels surrounding the stencil wins.
labels: a 3D numpy array containing integer labels
representing shapes to be dilated.
background_only:
True: Only evaluate background voxels for dilation.
False: Allow labels to erode each other as they grow.
parallel: how many pthreads to use in a threadpool
"""
if parallel == 0:
parallel = mp.cpu_count()
parallel = min(parallel, mp.cpu_count())

labels = np.asfortranarray(labels)
while labels.ndim < 3:
labels = labels[..., np.newaxis]
output = fastmorphops.dilate(labels, background_only, parallel)
return output.view(labels.dtype)

def erode(labels:np.ndarray, parallel:int = 1) -> np.ndarray:
"""
Erodes forground labels using a 3x3x3 stencil with
all elements "on".
labels: a 3D numpy array containing integer labels
representing shapes to be dilated.
"""
if parallel == 0:
parallel = mp.cpu_count()
parallel = min(parallel, mp.cpu_count())

labels = np.asfortranarray(labels)
while labels.ndim < 3:
labels = labels[..., np.newaxis]
output = fastmorphops.erode(labels, parallel)
return output.view(labels.dtype)

def opening(
labels:np.ndarray,
background_only:bool = True,
parallel:int = 1,
) -> np.ndarray:
"""Performs morphological opening of labels.
background_only is passed through to dilate.
True: Only evaluate background voxels for dilation.
False: Allow labels to erode each other as they grow.
parallel: how many pthreads to use in a threadpool
"""
return dilate(erode(labels, parallel), background_only, parallel)

def closing(
labels:np.ndarray,
background_only:bool = True,
) -> np.ndarray:
"""Performs morphological closing of labels.
background_only is passed through to dilate.
True: Only evaluate background voxels for dilation.
False: Allow labels to erode each other as they grow.
parallel: how many pthreads to use in a threadpool
"""
return erode(dilate(labels, background_only, parallel), parallel)

def spherical_dilate(
labels:np.ndarray,
radius:float = 1.0,
Expand All @@ -26,7 +104,7 @@ def spherical_dilate(
anisotropy: voxel resolution in x, y, and z
in_place: save memory by modifying labels directly instead of creating a new image
Returns: dilated binary iamge
Returns: dilated binary image
"""
assert np.issubdtype(labels.dtype, bool), "Dilation is currently only supported for binary images."
dt = edt.edt(labels == 0, parallel=parallel, anisotropy=anisotropy)
Expand Down
Loading

0 comments on commit bbdc368

Please sign in to comment.