Skip to content
Snippets Groups Projects
Commit bce4b0cf authored by fima's avatar fima :beers:
Browse files

Merge branch 'fade_synthetic_objects' into 'main'

Fade synthetic objects

See merge request !133
parents 72f94718 d3c567cf
Branches
No related tags found
1 merge request!133Fade synthetic objects
docs/assets/screenshots/synthetic_blob_cylinder_slice.png

168 KiB

docs/assets/screenshots/synthetic_blob_tube_slice.png

194 KiB

docs/assets/screenshots/synthetic_collection_cylinder_slices.png

179 KiB

docs/assets/screenshots/synthetic_collection_tube_slices.png

100 KiB

...@@ -2,6 +2,8 @@ import numpy as np ...@@ -2,6 +2,8 @@ import numpy as np
import scipy.ndimage import scipy.ndimage
from noise import pnoise3 from noise import pnoise3
import qim3d.processing
def blob( def blob(
base_shape: tuple = (128, 128, 128), base_shape: tuple = (128, 128, 128),
final_shape: tuple = (128, 128, 128), final_shape: tuple = (128, 128, 128),
...@@ -11,6 +13,7 @@ def blob( ...@@ -11,6 +13,7 @@ def blob(
max_value: int = 255, max_value: int = 255,
threshold: float = 0.5, threshold: float = 0.5,
smooth_borders: bool = False, smooth_borders: bool = False,
object_shape: str = None,
dtype: str = "uint8", dtype: str = "uint8",
) -> np.ndarray: ) -> np.ndarray:
""" """
...@@ -25,6 +28,7 @@ def blob( ...@@ -25,6 +28,7 @@ def blob(
max_value (int, optional): Maximum value for the volume intensity. Defaults to 255. 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. 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. 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.
object_shape (str, optional): Shape of the object to generate, either "cylinder", or "tube". Defaults to None.
dtype (str, optional): Desired data type of the output volume. Defaults to "uint8". dtype (str, optional): Desired data type of the output volume. Defaults to "uint8".
Returns: Returns:
...@@ -41,17 +45,63 @@ def blob( ...@@ -41,17 +45,63 @@ def blob(
# Generate synthetic blob # Generate synthetic blob
synthetic_blob = qim3d.generate.blob(noise_scale = 0.015) synthetic_blob = qim3d.generate.blob(noise_scale = 0.015)
# Visualize 3D volume
qim3d.viz.vol(synthetic_blob)
```
<iframe src="https://platform.qim.dk/k3d/synthetic_blob.html" width="100%" height="500" frameborder="0"></iframe>
```python
# Visualize slices # Visualize slices
qim3d.viz.slices(synthetic_blob, vmin = 0, vmax = 255, n_slices = 15) qim3d.viz.slices(synthetic_blob, vmin = 0, vmax = 255, n_slices = 15)
``` ```
![synthetic_blob](assets/screenshots/synthetic_blob_slices.png) ![synthetic_blob](assets/screenshots/synthetic_blob_slices.png)
Example:
```python ```python
# Visualize 3D volume import qim3d
qim3d.viz.vol(synthetic_blob)
# Generate tubular synthetic blob
vol = qim3d.generate.blob(base_shape = (10, 300, 300),
final_shape = (100, 100, 100),
noise_scale = 0.3,
gamma = 2,
threshold = 0.0,
object_shape = "cylinder"
)
# Visualize synthetic blob
qim3d.viz.vol(vol)
``` ```
<iframe src="https://platform.qim.dk/k3d/synthetic_blob.html" width="100%" height="500" frameborder="0"></iframe> <iframe src="https://platform.qim.dk/k3d/synthetic_blob_cylinder.html" width="100%" height="500" frameborder="0"></iframe>
```python
# Visualize slices
qim3d.viz.slices(vol, n_slices=15, axis=1) ```
![synthetic_blob_cylinder_slice](assets/screenshots/synthetic_blob_cylinder_slice.png)
Example:
```python
import qim3d
# Generate tubular synthetic blob
vol = qim3d.generate.blob(base_shape = (200, 100, 100),
final_shape = (400, 100, 100),
noise_scale = 0.03,
gamma = 0.12,
threshold = 0.85,
object_shape = "tube"
)
# Visualize synthetic blob
qim3d.viz.vol(vol)
```
<iframe src="https://platform.qim.dk/k3d/synthetic_blob_tube.html" width="100%" height="500" frameborder="0"></iframe>
```python
# Visualize
qim3d.viz.slices(vol, n_slices=15)
```
![synthetic_blob_tube_slice](assets/screenshots/synthetic_blob_tube_slice.png)
""" """
if not isinstance(final_shape, tuple) or len(final_shape) != 3: if not isinstance(final_shape, tuple) or len(final_shape) != 3:
...@@ -93,6 +143,10 @@ def blob( ...@@ -93,6 +143,10 @@ def blob(
# Scale the volume to the maximum value # Scale the volume to the maximum value
volume = volume * max_value volume = volume * max_value
# If object shape is specified, smooth borders are disabled
if object_shape:
smooth_borders = False
if smooth_borders: if smooth_borders:
# Maximum value among the six sides of the 3D volume # Maximum value among the six sides of the 3D volume
max_border_value = np.max([ max_border_value = np.max([
...@@ -115,4 +169,46 @@ def blob( ...@@ -115,4 +169,46 @@ def blob(
volume, np.array(final_shape) / np.array(base_shape), order=order volume, np.array(final_shape) / np.array(base_shape), order=order
) )
return volume.astype(dtype) # Fade into a shape if specified
if object_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 object is the largest
target_max_normalized_distance = 1.4 # This value ensures that the object will become cylindrical
volume = qim3d.processing.operations.fade_mask(volume,
geometry = geometry,
axis = axis,
target_max_normalized_distance = target_max_normalized_distance
)
elif object_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 object is the largest
decay_rate = 5 # Decay rate for the fade operation
target_max_normalized_distance = 1.4 # This value ensures that the object will become cylindrical
# Fade once for making the object cylindrical
volume = qim3d.processing.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 object a tube (i.e. with a hole in the middle)
volume = qim3d.processing.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
\ No newline at end of file
...@@ -139,6 +139,7 @@ def collection( ...@@ -139,6 +139,7 @@ def collection(
min_threshold: float = 0.5, min_threshold: float = 0.5,
max_threshold: float = 0.6, max_threshold: float = 0.6,
smooth_borders: bool = False, smooth_borders: bool = False,
object_shape: str = None,
seed: int = 0, seed: int = 0,
verbose: bool = False, verbose: bool = False,
) -> tuple[np.ndarray, object]: ) -> tuple[np.ndarray, object]:
...@@ -163,30 +164,30 @@ def collection( ...@@ -163,30 +164,30 @@ def collection(
max_high_value (int, optional): Maximum maximum value for the volume intensity. Defaults to 255. 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. 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. max_threshold (float, optional): Maximum threshold value for clipping low intensity values. Defaults to 0.6.
smooth_borders (bool, optional): Flag for smoothing blob borders to avoid straight edges in the objects. If True, the `min_threshold` and `max_threshold` parameters are ignored. Defaults to False. smooth_borders (bool, optional): Flag for smoothing object borders to avoid straight edges in the objects. If True, the `min_threshold` and `max_threshold` parameters are ignored. Defaults to False.
object_shape (str, optional): Shape of the object to generate, either "cylinder", or "tube". Defaults to None.
seed (int, optional): Seed for reproducibility. Defaults to 0. seed (int, optional): Seed for reproducibility. Defaults to 0.
verbose (bool, optional): Flag to enable verbose logging. Defaults to False. verbose (bool, optional): Flag to enable verbose logging. Defaults to False.
Returns: Returns:
synthetic_collection (numpy.ndarray): 3D volume of the generated collection of synthetic objects with specified parameters. synthetic_collection (numpy.ndarray): 3D volume of the generated collection of synthetic objects with specified parameters.
labels (numpy.ndarray): Array with labels for each voxel, same shape as synthetic_collection. labels (numpy.ndarray): Array with labels for each voxel, same shape as synthetic_collection.
Raises: Raises:
TypeError: If `collection_shape` is not 3D. TypeError: If `collection_shape` is not 3D.
ValueError: If blob parameters are invalid. ValueError: If object parameters are invalid.
Note: Note:
- The function places objects without overlap. - The function places objects without overlap.
- The function can either place objects 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_objects = 2` with `positions = [(12, 8, 10), (24, 20, 18)]`). - The function can either place objects 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_objects = 2` with `positions = [(12, 8, 10), (24, 20, 18)]`).
- If not all `num_objects` can be placed, the function returns the `synthetic_collection` volume with as many blobs as possible in it, and logs an error. - If not all `num_objects` can be placed, the function returns the `synthetic_collection` volume with as many objects as possible in it, and logs an error.
- Labels for all objects are returned, even if they are not a sigle connected component. - Labels for all objects are returned, even if they are not a single connected component.
Example: Example:
```python ```python
import qim3d import qim3d
# Generate synthetic collection of blobs # Generate synthetic collection of objects
num_objects = 15 num_objects = 15
synthetic_collection, labels = qim3d.generate.collection(num_objects = num_objects) synthetic_collection, labels = qim3d.generate.collection(num_objects = num_objects)
...@@ -207,12 +208,11 @@ def collection( ...@@ -207,12 +208,11 @@ def collection(
``` ```
![synthetic_collection](assets/screenshots/synthetic_collection_default_labels.gif) ![synthetic_collection](assets/screenshots/synthetic_collection_default_labels.gif)
Example: Example:
```python ```python
import qim3d import qim3d
# Generate synthetic collection of dense blobs # Generate synthetic collection of dense objects
synthetic_collection, labels = qim3d.generate.collection( synthetic_collection, labels = qim3d.generate.collection(
min_high_value = 255, min_high_value = 255,
max_high_value = 255, max_high_value = 255,
...@@ -228,34 +228,66 @@ def collection( ...@@ -228,34 +228,66 @@ def collection(
``` ```
<iframe src="https://platform.qim.dk/k3d/synthetic_collection_dense.html" width="100%" height="500" frameborder="0"></iframe> <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
vol, labels = qim3d.generate.collection(num_objects = 40,
collection_shape = (300, 150, 150),
min_shape = (280, 10, 10),
max_shape = (290, 15, 15),
min_object_noise = 0.08,
max_object_noise = 0.09,
max_rotation_degrees = 5,
min_threshold = 0.7,
max_threshold = 0.9,
min_gamma = 0.10,
max_gamma = 0.11,
object_shape = "cylinder"
)
# Visualize synthetic collection
qim3d.viz.vol(vol)
```
<iframe src="https://platform.qim.dk/k3d/synthetic_collection_cylinder.html" width="100%" height="500" frameborder="0"></iframe>
```python
# Visualize slices
qim3d.viz.slices(vol, n_slices=15)
```
![synthetic_collection_cylinder](assets/screenshots/synthetic_collection_cylinder_slices.png)
Example: Example:
```python ```python
import qim3d import qim3d
# Generate synthetic collection of tubular structures # Generate synthetic collection of tubular (hollow) structures
synthetic_collection, labels = qim3d.generate.collection( vol, labels = qim3d.generate.collection(num_objects = 10,
num_objects=10, collection_shape = (200, 200, 200),
collection_shape=(200,100,100), min_shape = (180, 25, 25),
min_shape = (190, 50, 50), max_shape = (190, 35, 35),
max_shape = (200, 60, 60), min_object_noise = 0.02,
object_shape_zoom = (1, 0.2, 0.2), max_object_noise = 0.03,
min_object_noise = 0.01, max_rotation_degrees = 5,
max_object_noise = 0.02, min_threshold = 0.7,
max_rotation_degrees=10, max_threshold = 0.9,
min_threshold = 0.95, min_gamma = 0.10,
max_threshold = 0.98, max_gamma = 0.11,
min_gamma = 0.02, object_shape = "tube"
max_gamma = 0.03
) )
# Visualize synthetic collection # Visualize synthetic collection
qim3d.viz.vol(synthetic_collection) qim3d.viz.vol(vol)
``` ```
<iframe src="https://platform.qim.dk/k3d/synthetic_collection_tubular.html" width="100%" height="500" frameborder="0"></iframe> <iframe src="https://platform.qim.dk/k3d/synthetic_collection_tube.html" width="100%" height="500" frameborder="0"></iframe>
```python
# Visualize slices
qim3d.viz.slices(vol, n_slices=15, axis=1)
```
![synthetic_collection_tube](assets/screenshots/synthetic_collection_tube_slices.png)
""" """
if verbose: if verbose:
original_log_level = log.getEffectiveLevel() original_log_level = log.getEffectiveLevel()
...@@ -270,10 +302,6 @@ def collection( ...@@ -270,10 +302,6 @@ def collection(
if len(min_shape) != len(max_shape): if len(min_shape) != len(max_shape):
raise ValueError("Object shapes must be tuples of the same length") raise ValueError("Object shapes must be tuples of the same length")
# if not isinstance(blob_shapes, list) or \
# len(blob_shapes) != 2 or len(blob_shapes[0]) != 3 or len(blob_shapes[1]) != 3:
# raise TypeError("Blob shapes must be a list of two tuples with three dimensions (z, y, x)")
if (positions is not None) and (len(positions) != num_objects): if (positions is not None) and (len(positions) != num_objects):
raise ValueError( raise ValueError(
"Number of objects must match number of positions, otherwise set positions = None" "Number of objects must match number of positions, otherwise set positions = None"
...@@ -301,6 +329,10 @@ def collection( ...@@ -301,6 +329,10 @@ def collection(
) )
log.debug(f"- Blob shape: {blob_shape}") log.debug(f"- Blob shape: {blob_shape}")
# Scale object shape
final_shape = tuple(l * r for l, r in zip(blob_shape, object_shape_zoom))
final_shape = tuple(int(x) for x in final_shape) # NOTE: Added this
# Sample noise scale # Sample noise scale
noise_scale = rng.uniform(low=min_object_noise, high=max_object_noise) noise_scale = rng.uniform(low=min_object_noise, high=max_object_noise)
log.debug(f"- Object noise scale: {noise_scale:.4f}") log.debug(f"- Object noise scale: {noise_scale:.4f}")
...@@ -317,15 +349,16 @@ def collection( ...@@ -317,15 +349,16 @@ def collection(
threshold = rng.uniform(low=min_threshold, high=max_threshold) threshold = rng.uniform(low=min_threshold, high=max_threshold)
log.debug(f"- Threshold: {threshold:.3f}") log.debug(f"- Threshold: {threshold:.3f}")
# Generate synthetic blob # Generate synthetic object
blob = qim3d.generate.blob( blob = qim3d.generate.blob(
base_shape=blob_shape, base_shape=blob_shape,
final_shape=tuple(l * r for l, r in zip(blob_shape, object_shape_zoom)), final_shape=final_shape,
noise_scale=noise_scale, noise_scale=noise_scale,
gamma=gamma, gamma=gamma,
max_value=max_value, max_value=max_value,
threshold=threshold, threshold=threshold,
smooth_borders=smooth_borders, smooth_borders=smooth_borders,
object_shape=object_shape,
) )
# Rotate object # Rotate object
...@@ -336,21 +369,21 @@ def collection( ...@@ -336,21 +369,21 @@ def collection(
axes = rng.choice(rotation_axes) # Sample the two axes to rotate around axes = rng.choice(rotation_axes) # Sample the two axes to rotate around
log.debug(f"- Rotation angle: {angle:.2f} at axes: {axes}") log.debug(f"- Rotation angle: {angle:.2f} at axes: {axes}")
blob = scipy.ndimage.rotate(blob, angle, axes, order=0) blob = scipy.ndimage.rotate(blob, angle, axes, order=1)
# Place synthetic object into the collection # Place synthetic object into the collection
# If positions are specified, place blob at one of the specified positions # If positions are specified, place object at one of the specified positions
collection_before = collection_array.copy() collection_before = collection_array.copy()
if positions: if positions:
collection_array, placed, positions = specific_placement( collection_array, placed, positions = specific_placement(
collection_array, blob, positions collection_array, blob, positions
) )
# Otherwise, place blob at a random available position # Otherwise, place object at a random available position
else: else:
collection_array, placed = random_placement(collection_array, blob, rng) collection_array, placed = random_placement(collection_array, blob, rng)
# Break if blob could not be placed # Break if object could not be placed
if not placed: if not placed:
break break
......
...@@ -125,6 +125,7 @@ def fade_mask( ...@@ -125,6 +125,7 @@ def fade_mask(
geometry: str = "spherical", geometry: str = "spherical",
invert: bool = False, invert: bool = False,
axis: int = 0, axis: int = 0,
**kwargs,
) -> np.ndarray: ) -> np.ndarray:
""" """
Apply edge fading to a volume. Apply edge fading to a volume.
...@@ -133,8 +134,10 @@ def fade_mask( ...@@ -133,8 +134,10 @@ def fade_mask(
vol (np.ndarray): The volume to apply edge fading to. vol (np.ndarray): The volume to apply edge fading to.
decay_rate (float, optional): The decay rate of the fading. Defaults to 10. decay_rate (float, optional): The decay rate of the fading. Defaults to 10.
ratio (float, optional): The ratio of the volume to fade. Defaults to 0.5. ratio (float, optional): The ratio of the volume to fade. Defaults to 0.5.
geometric (str, optional): The geometric shape of the fading. Can be 'spherical' or 'cylindrical'. Defaults to 'spherical'. geometry (str, optional): The geometric shape of the fading. Can be 'spherical' or 'cylindrical'. Defaults to 'spherical'.
invert (bool, optional): Flag for inverting the fading. Defaults to False.
axis (int, optional): The axis along which to apply the fading. Defaults to 0. axis (int, optional): The axis along which to apply the fading. Defaults to 0.
**kwargs: Additional keyword arguments for the edge fading.
Returns: Returns:
vol_faded (np.ndarray): The volume with edge fading applied. vol_faded (np.ndarray): The volume with edge fading applied.
...@@ -165,6 +168,9 @@ def fade_mask( ...@@ -165,6 +168,9 @@ def fade_mask(
shape = vol.shape shape = vol.shape
z, y, x = np.indices(shape) z, y, x = np.indices(shape)
# Store the original maximum value of the volume
original_max_value = np.max(vol)
# Calculate the center of the array # Calculate the center of the array
center = np.array([(s - 1) / 2 for s in shape]) center = np.array([(s - 1) / 2 for s in shape])
...@@ -177,10 +183,18 @@ def fade_mask( ...@@ -177,10 +183,18 @@ def fade_mask(
distance_list = np.delete(distance_list, axis, axis=0) distance_list = np.delete(distance_list, axis, axis=0)
distance = np.linalg.norm(distance_list, axis=0) distance = np.linalg.norm(distance_list, axis=0)
else: else:
raise ValueError("geometric must be 'spherical' or 'cylindrical'") raise ValueError("Geometry must be 'spherical' or 'cylindrical'")
# Normalize the distances so that they go from 0 at the center to 1 at the farthest point # Compute the maximum distance from the center
max_distance = np.linalg.norm(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) normalized_distance = distance / (max_distance * ratio)
# Apply the decay rate # Apply the decay rate
...@@ -196,7 +210,10 @@ def fade_mask( ...@@ -196,7 +210,10 @@ def fade_mask(
# Apply the fading to the volume # Apply the fading to the volume
vol_faded = vol * fade_array vol_faded = vol * fade_array
return vol_faded # Normalize the volume to retain the original maximum value
vol_normalized = vol_faded * (original_max_value / np.max(vol_faded))
return vol_normalized
def overlay_rgb_images( def overlay_rgb_images(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment