diff --git a/fury/actor.py b/fury/actor.py index e3214c360..b04afe250 100644 --- a/fury/actor.py +++ b/fury/actor.py @@ -80,6 +80,7 @@ set_polydata_triangles, set_polydata_vertices, shallow_copy, + normalize_color, ) @@ -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: @@ -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) @@ -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] @@ -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 @@ -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 @@ -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 @@ -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 @@ -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, @@ -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 @@ -1915,6 +1931,7 @@ def cylinder( >>> # window.show(scene) """ + colors = normalize_color(colors) if repeat_primitive: if resolution < 8: @@ -2030,6 +2047,8 @@ def disk( src = None rotate = None + colors = normalize_color(colors) + disk_actor = repeat_sources( centers=centers, colors=colors, @@ -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, @@ -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) @@ -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, @@ -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) @@ -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( @@ -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 @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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 @@ -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 @@ -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) @@ -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] @@ -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, @@ -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( diff --git a/fury/tests/test_utils.py b/fury/tests/test_utils.py index 9216291f1..872fedde6 100644 --- a/fury/tests/test_utils.py +++ b/fury/tests/test_utils.py @@ -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 @@ -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 @@ -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] + 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=[]) @@ -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 diff --git a/fury/utils.py b/fury/utils.py index be62ce104..ec47d6af6 100644 --- a/fury/utils.py +++ b/fury/utils.py @@ -1,4 +1,5 @@ import numpy as np +import warnings from scipy.ndimage import map_coordinates from fury.colormap import line_colors @@ -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))) @@ -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`