diff --git a/docs/notebooks/blob_detection.ipynb b/docs/notebooks/blob_detection.ipynb index d5490b4bac824fcd66be93af99815a94a1ed482e..86f6d94691fed612a44439db08d3382fa2d47c6b 100644 --- a/docs/notebooks/blob_detection.ipynb +++ b/docs/notebooks/blob_detection.ipynb @@ -4,15 +4,7 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "WARNING:root:Could not load CuPy: No module named 'cupy'\n" - ] - } - ], + "outputs": [], "source": [ "import qim3d" ] @@ -30,7 +22,9 @@ "source": [ "This notebook shows how to do **blob detection** in a 3D volume using the `qim3d` library. \n", "\n", - "Blob detection is done by initializing a `qim3d.processing.Blob` object, and then calling the `qim3d.processing.Blob.detect` method. The `qim3d.processing.Blob.detect` method detects blobs by using the Difference of Gaussian (DoG) blob detection method, and returns an array `blobs` with the blobs found in the volume stored as `(p, r, c, radius)`. Subsequently, a binary mask of the volume can be retrieved with the `qim3d.processing.get_mask` method, in which the found blobs are marked as `True`." + "Blob detection is done by using the `qim3d.processing.blob_detection` method, which detects blobs by using the Difference of Gaussian (DoG) blob detection method, and returns two arrays:\n", + "- `blobs`: The blobs found in the volume stored as `(p, r, c, radius)`\n", + "- `binary_volume`: A binary mask of the volume with the blobs marked as `True`" ] }, { @@ -49,7 +43,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -69,7 +63,7 @@ "<Figure size 1000x200 with 5 Axes>" ] }, - "execution_count": 4, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -97,9 +91,16 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 3, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Bright background selected, volume will be inverted.\n" + ] + }, { "name": "stdout", "output_type": "stream", @@ -109,31 +110,29 @@ } ], "source": [ - "# Initialize blob detector\n", - "blob_detector = qim3d.processing.Blob(\n", - " background = \"bright\", \n", - " min_sigma = 1, \n", - " max_sigma = 8, \n", - " threshold = 0.001, \n", - " overlap = 0.1\n", + "# Detect blobs, and get binary mask\n", + "blobs, mask = qim3d.processing.blob_detection(\n", + " cement_filtered,\n", + " min_sigma=1,\n", + " max_sigma=8,\n", + " threshold=0.001,\n", + " overlap=0.1,\n", + " background=\"bright\"\n", " )\n", "\n", - "# Detect blobs in filtered volume\n", - "blobs = blob_detector.detect(vol = cement_filtered)\n", - "\n", "# Number of blobs found\n", "print(f'Number of blobs found in the volume: {len(blobs)} blobs')" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4d128089a7e545d7a3fd8a4607c02085", + "model_id": "54d5e4864544453695c7d87287aa7cf9", "version_major": 2, "version_minor": 0 }, @@ -141,32 +140,32 @@ "interactive(children=(IntSlider(value=64, description='Slice', max=127), Output()), layout=Layout(align_items=…" ] }, - "execution_count": 13, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Visualize blobs on slices of cement volume\n", - "qim3d.viz.detection.circles(blobs, cement, show = True)" + "qim3d.viz.detection.circles(blobs, cement, alpha = 0.8, show = True, color = 'red')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Get binary mask of detected blobs**" + "**Binary mask of detected blobs**" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2d8e5b955da948de8f2bea5bb19000b9", + "model_id": "11fd726e79a246c98948ec54b3c752e2", "version_major": 2, "version_minor": 0 }, @@ -174,16 +173,13 @@ "interactive(children=(IntSlider(value=64, description='Slice', max=127), Output()), layout=Layout(align_items=…" ] }, - "execution_count": 14, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Get binary mask of detected blobs\n", - "mask = blob_detector.get_mask()\n", - "\n", - "# Visualize mask\n", + "# Visualize binary mask\n", "qim3d.viz.slicer(mask)" ] } diff --git a/docs/processing.md b/docs/processing.md index ab737cb3ff984210f03ee249f18b87622a7a9495..558f857e94aa01cbc128a3d3245b668ca82c1998 100644 --- a/docs/processing.md +++ b/docs/processing.md @@ -5,11 +5,16 @@ Here, we provide functionalities designed specifically for 3D image analysis and ::: qim3d.processing options: members: + - test_blob_detection + - blob_detection - structure_tensor - local_thickness - get_3d_cc - - Pipeline - - Blob + - gaussian + - median + - maximum + - minimum + - tophat ::: qim3d.processing.operations options: @@ -18,3 +23,7 @@ Here, we provide functionalities designed specifically for 3D image analysis and - watershed - edge_fade - fade_mask +::: qim3d.processing + options: + members: + - Pipeline \ No newline at end of file diff --git a/qim3d/processing/__init__.py b/qim3d/processing/__init__.py index 34dea0c4f0c556184277de549acf22dc9ad3f59e..79d9acf5f85d48c285d04424380ac455069330c6 100644 --- a/qim3d/processing/__init__.py +++ b/qim3d/processing/__init__.py @@ -1,6 +1,8 @@ +"Testing docstring" + from .local_thickness_ import local_thickness from .structure_tensor_ import structure_tensor +from .detection import blob_detection from .filters import * -from .detection import * from .operations import * from .cc import get_3d_cc diff --git a/qim3d/processing/detection.py b/qim3d/processing/detection.py index b1397e4e36f7f5c4f49f67b9dc3f1e80fb0b6592..c5743a1501f8d2e20edda9f22c06b1eee76a48ad 100644 --- a/qim3d/processing/detection.py +++ b/qim3d/processing/detection.py @@ -1,60 +1,40 @@ +""" Blob detection using Difference of Gaussian (DoG) method """ + import numpy as np from qim3d.io.logger import log from skimage.feature import blob_dog -__all__ = ["Blob"] - -class Blob: +def blob_detection( + vol: np.ndarray, + background: str = "dark", + min_sigma: float = 1, + max_sigma: float = 50, + sigma_ratio: float = 1.6, + threshold: float = 0.5, + overlap: float = 0.5, + threshold_rel: float = None, + exclude_border: bool = False, +) -> np.ndarray: """ - Extract blobs from a volume using Difference of Gaussian (DoG) method - """ - def __init__( - self, - background="dark", - min_sigma=1, - max_sigma=50, - sigma_ratio=1.6, - threshold=0.5, - overlap=0.5, - threshold_rel=None, - exclude_border=False, - ): - """ - Initialize the blob detection object - - Args: - background: 'dark' if background is darker than the blobs, 'bright' if background is lighter than the blobs - min_sigma: The minimum standard deviation for Gaussian kernel - max_sigma: The maximum standard deviation for Gaussian kernel - sigma_ratio: The ratio between the standard deviation of Gaussian Kernels - threshold: The absolute lower bound for scale space maxima. Reduce this to detect blobs with lower intensities. - overlap: The fraction of area of two blobs that overlap - threshold_rel: The relative lower bound for scale space maxima - exclude_border: If True, exclude blobs that are too close to the border of the image - """ - self.background = background - self.min_sigma = min_sigma - self.max_sigma = max_sigma - self.sigma_ratio = sigma_ratio - self.threshold = threshold - self.overlap = overlap - self.threshold_rel = threshold_rel - self.exclude_border = exclude_border - self.vol_shape = None - self.blobs = None - - def detect(self, vol): - """ - Detect blobs in the volume - - Args: - vol: The volume to detect blobs in - - Returns: - blobs: The blobs found in the volume as (p, r, c, radius) - - Example: + Extract blobs from a volume using Difference of Gaussian (DoG) method, and retrieve a binary volume with the blobs marked as True + + Args: + vol: The volume to detect blobs in + background: 'dark' if background is darker than the blobs, 'bright' if background is lighter than the blobs + min_sigma: The minimum standard deviation for Gaussian kernel + max_sigma: The maximum standard deviation for Gaussian kernel + sigma_ratio: The ratio between the standard deviation of Gaussian Kernels + threshold: The absolute lower bound for scale space maxima. Reduce this to detect blobs with lower intensities + overlap: The fraction of area of two blobs that overlap + threshold_rel: The relative lower bound for scale space maxima + exclude_border: If True, exclude blobs that are too close to the border of the image + + Returns: + blobs: The blobs found in the volume as (p, r, c, radius) + binary_volume: A binary volume with the blobs marked as True + + Example: ```python import qim3d @@ -62,8 +42,9 @@ class Blob: vol = qim3d.examples.cement_128x128x128 vol_blurred = qim3d.processing.gaussian(vol, sigma=2) - # Initialize Blob detector - blob_detector = qim3d.processing.Blob( + # Detect blobs, and get binary mask + blobs, mask = qim3d.processing.blob_detection( + vol_blurred, min_sigma=1, max_sigma=8, threshold=0.001, @@ -71,88 +52,63 @@ class Blob: background="bright" ) - # Detect blobs - blobs = blob_detector.detect(vol_blurred) - - # Visualize results - qim3d.viz.circles(blobs,vol,alpha=0.8,color='blue') + # Visualize detected blobs + qim3d.viz.circles(blobs, vol, alpha=0.8, color='blue') ```  - """ - self.vol_shape = vol.shape - if self.background == "bright": - log.info("Bright background selected, volume will be inverted.") - vol = np.invert(vol) - - blobs = blob_dog( - vol, - min_sigma=self.min_sigma, - max_sigma=self.max_sigma, - sigma_ratio=self.sigma_ratio, - threshold=self.threshold, - overlap=self.overlap, - threshold_rel=self.threshold_rel, - exclude_border=self.exclude_border, - ) - blobs[:, 3] = blobs[:, 3] * np.sqrt(3) # Change sigma to radius - self.blobs = blobs - return self.blobs - - def get_mask(self): - ''' - Retrieve a binary volume with the blobs marked as True - - Returns: - binary_volume: A binary volume with the blobs marked as True - - Example: - ```python - import qim3d - - # Get data - vol = qim3d.examples.cement_128x128x128 - vol_blurred = qim3d.processing.gaussian(vol, sigma=2) - - # Initialize Blob detector - blob_detector = qim3d.processing.Blob( - min_sigma=1, - max_sigma=8, - threshold=0.001, - overlap=0.1, - background="bright" - ) - - - # Detect blobs - blobs = blob_detector.detect(vol_blurred) - # Get mask and visualize - mask = blob_detector.get_mask() + ```python + # Visualize binary mask qim3d.viz.slicer(mask) ```  - ''' - binary_volume = np.zeros(self.vol_shape, dtype=bool) - - for z, y, x, radius in self.blobs: - # Calculate the bounding box around the blob - z_start = max(0, int(z - radius)) - z_end = min(self.vol_shape[0], int(z + radius) + 1) - y_start = max(0, int(y - radius)) - y_end = min(self.vol_shape[1], int(y + radius) + 1) - x_start = max(0, int(x - radius)) - x_end = min(self.vol_shape[2], int(x + radius) + 1) - - z_indices, y_indices, x_indices = np.indices((z_end - z_start, y_end - y_start, x_end - x_start)) - z_indices += z_start - y_indices += y_start - x_indices += x_start - - # Calculate distances from the center of the blob to voxels within the bounding box - dist = np.sqrt((x_indices - x)**2 + (y_indices - y)**2 + (z_indices - z)**2) + """ - binary_volume[z_start:z_end, y_start:y_end, x_start:x_end][dist <= radius] = True + if background == "bright": + log.info("Bright background selected, volume will be inverted.") + vol = np.invert(vol) + + blobs = blob_dog( + vol, + min_sigma=min_sigma, + max_sigma=max_sigma, + sigma_ratio=sigma_ratio, + threshold=threshold, + overlap=overlap, + threshold_rel=threshold_rel, + exclude_border=exclude_border, + ) + + # Change sigma to radius + blobs[:, 3] = blobs[:, 3] * np.sqrt(3) + + # Create binary mask of detected blobs + vol_shape = vol.shape + binary_volume = np.zeros(vol_shape, dtype=bool) + + for z, y, x, radius in blobs: + # Calculate the bounding box around the blob + z_start = max(0, int(z - radius)) + z_end = min(vol_shape[0], int(z + radius) + 1) + y_start = max(0, int(y - radius)) + y_end = min(vol_shape[1], int(y + radius) + 1) + x_start = max(0, int(x - radius)) + x_end = min(vol_shape[2], int(x + radius) + 1) + + z_indices, y_indices, x_indices = np.indices( + (z_end - z_start, y_end - y_start, x_end - x_start) + ) + z_indices += z_start + y_indices += y_start + x_indices += x_start - return binary_volume + # Calculate distances from the center of the blob to voxels within the bounding box + dist = np.sqrt( + (x_indices - x) ** 2 + (y_indices - y) ** 2 + (z_indices - z) ** 2 + ) + binary_volume[z_start:z_end, y_start:y_end, x_start:x_end][ + dist <= radius + ] = True + return blobs, binary_volume diff --git a/qim3d/processing/filters.py b/qim3d/processing/filters.py index 354214c4d7e699de2c934cfbfed0710dd51e1c23..fc2d89134ded623e2e478f24f75a4e3735224169 100644 --- a/qim3d/processing/filters.py +++ b/qim3d/processing/filters.py @@ -265,11 +265,13 @@ def minimum(vol, **kwargs): def tophat(vol, **kwargs): """ - Remove background from the volume + Remove background from the volume. + Args: vol: The volume to remove background from radius: The radius of the structuring element (default: 3) background: color of the background, 'dark' or 'bright' (default: 'dark'). If 'bright', volume will be inverted. + Returns: vol: The volume with background removed """ diff --git a/qim3d/viz/detection.py b/qim3d/viz/detection.py index 8f792f95a6ebd84ffa3c0e357aef4789975e3c94..f6ff1bf102c44106696b2eda97b868e83d4dff67 100644 --- a/qim3d/viz/detection.py +++ b/qim3d/viz/detection.py @@ -15,7 +15,7 @@ def circles(blobs, vol, alpha=0.5, color="#ff9900", **kwargs): Args: blobs (array-like): An array-like object of blobs, where each blob is represented - as a 4-tuple (p, r, c, radius). Usally the result of `qim3d.processing.Blob().detect()` + as a 4-tuple (p, r, c, radius). Usually the result of `qim3d.processing.blob_detection(vol)` vol (array-like): The 3D volume on which to plot the blobs. alpha (float, optional): The transparency of the blobs. Defaults to 0.5. color (str, optional): The color of the blobs. Defaults to "#ff9900".