From 74b3665d25a0e678d8a72179957f40ad8a4ffc6c Mon Sep 17 00:00:00 2001 From: devmessias Date: Fri, 13 Aug 2021 11:21:41 -0300 Subject: [PATCH 1/2] feature: node labels --- helios/backends/fury/actors.py | 223 ++++++++++++++++++++++++++++++++- helios/backends/labels.py | 33 +++++ requirements.txt | 2 +- tests/test_text.py | 63 ++++++++++ 4 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 helios/backends/labels.py create mode 100644 tests/test_text.py diff --git a/helios/backends/fury/actors.py b/helios/backends/fury/actors.py index 6920790..94b3388 100644 --- a/helios/backends/fury/actors.py +++ b/helios/backends/fury/actors.py @@ -5,7 +5,7 @@ """ - +import vtk import numpy as np from fury.shaders import add_shader_callback, attribute_to_actor from fury.shaders import shader_to_actor, load @@ -14,20 +14,241 @@ from fury.utils import vertices_from_actor, array_from_actor from fury.utils import update_actor from fury.actor import line as line_actor +from fury.utils import one_chanel_to_vtk try: from fury.shaders import shader_apply_effects except ImportError: shader_apply_effects = None from fury import window +from fury import text_tools from helios.backends.fury.tools import Uniform, Uniforms +from helios.backends.labels import pad_labels _MARKER2Id = { 'o': 0, 's': 1, 'd': 2, '^': 3, 'p': 4, 'h': 5, 's6': 6, 'x': 7, '+': 8, '3d': 0} +class FurySuperLabels: + def __init__( + self, + positions, + labels, + colors=(0, 1, 0), + scales=1, + align='center', + x_offset_ratio=1., + y_offset_ratio=1., + min_label_size=None, + ): + + self._min_label_size = min_label_size + if min_label_size is not None: + labels = pad_labels(labels, min_label_size, align=align) + + self._labels = labels + self._scales = scales + self.align = align + self.x_offset_ratio = x_offset_ratio + self.y_offset_ratio = y_offset_ratio + self._label_center = positions + self._label_count = self._label_center.shape[0] + self._should_update_labels = False + self._should_update_positions = False + self._img_arr, self._char2pos = text_tools.create_bitmap_font() + self._init_actor( + colors, scales) + + self.uniforms_list = [] + + if len(self.uniforms_list) > 0: + self.Uniforms = Uniforms(self.uniforms_list) + self.uniforms_observerId = add_shader_callback( + self.vtk_actor, self.Uniforms) + + self._init_shader_frag() + + def _init_actor(self, colors, scales): + + padding, labels_positions,\ + uv, relative_sizes = \ + text_tools.get_positions_labels_billboards( + self._labels, self._label_center, self._char2pos, scales, + align=self.align, + x_offset_ratio=self.x_offset_ratio, + y_offset_ratio=self.y_offset_ratio) + # to avoid memory corruption + num_chars = labels_positions.shape[0] + self._vcount = num_chars + centers = np.zeros((num_chars, 3)) + + verts, faces = fp.prim_square() + res = fp.repeat_primitive( + verts, faces, centers=centers, + colors=colors, + scales=scales) + + big_verts, big_faces, big_colors, big_centers = res + actor = get_actor_from_primitive( + big_verts, big_faces, big_colors) + actor.GetMapper().SetVBOShiftScaleMethod(False) + actor.GetProperty().BackfaceCullingOff() + + attribute_to_actor(actor, big_centers, 'center') + + self._centers_geo = array_from_actor(actor, array_name="center") + self._centers_geo_orig = np.array(self._centers_geo) + self._centers_length = int(self._centers_geo.shape[0] / num_chars) + self._verts_geo = vertices_from_actor(actor) + self._verts_geo_orig = np.array(self._verts_geo) + + self._colors_geo = array_from_actor(actor, array_name="colors") + + self.vtk_actor = actor + img_vtk = one_chanel_to_vtk(self._img_arr) + tex = vtk.vtkTexture() + tex.SetInputDataObject(img_vtk) + tex.Update() + self.vtk_actor.GetProperty().SetTexture('charactersTexture', tex) + + attribute_to_actor( + self.vtk_actor, + uv, + 'vUV') + attribute_to_actor( + self.vtk_actor, + relative_sizes, + 'vRelativeSize') + padding = np.repeat(padding, 4, axis=0) + attribute_to_actor( + self.vtk_actor, + padding, + 'vPadding') + + self._uv = array_from_actor( + self.vtk_actor, array_name="vUV") + self._relative_sizes = array_from_actor( + self.vtk_actor, array_name="vRelativeSize") + self._padding = array_from_actor( + self.vtk_actor, array_name="vPadding") + + self._centers_geo[:] = np.repeat( + labels_positions, self._centers_length, axis=0) + self._verts_geo[:] = self._verts_geo_orig + self._centers_geo + self.update() + + @property + def shader_dec_vert(self): + shader = load("billboard_dec.vert") + shader += f'\n{load("text_billboard_dec.vert")}' + + return shader + + @property + def shader_impl_vert(self): + shader = load("text_billboard_impl.vert") + + return shader + + @property + def shader_dec_frag(self): + shader = load("billboard_dec.frag") + shader += f'\n{load("text_billboard_dec.frag")}' + + return shader + + @property + def shader_impl_frag(self): + shader = load('billboard_impl.frag') + shader += f'\n{load("text_billboard_impl.frag")}' + return shader + + def _init_shader_frag(self): + # fs_impl_code = load('billboard_impl.frag') + # if self._marker_is_3d: + # fs_impl_code += f'{load("billboard_spheres_impl.frag")}' + # else: + # fs_impl_code += f'{load("marker_billboard_impl.frag")}' + + shader_to_actor( + self.vtk_actor, + "vertex", impl_code=self.shader_impl_vert, + decl_code=self.shader_dec_vert) + shader_to_actor( + self.vtk_actor, + "fragment", decl_code=self.shader_dec_frag) + shader_to_actor( + self.vtk_actor, + "fragment", impl_code=self.shader_impl_frag, + block="light") + + @property + def positions(self): + pass + + def recompute_labels( + self, update_labels=True, update_center=True,): + + padding, labels_positions,\ + uv, relative_sizes = \ + text_tools.get_positions_labels_billboards( + self._labels, self._label_center, self._char2pos, self._scales, + align=self.align, + x_offset_ratio=self.x_offset_ratio, + y_offset_ratio=self.y_offset_ratio) + if update_labels: + padding = np.repeat(padding, 4, axis=0) + self._padding[:] = padding + self._uv[:] = uv + self._relative_sizes[:] = relative_sizes + if update_center: + self._centers_geo[:] = np.repeat( + labels_positions, self._centers_length, axis=0) + self._verts_geo[:] = self._verts_geo_orig + self._centers_geo + + self.update() + + def update_labels(self, labels, positions): + self.labels = labels + self.positions = positions + self.recompute_labels() + + @positions.setter + def positions(self, positions): + self._label_center = positions + + @property + def labels(self): + pass + + @labels.setter + def labels(self, labels): + if self._min_label_size is not None: + labels = pad_labels( + labels, self._min_label_size, align=self.align) + self._labels = labels + + @property + def colors(self): + return self._colors_geo[0::self._centers_length] + + @colors.setter + def colors(self, new_colors): + self._colors_geo[:] = np.repeat( + new_colors, self._centers_length, axis=0) + + def update(self): + update_actor(self.vtk_actor) + + def __str__(self): + return f'FurySuperActorLabel num_nodes {self._vcount}' + + def __repr__(self): + return f'FurySuperActorLabel num_nodes {self._vcount}' + + class FurySuperNode: def __init__( self, diff --git a/helios/backends/labels.py b/helios/backends/labels.py new file mode 100644 index 0000000..99f685b --- /dev/null +++ b/helios/backends/labels.py @@ -0,0 +1,33 @@ +def pad_labels(labels, min_label_size, align='center'): + """Pad labels to the same size + + Parameters + ---------- + labels: list + list of labels + min_label_size: int + minimum size of the labels + align: str + alignment of the labels + + Returns + ------- + new_labels: list + list of padded labels + + """ + new_labels = [] + for label in labels: + num = len(label) + if num < min_label_size: + if align == 'center': + label = label.center(min_label_size) + elif align == 'left': + label = label.ljust(min_label_size) + elif align == 'right': + label = label.rjust(min_label_size) + elif num > min_label_size: + label = label[:min_label_size] + + new_labels.append(label) + return new_labels diff --git a/requirements.txt b/requirements.txt index 16f56dd..39bae06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ numpy>=1.8 pymde heliosFR -git+https://github.com/devmessias/fury.git@a94e22dbc28#egg=fury +git+https://github.com/devmessias/fury.git@54773522a78b59d191590ba7ae3cad49a6c8d029#egg=fury \ No newline at end of file diff --git a/tests/test_text.py b/tests/test_text.py new file mode 100644 index 0000000..a0d0c73 --- /dev/null +++ b/tests/test_text.py @@ -0,0 +1,63 @@ + +import numpy as np +from helios import NetworkDraw +from helios.backends.fury.actors import FurySuperLabels + + +def test_text_draw(): + interactive = False + centers = np.array([ + [0, 0, 0], + [1, 0, 0.], + [0, 1., 0.] + ])*5 + network_draw = NetworkDraw( + positions=centers, + scales=0.1, + marker='3d', + ) + + def get_labels(centers): + labels = [ + f'({center[0]:.2f}, {center[1]:.2f}, {center[2]:.2f})' + for center in centers + ] + return labels + + labels = get_labels(centers) + # generate random labels + network_labels = FurySuperLabels( + centers, + labels, + min_label_size=50, + scales=0.1 + ) + network_draw.showm.scene.add(network_labels.vtk_actor) + + dx = np.array([ + [-1, 0, 1], + [1, 0, 1], + [0, 1, 1] + ]) + data = {'i': 0.} + total = 100 + if interactive: + def update_positions(_, __): + new_positions = centers + dx*data['i']/total + network_draw.positions = new_positions + network_labels.positions = new_positions + labels = get_labels(new_positions) + network_labels.labels = labels + data['i'] += 1 + network_labels.recompute_labels() + network_draw.refresh() + network_draw.showm.add_timer_callback(True, 10, update_positions) + network_draw.showm.start() + else: + new_positions = centers + d + network_draw.positions = new_positions + network_labels.positions = new_positions + labels = get_labels(new_positions) + network_labels.labels = labels + network_labels.recompute_labels() + network_draw.refresh() From 545e9779391f7b92ba2a99030667755bf2b7aa4a Mon Sep 17 00:00:00 2001 From: devmessias Date: Fri, 13 Aug 2021 12:15:50 -0300 Subject: [PATCH 2/2] feature: dynamic node labels --- docs/examples/viz_labels.py | 110 +++++++++++++++++++++++++++++++++ helios/backends/fury/actors.py | 43 +++++++++++-- tests/test_text.py | 26 +++----- 3 files changed, 156 insertions(+), 23 deletions(-) create mode 100644 docs/examples/viz_labels.py diff --git a/docs/examples/viz_labels.py b/docs/examples/viz_labels.py new file mode 100644 index 0000000..bdbb9f0 --- /dev/null +++ b/docs/examples/viz_labels.py @@ -0,0 +1,110 @@ +""" +============== +Network Labels +============== + +The goal of this example is to show how to use the Helios Network +draw with the Helios Force-Directed. + +""" +import numpy as np +import networkx as nx +import argparse +from fury.window import record + +from helios import NetworkDraw +from helios.layouts import HeliosFr + +parser = argparse.ArgumentParser() +parser.add_argument( + '--interactive', dest='interactive', default=True, action='store_false') +args = parser.parse_args() + +interactive = args.interactive + +size = 100 +s = size +sizes = [s, s, s] +probs = np.array( + [[0.45, 0.05, 0.02], [0.05, 0.45, 0.07], [0.02, 0.07, 0.40]])/10 +g = nx.stochastic_block_model(sizes, probs, seed=0) + +num_nodes = len(g) +edges = [] +for source, target in g.edges(): + edges.append([source, target]) +edges = np.array(edges) + +colors_by_block = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] + +edge_colors = [] +for source, target in g.edges(): + c0 = colors_by_block[source//s] + c1 = colors_by_block[target//s] + edge_colors += [c0, c1] + +colors_by_block = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] +colors = np.array( + [colors_by_block[i//s] + for i in range(len(g))]).astype('float64') + +markers = [['o', 's', 'd'][i//s] for i in range(len(g))] +edge_colors = np.array(edge_colors).astype('float64') + +centers = np.random.normal(size=(len(g), 3)) +network_draw = NetworkDraw( + positions=centers, + edges=edges, + colors=colors, + scales=1, + node_edge_width=0, + marker=markers, + edge_line_color=edge_colors, + window_size=(600, 600) +) + + + + +def get_labels(centers): + labels = [ + f'({center[0]:.2f}, {center[1]:.2f})' + for center in centers + ] + return labels + + +labels = get_labels(centers) + +network_draw.add_labels( + labels, align='center', colors=(0, 0, 1), + scales=.1, + y_offset_ratio=5, + min_label_size=50) + +layout = HeliosFr( + edges, network_draw, update_interval_workers=100, max_workers=2) + + +if interactive: + def update_positions(_, __): + labels = get_labels(network_draw.positions) + network_draw.labels.labels = labels + network_draw.labels.positions = network_draw.positions + network_draw.refresh() + network_draw.showm.add_timer_callback(True, 10, update_positions) + +if not interactive: + layout.steps(100) + record( + network_draw.showm.scene, out_path='viz_labels.png', size=(600, 600)) + +if interactive: + layout.start() + #layout.steps(100) + network_draw.showm.initialize() + network_draw.showm.start() + + + + \ No newline at end of file diff --git a/helios/backends/fury/actors.py b/helios/backends/fury/actors.py index 94b3388..5a9f19b 100644 --- a/helios/backends/fury/actors.py +++ b/helios/backends/fury/actors.py @@ -69,6 +69,8 @@ def __init__( self.vtk_actor, self.Uniforms) self._init_shader_frag() + self._should_update_labels = False + self._should_update_positions = False def _init_actor(self, colors, scales): @@ -188,9 +190,7 @@ def _init_shader_frag(self): def positions(self): pass - def recompute_labels( - self, update_labels=True, update_center=True,): - + def recompute_labels(self): padding, labels_positions,\ uv, relative_sizes = \ text_tools.get_positions_labels_billboards( @@ -198,15 +198,17 @@ def recompute_labels( align=self.align, x_offset_ratio=self.x_offset_ratio, y_offset_ratio=self.y_offset_ratio) - if update_labels: + if self._should_update_labels: padding = np.repeat(padding, 4, axis=0) self._padding[:] = padding self._uv[:] = uv self._relative_sizes[:] = relative_sizes - if update_center: + self._should_update_labels = False + if self._should_update_positions: self._centers_geo[:] = np.repeat( labels_positions, self._centers_length, axis=0) self._verts_geo[:] = self._verts_geo_orig + self._centers_geo + self._should_update_positions = False self.update() @@ -218,6 +220,7 @@ def update_labels(self, labels, positions): @positions.setter def positions(self, positions): self._label_center = positions + self._should_update_positions = True @property def labels(self): @@ -229,6 +232,7 @@ def labels(self, labels): labels = pad_labels( labels, self._min_label_size, align=self.align) self._labels = labels + self._should_update_labels = True @property def colors(self): @@ -988,7 +992,8 @@ def __init__( edge_line_opacity=.5, edge_line_width=1, write_frag_depth=True - ): + ): + self.showm = None self._is_2d = positions.shape[1] == 2 if self._is_2d: positions = np.array([ @@ -1017,6 +1022,28 @@ def __init__( self.vtk_actors += [edges.vtk_actor] self.edges = edges + self.labels = None + + def add_labels( + self, labels, align='center', + colors=(0, 0, 1), + x_offset_ratio=1, y_offset_ratio=1, + min_label_size=None, scales=1): + if self.labels is not None: + return + network_labels = FurySuperLabels( + positions=self.nodes.positions, + labels=labels, + colors=colors, + align=align, + x_offset_ratio=x_offset_ratio, + y_offset_ratio=y_offset_ratio, + min_label_size=min_label_size, + scales=scales + ) + self.labels = network_labels + self.vtk_actors += [network_labels.vtk_actor] + self.showm.scene.add(network_labels.vtk_actor) @property def positions(self): @@ -1031,6 +1058,10 @@ def positions(self, positions): self.nodes.positions = positions if self.edges is not None: self.edges.positions = positions + if self.labels is not None: + self.labels.positions = positions + self.labels.recompute_labels() + self.update() def update(self): for actor in self.vtk_actors: diff --git a/tests/test_text.py b/tests/test_text.py index a0d0c73..3dedc3e 100644 --- a/tests/test_text.py +++ b/tests/test_text.py @@ -1,16 +1,12 @@ import numpy as np from helios import NetworkDraw -from helios.backends.fury.actors import FurySuperLabels def test_text_draw(): interactive = False - centers = np.array([ - [0, 0, 0], - [1, 0, 0.], - [0, 1., 0.] - ])*5 + centers = np.random.normal(0, 0.1, size=(100, 3)) + network_draw = NetworkDraw( positions=centers, scales=0.1, @@ -25,25 +21,21 @@ def get_labels(centers): return labels labels = get_labels(centers) - # generate random labels - network_labels = FurySuperLabels( - centers, - labels, - min_label_size=50, - scales=0.1 - ) - network_draw.showm.scene.add(network_labels.vtk_actor) + network_draw.add_labels(labels, min_label_size=50, scales=.1) + network_labels = network_draw.labels dx = np.array([ [-1, 0, 1], [1, 0, 1], [0, 1, 1] ]) + dx = 5 data = {'i': 0.} - total = 100 + total = 10 if interactive: def update_positions(_, __): - new_positions = centers + dx*data['i']/total + # new_positions = centers + dx*data['i']/total + new_positions = centers + centers*data['i']/total network_draw.positions = new_positions network_labels.positions = new_positions labels = get_labels(new_positions) @@ -54,7 +46,7 @@ def update_positions(_, __): network_draw.showm.add_timer_callback(True, 10, update_positions) network_draw.showm.start() else: - new_positions = centers + d + new_positions = centers + dx network_draw.positions = new_positions network_labels.positions = new_positions labels = get_labels(new_positions)