Skip to content

Operations on volumetric data

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

Operations on volumes.

qim3d.operations.remove_background

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

Applies a background correction pipeline using median filtering and morphological operations.

This function acts as a convenience wrapper for a sequential processing pipeline designed to smooth the image and suppress clutter. It performs two distinct operations:

  1. Median Filter: Reduces high-frequency impulse noise (salt-and-pepper) using a kernel of size median_filter_size.
  2. Morphological Opening: Removes bright features (or dark, if background='bright') that are smaller than the min_object_radius. This effectively separates large structural components from small background artifacts or texture.

Parameters:

Name Type Description Default
volume ndarray

The input volume to process.

required
median_filter_size int

The size of the kernel for the initial median denoising step. Defaults to 2.

2
min_object_radius int

The radius of the structuring element (ball) used for the morphological operation. Details smaller than this size are removed. Defaults to 3.

3
background str

The intensity of the background relative to the objects.

  • 'dark': Use for bright objects on a dark background.
  • 'bright': Use for dark objects on a bright background (the volume is inverted during processing).
'dark'
**median_kwargs Any

Additional keyword arguments passed to the underlying Median filter.

{}

Returns:

Name Type Description
filtered_volume ndarray

The processed volume with background clutter and noise suppressed.

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(
    volume: np.ndarray,
    median_filter_size: int = 2,
    min_object_radius: int = 3,
    background: str = 'dark',
    **median_kwargs,
) -> np.ndarray:
    """
    Applies a background correction pipeline using median filtering and morphological operations.

    This function acts as a convenience wrapper for a sequential processing pipeline designed to smooth the image and suppress clutter. It performs two distinct operations:

    1.  **Median Filter:** Reduces high-frequency impulse noise (salt-and-pepper) using a kernel of size `median_filter_size`.
    2.  **Morphological Opening:** Removes bright features (or dark, if `background='bright'`) that are smaller than the `min_object_radius`. This effectively separates large structural components from small background artifacts or texture.

    Args:
        volume (np.ndarray): The input volume to process.
        median_filter_size (int, optional): The size of the kernel for the initial median denoising step. Defaults to 2.
        min_object_radius (int, optional): The radius of the structuring element (ball) used for the morphological operation. Details smaller than this size are removed. Defaults to 3.
        background (str, optional): The intensity of the background relative to the objects.

            * **'dark'**: Use for bright objects on a dark background.
            * **'bright'**: Use for dark objects on a bright background (the volume is inverted during processing).

        **median_kwargs (Any): Additional keyword arguments passed to the underlying `Median` filter.

    Returns:
        filtered_volume (np.ndarray):
            The processed volume with background clutter and noise suppressed.

    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(volume)

qim3d.operations.fade_mask

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

Applies a soft attenuation mask (vignetting) to the volume to suppress boundary artifacts.

This function multiplies the input volume by a generated mask that decays from the center outwards based on a power-law profile. It is commonly used to remove reconstruction artifacts at the edges of a scan or to isolate a central Region of Interest (ROI) by suppressing peripheral data. The shape of the mask can be spherical or cylindrical.

Parameters:

Name Type Description Default
volume ndarray

The 3D input volume.

required
decay_rate float

The exponent for the power-law decay. Higher values create a "flatter" central region with a sharper drop-off near the mask edge, while lower values cause a more gradual fade from the center. Defaults to 10.

10
ratio float

The effective radius of the non-zero mask region relative to the volume size. Defaults to 0.5.

0.5
geometry str

The geometric shape of the mask.

  • 'spherical': Fades in all directions from the volume center.
  • 'cylindrical': Fades radially from a central axis (defined by axis), maintaining constant intensity along that axis. Defaults to 'spherical'.
'spherical'
invert bool

If True, inverts the mask (fades the center and keeps the edges). Defaults to False.

False
axis int

The axis of alignment for the cylinder if geometry='cylindrical'. Defaults to 0.

0
**kwargs Any

Additional keyword arguments.

{}

Returns:

Name Type Description
faded_vol ndarray

The volume with the attenuation mask applied, renormalized to match the original maximum intensity.

Raises:

Type Description
ValueError

If axis is invalid or geometry is not 'spherical' or 'cylindrical'.

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(
    volume: np.ndarray,
    decay_rate: float = 10,
    ratio: float = 0.5,
    geometry: str = 'spherical',
    invert: bool = False,
    axis: int = 0,
    **kwargs,
) -> np.ndarray:
    """
    Applies a soft attenuation mask (vignetting) to the volume to suppress boundary artifacts.

    This function multiplies the input volume by a generated mask that decays from the center outwards based on a power-law profile. It is commonly used to remove reconstruction artifacts at the edges of a scan or to isolate a central Region of Interest (ROI) by suppressing peripheral data. The shape of the mask can be spherical or cylindrical.

    Args:
        volume (np.ndarray): The 3D input volume.
        decay_rate (float, optional): The exponent for the power-law decay. Higher values create a "flatter" central region with a sharper drop-off near the mask edge, while lower values cause a more gradual fade from the center. Defaults to 10.
        ratio (float, optional): The effective radius of the non-zero mask region relative to the volume size. Defaults to 0.5.
        geometry (str, optional): The geometric shape of the mask.

            * **'spherical'**: Fades in all directions from the volume center.
            * **'cylindrical'**: Fades radially from a central axis (defined by `axis`), maintaining constant intensity along that axis. Defaults to 'spherical'.

        invert (bool, optional): If `True`, inverts the mask (fades the center and keeps the edges). Defaults to `False`.
        axis (int, optional): The axis of alignment for the cylinder if `geometry='cylindrical'`. Defaults to 0.
        **kwargs (Any): Additional keyword arguments.

    Returns:
        faded_vol (np.ndarray):
            The volume with the attenuation mask applied, renormalized to match the original maximum intensity.

    Raises:
        ValueError: If `axis` is invalid or `geometry` is not 'spherical' or 'cylindrical'.

    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 >= volume.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 = volume.shape
    z, y, x = np.indices(shape)

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

    # 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 = volume * 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
)

Composites a foreground image onto a background using alpha blending.

This function overlays a mask or secondary image (foreground) onto a base image (background). It automatically normalizes inputs (handling 2D/3D, float/integer, and range mismatches) to ensure compatible 8-bit RGB formats before blending. A key feature is the conditional transparency (hide_black), which treats black pixels in the foreground as fully transparent, making it ideal for overlaying sparse segmentation masks without obscuring the rest of the image.

Parameters:

Name Type Description Default
background ndarray

The base image.

required
foreground ndarray

The overlay image (e.g., a segmentation mask or heatmap). Must match the spatial dimensions of the background.

required
alpha float

The global opacity of the foreground, ranging from 0.0 (fully transparent) to 1.0 (fully opaque). Defaults to 0.5.

0.5
hide_black bool

If True, forces the alpha channel to 0 for all perfectly black pixels [0, 0, 0] in the foreground. This prevents the background of a sparse mask from darkening the base image. Defaults to True.

True

Returns:

Name Type Description
composite ndarray

The resulting 8-bit RGB image after blending.

Raises:

Type Description
ValueError

If the spatial dimensions (height/width) of the input images do not match.

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:
    """
    Composites a foreground image onto a background using alpha blending.

    This function overlays a mask or secondary image (foreground) onto a base image (background). It automatically normalizes inputs (handling 2D/3D, float/integer, and range mismatches) to ensure compatible 8-bit RGB formats before blending. A key feature is the conditional transparency (`hide_black`), which treats black pixels in the foreground as fully transparent, making it ideal for overlaying sparse segmentation masks without obscuring the rest of the image.

    Args:
        background (np.ndarray): The base image.
        foreground (np.ndarray): The overlay image (e.g., a segmentation mask or heatmap). Must match the spatial dimensions of the background.
        alpha (float, optional): The global opacity of the foreground, ranging from 0.0 (fully transparent) to 1.0 (fully opaque). Defaults to 0.5.
        hide_black (bool, optional): If `True`, forces the alpha channel to 0 for all perfectly black pixels `[0, 0, 0]` in the foreground. This prevents the background of a sparse mask from darkening the base image. Defaults to `True`.

    Returns:
        composite (np.ndarray):
            The resulting 8-bit RGB image after blending.

    Raises:
        ValueError: If the spatial dimensions (height/width) of the input images do not match.
    """

    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(volume, thickness)

Constructs a hollow shell from a solid 3D volume.

This function isolates the outer boundary layer of an object. It achieves this by performing a morphological erosion (using a minimum filter) to identify the inner core, which is then subtracted from the original volume via a logical XOR operation. The result is a shell that retains the original intensity values, while the interior is set to zero.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume. Non-zero values are treated as the object.

required
thickness int

The width of the resulting shell in voxels. This value determines the size of the erosion kernel used to define the hollow core.

required

Returns:

Name Type Description
vol_hollowed ndarray

The processed volume containing only the outer shell of the object.

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(
    volume: np.ndarray,
    thickness: int,
) -> np.ndarray:
    """
    Constructs a hollow shell from a solid 3D volume.

    This function isolates the outer boundary layer of an object. It achieves this by performing a morphological erosion (using a minimum filter) to identify the inner core, which is then subtracted from the original volume via a logical XOR operation. The result is a shell that retains the original intensity values, while the interior is set to zero.

    Args:
        volume (np.ndarray): The input 3D volume. Non-zero values are treated as the object.
        thickness (int): The width of the resulting shell in voxels. This value determines the size of the erosion kernel used to define the hollow core.

    Returns:
        vol_hollowed (np.ndarray):
            The processed volume containing only the outer shell of the object.

    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 = volume > 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 = volume * vol_mask

    return vol_hollowed

qim3d.operations.pad

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

Symmetrically pads a 3D volume with zeros (zero-padding).

This function increases the dimensions of the volume by adding empty space (zeros) around the original data. It is commonly used to prepare data for neural network inputs (to match required input sizes), prevent boundary artifacts during convolution, or center an object within a larger field of view.

The padding amount is applied to each side of the axis. For example, x_axis=10 adds 10 pixels to the left and 10 pixels to the right, increasing the total width by 20.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (Z, Y, X).

required
x_axis float

Pixels to add to each side of the X dimension (width). Can be an integer or half-integer (e.g., 2.5 adds 2 pixels to one side and 3 to the other). Defaults to 0.

0
y_axis float

Pixels to add to each side of the Y dimension (height). Defaults to 0.

0
z_axis float

Pixels to add to each side of the Z dimension (depth). Defaults to 0.

0

Returns:

Name Type Description
padded_volume ndarray

The resized volume containing the centered original data.

Raises:

Type Description
AssertionError

If the input volume is not 3D or if padding values are 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:
    """
    Symmetrically pads a 3D volume with zeros (zero-padding).

    This function increases the dimensions of the volume by adding empty space (zeros) around the original data. It is commonly used to prepare data for neural network inputs (to match required input sizes), prevent boundary artifacts during convolution, or center an object within a larger field of view.

    The padding amount is applied to *each side* of the axis. For example, `x_axis=10` adds 10 pixels to the left and 10 pixels to the right, increasing the total width by 20.

    Args:
        volume (np.ndarray): The input 3D volume (Z, Y, X).
        x_axis (float, optional): Pixels to add to each side of the X dimension (width). Can be an integer or half-integer (e.g., 2.5 adds 2 pixels to one side and 3 to the other). Defaults to 0.
        y_axis (float, optional): Pixels to add to each side of the Y dimension (height). Defaults to 0.
        z_axis (float, optional): Pixels to add to each side of the Z dimension (depth). Defaults to 0.

    Returns:
        padded_volume (np.ndarray):
            The resized volume containing the centered original data.

    Raises:
        AssertionError: If the input volume is not 3D or if padding values are 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 volume with zeros to match a specific target shape.

This function centers the original volume within a larger array of size shape. It is useful for standardizing image sizes in a dataset (e.g., for machine learning models that require fixed input dimensions) without resizing or interpolating the data.

If a target dimension is smaller than the original volume dimension, padding is skipped for that axis (the volume is not cropped).

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
shape tuple[int, int, int]

The desired output shape (Z, Y, X).

required

Returns:

Name Type Description
padded_volume ndarray

The volume padded to the specified dimensions.

Raises:

Type Description
AssertionError

If the input volume or target shape is not 3D, or if shape contains non-integers.

Example

import qim3d
import numpy as np

# Create volume of shape (100,100,100)
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 volume with zeros to match a specific target shape.

    This function centers the original volume within a larger array of size `shape`. It is useful for standardizing image sizes in a dataset (e.g., for machine learning models that require fixed input dimensions) without resizing or interpolating the data.

    If a target dimension is smaller than the original volume dimension, padding is skipped for that axis (the volume is not cropped).

    Args:
        volume (np.ndarray): The input 3D volume.
        shape (tuple[int, int, int]): The desired output shape `(Z, Y, X)`.

    Returns:
        padded_volume (np.ndarray):
            The volume padded to the specified dimensions.

    Raises:
        AssertionError: If the input volume or target shape is not 3D, or if `shape` contains non-integers.

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

        # Create volume of shape (100,100,100)
        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)

Crops the volume to the bounding box of the non-zero content.

This function removes all "empty" (all-zero) planes from the borders of the 3D volume. It effectively shrinks the array to the smallest cuboid that still contains all the data, discarding the surrounding background. This is useful for reducing memory usage and file size after applying a mask or ROI.

Parameters:

Name Type Description Default
volume ndarray

The 3D input volume.

required

Returns:

Name Type Description
trimmed_volume ndarray

The cropped volume containing only the region with non-zero values.

Raises:

Type Description
AssertionError

If the input volume 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:
    """
    Crops the volume to the bounding box of the non-zero content.

    This function removes all "empty" (all-zero) planes from the borders of the 3D volume. It effectively shrinks the array to the smallest cuboid that still contains all the data, discarding the surrounding background. This is useful for reducing memory usage and file size after applying a mask or ROI.

    Args:
        volume (np.ndarray): The 3D input volume.

    Returns:
        trimmed_volume (np.ndarray):
            The cropped volume containing only the region with non-zero values.

    Raises:
        AssertionError: If the input `volume` 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 geometric shear transformation to distort the 3D volume.

This function displaces voxels along one axis based on their position along another axis. It essentially "slides" the slices of the volume relative to each other. This is useful for data augmentation (creating variations for training) or correcting geometric distortions (e.g., deskewing data acquired from a misaligned stage).

The shift parameters define the maximum displacement in pixels. The shift is applied linearly from -shift at one end of the axis to +shift at the other, resulting in a total range of 2 * shift.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (Z, Y, X).

required
x_shift_y int

Max shift in X, varying along the Y-axis. Defaults to 0.

0
x_shift_z int

Max shift in X, varying along the Z-axis. Defaults to 0.

0
y_shift_x int

Max shift in Y, varying along the X-axis. Defaults to 0.

0
y_shift_z int

Max shift in Y, varying along the Z-axis. Defaults to 0.

0
z_shift_x int

Max shift in Z, varying along the X-axis. Defaults to 0.

0
z_shift_y int

Max shift in Z, varying along the Y-axis. Defaults to 0.

0
order int

The order of the spline interpolation used during resampling.

  • 0: Nearest-neighbor (preserves original values, good for labels).
  • 1: Linear interpolation (default, good for intensity data).
  • 2-5: Higher-order splines (smoother but slower).
1

Returns:

Name Type Description
sheared_volume ndarray

The deformed volume.

Raises:

Type Description
AssertionError

If the input is not 3D, order is invalid, or shift values are not integers.

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 geometric shear transformation to distort the 3D volume.

    This function displaces voxels along one axis based on their position along another axis. It essentially "slides" the slices of the volume relative to each other. This is useful for data augmentation (creating variations for training) or correcting geometric distortions (e.g., deskewing data acquired from a misaligned stage).

    The `shift` parameters define the maximum displacement in pixels. The shift is applied linearly from `-shift` at one end of the axis to `+shift` at the other, resulting in a total range of `2 * shift`.

    Args:
        volume (np.ndarray): The input 3D volume (Z, Y, X).
        x_shift_y (int, optional): Max shift in X, varying along the Y-axis. Defaults to 0.
        x_shift_z (int, optional): Max shift in X, varying along the Z-axis. Defaults to 0.
        y_shift_x (int, optional): Max shift in Y, varying along the X-axis. Defaults to 0.
        y_shift_z (int, optional): Max shift in Y, varying along the Z-axis. Defaults to 0.
        z_shift_x (int, optional): Max shift in Z, varying along the X-axis. Defaults to 0.
        z_shift_y (int, optional): Max shift in Z, varying along the Y-axis. Defaults to 0.
        order (int, optional): The order of the spline interpolation used during resampling.

            * **0**: Nearest-neighbor (preserves original values, good for labels).
            * **1**: Linear interpolation (default, good for intensity data).
            * **2-5**: Higher-order splines (smoother but slower).

    Returns:
        sheared_volume (np.ndarray):
            The deformed volume.

    Raises:
        AssertionError: If the input is not 3D, `order` is invalid, or shift values are not integers.

    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 a sinusoidal geometric distortion (warping) to the 3D volume along the Z-axis.

This function displaces voxels in the X and/or Y directions based on their position along the Z-axis, following a sine wave pattern. This creates a "wavy" or "snake-like" deformation through the depth of the volume. It is primarily used for data augmentation to simulate non-rigid deformations or acquisition artifacts in training data.

The displacement is calculated as: delta_x(z) = x_amp * sin(2 * pi * x_periods * (z / depth) + x_offset)

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (Z, Y, X).

required
x_amp float

The amplitude of the sine wave in the X-direction. Defines the maximum shift in pixels. Defaults to 0.

0
y_amp float

The amplitude of the sine wave in the Y-direction. Defaults to 0.

0
x_periods float

The frequency of the wave. Represents the number of full sine wave cycles that occur along the full length of the Z-axis. Defaults to 1.0.

1.0
y_periods float

The frequency of the wave in the Y-direction. Defaults to 1.0.

1.0
x_offset float

The phase shift or starting position of the wave in the X-direction (in radians). Defaults to 0.0.

0.0
y_offset float

The phase shift in the Y-direction. Defaults to 0.0.

0.0
order int

The order of the spline interpolation used during resampling.

  • 0: Nearest-neighbor.
  • 1: Linear interpolation (default).
  • 2-5: Higher-order splines.
1

Returns:

Name Type Description
warped_volume ndarray

The distorted volume.

Raises:

Type Description
AssertionError

If the input volume is not 3D or if order is invalid.

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 a sinusoidal geometric distortion (warping) to the 3D volume along the Z-axis.

    This function displaces voxels in the X and/or Y directions based on their position along the Z-axis, following a sine wave pattern. This creates a "wavy" or "snake-like" deformation through the depth of the volume. It is primarily used for data augmentation to simulate non-rigid deformations or acquisition artifacts in training data.

    The displacement is calculated as:
    `delta_x(z) = x_amp * sin(2 * pi * x_periods * (z / depth) + x_offset)`

    Args:
        volume (np.ndarray): The input 3D volume (Z, Y, X).
        x_amp (float, optional): The amplitude of the sine wave in the X-direction. Defines the maximum shift in pixels. Defaults to 0.
        y_amp (float, optional): The amplitude of the sine wave in the Y-direction. Defaults to 0.
        x_periods (float, optional): The frequency of the wave. Represents the number of full sine wave cycles that occur along the full length of the Z-axis. Defaults to 1.0.
        y_periods (float, optional): The frequency of the wave in the Y-direction. Defaults to 1.0.
        x_offset (float, optional): The phase shift or starting position of the wave in the X-direction (in radians). Defaults to 0.0.
        y_offset (float, optional): The phase shift in the Y-direction. Defaults to 0.0.
        order (int, optional): The order of the spline interpolation used during resampling.

            * **0**: Nearest-neighbor.
            * **1**: Linear interpolation (default).
            * **2-5**: Higher-order splines.

    Returns:
        warped_volume (np.ndarray):
            The distorted volume.

    Raises:
        AssertionError: If the input volume is not 3D or if `order` is invalid.

    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
)

Resizes (stretches or compresses) the volume along one or more axes using interpolation.

This function changes the aspect ratio and spatial resolution of the volume by resampling it onto a new grid.

  • Positive stretch: Increases the size of the volume (upsampling). The content appears elongated.
  • Negative stretch: Decreases the size of the volume (downsampling). The content appears compressed.

The operation is "symmetric" in terms of the input parameter: a stretch of N adds (or removes) N pixels to both ends of the axis, changing the total dimension by 2 * N.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (Z, Y, X).

required
x_stretch int

Pixels added/removed per side along the X-axis. Total width changes by 2 * x_stretch. Defaults to 0.

0
y_stretch int

Pixels added/removed per side along the Y-axis. Defaults to 0.

0
z_stretch int

Pixels added/removed per side along the Z-axis. Defaults to 0.

0
order int

The order of spline interpolation.

  • 0: Nearest-neighbor (preserves integer labels).
  • 1: Linear interpolation (default, good for intensity data).
  • 2-5: Higher-order splines.
1

Returns:

Name Type Description
stretched_volume ndarray

The resized volume.

Raises:

Type Description
AssertionError

If the input volume is not 3D, order is invalid, or stretch values are not integers.

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:
    """
    Resizes (stretches or compresses) the volume along one or more axes using interpolation.

    This function changes the aspect ratio and spatial resolution of the volume by resampling it onto a new grid.

    * **Positive stretch:** Increases the size of the volume (upsampling). The content appears elongated.
    * **Negative stretch:** Decreases the size of the volume (downsampling). The content appears compressed.

    The operation is "symmetric" in terms of the input parameter: a stretch of `N` adds (or removes) `N` pixels to *both* ends of the axis, changing the total dimension by `2 * N`.

    Args:
        volume (np.ndarray): The input 3D volume (Z, Y, X).
        x_stretch (int, optional): Pixels added/removed per side along the X-axis. Total width changes by `2 * x_stretch`. Defaults to 0.
        y_stretch (int, optional): Pixels added/removed per side along the Y-axis. Defaults to 0.
        z_stretch (int, optional): Pixels added/removed per side along the Z-axis. Defaults to 0.
        order (int, optional): The order of spline interpolation.

            * **0**: Nearest-neighbor (preserves integer labels).
            * **1**: Linear interpolation (default, good for intensity data).
            * **2-5**: Higher-order splines.

    Returns:
        stretched_volume (np.ndarray):
            The resized volume.

    Raises:
        AssertionError: If the input volume is not 3D, `order` is invalid, or stretch values are not integers.

    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 geometric twist transformation to the volume around a central axis.

This function progressively rotates the slices of the volume along the specified axis, creating a spiral or screw-like distortion. The rotation angle increases linearly from 0 degrees at the start of the axis to rotation_angle at the end. This is often used for data augmentation to simulate torsional deformation in biological samples or materials.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume (Z, Y, X).

required
rotation_angle float

The total rotation in degrees applied from the bottom to the top of the axis. Defaults to 90.

90
axis str

The axis of rotation.

  • 'z': Rotates the XY planes around the Z-axis (default).
  • 'y': Rotates the XZ planes around the Y-axis.
  • 'x': Rotates the YZ planes around the X-axis.
'z'
order int

The order of spline interpolation used during resampling.

  • 0: Nearest-neighbor.
  • 1: Linear interpolation (default).
  • 2-5: Higher-order splines.
1

Returns:

Name Type Description
twisted_volume ndarray

The distorted volume.

Raises:

Type Description
AssertionError

If the input volume is not 3D, order is invalid, or axis is 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 geometric twist transformation to the volume around a central axis.

    This function progressively rotates the slices of the volume along the specified axis, creating a spiral or screw-like distortion. The rotation angle increases linearly from 0 degrees at the start of the axis to `rotation_angle` at the end. This is often used for data augmentation to simulate torsional deformation in biological samples or materials.

    Args:
        volume (np.ndarray): The input 3D volume (Z, Y, X).
        rotation_angle (float, optional): The total rotation in degrees applied from the bottom to the top of the axis. Defaults to 90.
        axis (str, optional): The axis of rotation.

            * **'z'**: Rotates the XY planes around the Z-axis (default).
            * **'y'**: Rotates the XZ planes around the Y-axis.
            * **'x'**: Rotates the YZ planes around the X-axis.

        order (int, optional): The order of spline interpolation used during resampling.

            * **0**: Nearest-neighbor.
            * **1**: Linear interpolation (default).
            * **2-5**: Higher-order splines.

    Returns:
        twisted_volume (np.ndarray):
            The distorted volume.

    Raises:
        AssertionError: If the input volume is not 3D, `order` is invalid, or `axis` is 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)

Extracts an arbitrary 2D plane (slice) from the 3D volume at a random orientation and position.

This function samples a rectangular region from the volume by defining a random plane vector and origin point. It effectively acts as a "virtual camera" placed inside the volume at a random angle. This is particularly useful for data augmentation in machine learning, allowing a model to see the 3D structure from diverse angles rather than just the fixed orthogonal (XY, XZ, YZ) views.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
width int

The width of the extracted slice in pixels.

required
length int

The length (height) of the extracted slice in pixels.

required
seed int

A seed for the random number generator. Providing a value ensures that the same random slice can be reproduced. Defaults to None.

None

Returns:

Name Type Description
slice2d ndarray

The extracted 2D image of shape (width, length).

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:
    """
    Extracts an arbitrary 2D plane (slice) from the 3D volume at a random orientation and position.

    This function samples a rectangular region from the volume by defining a random plane vector and origin point. It effectively acts as a "virtual camera" placed inside the volume at a random angle. This is particularly useful for data augmentation in machine learning, allowing a model to see the 3D structure from diverse angles rather than just the fixed orthogonal (XY, XZ, YZ) views.

    Args:
        volume (np.ndarray): The input 3D volume.
        width (int): The width of the extracted slice in pixels.
        length (int): The length (height) of the extracted slice in pixels.
        seed (int, optional): A seed for the random number generator. Providing a value ensures that the same random slice can be reproduced. Defaults to `None`.

    Returns:
        slice2d (np.ndarray):
            The extracted 2D image of shape `(width, length)`.

    !!! 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.operations.subsample

subsample(volume, coarseness)

Reduces the volume resolution by extracting every N'th voxel (strided slicing).

This function performs downsampling by selecting voxels at regular intervals defined by the coarseness factor. It is highly efficient because it returns a view of the original array rather than a copy. This means it consumes almost no additional memory, making it ideal for generating quick previews, thumbnails, or testing pipelines on large datasets without full processing.

Important notice

Since the returned object is a view, modifying the subsampled volume will also modify the original input volume.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
coarseness int or list[int]

The step size (stride) for sampling.

  • int: Applies the same step size to all axes (isotropic downsampling). A value of 1 returns the original volume. A value of 2 takes every second voxel.
  • list[int]: A list of 3 integers specifying the step size for each axis [z, y, x].
required

Returns:

Name Type Description
subsampled_volume ndarray

A view of the input volume with reduced dimensions.

Example

import qim3d
import numpy as np

# Create a sample volume
vol = np.zeros((100, 100, 100))

# Subsample by taking every 4th voxel
vol_small = qim3d.operations.subsample(vol, coarseness=4)

print(f"Original shape: {vol.shape}")
print(f"Subsampled shape: {vol_small.shape}")
Original shape: (100, 100, 100)

Subsampled shape: (25, 25, 25)

Source code in qim3d/operations/_slicing_operations.py
def subsample(volume: np.ndarray, coarseness: int | list[int]) -> np.ndarray:
    """
    Reduces the volume resolution by extracting every N'th voxel (strided slicing).

    This function performs downsampling by selecting voxels at regular intervals defined by the `coarseness` factor. It is highly efficient because it returns a **view** of the original array rather than a copy. This means it consumes almost no additional memory, making it ideal for generating quick previews, thumbnails, or testing pipelines on large datasets without full processing.

    !!! warning "Important notice"
        Since the returned object is a view, modifying the subsampled volume will also modify the original input volume.

    Args:
        volume (np.ndarray): The input 3D volume.
        coarseness (int or list[int]): The step size (stride) for sampling.

            * **int**: Applies the same step size to all axes (isotropic downsampling). A value of `1` returns the original volume. A value of `2` takes every second voxel.
            * **list[int]**: A list of 3 integers specifying the step size for each axis `[z, y, x]`.

    Returns:
        subsampled_volume (np.ndarray):
            A view of the input volume with reduced dimensions.

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

        # Create a sample volume
        vol = np.zeros((100, 100, 100))

        # Subsample by taking every 4th voxel
        vol_small = qim3d.operations.subsample(vol, coarseness=4)

        print(f"Original shape: {vol.shape}")
        print(f"Subsampled shape: {vol_small.shape}")
        ```
        Original shape: (100, 100, 100)

        Subsampled shape: (25, 25, 25)
    """
    if isinstance(coarseness, int):
        coarseness = tuple(coarseness for _ in range(3))

    vol_subsample = volume[tuple(slice(None, None, step) for step in coarseness)]
    ratio = vol_subsample.size / volume.size
    log.info(f'Subsampled volume has size {100*ratio:.3g}% of the original volume.')

    # User warnings
    min_elements = 1000
    min_axis_len = 5
    if vol_subsample.size < min_elements:
        log.info(
            f'User warning: less than {min_elements} elements in subsample. Consider using a lower coarseness for higher precision.'
        )
    elif np.min(vol_subsample.shape) < min_axis_len:
        log.info(
            f'User warning: subsampled volume contains an axis with size less than {min_axis_len}. Consider using a lower coarseness for higher precision.'
        )

    return vol_subsample

qim3d.operations.ratio_subsample

ratio_subsample(volume, ratio)

Subsamples the volume to retain a specific fraction of the original data.

This function automatically calculates the integer stride (step size) required to reduce the volume's total element count to approximately the requested ratio. Like subsample, it returns a view of the original array, making it extremely memory-efficient and fast for creating lightweight previews or managing large datasets.

Note: The exact ratio may not be achievable because the stride must be an integer. The function selects the stride that results in a size closest to the target.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
ratio float

The target fraction of elements to keep, where 0 < ratio <= 1. For example, 0.125 aims to keep 12.5% of the voxels (equivalent to a stride of 2 in 3D: 1/2^3 = 1/8).

required

Returns:

Name Type Description
subsampled_volume ndarray

A view of the input volume with reduced dimensions.

Example

import qim3d
import numpy as np

# Create a volume with 1 million voxels
vol = np.zeros((100, 100, 100))

# Subsample to keep ~1.5% of the data
# Ideally, stride = cbrt(1/0.015) ≈ 4.05 -> stride 4
vol_small = qim3d.operations.ratio_subsample(vol, ratio=0.015)

print(f"Original size: {vol.size}")
print(f"Subsampled size: {vol_small.size}")
print(f"Actual ratio: {vol_small.size / vol.size:.4f}")
Subsampled volume has size 1.56% of the original volume. Used a spacing of 4 in each axis.

Original size: 1000000

Subsampled size: 15625

Actual ratio: 0.0156

Source code in qim3d/operations/_slicing_operations.py
def ratio_subsample(volume: np.ndarray, ratio: float) -> np.ndarray:
    """
    Subsamples the volume to retain a specific fraction of the original data.

    This function automatically calculates the integer stride (step size) required to reduce the volume's total element count to approximately the requested `ratio`. Like `subsample`, it returns a **view** of the original array, making it extremely memory-efficient and fast for creating lightweight previews or managing large datasets.

    **Note:** The exact ratio may not be achievable because the stride must be an integer. The function selects the stride that results in a size closest to the target.

    Args:
        volume (np.ndarray): The input 3D volume.
        ratio (float): The target fraction of elements to keep, where 0 < ratio <= 1. For example, `0.125` aims to keep 12.5% of the voxels (equivalent to a stride of 2 in 3D: 1/2^3 = 1/8).

    Returns:
        subsampled_volume (np.ndarray):
            A view of the input volume with reduced dimensions.

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

        # Create a volume with 1 million voxels
        vol = np.zeros((100, 100, 100))

        # Subsample to keep ~1.5% of the data
        # Ideally, stride = cbrt(1/0.015) ≈ 4.05 -> stride 4
        vol_small = qim3d.operations.ratio_subsample(vol, ratio=0.015)

        print(f"Original size: {vol.size}")
        print(f"Subsampled size: {vol_small.size}")
        print(f"Actual ratio: {vol_small.size / vol.size:.4f}")
        ```
        Subsampled volume has size 1.56% of the original volume. Used a spacing of 4 in each axis.

        Original size: 1000000

        Subsampled size: 15625

        Actual ratio: 0.0156
    """
    def calc_ratio(vol: np.ndarray, stride: int) -> float:
        """Compute the achieved ratio given a stride value."""
        shape = np.array(vol.shape)
        steps_per_dim = np.floor((shape - 1) / stride) + 1
        return np.prod(steps_per_dim) / vol.size

    # Estimate ideal stride assuming perfect cube relationship: 1 / stride**3 ~= ratio
    # Here stride is the number of elements and not the number of bytes.
    float_stride = np.power(1 / ratio, 1 / 3)

    if float_stride.is_integer():
        stride = int(float_stride)
    else:
        stride_below = int(np.floor(float_stride))
        stride_above = int(np.ceil(float_stride))
        ratio_below = calc_ratio(volume, stride_below)
        ratio_above = calc_ratio(volume, stride_above)
        # Pick the stride yielding ratio closer to the target
        stride = stride_above if abs(ratio_above - ratio) < abs(ratio_below - ratio) else stride_below

    vol_subsample = volume[::stride, ::stride, ::stride]
    actual_ratio = vol_subsample.size / volume.size
    log.info(f'Subsampled volume has size {100*actual_ratio:.3g}% of the original volume. Used a spacing of {stride} in each axis.')

    return vol_subsample

Morphological operations for volumetric data.

qim3d.morphology.dilate

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

Performs morphological dilation on a 3D volume using CPU or GPU-accelerated methods.

Dilation enlarges bright regions (foreground) and shrinks dark regions (background). It is commonly used to close small holes, connect disjoint features, or thicken object boundaries.

This function supports efficient GPU acceleration using the pygorpho library, based on zonohedral approximations. If a GPU is not available, it is recommended to use the 'scipy.ndimage' method.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
kernel int or ndarray

The structuring element. * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel. * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.

required
method str

The backend implementation to use. Defaults to 'pygorpho.linear'. * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels. * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes. * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).

'pygorpho.linear'
**kwargs Any

Additional keyword arguments passed to the underlying method.

{}

Returns:

Name Type Description
dilated_vol ndarray

The dilated volume.

Reference

The GPU methods implement the algorithms described in: Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology.

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(
    volume: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Performs morphological dilation on a 3D volume using CPU or GPU-accelerated methods.

    Dilation enlarges bright regions (foreground) and shrinks dark regions (background). It is commonly used to close small holes, connect disjoint features, or thicken object boundaries.

    This function supports efficient GPU acceleration using the `pygorpho` library, based on zonohedral approximations. If a GPU is not available, it is recommended to use the 'scipy.ndimage' method.

    Args:
        volume (np.ndarray): The input 3D volume.
        kernel (int or np.ndarray): The structuring element.
            * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel.
            * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.
        method (str, optional): The backend implementation to use. Defaults to 'pygorpho.linear'.
            * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels.
            * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes.
            * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).
        **kwargs (Any): Additional keyword arguments passed to the underlying method.

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

    !!! quote "Reference"
        The GPU methods implement the algorithms described in:
        [Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf).

    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:
        volume = np.asarray(volume)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(volume.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(volume, 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(volume, linesteps, linelens)

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

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

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

qim3d.morphology.erode

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

Performs morphological erosion on a 3D volume using CPU or GPU-accelerated methods.

Erosion shrinks bright regions (foreground) and enlarges dark regions (background). It is commonly used to remove small noise (salt noise), detach touching objects, or thin out features.

This function supports efficient GPU acceleration using the pygorpho library. If a GPU is not available, it is recommended to use the 'scipy.ndimage' method.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
kernel int or ndarray

The structuring element. * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel. * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.

required
method str

The backend implementation to use. Defaults to 'pygorpho.linear'. * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels. * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes. * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).

'pygorpho.linear'
**kwargs Any

Additional keyword arguments passed to the underlying method.

{}

Returns:

Name Type Description
eroded_vol ndarray

The eroded volume.

Reference

The GPU methods implement the algorithms described in: Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology.

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(
    volume: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Performs morphological erosion on a 3D volume using CPU or GPU-accelerated methods.

    Erosion shrinks bright regions (foreground) and enlarges dark regions (background). It is commonly used to remove small noise (salt noise), detach touching objects, or thin out features.

    This function supports efficient GPU acceleration using the `pygorpho` library. If a GPU is not available, it is recommended to use the 'scipy.ndimage' method.

    Args:
        volume (np.ndarray): The input 3D volume.
        kernel (int or np.ndarray): The structuring element.
            * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel.
            * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.
        method (str, optional): The backend implementation to use. Defaults to 'pygorpho.linear'.
            * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels.
            * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes.
            * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).
        **kwargs (Any): Additional keyword arguments passed to the underlying method.

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

    !!! quote "Reference"
        The GPU methods implement the algorithms described in:
        [Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf).

    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:
        volume = np.asarray(volume)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(volume.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(volume, 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(volume, 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(volume, footprint=kernel, **kwargs)

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

qim3d.morphology.opening

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

Performs morphological opening on a 3D volume using CPU or GPU-accelerated methods.

Opening is defined as an erosion followed by a dilation. It is primarily used to remove small bright objects (salt noise) from the background while preserving the shape and size of larger objects. It smooths object contours by breaking narrow isthmuses and eliminating thin protrusions.

This function supports efficient GPU acceleration using the pygorpho library. If a GPU is not available, it is recommended to use the scipy.ndimage method.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
kernel int or ndarray

The structuring element. * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel. * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.

required
method str

The backend implementation to use. Defaults to 'pygorpho.linear'. * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels. * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes. * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).

'pygorpho.linear'
**kwargs Any

Additional keyword arguments passed to the underlying method.

{}

Returns:

Name Type Description
opened_vol ndarray

The opened volume.

Reference

The GPU methods implement the algorithms described in: Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology.

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(
    volume: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Performs morphological opening on a 3D volume using CPU or GPU-accelerated methods.

    Opening is defined as an **erosion** followed by a **dilation**. It is primarily used to remove small bright objects (salt noise) from the background while preserving the shape and size of larger objects. It smooths object contours by breaking narrow isthmuses and eliminating thin protrusions.

    This function supports efficient GPU acceleration using the `pygorpho` library. If a GPU is not available, it is recommended to use the [scipy.ndimage](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.grey_dilation.html) method.

    Args:
        volume (np.ndarray): The input 3D volume.
        kernel (int or np.ndarray): The structuring element.
            * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel.
            * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.
        method (str, optional): The backend implementation to use. Defaults to 'pygorpho.linear'.
            * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels.
            * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes.
            * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).
        **kwargs (Any): Additional keyword arguments passed to the underlying method.

    Returns:
        opened_vol (np.ndarray):
            The opened volume.

    !!! quote "Reference"
        The GPU methods implement the algorithms described in:
        [Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf).

    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:
        volume = np.asarray(volume)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(volume.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(volume, 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(volume, 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(volume, footprint=kernel, **kwargs)

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

qim3d.morphology.closing

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

Performs morphological closing on a 3D volume using CPU or GPU-accelerated methods.

Closing is defined as a dilation followed by an erosion. It is primarily used to fill small dark holes, cracks, or gaps within bright objects while preserving their overall shape and size. It smooths object contours by fusing narrow breaks and filling small depressions.

This function supports efficient GPU acceleration using the pygorpho library. If a GPU is not available, it is recommended to use the scipy.ndimage method.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
kernel int or ndarray

The structuring element. * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel. * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.

required
method str

The backend implementation to use. Defaults to 'pygorpho.linear'. * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels. * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes. * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).

'pygorpho.linear'
**kwargs Any

Additional keyword arguments passed to the underlying method.

{}

Returns:

Name Type Description
closed_vol ndarray

The closed volume.

Reference

The GPU methods implement the algorithms described in: Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology.

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(
    volume: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Performs morphological closing on a 3D volume using CPU or GPU-accelerated methods.

    Closing is defined as a **dilation** followed by an **erosion**. It is primarily used to fill small dark holes, cracks, or gaps within bright objects while preserving their overall shape and size. It smooths object contours by fusing narrow breaks and filling small depressions.

    This function supports efficient GPU acceleration using the `pygorpho` library. If a GPU is not available, it is recommended to use the [scipy.ndimage](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.grey_dilation.html) method.

    Args:
        volume (np.ndarray): The input 3D volume.
        kernel (int or np.ndarray): The structuring element.
            * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel.
            * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.
        method (str, optional): The backend implementation to use. Defaults to 'pygorpho.linear'.
            * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels.
            * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes.
            * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).
        **kwargs (Any): Additional keyword arguments passed to the underlying method.

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

    !!! quote "Reference"
        The GPU methods implement the algorithms described in:
        [Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf).

    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:
        volume = np.asarray(volume)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(volume.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(volume, 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(volume, 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(volume, footprint=kernel, **kwargs)

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

qim3d.morphology.black_tophat

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

Performs the black top-hat transform on a 3D volume.

The black top-hat transform is defined as the difference between the morphological closing of the volume and the original volume (Closing - Input). It is used to extract dark features and valleys that are smaller than the structuring element (kernel) from a brighter background. This is particularly effective for background correction or isolating small dark structures in a non-uniformly lit image.

This function supports efficient GPU acceleration using the pygorpho library. If a GPU is not available, it is recommended to use the scipy.ndimage method.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
kernel int or ndarray

The structuring element. * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel. * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.

required
method str

The backend implementation to use. Defaults to 'pygorpho.linear'. * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels. * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes. * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).

'pygorpho.linear'
**kwargs Any

Additional keyword arguments passed to the underlying method.

{}

Returns:

Name Type Description
bothat_vol ndarray

The processed volume containing the extracted dark features.

Reference

The GPU methods implement the algorithms described in: Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology.

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 black top-hat to extract dark details
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(
    volume: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Performs the black top-hat transform on a 3D volume.

    The black top-hat transform is defined as the difference between the morphological closing of the volume and the original volume (Closing - Input). It is used to extract dark features and valleys that are smaller than the structuring element (kernel) from a brighter background. This is particularly effective for background correction or isolating small dark structures in a non-uniformly lit image.

    This function supports efficient GPU acceleration using the `pygorpho` library. If a GPU is not available, it is recommended to use the [scipy.ndimage](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.black_tophat.html) method.

    Args:
        volume (np.ndarray): The input 3D volume.
        kernel (int or np.ndarray): The structuring element.
            * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel.
            * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.
        method (str, optional): The backend implementation to use. Defaults to 'pygorpho.linear'.
            * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels.
            * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes.
            * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).
        **kwargs (Any): Additional keyword arguments passed to the underlying method.

    Returns:
        bothat_vol (np.ndarray):
            The processed volume containing the extracted dark features.

    !!! quote "Reference"
        The GPU methods implement the algorithms described in:
        [Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf).

    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 black top-hat to extract dark details
        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:
        volume = np.asarray(volume)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(volume.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(volume, 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(volume, 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(volume, footprint=kernel, **kwargs)

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

qim3d.morphology.white_tophat

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

Performs the white top-hat transform on a 3D volume.

The white top-hat transform is defined as the difference between the original volume and its morphological opening (Input - Opening). It is used to extract bright features and peaks that are smaller than the structuring element (kernel) from a darker background. This is a powerful tool for background subtraction, enhancing small bright spots, or correcting uneven illumination in a volume.

This function supports efficient GPU acceleration using the pygorpho library. If a GPU is not available, it is recommended to use the scipy.ndimage method.

Parameters:

Name Type Description Default
volume ndarray

The input 3D volume.

required
kernel int or ndarray

The structuring element. * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel. * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.

required
method str

The backend implementation to use. Defaults to 'pygorpho.linear'. * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels. * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes. * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).

'pygorpho.linear'
**kwargs Any

Additional keyword arguments passed to the underlying method.

{}

Returns:

Name Type Description
tophat_vol ndarray

The processed volume containing the extracted bright features.

Reference

The GPU methods implement the algorithms described in: Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology.

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 white top-hat to extract bright details
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(
    volume: np.ndarray, kernel: int | np.ndarray, method: str = 'pygorpho.linear', **kwargs
) -> np.ndarray:
    """
    Performs the white top-hat transform on a 3D volume.

    The white top-hat transform is defined as the difference between the original volume and its morphological opening (Input - Opening). It is used to extract bright features and peaks that are smaller than the structuring element (kernel) from a darker background. This is a powerful tool for background subtraction, enhancing small bright spots, or correcting uneven illumination in a volume.

    This function supports efficient GPU acceleration using the `pygorpho` library. If a GPU is not available, it is recommended to use the [scipy.ndimage](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.white_tophat.html) method.

    Args:
        volume (np.ndarray): The input 3D volume.
        kernel (int or np.ndarray): The structuring element.
            * If method is 'pygorpho.linear': Must be an integer representing the radius of the ball-shaped kernel.
            * If method is 'pygorpho.flat' or 'scipy.ndimage': Must be a 3D numpy array defining the footprint.
        method (str, optional): The backend implementation to use. Defaults to 'pygorpho.linear'.
            * 'pygorpho.linear': GPU-accelerated. Best for large, spherical kernels.
            * 'pygorpho.flat': GPU-accelerated. Supports arbitrary kernel shapes.
            * 'scipy.ndimage': CPU-based. Standard implementation (slower for large volumes).
        **kwargs (Any): Additional keyword arguments passed to the underlying method.

    Returns:
        tophat_vol (np.ndarray):
            The processed volume containing the extracted bright features.

    !!! quote "Reference"
        The GPU methods implement the algorithms described in:
        [Zonohedral Approximation of Spherical Structuring Element for Volumetric Morphology](https://backend.orbit.dtu.dk/ws/portalfiles/portal/172879029/SCIA19_Zonohedra.pdf).

    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 white top-hat to extract bright details
        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:
        volume = np.asarray(volume)
    except TypeError as e:
        err = 'Input volume must be array-like.'
        raise TypeError(err) from e

    assert len(volume.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(volume, 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(volume, 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(volume, footprint=kernel, **kwargs)

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