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

Standardize actors colors #773

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion fury/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
set_polydata_triangles,
set_polydata_vertices,
shallow_copy,
normalize_color,
)


Expand Down Expand Up @@ -383,6 +384,7 @@ def surface(vertices, faces=None, colors=None, smooth=None, subdivision=3):
triangle_poly_data.SetPoints(points)

if colors is not None:
colors = normalize_color(colors)
triangle_poly_data.GetPointData().SetScalars(numpy_to_vtk_colors(255 * colors))

if faces is None:
Expand Down Expand Up @@ -528,6 +530,9 @@ def contour_from_roi(data, affine=None, color=np.array([1, 0, 0]), opacity=1):
skin_actor = Actor()

skin_actor.SetMapper(skin_mapper)

color = normalize_color(color)

skin_actor.GetProperty().SetColor(color[0], color[1], color[2])
skin_actor.GetProperty().SetOpacity(opacity)

Expand Down Expand Up @@ -570,6 +575,8 @@ def contour_from_label(data, affine=None, color=None):
elif color.shape != (nb_surfaces, 3) and color.shape != (nb_surfaces, 4):
raise ValueError('Incorrect color array shape')

color = normalize_color(color)

if color.shape == (nb_surfaces, 4):
opacity = color[:, -1]
color = color[:, :-1]
Expand Down Expand Up @@ -680,6 +687,7 @@ def streamtube(

"""
# Poly data with lines and colors
colors = normalize_color(colors)
poly_data, color_is_scalar = lines_to_vtk_polydata(lines, colors)
next_input = poly_data

Expand Down Expand Up @@ -831,6 +839,7 @@ def line(

"""
# Poly data with lines and colors
colors = normalize_color(colors)
poly_data, color_is_scalar = lines_to_vtk_polydata(lines, colors)
next_input = poly_data

Expand Down Expand Up @@ -948,6 +957,8 @@ def axes(
dirs = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
colors = np.array([colorx + (opacity,), colory + (opacity,), colorz + (opacity,)])

colors = normalize_color(colors)

scales = np.asarray(scale)
arrow_actor = arrow(centers, dirs, colors, scales, repeat_primitive=False)
return arrow_actor
Expand Down Expand Up @@ -1690,6 +1701,7 @@ def dot(points, colors=None, opacity=None, dot_size=5):
vtk_faces.InsertNextCell(1)
vtk_faces.InsertCellPoint(idd)

colors = normalize_color(colors)
color_tuple = color_check(len(points), colors)
color_array, global_opacity = color_tuple

Expand Down Expand Up @@ -1757,6 +1769,9 @@ def point(points, colors, point_radius=0.1, phi=8, theta=8, opacity=1.0):
>>> # window.show(scene)

"""

colors = normalize_color(colors)

return sphere(
centers=points,
colors=colors,
Expand Down Expand Up @@ -1818,6 +1833,7 @@ def sphere(
>>> # window.show(scene)

"""
colors = normalize_color(colors)
if not use_primitive:
src = SphereSource() if faces is None else None

Expand Down Expand Up @@ -1915,6 +1931,7 @@ def cylinder(
>>> # window.show(scene)

"""
colors = normalize_color(colors)
if repeat_primitive:

if resolution < 8:
Expand Down Expand Up @@ -2030,6 +2047,8 @@ def disk(
src = None
rotate = None

colors = normalize_color(colors)

disk_actor = repeat_sources(
centers=centers,
colors=colors,
Expand Down Expand Up @@ -2072,6 +2091,7 @@ def square(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1):
>>> # window.show(scene)

"""
colors = normalize_color(colors)
verts, faces = fp.prim_square()
res = fp.repeat_primitive(
verts,
Expand Down Expand Up @@ -2124,6 +2144,7 @@ def rectangle(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 0))
>>> # window.show(scene)

"""
colors = normalize_color(colors)
return square(centers=centers, directions=directions, colors=colors, scales=scales)


Expand Down Expand Up @@ -2157,6 +2178,7 @@ def box(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 3)):
>>> # window.show(scene)

"""
colors = normalize_color(colors)
verts, faces = fp.prim_box()
res = fp.repeat_primitive(
verts,
Expand Down Expand Up @@ -2205,6 +2227,7 @@ def cube(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1):
>>> # window.show(scene)

"""
colors = normalize_color(colors)
return box(centers=centers, directions=directions, colors=colors, scales=scales)


Expand Down Expand Up @@ -2265,6 +2288,7 @@ def arrow(
>>> # window.show(scene)

"""
colors = normalize_color(colors)
if repeat_primitive:
vertices, faces = fp.prim_arrow()
res = fp.repeat_primitive(
Expand Down Expand Up @@ -2352,6 +2376,7 @@ def cone(
>>> # window.show(scene)

"""
colors = normalize_color(colors)
if not use_primitive:
src = ConeSource() if faces is None else None

Expand Down Expand Up @@ -2416,6 +2441,7 @@ def triangularprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1):
>>> # window.show(scene)

"""
colors = normalize_color(colors)
verts, faces = fp.prim_triangularprism()
res = fp.repeat_primitive(
verts,
Expand Down Expand Up @@ -2464,6 +2490,7 @@ def rhombicuboctahedron(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=
>>> # window.show(scene)

"""
colors = normalize_color(colors)
verts, faces = fp.prim_rhombicuboctahedron()
res = fp.repeat_primitive(
verts,
Expand Down Expand Up @@ -2513,6 +2540,7 @@ def pentagonalprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1):
>>> # window.show(scene)

"""
colors = normalize_color(colors)
verts, faces = fp.prim_pentagonalprism()
res = fp.repeat_primitive(
verts,
Expand Down Expand Up @@ -2562,6 +2590,7 @@ def octagonalprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1):
>>> # window.show(scene)

"""
colors = normalize_color(colors)
verts, faces = fp.prim_octagonalprism()
res = fp.repeat_primitive(
verts,
Expand Down Expand Up @@ -2611,6 +2640,7 @@ def frustum(centers, directions=(1, 0, 0), colors=(0, 1, 0), scales=1):
>>> # window.show(scene)

"""
colors = normalize_color(colors)
verts, faces = fp.prim_frustum()
res = fp.repeat_primitive(
verts,
Expand Down Expand Up @@ -2682,6 +2712,8 @@ def have_2_dimensions(arr):
else:
roundness = np.array(roundness)

colors = normalize_color(colors)

res = fp.repeat_primitive_function(
func=fp.prim_superquadric,
centers=centers,
Expand Down Expand Up @@ -2744,6 +2776,7 @@ def billboard(
-------
billboard_actor: Actor
"""
colors = normalize_color(colors)
verts, faces = fp.prim_square()
res = fp.repeat_primitive(
verts, faces, centers=centers, colors=colors, scales=scales
Expand Down Expand Up @@ -2954,6 +2987,7 @@ def add_to_scene(scene):

texta.SetMapper(textm)

color = normalize_color(color)
texta.GetProperty().SetColor(color)

# Set ser rotation origin to the center of the text is following the camera
Expand Down Expand Up @@ -3076,6 +3110,8 @@ def get_position(self):
text_actor.set_position(position)
text_actor.font_family(font_family)
text_actor.font_style(bold, italic, shadow)

color = normalize_color(color)
text_actor.color(color)
text_actor.justification(justification)
text_actor.vertical_justification(vertical_justification)
Expand Down Expand Up @@ -3377,7 +3413,7 @@ def texture(rgb, interp=True):
act: Actor

"""
arr = rgb
arr = normalize_color(rgb)
grid = rgb_to_vtk(np.ascontiguousarray(arr))

Y, X = arr.shape[:2]
Expand Down Expand Up @@ -3558,6 +3594,7 @@ def sdf(centers, directions=(1, 0, 0), colors=(1, 0, 0), primitives='torus', sca
"""
prims = {'sphere': 1, 'torus': 2, 'ellipsoid': 3, 'capsule': 4}

colors = normalize_color(colors)
verts, faces = fp.prim_box()
repeated = fp.repeat_primitive(
verts,
Expand Down Expand Up @@ -3670,6 +3707,7 @@ def markers(
>>> # window.show(scene, size=(600, 600))

"""
colors = normalize_color(colors)
n_markers = centers.shape[0]
verts, faces = fp.prim_square()
res = fp.repeat_primitive(
Expand Down
44 changes: 41 additions & 3 deletions fury/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ def test_color_check():
npt.assert_equal(global_opacity, 1)

points = np.array([[0, 0, 0], [0, 1, 0], [1, 0, 0]])
colors = (1, 1, 1, 0.5)
colors = np.array([1, 1, 1, 0.5])

color_tuple = color_check(len(points), colors)
color_array, global_opacity = color_tuple
Expand All @@ -848,7 +848,7 @@ def test_color_check():
npt.assert_equal(global_opacity, 0.5)

points = np.array([[0, 0, 0], [0, 1, 0], [1, 0, 0]])
colors = (1, 0, 0)
colors = np.array([1, 0, 0])

color_tuple = color_check(len(points), colors)
color_array, global_opacity = color_tuple
Expand All @@ -865,6 +865,44 @@ def test_color_check():
npt.assert_equal(global_opacity, 1)


def test_normalize_color():
# Test input None in color
none_color = None
assert utils.normalize_color(none_color) == None

# Test 2d input data
valid_color_array = np.array([[0.1, 0.2, 0.3, 0.4], [0.4, 0.5, 0.6, 0.7]])
outbound_color_array = [[0.1, 0.2, 1.0, 0.3], [255, 255, 255, 0.7]]
outbound_color_array_expected = np.array(
[[0.1, 0.2, 1.0, 0.3], [1.0, 1.0, 1.0, 0.7]])

# Test for valid 2d input
npt.assert_array_equal(utils.normalize_color(
valid_color_array), valid_color_array)

# Test for invalid 2d input
npt.assert_array_equal(utils.normalize_color(
outbound_color_array), outbound_color_array_expected)

# Test for input of type tuple
color_tuple = (0.1, 0.2, 0.3)
color_tuple_expected = np.array([0.1, 0.2, 0.3])
npt.assert_array_equal(utils.normalize_color(color_tuple), color_tuple_expected)

# Test for input of type list
color_list = [100, 150, 200, 0.4]
Clarkszw marked this conversation as resolved.
Show resolved Hide resolved
color_list_expected = np.array(color_list)
color_list_expected[:3] = color_list_expected[:3]/255.0
npt.assert_array_equal(
utils.normalize_color(color_list), color_list_expected)

# Test for input of type 1d np.array
color_1d = np.array([0.1, 0.5, 0.9, 0.3])
color_1d_expected = np.array([0.1, 0.5, 0.9, 0.3])
npt.assert_array_equal(
utils.normalize_color(color_1d), color_1d_expected)


def test_is_ui():
panel = Panel2D(position=(0, 0), size=(100, 100))
valid_ui = DummyUI(act=[])
Expand All @@ -885,7 +923,7 @@ def test_empty_array_to_polydata():
npt.assert_raises(ValueError, utils.lines_to_vtk_polydata, lines)


@pytest.mark.skipif(not have_dipy, reason='Requires DIPY')
@ pytest.mark.skipif(not have_dipy, reason='Requires DIPY')
def test_empty_array_sequence_to_polydata():
from dipy.tracking.streamline import Streamlines

Expand Down
52 changes: 51 additions & 1 deletion fury/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
import warnings
from scipy.ndimage import map_coordinates

from fury.colormap import line_colors
Expand Down Expand Up @@ -1532,7 +1533,7 @@ def color_check(pts_len, colors=None):
# Automatic RGB colors
colors = np.asarray((1, 1, 1))
color_array = numpy_to_vtk_colors(np.tile(255 * colors, (pts_len, 1)))
elif type(colors) is tuple:
elif colors.shape in [(3,), (4,)]:
global_opacity = 1 if len(colors) == 3 else colors[3]
colors = np.asarray(colors)
color_array = numpy_to_vtk_colors(np.tile(255 * colors, (pts_len, 1)))
Expand All @@ -1547,6 +1548,55 @@ def color_check(pts_len, colors=None):
return color_array, global_opacity


def normalize_color(color_array):
"""
Normalize an array of RGB or RGBA color values to be within the range
[0, 1].

If any values are out of bounds, normalize the color array by scaling all
color values to fit within the valid range.

Parameters
----------
color_array : ndarray (N,3) or (N, 4) or tuple (3,) or tuple (4,)
An array of RGB or RGBA color values, where each row represents
a single color as an array of shape (3,) or (4,). Alternatively,
a tuple of length 3 or 4 can be passed to represent a single color.

Returns
-------
color_array : ndarray (N,3) or (N, 4) or tuple (3,) or tuple (4,)
The original array if all values are within the range [0,1].
If any values in the input array were out of bounds, a new array
or tuple is returned with the colors normalized to fit within the
valid range.

"""
# Keep the option for color = None for some actors
if color_array is None:
return color_array

# Convert tuple or list to ndarray
if isinstance(color_array, (tuple, list)):
color_array = np.asarray(color_array)

# Normalize the out of bounds array
if color_array.ndim == 1 and np.any(color_array > 1):
print(
f"{color_array} in the color array are outside the valid range [0, 1]")
color_array[:3] = color_array[:3] / 255.0
print("It has been normalized to fit the range.")

elif color_array.ndim == 2:
for i, row in enumerate(color_array):
if np.any(row > 1):
print(f"{row} in the color array are outside the valid range [0, 1]")
color_array[i, :3] = color_array[i, :3] / 255.0
print("It has been normalized to fit the range.")

return color_array


def is_ui(actor):
"""Method to check if the passed actor is `UI` or `vtkProp3D`

Expand Down