diff --git a/qim3d/utils/__init__.py b/qim3d/utils/__init__.py index 5b4866e28b024633a564f662517f78cb9d80bf59..3d9e2150f8e949b4a00e31a2e88e340100be382c 100644 --- a/qim3d/utils/__init__.py +++ b/qim3d/utils/__init__.py @@ -2,7 +2,7 @@ from . import doi, internal_tools from .augmentations import Augmentation from .data import Dataset, prepare_dataloaders, prepare_datasets -from .img import overlay_rgb_images +from .img import generate_volume, overlay_rgb_images from .models import inference, model_summary, train_model -from .system import Memory from .preview import image_preview +from .system import Memory diff --git a/qim3d/utils/img.py b/qim3d/utils/img.py index 347e4cc2ecbc1176ca4fc663884f7febbfc93598..c75aa93c20ca05be328cb779640e935041af8c48 100644 --- a/qim3d/utils/img.py +++ b/qim3d/utils/img.py @@ -1,4 +1,7 @@ import numpy as np +import scipy.ndimage +from noise import pnoise3 + def overlay_rgb_images(background, foreground, alpha=0.5): """Overlay a RGB foreground onto an RGB background using alpha blending. @@ -35,7 +38,9 @@ def overlay_rgb_images(background, foreground, alpha=0.5): # Normalize if we have something if np.max(foreground_max_projection) > 0: - foreground_max_projection = foreground_max_projection / np.max(foreground_max_projection) + foreground_max_projection = foreground_max_projection / np.max( + foreground_max_projection + ) composite = background * (1 - alpha) + foreground * alpha composite = np.clip(composite, 0, 255).astype("uint8") @@ -44,3 +49,88 @@ def overlay_rgb_images(background, foreground, alpha=0.5): composite = composite + (background * (1 - alpha)) * (1 - foreground_max_projection) return composite.astype("uint8") + + +def generate_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, + dtype="uint8", +): + """ + Generate a 3D volume with Perlin noise, spherical gradient, and optional scaling and gamma correction. + + Args: + base_shape (tuple, optional): Shape of the initial volume to generate. Defaults to (128, 128, 128). + final_shape (tuple, 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. + dtype (str, optional): Desired data type of the output volume. Defaults to "uint8". + + Returns: + numpy.ndarray: Generated 3D volume with specified parameters. + + Raises: + ValueError: 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 + vol = qim3d.utils.generate_volume() + qim3d.viz.slices(vol, vmin=0, vmax=255) + """ + + if not isinstance(final_shape, tuple) or len(final_shape) != 3: + raise ValueError("Size must be a tuple") + if not np.issubdtype(dtype, np.number): + raise ValueError("Invalid data type") + + # Define the dimensions of the shape for generating Perlin noise + + # Initialize the 3D array for the shape + volume = np.empty((base_shape[0], base_shape[1], base_shape[2]), dtype=np.float32) + + # Fill the 3D array with values from the Perlin noise function + for i in range(base_shape[0]): + for j in range(base_shape[1]): + for k in range(base_shape[2]): + # Calculate the distance from the center of the shape + dist = np.sqrt( + (i - base_shape[0] / 2) ** 2 + + (j - base_shape[1] / 2) ** 2 + + (k - base_shape[2] / 2) ** 2 + ) / np.sqrt(3 * ((base_shape[0] / 2) ** 2)) + # Generate Perlin noise and adjust the values based on the distance from the center + # This creates a spherical shape with noise + volume[i][j][k] = (1 + pnoise3(i * noise_scale, j * noise_scale, k * noise_scale)) * ( + 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 + + # 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 + ) + + return volume.astype(dtype) diff --git a/requirements.txt b/requirements.txt index b7508e720f85eb6623bcefa9cd20bae3ad451acf..c22d3ed74bbf8c91aa0e336395bb2060ed552993 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,4 +23,5 @@ dask>=2023.6.0, k3d>=2.16.1 olefile>=0.46 psutil>=5.9.0 -structure-tensor>=0.2.1 \ No newline at end of file +structure-tensor>=0.2.1 +noise>=1.2.2 \ No newline at end of file diff --git a/setup.py b/setup.py index 34fa87fbd4c4f7d71f7c8406b604136300740c04..6cd31a6e6a912ac5d95faa6bf439fea5042892e9 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ setup( "k3d>=2.16.1", "olefile>=0.46", "psutil>=5.9.0", - "structure-tensor>=0.2.1" + "structure-tensor>=0.2.1", + "noise>=1.2.2" ], )