Skip to content

Explore data

Visualization of volumetric data.

qim3d.viz.threshold

threshold(
    volume, colormap='magma', min_value=None, max_value=None
)

Launches an interactive widget to perform 3D image segmentation via thresholding (binarization).

This tool allows you to explore the volume slice-by-slice to determine the optimal cut-off value for creating a binary mask. It is essential for separating objects of interest from the background based on intensity. The interface provides real-time feedback by displaying the intensity histogram and overlaying the resulting mask on the original data.

Key Features:

  • Visualization: Simultaneously views the original slice, intensity histogram, binary mask, and a color overlay.
  • Manual Control: Adjust the threshold value precisely using a slider.
  • Automatic Algorithms: Applies standard skimage auto-thresholding methods including Otsu, Isodata, Li, Mean, Minimum, Triangle, and Yen.
  • Slice Navigation: Scroll through the 3D stack to ensure the chosen threshold works across different depths.

Parameters:

Name Type Description Default
volume ndarray

The 3D input data (image stack) to threshold.

required
colormap str

The Matplotlib colormap for the original image display.

'magma'
min_value float

Custom minimum value for display contrast (vmin). If None, uses the data minimum.

None
max_value float

Custom maximum value for display contrast (vmax). If None, uses the data maximum.

None

Returns:

Name Type Description
slicer_obj VBox

The interactive Jupyter widget containing the visualization plots and control sliders.

Example

import qim3d

# Load a sample volume
vol = qim3d.examples.bone_128x128x128

# Visualize interactive thresholding
qim3d.viz.threshold(vol)
interactive threshold

Source code in qim3d/viz/_data_exploration.py
@coarseness('volume')
def threshold(
    volume: np.ndarray,
    colormap: str = 'magma',
    min_value: float = None,
    max_value: float = None,
) -> widgets.VBox:
    """
    Launches an interactive widget to perform 3D image segmentation via thresholding (binarization).

    This tool allows you to explore the volume slice-by-slice to determine the optimal cut-off value for creating a binary mask. It is essential for separating objects of interest from the background based on intensity. The interface provides real-time feedback by displaying the intensity histogram and overlaying the resulting mask on the original data.

    **Key Features:**

    * **Visualization:** Simultaneously views the original slice, intensity histogram, binary mask, and a color overlay.
    * **Manual Control:** Adjust the threshold value precisely using a slider.
    * **Automatic Algorithms:** Applies standard `skimage` auto-thresholding methods including Otsu, Isodata, Li, Mean, Minimum, Triangle, and Yen.
    * **Slice Navigation:** Scroll through the 3D stack to ensure the chosen threshold works across different depths.

    Args:
        volume (np.ndarray): The 3D input data (image stack) to threshold.
        colormap (str, optional): The Matplotlib colormap for the original image display.
        min_value (float, optional): Custom minimum value for display contrast (vmin). If `None`, uses the data minimum.
        max_value (float, optional): Custom maximum value for display contrast (vmax). If `None`, uses the data maximum.

    Returns:
        slicer_obj (widgets.VBox):
            The interactive Jupyter widget containing the visualization plots and control sliders.

    Example:
        ```python
        import qim3d

        # Load a sample volume
        vol = qim3d.examples.bone_128x128x128

        # Visualize interactive thresholding
        qim3d.viz.threshold(vol)
        ```
        ![interactive threshold](../../assets/screenshots/interactive_thresholding.gif)
    """

    # Centralized state dictionary to track current parameters
    state = {
        'position': volume.shape[0] // 2,
        'method': 'Manual',
    }

    if np.issubdtype(volume.dtype, np.integer):
        step = 1
        state['threshold'] = int((volume.min() + volume.max()) / 2)
    elif np.issubdtype(volume.dtype, np.floating):
        step = (volume.max() - volume.min())/1000
        state['threshold'] = (volume.min() + volume.max()) / 2
    else:
        pass

    threshold_methods = {
        'Otsu': threshold_otsu,
        'Isodata': threshold_isodata,
        'Li': threshold_li,
        'Mean': threshold_mean,
        'Minimum': threshold_minimum,
        'Triangle': threshold_triangle,
        'Yen': threshold_yen,
    }

    # Create an output widget to display the plot
    output = widgets.Output()

    # Function to update the state and trigger visualization
    def update_state(change: dict[str, Any]) -> None:
        # Update state based on widget values
        state['position'] = position_slider.value
        state['method'] = method_dropdown.value

        if state['method'] == 'Manual':
            state['threshold'] = threshold_slider.value
            threshold_slider.disabled = False
        else:
            threshold_func = threshold_methods.get(state['method'])
            if threshold_func:
                slice_img = volume[state['position'], :, :]
                computed_threshold = threshold_func(slice_img)
                state['threshold'] = computed_threshold

                # Programmatically update the slider without triggering callbacks
                threshold_slider.unobserve_all()
                threshold_slider.value = computed_threshold
                threshold_slider.disabled = True
                threshold_slider.observe(update_state, names='value')
            else:
                msg = f"Unsupported thresholding method: {state['method']}"
                raise ValueError(msg)

        # Trigger visualization
        update_visualization()

    # Visualization function
    def update_visualization() -> None:
        slice_img = np.take(volume, state['position'], axis = slice_axis)
        with output:
            output.clear_output(wait=True)  # Clear previous plot
            fig, axes = plt.subplots(1, 4, figsize=(25, 5))

            # Original image
            new_min_value = (
                None
                if (isinstance(min_value, float | int) and min_value > np.max(slice_img))
                else min_value
            )
            new_max_value = (
                None
                if (isinstance(max_value, float | int) and max_value < np.min(slice_img))
                else max_value
            )
            axes[0].imshow(slice_img, cmap=colormap, vmin=new_min_value, vmax=new_max_value)
            axes[0].set_title('Original')
            axes[0].axis('off')

            # Histogram
            histogram(
                volume=volume,
                bins=32,
                slice_index=state['position'],
                vertical_line=state['threshold'],
                kde=False,
                ax=axes[1],
                show=False,
            )
            thr = state['threshold']
            if isinstance(step, float):
                thr = f'{thr:.3f}'
            else:
                thr = int(thr)
            axes[1].set_title(f"Histogram with Threshold = {thr}")

            # Binary mask
            mask = slice_img >= state['threshold']
            axes[2].imshow(mask, cmap='gray')
            axes[2].set_title('Binary mask')
            axes[2].axis('off')

            # Overlay
            mask_rgb = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8)
            mask_rgb[:, :, 0] = mask
            masked_volume = qim3d.operations.overlay_rgb_images(
                background=slice_img,
                foreground=mask_rgb,
            )
            axes[3].imshow(masked_volume, vmin=new_min_value, vmax=new_max_value)
            axes[3].set_title('Overlay')
            axes[3].axis('off')

            plt.show()

    # Widgets
    position_slider = widgets.IntSlider(
        value=state['position'],
        min=0,
        max=volume.shape[0] - 1,
        description='Slice',
    )

    threshold_slider = widgets.FloatSlider(
        value=state['threshold'],
        min=volume.min(),
        max=volume.max(),
        step = step,
        description='Threshold',
        readout_format = 'd' if step == 1 else '.3f'
    )

    method_dropdown = widgets.Dropdown(
        options=[
            'Manual',
            'Otsu',
            'Isodata',
            'Li',
            'Mean',
            'Minimum',
            'Triangle',
            'Yen',
        ],
        value=state['method'],
        description='Method',
    )

    # Attach the state update function to widgets
    position_slider.observe(update_state, names='value')
    threshold_slider.observe(update_state, names='value')
    method_dropdown.observe(update_state, names='value')

    # Layout
    controls_left = widgets.VBox([position_slider, threshold_slider])
    controls_right = widgets.VBox([method_dropdown])
    controls_layout = widgets.HBox(
        [controls_left, controls_right],
        layout=widgets.Layout(justify_content='flex-start'),
    )
    interactive_ui = widgets.VBox([controls_layout, output])
    update_visualization()

    return interactive_ui

qim3d.viz.line_profile

line_profile(
    volume,
    slice_axis=0,
    slice_index='middle',
    vertical_position='middle',
    horizontal_position='middle',
    angle=0,
    fraction_range=(0.0, 1.0),
    y_limits='auto',
)

Creates an interactive tool to visualize intensity profiles along a line segment within a 3D volume.

This function allows you to draw a line on a specific slice of your data and plot the pixel or voxel intensity values along that path. It is ideal for quantitative analysis, such as checking material homogeneity, measuring the sharpness of edges (step functions), or inspecting noise levels across a region of interest (ROI). The tool supports arbitrary angles, dynamic pivot points, and adjustable plot limits.

Key Features:

  • Profile Plotting: Real-time graph of intensity values (gray levels) versus distance.
  • Flexible Positioning: Define the line by a pivot point (vertical/horizontal) and an angle of rotation.
  • Navigation: Select specific slices using indices or keywords like 'middle'.
  • Zooming: Focus on specific segments of the line using the fraction_range parameter.

Parameters:

Name Type Description Default
volume ndarray

The 3D input volume (image stack).

required
slice_axis int

The axis along which to extract the 2D slice (0, 1, or 2). Defaults to 0.

0
slice_index int or str

The index of the slice to display. Can be an integer or a position string ('start', 'middle', 'end'). Defaults to 'middle'.

'middle'
vertical_position int or str

The vertical coordinate of the line's pivot point. Can be an integer or 'start', 'middle', 'end'. Defaults to 'middle'.

'middle'
horizontal_position int or str

The horizontal coordinate of the line's pivot point. Can be an integer or 'start', 'middle', 'end'. Defaults to 'middle'.

'middle'
angle int or float

The angle of the line in degrees relative to the horizontal axis. Floats are rounded to the nearest integer. Defaults to 0.

0
fraction_range tuple[float, float]

The start and end points of the line segment as a fraction of the image width/height (0.0 to 1.0). Defaults to (0.00, 1.00).

(0.0, 1.0)
y_limits str or tuple[float, float]

Controls the Y-axis range of the intensity plot. Defaults to 'auto'.

  • 'auto': Dynamically adapts to the min/max intensity on the current line.
  • 'full': Fixes the range to the global min/max of the entire volume.
  • tuple: Manually sets the range (e.g., (0, 255)).
'auto'

Returns:

Name Type Description
widget interactive

The interactive widget object containing the slice viewer and the intensity plot.

Example

import qim3d

vol = qim3d.examples.bone_128x128x128
qim3d.viz.line_profile(vol)
viz histogram

Source code in qim3d/viz/_data_exploration.py
@coarseness('volume')
def line_profile(
    volume: np.ndarray,
    slice_axis: int = 0,
    slice_index: int | str = 'middle',
    vertical_position: int | str = 'middle',
    horizontal_position: int | str = 'middle',
    angle: int = 0,
    fraction_range: tuple[float, float] = (0.00, 1.00),
    y_limits: str | tuple[float, float] = 'auto',
) -> widgets.interactive:
    """
    Creates an interactive tool to visualize intensity profiles along a line segment within a 3D volume.

    This function allows you to draw a line on a specific slice of your data and plot the pixel or voxel intensity values along that path. It is ideal for quantitative analysis, such as checking material homogeneity, measuring the sharpness of edges (step functions), or inspecting noise levels across a region of interest (ROI). The tool supports arbitrary angles, dynamic pivot points, and adjustable plot limits.

    **Key Features:**

    * **Profile Plotting:** Real-time graph of intensity values (gray levels) versus distance.
    * **Flexible Positioning:** Define the line by a pivot point (vertical/horizontal) and an angle of rotation.
    * **Navigation:** Select specific slices using indices or keywords like 'middle'.
    * **Zooming:** Focus on specific segments of the line using the `fraction_range` parameter.

    Args:
        volume (np.ndarray): The 3D input volume (image stack).
        slice_axis (int, optional): The axis along which to extract the 2D slice (0, 1, or 2). Defaults to 0.
        slice_index (int or str, optional): The index of the slice to display. Can be an integer or a position string ('start', 'middle', 'end'). Defaults to 'middle'.
        vertical_position (int or str, optional): The vertical coordinate of the line's pivot point. Can be an integer or 'start', 'middle', 'end'. Defaults to 'middle'.
        horizontal_position (int or str, optional): The horizontal coordinate of the line's pivot point. Can be an integer or 'start', 'middle', 'end'. Defaults to 'middle'.
        angle (int or float, optional): The angle of the line in degrees relative to the horizontal axis. Floats are rounded to the nearest integer. Defaults to 0.
        fraction_range (tuple[float, float], optional): The start and end points of the line segment as a fraction of the image width/height (0.0 to 1.0). Defaults to (0.00, 1.00).
        y_limits (str or tuple[float, float], optional): Controls the Y-axis range of the intensity plot. Defaults to 'auto'.

            * **'auto'**: Dynamically adapts to the min/max intensity on the current line.
            * **'full'**: Fixes the range to the global min/max of the entire volume.
            * **tuple**: Manually sets the range (e.g., `(0, 255)`).

    Returns:
        widget (widgets.interactive):
            The interactive widget object containing the slice viewer and the intensity plot.

    Example:
        ```python
        import qim3d

        vol = qim3d.examples.bone_128x128x128
        qim3d.viz.line_profile(vol)
        ```
        ![viz histogram](../../assets/screenshots/viz-line_profile.gif)
    """

    def parse_position(
        pos: int | str,
        pos_range: tuple[int, int],
        name: str,
    ) -> int:
        if isinstance(pos, int):
            if not pos_range[0] <= pos < pos_range[1]:
                msg = (
                    f'Value for {name} must be inside [{pos_range[0]}, {pos_range[1]}]'
                )
                raise ValueError(msg)
            return pos
        elif isinstance(pos, str):
            pos = pos.lower()
            if pos == 'start':
                return pos_range[0]
            elif pos == 'middle':
                return pos_range[0] + (pos_range[1] - pos_range[0]) // 2
            elif pos == 'end':
                return pos_range[1]
            else:
                msg = (
                    f"Invalid string '{pos}' for {name}. "
                    "Must be 'start', 'middle', or 'end'."
                )
                raise ValueError(msg)
        else:
            msg = 'Axis position must be of type int or str.'
            raise TypeError(msg)

    if not isinstance(volume, np.ndarray | da.Array):
        msg = 'Data type for volume not supported.'
        raise ValueError(msg)
    if volume.ndim != 3:
        msg = 'Volume must be 3D.'
        raise ValueError(msg)

    dims = volume.shape
    slice_index = parse_position(slice_index, (0, dims[slice_axis] - 1), 'slice_index')
    # the omission of the ends for the pivot point is due to border issues.
    vertical_position = parse_position(
        vertical_position, (1, np.delete(dims, slice_axis)[0] - 2), 'vertical_position'
    )
    horizontal_position = parse_position(
        horizontal_position,
        (1, np.delete(dims, slice_axis)[1] - 2),
        'horizontal_position',
    )

    if not isinstance(angle, float | int):
        msg = 'Invalid type for angle.'
        raise ValueError(msg)
    angle = round(angle) % 360

    if not (
        0.0 <= fraction_range[0] <= 1.0
        and 0.0 <= fraction_range[1] <= 1.0
        and fraction_range[0] <= fraction_range[1]
    ):
        msg = 'Invalid values for fraction_range.'
        raise ValueError(msg)

    if isinstance(y_limits, str):
        if y_limits not in ['auto', 'full']:
            msg = 'Invalid string value for y_limits.'
            raise ValueError(msg)
    else:
        y_limits = [*y_limits]

    lp = _LineProfile(
        volume,
        slice_axis,
        slice_index,
        vertical_position,
        horizontal_position,
        angle,
        fraction_range,
        y_limits,
    )
    return lp.build_interactive()

qim3d.viz.histogram

histogram(
    volume,
    coarseness=1,
    ignore_zero=True,
    bins='auto',
    slice_index=None,
    slice_axis=0,
    vertical_line=None,
    vertical_line_colormap='qim',
    kde=False,
    log_scale=False,
    despine=True,
    show_title=True,
    color='qim3d',
    edgecolor=None,
    figsize=(8, 4.5),
    bin_style='step',
    return_fig=False,
    show=True,
    ax=None,
    **sns_kwargs,
)

Computes and displays the intensity distribution (histogram) of a 3D volume or a specific 2D slice.

This function visualizes the frequency of voxel intensities (gray values), which is essential for analyzing data contrast, identifying material phases, and determining threshold values for segmentation. It utilizes seaborn.histplot and includes optimizations for 3D data, such as subsampling (coarseness) to handle large datasets efficiently. You can also overlay Kernel Density Estimates (KDE) or specific threshold markers.

Parameters:

Name Type Description Default
volume ndarray

The 3D input volume.

required
coarseness int or list[int]

Subsampling factor to speed up computation. A value of 1 uses all data; 2 uses every second voxel, etc.

1
ignore_zero bool

If True, excludes zero-valued voxels (background) from the statistics.

True
bins int or str

The number of bins or a binning strategy (e.g., 'auto', 'sturges').

'auto'
slice_index int, str, or None

The specific slice to analyze. If None, analyzes the entire volume. Accepts an integer index or 'middle'.

None
slice_axis int

The axis along which to extract the slice (0, 1, or 2). Used only if slice_index is provided.

0
vertical_line int or Iterable

One or more intensity values to mark with vertical dashed lines (e.g., to visualize a threshold cut-off).

None
vertical_line_colormap str or Iterable

The colormap or list of colors for the vertical lines.

'qim'
kde bool

If True, computes and overlays a Kernel Density Estimate (smooth curve) of the distribution.

False
log_scale bool

If True, sets the Y-axis (frequency) to a logarithmic scale.

False
despine bool

If True, removes the top and right borders of the plot for a cleaner look.

True
show_title bool

If True, adds a descriptive title with slice/volume information.

True
color str

The main color of the histogram bars.

'qim3d'
edgecolor str

The color of the bar edges.

None
figsize tuple[float, float]

The width and height of the figure in inches.

(8, 4.5)
bin_style str

The visual style of the histogram ('bars', 'step', or 'poly').

'step'
return_fig bool

If True, returns the matplotlib.figure.Figure object.

False
show bool

If True, calls plt.show() to display the plot.

True
ax Axes

An existing Axes object to plot onto. If provided, the function returns this Axes object (unless return_fig=True).

None
**sns_kwargs str | float | bool

Additional keyword arguments passed to seaborn.histplot.

{}

Returns:

Name Type Description
object matplotlib.figure.Figure, matplotlib.axes.Axes, or None

The plot object, depending on parameters:

  • matplotlib.figure.Figure: Returned if return_fig=True.
  • matplotlib.axes.Axes: Returned if return_fig=False and ax was provided.
  • None: If return_fig=False and no ax is provided (plot is displayed immediately).

Raises:

Type Description
ValueError

If slice_axis is invalid or slice_index is out of bounds.

Example

import qim3d

vol = qim3d.examples.bone_128x128x128
qim3d.viz.histogram(vol)
viz histogram

Histogram from a single slice

import qim3d

vol = qim3d.examples.bone_128x128x128
qim3d.viz.histogram(vol, slice_index=100, slice_axis=1, bin_style='bars', edgecolor='white')
viz histogram

Using coarseness for faster computation

import qim3d

vol = qim3d.examples.bone_128x128x128
qim3d.viz.histogram(vol, coarseness=2, kde=True, log_scale=True)
viz histogram

Source code in qim3d/viz/_data_exploration.py
@coarseness('volume')
def histogram(
    volume: np.ndarray,
    coarseness: int | list[int] = 1,
    ignore_zero: bool = True,
    bins: int | str = 'auto',
    slice_index: int | str | None = None,
    slice_axis: int = 0,
    vertical_line: int | Iterable = None,
    vertical_line_colormap: str | Iterable = 'qim',
    kde: bool = False,
    log_scale: bool = False,
    despine: bool = True,
    show_title: bool = True,
    color: str = 'qim3d',
    edgecolor: str | None = None,
    figsize: tuple[float, float] = (8, 4.5),
    bin_style: Literal['bars', 'step', 'poly'] = 'step',
    return_fig: bool = False,
    show: bool = True,
    ax: plt.Axes | None = None,
    **sns_kwargs: str | float | bool,
) -> plt.Figure | plt.Axes | None:
    """
    Computes and displays the intensity distribution (histogram) of a 3D volume or a specific 2D slice.

    This function visualizes the frequency of voxel intensities (gray values), which is essential for analyzing data contrast, identifying material phases, and determining threshold values for segmentation. It utilizes `seaborn.histplot` and includes optimizations for 3D data, such as subsampling (`coarseness`) to handle large datasets efficiently. You can also overlay Kernel Density Estimates (KDE) or specific threshold markers.

    Args:
        volume (np.ndarray): The 3D input volume.
        coarseness (int or list[int], optional): Subsampling factor to speed up computation. A value of `1` uses all data; `2` uses every second voxel, etc.
        ignore_zero (bool, optional): If `True`, excludes zero-valued voxels (background) from the statistics.
        bins (int or str, optional): The number of bins or a binning strategy (e.g., 'auto', 'sturges').
        slice_index (int, str, or None, optional): The specific slice to analyze. If `None`, analyzes the entire volume. Accepts an integer index or 'middle'.
        slice_axis (int, optional): The axis along which to extract the slice (0, 1, or 2). Used only if `slice_index` is provided.
        vertical_line (int or Iterable, optional): One or more intensity values to mark with vertical dashed lines (e.g., to visualize a threshold cut-off).
        vertical_line_colormap (str or Iterable, optional): The colormap or list of colors for the vertical lines.
        kde (bool, optional): If `True`, computes and overlays a Kernel Density Estimate (smooth curve) of the distribution.
        log_scale (bool, optional): If `True`, sets the Y-axis (frequency) to a logarithmic scale.
        despine (bool, optional): If `True`, removes the top and right borders of the plot for a cleaner look.
        show_title (bool, optional): If `True`, adds a descriptive title with slice/volume information.
        color (str, optional): The main color of the histogram bars.
        edgecolor (str, optional): The color of the bar edges.
        figsize (tuple[float, float], optional): The width and height of the figure in inches.
        bin_style (str, optional): The visual style of the histogram ('bars', 'step', or 'poly').
        return_fig (bool, optional): If `True`, returns the `matplotlib.figure.Figure` object.
        show (bool, optional): If `True`, calls `plt.show()` to display the plot.
        ax (matplotlib.axes.Axes, optional): An existing Axes object to plot onto. If provided, the function returns this Axes object (unless `return_fig=True`).
        **sns_kwargs: Additional keyword arguments passed to `seaborn.histplot`.

    Returns:
        object (matplotlib.figure.Figure, matplotlib.axes.Axes, or None):
            The plot object, depending on parameters:

            * **matplotlib.figure.Figure**: Returned if `return_fig=True`.
            * **matplotlib.axes.Axes**: Returned if `return_fig=False` and `ax` was provided.
            * **None**: If `return_fig=False` and no `ax` is provided (plot is displayed immediately).

    Raises:
        ValueError: If `slice_axis` is invalid or `slice_index` is out of bounds.

    Example:
        ```python
        import qim3d

        vol = qim3d.examples.bone_128x128x128
        qim3d.viz.histogram(vol)
        ```
        ![viz histogram](../../assets/screenshots/viz-histogram-vol.png)

    Example: Histogram from a single slice
        ```python
        import qim3d

        vol = qim3d.examples.bone_128x128x128
        qim3d.viz.histogram(vol, slice_index=100, slice_axis=1, bin_style='bars', edgecolor='white')
        ```
        ![viz histogram](../../assets/screenshots/viz-histogram-slice.png)

    Example: Using coarseness for faster computation
        ```python
        import qim3d

        vol = qim3d.examples.bone_128x128x128
        qim3d.viz.histogram(vol, coarseness=2, kde=True, log_scale=True)
        ```
        ![viz histogram](../../assets/screenshots/viz-histogram-coarse.png)
    """
    if not (0 <= slice_axis < volume.ndim):
        msg = f'Axis must be an integer between 0 and {volume.ndim - 1}.'
        raise ValueError(msg)

    title_suffixes = []

    if slice_index == 'middle':
        slice_index = volume.shape[slice_axis] // 2

    if slice_index is not None:
        if 0 <= slice_index < volume.shape[slice_axis]:
            img_slice = np.take(volume, indices=slice_index, axis=slice_axis)
            data = img_slice.ravel()
            title = f'Intensity histogram of slice #{slice_index} {img_slice.shape} along axis {slice_axis}'
        else:
            msg = f'Slice index out of range. Must be between 0 and {volume.shape[slice_axis] - 1}.'
            raise ValueError(msg)
    else:
        data = volume.ravel()
        title = f'Intensity histogram for volume {volume.shape}'

    if ignore_zero:
        data = data[data > 0]
        title_suffixes.append('zero-values ignored')

    if title_suffixes:
        title += ' (' + ', '.join(title_suffixes) + ')'

    # Use provided Axes or create new figure
    if ax is None:
        fig, ax = plt.subplots(figsize=figsize)
    else:
        fig = None

    if log_scale:
        ax.set_yscale('log')

    if color == 'qim3d':
        color = qim3d.viz.colormaps.qim(1.0)

    sns.histplot(
        data,
        bins=bins,
        kde=kde,
        color=color,
        element=bin_style,
        edgecolor=edgecolor,
        ax=ax,  # Plot directly on the specified Axes
        **sns_kwargs,
    )

    if vertical_line is not None:

        if isinstance(vertical_line_colormap, str):
            colors = matplotlib.colormaps[vertical_line_colormap]
        elif isinstance(vertical_line_colormap, Iterable):
            colors = lambda x: vertical_line_colormap[x]


        if isinstance(vertical_line, (float, int)):
            ax.axvline(
                x=vertical_line,
                color=colors(0),
                linestyle='--',
                linewidth=2,
            )
        elif isinstance(vertical_line, Iterable):
            for index, line_position in enumerate(vertical_line):
                if isinstance(vertical_line_colormap, str):
                    index = index/(max(len(vertical_line)-1, 1))
                ax.axvline(
                    x=line_position,
                    color=colors(index),
                    linestyle='--',
                    linewidth=2,
                )

    if despine:
        sns.despine(
            fig=None,
            ax=ax,
            top=True,
            right=True,
            left=False,
            bottom=False,
            offset={'left': 0, 'bottom': 18},
            trim=True,
        )

    ax.set_xlabel('Voxel Intensity')
    ax.set_ylabel('Frequency')

    if show_title:
        ax.set_title(title, fontsize=10)

    # Handle show and return
    if show and fig is not None:
        plt.show()

    if return_fig:
        return fig
    elif ax is not None:
        return ax

qim3d.viz.overlay

overlay(
    volume1,
    volume2,
    volume1_values=(None, None),
    volume2_values=(None, None),
    colormaps='gray',
    display_size=512,
)

Creates an interactive widget to compare two 3D volumes by overlaying them with adjustable transparency.

This tool is essential for tasks like image registration (checking alignment between two scans), segmentation validation (comparing a binary mask against the original raw data), or general change detection. It provides a slider to smoothly fade (blend) between the two volumes, allowing for precise visual inspection of differences and spatial correspondence slice-by-slice.

Parameters:

Name Type Description Default
volume1 ndarray

The first 3D volume (e.g., the reference image).

required
volume2 ndarray

The second 3D volume (e.g., the moving image or segmentation mask). Must have the same shape as volume1.

required
volume1_values tuple[float, float]

Intensity limits (min, max) for contrast stretching volume1. If (None, None), uses the volume's min and max.

(None, None)
volume2_values tuple[float, float]

Intensity limits (min, max) for contrast stretching volume2.

(None, None)
colormaps str or Colormap or tuple

The colormap(s) to apply. Can be a single value (applied to both) or a tuple (cmap1, cmap2). Defaults to 'gray'.

'gray'
display_size int

The maximum width/height of the displayed image in pixels. Defaults to 512.

512

Returns:

Name Type Description
widget VBox

The interactive widget containing the slicer controls and the fading overlay display.

Example

import qim3d

vol = qim3d.examples.cement_128x128x128
binary = qim3d.filters.gaussian(vol, sigma=2) < 60
labeled_volume, num_labels = qim3d.segmentation.watershed(binary)

segm_cmap = qim3d.viz.colormaps.segmentation(num_labels, style = 'bright')

qim3d.viz.overlay(vol, labeled_volume, colormaps=('grey', segm_cmap), volume2_values=(0, num_labels))
viz overlay

Source code in qim3d/viz/_data_exploration.py
def overlay(
    volume1: np.ndarray,
    volume2: np.ndarray,
    volume1_values: tuple[float, float] = (None, None),
    volume2_values: tuple[float, float] = (None, None),
    colormaps: ColormapLike | tuple[ColormapLike, ColormapLike] = 'gray',
    display_size: int = 512,
) -> widgets.interactive:
    """
    Creates an interactive widget to compare two 3D volumes by overlaying them with adjustable transparency.

    This tool is essential for tasks like image registration (checking alignment between two scans), segmentation validation (comparing a binary mask against the original raw data), or general change detection. It provides a slider to smoothly fade (blend) between the two volumes, allowing for precise visual inspection of differences and spatial correspondence slice-by-slice.

    Args:
        volume1 (np.ndarray): The first 3D volume (e.g., the reference image).
        volume2 (np.ndarray): The second 3D volume (e.g., the moving image or segmentation mask). Must have the same shape as `volume1`.
        volume1_values (tuple[float, float], optional): Intensity limits `(min, max)` for contrast stretching `volume1`. If `(None, None)`, uses the volume's min and max.
        volume2_values (tuple[float, float], optional): Intensity limits `(min, max)` for contrast stretching `volume2`.
        colormaps (str or matplotlib.colors.Colormap or tuple, optional): The colormap(s) to apply. Can be a single value (applied to both) or a tuple `(cmap1, cmap2)`. Defaults to 'gray'.
        display_size (int, optional): The maximum width/height of the displayed image in pixels. Defaults to 512.

    Returns:
        widget (widgets.VBox):
            The interactive widget containing the slicer controls and the fading overlay display.

    Example:
        ```python
        import qim3d

        vol = qim3d.examples.cement_128x128x128
        binary = qim3d.filters.gaussian(vol, sigma=2) < 60
        labeled_volume, num_labels = qim3d.segmentation.watershed(binary)

        segm_cmap = qim3d.viz.colormaps.segmentation(num_labels, style = 'bright')

        qim3d.viz.overlay(vol, labeled_volume, colormaps=('grey', segm_cmap), volume2_values=(0, num_labels))
        ```
        ![viz overlay](../../assets/screenshots/viz-overlay.gif)
    """
    if volume1.ndim != 3:
        msg = 'Volume must be 3D.'
        raise ValueError(msg)
    if volume1.shape != volume2.shape:
        msg = 'Volumes must have the same shape.'
        raise ValueError(msg)

    interactive_widget = OverlaySlicer(
        vol1=volume1,
        vol2=volume2,
        cmaps=colormaps,
        display_size=display_size,
        volume1_values=volume1_values,
        volume2_values=volume2_values,
    ).build_interactive()
    return interactive_widget

qim3d.viz.compare_volumes

compare_volumes(
    volume1,
    volume2,
    slice_axis=0,
    slice_index=None,
    volumetric_visualization=False,
)

Launches an interactive dashboard to visually compare two 3D volumes side-by-side.

This tool is essential for registration validation (checking alignment), change detection, or analyzing reconstruction errors (residuals). It displays synchronized slices of both volumes alongside a computed difference map. You can switch between 'difference', 'absolute difference', and 'quadratic difference' modes to highlight discrepancies effectively.

If enabled, the tool also provides 3D volumetric rendering (via k3d), allowing you to inspect the spatial distribution of the errors or changes in 3D space.

Parameters:

Name Type Description Default
volume1 ndarray

The first 3D volume (e.g., Ground Truth or Reference).

required
volume2 ndarray

The second 3D volume (e.g., Prediction or Moving Image). Must have the same shape as volume1.

required
slice_axis int

The initial axis along which to slice (0, 1, or 2). Defaults to 0.

0
slice_index int

The initial slice index to display. If None, defaults to the middle slice.

None
volumetric_visualization bool

If True, includes interactive 3D volumetric renderings below the slice views. Defaults to False.

False

Returns:

Name Type Description
widget VBox

The interactive widget containing the comparison controls, slice plots, and optional 3D views.

Example

import qim3d

vol1 = qim3d.generate.volume(noise_scale=0.020, dtype='float32')
vol2 = qim3d.generate.volume(noise_scale=0.021, dtype='float32')

qim3d.viz.compare_volumes(vol1, vol2, volumetric_visualization=True)
volume_comparison

Source code in qim3d/viz/_data_exploration.py
@coarseness('volume1', 'volume2')
def compare_volumes(
    volume1: np.ndarray,
    volume2: np.ndarray,
    slice_axis: int = 0,
    slice_index: int = None,
    volumetric_visualization: bool = False,
) -> widgets.interactive:
    """
    Launches an interactive dashboard to visually compare two 3D volumes side-by-side.

    This tool is essential for registration validation (checking alignment), change detection, or analyzing reconstruction errors (residuals). It displays synchronized slices of both volumes alongside a computed difference map. You can switch between 'difference', 'absolute difference', and 'quadratic difference' modes to highlight discrepancies effectively.

    If enabled, the tool also provides 3D volumetric rendering (via `k3d`), allowing you to inspect the spatial distribution of the errors or changes in 3D space.

    Args:
        volume1 (np.ndarray): The first 3D volume (e.g., Ground Truth or Reference).
        volume2 (np.ndarray): The second 3D volume (e.g., Prediction or Moving Image). Must have the same shape as `volume1`.
        slice_axis (int, optional): The initial axis along which to slice (0, 1, or 2). Defaults to 0.
        slice_index (int, optional): The initial slice index to display. If `None`, defaults to the middle slice.
        volumetric_visualization (bool, optional): If `True`, includes interactive 3D volumetric renderings below the slice views. Defaults to `False`.

    Returns:
        widget (widgets.VBox):
            The interactive widget containing the comparison controls, slice plots, and optional 3D views.

    Example:
        ```python
        import qim3d

        vol1 = qim3d.generate.volume(noise_scale=0.020, dtype='float32')
        vol2 = qim3d.generate.volume(noise_scale=0.021, dtype='float32')

        qim3d.viz.compare_volumes(vol1, vol2, volumetric_visualization=True)
        ```
        ![volume_comparison](../../assets/screenshots/viz-compare_volumes.png)
    """

    if volume1.ndim != 3:
        msg = 'Volume must be 3D.'
        raise ValueError(msg)
    if volume1.shape != volume2.shape:
        msg = 'Volumes must have the same shape.'
        raise ValueError(msg)

    if np.issubdtype(volume1.dtype, np.unsignedinteger) and np.issubdtype(
        volume2.dtype, np.unsignedinteger
    ):
        log.warning(
            'Volumes have unsigned integer datatypes. Beware of over-/underflow.'
        )

    if slice_axis not in (0, 1, 2):
        msg = 'Invalid slice_axis.'
        raise ValueError(msg)

    if slice_index is None:
        slice_index = volume1.shape[slice_axis] // 2
    if not isinstance(slice_index, int):
        msg = 'slice_index must be an integer.'
        raise ValueError(msg)

    vc = _VolumeComparison(
        volume1, volume2, slice_axis, slice_index, volumetric_visualization
    )
    return vc.build_interactive()

qim3d.viz.chunks

chunks(zarr_path, **kwargs)

Launches an interactive explorer for large-scale OME-Zarr and Zarr datasets.

This tool enables you to inspect massive 3D or 5D datasets (e.g., bio-imaging pyramids, large block-wise volumes) one chunk at a time without loading the entire file into RAM. It relies on lazy loading, making it ideal for checking data integrity, visualizing specific regions of interest (ROI) in big data, or navigating multi-resolution hierarchies.

Key Features:

  • Lazy Exploration: Loads only the specific chunk selected via dropdown menus.
  • Multiscale Support: Automatically detects and navigates resolution levels (pyramids) in OME-Zarr groups.
  • 5D Navigation: Supports dimensions for Time (T) and Channel (C) in addition to spatial axes (Z, Y, X).
  • Versatile Visualization: Switch instantly between a slicer, a slices_grid, or a 3D volumetric rendering for the selected chunk.

Parameters:

Name Type Description Default
zarr_path str

The filesystem path to the OME-Zarr or Zarr dataset.

required
**kwargs Any

Additional keyword arguments passed selectively to the underlying visualization function. For example, you can pass cmap='magma' (for slicer/slices) or zoom=1.5 (for volume). Arguments not supported by the currently selected visualization method are ignored.

{}

Returns:

Name Type Description
chunk_explorer VBox

The interactive interface containing controls for scale selection, chunk coordinates, and the visualization display.

Raises:

Type Description
ValueError

If the dataset dimensionality is not 3D or 5D.

Example

import qim3d

# Visualize interactive chunks explorer
qim3d.viz.chunks('path/to/zarr/dataset.zarr')
interactive chunks explorer

Source code in qim3d/viz/_data_exploration.py
def chunks(zarr_path: str, **kwargs) -> widgets.VBox:
    """
    Launches an interactive explorer for large-scale OME-Zarr and Zarr datasets.

    This tool enables you to inspect massive 3D or 5D datasets (e.g., bio-imaging pyramids, large block-wise volumes) one chunk at a time without loading the entire file into RAM. It relies on lazy loading, making it ideal for checking data integrity, visualizing specific regions of interest (ROI) in big data, or navigating multi-resolution hierarchies.

    **Key Features:**

    * **Lazy Exploration:** Loads only the specific chunk selected via dropdown menus.
    * **Multiscale Support:** Automatically detects and navigates resolution levels (pyramids) in OME-Zarr groups.
    * **5D Navigation:** Supports dimensions for Time (T) and Channel (C) in addition to spatial axes (Z, Y, X).
    * **Versatile Visualization:** Switch instantly between a `slicer`, a `slices_grid`, or a 3D `volumetric` rendering for the selected chunk.

    Args:
        zarr_path (str): The filesystem path to the OME-Zarr or Zarr dataset.
        **kwargs (Any): Additional keyword arguments passed selectively to the underlying visualization function.
            For example, you can pass `cmap='magma'` (for `slicer`/`slices`) or `zoom=1.5` (for `volume`). Arguments not supported by the currently selected visualization method are ignored.

    Returns:
        chunk_explorer (widgets.VBox):
            The interactive interface containing controls for scale selection, chunk coordinates, and the visualization display.

    Raises:
        ValueError: If the dataset dimensionality is not 3D or 5D.

    Example:
        ```python
        import qim3d

        # Visualize interactive chunks explorer
        qim3d.viz.chunks('path/to/zarr/dataset.zarr')
        ```
        ![interactive chunks explorer](../../assets/screenshots/chunks_explorer.gif)
    """
    # Opens the Zarr dataset - doesn't load to memory yet
    zarr_data = zarr.open(zarr_path, mode='r')

    title = widgets.HTML('<h2>Chunk Explorer</h2>')
    info_label = widgets.HTML(value='Chunk info will be displayed here')

    def get_num_chunks(shape: Sequence[int], chunk_size: Sequence[int]) -> list[int]:
        return [(s + chunk_size[i] - 1) // chunk_size[i] for i, s in enumerate(shape)]

    def _filter_kwargs(
        function: Callable[..., Any], kwargs: dict[str, Any]
    ) -> dict[str, Any]:
        """Filter kwargs to only include those that are accepted by the function."""
        sig = inspect.signature(function)
        return {k: v for k, v in kwargs.items() if k in sig.parameters}

    def load_and_visualize(
        key: int,
        *coords: int,
        visualization_method: Literal['slicer', 'slices', 'volume'],
        **inner_kwargs: object,
    ) -> Widget | Figure | Output:
        key = _path_from_dropdown(key)
        arr = da.from_zarr(zarr_data) if isinstance(zarr_data, zarr.Array) else da.from_zarr(zarr_data[key])
        shape = arr.shape
        chunksz = arr.chunks

        if arr.ndim == 3:
            z_idx, y_idx, x_idx = coords
            slices = (
                slice(
                    z_idx * chunksz[0][0], min((z_idx + 1) * chunksz[0][0], shape[0])
                ),
                slice(
                    y_idx * chunksz[1][0], min((y_idx + 1) * chunksz[1][0], shape[1])
                ),
                slice(
                    x_idx * chunksz[2][0], min((x_idx + 1) * chunksz[2][0], shape[2])
                ),
            )
            chunk = arr[slices].compute()
        elif arr.ndim == 5:
            t_idx, c_idx, z_idx, y_idx, x_idx = coords
            slices = (
                slice(
                    t_idx * chunksz[0][0], min((t_idx + 1) * chunksz[0][0], shape[0])
                ),
                slice(
                    c_idx * chunksz[1][0], min((c_idx + 1) * chunksz[1][0], shape[1])
                ),
                slice(
                    z_idx * chunksz[2][0], min((z_idx + 1) * chunksz[2][0], shape[2])
                ),
                slice(
                    y_idx * chunksz[3][0], min((y_idx + 1) * chunksz[3][0], shape[3])
                ),
                slice(
                    x_idx * chunksz[4][0], min((x_idx + 1) * chunksz[4][0], shape[4])
                ),
            )
            chunk = arr[slices].compute()
            chunk = chunk[0, 0, ...]
        else:
            msg = f'Unsupported ndim={arr.ndim}'
            raise ValueError(msg)

        mins, maxs, means = chunk.min(), chunk.max(), chunk.mean()
        ranges = [f'{sl.start}-{sl.stop}' for sl in slices]
        coords_str = ', '.join(str(c) for c in coords)
        info_html = (
            f"<div style='font-size:14px; margin-left:32px'>"
            f"<h3 style='margin:0'>Chunk Info</h3>"
            f'<pre>'
            f'shape      : {chunk.shape}\n'
            f'coords     : ({coords_str})\n'
            f'ranges     : {ranges}\n'
            f'dtype      : {chunk.dtype}\n'
            f'min / max  : {mins:.0f} / {maxs:.0f}\n'
            f'mean value : {means:.0f}\n'
            f'</pre></div>'
        )
        info_label.value = info_html

        if visualization_method == 'slicer':
            kw = _filter_kwargs(qim3d.viz.slicer, inner_kwargs)
            return qim3d.viz.slicer(chunk, **kw)
        if visualization_method == 'slices':
            out = widgets.Output()
            with out:
                kw = _filter_kwargs(qim3d.viz.slices_grid, inner_kwargs)
                fig = qim3d.viz.slices_grid(chunk, **kw)
                display(fig)
            return out
        # volume
        out = widgets.Output()
        with out:
            kw = _filter_kwargs(qim3d.viz.volumetric, inner_kwargs)
            vol = qim3d.viz.volumetric(chunk, show=False, **kw)
            display(vol)
        return out

    def _path_from_dropdown(string:str):
        return string.split('(')[0].strip()

    if isinstance(zarr_data, zarr.Group): 
        scale_opts = [f'{key} {zarr_data[key].shape}' for key in sorted(zarr_data.keys())]
    elif isinstance(zarr_data, zarr.Array):
        scale_opts = [f'{zarr_data.shape}',]
    drop_style = {'description_width': '120px'}
    scale_dd = widgets.Dropdown(
        options=scale_opts, description='Scale:', style=drop_style
    )

    # first_shape = zarr_data[0].shape
    if isinstance(zarr_data, zarr.Array):
        first_shape = zarr_data.shape
        chunks = zarr_data.chunks
    else:
        first_array = zarr_data[_path_from_dropdown(scale_opts[0])]
        first_shape = first_array.shape
        chunks = first_array.chunks


    if len(first_shape) == 3:
        axis_names = ['Z', 'Y', 'X']
    elif len(first_shape) == 5:
        axis_names = ['T', 'C', 'Z', 'Y', 'X']
    else:
        msg = f'Only 3D or 5D supported, got ndim={len(first_shape)}'
        raise ValueError(msg)

    counts0 = get_num_chunks(first_shape, chunks)
    axis_dds = []
    for name, cnt in zip(axis_names, counts0):
        dd = widgets.Dropdown(
            options=list(range(cnt)), value=0, description=f'{name}:', style=drop_style
        )
        axis_dds.append(dd)

    method_dd = widgets.Dropdown(
        options=['slicer', 'slices', 'volume'],
        value='slicer',
        description='Viz:',
        style=drop_style,
    )

    def disable_observers() -> None:
        for dd in (*axis_dds, method_dd):
            dd.unobserve(_update_vis, names='value')

    def enable_observers() -> None:
        for dd in (*axis_dds, method_dd):
            dd.observe(_update_vis, names='value')

    def _update_coords(key: str) -> None:
        disable_observers()
        key = _path_from_dropdown(key)
        shp = zarr_data[key].shape
        cnts = get_num_chunks(shp, zarr_data[key].chunks)
        for dd, c in zip(axis_dds, cnts):
            dd.options = list(range(c))
            dd.disabled = c == 1
            dd.value = 0
        enable_observers()
        _update_vis()

    def _update_vis(*_) -> None:
        coords = [dd.value for dd in axis_dds]
        widget = load_and_visualize(
            scale_dd.value, *coords, visualization_method=method_dd.value, **kwargs
        )
        container.children = [title, controls_with_info, widget]

    scale_dd.observe(lambda change: _update_coords(scale_dd.value), names='value')
    enable_observers()

    initial = load_and_visualize(
        scale_dd.value,
        *[dd.value for dd in axis_dds],
        visualization_method=method_dd.value,
        **kwargs,
    )

    control_box = widgets.VBox([scale_dd, *axis_dds, method_dd])
    controls_with_info = widgets.HBox([control_box, info_label])
    container = widgets.VBox([title, controls_with_info, initial])
    return container

qim3d.viz.fade_mask

fade_mask(
    volume,
    axis=0,
    colormap='magma',
    min_value=None,
    max_value=None,
)

Launches an interactive tool to tune parameters for edge fading (vignetting) on a 3D volume.

This function helps you find the optimal settings for suppressing boundary artifacts or focusing on the center of the volume. It visualizes the process by displaying three panels: the original slice, the generated weight mask (attenuation map), and the final result. You can adjust the decay rate, ratio (radius), and geometry (spherical or cylindrical) in real-time before applying them permanently using qim3d.operations.fade_mask.

Parameters:

Name Type Description Default
volume ndarray

The 3D input volume.

required
axis int

The axis alignment for the mask geometry (relevant for cylindrical fading). Defaults to 0.

0
colormap str

The Matplotlib colormap used for displaying the volume and mask. Defaults to 'magma'.

'magma'
min_value float

Custom minimum intensity for display contrast. If None, uses the data minimum.

None
max_value float

Custom maximum intensity for display contrast. If None, uses the data maximum.

None

Returns:

Name Type Description
slicer_obj interactive

The interactive widget containing the parameter sliders and the side-by-side visualization.

Example

import qim3d
vol = qim3d.examples.cement_128x128x128
qim3d.viz.fade_mask(vol)
operations-edge_fade_before

Source code in qim3d/viz/_data_exploration.py
@coarseness('volume')
def fade_mask(
    volume: np.ndarray,
    axis: int = 0,
    colormap: str = 'magma',
    min_value: float = None,
    max_value: float = None,
) -> widgets.interactive:
    """
    Launches an interactive tool to tune parameters for edge fading (vignetting) on a 3D volume.

    This function helps you find the optimal settings for suppressing boundary artifacts or focusing on the center of the volume. It visualizes the process by displaying three panels: the original slice, the generated weight mask (attenuation map), and the final result. You can adjust the decay rate, ratio (radius), and geometry (spherical or cylindrical) in real-time before applying them permanently using `qim3d.operations.fade_mask`.

    Args:
        volume (np.ndarray): The 3D input volume.
        axis (int, optional): The axis alignment for the mask geometry (relevant for cylindrical fading). Defaults to 0.
        colormap (str, optional): The Matplotlib colormap used for displaying the volume and mask. Defaults to 'magma'.
        min_value (float, optional): Custom minimum intensity for display contrast. If `None`, uses the data minimum.
        max_value (float, optional): Custom maximum intensity for display contrast. If `None`, uses the data maximum.

    Returns:
        slicer_obj (widgets.interactive):
            The interactive widget containing the parameter sliders and the side-by-side visualization.

    Example:
        ```python
        import qim3d
        vol = qim3d.examples.cement_128x128x128
        qim3d.viz.fade_mask(vol)
        ```
        ![operations-edge_fade_before](../../assets/screenshots/viz-fade_mask.gif)
    """

    # Create the interactive widget
    def _slicer(
        position: int,
        decay_rate: float,
        ratio: float,
        geometry: str,
        invert: bool,
    ) -> Figure:
        fig, axes = plt.subplots(1, 3, figsize=(9, 3))

        slice_img = volume[position, :, :]
        # If min_value is higher than the highest value in the image ValueError is raised
        # We don't want to override the values because next slices might be okay
        new_min_value = (
            None
            if (isinstance(min_value, float | int) and min_value > np.max(slice_img))
            else min_value
        )
        new_max_value = (
            None
            if (isinstance(max_value, float | int) and max_value < np.min(slice_img))
            else max_value
        )

        axes[0].imshow(
            slice_img, cmap=colormap, vmin=new_min_value, vmax=new_max_value
        )
        axes[0].set_title('Original')
        axes[0].axis('off')

        mask = qim3d.operations.fade_mask(
            np.ones_like(volume),
            decay_rate=decay_rate,
            ratio=ratio,
            geometry=geometry,
            axis=axis,
            invert=invert,
        )
        axes[1].imshow(mask[position, :, :], cmap=colormap)
        axes[1].set_title('Mask')
        axes[1].axis('off')

        masked_volume = qim3d.operations.fade_mask(
            volume,
            decay_rate=decay_rate,
            ratio=ratio,
            geometry=geometry,
            axis=axis,
            invert=invert,
        )
        # If min_value is higher than the highest value in the image ValueError is raised
        # We don't want to override the values because next slices might be okay
        slice_img = masked_volume[position, :, :]
        new_min_value = (
            None
            if (isinstance(min_value, float | int) and min_value > np.max(slice_img))
            else min_value
        )
        new_max_value = (
            None
            if (isinstance(max_value, float | int) and max_value < np.min(slice_img))
            else max_value
        )
        axes[2].imshow(
            slice_img, cmap=colormap, vmin=new_min_value, vmax=new_max_value
        )
        axes[2].set_title('Masked')
        axes[2].axis('off')

        return fig

    shape_dropdown = widgets.Dropdown(
        options=['spherical', 'cylindrical'],
        value='spherical',  # default value
        description='Geometry',
    )

    position_slider = widgets.IntSlider(
        value=volume.shape[0] // 2,
        min=0,
        max=volume.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,
        description='Invert',  # default value
    )

    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