Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5b1508c
fix: remove debug code
william-silversmith Oct 10, 2025
16dc0cb
feat: add morphological closing feature
william-silversmith Oct 10, 2025
8791997
fix: remove debug profile
william-silversmith Oct 10, 2025
e1fc1bb
redesign: get rid of morphological close, add 1 to labels
william-silversmith Oct 10, 2025
97a8962
fix: reduce "escape" for holes on borders by using 4 connectivty
william-silversmith Oct 10, 2025
9891310
fix: give an adjustable threshold for how much a hole can poke out
william-silversmith Oct 10, 2025
8e3c5bf
fix: got this mostly working
william-silversmith Oct 11, 2025
8615154
fix: preserve labels as "filled" if they simply are touching a lot of…
william-silversmith Oct 11, 2025
4a886f2
fix: ensure bg contacts have non-zero surface contact
william-silversmith Oct 11, 2025
b3bce73
perf: reduce peak memory usage
william-silversmith Oct 11, 2025
81c95bb
perf: use crackle for low memory representations
william-silversmith Oct 11, 2025
7131633
feat: make crackle-codec an optional dependency
william-silversmith Oct 11, 2025
e3e8a74
fix: switch from Sequence to Iterator
william-silversmith Oct 12, 2025
49e27cd
docs: add docstring to fill_holes_v2
william-silversmith Oct 12, 2025
51f00f9
refactor: add fill_holes_v1 as alias for fill_holes
william-silversmith Oct 12, 2025
2f6ca85
fix: trying to replicate exactly hole filling semantics
william-silversmith Oct 14, 2025
e9c7757
fix: remove debug code
william-silversmith Oct 14, 2025
4b437dd
fix: remove debug code
william-silversmith Oct 14, 2025
9af3bca
fix: matched scipy! found a bug in fill_voids?
william-silversmith Oct 14, 2025
8a3784d
feat: add merge threshold back in
william-silversmith Oct 14, 2025
2d4e590
fix: referencing non-existent edges
william-silversmith Oct 14, 2025
197ebb3
fix: incorrect optimization
william-silversmith Oct 14, 2025
2c93b3a
fix: incorrect threshold direction
william-silversmith Oct 14, 2025
03b8e51
feat: allow closing of background on border
william-silversmith Oct 14, 2025
d212bf5
ci: update cibuildwheel
william-silversmith Oct 15, 2025
402c0aa
fix: bool dtype
william-silversmith Oct 15, 2025
13e5e22
fix: return proper dtype
william-silversmith Oct 15, 2025
055dc47
test: check fill_holes_v2 for correctness
william-silversmith Oct 15, 2025
9ad2031
install: add scipy to dev requirements
william-silversmith Oct 15, 2025
3fae74f
ci: install scipy
william-silversmith Oct 15, 2025
25d82f5
perf: add special handling for binary images
william-silversmith Oct 16, 2025
f4520ec
refactor: remove unneeded special handling for binary images
william-silversmith Oct 16, 2025
16c0107
docs: describe hole filling algorithm
william-silversmith Oct 16, 2025
17e22ba
fix: discard the sentinel
william-silversmith Oct 17, 2025
ecdd1e5
feat: perform real hole filling in 2d
william-silversmith Oct 19, 2025
92efc62
fix: treat zero correctly
william-silversmith Oct 19, 2025
0dea782
fix: make everything work with fixed borders
william-silversmith Oct 19, 2025
4e02a53
refactor: remove unreachable branch
william-silversmith Oct 20, 2025
e47b5b7
fix: don't reprocess an already processed hole
william-silversmith Oct 20, 2025
9edde45
fix: removed explored hole group
william-silversmith Oct 20, 2025
c0a430d
fix: type annotation and return type for edge hole
william-silversmith Oct 20, 2025
c146a6c
perf: handle large hole groups more efficiently
william-silversmith Oct 20, 2025
5b6cf45
fix: more suitable variable
william-silversmith Oct 20, 2025
be0083b
fix: better annotations
william-silversmith Oct 20, 2025
2bec4dc
docs: describe hole filling techniques
william-silversmith Oct 20, 2025
943d361
fix: ensure holes touch the border properly
william-silversmith Oct 20, 2025
b464eb1
fix: don't allow setting return_crackle when crackle is not present
william-silversmith Oct 20, 2025
f4d7b8f
perf: delete remap variable after use
william-silversmith Oct 20, 2025
e97348d
fix: harmonize anisotropy type and generalize to float
william-silversmith Oct 21, 2025
32b8be9
fix: handle fill holes on a solid color
william-silversmith Oct 21, 2025
dec4c9e
fix: fill holes on no color
william-silversmith Oct 21, 2025
4a1be8a
test: check if fix borders works right
william-silversmith Oct 21, 2025
085ae8e
fix: handle fix_borders without skipping over labels that are fully f…
william-silversmith Oct 21, 2025
fccc900
test: check zero image works
william-silversmith Oct 21, 2025
141d955
docs: correct the documentation
william-silversmith Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
uses: docker/setup-qemu-action@v1

- name: Build wheels
uses: pypa/cibuildwheel@v3.1.4
uses: pypa/cibuildwheel@v3.2.0
# to supply options, put them in 'env', like:
env:
CIBW_ARCHS_LINUX: ${{matrix.arch}}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest pybind11 setuptools wheel crackle-codec
python -m pip install pytest pybind11 setuptools wheel crackle-codec scipy
python -m pip install connected-components-3d edt fastremap numpy tqdm fill-voids

- name: Compile
Expand Down
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,53 @@ morphed = fastmorph.spherical_close(labels, radius=1, parallel=2, anisotropy=(1,
# The rest support multilabel images.
morphed = fastmorph.spherical_erode(labels, radius=1, parallel=2, anisotropy=(1,1,1))


# Rapid multilabel hole filling. There are two versions that use different techniques
# and have different interfaces for their "aggressive" modes. Both modes fill
# holes appropriately by default.
#
# Generally speaking, fill_holes_v2 will be much faster. v2 uses a
# mostly linear time contact graph analysis. v1 analyzes a sequence
# of binary images. v2 exhibits much better scaling behavior and supports
# returning the filled and hole labels as CrackleArray compressed objects
# to save memory.
#
# The main advantage of v1 is that it includes a morphological closure mode
# that operates on voxels for closing small holes. The downside is that this
# can modify the surface of the object.
#
# v2 allows merging holes that are less than 100% closed, but if this
# threshold is set too high, holes won't be closed. If it is too low,
# improper merging can occur.
#
# In both methods, objects that contact the sides or more than one side
# (in the case of fix_borders) cannot be merged.

filled_labels, hole_labels = fastmorph.fill_holes_v2(labels)
# requires: pip install crackle-codec
# returns as compressed CrackleArrays that have speedy access to labels
# in the compressed state (often hundreds of times smaller than the full array)
filled_labels, hole_labels = fastmorph.fill_holes_v2(labels, return_crackle=True)

# fix_borders runs hole filling for each object on the edge to reduce edge contacts
filled_labels, hole_labels = fastmorph.fill_holes_v2(labels, fix_borders=True)

# merge_threshold (range 0.0 - 1.0) controls how much surface area can be
# "exposed" for a hole to still be filled. The default (1.0) means a hole
# must be perfectly sealed (typical for hole filling algorithms).
filled_labels, hole_labels = fastmorph.fill_holes_v2(labels, merge_threshold=0.97)

# Note: for boolean images, this function will directly call fill_voids
# and return a scalar for ct
# For integer images, more processing will be done to deal with multiple labels.
# A dict of { label: num_voxels_filled } for integer images will be returned.
# Note that for multilabel images, by default, if a label is totally enclosed by another,
# a FillError will be raised. If remove_enclosed is True, the label will be overwritten.
filled_labels, ct = fastmorph.fill_holes(labels, return_fill_count=True, remove_enclosed=False)
filled_labels, ct = fastmorph.fill_holes_v1(labels, return_fill_count=True, remove_enclosed=False)

# If the holes in your segmentation are imperfectly sealed, consider
# using the following options.
filled_labels = fastmorph.fill_holes(
filled_labels = fastmorph.fill_holes_v1(
labels,
# runs 2d fill on the sides of the cube for each binary image
fix_borders=True,
Expand Down
85 changes: 85 additions & 0 deletions automated_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import time

import pytest
import numpy as np
import fastmorph
import scipy.ndimage

def test_spherical_dilate():
labels = np.zeros((10,10,10), dtype=bool)
Expand Down Expand Up @@ -109,6 +112,88 @@ def test_complex_fill():

assert np.all(res == ans)

def test_fill_holes_v2():
labels = np.zeros((10,10,10), dtype=np.uint64)
filled, holes = fastmorph.fill_holes_v2(labels)
assert not np.any(filled)
assert not np.any(holes)

labels = np.ones((10,10,10), dtype=np.uint8)
labels[:,:,:5] = 2

labels[5,5,2] = 0
labels[5,5,7] = 0

assert np.count_nonzero(labels) == 998
filled, holes = fastmorph.fill_holes_v2(labels)
assert np.count_nonzero(filled) == 1000
assert list(np.unique(filled)) == [1,2]

assert filled[5,5,2] == 2
assert filled[5,5,7] == 1

labels = np.ones((10,10,10), dtype=np.uint32)
labels[5,5,2] = 777

filled, holes = fastmorph.fill_holes_v2(labels, return_crackle=True)
assert set(holes.labels()) == set([0,777])

labels = np.ones((10,10,10), dtype=bool)
labels[5,5,2] = 0

res, holes = fastmorph.fill_holes_v2(labels)
assert np.all(res)
assert not np.any(holes)

def test_fill_v2_fix_borders():
labels = np.ones([100,100,100], dtype=np.uint8)
labels[40:60,40:60,:] = 2
labels[40:60,:,40:60] = 2
labels[:,40:60,40:60] = 2

filled, holes = fastmorph.fill_holes_v2(
labels,
fix_borders=False,
)
assert np.all(filled == labels)

filled, holes = fastmorph.fill_holes_v2(
labels,
fix_borders=True,
)
assert np.all(filled == 1)
assert np.count_nonzero(holes) == 20*20*100*3 - 20*20*20*2

labels = np.ones([100,100,100], dtype=np.uint8)
labels[40:60,40:60,0] = 2
labels[40:60,0,40:60] = 2
labels[0,40:60,40:60] = 2

filled, holes = fastmorph.fill_holes_v2(
labels,
fix_borders=True,
)

assert np.all(filled == 1)
assert np.count_nonzero(holes) == 20*20*3

def test_fill_holes_v2_data():
import crackle
labels = crackle.load("connectomics.npy.ckl.gz")

s = time.time()
filled, holes = fastmorph.fill_holes_v2(labels)
print(f"{time.time() - s:.3f}")

uniq = np.unique(filled)
for lbl in uniq[:10]:
print(lbl)
if lbl == 0:
continue
binary_image_fm = filled == lbl
binary_image_scipy = scipy.ndimage.binary_fill_holes(labels == lbl)
assert np.all(binary_image_fm == binary_image_scipy)

def test_spherical_open_close_run():
labels = np.zeros((10,10,10), dtype=bool)
res = fastmorph.spherical_open(labels, radius=1)
Expand Down
Loading
Loading