matplotlib如何将2D面片转换为具有任意法线的3D?

vohkndzv  于 2023-05-07  发布在  其他
关注(0)|答案(5)|浏览(138)

简短问题

matplotlib如何将2D面片转换为具有任意法线的3D?

长问题

我想绘制Patches在轴与三维投影。但是,mpl_toolkits.mplot3d.art3d提供的方法仅提供了使面片具有沿着主轴的法线的方法。我如何添加补丁到3d轴有任意法线?

sy5wg1nm

sy5wg1nm1#

简答

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

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
    1.计算将z方向旋转到法线方向的旋转矩阵R(rotation_matrix
    1.将旋转矩阵应用于所有顶点(pathpatch_2d_to_3d
    1.沿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)

xmd2e60i

xmd2e60i2#

非常有用的代码,但有一个小警告:它不能处理指向下的法线,因为它只使用Angular 的正弦。
你还需要使用余弦:

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
ix0qys7i

ix0qys7i3#

这里有一个更通用的方法,它允许以比沿着法线更复杂的方式嵌入:

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)

要按照问题中的要求使用它,我们需要一个helper函数

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

b4qexyjb4#

我想分享我的解决方案,扩展了以前的建议。它允许将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))
nlejzf6q

nlejzf6q5#

从matplotlib 3.7开始,你可以对Patch3D.set_3d_properties进行猴子补丁,art3d.pathpatch_2d_to_3d就可以正常工作了。

import mpl_toolkits.mplot3d.art3d
import numpy as np

def _transform_zdir(zdir):
    zdir = mpl_toolkits.mplot3d.art3d.get_dir_vector(zdir)
    zn = zdir / np.linalg.norm(zdir)

    cos_angle = zn[2]
    sin_angle = np.linalg.norm(zn[:2])
    if sin_angle == 0:
        return np.sign(cos_angle) * np.eye(3)

    d = np.array((zn[1], -zn[0], 0))
    d /= sin_angle
    ddt = np.outer(d, d)
    skew = np.array([[0, 0, -d[1]], [0, 0, d[0]], [d[1], -d[0], 0]], dtype=np.float64)
    return ddt + cos_angle * (np.eye(3) - ddt) + sin_angle * skew

def set_3d_properties(self, verts, zs=0, zdir="z"):
    zs = np.broadcast_to(zs, len(verts))
    self._segment3d = np.asarray(
        [
            np.dot(_transform_zdir(zdir), (x, y, 0)) + (0, 0, z)
            for ((x, y), z) in zip(verts, zs)
        ]
    )

def pathpatch_translate(pathpatch, delta):
    pathpatch._segment3d += np.asarray(delta)

mpl_toolkits.mplot3d.art3d.Patch3D.set_3d_properties = set_3d_properties
mpl_toolkits.mplot3d.art3d.Patch3D.translate = pathpatch_translate

这里,_transform_zdirT. Lepage's answerrotation_matrix的简化版本。那么,被接受的答案中的例子将简化为

import itertools

import matplotlib.pyplot as plt
from matplotlib.patches import Circle
from mpl_toolkits.mplot3d import art3d

ax = plt.axes(projection="3d")  # Create axes

p = ax.add_patch(Circle((0, 0), 0.2, facecolor="r"))
art3d.pathpatch_2d_to_3d(p, z=0.5, zdir="y")
p.translate((0.5, 1, 0))

for normal in itertools.product((-1, 1), repeat=3):
    p = ax.add_patch(Circle((0, 0), 0.2, facecolor="y", alpha=0.2))
    art3d.pathpatch_2d_to_3d(p, z=0, zdir=normal)
    p.translate(0.5)

更方便的是,您可以使用

def to_3d(pathpatch, z=0.0, zdir="z", delta=(0, 0, 0)):
    if not hasattr(pathpatch.axes, "get_zlim"):
        raise ValueError("Axes projection must be 3D")
    mpl_toolkits.mplot3d.art3d.pathpatch_2d_to_3d(pathpatch, z=z, zdir=zdir)
    pathpatch.translate(delta)
    return pathpatch

matplotlib.patches.Patch.to_3d = to_3d

然后

ax.add_patch(Circle((0, 0), 0.2, facecolor="r")).to_3d(z=0.5, zdir='y', delta=(0.5, 1, 0))

相关问题