Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ellipsoid actor implemented with SDF #791

Merged
merged 11 commits into from Jul 19, 2023
73 changes: 73 additions & 0 deletions fury/actor.py
Expand Up @@ -10,6 +10,7 @@
from fury import layout
from fury.actors.odf_slicer import OdfSlicerActor
from fury.actors.peak import PeakActor
from fury.actors.tensor import tensor_ellipsoid
from fury.colormap import colormap_lookup_table
from fury.deprecator import deprecate_with_version, deprecated_params
from fury.io import load_image
Expand Down Expand Up @@ -3795,3 +3796,75 @@
shader_to_actor(sq_actor, 'fragment', impl_code=fs_impl_code, block='light')

return sq_actor


def ellipsoid(
centers,
axes,
lengths,
colors=(1, 0, 0),
scales=1.0,
opacity=1.0
):
"""
VTK actor for visualizing ellipsoids.

Parameters
----------
centers : ndarray(N, 3)
Ellipsoid positions.
axes : ndarray (3, 3) or (N, 3, 3)
Axes of the ellipsoid.
lengths : ndarray (3, ) or (N, 3)
Axes lengths.
colors : ndarray (N,3) or tuple (3,), optional
Default red color. R, G and B should be in the range [0, 1].
scales : float or ndarray (N, ), optional
Ellipsoid size, default(1.0).
opacity : float, optional
Takes values from 0 (fully transparent) to 1 (opaque), default(1.0).

Returns
-------
tensor_ellipsoid: Actor

"""

if not isinstance(centers, np.ndarray):
centers = np.array(centers)
if centers.ndim == 1:
centers = np.array([centers])

if not isinstance(axes, np.ndarray):
axes = np.array(axes)
if axes.ndim == 2:
axes = np.array([axes])

Check warning on line 3841 in fury/actor.py

View check run for this annotation

Codecov / codecov/patch

fury/actor.py#L3841

Added line #L3841 was not covered by tests
if axes.shape[0] != centers.shape[0]:
raise ValueError('number of axes defined does not match with number of'
'centers')

if not isinstance(lengths, np.ndarray):
lengths = np.array(lengths)

Check warning on line 3847 in fury/actor.py

View check run for this annotation

Codecov / codecov/patch

fury/actor.py#L3847

Added line #L3847 was not covered by tests
if lengths.ndim == 1:
lengths = np.array([lengths])

Check warning on line 3849 in fury/actor.py

View check run for this annotation

Codecov / codecov/patch

fury/actor.py#L3849

Added line #L3849 was not covered by tests
if lengths.shape[0] != centers.shape[0]:
raise ValueError('number of lengths defined does not match with number'

Check warning on line 3851 in fury/actor.py

View check run for this annotation

Codecov / codecov/patch

fury/actor.py#L3851

Added line #L3851 was not covered by tests
'of centers')

if not isinstance(scales, np.ndarray):
scales = np.array(scales)
if scales.size == 1:
scales = np.repeat(scales, centers.shape[0])
elif scales.size != centers.shape[0]:
scales = np.concatenate(

Check warning on line 3859 in fury/actor.py

View check run for this annotation

Codecov / codecov/patch

fury/actor.py#L3859

Added line #L3859 was not covered by tests
(scales, np.ones(centers.shape[0] - scales.shape[0])), axis=None)

if isinstance(colors, tuple):
colors = np.array([colors])

Check warning on line 3863 in fury/actor.py

View check run for this annotation

Codecov / codecov/patch

fury/actor.py#L3863

Added line #L3863 was not covered by tests
elif not isinstance(colors, np.ndarray):
colors = np.array(colors)

Check warning on line 3865 in fury/actor.py

View check run for this annotation

Codecov / codecov/patch

fury/actor.py#L3865

Added line #L3865 was not covered by tests
if colors.shape[1] == 4:
colors = colors[:, :-1]

Check warning on line 3867 in fury/actor.py

View check run for this annotation

Codecov / codecov/patch

fury/actor.py#L3867

Added line #L3867 was not covered by tests

return tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity)

213 changes: 213 additions & 0 deletions fury/actors/tensor.py
@@ -0,0 +1,213 @@
import os

import numpy as np

from fury import actor
from fury.shaders import (attribute_to_actor, compose_shader,
import_fury_shader, shader_to_actor)

def tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity):
tvcastillod marked this conversation as resolved.
Show resolved Hide resolved
"""
Visualize one or many Tensor Ellipsoids with different features.

Parameters
----------
centers : ndarray(N, 3)
Tensor ellipsoid positions.
axes : ndarray (3, 3) or (N, 3, 3)
tvcastillod marked this conversation as resolved.
Show resolved Hide resolved
Axes of the tensor ellipsoid.
lengths : ndarray (3, ) or (N, 3)
tvcastillod marked this conversation as resolved.
Show resolved Hide resolved
Axes lengths.
colors : ndarray (N,3) or tuple (3,)
R, G and B should be in the range [0, 1].
scales : float or ndarray (N, )
Tensor ellipsoid size.
opacity : float
Takes values from 0 (fully transparent) to 1 (opaque).
tvcastillod marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
box_actor: Actor

"""

box_actor = actor.box(centers, colors=colors, scales=scales)
box_actor.GetMapper().SetVBOShiftScaleMethod(False)
box_actor.GetProperty().SetOpacity(opacity)

# Number of vertices that make up the box
n_verts = 8

big_centers = np.repeat(centers, n_verts, axis=0)
attribute_to_actor(box_actor, big_centers, 'center')

big_scales = np.repeat(scales, n_verts, axis=0)
attribute_to_actor(box_actor, big_scales, 'scale')

big_values = np.repeat(np.array(lengths, dtype=float), n_verts, axis=0)
attribute_to_actor(box_actor, big_values, 'evals')

evec1 = np.array([item[0] for item in axes])
evec2 = np.array([item[1] for item in axes])
evec3 = np.array([item[2] for item in axes])

big_vectors_1 = np.repeat(evec1, n_verts, axis=0)
attribute_to_actor(box_actor, big_vectors_1, 'evec1')
big_vectors_2 = np.repeat(evec2, n_verts, axis=0)
attribute_to_actor(box_actor, big_vectors_2, 'evec2')
big_vectors_3 = np.repeat(evec3, n_verts, axis=0)
attribute_to_actor(box_actor, big_vectors_3, 'evec3')

# Start of shader implementation

vs_dec = \
"""
in vec3 center;
in float scale;
in vec3 evals;
in vec3 evec1;
in vec3 evec2;
in vec3 evec3;

out vec4 vertexMCVSOutput;
out vec3 centerMCVSOutput;
out float scaleVSOutput;
out vec3 evalsVSOutput;
out mat3 tensorMatrix;
"""

# Variables assignment
v_assign = \
"""
vertexMCVSOutput = vertexMC;
centerMCVSOutput = center;
scaleVSOutput = scale;
"""

# Normalization
n_evals = "evalsVSOutput = evals/(max(evals.x, max(evals.y, evals.z)));"

# Values constraint to avoid incorrect visualizations
evals = "evalsVSOutput = clamp(evalsVSOutput,0.05,1);"
tvcastillod marked this conversation as resolved.
Show resolved Hide resolved

# Scaling matrix
sc_matrix = \
"""
mat3 S = mat3(1/evalsVSOutput.x, 0.0, 0.0,
0.0, 1/evalsVSOutput.y, 0.0,
0.0, 0.0, 1/evalsVSOutput.z);
"""

# Rotation matrix
rot_matrix = "mat3 R = mat3(evec1, evec2, evec3);"

# Tensor matrix
t_matrix = "tensorMatrix = inverse(R) * S * R;"

vs_impl = compose_shader([v_assign, n_evals, evals, sc_matrix, rot_matrix,
t_matrix])

# Adding shader implementation to actor
shader_to_actor(box_actor, 'vertex', decl_code=vs_dec, impl_code=vs_impl)

fs_vars_dec = \
"""
in vec4 vertexMCVSOutput;
in vec3 centerMCVSOutput;
in float scaleVSOutput;
in vec3 evalsVSOutput;
in mat3 tensorMatrix;

uniform mat4 MCVCMatrix;
"""

# Importing the sphere SDF
sd_sphere = import_fury_shader(os.path.join('sdf', 'sd_sphere.frag'))

# SDF definition
sdf_map = \
"""
float map(in vec3 position)
{
/*
As the scaling is not a rigid body transformation, we multiply
by a factor to compensate for distortion and not overestimate
the distance.
*/
float scFactor = min(evalsVSOutput.x, min(evalsVSOutput.y,
evalsVSOutput.z));

/*
The approximation of distance is calculated by stretching the
space such that the ellipsoid becomes a sphere (multiplying by
the transformation matrix) and then computing the distance to
a sphere in that space (using the sphere SDF).
*/
return sdSphere(tensorMatrix * (position - centerMCVSOutput),
scaleVSOutput*0.48) * scFactor;
tvcastillod marked this conversation as resolved.
Show resolved Hide resolved
}
"""

# Importing central differences function for computing surface normals
central_diffs_normal = import_fury_shader(os.path.join(
'sdf', 'central_diffs.frag'))

# Importing raymarching function
cast_ray = import_fury_shader(os.path.join(
'ray_marching', 'cast_ray.frag'))

# Importing Blinn-Phong model for lighting
blinn_phong_model = import_fury_shader(os.path.join(
'lighting', 'blinn_phong_model.frag'))

# Full fragment shader declaration
fs_dec = compose_shader([fs_vars_dec, sd_sphere, sdf_map,
central_diffs_normal, cast_ray,
blinn_phong_model])

shader_to_actor(box_actor, 'fragment', decl_code=fs_dec)

# Vertex in Model Coordinates.
point = "vec3 point = vertexMCVSOutput.xyz;"

# Camera position in world space
ray_origin = "vec3 ro = (-MCVCMatrix[3] * MCVCMatrix).xyz;"

ray_direction = "vec3 rd = normalize(point - ro);"

light_direction = "vec3 ld = normalize(ro - point);"

ray_origin_update = "ro += point - ro;"

# Total distance traversed along the ray
distance = "float t = castRay(ro, rd);"

# Fragment shader output definition
# If surface is detected, color is assigned, otherwise, nothing is painted
frag_output_def = \
"""
if(t < 20)
{
vec3 pos = ro + t * rd;
vec3 normal = centralDiffsNormals(pos, .0001);
// Light Attenuation
float la = dot(ld, normal);
vec3 color = blinnPhongIllumModel(la, lightColor0,
diffuseColor, specularPower, specularColor, ambientColor);
fragOutput0 = vec4(color, opacity);
}
else
{
discard;
}
"""

# Full fragment shader implementation
sdf_frag_impl = compose_shader([point, ray_origin, ray_direction,
light_direction, ray_origin_update,
distance, frag_output_def])

shader_to_actor(box_actor, 'fragment', impl_code=sdf_frag_impl,
block='light')

return box_actor
39 changes: 39 additions & 0 deletions fury/tests/test_actors.py
Expand Up @@ -1733,6 +1733,45 @@ def test_marker_actor(interactive=False):
npt.assert_equal(report.objects, 12)


def test_ellipsoid_actor(interactive=False):
# number of axes does not match with number of centers
centers = [-1, 1, 0]
axes = [[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[1, 2, -2], [2, 1, 2], [2, -2, -1]]]
lengths = [[1, 1, 1]]
npt.assert_raises(ValueError, actor.ellipsoid, centers, axes, lengths)

# number of lengths does not match with number of centers
lengths = [[1, 1, 1], [1, 1, .5]]
npt.assert_raises(ValueError, actor.ellipsoid, centers, axes, lengths)

scene = window.Scene()
scene.background((0, 0, 0))

axes = np.array([[[-.6, .5, -.6], [-.8, -.4, .5], [-.1, -.7, -.7]],
[[.1, .6, -.8], [.6, .5, .5], [-.8, .6, .3]],
[[.7, .5, -.5], [0, -.7, -.7], [-.7, .6, -.5]],
[[.7, -.3, -.6], [.2, -.8, .6], [.7, .6, .5]],
[[1, 2, -2], [2, 1, 2], [2, -2, -1]],
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]])
lengths = np.array([[1, 1, 1], [1, 1, .5], [1, .5, .5],
[1, .5, .25], [1, 1, .3], [1, .3, .3]])
centers = np.array([[-1, 1, 0], [0, 1, 0], [1, 1, 0],
[-1, 0, 0], [0, 0, 0], [1, 0, 0]])
colors = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1],
[1, 1, 0], [1, 0, 1], [0, 1, 1]])

ellipsoids = actor.ellipsoid(axes=axes, lengths=lengths, centers=centers,
scales=1.0, colors=colors)
scene.add(ellipsoids)

if interactive:
window.show(scene)

report = window.analyze_scene(scene)
npt.assert_equal(report.actors, 1)


def test_actors_primitives_count():
centers = np.array([[1, 1, 1], [2, 2, 2]])
directions = np.array([[1, 0, 0], [1, 0, 0]])
Expand Down