Skip to content

Operations on volumetric data

The qim3d library provides a set of methods for different operations on volumes.

qim3d.operations

Operations on volumes.

qim3d.operations.remove_background

remove_background(vol, median_filter_size=2, min_object_radius=3, background='dark', **median_kwargs)

Remove background from a volume using a qim3d filters.

Parameters:

Name Type Description Default
vol ndarray

The volume to remove background from.

required
median_filter_size int

The size of the median filter. Defaults to 2.

2
min_object_radius int

The radius of the structuring element for the tophat filter. Defaults to 3.

3
background 'dark' or 'bright

The background type. Can be 'dark' or 'bright'. Defaults to 'dark'.

'dark'
**median_kwargs Any

Additional keyword arguments for the Median filter.

{}

Returns:

Name Type Description
filtered_vol ndarray

The volume with background removed.

Example

import qim3d

vol = qim3d.examples.cement_128x128x128
fig1 = qim3d.viz.slices_grid(vol, value_min=0, value_max=255, num_slices=5, display_figure=True)
operations-remove_background_before

vol_filtered  = qim3d.operations.remove_background(vol,
                                                      min_object_radius=3,
                                                      background="bright")
fig2 = qim3d.viz.slices_grid(vol_filtered, value_min=0, value_max=255, num_slices=5, display_figure=True)
operations-remove_background_after

Source code in qim3d/operations/_common_operations_methods.py
def remove_background(
    vol: np.ndarray,
    median_filter_size: int = 2,
    min_object_radius: int = 3,
    background: str = 'dark',
    **median_kwargs,
) -> np.ndarray:
    """
    Remove background from a volume using a qim3d filters.

    Args:
        vol (np.ndarray): The volume to remove background from.
        median_filter_size (int, optional): The size of the median filter. Defaults to 2.
        min_object_radius (int, optional): The radius of the structuring element for the tophat filter. Defaults to 3.
        background ('dark' or 'bright, optional): The background type. Can be 'dark' or 'bright'. Defaults to 'dark'.
        **median_kwargs (Any): Additional keyword arguments for the Median filter.

    Returns:
        filtered_vol (np.ndarray): The volume with background removed.


    Example:
        ```python
        import qim3d

        vol = qim3d.examples.cement_128x128x128
        fig1 = qim3d.viz.slices_grid(vol, value_min=0, value_max=255, num_slices=5, display_figure=True)
        ```
        ![operations-remove_background_before](../../assets/screenshots/operations-remove_background_before.png)

        ```python
        vol_filtered  = qim3d.operations.remove_background(vol,
                                                              min_object_radius=3,
                                                              background="bright")
        fig2 = qim3d.viz.slices_grid(vol_filtered, value_min=0, value_max=255, num_slices=5, display_figure=True)
        ```
        ![operations-remove_background_after](../../assets/screenshots/operations-remove_background_after.png)

    """

    # Create a pipeline with a median filter and a tophat filter
    pipeline = filters.Pipeline(
        filters.Median(size=median_filter_size, **median_kwargs),
        filters.Tophat(radius=min_object_radius, background=background),
    )

    # Apply the pipeline to the volume
    return pipeline(vol)

qim3d.operations.fade_mask

fade_mask(vol, decay_rate=10, ratio=0.5, geometry='spherical', invert=False, axis=0, **kwargs)

Apply edge fading to a volume.

Parameters:

Name Type Description Default
vol ndarray

The volume to apply edge fading to.

required
decay_rate float

The decay rate of the fading. Defaults to 10.

10
ratio float

The ratio of the volume to fade. Defaults to 0.5.

0.5
geometry spherical or cylindrical

The geometric shape of the fading. Can be 'spherical' or 'cylindrical'. Defaults to 'spherical'.

'spherical'
invert bool

Flag for inverting the fading. Defaults to False.

False
axis int

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

0
**kwargs Any

Additional keyword arguments for the edge fading.

{}

Returns:

Name Type Description
faded_vol ndarray

The volume with edge fading applied.

Example

import qim3d
vol = qim3d.examples.fly_150x256x256
qim3d.viz.volumetric(vol)
Image before edge fading has visible artifacts, which obscures the object of interest.

vol_faded = qim3d.operations.fade_mask(vol, geometric='cylindrical', decay_rate=5, ratio=0.65, axis=1)
qim3d.viz.volumetric(vol_faded)
Afterwards the artifacts are faded out, making the object of interest more visible for visualization purposes.

Source code in qim3d/operations/_common_operations_methods.py
def fade_mask(
    vol: np.ndarray,
    decay_rate: float = 10,
    ratio: float = 0.5,
    geometry: str = 'spherical',
    invert: bool = False,
    axis: int = 0,
    **kwargs,
) -> np.ndarray:
    """
    Apply edge fading to a volume.

    Args:
        vol (np.ndarray): The volume to apply edge fading to.
        decay_rate (float, optional): The decay rate of the fading. Defaults to 10.
        ratio (float, optional): The ratio of the volume to fade. Defaults to 0.5.
        geometry ('spherical' or 'cylindrical', optional): The geometric shape of the fading. Can be 'spherical' or 'cylindrical'. Defaults to 'spherical'.
        invert (bool, optional): Flag for inverting the fading. Defaults to False.
        axis (int, optional): The axis along which to apply the fading. Defaults to 0.
        **kwargs (Any): Additional keyword arguments for the edge fading.

    Returns:
        faded_vol (np.ndarray): The volume with edge fading applied.

    Example:
        ```python
        import qim3d
        vol = qim3d.examples.fly_150x256x256
        qim3d.viz.volumetric(vol)
        ```
        Image before edge fading has visible artifacts, which obscures the object of interest.
        <iframe src="https://platform.qim.dk/k3d/fly.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        vol_faded = qim3d.operations.fade_mask(vol, geometric='cylindrical', decay_rate=5, ratio=0.65, axis=1)
        qim3d.viz.volumetric(vol_faded)
        ```
        Afterwards the artifacts are faded out, making the object of interest more visible for visualization purposes.
        <iframe src="https://platform.qim.dk/k3d/fly_faded.html" width="100%" height="500" frameborder="0"></iframe>

    """
    if axis < 0 or axis >= vol.ndim:
        error = 'Axis must be between 0 and the number of dimensions of the volume'
        raise ValueError(error)

    # Generate the coordinates of each point in the array
    shape = vol.shape
    z, y, x = np.indices(shape)

    # Store the original maximum value of the volume
    original_max_value = np.max(vol)

    # Calculate the center of the array
    center = np.array([(s - 1) / 2 for s in shape])

    # Calculate the distance of each point from the center
    if geometry == 'spherical':
        distance = np.linalg.norm([z - center[0], y - center[1], x - center[2]], axis=0)
    elif geometry == 'cylindrical':
        distance_list = np.array([z - center[0], y - center[1], x - center[2]])
        # remove the axis along which the fading is not applied
        distance_list = np.delete(distance_list, axis, axis=0)
        distance = np.linalg.norm(distance_list, axis=0)
    else:
        error = "Geometry must be 'spherical' or 'cylindrical'"
        raise ValueError(error)

    # Compute the maximum distance from the center
    max_distance = np.linalg.norm(center)

    # Compute ratio to make synthetic blobs exactly cylindrical
    # target_max_normalized_distance = 1.4 works well to make the blobs cylindrical
    if 'target_max_normalized_distance' in kwargs:
        target_max_normalized_distance = kwargs['target_max_normalized_distance']
        ratio = np.max(distance) / (target_max_normalized_distance * max_distance)

    # Normalize the distances so that they go from 0 at the center to 1 at the farthest point
    normalized_distance = distance / (max_distance * ratio)

    # Apply the decay rate
    faded_distance = normalized_distance**decay_rate

    # Invert the distances to have 1 at the center and 0 at the edges
    fade_array = 1 - faded_distance
    fade_array[fade_array <= 0] = 0

    if invert:
        fade_array = -(fade_array - 1)

    # Apply the fading to the volume
    vol_faded = vol * fade_array

    # Normalize the volume to retain the original maximum value
    vol_normalized = vol_faded * (original_max_value / np.max(vol_faded))

    return vol_normalized

qim3d.operations.overlay_rgb_images

overlay_rgb_images(background, foreground, alpha=0.5, hide_black=True)

Overlay an RGB foreground onto an RGB background using alpha blending.

Parameters:

Name Type Description Default
background ndarray

The background RGB image.

required
foreground ndarray

The foreground RGB image (usually masks).

required
alpha float

The alpha value for blending. Defaults to 0.5.

0.5
hide_black bool

If True, black pixels will have alpha value 0, so the black won't be visible. Used for segmentation where we don't care about background. Defaults to True.

True

Returns:

Name Type Description
composite ndarray

The composite RGB image with overlaid foreground.

Raises:

Type Description
ValueError

If input images have different shapes.

Note
  • The function performs alpha blending to overlay the foreground onto the background.
  • It ensures that the background and foreground have the same first two dimensions (image size matches).
  • It can handle greyscale images, values from 0 to 1, raw values which are negative or bigger than 255.
  • It calculates the maximum projection of the foreground and blends them onto the background.
Source code in qim3d/operations/_common_operations_methods.py
def overlay_rgb_images(
    background: np.ndarray,
    foreground: np.ndarray,
    alpha: float = 0.5,
    hide_black: bool = True,
) -> np.ndarray:
    """
    Overlay an RGB foreground onto an RGB background using alpha blending.

    Args:
        background (numpy.ndarray): The background RGB image.
        foreground (numpy.ndarray): The foreground RGB image (usually masks).
        alpha (float, optional): The alpha value for blending. Defaults to 0.5.
        hide_black (bool, optional): If True, black pixels will have alpha value 0, so the black won't be visible. Used for segmentation where we don't care about background. Defaults to True.

    Returns:
        composite (numpy.ndarray): The composite RGB image with overlaid foreground.

    Raises:
        ValueError: If input images have different shapes.

    Note:
        - The function performs alpha blending to overlay the foreground onto the background.
        - It ensures that the background and foreground have the same first two dimensions (image size matches).
        - It can handle greyscale images, values from 0 to 1, raw values which are negative or bigger than 255.
        - It calculates the maximum projection of the foreground and blends them onto the background.

    """

    def to_uint8(image: np.ndarray) -> np.ndarray:
        if np.min(image) < 0:
            image = image - np.min(image)

        maxim = np.max(image)
        if maxim > 255:
            image = (image / maxim) * 255
        elif maxim <= 1:
            image = image * 255

        if image.ndim == 2:
            image = np.repeat(image[..., None], 3, -1)
        elif image.ndim == 3:
            image = image[..., :3]  # Ignoring alpha channel
        else:
            error = f'Input image can not have higher dimension than 3. Yours have {image.ndim}'
            raise ValueError(error)

        return image.astype(np.uint8)

    background = to_uint8(background)
    foreground = to_uint8(foreground)

    # Ensure both images have the same shape
    if background.shape != foreground.shape:
        error = f'Input images must have the same first two dimensions. But background is of shape {background.shape} and foreground is of shape {foreground.shape}'
        raise ValueError(error)

    # Perform alpha blending
    foreground_max_projection = np.amax(foreground, axis=2)
    foreground_max_projection = np.stack((foreground_max_projection,) * 3, axis=-1)

    # Normalize if we have something
    if np.max(foreground_max_projection) > 0:
        foreground_max_projection = foreground_max_projection / np.max(
            foreground_max_projection
        )
    # Check alpha validity
    if alpha < 0:
        error = f'Alpha has to be positive number. You used {alpha}'
        raise ValueError(error)
    elif alpha > 1:
        alpha = 1

    # If the pixel is black, its alpha value is set to 0, so it has no effect on the image
    if hide_black:
        alpha = np.full((background.shape[0], background.shape[1], 1), alpha)
        alpha[
            np.apply_along_axis(
                lambda x: (x == [0, 0, 0]).all(), axis=2, arr=foreground
            )
        ] = 0

    composite = background * (1 - alpha) + foreground * alpha
    composite = np.clip(composite, 0, 255).astype('uint8')

    return composite.astype('uint8')

qim3d.operations.make_hollow

make_hollow(vol, thickness)

Make a volume hollow by applying a mask created by a minimum filter and an xor-gate.

Parameters:

Name Type Description Default
vol ndarray

The volume to hollow.

required
thickness int

The thickness of the shell after hollowing.

required

Returns:

Name Type Description
vol_hollowed ndarray

The hollowed volume.

Example

import qim3d

# Generate volume and visualize it
vol = qim3d.generate.volume(noise_scale = 0.01)
qim3d.viz.slicer(vol)
synthetic_collection
# Hollow volume and visualize it
vol_hollowed = qim3d.operations.make_hollow(vol, thickness=10)
qim3d.viz.slicer(vol_hollowed)
synthetic_collection

Source code in qim3d/operations/_common_operations_methods.py
def make_hollow(
    vol: np.ndarray,
    thickness: int,
) -> np.ndarray:
    """
    Make a volume hollow by applying a mask created by a minimum filter and an xor-gate.

    Args:
        vol (np.ndarray): The volume to hollow.
        thickness (int): The thickness of the shell after hollowing.

    Returns:
        vol_hollowed (np.ndarray): The hollowed volume.

    Example:
        ```python
        import qim3d

        # Generate volume and visualize it
        vol = qim3d.generate.volume(noise_scale = 0.01)
        qim3d.viz.slicer(vol)
        ```
        ![synthetic_collection](../../assets/screenshots/hollow_slicer_1.gif)
        ```python
        # Hollow volume and visualize it
        vol_hollowed = qim3d.operations.make_hollow(vol, thickness=10)
        qim3d.viz.slicer(vol_hollowed)
        ```
        ![synthetic_collection](../../assets/screenshots/hollow_slicer_2.gif)

    """
    # Create base mask
    vol_mask_base = vol > 0

    # apply minimum filter to the mask
    vol_eroded = filters.minimum(vol_mask_base, size=thickness)
    # Apply xor to only keep the voxels eroded by the minimum filter
    vol_mask = np.logical_xor(vol_mask_base, vol_eroded)

    # Apply the mask to the original volume to remove 'inner' voxels
    vol_hollowed = vol * vol_mask

    return vol_hollowed

qim3d.operations.pad

pad(volume, x_axis=0, y_axis=0, z_axis=0)

Pads the input 3D volume.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (shape: n, h, w).

required
x_axis float

Amount of pixels to pad the x-dimension. Must be an integer or a half-integer (e.g., 5, 5.5). The padding is symmetric and applied to both sides of the volume. Defaults to 0.

0
y_axis float

Amount of pixels to pad the y-dimension. Must be an integer or a half-integer. Defaults to 0.

0
z_axis float

Amount of pixels to pad the z-dimension. Must be an integer or a half-integer. Defaults to 0.

0

Returns:

Type Description
ndarray

numpy.ndarray: The padded volume.

Raises:

Type Description
AssertionError

If the input volume is not 3D.

AssertionError

If any padding value is negative.

Example

import qim3d
import numpy as np

vol = np.zeros((100, 100, 100))
print(vol.shape)
(100, 100, 100)
# Pad x-axis with 10 pixels on each side and y-axis with 20% of the original volume size
padded_volume = qim3d.operations.pad(vol, x_axis=10, y_axis=vol.shape[1] * 0.1)
print(padded_volume.shape)
(100, 120, 120)

Source code in qim3d/operations/_volume_operations.py
def pad(
    volume: np.ndarray, x_axis: float = 0, y_axis: float = 0, z_axis: float = 0
) -> np.ndarray:
    """
    Pads the input 3D volume.

    Args:
        volume (numpy.ndarray): The input 3D volume (shape: n, h, w).
        x_axis (float): Amount of pixels to pad the x-dimension. Must be an integer or a half-integer (e.g., 5, 5.5). The padding is symmetric and applied to both sides of the volume. Defaults to 0.
        y_axis (float): Amount of pixels to pad the y-dimension. Must be an integer or a half-integer. Defaults to 0.
        z_axis (float): Amount of pixels to pad the z-dimension. Must be an integer or a half-integer. Defaults to 0.

    Returns:
        numpy.ndarray: The padded volume.

    Raises:
        AssertionError: If the input volume is not 3D.
        AssertionError: If any padding value is negative.

    Example:
        ```python
        import qim3d
        import numpy as np

        vol = np.zeros((100, 100, 100))
        print(vol.shape)
        ```
        (100, 100, 100)
        ```python
        # Pad x-axis with 10 pixels on each side and y-axis with 20% of the original volume size
        padded_volume = qim3d.operations.pad(vol, x_axis=10, y_axis=vol.shape[1] * 0.1)
        print(padded_volume.shape)
        ```
        (100, 120, 120)

    """
    assert len(volume.shape) == 3, 'Volume must be 3D'
    assert z_axis >= 0, 'Padded shape must be positive in z-axis.'
    assert y_axis >= 0, 'Padded shape must be positive in y-axis.'
    assert x_axis >= 0, 'Padded shape must be positive in x-axis.'

    n, h, w = volume.shape

    # Round to nearest half integer
    x_axis = round(x_axis * 2) / 2
    y_axis = round(y_axis * 2) / 2
    z_axis = round(z_axis * 2) / 2

    # Add to both sides and determine new sizes
    new_w = w + int(2 * x_axis)
    new_h = h + int(2 * y_axis)
    new_n = n + int(2 * z_axis)

    # Create a new volume with padding and center the original in the padded volume
    padded_volume = np.zeros((new_n, new_h, new_w))
    padded_volume[
        int(z_axis) : int(z_axis) + n,
        int(y_axis) : int(y_axis) + h,
        int(x_axis) : int(x_axis) + w,
    ] = volume

    return padded_volume

qim3d.operations.pad_to

pad_to(volume, shape)

Pads the input 3D volume to a certain shape.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (shape: n, h, w).

required
shape tuple[int, int, int]

The shape to pad the volume to.

required

Returns:

Name Type Description
padded_volume ndarray

The padded volume.

Raises:

Type Description
AssertionError

If the input shape is not 3D.

AssertionError

If the input volume is not 3D.

AssertionError

If the shape tuple is not integers.

AssertionError

If the padded shape is not larger than the original shape.

Example

import qim3d
import numpy as np

# Create volume of shape (100,100,100) and add values in a box inside
vol = np.zeros((100,100,100))
print(vol.shape)
(100, 100, 100)
# Pad the volume to shape (110, 110, 110)
padded_volume = qim3d.operations.pad_to(vol, (110,110,110))
print(padded_volume.shape)
(110, 110, 110)

Source code in qim3d/operations/_volume_operations.py
def pad_to(volume: np.ndarray, shape: tuple[int, int, int]) -> np.ndarray:
    """
    Pads the input 3D volume to a certain shape.

    Args:
        volume (numpy.ndarray): The input 3D volume (shape: n, h, w).
        shape (tuple[int, int, int]): The shape to pad the volume to.

    Returns:
        padded_volume (numpy.ndarray): The padded volume.

    Raises:
        AssertionError: If the input shape is not 3D.
        AssertionError: If the input volume is not 3D.
        AssertionError: If the shape tuple is not integers.
        AssertionError: If the padded shape is not larger than the original shape.

    Example:
        ```python
        import qim3d
        import numpy as np

        # Create volume of shape (100,100,100) and add values in a box inside
        vol = np.zeros((100,100,100))
        print(vol.shape)
        ```
        (100, 100, 100)
        ```python
        # Pad the volume to shape (110, 110, 110)
        padded_volume = qim3d.operations.pad_to(vol, (110,110,110))
        print(padded_volume.shape)
        ```
        (110, 110, 110)

    """
    assert len(shape) == 3, 'Shape must be 3D'
    assert len(volume.shape) == 3, 'Volume must be 3D'
    assert all(isinstance(x, int) for x in shape), 'Shape tuple must contain integers'

    shape_np = np.array(shape)
    for i in range(len(shape_np)):
        if shape_np[i] < volume.shape[i]:
            print(
                'Pad shape is smaller than the volume shape. Changing it to original shape volume.'
            )
            shape_np[i] = volume.shape[i]

    new_z = (shape_np[0] - volume.shape[0]) / 2
    new_y = (shape_np[1] - volume.shape[1]) / 2
    new_x = (shape_np[2] - volume.shape[2]) / 2

    return pad(volume, x_axis=new_x, y_axis=new_y, z_axis=new_z)

qim3d.operations.trim

trim(volume)

Removes all empty slices (i.e., slices that contain all zeros) along the x, y, and z axes.

Parameters:

Name Type Description Default
volume ndarray

The 3D input volume (shape: n, h, w).

required

Returns:

Name Type Description
trimmed_volume ndarray

The transformed volume with empty slices removed along all axes.

Raises:

Type Description
AssertionError

If the input shape is not 3D.

Example

import qim3d
import numpy as np

# Create volume of shape (100,100,100) and add values in a box inside
vol = np.zeros((100,100,100))
vol[10:90, 10:90, 10:90] = 1
print(vol.shape)
(100, 100, 100)
# Trim the slices without voxel values on all axes
trimmed_volume = qim3d.operations.trim(vol)
print(trimmed_volume.shape)
(80, 80, 80)

Source code in qim3d/operations/_volume_operations.py
def trim(volume: np.ndarray) -> np.ndarray:
    """
    Removes all empty slices (i.e., slices that contain all zeros) along the x, y, and z axes.

    Args:
        volume (numpy.ndarray): The 3D input volume (shape: n, h, w).

    Returns:
        trimmed_volume (numpy.ndarray): The transformed volume with empty slices removed along all axes.

    Raises:
        AssertionError: If the input shape is not 3D.

    Example:
        ```python
        import qim3d
        import numpy as np

        # Create volume of shape (100,100,100) and add values in a box inside
        vol = np.zeros((100,100,100))
        vol[10:90, 10:90, 10:90] = 1
        print(vol.shape)
        ```
        (100, 100, 100)
        ```python
        # Trim the slices without voxel values on all axes
        trimmed_volume = qim3d.operations.trim(vol)
        print(trimmed_volume.shape)
        ```
        (80, 80, 80)

    """
    assert len(volume.shape) == 3, 'Volume must be three-dimensional.'

    # Remove empty slices along the x-axis (columns)
    non_empty_x = np.any(volume, axis=(1, 2))  # Check non-empty slices in the y-z plane
    volume = volume[non_empty_x, :, :]  # Keep only non-empty slices along x

    # Remove empty slices along the y-axis (rows)
    non_empty_y = np.any(volume, axis=(0, 2))  # Check non-empty slices in the x-z plane
    volume = volume[:, non_empty_y, :]  # Keep only non-empty slices along y

    # Remove empty slices along the z-axis (depth)
    non_empty_z = np.any(volume, axis=(0, 1))  # Check non-empty slices in the x-y plane
    volume = volume[:, :, non_empty_z]  # Keep only non-empty slices along z

    trimmed_volume = volume

    return trimmed_volume

qim3d.operations.shear3d

shear3d(volume, x_shift_y=0, x_shift_z=0, y_shift_x=0, y_shift_z=0, z_shift_x=0, z_shift_y=0, order=1)

Applies a shear transformation to a 3D volume using pixel-based shifts.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (shape: n, h, w).

required
x_shift_y int

Maximum pixel shift in the x-direction, applied progressively along the y-axis.

0
x_shift_z int

Maximum pixel shift in the x-direction, applied progressively along the z-axis.

0
y_shift_x int

Maximum pixel shift in the y-direction, applied progressively along the x-axis.

0
y_shift_z int

Maximum pixel shift in the y-direction, applied progressively along the z-axis.

0
z_shift_x int

Maximum pixel shift in the z-direction, applied progressively along the x-axis.

0
z_shift_y int

Maximum pixel shift in the z-direction, applied progressively along the y-axis.

0
order int

Order of interpolation. Order=0 (nearest-neighbor) keeps voxel values unchanged. Defaults to 1.

1

Returns:

Name Type Description
sheared_volume ndarray

The transformed volume.

Raises:

Type Description
AssertionError

If the input shape is not 3D.

AssertionError

If the order is not integer and in the range of 0-5.

AssertionError

If the shift values are not integer.

Example

import qim3d
import numpy as np

# Generate box for shearing
vol = np.zeros((60,100,100))
vol[:, 20:80, 20:80] = 1

qim3d.viz.slicer(vol, slice_axis=1)
warp_box
# Shear the volume by 20% factor in x-direction along z-axis
factor = 0.2
shift = int(vol.shape[0]*factor)
sheared_vol = qim3d.operations.shear3d(vol, x_shift_z=shift, order=1)

qim3d.viz.slicer(sheared_vol, slice_axis=1)
warp_box_shear

Source code in qim3d/operations/_volume_operations.py
def shear3d(
    volume: np.ndarray,
    x_shift_y: int = 0,
    x_shift_z: int = 0,
    y_shift_x: int = 0,
    y_shift_z: int = 0,
    z_shift_x: int = 0,
    z_shift_y: int = 0,
    order: int = 1,
) -> np.ndarray:
    """
    Applies a shear transformation to a 3D volume using pixel-based shifts.

    Args:
        volume (numpy.ndarray): The input 3D volume (shape: n, h, w).
        x_shift_y (int): Maximum pixel shift in the x-direction, applied progressively along the y-axis.
        x_shift_z (int): Maximum pixel shift in the x-direction, applied progressively along the z-axis.
        y_shift_x (int): Maximum pixel shift in the y-direction, applied progressively along the x-axis.
        y_shift_z (int): Maximum pixel shift in the y-direction, applied progressively along the z-axis.
        z_shift_x (int): Maximum pixel shift in the z-direction, applied progressively along the x-axis.
        z_shift_y (int): Maximum pixel shift in the z-direction, applied progressively along the y-axis.
        order (int): Order of interpolation. Order=0 (nearest-neighbor) keeps voxel values unchanged. Defaults to 1.

    Returns:
        sheared_volume (numpy.ndarray): The transformed volume.

    Raises:
        AssertionError: If the input shape is not 3D.
        AssertionError: If the order is not integer and in the range of 0-5.
        AssertionError: If the shift values are not integer.

    Example:
        ```python
        import qim3d
        import numpy as np

        # Generate box for shearing
        vol = np.zeros((60,100,100))
        vol[:, 20:80, 20:80] = 1

        qim3d.viz.slicer(vol, slice_axis=1)
        ```
        ![warp_box](../../assets/screenshots/warp_box_1.png)
        ```python
        # Shear the volume by 20% factor in x-direction along z-axis
        factor = 0.2
        shift = int(vol.shape[0]*factor)
        sheared_vol = qim3d.operations.shear3d(vol, x_shift_z=shift, order=1)

        qim3d.viz.slicer(sheared_vol, slice_axis=1)
        ```
        ![warp_box_shear](../../assets/screenshots/warp_box_shear.png)

    """
    assert len(volume.shape) == 3, 'Volume must be three-dimensional.'
    assert isinstance(order, int), 'Order must be an integer.'
    assert 0 <= order <= 5, 'Order must be in the range 0-5.'
    assert all(
        isinstance(var, int)
        for var in (x_shift_y, x_shift_z, y_shift_x, y_shift_z, z_shift_x, z_shift_y)
    ), 'All shift values must be integers.'

    n, h, w = volume.shape

    # Create coordinate grid
    z, y, x = np.mgrid[0:n, 0:h, 0:w]

    # Generate linearly increasing shift maps
    x_shear_y = np.linspace(-x_shift_y, x_shift_y, h)  # X shift varies along Y
    x_shear_z = np.linspace(-x_shift_z, x_shift_z, n)  # X shift varies along Z

    y_shear_x = np.linspace(-y_shift_x, y_shift_x, w)  # Y shift varies along X
    y_shear_z = np.linspace(-y_shift_z, y_shift_z, n)  # Y shift varies along Z

    z_shear_x = np.linspace(-z_shift_x, z_shift_x, w)  # Z shift varies along X
    z_shear_y = np.linspace(-z_shift_y, z_shift_y, h)  # Z shift varies along Y

    # Apply pixelwise shifts
    x_new = x + x_shear_y[y] + x_shear_z[z]
    y_new = y + y_shear_x[x] + y_shear_z[z]
    z_new = z + z_shear_x[x] + z_shear_y[y]

    # Stack the new coordinates
    coords = np.array([z_new, y_new, x_new])

    # Apply transformation
    sheared_volume = scipy.ndimage.map_coordinates(
        volume, coords, order=order, mode='nearest'
    )

    return sheared_volume

qim3d.operations.curve_warp

curve_warp(volume, x_amp=0, y_amp=0, x_periods=1.0, y_periods=1.0, x_offset=0.0, y_offset=0.0, order=1)

Applies an curve transformation along the z-axis using sine functions.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (shape: n, h, w).

required
x_amp float

Determines the amplitude (height) of the curve in the x-direction. Defaults to 0.

0
y_amp float

Determines the amplitude (height) of the curve in the y-direction. Defautls to 0.

0
x_periods float

Determines the amount of periods (amount of wave crests) along the x-direction. Defaults to 1.0.

1.0
y_periods float

Determines the amount of periods (amount of wave crests) along the y-direction. Defaults to 1.0.

1.0
x_offset float

Determines pixelwise curve offset in x-direction. Defaults to 0.0.

0.0
y_offset float

Determines pixelwise curve offset in y-direction. Defaults to 0.0.

0.0
order int

Order of spline interpolation. Order=0 (nearest-neighbor) will keep voxel values unchanged. Defaults to 1.

1

Returns:

Name Type Description
warped_volume ndarray

The transformed volume.

Raises:

Type Description
AssertionError

If the input shape is not 3D.

AssertionError

If the order is not integer and in the range of 0-5.

Example

import qim3d
import numpy as np

# Generate box for warping
vol = np.zeros((100,100,100))
vol[:,40:60, 40:60] = 1
qim3d.viz.slicer(vol, slice_axis=1)
warp_box_long
# Warp the box along the x dimension
warped_volume = qim3d.operations.curve_warp(vol, x_amp=10, x_periods=4)
qim3d.viz.slicer(warped_volume, slice_axis=1)
warp_box_curved

Source code in qim3d/operations/_volume_operations.py
def curve_warp(
    volume: np.ndarray,
    x_amp: float = 0,
    y_amp: float = 0,
    x_periods: float = 1.0,
    y_periods: float = 1.0,
    x_offset: float = 0.0,
    y_offset: float = 0.0,
    order: int = 1,
) -> np.ndarray:
    """
    Applies an curve transformation along the z-axis using sine functions.

    Args:
        volume (numpy.ndarray): The input 3D volume (shape: n, h, w).
        x_amp (float): Determines the amplitude (height) of the curve in the x-direction. Defaults to 0.
        y_amp (float): Determines the amplitude (height) of the curve in the y-direction. Defautls to 0.
        x_periods (float): Determines the amount of periods (amount of wave crests) along the x-direction. Defaults to 1.0.
        y_periods (float): Determines the amount of periods (amount of wave crests) along the y-direction. Defaults to 1.0.
        x_offset (float): Determines pixelwise curve offset in x-direction. Defaults to 0.0.
        y_offset (float): Determines pixelwise curve offset in y-direction. Defaults to 0.0.
        order (int): Order of spline interpolation. Order=0 (nearest-neighbor) will keep voxel values unchanged. Defaults to 1.

    Returns:
        warped_volume (numpy.ndarray): The transformed volume.

    Raises:
        AssertionError: If the input shape is not 3D.
        AssertionError: If the order is not integer and in the range of 0-5.

    Example:
        ```python
        import qim3d
        import numpy as np

        # Generate box for warping
        vol = np.zeros((100,100,100))
        vol[:,40:60, 40:60] = 1
        qim3d.viz.slicer(vol, slice_axis=1)
        ```
        ![warp_box_long](../../assets/screenshots/warp_box_long.png)
        ```python
        # Warp the box along the x dimension
        warped_volume = qim3d.operations.curve_warp(vol, x_amp=10, x_periods=4)
        qim3d.viz.slicer(warped_volume, slice_axis=1)
        ```
        ![warp_box_curved](../../assets/screenshots/warp_box_curve.png)

    """
    assert len(volume.shape) == 3, 'Volume must be three-dimensional.'
    assert isinstance(order, int), 'Order must be an integer.'
    assert 0 <= order <= 5, 'Order must be in the range 0-5.'

    n, h, w = volume.shape

    # Create a coordinate grid for the expanded volume
    z, y, x = np.mgrid[0:n, 0:h, 0:w]

    # Normalize z for smooth oscillations
    z_norm = z / (n - 1)  # Ranges from 0 to 1

    # Compute sinusoidal shifts
    x_amp = x_amp * np.sin(2 * np.pi * x_periods * z_norm + x_offset)
    x_new = x + x_amp

    y_amp = y_amp * np.sin(2 * np.pi * y_periods * z_norm + y_offset)
    y_new = y + y_amp

    # Stack the new coordinates for interpolation and interpolate
    coords = np.array([z, y_new, x_new])
    warped_volume = scipy.ndimage.map_coordinates(
        volume, coords, order=order, mode='nearest'
    )

    return warped_volume

qim3d.operations.stretch

stretch(volume, x_stretch=0, y_stretch=0, z_stretch=0, order=1)

Stretches a volume by increasing the size of the volume in the input dimension with interpolation. The volume will therefore increase (or decrease if the stretch is negative) at the same rate as the volume, keeping its relative size.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (shape: n, h, w).

required
x_stretch int

Amount of pixels to stretch the x-dimension. The operation is symmetric, and will be effective on both sides of the volume. Defaults to 0.

0
y_stretch int

Amount of pixels to stretch the x-dimension. The operation is symmetric, and will be effective on both sides of the volume. Defaults to 0.

0
z_stretch int

Amount of pixels to stretch the x-dimension. The operation is symmetric, and will be effective on both sides of the volume. Defaults to 0.

0
order int

Order of spline interpolation. Order=0 (nearest-neighbor) will keep voxel values unchanged. Defaults to 1.

1

Returns:

Name Type Description
stretched_volume ndarray

The stretched volume.

Raises:

Type Description
AssertionError

If the input shape is not 3D.

AssertionError

If the order is not integer and in the range of 0-5.

AssertionError

If the stretching inputs are not integer.

Example

import qim3d
import numpy as np

# Generate box for stretching
vol = np.zeros((100,100,100))
vol[:,20:80, 20:80] = 1

qim3d.viz.slicer(vol)
warp_box

# Stretch the box along the x dimension
stretched_volume = qim3d.operations.stretch(vol, x_stretch=20)
print(stretched_volume.shape)
qim3d.viz.slicer(stretched_volume)
(100, 100, 140)

warp_box_stretch

# Squeeze the box along the y dimension
squeezed_volume = qim3d.operations.stretch(vol, x_stretch=-20)
print(squeezed_volume.shape)
qim3d.viz.slicer(squeezed_volume)
(100, 100, 60)

warp_box_squeeze

Source code in qim3d/operations/_volume_operations.py
def stretch(
    volume: np.ndarray,
    x_stretch: int = 0,
    y_stretch: int = 0,
    z_stretch: int = 0,
    order: int = 1,
) -> np.ndarray:
    """
    Stretches a volume by increasing the size of the volume in the input dimension with interpolation. The volume will therefore increase (or decrease if the stretch is negative) at the same rate as the volume, keeping its relative size.

    Args:
        volume (numpy.ndarray): The input 3D volume (shape: n, h, w).
        x_stretch (int): Amount of pixels to stretch the x-dimension. The operation is symmetric, and will be effective on both sides of the volume. Defaults to 0.
        y_stretch (int): Amount of pixels to stretch the x-dimension. The operation is symmetric, and will be effective on both sides of the volume. Defaults to 0.
        z_stretch (int): Amount of pixels to stretch the x-dimension. The operation is symmetric, and will be effective on both sides of the volume. Defaults to 0.
        order (int): Order of spline interpolation. Order=0 (nearest-neighbor) will keep voxel values unchanged. Defaults to 1.

    Returns:
        stretched_volume (numpy.ndarray): The stretched volume.

    Raises:
        AssertionError: If the input shape is not 3D.
        AssertionError: If the order is not integer and in the range of 0-5.
        AssertionError: If the stretching inputs are not integer.

    Example:
        ```python
        import qim3d
        import numpy as np

        # Generate box for stretching
        vol = np.zeros((100,100,100))
        vol[:,20:80, 20:80] = 1

        qim3d.viz.slicer(vol)
        ```
        ![warp_box](../../assets/screenshots/warp_box_0.png)

        ```python
        # Stretch the box along the x dimension
        stretched_volume = qim3d.operations.stretch(vol, x_stretch=20)
        print(stretched_volume.shape)
        qim3d.viz.slicer(stretched_volume)
        ```
        (100, 100, 140)

        ![warp_box_stretch](../../assets/screenshots/warp_box_stretch.png)
        ```python
        # Squeeze the box along the y dimension
        squeezed_volume = qim3d.operations.stretch(vol, x_stretch=-20)
        print(squeezed_volume.shape)
        qim3d.viz.slicer(squeezed_volume)
        ```
        (100, 100, 60)

        ![warp_box_squeeze](../../assets/screenshots/warp_box_squeeze.png)

    """
    assert len(volume.shape) == 3, 'Volume must be three-dimensional.'
    assert isinstance(order, int), 'Order must be an integer.'
    assert 0 <= order <= 5, 'Order must be in the range 0-5.'
    assert all(
        isinstance(var, int) for var in (x_stretch, y_stretch, z_stretch)
    ), 'Amount of pixel stretching must be integer'

    n, h, w = volume.shape

    # New dimensions after stretching
    new_n = n + 2 * z_stretch
    new_h = h + 2 * y_stretch
    new_w = w + 2 * x_stretch

    # Generate coordinate grid for the original volume
    z_grid, y_grid, x_grid = np.meshgrid(
        np.linspace(0, n - 1, new_n),
        np.linspace(0, h - 1, new_h),
        np.linspace(0, w - 1, new_w),
        indexing='ij',
    )

    # Stack coordinates and reshape for map_coordinates
    coords = np.vstack([z_grid.ravel(), y_grid.ravel(), x_grid.ravel()])

    # Perform interpolation
    stretched_volume = scipy.ndimage.map_coordinates(
        volume, coords, order=order, mode='nearest'
    )

    # Reshape back to the new volume dimensions
    return stretched_volume.reshape((new_n, new_h, new_w))

qim3d.operations.center_twist

center_twist(volume, rotation_angle=90, axis='z', order=1)

Applies a warping transformation that twists the volume around the center along the given axis.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (shape: n, h, w).

required
rotation_angle float

Amount of rotation from bottom of rotation axis to top. Defaults to 90.

90
axis str

Axis for rotation. Should either take value 'x', 'y' or 'z'. Defaults to 'z'.

'z'
order int

Order of spline interpolation. Order=0 (nearest-neighbor) will keep voxel values unchanged. Defaults to 1.

1

Returns:

Name Type Description
twisted_volume ndarray

The center rotated volume.

Raises:

Type Description
AssertionError

If the input shape is not 3D.

AssertionError

If the order is not integer and in the range of 0-5.

AssertionError

If the axis are not x, y or z

Example
import qim3d
import numpy as np

# Generate box for stretching
vol = np.zeros((100,100,100))
vol[:,20:80, 20:80] = 1
qim3d.viz.volumetric(vol)
# Twist the box 180 degrees along the z-axis
twisted_volume = qim3d.operations.center_twist(vol, rotation_angle=180, axis='z', order=1)
qim3d.viz.volumetric(twisted_volume)
Source code in qim3d/operations/_volume_operations.py
def center_twist(
    volume: np.ndarray, rotation_angle: float = 90, axis: str = 'z', order: int = 1
) -> np.ndarray:
    """
    Applies a warping transformation that twists the volume around the center along the given axis.

    Args:
        volume (numpy.ndarray): The input 3D volume (shape: n, h, w).
        rotation_angle (float): Amount of rotation from bottom of rotation axis to top. Defaults to 90.
        axis (str): Axis for rotation. Should either take value 'x', 'y' or 'z'. Defaults to 'z'.
        order (int): Order of spline interpolation. Order=0 (nearest-neighbor) will keep voxel values unchanged. Defaults to 1.

    Returns:
        twisted_volume (numpy.ndarray): The center rotated volume.

    Raises:
        AssertionError: If the input shape is not 3D.
        AssertionError: If the order is not integer and in the range of 0-5.
        AssertionError: If the axis are not x, y or z

    Example:
        ```python
        import qim3d
        import numpy as np

        # Generate box for stretching
        vol = np.zeros((100,100,100))
        vol[:,20:80, 20:80] = 1
        qim3d.viz.volumetric(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/warp_box.html" width="100%" height="500" frameborder="0"></iframe>
        ```python
        # Twist the box 180 degrees along the z-axis
        twisted_volume = qim3d.operations.center_twist(vol, rotation_angle=180, axis='z', order=1)
        qim3d.viz.volumetric(twisted_volume)
        ```
        <iframe src="https://platform.qim.dk/k3d/warp_box_twist.html" width="100%" height="500" frameborder="0"></iframe>

    """
    assert len(volume.shape) == 3, 'Volume must be three-dimensional.'
    assert isinstance(order, int), 'Order must be an integer.'
    assert 0 <= order <= 5, 'Order must be in the range 0-5.'
    assert axis in ['x', 'y', 'z'], 'Axis for rotation not recognized'

    # Get original dimensions
    n, h, w = volume.shape

    # Create a coordinate grid
    z, y, x = np.mgrid[0:n, 0:h, 0:w]

    if axis == 'z' or not axis:
        # Normalize
        z_norm = z / (n - 1)
        # Compute rotation angle per z-layer
        angles = np.radians(rotation_angle * z_norm)  # Convert to radians

        # Compute center and shift
        x_center, y_center = w / 2, h / 2
        x_shifted, y_shifted = x - x_center, y - y_center
        # Calculate new coordinates
        x_rot = x_center + x_shifted * np.cos(angles) - y_shifted * np.sin(angles)
        y_rot = y_center + x_shifted * np.sin(angles) + y_shifted * np.cos(angles)
        coords = np.array([z, y_rot, x_rot])
    elif axis == 'x':
        # Normalize
        x_norm = x / (w - 1)
        # Compute rotation angle per x-layer
        angles = np.radians(rotation_angle * x_norm)  # Convert to radians

        # Compute center and shift
        z_center, y_center = n / 2, h / 2
        z_shifted, y_shifted = z - z_center, y - y_center
        # Calculate new coordinates
        z_rot = z_center + z_shifted * np.cos(angles) - y_shifted * np.sin(angles)
        y_rot = y_center + z_shifted * np.sin(angles) + y_shifted * np.cos(angles)
        coords = np.array([z_rot, y_rot, x])
    elif axis == 'y':
        # Normalize
        y_norm = y / (h - 1)
        # Compute rotation angle per y-layer
        angles = np.radians(rotation_angle * y_norm)  # Convert to radians

        # Compute center and shift
        x_center, z_center = w / 2, n / 2
        x_shifted, z_shifted = x - x_center, z - z_center
        # Calculate new coordinates
        x_rot = x_center + z_shifted * np.sin(angles) + x_shifted * np.cos(angles)
        z_rot = z_center + z_shifted * np.cos(angles) - x_shifted * np.sin(angles)
        coords = np.array([z_rot, y, x_rot])

    # Interpolate at new coordinates
    swirled_volume = scipy.ndimage.map_coordinates(
        volume, coords, order=order, mode='nearest'
    )

    return swirled_volume

qim3d.operations.get_random_slice

get_random_slice(volume, width, length, seed=None)

Extract a randomly oriented 2D slice from a 3D volume.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
width int

The width of the extracted slice.

required
length int

The length of the extracted slice.

required
seed int | None

Seed for the random number generator, for reproducibility.

None

Returns:

Type Description
ndarray

np.ndarray: A 2D slice of shape (width, length) extracted from the volume.

Reference

This slicer is adapted from the interactive-unet package developed by William Laprade.

Example

import qim3d
import numpy as np

vol = qim3d.examples.shell_225x128x128
qim3d.viz.slices_grid(vol)
Normal slices

random_slices = []

for i in range(15):
    random_slices.append(qim3d.operations.get_random_slice(vol, width=100, length=100))

qim3d.viz.slices_grid(np.array(random_slices))
Random slices

Source code in qim3d/operations/_slicing_operations.py
def get_random_slice(
    volume: np.ndarray, width: int, length: int, seed: int | None = None
) -> np.ndarray:
    """
    Extract a randomly oriented 2D slice from a 3D volume.

    Args:
        volume (np.ndarray): The input 3D volume.
        width (int): The width of the extracted slice.
        length (int): The length of the extracted slice.
        seed (int | None, optional): Seed for the random number generator, for reproducibility.

    Returns:
        np.ndarray: A 2D slice of shape (width, length) extracted from the volume.

    !!! quote "Reference"
        This slicer is adapted from the
        [interactive-unet](https://github.com/laprade117/interactive-unet/blob/master/interactive_unet/slicer.py)
        package developed by William Laprade.

    Example:
        ```python
        import qim3d
        import numpy as np

        vol = qim3d.examples.shell_225x128x128
        qim3d.viz.slices_grid(vol)
        ```
        ![Normal slices](../../assets/screenshots/random_slice-before.png)

        ```python
        random_slices = []

        for i in range(15):
            random_slices.append(qim3d.operations.get_random_slice(vol, width=100, length=100))

        qim3d.viz.slices_grid(np.array(random_slices))

        ```
        ![Random slices](../../assets/screenshots/random_slice-after.png)

    """

    if seed is not None:
        np.random.seed(seed)

    # Build the slicer for this volume
    slicer = _Slicer(volume.shape)

    # Randomize orientation and origin
    slicer.randomize(sampling_mode='random')

    # Extract square slice
    slice2d = slicer.get_slice(volume, width=width, length=length)

    return slice2d

qim3d.morphology

Morphological operations for volumetric data.

qim3d.morphology.dilate

dilate(vol, kernel, method='pygorpho.linear', **kwargs)

Dilate an image. If method is either pygorpho.linear or pygorpho.flat, the dilation methods from Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology are used. These methods require a GPU, and we therefore recommend using the scipy implementation (scipy.ndimage) if no GPU is available on your current device.

Parameters:

Name Type Description Default
vol ndarray

The volume to dilate.

required
kernel int or ndarray

The structuring element/kernel to use while performing dilation. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.

required
method str

Determines the method for dilation. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.

'pygorpho.linear'
**kwargs Any

Additional keyword arguments for the used method. See the documentation for more information.

{}

Returns:

Name Type Description
dilated_vol ndarray

The dilated volume.

Example
import qim3d
import numpy as np

# Generate tubular synthetic blob
vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

# Visualize synthetic volume
qim3d.viz.volumetric(vol)
# Apply dilation
vol_dilated = qim3d.morphology.dilate(vol, kernel=(8,8,8), method='scipy.ndimage')

# Visualize
qim3d.viz.volumetric(vol_dilated)
Source code in qim3d/morphology/_common_morphologies.py
def dilate(
    vol: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Dilate an image. If method is either pygorpho.linear or pygorpho.flat, the dilation methods from [Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf) are used. These methods require a GPU, and we therefore recommend using the
    [scipy implementation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.grey_dilation.html) (scipy.ndimage) if no GPU is available on your current device.

    Args:
        vol (np.ndarray): The volume to dilate.
        kernel (int or np.ndarray): The structuring element/kernel to use while performing dilation. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.
        method (str, optional): Determines the method for dilation. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.
        **kwargs (Any): Additional keyword arguments for the used method. See the documentation for more information.

    Returns:
        dilated_vol (np.ndarray): The dilated volume.


    Example:
        ```python
        import qim3d
        import numpy as np

        # Generate tubular synthetic blob
        vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

        # Visualize synthetic volume
        qim3d.viz.volumetric(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/zonohedra_original.html" width="100%" height="500" frameborder="0"></iframe>

        ```python
        # Apply dilation
        vol_dilated = qim3d.morphology.dilate(vol, kernel=(8,8,8), method='scipy.ndimage')

        # Visualize
        qim3d.viz.volumetric(vol_dilated)
        ```
        <iframe src="https://platform.qim.dk/k3d/zonohedra_dilated.html" width="100%" height="500" frameborder="0"></iframe>

    """

    try:
        vol = np.asarray(vol)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(vol.shape) == 3, 'Volume must be three-dimensional.'

    if method == 'pygorpho.flat':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        return pg.flat.dilate(vol, kernel, **kwargs)

    elif method == 'pygorpho.linear':
        assert isinstance(
            kernel, int
        ), 'Kernel is generated within function and must therefore be an integer.'

        linesteps, linelens = pg.strel.flat_ball_approx(kernel)

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        return pg.flat.linear_dilate(vol, linesteps, linelens)

    elif method == 'scipy.ndimage':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        return ndi.grey_dilation(vol, footprint=kernel, **kwargs)

    else:
        err = 'Unknown closing method.'
        raise ValueError(err)

qim3d.morphology.erode

erode(vol, kernel, method='pygorpho.linear', **kwargs)

Erode an image. If method is either pygorpho.linear or pygorpho.flat, the erosion methods from Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology are used. These methods require a GPU, and we therefore recommend using the scipy implementation (scipy.ndimage) if no GPU is available on your current device.

Parameters:

Name Type Description Default
vol ndarray

The volume to erode.

required
kernel int or ndarray

The structuring element/kernel to use while performing erosion. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.

required
method str

Determines the method for erosion. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.

'pygorpho.linear'
**kwargs Any

Additional keyword arguments for the used method. See the documentation for more information.

{}

Returns:

Name Type Description
eroded_vol ndarray

The eroded volume.

Example
    import qim3d
    import numpy as np

    # Generate tubular synthetic blob
    vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

    # Visualize synthetic volume
    qim3d.viz.volumetric(vol)
    # Apply erosion
    vol_eroded = qim3d.morphology.erode(vol, kernel=(10,10,10), method='scipy.ndimage')

    # Visualize
    qim3d.viz.volumetric(vol_eroded)
Source code in qim3d/morphology/_common_morphologies.py
def erode(
    vol: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Erode an image. If method is either pygorpho.linear or pygorpho.flat, the erosion methods from [Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf) are used. These methods require a GPU, and we therefore recommend using the [scipy implementation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.grey_dilation.html) (scipy.ndimage) if no GPU is available on your current device.

    Args:
            vol (np.ndarray): The volume to erode.
            kernel (int or np.ndarray): The structuring element/kernel to use while performing erosion. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.
            method (str, optional): Determines the method for erosion. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.
            **kwargs (Any): Additional keyword arguments for the used method. See the documentation for more information.

    Returns:
            eroded_vol (np.ndarray): The eroded volume.


    Example:
        ```python
            import qim3d
            import numpy as np

            # Generate tubular synthetic blob
            vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

            # Visualize synthetic volume
            qim3d.viz.volumetric(vol)
        ```
        <iframe src="https://platform.qim.dk/k3d/zonohedra_original.html" width="100%" height="500" frameborder="0"></iframe>
        ```python
            # Apply erosion
            vol_eroded = qim3d.morphology.erode(vol, kernel=(10,10,10), method='scipy.ndimage')

            # Visualize
            qim3d.viz.volumetric(vol_eroded)
        ```
        <iframe src="https://platform.qim.dk/k3d/zonohedra_eroded.html" width="100%" height="500" frameborder="0"></iframe>

    """

    try:
        vol = np.asarray(vol)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(vol.shape) == 3, 'Volume must be three-dimensional.'

    if method == 'pygorpho.flat':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        return pg.flat.erode(vol, kernel, **kwargs)

    elif method == 'pygorpho.linear':
        assert isinstance(
            kernel, int
        ), 'Kernel is generated within function and must therefore be an integer.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        linesteps, linelens = pg.strel.flat_ball_approx(kernel)
        return pg.flat.linear_erode(vol, linesteps, linelens, **kwargs)

    elif method == 'scipy.ndimage':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        return ndi.grey_erosion(vol, footprint=kernel, **kwargs)

    else:
        err = 'Unknown closing method.'
        raise ValueError(err)

qim3d.morphology.opening

opening(vol, kernel, method='pygorpho.linear', **kwargs)

Morphologically open a volume. If method is either pygorpho.linear or pygorpho.flat, the open methods from Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology are used. These methods require a GPU, and we therefore recommend using the scipy implementation (scipy.ndimage) if no GPU is available on your current device.

Parameters:

Name Type Description Default
vol ndarray

The volume to open.

required
kernel int or ndarray

The structuring element/kernel to use while performing erosion. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.

required
method str

Determines the method for opening. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.

'pygorpho.linear'
**kwargs Any

Additional keyword arguments for the used method. See the documentation for more information.

{}

Returns:

Name Type Description
eroded_vol ndarray

The eroded volume.

Example
import qim3d
import numpy as np

# Generate tubular synthetic blob
vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

# Add noise to the data
vol_noised = qim3d.generate.background(
    background_shape=vol.shape,
    apply_method = 'add',
    apply_to = vol
)

# Visualize synthetic volume
qim3d.viz.volumetric(vol_noised, grid_visible=True)
# Apply opening
vol_opened = qim3d.morphology.opening(vol_noised, kernel=(6,6,6), method='scipy.ndimage')

# Visualize
qim3d.viz.volumetric(vol_opened)
Source code in qim3d/morphology/_common_morphologies.py
def opening(
    vol: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Morphologically open a volume.
    If method is either pygorpho.linear or pygorpho.flat, the open methods from [Zonohedral Approximation of Spherical Structuring Element for
    Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf) are used.
    These methods require a GPU, and we therefore recommend using the [scipy implementation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.grey_dilation.html) (scipy.ndimage) if no GPU is available on your current device.

    Args:
        vol (np.ndarray): The volume to open.
        kernel (int or np.ndarray): The structuring element/kernel to use while performing erosion. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.
        method (str, optional): Determines the method for opening. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.
        **kwargs (Any): Additional keyword arguments for the used method. See the documentation for more information.

    Returns:
        eroded_vol (np.ndarray): The eroded volume.


    Example:
        ```python
        import qim3d
        import numpy as np

        # Generate tubular synthetic blob
        vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

        # Add noise to the data
        vol_noised = qim3d.generate.background(
            background_shape=vol.shape,
            apply_method = 'add',
            apply_to = vol
        )

        # Visualize synthetic volume
        qim3d.viz.volumetric(vol_noised, grid_visible=True)
        ```

        <iframe src="https://platform.qim.dk/k3d/zonohedra_noised_volume.html" width="100%" height="500" frameborder="0"></iframe>

        ```python

        # Apply opening
        vol_opened = qim3d.morphology.opening(vol_noised, kernel=(6,6,6), method='scipy.ndimage')

        # Visualize
        qim3d.viz.volumetric(vol_opened)
        ```

        <iframe src="https://platform.qim.dk/k3d/zonohedra_opening.html" width="100%" height="500" frameborder="0"></iframe>

    """

    try:
        vol = np.asarray(vol)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(vol.shape) == 3, 'Volume must be three-dimensional.'

    if method == 'pygorpho.flat':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        return pg.flat.open(vol, kernel, **kwargs)

    elif method == 'pygorpho.linear':
        assert isinstance(
            kernel, int
        ), 'Kernel is generated within function and must therefore be an integer.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        linesteps, linelens = pg.strel.flat_ball_approx(kernel)
        return pg.flat.linear_open(vol, linesteps, linelens, **kwargs)

    elif method == 'scipy.ndimage':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        return ndi.grey_opening(vol, footprint=kernel, **kwargs)

    else:
        err = 'Unknown closing method.'
        raise ValueError(err)

qim3d.morphology.closing

closing(vol, kernel, method='pygorpho.linear', **kwargs)

Morphologically close a volume. If method is either pygorpho.linear or pygorpho.flat, the close methods from Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology are used. These methods require a GPU, and we therefore recommend using the scipy implementation (scipy.ndimage) if no GPU is available on your current device.

Parameters:

Name Type Description Default
vol ndarray

The volume to be closed.

required
kernel int or ndarray

The structuring element/kernel to use while performing opening. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.

required
method str

Determines the method for closing. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.

'pygorpho.linear'
**kwargs Any

Additional keyword arguments for the used method. See the documentation for more information.

{}

Returns:

Name Type Description
closed_vol ndarray

The closed volume.

Example
import qim3d
import numpy as np

# Generate a cube with a hole through it
cube = np.zeros((110,110,110))
cube[10:90, 10:90, 10:90] = 1
cube[60:70,:,60:70]=0

# Visualize synthetic volume
qim3d.viz.volumetric(cube)
# Apply closing
cube_closed = qim3d.morphology.closing(cube, kernel=(15,15,15), method='scipy.ndimage')

# Visualize
qim3d.viz.volumetric(cube_closed)
Source code in qim3d/morphology/_common_morphologies.py
def closing(
    vol: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Morphologically close a volume.
    If method is either pygorpho.linear or pygorpho.flat, the close methods from [Zonohedral Approximation of Spherical Structuring Element for
    Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf) are used.
    These methods require a GPU, and we therefore recommend using the [scipy implementation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.grey_dilation.html) (scipy.ndimage) if no GPU is available on your current device.

    Args:
        vol (np.ndarray): The volume to be closed.
        kernel (int or np.ndarray): The structuring element/kernel to use while performing opening. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.
        method (str, optional): Determines the method for closing. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.
        **kwargs (Any): Additional keyword arguments for the used method. See the documentation for more information.

    Returns:
        closed_vol (np.ndarray): The closed volume.


    Example:
        ```python
        import qim3d
        import numpy as np

        # Generate a cube with a hole through it
        cube = np.zeros((110,110,110))
        cube[10:90, 10:90, 10:90] = 1
        cube[60:70,:,60:70]=0

        # Visualize synthetic volume
        qim3d.viz.volumetric(cube)
        ```
        <iframe src="https://platform.qim.dk/k3d/zonohedra_cube.html" width="100%" height="500" frameborder="0"></iframe>
        ```python
        # Apply closing
        cube_closed = qim3d.morphology.closing(cube, kernel=(15,15,15), method='scipy.ndimage')

        # Visualize
        qim3d.viz.volumetric(cube_closed)
        ```
        <iframe src="https://platform.qim.dk/k3d/zonohedra_cube_closed.html" width="100%" height="500" frameborder="0"></iframe>

    """

    try:
        vol = np.asarray(vol)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(vol.shape) == 3, 'Volume must be three-dimensional.'

    if method == 'pygorpho.flat':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        return pg.flat.close(vol, kernel, **kwargs)

    elif method == 'pygorpho.linear':
        assert isinstance(
            kernel, int
        ), 'Kernel is generated within function and must therefore be an integer.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        linesteps, linelens = pg.strel.flat_ball_approx(kernel)
        return pg.flat.linear_close(vol, linesteps, linelens, **kwargs)

    elif method == 'scipy.ndimage':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        return ndi.grey_closing(vol, footprint=kernel, **kwargs)

    else:
        err = 'Unknown closing method.'
        raise ValueError(err)

qim3d.morphology.black_tophat

black_tophat(vol, kernel, method='pygorpho.linear', **kwargs)

Perform black tophat operation on a volume. This operation is defined as bothat(x)=close(x)-x. If method is either pygorpho.linear or pygorpho.flat, the close methods from Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology are used. These methods require a GPU, and we therefore recommend using the scipy implementation (scipy.ndimage) if no GPU is available on your current device.

Parameters:

Name Type Description Default
vol ndarray

The volume to perform the black tophat on.

required
kernel int or ndarray

The structuring element/kernel to use while performing opening. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.

required
method str

Determines the method for black tophat. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.

'pygorpho.linear'
**kwargs Any

Additional keyword arguments for the used method. See the documentation for more information.

{}

Returns:

Name Type Description
bothat_vol ndarray

The morphed volume.

Example
import qim3d
import numpy as np

# Generate tubular synthetic blob
vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

# Visualize synthetic volume
qim3d.viz.volumetric(vol)
# Apply the tophat
vol_black = qim3d.morphology.black_tophat(vol, kernel=(10,10,10), method='scipy.ndimage')

qim3d.viz.volumetric(vol_black)
Source code in qim3d/morphology/_common_morphologies.py
def black_tophat(
    vol: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Perform black tophat operation on a volume.
    This operation is defined as bothat(x)=close(x)-x.
    If method is either pygorpho.linear or pygorpho.flat, the close methods from [Zonohedral Approximation of Spherical Structuring Element for
    Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf) are used.
    These methods require a GPU, and we therefore recommend using the [scipy implementation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.grey_dilation.html) (scipy.ndimage) if no GPU is available on your current device.

    Args:
        vol (np.ndarray): The volume to perform the black tophat on.
        kernel (int or np.ndarray): The structuring element/kernel to use while performing opening. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.
        method (str, optional): Determines the method for black tophat. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.
        **kwargs (Any): Additional keyword arguments for the used method. See the documentation for more information.

    Returns:
        bothat_vol (np.ndarray): The morphed volume.


    Example:
            ```python
            import qim3d
            import numpy as np

            # Generate tubular synthetic blob
            vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

            # Visualize synthetic volume
            qim3d.viz.volumetric(vol)
            ```
            <iframe src="https://platform.qim.dk/k3d/zonohedra_original.html" width="100%" height="500" frameborder="0"></iframe>
            ```python
            # Apply the tophat
            vol_black = qim3d.morphology.black_tophat(vol, kernel=(10,10,10), method='scipy.ndimage')

            qim3d.viz.volumetric(vol_black)
            ```
            <iframe src="https://platform.qim.dk/k3d/zonohedra_black_tophat.html" width="100%" height="500" frameborder="0"></iframe>

    """

    try:
        vol = np.asarray(vol)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(vol.shape) == 3, 'Volume must be three-dimensional.'

    if method == 'pygorpho.flat':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        return pg.flat.bothat(vol, kernel, **kwargs)

    elif method == 'pygorpho.linear':
        assert isinstance(
            kernel, int
        ), 'Kernel is generated within function and must therefore be an integer.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        linesteps, linelens = pg.strel.flat_ball_approx(kernel)
        return pg.flat.bothat(vol, linesteps, linelens, **kwargs)

    elif method == 'scipy.ndimage':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        return ndi.black_tophat(vol, footprint=kernel, **kwargs)

    else:
        err = 'Unknown closing method.'
        raise ValueError(err)

qim3d.morphology.white_tophat

white_tophat(vol, kernel, method='pygorpho.linear', **kwargs)

Perform white tophat operation on a volume. This operation is defined as tophat(x)=x-open(x). If method is either pygorpho.linear or pygorpho.flat, the open methods from Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology are used. These methods require a GPU, and we therefore recommend using the scipy implementation (scipy.ndimage) if no GPU is available on your current device.

Parameters:

Name Type Description Default
vol ndarray

The volume to perform the white tophat on.

required
kernel int or ndarray

The structuring element/kernel to use while performing opening. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.

required
method str

Determines the method for white tophat. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.

'pygorpho.linear'
**kwargs Any

Additional keyword arguments for the used method. See the documentation for more information.

{}

Returns:

Name Type Description
tophat_vol ndarray

The morphed volume.

Example
import qim3d
import numpy as np

# Generate tubular synthetic blob
vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

# Visualize synthetic volume
qim3d.viz.volumetric(vol)
# Apply tophat
vol_white = qim3d.morphology.white_tophat(vol, kernel=(10,10,10), method='scipy.ndimage')

qim3d.viz.volumetric(vol_white)
Source code in qim3d/morphology/_common_morphologies.py
def white_tophat(
    vol: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Perform white tophat operation on a volume.
    This operation is defined as tophat(x)=x-open(x).
    If method is either pygorpho.linear or pygorpho.flat, the open methods from [Zonohedral Approximation of Spherical Structuring Element for
    Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf) are used.
    These methods require a GPU, and we therefore recommend using the [scipy implementation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.grey_dilation.html) (scipy.ndimage) if no GPU is available on your current device.

    Args:
        vol (np.ndarray): The volume to perform the white tophat on.
        kernel (int or np.ndarray): The structuring element/kernel to use while performing opening. Note that the kernel should be 3D unless if the linear method is used. If this method is used, a kernel resembling a ball will be created with an integer radius.
        method (str, optional): Determines the method for white tophat. Use either 'pygorpho.linear', 'pygorpho.flat' or 'scipy.ndimage'. Defaults to 'pygorpho.linear'.
        **kwargs (Any): Additional keyword arguments for the used method. See the documentation for more information.

    Returns:
        tophat_vol (np.ndarray): The morphed volume.


    Example:
            ```python
            import qim3d
            import numpy as np

            # Generate tubular synthetic blob
            vol = qim3d.generate.volume(noise_scale=0.025, seed=50)

            # Visualize synthetic volume
            qim3d.viz.volumetric(vol)
            ```
            <iframe src="https://platform.qim.dk/k3d/zonohedra_original.html" width="100%" height="500" frameborder="0"></iframe>

            ```python
            # Apply tophat
            vol_white = qim3d.morphology.white_tophat(vol, kernel=(10,10,10), method='scipy.ndimage')

            qim3d.viz.volumetric(vol_white)
            ```
            <iframe src="https://platform.qim.dk/k3d/zonohedra_white_tophat.html" width="100%" height="500" frameborder="0"></iframe>

    """

    try:
        vol = np.asarray(vol)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(vol.shape) == 3, 'Volume must be three-dimensional.'

    if method == 'pygorpho.flat':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        return pg.flat.tophat(vol, kernel, **kwargs)

    elif method == 'pygorpho.linear':
        assert isinstance(
            kernel, int
        ), 'Kernel is generated within function and must therefore be an integer.'

        if not pg.cuda.get_device_count():
            err = 'no CUDA device available. Use method=scipy.ndimage.'
            raise RuntimeError(err)

        linesteps, linelens = pg.strel.flat_ball_approx(kernel)
        return pg.flat.tophat(vol, linesteps, linelens, **kwargs)

    elif method == 'scipy.ndimage':
        kernel = _create_kernel(kernel)
        assert kernel.ndim == 3, 'Kernel must a 3D np.ndarray.'

        return ndi.white_tophat(vol, footprint=kernel, **kwargs)

    else:
        err = 'Unknown closing method.'
        raise ValueError(err)