Skip to content

Explore data

Visualization of volumetric data.

qim3d.viz.threshold

threshold(volume, cmap_image='magma', vmin=None, vmax=None)

An interactive interface to explore thresholding on a 3D volume slice-by-slice. Users can either manually set the threshold value using a slider or select an automatic thresholding method from skimage.

The visualization includes the original image slice, a binary mask showing regions above the threshold and an overlay combining the binary mask and the original image.

Parameters:

Name Type Description Default
volume ndarray

3D volume to threshold.

required
cmap_image str

Colormap for the original image. Defaults to 'viridis'.

'magma'
vmin float

Minimum value for the colormap. Defaults to None.

None
vmax float

Maximum value for the colormap. Defaults to None.

None

Returns:

Name Type Description
slicer_obj VBox

The interactive widget for thresholding a 3D volume.

Interactivity
  • Manual Thresholding: Select 'Manual' from the dropdown menu to manually adjust the threshold using the slider.
  • Automatic Thresholding: Choose a method from the dropdown menu to apply an automatic thresholding algorithm. Available methods include:

    • Otsu
    • Isodata
    • Li
    • Mean
    • Minimum
    • Triangle
    • Yen

    The threshold slider will display the computed value and will be disabled in this mode.

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,
    cmap_image: str = 'magma',
    vmin: float = None,
    vmax: float = None,
) -> widgets.VBox:
    """
    An interactive interface to explore thresholding on a
    3D volume slice-by-slice. Users can either manually set the threshold value
    using a slider or select an automatic thresholding method from `skimage`.

    The visualization includes the original image slice, a binary mask showing regions above the
    threshold and an overlay combining the binary mask and the original image.

    Args:
        volume (np.ndarray): 3D volume to threshold.
        cmap_image (str, optional): Colormap for the original image. Defaults to 'viridis'.
        vmin (float, optional): Minimum value for the colormap. Defaults to None.
        vmax (float, optional): Maximum value for the colormap. Defaults to None.

    Returns:
        slicer_obj (widgets.VBox): The interactive widget for thresholding a 3D volume.

    Interactivity:
        - **Manual Thresholding**:
            Select 'Manual' from the dropdown menu to manually adjust the threshold
            using the slider.
        - **Automatic Thresholding**:
            Choose a method from the dropdown menu to apply an automatic thresholding
            algorithm. Available methods include:
            - Otsu
            - Isodata
            - Li
            - Mean
            - Minimum
            - Triangle
            - Yen

            The threshold slider will display the computed value and will be disabled
            in this mode.


        ```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,
        'threshold': int((volume.min() + volume.max()) / 2),
        'method': 'Manual',
    }

    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 = volume[state['position'], :, :]
        with output:
            output.clear_output(wait=True)  # Clear previous plot
            fig, axes = plt.subplots(1, 4, figsize=(25, 5))

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

            # Histogram
            histogram(
                volume=volume,
                bins=32,
                slice_idx=state['position'],
                vertical_line=state['threshold'],
                slice_axis=1,
                kde=False,
                ax=axes[1],
                show=False,
            )
            axes[1].set_title(f"Histogram with Threshold = {int(state['threshold'])}")

            # 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_vmin, vmax=new_vmax)
            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.IntSlider(
        value=state['threshold'],
        min=volume.min(),
        max=volume.max(),
        description='Threshold',
    )

    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',
)

Returns an interactive widget for visualizing the intensity profiles of lines on slices.

Parameters:

Name Type Description Default
volume ndarray

The 3D volume of interest.

required
slice_axis int

Specifies the initial axis along which to slice.

0
slice_index int or str

Specifies the initial slice index along slice_axis.

'middle'
vertical_position int or str

Specifies the initial vertical position of the line's pivot point.

'middle'
horizontal_position int or str

Specifies the initial horizontal position of the line's pivot point.

'middle'
angle int or float

Specifies the initial angle (°) of the line around the pivot point. A float will be converted to an int. A value outside the range will be wrapped modulo.

0
fraction_range tuple or list

Specifies the fraction of the line segment to use from border to border. Both the start and the end should be in the range [0.0, 1.0].

(0.0, 1.0)
y_limits str or tuple or list

Specifies the behaviour of the limits on the y-axis of the intensity value plot. Option 'full' fixes to the volume's data range. Option 'auto' automatically adapts to the intensities on the current line. A manual range can be specified by passing a tuple or list of length 2. Defaults to 'auto'.

'auto'

Returns:

Name Type Description
widget VBox

The interactive widget.

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:
    """
    Returns an interactive widget for visualizing the intensity profiles of lines on slices.

    Args:
        volume (np.ndarray): The 3D volume of interest.
        slice_axis (int, optional): Specifies the initial axis along which to slice.
        slice_index (int or str, optional): Specifies the initial slice index along slice_axis.
        vertical_position (int or str, optional): Specifies the initial vertical position of the line's pivot point.
        horizontal_position (int or str, optional): Specifies the initial horizontal position of the line's pivot point.
        angle (int or float, optional): Specifies the initial angle (°) of the line around the pivot point. A float will be converted to an int. A value outside the range will be wrapped modulo.
        fraction_range (tuple or list, optional): Specifies the fraction of the line segment to use from border to border. Both the start and the end should be in the range [0.0, 1.0].
        y_limits (str or tuple or list, optional): Specifies the behaviour of the limits on the y-axis of the intensity value plot. Option 'full' fixes to the volume's data range. Option 'auto' automatically adapts to the intensities on the current line. A manual range can be specified by passing a tuple or list of length 2. Defaults to 'auto'.

    Returns:
        widget (widgets.widget_box.VBox): The interactive widget.


    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.core.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_idx=None,
    slice_axis=0,
    vertical_line=None,
    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,
)

Plots a histogram of voxel intensities from a 3D volume, with options to show a specific slice or the entire volume.

Utilizes seaborn.histplot for visualization.

Parameters:

Name Type Description Default
volume ndarray

A 3D NumPy array representing the volume to be visualized.

required
coarseness int or list[int]

A positive integer representing the coarseness of the subsampling. A value of 1 (default) uses the original volume, a value of 2 uses every second element along each axis and so on. Used to reduce the needed computation.

1
ignore_zero bool

Specifies if zero-values in the volume should be ignored.

True
bins Union[int, str]

Number of histogram bins or a binning strategy (e.g., "auto"). Default is "auto".

'auto'
slice_axis int

Axis along which to take a slice. Default is 0.

0
slice_idx Union[int, str]

Specifies the slice to visualize. If an integer, it represents the slice index along the selected axis. If "middle", the function uses the middle slice. If None, the entire volume is visualized. Default is None.

None
vertical_line int

Intensity value for a vertical line to be drawn on the histogram. Default is None.

None
kde bool

Whether to overlay a kernel density estimate.

False
log_scale bool

Whether to use a logarithmic scale on the y-axis. Default is False.

False
despine bool

If True, removes the top and right spines from the plot for cleaner appearance. Default is True.

True
show_title bool

If True, displays a title with slice information. Default is True.

True
color str

Color for the histogram bars. If "qim3d", defaults to the qim3d color. Default is "qim3d".

'qim3d'
edgecolor str

Color for the edges of the histogram bars. Default is None.

None
figsize tuple

Size of the figure (width, height). Default is (8, 4.5).

(8, 4.5)
bin_style str

Type of histogram to draw ('bars', 'step', or 'poly'). Default is "step".

'step'
return_fig bool

If True, returns the figure object instead of showing it directly. Default is False.

False
show bool

If True, displays the plot. If False, suppresses display. Default is True.

True
ax Axes

Axes object where the histogram will be plotted. Default is None.

None
**sns_kwargs str | float | bool

Additional keyword arguments for seaborn.histplot.

{}

Returns:

Type Description
Figure | Axes | None

Optional[matplotlib.figure.Figure or matplotlib.axes.Axes]: If return_fig is True, returns the generated figure object. If return_fig is False and ax is provided, returns the Axes object. Otherwise, returns None.

Raises:

Type Description
ValueError

If slice_axis is not a valid axis index (0, 1, or 2).

ValueError

If slice_idx is an integer and is out of range for the specified axis.

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_idx=100, slice_axis=1, bin_style='bars', edgecolor='white')
viz histogram

Using coarsness 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_idx: int | str | None = None,
    slice_axis: int = 0,
    vertical_line: int = None,
    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:
    """
    Plots a histogram of voxel intensities from a 3D volume, with options to show a specific slice or the entire volume.

    Utilizes [seaborn.histplot](https://seaborn.pydata.org/generated/seaborn.histplot.html) for visualization.

    Args:
        volume (np.ndarray): A 3D NumPy array representing the volume to be visualized.
        coarseness (int or list[int], optional): A positive integer representing the coarseness of the subsampling. A value of 1 (default) uses the original volume, a value of 2 uses every second element along each axis and so on. Used to reduce the needed computation.
        ignore_zero (bool, optional): Specifies if zero-values in the volume should be ignored.
        bins (Union[int, str], optional): Number of histogram bins or a binning strategy (e.g., "auto"). Default is "auto".
        slice_axis (int, optional): Axis along which to take a slice. Default is 0.
        slice_idx (Union[int, str], optional): Specifies the slice to visualize. If an integer, it represents the slice index along the selected axis.
                                               If "middle", the function uses the middle slice. If None, the entire volume is visualized. Default is None.
        vertical_line (int, optional): Intensity value for a vertical line to be drawn on the histogram. Default is None.
        kde (bool, optional): Whether to overlay a kernel density estimate.
        log_scale (bool, optional): Whether to use a logarithmic scale on the y-axis. Default is False.
        despine (bool, optional): If True, removes the top and right spines from the plot for cleaner appearance. Default is True.
        show_title (bool, optional): If True, displays a title with slice information. Default is True.
        color (str, optional): Color for the histogram bars. If "qim3d", defaults to the qim3d color. Default is "qim3d".
        edgecolor (str, optional): Color for the edges of the histogram bars. Default is None.
        figsize (tuple, optional): Size of the figure (width, height). Default is (8, 4.5).
        bin_style (str, optional): Type of histogram to draw ('bars', 'step', or 'poly'). Default is "step".
        return_fig (bool, optional): If True, returns the figure object instead of showing it directly. Default is False.
        show (bool, optional): If True, displays the plot. If False, suppresses display. Default is True.
        ax (matplotlib.axes.Axes, optional): Axes object where the histogram will be plotted. Default is None.
        **sns_kwargs: Additional keyword arguments for `seaborn.histplot`.

    Returns:
        Optional[matplotlib.figure.Figure or matplotlib.axes.Axes]:
            If `return_fig` is True, returns the generated figure object.
            If `return_fig` is False and `ax` is provided, returns the `Axes` object.
            Otherwise, returns None.

    Raises:
        ValueError: If `slice_axis` is not a valid axis index (0, 1, or 2).
        ValueError: If `slice_idx` is an integer and is out of range for the specified axis.

    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_idx=100, slice_axis=1, bin_style='bars', edgecolor='white')
        ```
        ![viz histogram](../../assets/screenshots/viz-histogram-slice.png)

    Example: Using coarsness 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_idx == 'middle':
        slice_idx = volume.shape[slice_axis] // 2

    if slice_idx is not None:
        if 0 <= slice_idx < volume.shape[slice_axis]:
            img_slice = np.take(volume, indices=slice_idx, axis=slice_axis)
            data = img_slice.ravel()
            title = f'Intensity histogram of slice #{slice_idx} {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:
        ax.axvline(
            x=vertical_line,
            color='red',
            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,
)

Returns an interactive widget for comparing two volumes along slices in a fading overlay image.

Parameters:

Name Type Description Default
volume1 ndarray

The first volume.

required
volume2 ndarray

The second volume.

required
volume1_values tuple[float, float]

Set the color limits of volume1.

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

Set the color limits of volume2.

(None, None)
colormaps ColormapLike or tuple[ColormapLike, ColormapLike]

Specifies the colormaps used for each volume. A single value will be applied to both volumes.

'gray'
display_size int

Size in pixels of the image. If image is non-square, then the largest dimension will have display_size pixels.

512

Returns:

Name Type Description
widget VBox

The interactive widget.

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:
    """
    Returns an interactive widget for comparing two volumes along slices in a fading overlay image.

    Args:
        volume1 (np.ndarray): The first volume.
        volume2 (np.ndarray): The second volume.
        volume1_values (tuple[float, float], optional): Set the color limits of volume1.
        volume2_values (tuple[float, float], optional): Set the color limits of volume2.
        colormaps (ColormapLike or tuple[ColormapLike, ColormapLike], optional): Specifies the colormaps used for each volume. A single value will be applied to both volumes.
        display_size (int, optional): Size in pixels of the image. If image is non-square, then the largest dimension will have display_size pixels.

    Returns:
        widget (widgets.widget_box.VBox): The interactive widget.


    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,
)

Returns an interactive widget for comparing two volumes along slices.

Parameters:

Name Type Description Default
volume1 ndarray

The first volume.

required
volume2 ndarray

The second volume.

required
slice_axis int

Specifies the initial axis along which to slice.

0
slice_index int

Specifies the initial index along slice_axis.

None
volumetric_visualization bool

Defines if k3d plots should also be shown.

False

Returns:

Name Type Description
widget VBox

The interactive widget.

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:
    """
    Returns an interactive widget for comparing two volumes along slices.

    Args:
        volume1 (np.ndarray): The first volume.
        volume2 (np.ndarray): The second volume.
        slice_axis (int, optional): Specifies the initial axis along which to slice.
        slice_index (int, optional): Specifies the initial index along slice_axis.
        volumetric_visualization (bool, optional): Defines if k3d plots should also be shown.

    Returns:
        widget (widgets.widget_box.VBox): The interactive widget.



    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)

Launch an interactive chunk explorer for a 3D or 5D OME-Zarr dataset.

Parameters:

Name Type Description Default
zarr_path str

Path to the OME-Zarr dataset.

required
**kwargs Any

Additional keyword arguments that are selectively forwarded only to the visualization method that supports them. Any key not accepted by the chosen method is ignored.

The visualization methods available in this tool are:

  • slicer → calls qim3d.viz.slicer
  • slices → calls qim3d.viz.slices_grid
  • volume → calls qim3d.viz.volumetric

Users select the desired method via the dropdown menu in the widget.

{}

Raises:

Type Description
ValueError

If the dataset's dimensionality is not 3 or 5.

Returns:

Name Type Description
chunk_explorer VBox

A widget containing dropdowns for selecting the OME-Zarr scale, chunk coordinates along each axis, and visualization method.

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:
    """
    Launch an interactive chunk explorer for a 3D or 5D OME-Zarr dataset.

    Args:
        zarr_path (str):
            Path to the OME-Zarr dataset.

        **kwargs (Any):
            Additional keyword arguments that are **selectively** forwarded
            only to the visualization method that supports them. Any key
            not accepted by the chosen method is ignored.

            The visualization methods available in this tool are:

            - `slicer` → calls `qim3d.viz.slicer`
            - `slices` → calls `qim3d.viz.slices_grid`
            - `volume` → calls `qim3d.viz.volumetric`

            Users select the desired method via the dropdown menu in the widget.

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

    Returns:
        chunk_explorer (widgets.VBox): A widget containing dropdowns for selecting the OME-Zarr scale, chunk coordinates along each axis, and visualization method.

    Example:
        ```python
        import qim3d

        # Visualize interactive chunks explorer
        qim3d.viz.chunks('path/to/zarr/dataset.zarr')
        ```
        ![interactive chunks explorer](../../assets/screenshots/chunks_explorer.gif)

    """
    # Load the Zarr dataset
    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(
        scale: int,
        *coords: int,
        visualization_method: Literal['slicer', 'slices', 'volume'],
        **inner_kwargs: object,
    ) -> Widget | Figure | Output:
        arr = da.from_zarr(zarr_data[scale])
        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

    scale_opts = {f'{i} {zarr_data[i].shape}': i for i in range(len(zarr_data))}
    drop_style = {'description_width': '120px'}
    scale_dd = widgets.Dropdown(
        options=scale_opts, value=0, description='Scale:', style=drop_style
    )

    first_shape = zarr_data[0].shape
    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, zarr_data[0].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(scale: int) -> None:
        disable_observers()
        shp = zarr_data[scale].shape
        cnts = get_num_chunks(shp, zarr_data[scale].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,
    color_map='magma',
    value_min=None,
    value_max=None,
)

Interactive widget for visualizing the effect of edge fading on a 3D volume.

This can be used to select the best parameters before applying the mask.

Parameters:

Name Type Description Default
volume ndarray

The volume to apply edge fading to.

required
axis int

The axis along which to apply the fading. Defaults to 0.

0
color_map str

Specifies the color map for the image. Defaults to "viridis".

'magma'
value_min float or None

Together with value_max define the data range the colormap covers. By default colormap covers the full range. Defaults to None.

None
value_max float or None

Together with value_min define the data range the colormap covers. By default colormap covers the full range. Defaults to None

None

Returns:

Name Type Description
slicer_obj HBox

The interactive widget for visualizing fade mask on slices of a 3D volume.

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,
    color_map: str = 'magma',
    value_min: float = None,
    value_max: float = None,
) -> widgets.interactive:
    """
    Interactive widget for visualizing the effect of edge fading on a 3D volume.

    This can be used to select the best parameters before applying the mask.

    Args:
        volume (np.ndarray): The volume to apply edge fading to.
        axis (int, optional): The axis along which to apply the fading. Defaults to 0.
        color_map (str, optional): Specifies the color map for the image. Defaults to "viridis".
        value_min (float or None, optional): Together with value_max define the data range the colormap covers. By default colormap covers the full range. Defaults to None.
        value_max (float or None, optional): Together with value_min define the data range the colormap covers. By default colormap covers the full range. Defaults to None

    Returns:
        slicer_obj (widgets.HBox): The interactive widget for visualizing fade mask on slices of a 3D volume.

    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 value_min 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_value_min = (
            None
            if (isinstance(value_min, float | int) and value_min > np.max(slice_img))
            else value_min
        )
        new_value_max = (
            None
            if (isinstance(value_max, float | int) and value_max < np.min(slice_img))
            else value_max
        )

        axes[0].imshow(
            slice_img, cmap=color_map, vmin=new_value_min, vmax=new_value_max
        )
        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=color_map)
        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 value_min 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_value_min = (
            None
            if (isinstance(value_min, float | int) and value_min > np.max(slice_img))
            else value_min
        )
        new_value_max = (
            None
            if (isinstance(value_max, float | int) and value_max < np.min(slice_img))
            else value_max
        )
        axes[2].imshow(
            slice_img, cmap=color_map, vmin=new_value_min, vmax=new_value_max
        )
        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