diff --git a/docs/assets/screenshots/interactive_edge_fading.png b/docs/assets/screenshots/interactive_edge_fading.png new file mode 100644 index 0000000000000000000000000000000000000000..443c2827ac1e0c05cd08ef80eed907343812e111 Binary files /dev/null and b/docs/assets/screenshots/interactive_edge_fading.png differ diff --git a/docs/assets/screenshots/operations-edge_fade_after.png b/docs/assets/screenshots/operations-edge_fade_after.png new file mode 100644 index 0000000000000000000000000000000000000000..cd1977d5929188f3a78394fc666633886592656a Binary files /dev/null and b/docs/assets/screenshots/operations-edge_fade_after.png differ diff --git a/docs/assets/screenshots/operations-edge_fade_before.png b/docs/assets/screenshots/operations-edge_fade_before.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ce709ef28adc5844c5179375f0d36f0097f322 Binary files /dev/null and b/docs/assets/screenshots/operations-edge_fade_before.png differ diff --git a/docs/notebooks/Untitled.ipynb b/docs/notebooks/Untitled.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..c946f3b248ee0bf392a9239a3b744dccd6193555 --- /dev/null +++ b/docs/notebooks/Untitled.ipynb @@ -0,0 +1,71 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "0b73f2d8", + "metadata": {}, + "outputs": [], + "source": [ + "import qim3d" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "73db6886", + "metadata": {}, + "outputs": [], + "source": [ + "vol = qim3d.examples.bone_128x128x128" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "22d86d4d", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2fefeafbd89c4f9fa5a08dc1a5d503d1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(interactive(children=(IntSlider(value=64, description='Z', max=127), Output()), layout=Layout(a…" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qim3d.viz.orthogonal(vol)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/processing.md b/docs/processing.md index 550cb1001b6a51777e4790f41a3af58e30b87046..8ecbce5063504f25ce132d8972f45b6021652cad 100644 --- a/docs/processing.md +++ b/docs/processing.md @@ -16,3 +16,4 @@ Here, we provide functionalities designed specifically for 3D image analysis and members: - remove_background - watershed + - edge_fade diff --git a/docs/viz.md b/docs/viz.md index 3ddd34a1c58617de014cf57e6479ad32fb826e72..e0ea9f1d5a64a2456ad4f740f857350ee72d0936 100644 --- a/docs/viz.md +++ b/docs/viz.md @@ -12,6 +12,7 @@ The `qim3d` library aims to provide easy ways to explore and get insights from v - vectors - plot_cc - colormaps + - interactive_edge_fade ::: qim3d.viz.colormaps options: diff --git a/qim3d/__init__.py b/qim3d/__init__.py index 73714bf3730e46ae2be9e91c6062acb52a66de4f..2180bfafea2ae34c24b13889e071234624845642 100644 --- a/qim3d/__init__.py +++ b/qim3d/__init__.py @@ -2,12 +2,14 @@ import logging logging.basicConfig(level=logging.ERROR) -from qim3d import io -from qim3d import gui -from qim3d import viz -from qim3d import utils -from qim3d import models -from qim3d import processing +from . import io +from . import gui +from . import viz + +from . import utils +from . import models +from . import processing + __version__ = "0.3.2" examples = io.ImgExamples() diff --git a/qim3d/processing/local_thickness_.py b/qim3d/processing/local_thickness_.py index 5e7b9416b53b0180623ffd60dd7ebe5f50396c99..f0de7e9954b4c4a3e5a7a7a3218bfbb0a6df0faa 100644 --- a/qim3d/processing/local_thickness_.py +++ b/qim3d/processing/local_thickness_.py @@ -5,8 +5,8 @@ import numpy as np from typing import Optional from skimage.filters import threshold_otsu from qim3d.io.logger import log -from qim3d.viz import local_thickness as viz_local_thickness - +#from qim3d.viz import local_thickness as viz_local_thickness +import qim3d def local_thickness( image: np.ndarray, @@ -96,6 +96,6 @@ def local_thickness( # Visualize the local thickness if requested if visualize: - display(viz_local_thickness(image, local_thickness, **viz_kwargs)) + display(qim3d.viz.local_thickness(image, local_thickness, **viz_kwargs)) return local_thickness diff --git a/qim3d/processing/operations.py b/qim3d/processing/operations.py index b932c7f8d2d6cf599e04b4ac53a95005f2e16907..dfaeb42e41d2bb88f54f5d1ce5db1ecfff4a5d30 100644 --- a/qim3d/processing/operations.py +++ b/qim3d/processing/operations.py @@ -1,7 +1,8 @@ import numpy as np -import qim3d.processing.filters as filters import scipy import skimage + +import qim3d.processing.filters as filters from qim3d.io.logger import log @@ -105,4 +106,82 @@ def watershed( 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 + return labeled_volume, num_labels + +def fade_mask( + vol: np.ndarray, + decay_rate: float = 10, + ratio: float = 0.5, + geometry: str = "sphere", + invert=False, + axis: int = 0, + ): + """ + Apply edge fading to a volume. + + Args: + vol (np.ndarray): The volume to apply edge fading to. + decay_rate (float, optional): The decay rate of the fading. Defaults to 10. + ratio (float, optional): The ratio of the volume to fade. Defaults to 0. + geometric (str, optional): The geometric shape of the fading. Can be 'spherical' or 'cylindrical'. Defaults to 'spherical'. + axis (int, optional): The axis along which to apply the fading. Defaults to 0. + + Returns: + np.ndarray: The volume with edge fading applied. + + Example: + ```python + import qim3d + qim3d.viz.vol(vol) + ``` + Image before edge fading has visible artifacts from the support. Which obscures the object of interest. +  + + ```python + import qim3d + vol_faded = qim3d.processing.operations.edge_fade(vol, decay_rate=4, ratio=0.45, geometric='cylindrical') + qim3d.viz.vol(vol_faded) + ``` + Afterwards the artifacts are faded out, making the object of interest more visible for visualization purposes. +  + + """ + if 0 > axis or axis >= vol.ndim: + raise ValueError("Axis must be between 0 and the number of dimensions of the volume") + + # Generate the coordinates of each point in the array + shape = vol.shape + z, y, x = np.indices(shape) + + # Calculate the center of the array + center = np.array([(s - 1) / 2 for s in shape]) + + # Calculate the distance of each point from the center + if geometry == "sphere": + distance = np.linalg.norm([z - center[0], y - center[1], x - center[2]], axis=0) + elif geometry == "cilinder": + distance_list = np.array([z - center[0], y - center[1], x - center[2]]) + # remove the axis along which the fading is not applied + distance_list = np.delete(distance_list, axis, axis=0) + distance = np.linalg.norm(distance_list, axis=0) + else: + raise ValueError("geometric must be 'spherical' or 'cylindrical'") + + # Normalize the distances so that they go from 0 at the center to 1 at the farthest point + max_distance = np.linalg.norm(center) + normalized_distance = distance / (max_distance*ratio) + + # Apply the decay rate + faded_distance = normalized_distance ** decay_rate + + # Invert the distances to have 1 at the center and 0 at the edges + fade_array = 1 - faded_distance + fade_array[fade_array<=0]=0 + + if invert: + fade_array = -(fade_array-1) + + # Apply the fading to the volume + vol_faded = vol * fade_array + + return vol_faded diff --git a/qim3d/viz/__init__.py b/qim3d/viz/__init__.py index c11df18022667389276dfafaf077b5579f388801..96352d2bef9d38dfd816d3500d240fb3ff838890 100644 --- a/qim3d/viz/__init__.py +++ b/qim3d/viz/__init__.py @@ -1,10 +1,17 @@ -from .visualizations import plot_metrics -from .img import grid_pred, grid_overview, slices, slicer, orthogonal, vol_masked -from .k3d import vol -from .structure_tensor import vectors -from .local_thickness_ import local_thickness -from .cc import plot_cc #from .colormaps import objects from . import colormaps - +from .cc import plot_cc from .detection import circles +from .img import ( + grid_overview, + grid_pred, + interactive_fade_mask, + orthogonal, + slicer, + slices, + vol_masked, +) +from .k3d import vol +from .local_thickness_ import local_thickness +from .structure_tensor import vectors +from .visualizations import plot_metrics diff --git a/qim3d/viz/cc.py b/qim3d/viz/cc.py index 7efee4923463ee44419b54443d06896c9cbd8436..8daee7dca7cbeb0f206ac215954521b4564b2393 100644 --- a/qim3d/viz/cc.py +++ b/qim3d/viz/cc.py @@ -3,9 +3,8 @@ import numpy as np from qim3d.io.logger import log from qim3d.processing.cc import CC -from qim3d.viz import slices from qim3d.viz.colormaps import objects as qim3dCmap - +import qim3d def plot_cc( connected_components, @@ -66,18 +65,18 @@ def plot_cc( overlay_crop = overlay[bb] # use cc as mask for overlay_crop, where all values in cc set to 0 should be masked out, cc contains integers overlay_crop = np.where(cc == 0, 0, overlay_crop) - fig = slices(overlay_crop, show=show, **kwargs) + fig = qim3d.viz.slices(overlay_crop, show=show, **kwargs) else: cc = connected_components.get_cc(component, crop=False) overlay_crop = np.where(cc == 0, 0, overlay) - fig = slices(overlay_crop, show=show, **kwargs) + fig = qim3d.viz.slices(overlay_crop, show=show, **kwargs) else: # assigns discrete color map to each connected component if not given if "cmap" not in kwargs: kwargs["cmap"] = qim3dCmap(len(component_indexs)) # Plot the connected component without overlay - fig = slices(connected_components.get_cc(component, crop=crop), show=show, **kwargs) + fig = qim3d.viz.slices(connected_components.get_cc(component, crop=crop), show=show, **kwargs) figs.append(fig) diff --git a/qim3d/viz/detection.py b/qim3d/viz/detection.py index acfffa70c5e09ebc535a5823ffa207da099de779..8f792f95a6ebd84ffa3c0e357aef4789975e3c94 100644 --- a/qim3d/viz/detection.py +++ b/qim3d/viz/detection.py @@ -1,10 +1,9 @@ import matplotlib.pyplot as plt -from qim3d.viz import slices from qim3d.io.logger import log import numpy as np import ipywidgets as widgets from IPython.display import clear_output, display - +import qim3d def circles(blobs, vol, alpha=0.5, color="#ff9900", **kwargs): """ @@ -29,7 +28,7 @@ def circles(blobs, vol, alpha=0.5, color="#ff9900", **kwargs): def _slicer(z_slice): clear_output(wait=True) - fig = slices( + fig = qim3d.viz.slices( vol, n_slices=1, position=z_slice, diff --git a/qim3d/viz/img.py b/qim3d/viz/img.py index 769db8cdbc2832c6cd189b1b887d6a9489bf0b05..53f6fe7dea0b83dfd39f843c612ce1507f830aab 100644 --- a/qim3d/viz/img.py +++ b/qim3d/viz/img.py @@ -5,14 +5,15 @@ Provides a collection of visualization functions. import math from typing import List, Optional, Union +import dask.array as da import ipywidgets as widgets import matplotlib.pyplot as plt import numpy as np import torch from matplotlib import colormaps from matplotlib.colors import LinearSegmentedColormap -import dask.array as da +import qim3d from qim3d.io.logger import log @@ -557,3 +558,83 @@ def vol_masked(vol, vol_mask, viz_delta=128): vol_masked = background + foreground return vol_masked + +def interactive_fade_mask(vol: np.ndarray, axis: int = 0): + """ Interactive widget for visualizing the effect of edge fading on a 3D volume. + + Args: + vol (np.ndarray): The volume to apply edge fading to. + axis (int, optional): The axis along which to apply the fading. Defaults to 0. + + Returns: + np.ndarray: The volume with edge fading applied. + + Example: + ```python + import qim3d + qim3d.viz.img.interactive_edge_fade(vol, geometric='cylindrical', axis=0) + ``` +  + + """ + + # Create the interactive widget + def _slicer(position, decay_rate, ratio, geometry, invert): + fig, axes = plt.subplots(1, 3, figsize=(9, 3)) + + axes[0].imshow(vol[position, :, :], cmap='viridis') + axes[0].set_title('Original') + axes[0].axis('off') + + mask = qim3d.processing.operations.fade_mask(np.ones_like(vol), decay_rate=decay_rate, ratio=ratio, geometry=geometry, axis=axis, invert=invert) + axes[1].imshow(mask[position, :, :], cmap='viridis') + axes[1].set_title('Mask') + axes[1].axis('off') + + masked_vol = qim3d.processing.operations.fade_mask(vol, decay_rate=decay_rate, ratio=ratio, geometry=geometry, axis=axis, invert=invert) + axes[2].imshow(masked_vol[position, :, :], cmap='viridis') + axes[2].set_title('Masked') + axes[2].axis('off') + + return fig + + shape_dropdown = widgets.Dropdown( + options=['sphere', 'cilinder'], + value='sphere', # default value + description='Geometry', + ) + + position_slider = widgets.IntSlider( + value=vol.shape[0] // 2, + min=0, + max=vol.shape[0] - 1, + description="Slice", + continuous_update=False, + ) + decay_rate_slider = widgets.FloatSlider( + value=10, + min=1, + max=50, + step=1.0, + description="Decay Rate", + continuous_update=False, + ) + ratio_slider = widgets.FloatSlider( + value=0.5, + min=0.1, + max=1, + step=0.01, + description="Ratio", + continuous_update=False, + ) + + # Create the Checkbox widget + invert_checkbox = widgets.Checkbox( + value=False, # default value + description='Invert' + ) + + slicer_obj = widgets.interactive(_slicer, position=position_slider, decay_rate=decay_rate_slider, ratio=ratio_slider, geometry=shape_dropdown, invert=invert_checkbox) + slicer_obj.layout = widgets.Layout(align_items="flex-start") + + return slicer_obj