I'm working on a visualization project in networkx
and plotly
. Is there a way to create a 3D graph that resembles how a human brain looks like in networkx
and then to visualize it with plotly
(so it will be interactive)?
The idea is to have the nodes on the outside (or only show the nodes if it's easier) and to color a set of them differently like the image above
Based on the clarified requirements, I took a new approach:
There is a clearly delineated spot to add in different graph data as well as change the logic by which the node colors are decided. The key parameters to play with so that things look decent after introducing new graph data are:
scale_factor
: This changes how much the original Kamada-Kuwai calculated coordinates are translated radially away from the center of the brain mesh before they are snapped back to its surface. Larger values will make more nodes snap to the outer surface of the brain. Smaller values will leave more nodes positioned on the surfaces between the two hemispheres.opacity
of the lines in the edge trace: Graphs with more edges will quickly clutter up field of view and make the overall brain shape less visible. This speaks to my biggest dissatisfaction with this overall approach -- that edges which appear outside of the mesh surface make it harder to see the overall shape of the mesh, especially between the temporal lobes.My other biggest caveat here is that there is no attempt has been made to check whether any nodes positioned on the brain surface happen to coincide or have any sort of equal spacing.
Here is a screenshot and the live demo on Colab. Full copy-pasteable code below.
There are a whole bunch of asides that could be discussed here, but for brevity I will only note two:
import plotly.graph_objects as go
import numpy as np
import networkx as nx
import math
def mesh_properties(mesh_coords):
"""Calculate center and radius of sphere minimally containing a 3-D mesh
Parameters
----------
mesh_coords : tuple
3-tuple with x-, y-, and z-coordinates (respectively) of 3-D mesh vertices
"""
radii = []
center = []
for coords in mesh_coords:
c_max = max(c for c in coords)
c_min = min(c for c in coords)
center.append((c_max + c_min) / 2)
radius = (c_max - c_min) / 2
radii.append(radius)
return(center, max(radii))
# Download and prepare dataset from BrainNet repo
coords = np.loadtxt(np.DataSource().open('https://raw.githubusercontent.com/mingruixia/BrainNet-Viewer/master/Data/SurfTemplate/BrainMesh_Ch2_smoothed.nv'), skiprows=1, max_rows=53469)
x, y, z = coords.T
triangles = np.loadtxt(np.DataSource().open('https://raw.githubusercontent.com/mingruixia/BrainNet-Viewer/master/Data/SurfTemplate/BrainMesh_Ch2_smoothed.nv'), skiprows=53471, dtype=int)
triangles_zero_offset = triangles - 1
i, j, k = triangles_zero_offset.T
# Generate 3D mesh. Simply replace with 'fig = go.Figure()' or turn opacity to zero if seeing brain mesh is not desired.
fig = go.Figure(data=[go.Mesh3d(x=x, y=y, z=z,
i=i, j=j, k=k,
color='lightpink', opacity=0.5, name='', showscale=False, hoverinfo='none')])
# Generate networkx graph and initial 3-D positions using Kamada-Kawai path-length cost-function inside sphere containing brain mesh
G = nx.gnp_random_graph(200, 0.02, seed=42) # Replace G with desired graph here
mesh_coords = (x, y, z)
mesh_center, mesh_radius = mesh_properties(mesh_coords)
scale_factor = 5 # Tune this value by hand to have more/fewer points between the brain hemispheres.
pos_3d = nx.kamada_kawai_layout(G, dim=3, center=mesh_center, scale=scale_factor*mesh_radius)
# Calculate final node positions on brain surface
pos_brain = {}
for node, position in pos_3d.items():
squared_dist_matrix = np.sum((coords - position) ** 2, axis=1)
pos_brain[node] = coords[np.argmin(squared_dist_matrix)]
# Prepare networkx graph positions for plotly node and edge traces
nodes_x = [position[0] for position in pos_brain.values()]
nodes_y = [position[1] for position in pos_brain.values()]
nodes_z = [position[2] for position in pos_brain.values()]
edge_x = []
edge_y = []
edge_z = []
for s, t in G.edges():
edge_x += [nodes_x[s], nodes_x[t]]
edge_y += [nodes_y[s], nodes_y[t]]
edge_z += [nodes_z[s], nodes_z[t]]
# Decide some more meaningful logic for coloring certain nodes. Currently the squared distance from the mesh point at index 42.
node_colors = []
for node in G.nodes():
if np.sum((pos_brain[node] - coords[42]) ** 2) < 1000:
node_colors.append('red')
else:
node_colors.append('gray')
# Add node plotly trace
fig.add_trace(go.Scatter3d(x=nodes_x, y=nodes_y, z=nodes_z,
#text=labels,
mode='markers',
#hoverinfo='text',
name='Nodes',
marker=dict(
size=5,
color=node_colors
)
))
# Add edge plotly trace. Comment out or turn opacity to zero if not desired.
fig.add_trace(go.Scatter3d(x=edge_x, y=edge_y, z=edge_z,
mode='lines',
hoverinfo='none',
name='Edges',
opacity=0.1,
line=dict(color='gray')
))
# Make axes invisible
fig.update_scenes(xaxis_visible=False,
yaxis_visible=False,
zaxis_visible=False)
# Manually adjust size of figure
fig.update_layout(autosize=False,
width=800,
height=800)
fig.show()
A possible way to do that:
import networkx as nx
import random
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from stl import mesh
# function to convert stl 3d-model to mesh
# Taken from : https://chart-studio.plotly.com/~empet/15276/converting-a-stl-mesh-to-plotly-gomes/#/
def stl2mesh3d(stl_mesh):
# stl_mesh is read by nympy-stl from a stl file; it is an array of faces/triangles (i.e. three 3d points)
# this function extracts the unique vertices and the lists I, J, K to define a Plotly mesh3d
p, q, r = stl_mesh.vectors.shape #(p, 3, 3)
# the array stl_mesh.vectors.reshape(p*q, r) can contain multiple copies of the same vertex;
# extract unique vertices from all mesh triangles
vertices, ixr = np.unique(stl_mesh.vectors.reshape(p*q, r), return_inverse=True, axis=0)
I = np.take(ixr, [3*k for k in range(p)])
J = np.take(ixr, [3*k+1 for k in range(p)])
K = np.take(ixr, [3*k+2 for k in range(p)])
return vertices, I, J, K
# Let's use a toy "brain" stl file. You can get it from my Dropbox: https://www.dropbox.com/s/lav2opci8vekaep/brain.stl?dl=0
#
# Note: I made it quick and dirty whith Blender and is not supposed to be an accurate representation
# of an actual brain. You can put your own model here.
my_mesh = mesh.Mesh.from_file('brain.stl')
vertices, I, J, K = stl2mesh3d(my_mesh)
x, y, z = vertices.T # x,y,z contain the stl vertices
# Let's generate a random spatial graph:
# Note: spatial graphs have a "pos" (position) attribute
# pos = nx.get_node_attributes(G, "pos")
G = nx.random_geometric_graph(30, 0.3, dim=3) # in dimension 3 --> pos = [x,y,z]
#nx.draw(G)
print('Nb. of nodes: ',G.number_of_nodes(), 'Nb. of edges: ',G.number_of_edges())
# Take G.number_of_nodes() of nodes and attribute them randomly to points in the list of vertices of the STL model:
# That is, we "scatter" the nodes on the brain surface:
Vec3dList=list(np.array(random.sample(list(vertices), G.number_of_nodes())))
for i in range(len(Vec3dList)):
G.nodes[i]['pos']=Vec3dList[i]
# Create nodes and edges graph objects:
# Code from: https://plotly.com/python/network-graphs/ modified to work with 3d graphs
edge_x = []
edge_y = []
edge_z = []
for edge in G.edges():
x0, y0, z0 = G.nodes[edge[0]]['pos']
x1, y1, z1 = G.nodes[edge[1]]['pos']
edge_x.append(x0)
edge_x.append(x1)
edge_x.append(None)
edge_y.append(y0)
edge_y.append(y1)
edge_y.append(None)
edge_z.append(z0)
edge_z.append(z1)
edge_z.append(None)
edge_trace = go.Scatter3d(
x=edge_x, y=edge_y, z=edge_z,
line=dict(width=2, color='#888'),
hoverinfo='none',
opacity=.3,
mode='lines')
node_x = []
node_y = []
node_z = []
for node in G.nodes():
X, Y, Z = G.nodes[node]['pos']
node_x.append(X)
node_y.append(Y)
node_z.append(Z)
node_trace = go.Scatter3d(
x=node_x, y=node_y,z=node_z,
mode='markers',
hoverinfo='text',
marker=dict(
showscale=True,
# colorscale options
#'Greys' | 'YlGnBu' | 'Greens' | 'YlOrRd' | 'Bluered' | 'RdBu' |
#'Reds' | 'Blues' | 'Picnic' | 'Rainbow' | 'Portland' | 'Jet' |
#'Hot' | 'Blackbody' | 'Earth' | 'Electric' | 'Viridis' |
colorscale='YlGnBu',
reversescale=True,
color=[],
size=5,
colorbar=dict(
thickness=15,
title='Node Connections',
xanchor='left',
titleside='right'
),
line_width=10))
node_adjacencies = []
node_text = []
for node, adjacencies in enumerate(G.adjacency()):
node_adjacencies.append(len(adjacencies[1]))
node_text.append('# of connections: '+str(len(adjacencies[1])))
node_trace.marker.color = node_adjacencies
node_trace.text = node_text
colorscale= [[0, '#e5dee5'], [1, '#e5dee5']]
mesh3D = go.Mesh3d(
x=x,
y=y,
z=z,
i=I,
j=J,
k=K,
flatshading=False,
colorscale=colorscale,
intensity=z,
name='Brain',
opacity=0.25,
hoverinfo='none',
showscale=False)
title = "Brain"
layout = go.Layout(paper_bgcolor='rgb(1,1,1)',
title_text=title, title_x=0.5,
font_color='white',
width=800,
height=800,
scene_camera=dict(eye=dict(x=1.25, y=-1.25, z=1)),
scene_xaxis_visible=False,
scene_yaxis_visible=False,
scene_zaxis_visible=False)
fig = go.Figure(data=[mesh3D, edge_trace, node_trace], layout=layout)
fig.data[0].update(lighting=dict(ambient= .2,
diffuse= 1,
fresnel= 1,
specular= 1,
roughness= .1,
facenormalsepsilon=0))
fig.data[0].update(lightposition=dict(x=3000,
y=3000,
z=10000));
fig.show()
Below, the result. As you can see the result is not that great... But, maybe, you can improve on it. Best regards
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With