如何使用matplotlib绘制3D数据集中的任意平面?

6
有一个包含3D数据的数组,例如形状为(64,64,64),如何通过该数据集中的点和法向量(类似于晶体学中的hkl平面)绘制一个平面呢?与MayaVi中通过数据旋转一个平面相似。
大多数情况下,生成的图形将包含非方形平面。这些能够使用matplotlib绘制吗?例如使用某种非矩形补丁?
编辑: 我几乎自己解决了这个问题(见下文),但仍然想知道如何在matplotlib中绘制非矩形补丁...?
编辑: 由于下面的讨论,我重申了问题。

应该采用一个通用的解决方案,通过任意平面进行插值,正如Thorsten建议的那样。当插值值超出数据立方体时,将分配一个“nan”,因为它必须进行外推而不是内插。这将使你绘制的立方体实际截面之外的区域看起来不同,而无需担心确定确切的边界。 - Jaime
关于 nan 的有趣点。然而,这并不会告诉 matplotlib 关于补丁边界的信息。对我来说这很重要,因为如果你想发布类似的内容,最好有一个带有正确边界的图表。 - BandGap
5个回答

3

我有一个次优解决此问题的方法。部分解决方案可以使用第二个答案中的方法在Matlab或matplotlib中基于法向量和一个点绘制平面

# coding: utf-8
import numpy as np
from matplotlib.pyplot import imshow,show

A=np.empty((64,64,64)) #This is the data array
def f(x,y):
    return np.sin(x/(2*np.pi))+np.cos(y/(2*np.pi))
xx,yy= np.meshgrid(range(64), range(64))
for x in range(64):
    A[:,:,x]=f(xx,yy)*np.cos(x/np.pi)

N=np.zeros((64,64)) 
"""This is the plane we cut from A. 
It should be larger than 64, due to diagonal planes being larger. 
Will be fixed."""

normal=np.array([-1,-1,1]) #Define cut plane here. Normal vector components restricted to integers
point=np.array([0,0,0])
d = -np.sum(point*normal)

def plane(x,y): # Get plane's z values
    return (-normal[0]*x-normal[1]*y-d)/normal[2]

def getZZ(x,y): #Get z for all values x,y. If z>64 it's out of range
    for i in x:
        for j in y:
            if plane(i,j)<64:
                N[i,j]=A[i,j,plane(i,j)]

getZZ(range(64),range(64))
imshow(N, interpolation="Nearest")
show()

这并不是最终解决方案,因为情节不仅限于具有z值的点,大于64 * 64的平面也没有考虑到,并且这些平面必须在(0,0,0)处定义。


1
你的代码中有很多次优的地方,甚至可能存在一些明显的错误。像Thorsten指出的那样去做是正确的方法,如果他在白天没有时间完成,我会尝试着去解决它。 - Jaime
@Jaime:你能更具体地解释一下我代码中的“plain error”是什么吗?还请参考我对Thorsten答案中插值的最后一条评论。 - BandGap
似乎你只对有限的解决方案感兴趣。如果是这样,事情就变得更容易了,当然。 - Thorsten Kranz
确实。请看上面的评论。 - BandGap
“plain error” 出现在这一行代码中:“from matplotlib.pyploy import imshow,show”。 - Thorsten Kranz
我会称之为打字错误,特别是因为我们都知道它应该怎么拼写。 - BandGap

2
这很有趣,我今天刚回答了一个类似的问题。方法是:插值。您可以使用scipy.interpolate中的griddata: Griddata 此页面提供了一个非常好的示例,并且函数的签名与您的数据非常接近。
您仍然必须以某种方式定义要对其进行数据插值的平面上的点。我会看一下这个问题,我的线性代数课程已经过去几年了。

1
我认为MayaVi不是这样做的。据我理解,您链接的函数会从我的数据集中提取大致位于指定平面上的那些点,并在它们之间进行插值? - BandGap
不,我会在三个维度上进行插值。您提供有关标量场和要插值的三维点的信息。 - Thorsten Kranz
我认为这里根本不需要插值。如果您需要比数据集中已提供的更多数据点,则可以使用插值。在您回答的另一个问题中,您是正确的,但是在这里,插值将由imshow完成,如果您不使用“Nearest”... - BandGap
你肯定需要插值。你的数据量有限(例如64 ** 3),但你的平面可能有无限多个方向。只要保持“法线”与其中一个维度平行,并且仅在64个步骤中移动“点”,则可以在不进行插值的情况下正常运行。但是,一旦这些变得任意起来,你很可能无法用平面命中任何数据点。那么,如何确定平面上的值?插值。 - Thorsten Kranz
好的,我明白你的意思了。当我提出问题时,我没有考虑到任意平面,而更多地考虑了晶体学中使用的hkl平面...最终并不是那么随意 :) 对于没有表达清楚,我很抱歉。 - BandGap

1

我曾经为MRI数据增强做过类似的事情:

可能代码可以被优化,但它现在就能工作。
我的数据是3维numpy数组,代表一个MRI扫描仪。它的大小为[128,128,128],但代码可以修改以接受任何维数。此外,当平面在立方体边界之外时,您必须在主函数中将变量fill赋默认值,在我的情况下我选择了:data_cube [0:5,0:5,0:5].mean()

def create_normal_vector(x, y,z):

    normal = np.asarray([x,y,z])
    normal = normal/np.sqrt(sum(normal**2))
    return normal



def get_plane_equation_parameters(normal,point):
    a,b,c = normal
    d = np.dot(normal,point)
    return a,b,c,d        #ax+by+cz=d  

def get_point_plane_proximity(plane,point):
    #just aproximation
    return np.dot(plane[0:-1],point) - plane[-1]

def get_corner_interesections(plane, cube_dim = 128): #to reduce the search space
    #dimension is 128,128,128
    corners_list = []
    only_x = np.zeros(4)
    min_prox_x = 9999
    min_prox_y = 9999
    min_prox_z = 9999
    min_prox_yz = 9999
    for i in range(cube_dim):
        temp_min_prox_x=abs(get_point_plane_proximity(plane,np.asarray([i,0,0])))
       # print("pseudo distance x: {0}, point: [{1},0,0]".format(temp_min_prox_x,i))
        if temp_min_prox_x <  min_prox_x:
            min_prox_x = temp_min_prox_x
            corner_intersection_x = np.asarray([i,0,0])
            only_x[0]= i

        temp_min_prox_y=abs(get_point_plane_proximity(plane,np.asarray([i,cube_dim,0])))
       # print("pseudo distance y: {0}, point: [{1},{2},0]".format(temp_min_prox_y,i,cube_dim))

        if temp_min_prox_y <  min_prox_y:
            min_prox_y = temp_min_prox_y
            corner_intersection_y = np.asarray([i,cube_dim,0]) 
            only_x[1]= i

        temp_min_prox_z=abs(get_point_plane_proximity(plane,np.asarray([i,0,cube_dim])))
        #print("pseudo distance z: {0}, point: [{1},0,{2}]".format(temp_min_prox_z,i,cube_dim))

        if temp_min_prox_z <  min_prox_z:
            min_prox_z = temp_min_prox_z
            corner_intersection_z = np.asarray([i,0,cube_dim])
            only_x[2]= i

        temp_min_prox_yz=abs(get_point_plane_proximity(plane,np.asarray([i,cube_dim,cube_dim])))
        #print("pseudo distance z: {0}, point: [{1},{2},{2}]".format(temp_min_prox_yz,i,cube_dim))

        if temp_min_prox_yz <  min_prox_yz:
            min_prox_yz = temp_min_prox_yz
            corner_intersection_yz = np.asarray([i,cube_dim,cube_dim])
            only_x[3]= i

    corners_list.append(corner_intersection_x)      
    corners_list.append(corner_intersection_y)            
    corners_list.append(corner_intersection_z)            
    corners_list.append(corner_intersection_yz)
    corners_list.append(only_x.min()) 
    corners_list.append(only_x.max())           

    return corners_list       

def get_points_intersection(plane,min_x,max_x,data_cube,shape=128):

    fill = data_cube[0:5,0:5,0:5].mean() #this can be a parameter
    extended_data_cube = np.ones([shape+2,shape,shape])*fill
    extended_data_cube[1:shape+1,:,:] = data_cube 
    diag_image = np.zeros([shape,shape])
    min_x_value = 999999

    for i in range(shape):

        for j in range(shape):

            for k in range(int(min_x),int(max_x)+1):


                current_value = abs(get_point_plane_proximity(plane,np.asarray([k,i,j])))
                #print("current_value:{0}, val: [{1},{2},{3}]".format(current_value,k,i,j))
                if current_value < min_x_value:
                    diag_image[i,j] = extended_data_cube[k,i,j]
                    min_x_value = current_value

            min_x_value = 999999

    return diag_image   

它的工作方式如下:
你创建一个普通向量: 例如 [5,0,3]
normal1=create_normal_vector(5, 0,3) #this is only to normalize

然后你创建一个点: (我的立方体数据形状为[128,128,128])

point = [64,64,64]

您可以计算平面方程的参数[a,b,c,d],其中ax+by+cz=d。

plane1=get_plane_equation_parameters(normal1,point)

然后为了减少搜索空间,您可以计算平面与立方体的交点:

corners1 = get_corner_interesections(plane1,128)

其中 corners1 = [intersection [x,0,0],intersection [x,128,0],intersection [x,0,128],intersection [x,128,128], min intersection [x,y,z], max intersection [x,y,z]]

有了这些,您可以计算立方体和平面之间的交点:

image1 = get_points_intersection(plane1,corners1[-2],corners1[-1],data_cube)

一些例子:
正常是[1,0,0],点是[64,64,64]。

enter image description here

正常情况下是 [5,1,0],[5,1,1],[5,0,1],点为 [64,64,64]:

enter image description here

正常是 [5,3,0],[5,3,3],[5,0,3],点是 [64,64,64]:

enter image description here

正常是 [5,-5,0],[5,-5,-5],[5,0,-5] 点是 [64,64,64]:

enter image description here

谢谢。


1
为了满足简化需求,我准备了一个简单的示例。
import numpy as np
import pylab as plt

data = np.arange((64**3))
data.resize((64,64,64))

def get_slice(volume, orientation, index):
    orientation2slicefunc = {
        "x" : lambda ar:ar[index,:,:], 
        "y" : lambda ar:ar[:,index,:],  
        "z" : lambda ar:ar[:,:,index]
    }
    return orientation2slicefunc[orientation](volume)

plt.subplot(221)
plt.imshow(get_slice(data, "x", 10), vmin=0, vmax=64**3)

plt.subplot(222)
plt.imshow(get_slice(data, "x", 39), vmin=0, vmax=64**3)

plt.subplot(223)
plt.imshow(get_slice(data, "y", 15), vmin=0, vmax=64**3)
plt.subplot(224)
plt.imshow(get_slice(data, "z", 25), vmin=0, vmax=64**3)  

plt.show()  

这导致了以下情节:

Four example slices

主要的技巧是使用字典映射方向到lambda方法,这样可以避免编写烦人的if-then-else块。当然,您可以决定为方向使用不同的名称,例如数字。
也许这可以帮助您。
Thorsten
附言:我不关心“IndexOutOfRange”,对我来说让这个异常弹出是可以接受的,因为在这种情况下它是完全可以理解的。

+1 对于漂亮简洁的代码表示赞赏,但这不符合 OP 的要求,请参考 http://en.wikipedia.org/wiki/Miller_index 和 http://en.wikipedia.org/wiki/Reciprocal_lattice#Simple_cubic_lattice,因此我认为他想要法向量形式为 [i/64, j/64, k/64] 的平面,其中 ijk 都是整数,并穿过一个点 [x, y, z],其中所有值都是 [0, 64) 中的整数。 - Jaime
这根本不是被要求的内容,我为什么要像你第一次写的那样编写一个函数呢?plt.imshow(data[index,:,:] ...)可以做完全相同的事情,而不会引入难以理解的代码。 - BandGap
我的函数的原因是:方向被认为是一个最小的“法向量”,这只有在法线必须平行于晶格的一条边时才可以。现在我明白了这不是你想要的。我使用了lambda作为你的建议(data[index,:,:]),但只有当你知道要索引哪个轴时才有效-但在我的例子中我们不知道,它取决于“方向”。 - Thorsten Kranz
但是为什么我们不知道我们想要哪个轴?为什么我们更想知道它是x、y还是z? - BandGap
当我们执行函数时,我们知道: n=[0,0,1] -> "z";n=[1,0,0] -> "x" 但是当我们定义函数时,我们不知道。 - Thorsten Kranz

1
这里的其他答案似乎没有使用显式像素循环或者设计用于非结构化输入数据的scipy.interpolate.griddata非常高效。以下是一种高效(向量化)和通用的解决方案。

这里有一个纯numpy实现(最近邻“插值”)和一个线性插值的实现,它将插值委托给scipy.ndimage.map_coordinates。(后者函数可能在2013年提问此问题时不存在。)

import numpy as np
from scipy.ndimage import map_coordinates
     
def slice_datacube(cube, center, eXY, mXY, fill=np.nan, interp=True):
    """Get a 2D slice from a 3-D array.
    
    Copyright: Han-Kwang Nienhuys, 2020.
    License: any of CC-BY-SA, CC-BY, BSD, GPL, LGPL
    Reference: https://dev59.com/2GzXa4cB1Zd3GeqPXMGn#62733930
    
    Parameters:
    
    - cube: 3D array, assumed shape (nx, ny, nz).
    - center: shape (3,) with coordinates of center.
      can be float. 
    - eXY: unit vectors, shape (2, 3) - for X and Y axes of the slice.
      (unit vectors must be orthogonal; normalization is optional).
    - mXY: size tuple of output array (mX, mY) - int.
    - fill: value to use for out-of-range points.
    - interp: whether to interpolate (rather than using 'nearest')
    
    Return:
        
    - slice: array, shape (mX, mY).
    """
    
    center = np.array(center, dtype=float)
    assert center.shape == (3,)
    
    eXY = np.array(eXY)/np.linalg.norm(eXY, axis=1)[:, np.newaxis]
    if not np.isclose(eXY[0] @ eXY[1], 0, atol=1e-6):
        raise ValueError(f'eX and eY not orthogonal.')

    # R: rotation matrix: data_coords = center + R @ slice_coords
    eZ = np.cross(eXY[0], eXY[1])
    R = np.array([eXY[0], eXY[1], eZ], dtype=np.float32).T
    
    # setup slice points P with coordinates (X, Y, 0)
    mX, mY = int(mXY[0]), int(mXY[1])    
    Xs = np.arange(0.5-mX/2, 0.5+mX/2)
    Ys = np.arange(0.5-mY/2, 0.5+mY/2)
    PP = np.zeros((3, mX, mY), dtype=np.float32)
    PP[0, :, :] = Xs.reshape(mX, 1)
    PP[1, :, :] = Ys.reshape(1, mY)
        
    # Transform to data coordinates (x, y, z) - idx.shape == (3, mX, mY)
    if interp:
        idx = np.einsum('il,ljk->ijk', R, PP) + center.reshape(3, 1, 1)
        slice = map_coordinates(cube, idx, order=1, mode='constant', cval=fill)
    else:
        idx = np.einsum('il,ljk->ijk', R, PP) + (0.5 + center.reshape(3, 1, 1))
        idx = idx.astype(np.int16)
        # Find out which coordinates are out of range - shape (mX, mY)
        badpoints = np.any([
            idx[0, :, :] < 0,
            idx[0, :, :] >= cube.shape[0], 
            idx[1, :, :] < 0,
            idx[1, :, :] >= cube.shape[1], 
            idx[2, :, :] < 0,
            idx[2, :, :] >= cube.shape[2], 
            ], axis=0)
        
        idx[:, badpoints] = 0
        slice = cube[idx[0], idx[1], idx[2]]
        slice[badpoints] = fill
        
    return slice
    
# Demonstration
nx, ny, nz = 50, 70, 100
cube = np.full((nx, ny, nz), np.float32(1))

cube[nx//4:nx*3//4, :, :] += 1
cube[:, ny//2:ny*3//4, :] += 3
cube[:, :, nz//4:nz//2] += 7
cube[nx//3-2:nx//3+2, ny//2-2:ny//2+2, :] = 0 # black dot
     
Rz, Rx = np.pi/6, np.pi/4 # rotation angles around z and x
cz, sz = np.cos(Rz), np.sin(Rz)
cx, sx = np.cos(Rx), np.sin(Rx)

Rmz = np.array([[cz, -sz, 0], [sz, cz, 0], [0, 0, 1]])
Rmx = np.array([[1, 0, 0], [0, cx, -sx], [0, sx, cx]])
eXY = (Rmx @ Rmz).T[:2]
  
slice = slice_datacube(
    cube, 
    center=[nx/3, ny/2, nz*0.7], 
    eXY=eXY,
    mXY=[80, 90],
    fill=np.nan,
    interp=False
    )

import matplotlib.pyplot as plt
plt.close('all')
plt.imshow(slice.T) # imshow expects shape (mY, mX)
plt.colorbar()

输出(对于 interp=False):

Test case: 2D slice of 3D dataset

对于这个测试案例(50x70x100数据立方体,80x90切片大小),在我的笔记本电脑上运行时间为376微秒(interp=False)和550微秒(interp=True)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接