如何创建一个互动的脑形图?

7 人关注

我正在用 networkx plotly 做一个可视化项目。有没有办法在 networkx 中创建一个类似于人脑的三维图形,然后用 plotly 将其可视化(因此它将是互动的)?

我们的想法是将节点放在外面(或者只显示节点,如果更容易的话),并像上面的图片那样给一组节点涂上不同的颜色

5 个评论
根据你对我第一个答案的评论,在此进行跟进。 所以你在寻找一种方法,把一个任意的图形(不一定是大脑数据--只是任何图形)映射到三维坐标表面,看起来像一个人的大脑,然后用plotly绘图?
@Frodnar 是的,没错
最后一个问题:你是否只想给有区别的节点加亮/着色(最容易也最有可能在赏金到期前发生)? 两个节点都被高亮显示的节点和任何边缘? 大致上被高亮的节点所包围的表面?
@Frodnar 我不太明白这个问题。你是问我只给节点上色还是给节点和边上色?
是的,基本上是这样。 我还问你是否想让大脑表面的区域也上色,只是因为这是你在问题中放的图片中的样子,但我将在悬赏中只用节点或节点+边来调用它(也就是说,我没有时间去弄清楚如何给表面单独上色)。
python
plotly
networkx
Penguin
Penguin
发布于 2021-11-02
4 个回答
Frodnar
Frodnar
发布于 2021-11-09
已采纳
0 人赞同

首先,这段代码是 沉重地 借用马蒂奥-曼奇尼的说法,他描述说 here 而且他有 在MIT许可下发布 .

在原始代码中,没有使用networkx,所以很明显你实际上不需要networkx来完成你的目标。 如果这不是一个严格的要求,我会考虑使用他的原始代码,并重新修改以适应你的输入数据。

既然你把networkx列为要求,我就简单地重写了他的代码,取了一个networkx Graph 对象,带有某些节点属性,如 'color' 'coord' ,在最后的plotly scatter中用于这些标记特征。 我只是选择了数据集中的前十个点来涂成红色,这就是为什么它们没有被分组。

完整的可复制粘贴的代码在下面。这里的截图显然不是交互式的,但你可以试试这个演示 这里是Google Colab .

如果在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
  • Otherwise: download the required files here.
  • Code:

    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()
        
    不知道找到并略微调整别人的预制方案是否真的值得悬赏,所以在颁奖前要考虑。真正使用Twitter的人可能会用DM或推特 原代码的作者 (然后在这里评论说你已经这样做了,以防止垃圾邮件)。 我想知道我的代码是否得到了很好的利用......
    有了你在这里包括的适当的署名,这应该不是一个问题,不是吗?(+1)
    这真是太酷了!但除了使用现有的数据外,我不知道如何使用这个。也就是说,代码使用了节点的特定坐标,所以如果我想添加更多的节点或改变边缘,我就必须知道坐标或玩弄它直到它对齐。除非我误解了什么?通常 networkx 图可以由一个邻接矩阵或一个边列表构成
    你是否有 some 的一种坐标数据? 我不是脑部绘图的专家,但我想通过一些研究,你可以找到一种方法,将你的坐标数据具体映射到这个大脑模型上,或者映射到神经科学中使用的事实上的标准上。 如果你所拥有的只是一个邻接矩阵或边缘列表,而没有任何种类的三维坐标数据,那么就没有办法说这些节点在大脑中的位置。 在没有这些数据的情况下,你可以从networkx中画图,原因是它们是由一个算法计算出来的,通常是在画小网络的时候。
    @Frodnar 我理解。如果我的问题令人困惑,请原谅,但确切的位置其实并不重要。我只是在寻找一种方法,将普通的图形表示(边缘列表/邻接矩阵/等)转换成类似于大脑的三维图形。带颜色的图片是一个尝试(也许是我做得不好),以显示我对大脑上的节点着色的意思的例子
    Frodnar
    Frodnar
    发布于 2021-11-09
    0 人赞同

    根据明确的要求,我采取了一种新的方法。

  • Download accurate brain mesh data from BrainNet Viewer github repo ;
  • Plot a random graph with 3D-coordinates using Kamada-Kuwai cost function in three dimensions centered in a sphere containing the brain mesh;
  • Radially expand the node positions away from the center of the brain mesh and then shift them back to the closest vertex actually on the brain mesh;
  • Color some nodes red based on an arbitrary distance criterion from a randomly selected mesh vertex;
  • Fiddle with a bunch of plotting parameters to make it look decent.
  • 有一个明确划分的位置,可以添加不同的图形数据,以及改变决定节点颜色的逻辑。 为了在引入新的图形数据后使事情看起来像样,需要玩的关键参数是:。

  • 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.
  • 我在这里的另一个最大的警告是,没有试图检查定位在大脑表面的任何结点是否恰好在 coincide 或有任何形式的等距。

    这里有一张截图和 在Colab上的现场演示 . 下面是完整的可复制粘贴的代码。

    这里有一大堆可以讨论的题外话,但为了简洁起见,我只指出两个。

  • Folks interested in this topic but feeling overwhelmed by programming details should absolutely check out BrainNet Viewer ;
  • There are plenty of other brain meshes in the BrainNet Viewer github repo that could be used. Even better, if you have any mesh which can be formatted or reworked to be compatible with this approach, you could at least try wrapping a set of nodes around any other non-brain and somewhat round-ish mesh representing any other object .
  • 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()
        
    @Penguin 谢谢你的精彩问题。 这绝对是一个有趣的工作。 如果你需要任何具体的帮助或修改,请随时给我留言。 你可以通过我的Stack Overflow资料了解如何联系我。
    @Penguin 你能不能让我知道这是否/如何被用于最终的应用?
    S_Bersier
    S_Bersier
    发布于 2021-11-09
    0 人赞同

    一个可能的方法。

    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(
                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()
    

    下面是结果。正如你所看到的,结果不是很好......但是,也许,你可以改进它。 最好的问候