Skip to content

Synthetic data generation

qim3d.generate

Generation for synthetic datasets.

qim3d.generate.volume

volume(base_shape=(128, 128, 128), final_shape=(128, 128, 128), noise_scale=0.05, order=1, gamma=1.0, max_value=255, threshold=0.5, smooth_borders=False, volume_shape=None, dtype='uint8')

Generate a 3D volume with Perlin noise, spherical gradient, and optional scaling and gamma correction.

Parameters:

Name Type Description Default
base_shape tuple of ints

Shape of the initial volume to generate. Defaults to (128, 128, 128).

(128, 128, 128)
final_shape tuple of ints

Desired shape of the final volume. Defaults to (128, 128, 128).

(128, 128, 128)
noise_scale float

Scale factor for Perlin noise. Defaults to 0.05.

0.05
order int

Order of the spline interpolation used in resizing. Defaults to 1.

1
gamma float

Gamma correction factor. Defaults to 1.0.

1.0
max_value int

Maximum value for the volume intensity. Defaults to 255.

255
threshold float

Threshold value for clipping low intensity values. Defaults to 0.5.

0.5
smooth_borders bool

Flag for automatic computation of the threshold value to ensure a blob with no straight edges. If True, the threshold parameter is ignored. Defaults to False.

False
volume_shape str

Shape of the volume to generate, either "cylinder", or "tube". Defaults to None.

None
dtype data - type

Desired data type of the output volume. Defaults to "uint8".

'uint8'

Returns:

Name Type Description
volume ndarray

Generated 3D volume with specified parameters.

Raises:

Type Description
TypeError

If final_shape is not a tuple or does not have three elements.

ValueError

If dtype is not a valid numpy number type.

Example
import qim3d

# Generate synthetic blob
vol = qim3d.generate.volume(noise_scale = 0.015)

# Visualize 3D volume
qim3d.viz.volumetric(vol)

# Visualize slices
qim3d.viz.slices_grid(vol, value_min = 0, value_max = 255, num_slices = 15)
synthetic_blob

Example
import qim3d

# Generate tubular synthetic blob
vol = qim3d.generate.volume(base_shape = (10, 300, 300),
                        final_shape = (100, 100, 100),
                        noise_scale = 0.3,
                        gamma = 2,
                        threshold = 0.0,
                        volume_shape = "cylinder"
                        )

# Visualize synthetic volume
qim3d.viz.volumetric(vol)

# Visualize slices
qim3d.viz.slices_grid(vol, num_slices=15, slice_axis=1)
synthetic_blob_cylinder_slice

Example
import qim3d

# Generate tubular synthetic blob
vol = qim3d.generate.volume(base_shape = (200, 100, 100),
                        final_shape = (400, 100, 100),
                        noise_scale = 0.03,
                        gamma = 0.12,
                        threshold = 0.85,
                        volume_shape = "tube"
                        )

# Visualize synthetic blob
qim3d.viz.volumetric(vol)

# Visualize
qim3d.viz.slices_grid(vol, num_slices=15)
synthetic_blob_tube_slice

Source code in qim3d/generate/_generators.py
def volume(
    base_shape: tuple = (128, 128, 128),
    final_shape: tuple = (128, 128, 128),
    noise_scale: float = 0.05,
    order: int = 1,
    gamma: int = 1.0,
    max_value: int = 255,
    threshold: float = 0.5,
    smooth_borders: bool = False,
    volume_shape: str = None,
    dtype: str = 'uint8',
) -> np.ndarray:
    """
    Generate a 3D volume with Perlin noise, spherical gradient, and optional scaling and gamma correction.

    Args:
        base_shape (tuple of ints, optional): Shape of the initial volume to generate. Defaults to (128, 128, 128).
        final_shape (tuple of ints, optional): Desired shape of the final volume. Defaults to (128, 128, 128).
        noise_scale (float, optional): Scale factor for Perlin noise. Defaults to 0.05.
        order (int, optional): Order of the spline interpolation used in resizing. Defaults to 1.
        gamma (float, optional): Gamma correction factor. Defaults to 1.0.
        max_value (int, optional): Maximum value for the volume intensity. Defaults to 255.
        threshold (float, optional): Threshold value for clipping low intensity values. Defaults to 0.5.
        smooth_borders (bool, optional): Flag for automatic computation of the threshold value to ensure a blob with no straight edges. If True, the `threshold` parameter is ignored. Defaults to False.
        volume_shape (str, optional): Shape of the volume to generate, either "cylinder", or "tube". Defaults to None.
        dtype (data-type, optional): Desired data type of the output volume. Defaults to "uint8".

    Returns:
        volume (numpy.ndarray): Generated 3D volume with specified parameters.

    Raises:
        TypeError: If `final_shape` is not a tuple or does not have three elements.
        ValueError: If `dtype` is not a valid numpy number type.

    Example:
        ```python
        import qim3d

        # Generate synthetic blob
        vol = qim3d.generate.volume(noise_scale = 0.015)

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

        ```python
        # Visualize slices
        qim3d.viz.slices_grid(vol, value_min = 0, value_max = 255, num_slices = 15)
        ```
        ![synthetic_blob](../../assets/screenshots/synthetic_blob_slices.png)

    Example:
        ```python
        import qim3d

        # Generate tubular synthetic blob
        vol = qim3d.generate.volume(base_shape = (10, 300, 300),
                                final_shape = (100, 100, 100),
                                noise_scale = 0.3,
                                gamma = 2,
                                threshold = 0.0,
                                volume_shape = "cylinder"
                                )

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

        ```python
        # Visualize slices
        qim3d.viz.slices_grid(vol, num_slices=15, slice_axis=1)
        ```
        ![synthetic_blob_cylinder_slice](../../assets/screenshots/synthetic_blob_cylinder_slice.png)

    Example:
        ```python
        import qim3d

        # Generate tubular synthetic blob
        vol = qim3d.generate.volume(base_shape = (200, 100, 100),
                                final_shape = (400, 100, 100),
                                noise_scale = 0.03,
                                gamma = 0.12,
                                threshold = 0.85,
                                volume_shape = "tube"
                                )

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

        ```python
        # Visualize
        qim3d.viz.slices_grid(vol, num_slices=15)
        ```
        ![synthetic_blob_tube_slice](../../assets/screenshots/synthetic_blob_tube_slice.png)

    """

    if not isinstance(final_shape, tuple) or len(final_shape) != 3:
        message = 'Size must be a tuple of 3 dimensions'
        raise TypeError(message)
    if not np.issubdtype(dtype, np.number):
        message = 'Invalid data type'
        raise ValueError(message)

    # Initialize the 3D array for the shape
    volume = np.empty((base_shape[0], base_shape[1], base_shape[2]), dtype=np.float32)

    # Generate grid of coordinates
    z, y, x = np.indices(base_shape)

    # Calculate the distance from the center of the shape
    center = np.array(base_shape) / 2

    dist = np.sqrt((z - center[0]) ** 2 + (y - center[1]) ** 2 + (x - center[2]) ** 2)

    dist /= np.sqrt(3 * (center[0] ** 2))

    # Generate Perlin noise and adjust the values based on the distance from the center
    vectorized_pnoise3 = np.vectorize(
        pnoise3
    )  # Vectorize pnoise3, since it only takes scalar input

    noise = vectorized_pnoise3(
        z.flatten() * noise_scale, y.flatten() * noise_scale, x.flatten() * noise_scale
    ).reshape(base_shape)

    volume = (1 + noise) * (1 - dist)

    # Normalize
    volume = (volume - np.min(volume)) / (np.max(volume) - np.min(volume))

    # Gamma correction
    volume = np.power(volume, gamma)

    # Scale the volume to the maximum value
    volume = volume * max_value

    # If volume shape is specified, smooth borders are disabled
    if volume_shape:
        smooth_borders = False

    if smooth_borders:
        # Maximum value among the six sides of the 3D volume
        max_border_value = np.max(
            [
                np.max(volume[0, :, :]),
                np.max(volume[-1, :, :]),
                np.max(volume[:, 0, :]),
                np.max(volume[:, -1, :]),
                np.max(volume[:, :, 0]),
                np.max(volume[:, :, -1]),
            ]
        )

        # Compute threshold such that there will be no straight cuts in the blob
        threshold = max_border_value / max_value

    # Clip the low values of the volume to create a coherent volume
    volume[volume < threshold * max_value] = 0

    # Clip high values
    volume[volume > max_value] = max_value

    # Scale up the volume of volume to size
    volume = scipy.ndimage.zoom(
        volume, np.array(final_shape) / np.array(base_shape), order=order
    )

    # Fade into a shape if specified
    if volume_shape == 'cylinder':
        # Arguments for the fade_mask function
        geometry = 'cylindrical'  # Fade in cylindrical geometry
        axis = np.argmax(
            volume.shape
        )  # Fade along the dimension where the volume is the largest
        target_max_normalized_distance = (
            1.4  # This value ensures that the volume will become cylindrical
        )

        volume = qim3d.operations.fade_mask(
            volume,
            geometry=geometry,
            axis=axis,
            target_max_normalized_distance=target_max_normalized_distance,
        )

    elif volume_shape == 'tube':
        # Arguments for the fade_mask function
        geometry = 'cylindrical'  # Fade in cylindrical geometry
        axis = np.argmax(
            volume.shape
        )  # Fade along the dimension where the volume is the largest
        decay_rate = 5  # Decay rate for the fade operation
        target_max_normalized_distance = (
            1.4  # This value ensures that the volume will become cylindrical
        )

        # Fade once for making the volume cylindrical
        volume = qim3d.operations.fade_mask(
            volume,
            geometry=geometry,
            axis=axis,
            decay_rate=decay_rate,
            target_max_normalized_distance=target_max_normalized_distance,
            invert=False,
        )

        # Fade again with invert = True for making the volume a tube (i.e. with a hole in the middle)
        volume = qim3d.operations.fade_mask(
            volume, geometry=geometry, axis=axis, decay_rate=decay_rate, invert=True
        )

    # Convert to desired data type
    volume = volume.astype(dtype)

    return volume

qim3d.generate.volume_collection

volume_collection(collection_shape=(200, 200, 200), num_volumes=15, positions=None, min_shape=(40, 40, 40), max_shape=(60, 60, 60), volume_shape_zoom=(1.0, 1.0, 1.0), min_volume_noise=0.02, max_volume_noise=0.05, min_rotation_degrees=0, max_rotation_degrees=360, rotation_axes=None, min_gamma=0.8, max_gamma=1.2, min_high_value=128, max_high_value=255, min_threshold=0.5, max_threshold=0.6, smooth_borders=False, volume_shape=None, seed=0, verbose=False)

Generate a 3D volume of multiple synthetic volumes using Perlin noise.

Parameters:

Name Type Description Default
collection_shape tuple of ints

Shape of the final collection volume to generate. Defaults to (200, 200, 200).

(200, 200, 200)
num_volumes int

Number of synthetic volumes to include in the collection. Defaults to 15.

15
positions list[tuple]

List of specific positions as (z, y, x) coordinates for the volumes. If not provided, they are placed randomly into the collection. Defaults to None.

None
min_shape tuple of ints

Minimum shape of the volumes. Defaults to (40, 40, 40).

(40, 40, 40)
max_shape tuple of ints

Maximum shape of the volumes. Defaults to (60, 60, 60).

(60, 60, 60)
volume_shape_zoom tuple of floats

Scaling factors for each dimension of each volume. Defaults to (1.0, 1.0, 1.0).

(1.0, 1.0, 1.0)
min_volume_noise float

Minimum scale factor for Perlin noise. Defaults to 0.02.

0.02
max_volume_noise float

Maximum scale factor for Perlin noise. Defaults to 0.05.

0.05
min_rotation_degrees int

Minimum rotation angle in degrees. Defaults to 0.

0
max_rotation_degrees int

Maximum rotation angle in degrees. Defaults to 360.

360
rotation_axes list[tuple]

List of axis pairs that will be randomly chosen to rotate around. Defaults to [(0, 1), (0, 2), (1, 2)].

None
min_gamma float

Minimum gamma correction factor. Defaults to 0.8.

0.8
max_gamma float

Maximum gamma correction factor. Defaults to 1.2.

1.2
min_high_value int

Minimum maximum value for the volume intensity. Defaults to 128.

128
max_high_value int

Maximum maximum value for the volume intensity. Defaults to 255.

255
min_threshold float

Minimum threshold value for clipping low intensity values. Defaults to 0.5.

0.5
max_threshold float

Maximum threshold value for clipping low intensity values. Defaults to 0.6.

0.6
smooth_borders bool

Flag for smoothing volume borders to avoid straight edges in the volumes. If True, the min_threshold and max_threshold parameters are ignored. Defaults to False.

False
volume_shape str or None

Shape of the volume to generate, either "cylinder", or "tube". Defaults to None.

None
seed int

Seed for reproducibility. Defaults to 0.

0
verbose bool

Flag to enable verbose logging. Defaults to False.

False

Returns:

Name Type Description
volume_collection ndarray

3D volume of the generated collection of synthetic volumes with specified parameters.

labels ndarray

Array with labels for each voxel, same shape as volume_collection.

Raises:

Type Description
TypeError

If collection_shape is not 3D.

ValueError

If volume parameters are invalid.

Note
  • The function places volumes without overlap.
  • The function can either place volumes at random positions in the collection (if positions = None) or at specific positions provided in the positions argument. If specific positions are provided, the number of blobs must match the number of positions (e.g. num_volumes = 2 with positions = [(12, 8, 10), (24, 20, 18)]).
  • If not all num_volumes can be placed, the function returns the volume_collection volume with as many volumes as possible in it, and logs an error.
  • Labels for all volumes are returned, even if they are not a single connected component.
Example
import qim3d

# Generate synthetic collection of volumes
num_volumes = 15
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = num_volumes)

# Visualize the collection
qim3d.viz.volumetric(volume_collection)

qim3d.viz.slicer(volume_collection)
synthetic_collection

# Visualize labels
cmap = qim3d.viz.colormaps.segmentation(num_labels=num_volumes)
qim3d.viz.slicer(labels, color_map=cmap, value_max=num_volumes)
synthetic_collection

Example
import qim3d

# Generate synthetic collection of dense objects
vol, labels = qim3d.generate.volume_collection(
    min_high_value = 255,
    max_high_value = 255,
    min_volume_noise = 0.05,
    max_volume_noise = 0.05,
    min_threshold = 0.99,
    max_threshold = 0.99,
    min_gamma = 0.02,
    max_gamma = 0.02
    )

# Visualize the collection
qim3d.viz.vol(volume_collection)
Example
import qim3d

# Generate synthetic collection of cylindrical structures
volume_collection, labels = qim3d.generate.volume_collection(
    num_volumes = 40,
    collection_shape = (300, 150, 150),
    min_shape = (280, 10, 10),
    max_shape = (290, 15, 15),
    min_volume_noise = 0.08,
    max_volume_noise = 0.09,
    max_rotation_degrees = 5,
    min_threshold = 0.7,
    max_threshold = 0.9,
    min_gamma = 0.10,
    max_gamma = 0.11,
    volume_shape = "cylinder"
    )

# Visualize the collection
qim3d.viz.volumetric(volume_collection)

# Visualize slices
qim3d.viz.slices_grid(volume_collection, num_slices=15)
synthetic_collection_cylinder

Example
import qim3d

# Generate synthetic collection of tubular (hollow) structures
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 10,
                                        collection_shape = (200, 200, 200),
                                        min_shape = (180, 25, 25),
                                        max_shape = (190, 35, 35),
                                        min_volume_noise = 0.02,
                                        max_volume_noise = 0.03,
                                        max_rotation_degrees = 5,
                                        min_threshold = 0.7,
                                        max_threshold = 0.9,
                                        min_gamma = 0.10,
                                        max_gamma = 0.11,
                                        volume_shape = "tube"
                                        )

# Visualize the collection
qim3d.viz.volumetric(volume_collection)

# Visualize slices
qim3d.viz.slices_grid(volume_collection, num_slices=15, slice_axis=1)
synthetic_collection_tube

Source code in qim3d/generate/_aggregators.py
def volume_collection(
    collection_shape: tuple = (200, 200, 200),
    num_volumes: int = 15,
    positions: list[tuple] = None,
    min_shape: tuple = (40, 40, 40),
    max_shape: tuple = (60, 60, 60),
    volume_shape_zoom: tuple = (1.0, 1.0, 1.0),
    min_volume_noise: float = 0.02,
    max_volume_noise: float = 0.05,
    min_rotation_degrees: int = 0,
    max_rotation_degrees: int = 360,
    rotation_axes: list[tuple] = None,
    min_gamma: float = 0.8,
    max_gamma: float = 1.2,
    min_high_value: int = 128,
    max_high_value: int = 255,
    min_threshold: float = 0.5,
    max_threshold: float = 0.6,
    smooth_borders: bool = False,
    volume_shape: str = None,
    seed: int = 0,
    verbose: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
    """
    Generate a 3D volume of multiple synthetic volumes using Perlin noise.

    Args:
        collection_shape (tuple of ints, optional): Shape of the final collection volume to generate. Defaults to (200, 200, 200).
        num_volumes (int, optional): Number of synthetic volumes to include in the collection. Defaults to 15.
        positions (list[tuple], optional): List of specific positions as (z, y, x) coordinates for the volumes. If not provided, they are placed randomly into the collection. Defaults to None.
        min_shape (tuple of ints, optional): Minimum shape of the volumes. Defaults to (40, 40, 40).
        max_shape (tuple of ints, optional): Maximum shape of the volumes. Defaults to (60, 60, 60).
        volume_shape_zoom (tuple of floats, optional): Scaling factors for each dimension of each volume. Defaults to (1.0, 1.0, 1.0).
        min_volume_noise (float, optional): Minimum scale factor for Perlin noise. Defaults to 0.02.
        max_volume_noise (float, optional): Maximum scale factor for Perlin noise. Defaults to 0.05.
        min_rotation_degrees (int, optional): Minimum rotation angle in degrees. Defaults to 0.
        max_rotation_degrees (int, optional): Maximum rotation angle in degrees. Defaults to 360.
        rotation_axes (list[tuple], optional): List of axis pairs that will be randomly chosen to rotate around. Defaults to [(0, 1), (0, 2), (1, 2)].
        min_gamma (float, optional): Minimum gamma correction factor. Defaults to 0.8.
        max_gamma (float, optional): Maximum gamma correction factor. Defaults to 1.2.
        min_high_value (int, optional): Minimum maximum value for the volume intensity. Defaults to 128.
        max_high_value (int, optional): Maximum maximum value for the volume intensity. Defaults to 255.
        min_threshold (float, optional): Minimum threshold value for clipping low intensity values. Defaults to 0.5.
        max_threshold (float, optional): Maximum threshold value for clipping low intensity values. Defaults to 0.6.
        smooth_borders (bool, optional): Flag for smoothing volume borders to avoid straight edges in the volumes. If True, the `min_threshold` and `max_threshold` parameters are ignored. Defaults to False.
        volume_shape (str or None, optional): Shape of the volume to generate, either "cylinder", or "tube". Defaults to None.
        seed (int, optional): Seed for reproducibility. Defaults to 0.
        verbose (bool, optional): Flag to enable verbose logging. Defaults to False.

    Returns:
        volume_collection (numpy.ndarray): 3D volume of the generated collection of synthetic volumes with specified parameters.
        labels (numpy.ndarray): Array with labels for each voxel, same shape as volume_collection.

    Raises:
        TypeError: If `collection_shape` is not 3D.
        ValueError: If volume parameters are invalid.

    Note:
        - The function places volumes without overlap.
        - The function can either place volumes at random positions in the collection (if `positions = None`) or at specific positions provided in the `positions` argument. If specific positions are provided, the number of blobs must match the number of positions (e.g. `num_volumes = 2` with `positions = [(12, 8, 10), (24, 20, 18)]`).
        - If not all `num_volumes` can be placed, the function returns the `volume_collection` volume with as many volumes as possible in it, and logs an error.
        - Labels for all volumes are returned, even if they are not a single connected component.

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        num_volumes = 15
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = num_volumes)

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

        ```python
        qim3d.viz.slicer(volume_collection)
        ```
        ![synthetic_collection](../../assets/screenshots/synthetic_collection_default.gif)

        ```python
        # Visualize labels
        cmap = qim3d.viz.colormaps.segmentation(num_labels=num_volumes)
        qim3d.viz.slicer(labels, color_map=cmap, value_max=num_volumes)
        ```
        ![synthetic_collection](../../assets/screenshots/synthetic_collection_default_labels.gif)

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of dense objects
        vol, labels = qim3d.generate.volume_collection(
            min_high_value = 255,
            max_high_value = 255,
            min_volume_noise = 0.05,
            max_volume_noise = 0.05,
            min_threshold = 0.99,
            max_threshold = 0.99,
            min_gamma = 0.02,
            max_gamma = 0.02
            )

        # Visualize the collection
        qim3d.viz.vol(volume_collection)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_collection_dense.html" width="100%" height="500" frameborder="0"></iframe>

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of cylindrical structures
        volume_collection, labels = qim3d.generate.volume_collection(
            num_volumes = 40,
            collection_shape = (300, 150, 150),
            min_shape = (280, 10, 10),
            max_shape = (290, 15, 15),
            min_volume_noise = 0.08,
            max_volume_noise = 0.09,
            max_rotation_degrees = 5,
            min_threshold = 0.7,
            max_threshold = 0.9,
            min_gamma = 0.10,
            max_gamma = 0.11,
            volume_shape = "cylinder"
            )

        # Visualize the collection
        qim3d.viz.volumetric(volume_collection)

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

        ```python
        # Visualize slices
        qim3d.viz.slices_grid(volume_collection, num_slices=15)
        ```
        ![synthetic_collection_cylinder](../../assets/screenshots/synthetic_collection_cylinder_slices.png)

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of tubular (hollow) structures
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 10,
                                                collection_shape = (200, 200, 200),
                                                min_shape = (180, 25, 25),
                                                max_shape = (190, 35, 35),
                                                min_volume_noise = 0.02,
                                                max_volume_noise = 0.03,
                                                max_rotation_degrees = 5,
                                                min_threshold = 0.7,
                                                max_threshold = 0.9,
                                                min_gamma = 0.10,
                                                max_gamma = 0.11,
                                                volume_shape = "tube"
                                                )

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

        ```python
        # Visualize slices
        qim3d.viz.slices_grid(volume_collection, num_slices=15, slice_axis=1)
        ```
        ![synthetic_collection_tube](../../assets/screenshots/synthetic_collection_tube_slices.png)

    """

    if rotation_axes is None:
        rotation_axes = [(0, 1), (0, 2), (1, 2)]

    if verbose:
        original_log_level = log.getEffectiveLevel()
        log.setLevel('DEBUG')

    # Check valid input types
    if not isinstance(collection_shape, tuple) or len(collection_shape) != 3:
        message = 'Shape of collection must be a tuple with three dimensions (z, y, x)'
        raise TypeError(message)

    if len(min_shape) != len(max_shape):
        message = 'Object shapes must be tuples of the same length'
        raise ValueError(message)

    if (positions is not None) and (len(positions) != num_volumes):
        message = 'Number of volumes must match number of positions, otherwise set positions = None'
        raise ValueError(message)

    # Set seed for random number generator
    rng = np.random.default_rng(seed)

    # Initialize the 3D array for the shape
    collection_array = np.zeros(
        (collection_shape[0], collection_shape[1], collection_shape[2]), dtype=np.uint8
    )
    labels = np.zeros_like(collection_array)

    # Fill the 3D array with synthetic blobs
    for i in tqdm(range(num_volumes), desc='Objects placed'):
        log.debug(f'\nObject #{i+1}')

        # Sample from blob parameter ranges
        if min_shape == max_shape:
            blob_shape = min_shape
        else:
            blob_shape = tuple(
                rng.integers(low=min_shape[i], high=max_shape[i]) for i in range(3)
            )
        log.debug(f'- Blob shape: {blob_shape}')

        # Scale volume shape
        final_shape = tuple(
            dim * zoom for dim, zoom in zip(blob_shape, volume_shape_zoom)
        )
        final_shape = tuple(int(x) for x in final_shape)  # NOTE: Added this

        # Sample noise scale
        noise_scale = rng.uniform(low=min_volume_noise, high=max_volume_noise)
        log.debug(f'- Object noise scale: {noise_scale:.4f}')

        gamma = rng.uniform(low=min_gamma, high=max_gamma)
        log.debug(f'- Gamma correction: {gamma:.3f}')

        if max_high_value > min_high_value:
            max_value = rng.integers(low=min_high_value, high=max_high_value)
        else:
            max_value = min_high_value
        log.debug(f'- Max value: {max_value}')

        threshold = rng.uniform(low=min_threshold, high=max_threshold)
        log.debug(f'- Threshold: {threshold:.3f}')

        # Generate synthetic volume
        blob = qim3d.generate.volume(
            base_shape=blob_shape,
            final_shape=final_shape,
            noise_scale=noise_scale,
            gamma=gamma,
            max_value=max_value,
            threshold=threshold,
            smooth_borders=smooth_borders,
            volume_shape=volume_shape,
        )

        # Rotate volume
        if max_rotation_degrees > 0:
            angle = rng.uniform(
                low=min_rotation_degrees, high=max_rotation_degrees
            )  # Sample rotation angle
            axes = rng.choice(rotation_axes)  # Sample the two axes to rotate around
            log.debug(f'- Rotation angle: {angle:.2f} at axes: {axes}')

            blob = scipy.ndimage.rotate(blob, angle, axes, order=1)

        # Place synthetic volume into the collection
        # If positions are specified, place volume at one of the specified positions
        collection_before = collection_array.copy()
        if positions:
            collection_array, placed, positions = specific_placement(
                collection_array, blob, positions
            )

        # Otherwise, place volume at a random available position
        else:
            collection_array, placed = random_placement(collection_array, blob, rng)

        # Break if volume could not be placed
        if not placed:
            break

        # Update labels
        new_labels = np.where(collection_array != collection_before, i + 1, 0).astype(
            labels.dtype
        )
        labels += new_labels

    if not placed:
        # Log error if not all num_volumes could be placed (this line of code has to be here, otherwise it will interfere with tqdm progress bar)
        log.error(
            f'Object #{i+1} could not be placed in the collection, no space found. Collection contains {i}/{num_volumes} volumes.'
        )

    if verbose:
        log.setLevel(original_log_level)

    return collection_array, labels

qim3d.generate.background

background(background_shape, baseline_value=0, min_noise_value=0, max_noise_value=20, generate_method='divide', apply_method=None, seed=0, dtype='uint8', apply_to=None)

Generate a noise volume with random intensity values from a uniform distribution.

Parameters:

Name Type Description Default
background_shape tuple

The shape of the noise volume to generate.

required
baseline_value float

The baseline intensity of the noise volume. Default is 0.

0
min_noise_value float

The minimum intensity of the noise. Default is 0.

0
max_noise_value float

The maximum intensity of the noise. Default is 20.

20
generate_method str

The method used to combine baseline_value and noise. Choose from 'add' (baseline + noise), 'subtract' (baseline - noise), 'multiply' (baseline * noise), or 'divide' (baseline / (1 + noise)). Default is 'divide'.

'divide'
apply_method str

The method to apply the generated noise to apply_to, if provided. Choose from 'add' (apply_to + background), 'subtract' (apply_to - background), 'multiply' (apply_to * background), or 'divide' (apply_to / (1 + background)). Default is None.

None
seed int

The seed for the random number generator. Default is 0.

0
dtype data - type

Desired data type of the output volume. Default is 'uint8'.

'uint8'
apply_to ndarray

An input volume to which noise will be applied. If None, the generated noise volume is returned.

None

Returns:

Name Type Description
background ndarray

The generated noise volume (if apply_to is None) or the input volume with added noise (if apply_to is not None).

Raises:

Type Description
ValueError

If apply_method is not one of 'add', 'subtract', 'multiply', or 'divide'.

ValueError

If apply_method is provided without apply_to input volume provided.

ValueError

If the shape of apply_to input volume does not match background_shape.

Example
import qim3d

# Generate noise volume
background = qim3d.generate.background(
    background_shape = (128, 128, 128),
    baseline_value = 20,
    min_noise_value = 100,
    max_noise_value = 200,
)

qim3d.viz.volumetric(background)
Example
import qim3d

# Generate synthetic collection of volumes
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

# Apply noise to the synthetic collection
noisy_collection = qim3d.generate.background(
    background_shape = volume_collection.shape,
    min_noise_value = 0,
    max_noise_value = 20,
    apply_to = volume_collection
)

qim3d.viz.volumetric(noisy_collection)
Example
import qim3d

# Generate synthetic collection of volumes
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

# Apply noise to the synthetic collection
noisy_collection = qim3d.generate.background(
    background_shape = volume_collection.shape,
    baseline_value = 0,
    min_noise_value = 0,
    max_noise_value = 30,
    generate_method = 'add',
    apply_method = 'divide',
    apply_to = volume_collection
)

qim3d.viz.volumetric(noisy_collection)

qim3d.viz.slices_grid(noisy_collection, num_slices=10, color_bar=True, color_bar_style="large")
synthetic_noisy_collection_slices

Example

import qim3d

# Generate synthetic collection of volumes
volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

# Apply noise to the synthetic collection
noisy_collection = qim3d.generate.background(
    background_shape = (200, 200, 200),
    baseline_value = 100,
    min_noise_value = 0.8,
    max_noise_value = 1.2,
    generate_method = "multiply",
    apply_method = "add",
    apply_to = volume_collection
)

qim3d.viz.slices_grid(noisy_collection, num_slices=10, color_bar=True, color_bar_style="large")
synthetic_noisy_collection_slices

Source code in qim3d/generate/_generators.py
def background(
    background_shape: tuple,
    baseline_value: float = 0,
    min_noise_value: float = 0,
    max_noise_value: float = 20,
    generate_method: str = 'divide',
    apply_method: str = None,
    seed: int = 0,
    dtype: str = 'uint8',
    apply_to: np.ndarray = None,
) -> np.ndarray:
    """
    Generate a noise volume with random intensity values from a uniform distribution.

    Args:
        background_shape (tuple): The shape of the noise volume to generate.
        baseline_value (float, optional): The baseline intensity of the noise volume. Default is 0.
        min_noise_value (float, optional): The minimum intensity of the noise. Default is 0.
        max_noise_value (float, optional): The maximum intensity of the noise. Default is 20.
        generate_method (str, optional): The method used to combine `baseline_value` and noise. Choose from 'add' (`baseline + noise`), 'subtract' (`baseline - noise`), 'multiply' (`baseline * noise`), or 'divide' (`baseline / (1 + noise)`). Default is 'divide'.
        apply_method (str, optional): The method to apply the generated noise to `apply_to`, if provided. Choose from 'add' (`apply_to + background`), 'subtract' (`apply_to - background`), 'multiply' (`apply_to * background`), or 'divide' (`apply_to / (1 + background)`). Default is None.
        seed (int, optional): The seed for the random number generator. Default is 0.
        dtype (data-type, optional): Desired data type of the output volume. Default is 'uint8'.
        apply_to (np.ndarray, optional): An input volume to which noise will be applied. If None, the generated noise volume is returned.

    Returns:
        background (np.ndarray): The generated noise volume (if `apply_to` is None) or the input volume with added noise (if `apply_to` is not None).

    Raises:
        ValueError: If `apply_method` is not one of 'add', 'subtract', 'multiply', or 'divide'.
        ValueError: If `apply_method` is provided without `apply_to` input volume provided.
        ValueError: If the shape of `apply_to` input volume does not match `background_shape`.

    Example:
        ```python
        import qim3d

        # Generate noise volume
        background = qim3d.generate.background(
            background_shape = (128, 128, 128),
            baseline_value = 20,
            min_noise_value = 100,
            max_noise_value = 200,
        )

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

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

        # Apply noise to the synthetic collection
        noisy_collection = qim3d.generate.background(
            background_shape = volume_collection.shape,
            min_noise_value = 0,
            max_noise_value = 20,
            apply_to = volume_collection
        )

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

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

        # Apply noise to the synthetic collection
        noisy_collection = qim3d.generate.background(
            background_shape = volume_collection.shape,
            baseline_value = 0,
            min_noise_value = 0,
            max_noise_value = 30,
            generate_method = 'add',
            apply_method = 'divide',
            apply_to = volume_collection
        )

        qim3d.viz.volumetric(noisy_collection)
        ```
        <iframe src="https://platform.qim.dk/k3d/synthetic_noisy_collection_2.html" width="100%" height="500" frameborder="0"></iframe>
        ```python
        qim3d.viz.slices_grid(noisy_collection, num_slices=10, color_bar=True, color_bar_style="large")
        ```
        ![synthetic_noisy_collection_slices](../../assets/screenshots/synthetic_noisy_collection_slices_2.png)

    Example:
        ```python
        import qim3d

        # Generate synthetic collection of volumes
        volume_collection, labels = qim3d.generate.volume_collection(num_volumes = 15)

        # Apply noise to the synthetic collection
        noisy_collection = qim3d.generate.background(
            background_shape = (200, 200, 200),
            baseline_value = 100,
            min_noise_value = 0.8,
            max_noise_value = 1.2,
            generate_method = "multiply",
            apply_method = "add",
            apply_to = volume_collection
        )

        qim3d.viz.slices_grid(noisy_collection, num_slices=10, color_bar=True, color_bar_style="large")
        ```
        ![synthetic_noisy_collection_slices](../../assets/screenshots/synthetic_noisy_collection_slices_3.png)

    """
    # Ensure dtype is a valid NumPy type
    dtype = np.dtype(dtype)

    # Define supported apply methods
    apply_operations = {
        'add': lambda a, b: a + b,
        'subtract': lambda a, b: a - b,
        'multiply': lambda a, b: a * b,
        'divide': lambda a, b: a / (b + 1e-8),  # Avoid division by zero
    }

    # Check if apply_method is provided without apply_to volume
    if (apply_to is None) and (apply_method is not None):
        msg = f"apply_method '{apply_method}' is only supported when apply_to input volume is provided."
        # Validate apply_method
        if apply_method not in apply_operations:
            msg = f"Invalid apply_method '{apply_method}'. Choose from {list(apply_operations.keys())}."
            raise ValueError(msg)

        raise ValueError(msg)

    # Check for shape mismatch
    if (apply_to is not None) and (apply_to.shape != background_shape):
        msg = f'Shape of input volume {apply_to.shape} does not match requested background_shape {background_shape}. Using input shape instead.'
        background_shape = apply_to.shape
        log.info(msg)

    # Generate the noise volume
    baseline = np.full(shape=background_shape, fill_value=baseline_value)

    # Start seeded generator
    rng = np.random.default_rng(seed=seed)
    noise = rng.uniform(
        low=float(min_noise_value), high=float(max_noise_value), size=background_shape
    )

    # Apply method to initial background computation
    background_volume = apply_operations[generate_method](baseline, noise)

    # Apply method to the target volume if specified
    if apply_to is not None:
        background_volume = apply_operations[apply_method](apply_to, background_volume)

    # Clip value before dtype convertion
    clip_value = (
        np.iinfo(dtype).max if np.issubdtype(dtype, np.integer) else np.finfo(dtype).max
    )
    background_volume = np.clip(background_volume, 0, clip_value).astype(dtype)

    return background_volume