我绘制了3D图,并用于quiver绘制x,y和z轴。
在matplotlib的交互式绘图中,我可以拖动和旋转3D绘图,但是存在一个问题:
当我拖动绘图时,似乎Z轴限制在一个平面上。无论我如何拖动绘图,Z轴只能以有限的方式(在一个平面内)旋转,而X轴和Y轴可以自由旋转。
我的问题是:这是对matplotlib的限制吗?是否有什么方法可以配置x,y和z轴的旋转方式?
任何建议表示赞赏。
随附一个最小的可复制示例,以供参考:
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
n_radii = 8
n_angles = 36
# Make radii and angles spaces (radius r=0 omitted to eliminate duplication).
radii = np.linspace(0.125, 1.0, n_radii)
angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)
# Repeat all angles for each radius.
angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
# Convert polar (radii, angles) coords to cartesian (x, y) coords.
# (0, 0) is manually added at this stage, so there will be no duplicate
# points in the (x, y) plane.
x = np.append(0, (radii*np.cos(angles)).flatten())
y = np.append(0, (radii*np.sin(angles)).flatten())
# Compute z to make the pringle surface.
z = np.sin(-x*y)
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot_trisurf(x, y, z, linewidth=0.2, antialiased=True)
steps = 100
theta = np.linspace(0, 2 * np.pi, steps)
r_max = 1.2
x = np.zeros_like(theta)
y = r_max * np.cos(theta)
z = r_max * np.sin(theta)
ax.plot(x, y, z, 'r')
ax.plot(y, x, z, 'g')
ax.plot(z, y, x, 'b')
scale = 1.08
ax.quiver((0,), (0), (0),
(0), (0), (r_max), color=('c'))
ax.text(0, 0, r_max * scale, 'Z Theta', weight='bold')
ax.quiver((0), (0), (0),
(0), (r_max), (0), color=('m'))
ax.text(0, r_max * scale, 0, 'Y', weight='bold')
ax.quiver((0), (0), (0),
(r_max), (0), (0), color=('y'))
ax.text(r_max * scale, 0, 0, 'X', weight='bold')
plt.show()
解决方案
我的第一个建议是这个。
但是,如果根本不可能,我找到了一个可行的解决方案。
The method _on_move in Axes3Dresponsible for processing the mouse events and rotating the plot.
As you can see this function only thinks in azimuth and elevation. That`s why it behaves the way it does.
It is possible to re-bind the default _on_move as seen in the method mouse_init() which is called in the constructor of Axes3D.
Say our custom mouse interaction style is defined in
def _my_on_move(self, event):
print('my custom mouse style', event)
This does not work:
ax._on_move = _my_on_move
because _my_on_move is a function but we need it to be a bound method so the self is available.
The solution is to bind the function as method, this is described in detail here:
import types
ax._on_move = types.MethodType(_my_on_move, ax)
and re-run the mouse initialization:
ax.mouse_init()
This part in the original _on_move will set elev and azim which are then used by get_proj() to set the
transformation matrix used in figure.canvas.draw_idle():
self.elev = art3d._norm_angle(self.elev - (dy/h)*180)
self.azim = art3d._norm_angle(self.azim - (dx/w)*180)
self.get_proj()
self.figure.canvas.draw_idle()
Somehow we have to sneak in a modified transformation matrix. I am not sure if there is a better way,
but we could just pass in modified values for elev and azim.
Since we want something smarter we should switch to quaternions.
I recommend using transformations.py
but there is also a module called mathutils from Blender with works fine.
Now to the fun part:
You have to get the current view (the current transformation matrix) and
rotate it based on the mouse movement. Then extract the equivalent elev and azim
from the rotated matrix. Fun task, some math, but it should be possible.
But I will leave that for to someone else :)
Maybe there is some inspitation found in VTK`s interactors or the ones from Blender.
If you want to try the interactors from Mayavi / VTK:
pip install mayavi (or pip3 install mayavi depending on your version and your virtual environment).
Then run
from mayavi import mlab
from tvtk.api import tvtk
for i in [tvtk.InteractorStyleTerrain(),
tvtk.InteractorStyleJoystickActor(),
tvtk.InteractorStyleTrackballActor(),
tvtk.InteractorStyleTrackball()]:
mlab.test_surf()
fig = mlab.gcf()
fig.scene.interactor.interactor_style = i
mlab.show()