Source code for ClearMap.Visualization.Vispy.graph_visual

# -*- coding: utf-8 -*-
"""
GraphVisual
===========

Module providing Graph visuals for rendering graphs.

Note
----
This module is providing vispy visuals only.
See :mod:`PlotGraph3d` module for plotting.
"""
__author__ = 'Christoph Kirst <ckirst@rockefeller.edu>'
__license__ = 'MIT License <https://www.opensource.org/licenses/mit-license.php>'
__copyright__ = 'Copyright (c) 2017 by Christoph Kirst, The Rockefeller University, New York City'


import numpy as np

import vispy.visuals as visuals
from vispy.geometry import create_sphere

import ClearMap.Visualization.Color as col
from ClearMap.Analysis.graphs.graph_rendering import mesh_tube_from_coordinates_and_radii


###############################################################################
# ## Graph visuals
###############################################################################

default_color = (0.8, 0.1, 0.1, 1.0)


[docs] class GraphLineVisual(visuals.LineVisual): """Displays a graph in 3d using tube rendering for the edges """ def __init__(self, graph, coordinates=None, color=None, vertex_colors=None, edge_colors=None, width=None, mode='gl'): connectivity = graph.edge_connectivity() if coordinates is None: name = 'coordinates' else: name = coordinates coordinates = graph.vertex_property(name) if vertex_colors is not None or edge_colors is not None: color = None if color is None: if edge_colors is None and vertex_colors is None: color = default_color elif vertex_colors is not None: if isinstance(vertex_colors, np.ndarray) and vertex_colors.ndim == 2: color = vertex_colors else: color = col.color(vertex_colors, alpha=True) elif edge_colors is not None: if isinstance(edge_colors, np.ndarray) and edge_colors.ndim == 2: # need a vertex pair for every edge if the color is different coordinates = coordinates[connectivity.flatten()] connectivity = np.arange(coordinates.shape[0]) connectivity = connectivity.reshape((-1, 2)) indices = np.arange(len(edge_colors)) indices = np.array([indices, indices]).T.flatten() color = edge_colors[indices] else: color = col.color(edge_colors, alpha=True) else: color = col.color(color, alpha=True) if width is None: width = 1 visuals.LineVisual.__init__(self, coordinates, connect=connectivity, color=color, width=width, method=mode)
[docs] class GraphMeshVisual(visuals.mesh.MeshVisual): """Displays a graph in 3d using tube rendering for the edges""" def __init__(self, graph, coordinates=None, radii=None, n_tube_points=8, default_radius=1, color=None, vertex_colors=None, edge_colors=None, mode='triangles', shading='smooth'): if vertex_colors is not None or edge_colors is not None: color = None if color is None and vertex_colors is None: color = default_color if graph.has_edge_geometry(coordinates if coordinates is not None else 'coordinates'): name = coordinates if coordinates is not None else 'coordinates' coordinates, indices = graph.edge_geometry(name=name, return_indices=True, as_list=False) # calculate mesh try: radius_name = 'radius_units' if 'radius_units' in graph.edge_geometry_properties else 'radii' name = radii if radii is not None else radius_name radii = graph.edge_geometry(name=name, return_indices=False, as_list=False) except: # FIXME: broad radii = self.use_default_radii(coordinates, default_radius) else: coordinates = graph.vertex_coordinates() indices = graph.edge_connectivity().flatten() coordinates = np.vstack(coordinates[indices]) try: radii = graph.vertex_radii() radii = radii[indices] except: # FIXME: broad radii = self.use_default_radii(coordinates, default_radius) n_edges = graph.n_edges indices = np.array([2*np.arange(0, n_edges), 2*np.arange(1, n_edges+1)]).T if vertex_colors is not None: # then, edges take as colors the average of their vertices colours connectivity = graph.edge_connectivity() edge_colors = (vertex_colors[connectivity[:, 0]] + vertex_colors[connectivity[:, 1]])/2.0 vertices, faces, vertex_colors = mesh_tube_from_coordinates_and_radii(coordinates, radii, indices, n_tube_points=n_tube_points, edge_colors=edge_colors, processes=None) visuals.mesh.MeshVisual.__init__(self, vertices, faces, color=color, vertex_colors=vertex_colors, shading=shading, mode=mode)
[docs] def use_default_radii(self, coordinates, default_radius): print(f'No radii found in the graph, using uniform radii = {default_radius}!') radii = np.full(coordinates.shape[0], default_radius) return radii
[docs] class GraphSphereVisual(visuals.mesh.MeshVisual): """ Render each vertex (or geometry sample-point) of a graph as a shaded 3-D sphere. Parameters ---------- graph : Graph The graph to visualise. coordinates : str | None Name of the (vertex- or geometry-)property that stores XYZ positions. radii : str | 1-d array | None Property name or explicit per-point radii. Falls back to ``graph.vertex_radii()`` or *default_radius*. n_sphere_points : int Number of longitudinal subdivisions (≥ 4). Latitudinal subdivisions are chosen automatically to give a roughly regular mesh. default_radius : float Uniform radius to use when no radii are stored. color, vertex_colors : colour spec or array Global or per-vertex colours (same semantics as the other visuals). use_geometry : bool If *True* plot every edge-geometry sample-point instead of the plain vertex set. mode, shading : str Passed straight to ``vispy.visuals.MeshVisual``. """ def __init__(self, graph, coordinates=None, radii=None, n_sphere_points=8, default_radius=1, color=None, vertex_colors=None, use_geometry=False, mode='triangles', shading='smooth'): # ------------------------------------------------------------- # --- Collect coordinates ------------------------------------ # ------------------------------------------------------------- name = coordinates if coordinates is not None else 'coordinates' if use_geometry and graph.has_edge_geometry(name): coords = graph.edge_geometry(name=name, as_list=False) else: coords = graph.vertex_property(name) # falls back to vertex_coordinates() coords = np.asarray(coords, dtype=float) n_points = coords.shape[0] # ------------------------------------------------------------- # --- Collect radii ----------------------------------------- # ------------------------------------------------------------- if radii is None: try: # vertices radius_name = 'radius_units' if 'radius_units' in graph.edge_geometry_properties else 'radii' radii = graph.vertex_property(radius_name) if not use_geometry else graph.edge_geometry(name=radius_name, as_list=False) except Exception: # FIXME: too broad radii = None if isinstance(radii, str): # property name radii = graph.vertex_property(radii) \ if not use_geometry else graph.edge_geometry(name=radii, as_list=False) if radii is None: radii = np.full(n_points, default_radius, dtype=float) else: radii = np.asarray(radii, dtype=float) if radii.size != n_points: # broadcast scalar or 1-value list if radii.size == 1: radii = np.full(n_points, float(radii.squeeze())) else: raise ValueError('Length of *radii* must match number of points.') # ------------------------------------------------------------- # --- Generate one unit sphere mesh -------------------------- # ------------------------------------------------------------- rows = max(4, int(n_sphere_points)) cols = rows * 2 # decent aspect ratio md = create_sphere(rows=rows, cols=cols, radius=1.0) sph_verts = md.get_vertices() sph_faces = md.get_faces() n_template_verts = sph_verts.shape[0] n_template_faces = sph_faces.shape[0] # ------------------------------------------------------------- # --- Replicate & transform template for every centre -------- # ------------------------------------------------------------- # vertices verts = np.repeat(sph_verts[np.newaxis, :, :], n_points, axis=0) verts *= radii[:, np.newaxis, np.newaxis] verts += coords[:, np.newaxis, :] verts = verts.reshape(-1, 3) # faces (need index offset per sphere) offsets = np.arange(n_points, dtype=int) * n_template_verts faces = sph_faces[np.newaxis, :, :] + offsets[:, np.newaxis, np.newaxis] faces = faces.reshape(-1, 3) # ------------------------------------------------------------- # --- Handle colours ---------------------------------------- # ------------------------------------------------------------- if vertex_colors is not None: # explicit per-point colours ------------------------------------------------ if not isinstance(vertex_colors, np.ndarray): vertex_colors = col.color(vertex_colors, alpha=True) if vertex_colors.ndim == 1: # single RGBA colour for *all* points vertex_colors = np.tile(vertex_colors, (n_points, 1)) elif vertex_colors.shape[0] != n_points: # sanity check raise ValueError('vertex_colors must have one entry per point.') # replicate colour of each point to all vertices of its sphere vcols = np.repeat(vertex_colors, n_template_verts, axis=0).astype(float) color_kw = dict(vertex_colors=vcols, color=None) else: # uniform colour ------------------------------------------------------------ if color is None: color = default_color color_kw = dict(color=col.color(color, alpha=True), vertex_colors=None) # ------------------------------------------------------------- # --- Initialise parent MeshVisual --------------------------- # ------------------------------------------------------------- visuals.mesh.MeshVisual.__init__(self, verts, faces, shading=shading, mode=mode, **color_kw)
############################################################################### # ## Tests ############################################################################### def _test(): import numpy as np import vispy import ClearMap.Analysis.graphs.graph_gt as ggt import ClearMap.Visualization.Vispy.Plot3d as p3d import ClearMap.Visualization.Vispy.graph_visual as gv # reload(gv) g = ggt.Graph() g.add_vertex(5) g.add_edge([[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]]) g.set_vertex_coordinates(20*np.random.rand(5, 3)) v = vispy.scene.visuals.create_visual_node(gv.GraphLineVisual) p = v(g, parent=p3d.initialize_view().scene) p3d.center(p) v = vispy.scene.visuals.create_visual_node(gv.GraphMeshVisual) p = v(g, parent=p3d.initialize_view().scene) p3d.center(p)