"""Module dedicated for basic primitive."""
from os.path import join as pjoin
from distutils.version import LooseVersion
import numpy as np
from fury.data import DATA_DIR
from fury.transform import cart2sphere
from fury.utils import fix_winding_order
from scipy.spatial import ConvexHull, transform
from scipy.version import short_version
import math
SCIPY_1_4_PLUS = LooseVersion(short_version) >= LooseVersion('1.4.0')
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=(1, 0, 0),
scales=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]
scales : 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,
scales=scales, have_tiled_verts=True)
[docs]def repeat_primitive(vertices, faces, centers, directions=None,
colors=(1, 0, 0), scales=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]
scales : 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 not isinstance(scales, np.ndarray):
scales = np.array(scales)
if scales.ndim == 1:
if scales.size == centers.shape[0]:
scales = np.repeat(scales, unit_verts_size, axis=0)
scales = scales.reshape((big_vertices.shape[0], 1))
elif scales.ndim == 2:
scales = np.repeat(scales, unit_verts_size, axis=0)
big_vertices *= scales
# 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) in [3, 4] \
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 arr is None:
return np.array([])
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)
big_colors *= 255
# update orientations
directions = normalize_input(directions, 'directions')
for pts, dirs in enumerate(directions):
w = np.cos(0.5 * np.pi)
denom = np.linalg.norm(dirs / 2.)
f = (np.sin(0.5 * np.pi) / denom) if denom else 0
dirs = np.append((dirs / 2.) * f, w)
rot = transform.Rotation.from_quat(dirs)
rotation_matrix = rot.as_matrix() if SCIPY_1_4_PLUS else rot.as_dcm()
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
[docs]def prim_rhombicuboctahedron():
"""Return vertices and triangle for rhombicuboctahedron geometry.
Returns
-------
my_vertices: ndarray
vertices coords that composed our rhombicuboctahedron
my_triangles: ndarray
Triangles that composed our rhombicuboctahedron
"""
my_vertices = np.array([[-2, 4, 2],
[-4, 2, 2],
[-4, -2, 2],
[-2, -4, 2],
[2, -4, 2],
[4, -2, 2],
[4, 2, 2],
[2, 4, 2],
[-2, 2, 4],
[-2, -2, 4],
[2, -2, 4],
[2, 2, 4],
[-2, 4, -2],
[-4, 2, -2],
[-4, -2, -2],
[-2, -4, -2],
[2, -4, -2],
[4, -2, -2],
[4, 2, -2],
[2, 4, -2],
[-2, 2, -4],
[-2, -2, -4],
[2, -2, -4],
[2, 2, -4]])
my_triangles = np.array([[0, 1, 8],
[1, 2, 9],
[1, 8, 9],
[2, 3, 9],
[3, 9, 10],
[3, 4, 10],
[4, 10, 5],
[5, 11, 10],
[5, 6, 11],
[6, 7, 11],
[7, 8, 11],
[7, 8, 0],
[8, 9, 10],
[8, 10, 11],
[12, 13, 20],
[13, 14, 21],
[13, 20, 21],
[14, 15, 21],
[15, 21, 22],
[15, 16, 22],
[16, 22, 17],
[17, 22, 23],
[17, 23, 18],
[18, 19, 23],
[19, 20, 23],
[19, 20, 12],
[20, 21, 22],
[20, 22, 23],
[7, 18, 19],
[6, 7, 18],
[6, 17, 18],
[5, 6, 17],
[4, 5, 16],
[5, 16, 17],
[0, 1, 12],
[1, 12, 13],
[1, 2, 13],
[2, 13, 14],
[2, 3, 14],
[3, 14, 15],
[0, 7, 12],
[7, 12, 19],
[3, 15, 16],
[3, 4, 16],
], dtype='i8')
return my_vertices, my_triangles
[docs]def prim_star(dim=2):
"""Return vertices and triangle for star geometry.
Parameters
----------
dim: int
Represents the dimension of the wanted star
Returns
-------
vertices: ndarray
vertices coords that composed our star
triangles: ndarray
Triangles that composed our star
"""
if dim == 2:
vert = np.array([[-2.0, -3.0, 0.0],
[0.0, -2.0, 0.0],
[3.0, -3.0, 0.0],
[2.0, -1.0, 0.0],
[3.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 3.0, 0.0],
[-1.0, 1.0, 0.0],
[-3.0, 1.0, 0.0],
[-2.0, -1.0, 0.0]])
triangles = np.array([[1, 9, 0],
[1, 2, 3],
[3, 4, 5],
[5, 6, 7],
[7, 8, 9],
[1, 9, 3],
[3, 7, 9],
[3, 5, 7]], dtype='i8')
if dim == 3:
vert = np.array([[-2.0, -3.0, 0.0],
[0.0, -2, 0.0],
[3.0, -3.0, 0.0],
[2.0, -1.0, 0.0],
[3.0, 0.5, 0.0],
[1.0, 0.5, 0.0],
[0, 3.0, 0.0],
[-1.0, 0.5, 0.0],
[-3.0, 0.5, 0.0],
[-2.0, -1.0, 0.0],
[0.0, 0.0, 0.5],
[0.0, 0.0, -0.5]])
triangles = np.array([[1, 9, 0],
[1, 2, 3],
[3, 4, 5],
[5, 6, 7],
[7, 8, 9],
[1, 9, 3],
[3, 7, 9],
[3, 5, 7],
[1, 0, 10],
[0, 9, 10],
[10, 9, 8],
[7, 8, 10],
[6, 7, 10],
[5, 6, 10],
[5, 10, 4],
[10, 3, 4],
[3, 10, 2],
[10, 1, 2],
[1, 0, 11],
[0, 9, 11],
[11, 9, 8],
[7, 8, 10],
[6, 7, 11],
[5, 6, 11],
[5, 10, 4],
[11, 3, 4],
[3, 11, 2],
[11, 1, 2]], dtype='i8')
return vert, triangles
[docs]def prim_triangularprism():
"""Return vertices and triangle for a regular triangular prism.
Returns
-------
vertices: ndarray
vertices coords that compose our prism
triangles: ndarray
triangles that compose our prism
"""
# Local variable to represent the square root of three rounded
# to 7 decimal places
three = float('{:.7f}'.format(math.sqrt(3)))
vertices = np.array([[0, -1/three, 1/2],
[-1/2, 1/2/three, 1/2],
[1/2, 1/2/three, 1/2],
[-1/2, 1/2/three, -1/2],
[1/2, 1/2/three, -1/2],
[0, -1/three, -1/2]])
triangles = np.array([[0, 1, 2],
[2, 1, 3],
[2, 3, 4],
[1, 0, 5],
[1, 5, 3],
[0, 2, 4],
[0, 4, 5],
[5, 4, 3]])
triangles = fix_winding_order(vertices, triangles, clockwise=True)
return vertices, triangles
[docs]def prim_octagonalprism():
"""Return vertices and triangle for an octagonal prism.
Returns
-------
vertices: ndarray
vertices coords that compose our prism
triangles: ndarray
triangles that compose our prism
"""
# Local variable to represent the square root of two rounded
# to 7 decimal places
two = float('{:.7f}'.format(math.sqrt(2)))
vertices = np.array([[-1, -(1 + two), -1],
[1, -(1 + two), -1],
[1, (1 + two), -1],
[-1, (1 + two), -1],
[-(1 + two), -1, -1],
[(1 + two), -1, -1],
[(1 + two), 1, -1],
[-(1 + two), 1, -1],
[-1, -(1 + two), 1],
[1, -(1 + two), 1],
[1, (1 + two), 1],
[-1, (1 + two), 1],
[-(1 + two), -1, 1],
[(1 + two), -1, 1],
[(1 + two), 1, 1],
[-(1 + two), 1, 1]])
triangles = np.array([[0, 8, 9],
[9, 1, 0],
[5, 13, 9],
[9, 1, 5],
[3, 11, 10],
[10, 2, 3],
[2, 10, 14],
[14, 6, 2],
[5, 13, 14],
[14, 6, 5],
[7, 15, 11],
[11, 3, 7],
[7, 15, 12],
[12, 4, 7],
[0, 8, 12],
[12, 4, 0],
[0, 3, 4],
[3, 4, 7],
[0, 3, 1],
[1, 2, 3],
[2, 5, 6],
[5, 2, 1],
[8, 11, 12],
[11, 12, 15],
[8, 11, 9],
[9, 10, 11],
[10, 13, 14],
[13, 10, 9]], dtype='u8')
vertices /= 4
triangles = fix_winding_order(vertices, triangles, clockwise=True)
return vertices, triangles
[docs]def prim_frustum():
"""Return vertices and triangle for a square frustum prism.
Returns
-------
vertices: ndarray
vertices coords that compose our prism
triangles: ndarray
triangles that compose our prism
"""
vertices = np.array([[-.5, -.5, .5],
[.5, -.5, .5],
[.5, .5, .5],
[-.5, .5, .5],
[-1, -1, -.5],
[1, -1, -.5],
[1, 1, -.5],
[-1, 1, -.5]])
triangles = np.array([[4, 6, 5],
[6, 4, 7],
[0, 2, 1],
[2, 0, 3],
[4, 3, 0],
[3, 4, 7],
[7, 2, 3],
[2, 7, 6],
[6, 1, 2],
[1, 6, 5],
[5, 0, 1],
[0, 5, 4]], dtype='u8')
vertices /= 2
triangles = fix_winding_order(vertices, triangles, clockwise=True)
return vertices, triangles
[docs]def prim_cylinder(radius=0.5, height=1, sectors=36, capped=True):
"""Return vertices and triangles for a cylinder.
Parameters
----------
radius: float
Radius of the cylinder
height: float
Height of the cylinder
sectors: int
Sectors in the cylinder
capped: bool
Whether the cylinder is capped at both ends or open
Returns
-------
vertices: ndarray
vertices coords that compose our cylinder
triangles: ndarray
triangles that compose our cylinder
"""
if not isinstance(sectors, int):
raise TypeError("Only integers are allowed for sectors parameter")
if not sectors > 7:
raise ValueError("Sectors parameter should be greater than 7")
sector_step = 2 * math.pi / sectors
unit_circle_vertices = []
# generate a unit circle on XY plane
for i in range(sectors + 1):
sector_angle = i * sector_step
unit_circle_vertices.append(math.cos(sector_angle))
unit_circle_vertices.append(0)
unit_circle_vertices.append(math.sin(sector_angle))
vertices = []
# generate vertices for a cylinder
for i in range(2):
h = -height / 2 + i * height
k = 0
for j in range(sectors + 1):
ux = unit_circle_vertices[k]
uz = unit_circle_vertices[k + 2]
# position vector
vertices.append(ux * radius)
vertices.append(h)
vertices.append(uz * radius)
k += 3
# base and top circle vertices
base_center_index = None
top_center_index = None
if capped:
base_center_index = int(len(vertices) / 3)
top_center_index = base_center_index + sectors + 1
for i in range(2):
h = -height / 2 + i * height
vertices.append(0)
vertices.append(h)
vertices.append(0)
k = 0
for j in range(sectors):
ux = unit_circle_vertices[k]
uz = unit_circle_vertices[k + 2]
# position vector
vertices.append(ux * radius)
vertices.append(h)
vertices.append(uz * radius)
k += 3
if capped:
vertices = (np.array(vertices).reshape(2 * (sectors + 1) + 2 * sectors + 2, 3))
else:
vertices = (np.array(vertices).reshape(2 * (sectors + 1), 3))
triangles = []
k1 = 0
k2 = sectors + 1
# triangles for the side surface
for i in range(sectors):
triangles.append(k1)
triangles.append(k2)
triangles.append(k1 + 1)
triangles.append(k2)
triangles.append(k2 + 1)
triangles.append(k1 + 1)
k1 += 1
k2 += 1
if capped:
k = base_center_index + 1
for i in range(sectors):
if i < sectors - 1:
triangles.append(base_center_index)
triangles.append(k)
triangles.append(k + 1)
else:
triangles.append(base_center_index)
triangles.append(k)
triangles.append(base_center_index + 1)
k += 1
k = top_center_index + 1
for i in range(sectors):
if i < sectors - 1:
triangles.append(top_center_index)
triangles.append(k + 1)
triangles.append(k)
else:
triangles.append(top_center_index)
triangles.append(top_center_index + 1)
triangles.append(k)
k += 1
if capped:
triangles = (np.array(triangles).reshape(4 * sectors, 3))
else:
triangles = (np.array(triangles).reshape(2 * sectors, 3))
return vertices, triangles