diff --git a/qim3d/__init__.py b/qim3d/__init__.py index 07782e4e1a6ec54d7f3f605268d707ceeab557a7..ecbfe255266c3a164ec42cd6e09b3d182f9ba724 100644 --- a/qim3d/__init__.py +++ b/qim3d/__init__.py @@ -33,18 +33,20 @@ class _LazyLoader: # List of submodules _submodules = [ - "examples", - "generate", - "gui", - "io", - "models", - "processing", - "tests", - "utils", - "viz", - "cli", + 'examples', + 'generate', + 'gui', + 'io', + 'models', + 'processing', + 'tests', + 'utils', + 'viz', + 'cli', + 'filters', + 'segmentation' ] # Creating lazy loaders for each submodule for submodule in _submodules: - globals()[submodule] = _LazyLoader(f"qim3d.{submodule}") + globals()[submodule] = _LazyLoader(f'qim3d.{submodule}') diff --git a/qim3d/processing/filters.py b/qim3d/filters.py similarity index 100% rename from qim3d/processing/filters.py rename to qim3d/filters.py diff --git a/qim3d/processing/__init__.py b/qim3d/processing/__init__.py index 8f6edf04173e5781339f0ca33ff304a29851f0f9..056804d123bbd9152a362dbd4cc1fe21a1aeeb91 100644 --- a/qim3d/processing/__init__.py +++ b/qim3d/processing/__init__.py @@ -1,7 +1,6 @@ from .local_thickness_ import local_thickness from .structure_tensor_ import structure_tensor from .detection import blob_detection -from .filters import * from .operations import * from .cc import get_3d_cc from .layers2d import segment_layers, get_lines diff --git a/qim3d/processing/operations.py b/qim3d/processing/operations.py index e559a16d0100186e36ba96b597b6bdfa58f286fb..bf6f16f4b33c2d7b9606f0ba57bff2aa35732f9f 100644 --- a/qim3d/processing/operations.py +++ b/qim3d/processing/operations.py @@ -1,5 +1,5 @@ import numpy as np -import qim3d.processing.filters as filters +import qim3d.filters as filters from qim3d.utils.logger import log @@ -52,74 +52,6 @@ def remove_background( return pipeline(vol) -def watershed(bin_vol: np.ndarray, min_distance: int = 5) -> tuple[np.ndarray, int]: - """ - Apply watershed segmentation to a binary volume. - - Args: - bin_vol (np.ndarray): Binary volume to segment. The input should be a 3D binary image where non-zero elements - represent the objects to be segmented. - min_distance (int): Minimum number of pixels separating peaks in the distance transform. Peaks that are - too close will be merged, affecting the number of segmented objects. Default is 5. - - Returns: - tuple[np.ndarray, int]: - - Labeled volume (np.ndarray): A 3D array of the same shape as the input `bin_vol`, where each segmented object - is assigned a unique integer label. - - num_labels (int): The total number of unique objects found in the labeled volume. - - Example: - ```python - import qim3d - - vol = qim3d.examples.cement_128x128x128 - binary = qim3d.processing.filters.gaussian(vol, sigma = 2)<60 - - qim3d.viz.slices(binary, axis=1) - ``` -  - - ```python - labeled_volume, num_labels = qim3d.processing.operations.watershed(binary) - - cmap = qim3d.viz.colormaps.objects(num_labels) - qim3d.viz.slices(labeled_volume, axis = 1, cmap = cmap) - ``` -  - - """ - import skimage - import scipy - - if len(np.unique(bin_vol)) > 2: - raise ValueError("bin_vol has to be binary volume - it must contain max 2 unique values.") - - # Compute distance transform of binary volume - distance = scipy.ndimage.distance_transform_edt(bin_vol) - - # Find peak coordinates in distance transform - coords = skimage.feature.peak_local_max( - distance, min_distance=min_distance, labels=bin_vol - ) - - # Create a mask with peak coordinates - mask = np.zeros(distance.shape, dtype=bool) - mask[tuple(coords.T)] = True - - # Label peaks - markers, _ = scipy.ndimage.label(mask) - - # Apply watershed segmentation - labeled_volume = skimage.segmentation.watershed( - -distance, markers=markers, mask=bin_vol - ) - - # Extract number of objects found - num_labels = len(np.unique(labeled_volume)) - 1 - log.info(f"Total number of objects found: {num_labels}") - - return labeled_volume, num_labels - def fade_mask( vol: np.ndarray, diff --git a/qim3d/segmentation/__init__.py b/qim3d/segmentation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a2b743b795bd2af1765cf7d365ce63e6c2c5c7ba --- /dev/null +++ b/qim3d/segmentation/__init__.py @@ -0,0 +1 @@ +from .standard import * \ No newline at end of file diff --git a/qim3d/segmentation/standard.py b/qim3d/segmentation/standard.py new file mode 100644 index 0000000000000000000000000000000000000000..333b2da0ad6009f0f6851299e49fcef9c81d9679 --- /dev/null +++ b/qim3d/segmentation/standard.py @@ -0,0 +1,72 @@ +import numpy as np +from qim3d.utils.logger import log + +__all__ = ["watershed"] + +def watershed(bin_vol: np.ndarray, min_distance: int = 5) -> tuple[np.ndarray, int]: + """ + Apply watershed segmentation to a binary volume. + + Args: + bin_vol (np.ndarray): Binary volume to segment. The input should be a 3D binary image where non-zero elements + represent the objects to be segmented. + min_distance (int): Minimum number of pixels separating peaks in the distance transform. Peaks that are + too close will be merged, affecting the number of segmented objects. Default is 5. + + Returns: + tuple[np.ndarray, int]: + - Labeled volume (np.ndarray): A 3D array of the same shape as the input `bin_vol`, where each segmented object + is assigned a unique integer label. + - num_labels (int): The total number of unique objects found in the labeled volume. + + Example: + ```python + import qim3d + + vol = qim3d.examples.cement_128x128x128 + binary = qim3d.processing.filters.gaussian(vol, sigma = 2)<60 + + qim3d.viz.slices(binary, axis=1) + ``` +  + + ```python + labeled_volume, num_labels = qim3d.processing.operations.watershed(binary) + + cmap = qim3d.viz.colormaps.objects(num_labels) + qim3d.viz.slices(labeled_volume, axis = 1, cmap = cmap) + ``` +  + + """ + import skimage + import scipy + + if len(np.unique(bin_vol)) > 2: + raise ValueError("bin_vol has to be binary volume - it must contain max 2 unique values.") + + # Compute distance transform of binary volume + distance = scipy.ndimage.distance_transform_edt(bin_vol) + + # Find peak coordinates in distance transform + coords = skimage.feature.peak_local_max( + distance, min_distance=min_distance, labels=bin_vol + ) + + # Create a mask with peak coordinates + mask = np.zeros(distance.shape, dtype=bool) + mask[tuple(coords.T)] = True + + # Label peaks + markers, _ = scipy.ndimage.label(mask) + + # Apply watershed segmentation + labeled_volume = skimage.segmentation.watershed( + -distance, markers=markers, mask=bin_vol + ) + + # Extract number of objects found + num_labels = len(np.unique(labeled_volume)) - 1 + log.info(f"Total number of objects found: {num_labels}") + + return labeled_volume, num_labels \ No newline at end of file diff --git a/qim3d/tests/processing/test_filters.py b/qim3d/tests/processing/test_filters.py index e63628118775b34d2c98f897d303d89e4c18b60a..5a6d0993fb4ac6cecaf9c26d06b450eeb2a9f893 100644 --- a/qim3d/tests/processing/test_filters.py +++ b/qim3d/tests/processing/test_filters.py @@ -1,5 +1,5 @@ import qim3d -from qim3d.processing.filters import * +from qim3d.filters import * import numpy as np import pytest import re