"""Module dedicated for basic primitive."""
from os.path import join as pjoin
import numpy as np
from fury.data import DATA_DIR
from fury.transform import cart2sphere, euler_matrix
from fury.utils import fix_winding_order
from scipy.spatial import ConvexHull
from scipy.spatial import transform
import math
SPHERE_FILES = {
'symmetric362': pjoin(DATA_DIR, 'evenly_distributed_sphere_362.npz'),
'symmetric642': pjoin(DATA_DIR, 'evenly_distributed_sphere_642.npz'),
'symmetric724': pjoin(DATA_DIR, 'evenly_distributed_sphere_724.npz'),
'repulsion724': pjoin(DATA_DIR, 'repulsion724.npz'),
'repulsion100': pjoin(DATA_DIR, 'repulsion100.npz'),
'repulsion200': pjoin(DATA_DIR, 'repulsion200.npz')
}
[docs]def faces_from_sphere_vertices(vertices):
"""
Triangulate a set of vertices on the sphere.
Parameters
----------
vertices : (M, 3) ndarray
XYZ coordinates of vertices on the sphere.
Returns
-------
faces : (N, 3) ndarray
Indices into vertices; forms triangular faces.
"""
hull = ConvexHull(vertices, qhull_options='Qbb Qc')
faces = np.ascontiguousarray(hull.simplices)
if len(vertices) < 2**16:
return np.asarray(faces, np.uint16)
else:
return faces
[docs]def repeat_primitive_function(func, centers, func_args=[],
directions=(1, 0, 0), colors=(255, 0, 0),
scale=1):
"""Repeat Vertices and triangles of a specific primitive function.
It could be seen as a glyph. The primitive function should generate and
return vertices and faces
Parameters
----------
func : callable
primitive functions
centers : ndarray, shape (N, 3)
Superquadrics positions
func_args : args
primitive functions arguments/parameters
directions : ndarray, shape (N, 3) or tuple (3,), optional
The orientation vector of the cone.
colors : ndarray (N,3) or (N, 4) or tuple (3,) or tuple (4,)
RGB or RGBA (for opacity) R, G, B and A should be at the range [0, 1]
scale : ndarray, shape (N) or (N,3) or float or int, optional
The height of the cone.
Returns
-------
big_vertices: ndarray
Expanded vertices at the centers positions
big_triangles: ndarray
Expanded triangles that composed our shape to duplicate
big_colors : ndarray
Expanded colors applied to all vertices/faces
"""
# Get faces
_, faces = func()
if len(func_args) == 1:
func_args = np.squeeze(np.array([func_args] * centers.shape[0]))
elif len(func_args) != centers.shape[0]:
raise IOError("sq_params should 1 or equal to the numbers \
of centers")
vertices = np.concatenate([func(i)[0] for i in func_args])
return repeat_primitive(vertices=vertices, faces=faces, centers=centers,
directions=directions, colors=colors, scale=scale,
have_tiled_verts=True)
[docs]def repeat_primitive(vertices, faces, centers, directions=(1, 0, 0),
colors=(255, 0, 0), scale=1, have_tiled_verts=False):
"""Repeat Vertices and triangles of a specific primitive shape.
It could be seen as a glyph.
Parameters
----------
vertices: ndarray
vertices coords to duplicate at the centers positions
triangles: ndarray
triangles that composed our shape to duplicate
centers : ndarray, shape (N, 3)
Superquadrics positions
directions : ndarray, shape (N, 3) or tuple (3,), optional
The orientation vector of the cone.
colors : ndarray (N,3) or (N, 4) or tuple (3,) or tuple (4,)
RGB or RGBA (for opacity) R, G, B and A should be at the range [0, 1]
scale : ndarray, shape (N) or (N,3) or float or int, optional
The height of the cone.
have_tiled_verts : bool
option to control if we need to duplicate vertices of a shape or not
Returns
-------
big_vertices: ndarray
Expanded vertices at the centers positions
big_triangles: ndarray
Expanded triangles that composed our shape to duplicate
big_colors : ndarray
Expanded colors applied to all vertices/faces
big_centers : ndarray
Expanded centers for all vertices/faces
"""
# duplicated vertices if needed
if not have_tiled_verts:
vertices = np.tile(vertices, (centers.shape[0], 1))
big_vertices = vertices
# Get unit shape
unit_verts_size = vertices.shape[0] // centers.shape[0]
unit_triangles_size = faces.shape[0]
# scale them
if isinstance(scale, (list, tuple, np.ndarray)):
scale = np.repeat(scale, unit_verts_size, axis=0)
scale = scale.reshape((big_vertices.shape[0], 1))
big_vertices *= scale
# update triangles
big_triangles = np.array(np.tile(faces,
(centers.shape[0], 1)),
dtype=np.int32)
big_triangles += np.repeat(np.arange(0,
centers.shape[0] * unit_verts_size,
step=unit_verts_size),
unit_triangles_size,
axis=0).reshape((big_triangles.shape[0],
1))
def normalize_input(arr, arr_name=''):
if isinstance(arr, (tuple, list, np.ndarray)) and len(arr) == 3 and \
not all(isinstance(i, (list, tuple, np.ndarray)) for i in arr):
return np.array([arr] * centers.shape[0])
elif isinstance(arr, np.ndarray) and len(arr) == 1:
return np.repeat(arr, centers.shape[0], axis=0)
elif len(arr) != len(centers):
msg = "{} size should be 1 or ".format(arr_name)
msg += "equal to the numbers of centers"
raise IOError(msg)
else:
return np.array(arr)
# update colors
colors = normalize_input(colors, 'colors')
big_colors = np.repeat(colors, unit_verts_size, axis=0)
# update orientations
directions = normalize_input(directions, 'directions')
for pts, dirs in enumerate(directions):
ai, aj, ak = transform.Rotation.from_rotvec(np.pi / 2 * dirs). \
as_euler('zyx')
rotation_matrix = euler_matrix(ai, aj, ak)
big_vertices[pts * unit_verts_size: (pts + 1) * unit_verts_size] = \
np.dot(rotation_matrix[:3, :3],
big_vertices[pts * unit_verts_size:
(pts + 1) * unit_verts_size].T).T
# apply centers position
big_centers = np.repeat(centers, unit_verts_size, axis=0)
big_vertices += big_centers
return big_vertices, big_triangles, big_colors, big_centers
[docs]def prim_square():
"""Return vertices and triangles for a square geometry.
Returns
-------
vertices: ndarray
4 vertices coords that composed our square
triangles: ndarray
2 triangles that composed our square
"""
vertices = np.array([[-.5, -.5, 0.0],
[-.5, 0.5, 0.0],
[0.5, 0.5, 0.0],
[0.5, -.5, 0.0]])
triangles = np.array([[0, 1, 2],
[2, 3, 0]], dtype='i8')
return vertices, triangles
[docs]def prim_box():
"""Return vertices and triangle for a box geometry.
Returns
-------
vertices: ndarray
8 vertices coords that composed our box
triangles: ndarray
12 triangles that composed our box
"""
vertices = np.array([[-.5, -.5, -.5],
[-.5, -.5, 0.5],
[-.5, 0.5, -.5],
[-.5, 0.5, 0.5],
[0.5, -.5, -.5],
[0.5, -.5, 0.5],
[0.5, 0.5, -.5],
[0.5, 0.5, 0.5]])
triangles = np.array([[0, 6, 4],
[0, 2, 6],
[0, 3, 2],
[0, 1, 3],
[2, 7, 6],
[2, 3, 7],
[4, 6, 7],
[4, 7, 5],
[0, 4, 5],
[0, 5, 1],
[1, 5, 7],
[1, 7, 3]], dtype='i8')
return vertices, triangles
[docs]def prim_sphere(name='symmetric362', gen_faces=False):
"""Provide vertices and triangles of the spheres.
Parameters
----------
name : str
which sphere - one of:
* 'symmetric362'
* 'symmetric642'
* 'symmetric724'
* 'repulsion724'
* 'repulsion100'
* 'repulsion200'
gen_faces : bool, optional
If True, triangulate a set of vertices on the sphere to get the faces.
Otherwise, we load the saved faces from a file. Default: False
Returns
-------
vertices: ndarray
vertices coords that composed our sphere
triangles: ndarray
triangles that composed our sphere
Examples
--------
>>> import numpy as np
>>> from fury.primitive import prim_sphere
>>> verts, faces = prim_sphere('symmetric362')
>>> verts.shape == (362, 3)
True
>>> faces.shape == (720, 3)
True
"""
fname = SPHERE_FILES.get(name)
if fname is None:
raise ValueError('No sphere called "%s"' % name)
res = np.load(fname)
verts = res['vertices'].copy()
faces = faces_from_sphere_vertices(verts) if gen_faces else res['faces']
faces = fix_winding_order(res['vertices'], faces, clockwise=True)
return res['vertices'], faces
[docs]def prim_superquadric(roundness=(1, 1), sphere_name='symmetric362'):
"""Provide vertices and triangles of a superquadrics.
Parameters
----------
roundness : tuple, optional
parameters (Phi and Theta) that control the shape of the superquadric
sphere_name : str, optional
which sphere - one of:
* 'symmetric362'
* 'symmetric642'
* 'symmetric724'
* 'repulsion724'
* 'repulsion100'
* 'repulsion200'
Returns
-------
vertices: ndarray
vertices coords that composed our sphere
triangles: ndarray
triangles that composed our sphere
Examples
--------
>>> import numpy as np
>>> from fury.primitive import prim_superquadric
>>> verts, faces = prim_superquadric(roundness=(1, 1))
>>> verts.shape == (362, 3)
True
>>> faces.shape == (720, 3)
True
"""
def _fexp(x, p):
"""Return a different kind of exponentiation."""
return np.sign(x) * (np.abs(x) ** p)
sphere_verts, sphere_triangles = prim_sphere(sphere_name)
_, sphere_phi, sphere_theta = cart2sphere(*sphere_verts.T)
phi, theta = roundness
x = _fexp(np.sin(sphere_phi), phi) * _fexp(np.cos(sphere_theta), theta)
y = _fexp(np.sin(sphere_phi), phi) * _fexp(np.sin(sphere_theta), theta)
z = _fexp(np.cos(sphere_phi), phi)
xyz = np.vstack([x, y, z]).T
vertices = np.ascontiguousarray(xyz)
return vertices, sphere_triangles
[docs]def prim_tetrahedron():
"""Return vertices and triangles for a tetrahedron.
This shape has a side length of two units.
Returns
-------
pyramid_vert: numpy.ndarray
4 vertices coordinates
triangles: numpy.ndarray
4 triangles representing the tetrahedron
"""
pyramid_vert = np.array([[0.5, 0.5, 0.5],
[0.5, -0.5, -0.5],
[-0.5, 0.5, -0.5],
[-0.5, -0.5, 0.5]])
pyramid_triag = np.array([[2, 0, 1],
[0, 3, 2],
[0, 3, 1],
[1, 2, 3]], dtype='i8')
return pyramid_vert, pyramid_triag
[docs]def prim_icosahedron():
"""Return vertices and triangles for icosahedron.
Returns
-------
icosahedron_vertices: numpy.ndarray
12 vertices coordinates to the icosahedron
icosahedron_mesh: numpy.ndarray
20 triangles representing the tetrahedron
"""
phi = (1 + math.sqrt(5)) / 2.0
icosahedron_vertices = np.array([[-1.0, 0.0, phi],
[0.0, phi, 1.0],
[1.0, 0.0, phi],
[-phi, 1.0, 0.0],
[0.0, phi, -1.0],
[phi, 1.0, 0.0],
[-phi, -1.0, 0.0],
[0.0, -phi, 1.0],
[phi, -1.0, 0.0],
[-1.0, 0.0, -phi],
[0.0, -phi, -1.0],
[1.0, 0.0, -phi]])
icosahedron_mesh = np.array([[1, 0, 2],
[2, 5, 1],
[5, 4, 1],
[3, 1, 4],
[0, 1, 3],
[0, 6, 3],
[9, 3, 6],
[8, 2, 7],
[2, 0, 7],
[0, 7, 6],
[5, 2, 8],
[11, 5, 8],
[11, 4, 5],
[9, 11, 4],
[4, 3, 9],
[11, 10, 8],
[8, 10, 7],
[6, 7, 10],
[10, 9, 6],
[9, 10, 11]], dtype='i8')
return icosahedron_vertices, icosahedron_mesh