如何创建交互式脑形图?

Pen*_*uin 7 python networkx plotly

networkx我正在和中从事可视化项目plotly。有没有一种方法可以创建一个类似于人脑的 3D 图形,networkx然后将其可视化plotly(因此它将是交互式的)? 在此输入图像描述

这个想法是将节点放在外面(或者如果更容易的话只显示节点)并对一组节点进行不同的着色,如上图所示

Fro*_*nar 5

首先,这段代码大量借鉴了 Matteo Mancini 的代码,他在此处对此进行了描述,并已根据 MIT 许可证发布

在原始代码中,未使用networkx,因此很明显您实际上并不需要networkx来实现您的目标。如果这不是严格的要求,我会考虑使用他的原始代码并对其进行修改以适合您的输入数据。

由于您将 networkx 列为一项要求,我只是修改了他的代码,以获取Graph具有某些节点属性的 networkx 对象,例如'color''coord'用于最终绘图散点中的那些标记特征。我只是选择数据集中的前十个点将其涂成红色,这就是它们没有分组的原因。

完整的可复制粘贴代码如下。这里的屏幕截图显然不是交互式的,但您可以在 Google Colab 上尝试演示。

Brain_network_plotly_from_networkx


要在 Linux/Mac 上的 Jupyter 笔记本中下载文件:

!wget https://github.com/matteomancini/neurosnippets/raw/master/brainviz/interactive-network/lh.pial.obj
!wget https://github.com/matteomancini/neurosnippets/raw/master/brainviz/interactive-network/icbm_fiber_mat.txt
!wget https://github.com/matteomancini/neurosnippets/raw/master/brainviz/interactive-network/fs_region_centers_68_sort.txt
!wget https://github.com/matteomancini/neurosnippets/raw/master/brainviz/interactive-network/freesurfer_regions_68_sort_full.txt
Run Code Online (Sandbox Code Playgroud)

代码:

import numpy as np
import plotly.graph_objects as go
import networkx as nx # New dependency


def obj_data_to_mesh3d(odata):
    # odata is the string read from an obj file
    vertices = []
    faces = []
    lines = odata.splitlines()   
   
    for line in lines:
        slist = line.split()
        if slist:
            if slist[0] == 'v':
                vertex = np.array(slist[1:], dtype=float)
                vertices.append(vertex)
            elif slist[0] == 'f':
                face = []
                for k in range(1, len(slist)):
                    face.append([int(s) for s in slist[k].replace('//','/').split('/')])
                if len(face) > 3: # triangulate the n-polyonal face, n>3
                    faces.extend([[face[0][0]-1, face[k][0]-1, face[k+1][0]-1] for k in range(1, len(face)-1)])
                else:    
                    faces.append([face[j][0]-1 for j in range(len(face))])
            else: pass
    
    
    return np.array(vertices), np.array(faces)


with open("lh.pial.obj", "r") as f:
    obj_data = f.read()
[vertices, faces] = obj_data_to_mesh3d(obj_data)

vert_x, vert_y, vert_z = vertices[:,:3].T
face_i, face_j, face_k = faces.T

cmat = np.loadtxt('icbm_fiber_mat.txt')
nodes = np.loadtxt('fs_region_centers_68_sort.txt')

labels=[]
with open("freesurfer_regions_68_sort_full.txt", "r") as f:
    for line in f:
        labels.append(line.strip('\n'))

# Instantiate Graph and add nodes (with their coordinates)
G = nx.Graph()

for idx, node in enumerate(nodes):
    G.add_node(idx, coord=node)

# Add made-up colors for the nodes as node attribute
colors_data = {node: ('gray' if node > 10 else 'red') for node in G.nodes}
nx.set_node_attributes(G, colors_data, name="color")

# Add edges
[source, target] = np.nonzero(np.triu(cmat)>0.01)
edges = list(zip(source, target))

G.add_edges_from(edges)

# Get node coordinates from node attribute
nodes_x = [data['coord'][0] for node, data in G.nodes(data=True)]
nodes_y = [data['coord'][1] for node, data in G.nodes(data=True)]
nodes_z = [data['coord'][2] for node, data in G.nodes(data=True)]

edge_x = []
edge_y = []
edge_z = []
for s, t in 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]]

# Get node colors from node attribute
node_colors = [data['color'] for node, data in G.nodes(data=True)]

fig = go.Figure()

# Changed color and opacity kwargs
fig.add_trace(go.Mesh3d(x=vert_x, y=vert_y, z=vert_z, i=face_i, j=face_j, k=face_k,
                        color='gray', opacity=0.1, name='', showscale=False, hoverinfo='none'))

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, # Changed node size...
                                       color=node_colors # ...and color
                                      )
                           ))
fig.add_trace(go.Scatter3d(x=edge_x, y=edge_y, z=edge_z,
                           mode='lines', hoverinfo='none', name='Edges',
                           opacity=0.3, # Added opacity kwarg
                           line=dict(color='pink') # Added line color
                           ))

fig.update_layout(
    scene=dict(
        xaxis=dict(showticklabels=False, visible=False),
        yaxis=dict(showticklabels=False, visible=False),
        zaxis=dict(showticklabels=False, visible=False),
    ),
    width=800, height=600
)

fig.show()
Run Code Online (Sandbox Code Playgroud)


Fro*_*nar 2

根据明确的要求,我采取了新的方法:

  1. 从BrainNet Viewer github 存储库下载准确的脑网格数据;
  2. 使用Kamada-Kuwai 成本函数在以包含脑网格的球体为中心的三个维度上绘制具有 3D 坐标的随机图;
  3. 将节点位置径向扩展远离脑网格的中心,然后将它们移回到脑网格上实际最近的顶点;
  4. 根据距随机选择的网格顶点的任意距离标准,将某些节点着色为红色;
  5. 摆弄一堆绘图参数以使其看起来不错。

有一个明确描绘的点可以添加不同的图形数据以及更改决定节点颜色的逻辑。在引入新的图形数据后,要使事情看起来不错的关键参数是:

  • scale_factor:这会改变原始 Kamada-Kuwai 计算的坐标在被捕捉回其表面之前从脑网格中心径向平移的程度。较大的值将使更多的节点捕捉到大脑的外表面。较小的值将在两个半球之间的表面上留下更多的节点。
  • opacity边缘轨迹中的线条:具有更多边缘的图形会很快使视野变得混乱,并使整个大脑形状不那么明显。这说明了我对这种整体方法最大的不满——出现在网格表面之外的边缘使得更难看到网格的整体形状,尤其是在颞叶之间。

我在这里的另一个最大的警告是,没有尝试检查位于大脑表面的任何节点是否恰好重合具有任何相等的间距。

这是屏幕截图和Colab 上的现场演示。下面是完整的可复制粘贴代码。

表面大脑网络

这里可以讨论一大堆旁白,但为了简洁起见,我只指出两个:

  1. 对这个主题感兴趣但对编程细节感到不知所措的人绝对应该看看BrainNet Viewer
  2. BrainNet Viewer github 存储库中还有许多其他可以使用的脑网格。更好的是,如果您有任何可以格式化或重新设计以与此方法兼容的网格,您至少可以尝试将一组节点包裹在代表任何其他对象的任何其他非大脑和有点圆形的网格周围

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()
Run Code Online (Sandbox Code Playgroud)