"""
The textures module allows to generate 3D textures for synthetic samples
"""
# Copyright (c) 2020 Idiap Research Institute, http://www.idiap.ch/
# Written by François Marelli <francois.marelli@idiap.ch>
#
# This file is part of CBI Toolbox.
#
# CBI Toolbox is free software: you can redistribute it and/or modify
# it under the terms of the 3-Clause BSD License.
#
# CBI Toolbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 3-Clause BSD License for more details.
#
# You should have received a copy of the 3-Clause BSD License along
# with CBI Toolbox. If not, see https://opensource.org/licenses/BSD-3-Clause.
#
# SPDX-License-Identifier: BSD-3-Clause
import numpy as np
import opensimplex
try:
import noise
SIMPLEX_BACKEND = "noise"
except ImportError:
SIMPLEX_BACKEND = "opensimplex"
from cbi_toolbox.simu import primitives
[docs]def spheres(shape, density=1, seed=None):
"""
Generates a texture full of hollow spheres
Parameters
----------
shape : tuple(int)
shape of the texture array
density : int, optional
spheres density in the texture, by default
seed : int, optional
seed of the rng, default is None
Returns
-------
array [shape]
the texture
"""
dtype = np.float32
n_spheres = int(density * 10000)
size = max(shape)
max_radius = int(0.1 * size)
min_radius = int(0.02 * size)
max_in_radius = 0.5
min_intens = 0.05
max_intens = 0.2
pad_shape = [s + 4 * max_radius for s in shape]
volume = np.ones(pad_shape, dtype=dtype)
rng = np.random.default_rng(seed)
for _ in range(n_spheres):
center = (rng.random(3) * (np.array(shape) + 2 * max_radius)).astype(
int
) + max_radius
radius = int(rng.uniform(min_radius, max_radius))
in_radius = rng.uniform(0, max_in_radius)
intens = rng.uniform(min_intens, max_intens)
obj = primitives.ball(radius * 2, in_radius=in_radius, dtype=dtype)
volume[
center[0] - radius : center[0] + radius,
center[1] - radius : center[1] + radius,
center[2] - radius : center[2] + radius,
] *= (
1 - obj * intens
)
return 1 - volume[volume.ndim * (slice(2 * max_radius, -2 * max_radius),)]
[docs]def simplex(shape, scale=1, seed=None):
"""
Generates 2D/3D simplex noise
Noise values are in [-1, 1]
Parameters
----------
shape : tuple (int)
shape of the texture array
scale : int, optional
scale of the noise, by default 1
ndim : int, optional
number of dimensions of the array to generate (2, 3), by default 3
seed : int, optional
seed for the noise, by default None
Returns
-------
array [shape]
the simplex noise
"""
if seed is None:
seed = np.random.default_rng().integers(2**10)
if len(shape) not in (2, 3):
raise ValueError(
f"Only 2D and 3D textures can be generated, got ndim={len(shape)}"
)
volume = np.empty(shape, dtype=np.float32)
if SIMPLEX_BACKEND == "opensimplex":
opensimplex.seed(seed)
else:
scale /= 2
for idx, x in enumerate(np.arange(shape[0]) / max(shape)):
for idy, y in enumerate(np.arange(shape[1]) / max(shape)):
if len(shape) == 2:
if SIMPLEX_BACKEND == "opensimplex":
volume[idx, idy] = opensimplex.noise2(x * scale, y * scale)
else:
volume[idx, idy] = noise.snoise2(
(seed + x) * scale, (seed + y) * scale
)
elif len(shape) == 3:
for idz, z in enumerate(np.arange(shape[2]) / max(shape)):
if SIMPLEX_BACKEND == "opensimplex":
volume[idx, idy, idz] = opensimplex.noise3(
x * scale, y * scale, z * scale
)
else:
volume[idx, idy, idz] = noise.snoise3(
(seed + x) * scale, (seed + y) * scale, (seed + z) * scale
)
return volume
[docs]def forward_simplex(coordinates, scale=1, out=None, seed=None):
"""
Computes simplex noise over given coordinates
Noise values are in [-1, 1]
Parameters
----------
coordinates : np.ndarray [D, W, H, <Z>]
coordinates where the noise must be computed (meshgrid)
scale : int, optional
scale of the noise, by default 1
out: array, optional
output array, by default None
seed : int, optional
seed for the noise, by default None
Returns
-------
np.ndarray [W, H, <Z>]
the simplex noise computed at the given coordinates
"""
if seed is None:
seed = np.random.default_rng().integers(2**10)
ndim = coordinates.shape[0]
if coordinates.ndim != ndim + 1:
raise ValueError(
"Coordinates should be in a meshgrid, but the size "
"of the first dimension plus one does not match the dimensions of the whole array. "
)
if not ndim in (2, 3):
raise NotImplementedError(
"Only 2D and 3D coordinate arrays are implemented (3D and 4D meshgrids)"
)
if out is None:
out = np.empty(coordinates.shape[1:])
elif out.shape != coordinates.shape[1:]:
raise ValueError("Output shape does not match coordinates. ")
if SIMPLEX_BACKEND == "opensimplex":
opensimplex.seed(seed)
else:
scale /= 2
for kx, row in enumerate(coordinates.T):
for ky, col in enumerate(row):
if ndim == 2:
if SIMPLEX_BACKEND == "opensimplex":
out[ky, kx] = opensimplex.noise2(col[0] * scale, col[1] * scale)
else:
out[ky, kx] = noise.snoise2(
(seed + col[0]) * scale, (seed + col[1]) * scale
)
elif ndim == 3:
for kz, dep in enumerate(col):
if SIMPLEX_BACKEND == "opensimplex":
out[kz, ky, kx] = opensimplex.noise3(
dep[0] * scale, dep[1] * scale, dep[2] * scale
)
else:
out[kz, ky, kx] = noise.snoise3(
(seed + dep[0]) * scale,
(seed + dep[1]) * scale,
(seed + dep[2]) * scale,
)
return out
if __name__ == "__main__":
import napari
TEST_SHAPE = (64, 64, 32)
s_spheres = spheres(TEST_SHAPE)
s_simplex = simplex(TEST_SHAPE, scale=8)
viewer = napari.view_image(s_spheres)
viewer.add_image(s_simplex)
if SIMPLEX_BACKEND != "opensimplex":
SIMPLEX_BACKEND = "opensimplex"
s_simplex2 = simplex(TEST_SHAPE, scale=8)
viewer.add_image(s_simplex2)
napari.run()