From 42e17d081ef5f325ea6d398e63edeb54c21dc08e Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Fri, 18 Jul 2025 15:29:11 -0400 Subject: [PATCH 1/9] Create _labeling folder --- blobid/_labeling/__init__.py | 8 ++++++++ blobid/{utils => _labeling}/ccl.py | 2 +- blobid/{utils/labeling.py => _labeling/database.py} | 0 blobid/solver.py | 6 +++--- tests/test_boundaries.py | 3 +-- tests/{test_labeling.py => test_database.py} | 2 +- 6 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 blobid/_labeling/__init__.py rename blobid/{utils => _labeling}/ccl.py (97%) rename blobid/{utils/labeling.py => _labeling/database.py} (100%) rename tests/{test_labeling.py => test_database.py} (98%) diff --git a/blobid/_labeling/__init__.py b/blobid/_labeling/__init__.py new file mode 100644 index 0000000..ed78ab0 --- /dev/null +++ b/blobid/_labeling/__init__.py @@ -0,0 +1,8 @@ +""" +Provides methods for labeling given connectedness +""" + +from .ccl import get_temporary_labels, stitch_boundaries +from .database import LabelDatabase + +__all__ = ['get_temporary_labels', 'stitch_boundaries', 'LabelDatabase'] diff --git a/blobid/utils/ccl.py b/blobid/_labeling/ccl.py similarity index 97% rename from blobid/utils/ccl.py rename to blobid/_labeling/ccl.py index 14caff2..16f8b8a 100644 --- a/blobid/utils/ccl.py +++ b/blobid/_labeling/ccl.py @@ -5,7 +5,7 @@ import numpy as np from blobid.utils.numba_support import njit -from blobid.utils.labeling import LabelDatabase, _DatabaseStorage, _merge +from .database import LabelDatabase, _DatabaseStorage, _merge def get_temporary_labels( diff --git a/blobid/utils/labeling.py b/blobid/_labeling/database.py similarity index 100% rename from blobid/utils/labeling.py rename to blobid/_labeling/database.py diff --git a/blobid/solver.py b/blobid/solver.py index f9ed2a9..71d8e37 100644 --- a/blobid/solver.py +++ b/blobid/solver.py @@ -7,7 +7,7 @@ from blobid.utils.domain import VOFDomain -from blobid.utils.ccl import get_temporary_labels, stitch_boundaries +from . import _labeling as labeling from blobid.utils.reconstruction import normals @@ -163,10 +163,10 @@ def get_labels( norm=normals(domain.vof(padding=1), normals_method) if use_normals else None) # do initial labeling - (labels, label_database) = get_temporary_labels(is_object, is_connected, label_type) + (labels, label_database) = labeling.get_temporary_labels(is_object, is_connected, label_type) # stitch together periodic boundaries - label_database = stitch_boundaries(labels, label_database, domain.periodic) + label_database = labeling.stitch_boundaries(labels, label_database, domain.periodic) # do final labeling with sequential labels labels = label_database.get_sequential_lookup_table()[labels] diff --git a/tests/test_boundaries.py b/tests/test_boundaries.py index 2e1b099..8522461 100644 --- a/tests/test_boundaries.py +++ b/tests/test_boundaries.py @@ -1,9 +1,8 @@ import pytest import numpy as np -from blobid.utils.ccl import stitch_boundaries from blobid.utils.domain import _pad_array -from blobid.utils.labeling import LabelDatabase +from blobid._labeling import LabelDatabase, stitch_boundaries from blobid.utils.reconstruction import normals diff --git a/tests/test_labeling.py b/tests/test_database.py similarity index 98% rename from tests/test_labeling.py rename to tests/test_database.py index 7a1c2c5..8a31e47 100644 --- a/tests/test_labeling.py +++ b/tests/test_database.py @@ -1,7 +1,7 @@ import pytest as pt import numpy as np -from blobid.utils.labeling import LabelDatabase +from blobid._labeling import LabelDatabase def test_label_sets(): From 59b3c7dfae10f757253707398731f2216a7cfdb0 Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Fri, 18 Jul 2025 17:15:27 -0400 Subject: [PATCH 2/9] Move labeling into one accessible call --- blobid/__init__.py | 3 +- blobid/_labeling/__init__.py | 8 --- blobid/labeling/__init__.py | 3 + blobid/{_labeling => labeling}/ccl.py | 23 ------- blobid/{_labeling => labeling}/database.py | 0 blobid/labeling/labeling.py | 79 ++++++++++++++++++++++ blobid/solver.py | 17 +++-- tests/test_boundaries.py | 5 +- tests/test_database.py | 2 +- 9 files changed, 96 insertions(+), 44 deletions(-) delete mode 100644 blobid/_labeling/__init__.py create mode 100644 blobid/labeling/__init__.py rename blobid/{_labeling => labeling}/ccl.py (78%) rename blobid/{_labeling => labeling}/database.py (100%) create mode 100644 blobid/labeling/labeling.py diff --git a/blobid/__init__.py b/blobid/__init__.py index 80e88e8..f13abce 100644 --- a/blobid/__init__.py +++ b/blobid/__init__.py @@ -1,3 +1,4 @@ from .solver import get_labels +from . import labeling -__all__ = ['get_labels',] +__all__ = ['get_labels', 'labeling'] diff --git a/blobid/_labeling/__init__.py b/blobid/_labeling/__init__.py deleted file mode 100644 index ed78ab0..0000000 --- a/blobid/_labeling/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Provides methods for labeling given connectedness -""" - -from .ccl import get_temporary_labels, stitch_boundaries -from .database import LabelDatabase - -__all__ = ['get_temporary_labels', 'stitch_boundaries', 'LabelDatabase'] diff --git a/blobid/labeling/__init__.py b/blobid/labeling/__init__.py new file mode 100644 index 0000000..e17d2eb --- /dev/null +++ b/blobid/labeling/__init__.py @@ -0,0 +1,3 @@ +from .labeling import apply_ccl + +__all__ = ['apply_ccl'] diff --git a/blobid/_labeling/ccl.py b/blobid/labeling/ccl.py similarity index 78% rename from blobid/_labeling/ccl.py rename to blobid/labeling/ccl.py index 16f8b8a..f396372 100644 --- a/blobid/_labeling/ccl.py +++ b/blobid/labeling/ccl.py @@ -1,7 +1,6 @@ """ Tools for running connected component labeling """ -from typing import Tuple import numpy as np from blobid.utils.numba_support import njit @@ -39,28 +38,6 @@ def get_temporary_labels( return (labels, label_database) -def stitch_boundaries( - labels: np.ndarray, - sets: LabelDatabase, - periodic: Tuple[bool, bool, bool] - ) -> LabelDatabase: - """Stitch together periodic boundaries and remove padding""" - - if periodic[0]: - for a, b in zip(labels[0, :, :].flat, labels[-2, :, :].flat): - sets.merge(a, b) - - if periodic[1]: - for a, b in zip(labels[:, 0, :].flat, labels[:, -2, :].flat): - sets.merge(a, b) - - if periodic[2]: - for a, b in zip(labels[:, :, 0].flat, labels[:, :, -2].flat): - sets.merge(a, b) - - return sets - - @njit def _tail_pass( labels: np.ndarray, diff --git a/blobid/_labeling/database.py b/blobid/labeling/database.py similarity index 100% rename from blobid/_labeling/database.py rename to blobid/labeling/database.py diff --git a/blobid/labeling/labeling.py b/blobid/labeling/labeling.py new file mode 100644 index 0000000..fe07f24 --- /dev/null +++ b/blobid/labeling/labeling.py @@ -0,0 +1,79 @@ +from typing import Tuple + +import numpy as np + +from .database import LabelDatabase +from .ccl import get_temporary_labels + + +def apply_ccl( + is_object: np.ndarray, + is_connected: np.ndarray, + periodic: Tuple[bool, bool, bool], + label_type +) -> np.ndarray: + r""" + Given connectedness, calculate unique labels for each connected region of object cells. + + Parameters + ---------- + is_object : ndarray[ni, nj, nk] + `is_object[i, j, k]` is true if cell `[i,j,k]` is an object cell + is_connected : ndarray[ni, nj, nk, 3] + `is_connected[i, j, k, d]` is true if cell `[i,j,k]` us connected to the neighbor in the negative d direction. + For example, `is_connected[i, j, k, 0]` is true if cell `[i,j,k]` is connected to cell `[i-1,j,k]` + periodic: (bool, bool, bool) + If `periodic[d]`, then cells on each edge in direction `d` are considered the same cell. + For example, cell `[0,j,k]` is the same as cell `[-1,j,k]` if `periodic[0]` is true. + label_type : dtype + Determines the integer data-type of `labels` + + Returns + ------- + labels : ndarray[ni, nj, nk] + An array of type `label_type` with the same shape as `is_object`. + + """ + # checks + assert is_object.ndim == 3 + assert is_connected.ndim == 4 + print(is_object.shape) + print(is_connected.shape) + assert np.all(is_object.shape == is_connected.shape[:-1]) + + assert not np.any(is_connected[0, :, :, 0]) + assert not np.any(is_connected[:, 0, :, 1]) + assert not np.any(is_connected[:, :, 0, 2]) + + # do initial labeling + (labels, label_database) = get_temporary_labels(is_object, is_connected, label_type) + + # stitch together periodic boundaries + label_database = _stitch_boundaries(labels, label_database, periodic) + + # do final labeling with sequential labels + labels = label_database.get_sequential_lookup_table()[labels] + + return labels + + +def _stitch_boundaries( + labels: np.ndarray, + sets: LabelDatabase, + periodic: Tuple[bool, bool, bool] + ) -> LabelDatabase: + """Stitch together periodic boundaries and remove padding""" + + if periodic[0]: + for a, b in zip(labels[0, :, :].flat, labels[-2, :, :].flat): + sets.merge(a, b) + + if periodic[1]: + for a, b in zip(labels[:, 0, :].flat, labels[:, -2, :].flat): + sets.merge(a, b) + + if periodic[2]: + for a, b in zip(labels[:, :, 0].flat, labels[:, :, -2].flat): + sets.merge(a, b) + + return sets diff --git a/blobid/solver.py b/blobid/solver.py index 71d8e37..11eb3b2 100644 --- a/blobid/solver.py +++ b/blobid/solver.py @@ -7,7 +7,7 @@ from blobid.utils.domain import VOFDomain -from . import _labeling as labeling +from . import labeling from blobid.utils.reconstruction import normals @@ -162,14 +162,13 @@ def get_labels( is_connected = _calc_connections(is_object, norm=normals(domain.vof(padding=1), normals_method) if use_normals else None) - # do initial labeling - (labels, label_database) = labeling.get_temporary_labels(is_object, is_connected, label_type) - - # stitch together periodic boundaries - label_database = labeling.stitch_boundaries(labels, label_database, domain.periodic) - - # do final labeling with sequential labels - labels = label_database.get_sequential_lookup_table()[labels] + # do the labeling + labels = labeling.apply_ccl( + is_object=is_object, + is_connected=is_connected, + periodic=domain.periodic, + label_type=label_type + ) # reshape to original dimensions (removes padding in periodic directions) return domain.convert_to_original_shape(labels) diff --git a/tests/test_boundaries.py b/tests/test_boundaries.py index 8522461..c58bdd8 100644 --- a/tests/test_boundaries.py +++ b/tests/test_boundaries.py @@ -2,7 +2,8 @@ import numpy as np from blobid.utils.domain import _pad_array -from blobid._labeling import LabelDatabase, stitch_boundaries +from blobid.labeling.database import LabelDatabase +from blobid.labeling.labeling import _stitch_boundaries from blobid.utils.reconstruction import normals @@ -121,7 +122,7 @@ def check_periodicity(labels, sets, periodicity): for a, b in zip(labels[:, :, -1].flat, labels[:, :, -2].flat): sets.merge(a, b) - sets = stitch_boundaries(labels, sets, per) + sets = _stitch_boundaries(labels, sets, per) # make sure the size is right assert labels.shape[0] == 5 + 2 * per[0] diff --git a/tests/test_database.py b/tests/test_database.py index 8a31e47..31f4dcb 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,7 +1,7 @@ import pytest as pt import numpy as np -from blobid._labeling import LabelDatabase +from blobid.labeling.database import LabelDatabase def test_label_sets(): From 76ab9ed1dce8ce8b702a2f78defb07c6f0dc3b05 Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Fri, 18 Jul 2025 17:32:49 -0400 Subject: [PATCH 3/9] Move numba_support --- blobid/{utils/numba_support.py => _numba_support.py} | 4 ++-- blobid/labeling/ccl.py | 2 +- blobid/labeling/database.py | 2 +- blobid/utils/reconstruction.py | 4 ++-- tests/test_reconstruction.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename blobid/{utils/numba_support.py => _numba_support.py} (74%) diff --git a/blobid/utils/numba_support.py b/blobid/_numba_support.py similarity index 74% rename from blobid/utils/numba_support.py rename to blobid/_numba_support.py index 764e471..e3d32d3 100644 --- a/blobid/utils/numba_support.py +++ b/blobid/_numba_support.py @@ -1,10 +1,10 @@ """Setup Numba if available""" -_numba_availible = True +numba_availible = True try: from numba import njit except ImportError: - _numba_availible = False + numba_availible = False # create a dummy njit decorator def njit(func): return func diff --git a/blobid/labeling/ccl.py b/blobid/labeling/ccl.py index f396372..c094ba8 100644 --- a/blobid/labeling/ccl.py +++ b/blobid/labeling/ccl.py @@ -3,7 +3,7 @@ """ import numpy as np -from blobid.utils.numba_support import njit +from .._numba_support import njit from .database import LabelDatabase, _DatabaseStorage, _merge diff --git a/blobid/labeling/database.py b/blobid/labeling/database.py index 03de62c..27b7a52 100644 --- a/blobid/labeling/database.py +++ b/blobid/labeling/database.py @@ -5,7 +5,7 @@ import numpy as np -from blobid.utils.numba_support import njit +from .._numba_support import njit _DatabaseStorage = namedtuple('_DatabaseStorage', 'r, n, t') _END_OF_SET = 0 diff --git a/blobid/utils/reconstruction.py b/blobid/utils/reconstruction.py index fb99302..46269a8 100644 --- a/blobid/utils/reconstruction.py +++ b/blobid/utils/reconstruction.py @@ -4,7 +4,7 @@ import warnings import numpy as np -from blobid.utils.numba_support import njit, _numba_availible +from .._numba_support import njit, numba_availible NORMALS_TYPE = np.int8 @@ -38,7 +38,7 @@ def normals_CD(f: np.ndarray) -> np.ndarray: def normals_WY(f: np.ndarray) -> np.ndarray: # Numba only supports float32 and float64 - if _numba_availible and (f.dtype not in (np.float32, np.float64)): + if numba_availible and (f.dtype not in (np.float32, np.float64)): warnings.warn(f"void_fraction type {f.dtype.name} not supported, using float32") f = f.astype(np.float32) diff --git a/tests/test_reconstruction.py b/tests/test_reconstruction.py index abddef1..bf7a652 100644 --- a/tests/test_reconstruction.py +++ b/tests/test_reconstruction.py @@ -3,7 +3,7 @@ import pytest import numpy as np -from blobid.utils.numba_support import _numba_availible +from blobid._numba_support import numba_availible from blobid.utils.reconstruction import normals, NORMALS_TYPE, _get_dominant_direction @@ -71,7 +71,7 @@ def test_vof_type(): f = np.random.rand(3, 3, 3) # float16 should give a warning - if _numba_availible: + if numba_availible: with pytest.warns(): normals(f.astype(np.float16), 'WY') From 6975de22732919c5d0f8765d161bbe62e86778ec Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Fri, 18 Jul 2025 17:38:04 -0400 Subject: [PATCH 4/9] Cleanup comment --- blobid/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blobid/solver.py b/blobid/solver.py index 11eb3b2..50360ce 100644 --- a/blobid/solver.py +++ b/blobid/solver.py @@ -155,7 +155,7 @@ def get_labels( periodic_padding=1, extra_padding=1 if (use_normals or (cutoff_method == 'neighbors')) else 0) - # calculate object cells, removing the extra padding + # calculate object cells is_object = _calc_object_cells(domain, cutoff, cutoff_method) # calculate connectivity From debaf08822f08acc9dd69f5c62ff8eadee99ff7d Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Fri, 18 Jul 2025 17:40:27 -0400 Subject: [PATCH 5/9] Remove print statements left from debug --- blobid/labeling/labeling.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/blobid/labeling/labeling.py b/blobid/labeling/labeling.py index fe07f24..18f462f 100644 --- a/blobid/labeling/labeling.py +++ b/blobid/labeling/labeling.py @@ -37,10 +37,7 @@ def apply_ccl( # checks assert is_object.ndim == 3 assert is_connected.ndim == 4 - print(is_object.shape) - print(is_connected.shape) assert np.all(is_object.shape == is_connected.shape[:-1]) - assert not np.any(is_connected[0, :, :, 0]) assert not np.any(is_connected[:, 0, :, 1]) assert not np.any(is_connected[:, :, 0, 2]) From 219c8fc84f5ce00aeefceedfd34a7a0e66ee681f Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Thu, 24 Jul 2025 17:43:42 -0400 Subject: [PATCH 6/9] Move utils/domain.py to _domain.py --- blobid/{utils/domain.py => _domain.py} | 0 blobid/solver.py | 2 +- tests/test_boundaries.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename blobid/{utils/domain.py => _domain.py} (100%) diff --git a/blobid/utils/domain.py b/blobid/_domain.py similarity index 100% rename from blobid/utils/domain.py rename to blobid/_domain.py diff --git a/blobid/solver.py b/blobid/solver.py index 50360ce..9e90b54 100644 --- a/blobid/solver.py +++ b/blobid/solver.py @@ -5,7 +5,7 @@ import numpy as np -from blobid.utils.domain import VOFDomain +from ._domain import VOFDomain from . import labeling from blobid.utils.reconstruction import normals diff --git a/tests/test_boundaries.py b/tests/test_boundaries.py index c58bdd8..56ad3fe 100644 --- a/tests/test_boundaries.py +++ b/tests/test_boundaries.py @@ -1,7 +1,7 @@ import pytest import numpy as np -from blobid.utils.domain import _pad_array +from blobid._domain import _pad_array from blobid.labeling.database import LabelDatabase from blobid.labeling.labeling import _stitch_boundaries from blobid.utils.reconstruction import normals From 6a3b66b503ef88d453f13f8222273c7a39e034e4 Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Thu, 24 Jul 2025 17:51:44 -0400 Subject: [PATCH 7/9] Move reconstruction to its own submodule --- blobid/reconstruction/__init__.py | 3 +++ blobid/{utils => reconstruction}/reconstruction.py | 0 blobid/solver.py | 4 ++-- tests/test_boundaries.py | 2 +- tests/test_reconstruction.py | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 blobid/reconstruction/__init__.py rename blobid/{utils => reconstruction}/reconstruction.py (100%) diff --git a/blobid/reconstruction/__init__.py b/blobid/reconstruction/__init__.py new file mode 100644 index 0000000..bd984a5 --- /dev/null +++ b/blobid/reconstruction/__init__.py @@ -0,0 +1,3 @@ +from .reconstruction import normals + +__all__ = ['normals'] diff --git a/blobid/utils/reconstruction.py b/blobid/reconstruction/reconstruction.py similarity index 100% rename from blobid/utils/reconstruction.py rename to blobid/reconstruction/reconstruction.py diff --git a/blobid/solver.py b/blobid/solver.py index 9e90b54..e6be140 100644 --- a/blobid/solver.py +++ b/blobid/solver.py @@ -8,7 +8,7 @@ from ._domain import VOFDomain from . import labeling -from blobid.utils.reconstruction import normals +from . import reconstruction def get_labels( @@ -160,7 +160,7 @@ def get_labels( # calculate connectivity is_connected = _calc_connections(is_object, - norm=normals(domain.vof(padding=1), normals_method) if use_normals else None) + norm=reconstruction.normals(domain.vof(padding=1), normals_method) if use_normals else None) # do the labeling labels = labeling.apply_ccl( diff --git a/tests/test_boundaries.py b/tests/test_boundaries.py index 56ad3fe..3f4c47a 100644 --- a/tests/test_boundaries.py +++ b/tests/test_boundaries.py @@ -4,7 +4,7 @@ from blobid._domain import _pad_array from blobid.labeling.database import LabelDatabase from blobid.labeling.labeling import _stitch_boundaries -from blobid.utils.reconstruction import normals +from blobid.reconstruction import normals @pytest.fixture diff --git a/tests/test_reconstruction.py b/tests/test_reconstruction.py index bf7a652..d1f03fc 100644 --- a/tests/test_reconstruction.py +++ b/tests/test_reconstruction.py @@ -4,7 +4,7 @@ import numpy as np from blobid._numba_support import numba_availible -from blobid.utils.reconstruction import normals, NORMALS_TYPE, _get_dominant_direction +from blobid.reconstruction.reconstruction import normals, NORMALS_TYPE, _get_dominant_direction def test_reconstruction_CD(): From 01d1404a6fd4a093848294acd3b70b029a18404e Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Thu, 24 Jul 2025 18:22:22 -0400 Subject: [PATCH 8/9] Group tests --- tests/{ => labeling}/test_database.py | 0 tests/{ => reconstruction}/test_reconstruction.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/{ => labeling}/test_database.py (100%) rename tests/{ => reconstruction}/test_reconstruction.py (100%) diff --git a/tests/test_database.py b/tests/labeling/test_database.py similarity index 100% rename from tests/test_database.py rename to tests/labeling/test_database.py diff --git a/tests/test_reconstruction.py b/tests/reconstruction/test_reconstruction.py similarity index 100% rename from tests/test_reconstruction.py rename to tests/reconstruction/test_reconstruction.py From ceaa31cf8f95254fce874c93788f931004a18ca4 Mon Sep 17 00:00:00 2001 From: Declan Gaylo Date: Thu, 24 Jul 2025 18:22:37 -0400 Subject: [PATCH 9/9] Fix long line --- blobid/solver.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/blobid/solver.py b/blobid/solver.py index e6be140..662e436 100644 --- a/blobid/solver.py +++ b/blobid/solver.py @@ -159,8 +159,12 @@ def get_labels( is_object = _calc_object_cells(domain, cutoff, cutoff_method) # calculate connectivity - is_connected = _calc_connections(is_object, - norm=reconstruction.normals(domain.vof(padding=1), normals_method) if use_normals else None) + if use_normals: + normals = reconstruction.normals(domain.vof(padding=1), normals_method) + else: + normals = None + + is_connected = _calc_connections(is_object, norm=normals) # do the labeling labels = labeling.apply_ccl(