14

简短的问题

matplotlib 2D 补丁如何转换为具有任意法线的 3D?

长问题

我想用 3d 投影在轴上绘制补丁。但是,mpl_toolkits.mplot3d.art3d提供的方法仅提供具有沿主轴的法线的补丁的方法。如何将补丁添加到具有任意法线的 3d 轴?

4

4 回答 4

16

简短的回答

将下面的代码复制到您的项目中并使用方法

def pathpatch_2d_to_3d(pathpatch, z = 0, normal = 'z'):
    """
    Transforms a 2D Patch to a 3D patch using the given normal vector.

    The patch is projected into they XY plane, rotated about the origin
    and finally translated by z.
    """

将您的 2D 补丁转换为具有任意法线的 3D 补丁。

from mpl_toolkits.mplot3d import art3d

def rotation_matrix(d):
    """
    Calculates a rotation matrix given a vector d. The direction of d
    corresponds to the rotation axis. The length of d corresponds to 
    the sin of the angle of rotation.

    Variant of: http://mail.scipy.org/pipermail/numpy-discussion/2009-March/040806.html
    """
    sin_angle = np.linalg.norm(d)

    if sin_angle == 0:
        return np.identity(3)

    d /= sin_angle

    eye = np.eye(3)
    ddt = np.outer(d, d)
    skew = np.array([[    0,  d[2],  -d[1]],
                  [-d[2],     0,  d[0]],
                  [d[1], -d[0],    0]], dtype=np.float64)

    M = ddt + np.sqrt(1 - sin_angle**2) * (eye - ddt) + sin_angle * skew
    return M

def pathpatch_2d_to_3d(pathpatch, z = 0, normal = 'z'):
    """
    Transforms a 2D Patch to a 3D patch using the given normal vector.

    The patch is projected into they XY plane, rotated about the origin
    and finally translated by z.
    """
    if type(normal) is str: #Translate strings to normal vectors
        index = "xyz".index(normal)
        normal = np.roll((1.0,0,0), index)

    normal /= np.linalg.norm(normal) #Make sure the vector is normalised

    path = pathpatch.get_path() #Get the path and the associated transform
    trans = pathpatch.get_patch_transform()

    path = trans.transform_path(path) #Apply the transform

    pathpatch.__class__ = art3d.PathPatch3D #Change the class
    pathpatch._code3d = path.codes #Copy the codes
    pathpatch._facecolor3d = pathpatch.get_facecolor #Get the face color    

    verts = path.vertices #Get the vertices in 2D

    d = np.cross(normal, (0, 0, 1)) #Obtain the rotation vector    
    M = rotation_matrix(d) #Get the rotation matrix

    pathpatch._segment3d = np.array([np.dot(M, (x, y, 0)) + (0, 0, z) for x, y in verts])

def pathpatch_translate(pathpatch, delta):
    """
    Translates the 3D pathpatch by the amount delta.
    """
    pathpatch._segment3d += delta

长答案

查看 art3d.pathpatch_2d_to_3d 的源代码给出了以下调用层次结构

  1. art3d.pathpatch_2d_to_3d
  2. art3d.PathPatch3D.set_3d_properties
  3. art3d.Patch3D.set_3d_properties
  4. art3d.juggle_axes

从 2D 到 3D 的转换发生在最后一次调用art3d.juggle_axes. 修改最后一步,我们可以获得具有任意法线的 3D 补丁。

我们分四步进行

  1. 将面片的顶点投影到 XY 平面 ( pathpatch_2d_to_3d)
  2. 计算一个将z方向旋转到法线方向的旋转矩阵R ( rotation_matrix)
  3. 将旋转矩阵应用于所有顶点 ( pathpatch_2d_to_3d)
  4. 在 z 方向平移生成的对象 ( pathpatch_2d_to_3d)

示例源代码和结果图如下所示。

from mpl_toolkits.mplot3d import proj3d
from matplotlib.patches import Circle
from itertools import product

ax = axes(projection = '3d') #Create axes

p = Circle((0,0), .2) #Add a circle in the yz plane
ax.add_patch(p)
pathpatch_2d_to_3d(p, z = 0.5, normal = 'x')
pathpatch_translate(p, (0, 0.5, 0))

p = Circle((0,0), .2, facecolor = 'r') #Add a circle in the xz plane
ax.add_patch(p)
pathpatch_2d_to_3d(p, z = 0.5, normal = 'y')
pathpatch_translate(p, (0.5, 1, 0))

p = Circle((0,0), .2, facecolor = 'g') #Add a circle in the xy plane
ax.add_patch(p)
pathpatch_2d_to_3d(p, z = 0, normal = 'z')
pathpatch_translate(p, (0.5, 0.5, 0))

for normal in product((-1, 1), repeat = 3):
    p = Circle((0,0), .2, facecolor = 'y', alpha = .2)
    ax.add_patch(p)
    pathpatch_2d_to_3d(p, z = 0, normal = normal)
    pathpatch_translate(p, 0.5)

结果图

于 2013-08-14T10:13:35.543 回答
5

非常有用的一段代码,但有一个小警告:它不能处理指向下方的法线,因为它只使用角度的正弦值。

您还需要使用余弦:

from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d import art3d
from mpl_toolkits.mplot3d import proj3d
import numpy as np

def rotation_matrix(v1,v2):
    """
    Calculates the rotation matrix that changes v1 into v2.
    """
    v1/=np.linalg.norm(v1)
    v2/=np.linalg.norm(v2)

    cos_angle=np.dot(v1,v2)
    d=np.cross(v1,v2)
    sin_angle=np.linalg.norm(d)

    if sin_angle == 0:
        M = np.identity(3) if cos_angle>0. else -np.identity(3)
    else:
        d/=sin_angle

        eye = np.eye(3)
        ddt = np.outer(d, d)
        skew = np.array([[    0,  d[2],  -d[1]],
                      [-d[2],     0,  d[0]],
                      [d[1], -d[0],    0]], dtype=np.float64)

        M = ddt + cos_angle * (eye - ddt) + sin_angle * skew

    return M

def pathpatch_2d_to_3d(pathpatch, z = 0, normal = 'z'):
    """
    Transforms a 2D Patch to a 3D patch using the given normal vector.

    The patch is projected into they XY plane, rotated about the origin
    and finally translated by z.
    """
    if type(normal) is str: #Translate strings to normal vectors
        index = "xyz".index(normal)
        normal = np.roll((1,0,0), index)

    path = pathpatch.get_path() #Get the path and the associated transform
    trans = pathpatch.get_patch_transform()

    path = trans.transform_path(path) #Apply the transform

    pathpatch.__class__ = art3d.PathPatch3D #Change the class
    pathpatch._code3d = path.codes #Copy the codes
    pathpatch._facecolor3d = pathpatch.get_facecolor #Get the face color    

    verts = path.vertices #Get the vertices in 2D

    M = rotation_matrix(normal,(0, 0, 1)) #Get the rotation matrix

    pathpatch._segment3d = np.array([np.dot(M, (x, y, 0)) + (0, 0, z) for x, y in verts])

def pathpatch_translate(pathpatch, delta):
    """
    Translates the 3D pathpatch by the amount delta.
    """
    pathpatch._segment3d += delta
于 2015-10-19T11:50:28.970 回答
1

这是一种更通用的方法,它允许以比法线更复杂的方式嵌入:

class EmbeddedPatch2D(art3d.PathPatch3D):
    def __init__(self, patch, transform):
        assert transform.shape == (4, 3)

        self._patch2d = patch
        self.transform = transform

        self._path2d = patch.get_path()
        self._facecolor2d = patch.get_facecolor()

        self.set_3d_properties()

    def set_3d_properties(self, *args, **kwargs):
        # get the fully-transformed path
        path = self._patch2d.get_path()
        trans = self._patch2d.get_patch_transform()
        path = trans.transform_path(path)

        # copy across the relevant properties
        self._code3d = path.codes
        self._facecolor3d = self._patch2d.get_facecolor()

        # calculate the transformed vertices
        verts = np.empty(path.vertices.shape + np.array([0, 1]))
        verts[:,:-1] = path.vertices
        verts[:,-1] = 1
        self._segment3d = verts.dot(self.transform.T)[:,:-1]

    def __getattr__(self, key):
        return getattr(self._patch2d, key)

要在问题中根据需要使用它,我们需要一个辅助函数

def matrix_from_normal(normal):
    """
    given a normal vector, builds a homogeneous rotation matrix such that M.dot([1, 0, 0]) == normal
    """ 
    normal = normal / np.linalg.norm(normal)
    res = np.eye(normal.ndim+1)
    res[:-1,0] = normal
    if normal [0] == 0:
        perp = [0, -normal[2], normal[1]]
    else:
        perp = np.cross(normal, [1, 0, 0])
        perp /= np.linalg.norm(perp)
    res[:-1,1] = perp
    res[:-1,2] = np.cross(self.dir, perp)
    return res

全部一起:

circ = Circle((0,0), .2, facecolor = 'y', alpha = .2)
# the matrix here turns (x, y, 1) into (0, x, y, 1)
mat = matrix_from_normal([1, 1, 0]).dot([
    [0, 0, 0],
    [1, 0, 0],
    [0, 1, 0],
    [0, 0, 1]
])
circ3d = EmbeddedPatch2D(circ, mat)
于 2016-07-26T20:02:42.320 回答
0

我想分享我的解决方案,扩展了以前的建议。它可以将 3d 元素和文本添加到 Axes3D 演示文稿中。

    # creation of a rotation matrix that preserves the x-axis in an xy-plane of the original coordinate system
    def rotationMatrix(normal):
        norm = np.linalg.norm(normal)
        if norm ==0: return Rotation.identity(None)
        zDir = normal/norm
        if np.abs(zDir[2])==1:
            yDir = np.array([0,zDir[2],0])
        else:
            yDir = (np.array([0,0,1]) - zDir[2]*zDir)/math.sqrt(1-zDir[2]**2)
        rotMat = np.empty((3,3))
        rotMat[:,0] = np.cross(zDir,yDir)
        rotMat[:,1] = yDir
        rotMat[:,2] = -zDir
        return Rotation.from_matrix(rotMat)

    def toVector(vec):
        if vec is None or not isinstance(vec,np.ndarray) : vec="z"
        if isinstance(vec,str):
            zdir = vec[0] if len(vec)>0 else "z"
            if not zdir in "xyz": zdir="z"
            index = "xyz".index(vec)
            return np.roll((1.0,0,0), index)
        else:
            return vec

    # Transforms a 2D Patch to a 3D patch using a pivot point and a the given normal vector.
    def pathpatch_2d_to_3d(pathpatch, pivot=np.zeros(3), zDir='z'):

        path = pathpatch.get_path() #Get the path and the associated transform
        trans = pathpatch.get_patch_transform()
        path = trans.transform_path(path) #Apply the transform

        pathpatch.__class__ =  mplot3d.art3d.PathPatch3D #Change the class
        pathpatch._path2d = path       #Copy the 2d path
        pathpatch._code3d = path.codes #Copy the codes
        pathpatch._facecolor3d = pathpatch.get_facecolor #Get the face color

        # Get the 2D vertices and add the third dimension
        verts3d = np.empty((path.vertices.shape[0],3))
        verts3d[:,0:2] = path.vertices
        verts3d[:,2] = pivot[2]
        R = rotationMatrix(toVector(zDir))
        pathpatch._segment3d = R.apply(verts3d - pivot) + pivot
        return pathpatch

    # places a 3D text element in axes with 3d projection. 
    def text3d(xyz, text, zDir="z", scalefactor=1.0, fp=FontProperties(), **kwargs):
        pt = PathPatch(TextPath(xyz[0:2], text, size=scalefactor*fp.get_size(), prop=fp , usetex=False),**kwargs)
        ax3D.add_patch(pathpatch_2d_to_3d(pt, xyz, zDir))

    # places a 3D circle in axes with 3d projection. 
    def circle3d(center, radius, zDir='z', **kwargs):
        pc = Circle(center[0:2], radius, **kwargs)
        ax3D.add_patch(pathpatch_2d_to_3d(pc, center, zDir))
于 2021-10-31T08:48:18.100 回答