-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Change order of mplot3d view angles to match order of rotations #28395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,7 @@ | |
====================== | ||
|
||
This example generates an "unfolded" 3D plot that shows each of the primary 3D | ||
view planes. The elevation, azimuth, and roll angles required for each view are | ||
view planes. The azimuth, elevation, and roll angles required for each view are | ||
labeled. You could print out this image and fold it into a box where each plane | ||
forms a side of the box. | ||
""" | ||
|
@@ -16,13 +16,13 @@ def annotate_axes(ax, text, fontsize=18): | |
ax.text(x=0.5, y=0.5, z=0.5, s=text, | ||
va="center", ha="center", fontsize=fontsize, color="black") | ||
|
||
# (plane, (elev, azim, roll)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change removes the ability to copy-paste these docs into one's functions, and is confusing wrt the keyword ordering below. |
||
views = [('XY', (90, -90, 0)), | ||
('XZ', (0, -90, 0)), | ||
('YZ', (0, 0, 0)), | ||
('-XY', (-90, 90, 0)), | ||
('-XZ', (0, 90, 0)), | ||
('-YZ', (0, 180, 0))] | ||
# (plane, (azim, elev, roll)) | ||
views = [('XY', (-90, 90, 0)), | ||
('XZ', (-90, 0, 0)), | ||
('YZ', (0, 0, 0)), | ||
('-XY', (90, -90, 0)), | ||
('-XZ', (90, 0, 0)), | ||
('-YZ', (180, 0, 0))] | ||
|
||
layout = [['XY', '.', 'L', '.'], | ||
['XZ', 'YZ', '-XZ', '-YZ'], | ||
|
@@ -34,10 +34,10 @@ def annotate_axes(ax, text, fontsize=18): | |
axd[plane].set_ylabel('y') | ||
axd[plane].set_zlabel('z') | ||
axd[plane].set_proj_type('ortho') | ||
axd[plane].view_init(elev=angles[0], azim=angles[1], roll=angles[2]) | ||
axd[plane].view_init(elev=angles[1], azim=angles[0], roll=angles[2]) | ||
axd[plane].set_box_aspect(None, zoom=1.25) | ||
|
||
label = f'{plane}\n{angles}' | ||
label = f'{plane}\nazim={angles[0]}\nelev={angles[1]}\nroll={angles[2]}' | ||
annotate_axes(axd[plane], label, fontsize=14) | ||
|
||
for plane in ('XY', '-XY'): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -139,7 +139,11 @@ | |
|
||
# inhibit autoscale_view until the axes are defined | ||
# they can't be defined until Axes.__init__ has been called | ||
self.view_init(self.initial_elev, self.initial_azim, self.initial_roll) | ||
self.view_init( | ||
elev=self.initial_elev, | ||
azim=self.initial_azim, | ||
roll=self.initial_roll, | ||
) | ||
|
||
self._sharez = sharez | ||
if sharez is not None: | ||
|
@@ -1094,25 +1098,66 @@ | |
def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z", | ||
share=False): | ||
""" | ||
Set the elevation and azimuth of the Axes in degrees (not radians). | ||
Set the azimuth, elevation, and roll of the Axes, in degrees (not radians). | ||
|
||
This can be used to rotate the Axes programmatically. | ||
|
||
To look normal to the primary planes, the following elevation and | ||
azimuth angles can be used. A roll angle of 0, 90, 180, or 270 deg | ||
will rotate these views while keeping the axes at right angles. | ||
To look normal to the primary planes, the following azimuth and | ||
elevation angles can be used: | ||
|
||
========== ==== ==== | ||
view plane elev azim | ||
view plane azim elev | ||
========== ==== ==== | ||
XY 90 -90 | ||
XZ 0 -90 | ||
YZ 0 0 | ||
-XY -90 90 | ||
-XZ 0 90 | ||
-YZ 0 180 | ||
XY -90 90 | ||
XZ -90 0 | ||
YZ 0 0 | ||
-XY 90 -90 | ||
-XZ 90 0 | ||
-YZ 180 0 | ||
========== ==== ==== | ||
|
||
A roll angle of 0, 90, 180, or 270 degrees will rotate these views | ||
while keeping the axes at right angles. | ||
|
||
The *azim*, *elev*, *roll* angles correspond to rotations of the scene | ||
observed by a stationary camera, as follows (assuming a default vertical | ||
axis of 'z'). First, a left-handed rotation about the z axis is applied | ||
(*azim*), then a right-handed rotation about the (camera) y axis (*elev*), | ||
then a right-handed rotation about the (camera) x axis (*roll*). Here, | ||
the z, y, and x axis are fixed axes (not the axes that rotate together | ||
with the original scene). | ||
|
||
If you would like to make the connection with quaternions (because | ||
`Euler angles are horrible | ||
<https://github.com/moble/quaternion/wiki/Euler-angles-are-horrible>`_): | ||
the *azim*, *elev*, *roll* angles relate to the (intrinsic) rotation of | ||
the plot via: | ||
|
||
*q* = exp(+roll **x̂** / 2) exp(+elev **ŷ** / 2) exp(−azim **ẑ** / 2) | ||
|
||
(with angles given in radians instead of degrees). That is, the angles | ||
are a kind of `Tait-Bryan angles | ||
<https://en.wikipedia.org/wiki/Euler_angles#Tait%E2%80%93Bryan_angles>`_: | ||
−z, +y', +x", rather than classic `Euler angles | ||
<https://en.wikipedia.org/wiki/Euler_angles>`_. | ||
|
||
To avoid confusion, it makes sense to provide the view angles as keyword | ||
arguments: | ||
``.view_init(azim=-60, elev=30, roll=0, ...)`` | ||
This specific order is consistent with the order in which the rotations | ||
actually are applied. Moreover, this particular order appears to be most | ||
common, see :ghissue:`28353`, and it is consistent with the ordering in | ||
`matplotlib.colors.LightSource`. | ||
|
||
For backwards compatibility, positional arguments in the old sequence | ||
(first ``elev``, then ``azim``) will still be accepted; but preferably, | ||
use keyword arguments, to avoid confusion as to which angle is which. | ||
Unfortunately, the order of the positional arguments does not match | ||
the actual order of the applied rotations, and it differs from that | ||
used in other programs (``azim, elev``). It would be nice if the sensible | ||
(keyword) ordering could take over eventually. | ||
|
||
|
||
Parameters | ||
---------- | ||
elev : float, default: None | ||
|
@@ -1145,10 +1190,10 @@ | |
|
||
self._dist = 10 # The camera distance from origin. Behaves like zoom | ||
|
||
if elev is None: | ||
elev = self.initial_elev | ||
if azim is None: | ||
azim = self.initial_azim | ||
if elev is None: | ||
elev = self.initial_elev | ||
if roll is None: | ||
roll = self.initial_roll | ||
vertical_axis = _api.check_getitem( | ||
|
@@ -1163,8 +1208,8 @@ | |
axes = [self] | ||
|
||
for ax in axes: | ||
ax.elev = elev | ||
ax.azim = azim | ||
ax.elev = elev | ||
ax.roll = roll | ||
ax._vertical_axis = vertical_axis | ||
|
||
|
@@ -1229,15 +1274,15 @@ | |
# Look into the middle of the world coordinates: | ||
R = 0.5 * box_aspect | ||
|
||
# elev: elevation angle in the z plane. | ||
# azim: azimuth angle in the xy plane. | ||
# elev: elevation angle in the z plane. | ||
# Coordinates for a point that rotates around the box of data. | ||
# p0, p1 corresponds to rotating the box only around the vertical axis. | ||
# p2 corresponds to rotating the box only around the horizontal axis. | ||
elev_rad = np.deg2rad(self.elev) | ||
azim_rad = np.deg2rad(self.azim) | ||
p0 = np.cos(elev_rad) * np.cos(azim_rad) | ||
p1 = np.cos(elev_rad) * np.sin(azim_rad) | ||
elev_rad = np.deg2rad(self.elev) | ||
p0 = np.cos(azim_rad) * np.cos(elev_rad) | ||
p1 = np.sin(azim_rad) * np.cos(elev_rad) | ||
p2 = np.sin(elev_rad) | ||
|
||
# When changing vertical axis the coordinates changes as well. | ||
|
@@ -1339,8 +1384,13 @@ | |
self._shared_axes["view"].join(self, other) | ||
self._shareview = other | ||
vertical_axis = self._axis_names[other._vertical_axis] | ||
self.view_init(elev=other.elev, azim=other.azim, roll=other.roll, | ||
vertical_axis=vertical_axis, share=True) | ||
self.view_init( | ||
elev=other.elev, | ||
azim=other.azim, | ||
roll=other.roll, | ||
vertical_axis=vertical_axis, | ||
share=True, | ||
) | ||
|
||
def clear(self): | ||
# docstring inherited. | ||
|
@@ -1392,8 +1442,8 @@ | |
# docstring inherited | ||
props, (elev, azim, roll) = view | ||
self.set(**props) | ||
self.elev = elev | ||
self.azim = azim | ||
self.elev = elev | ||
self.roll = roll | ||
|
||
def format_zdata(self, z): | ||
|
@@ -1430,11 +1480,11 @@ | |
""" | ||
Return the rotation angles as a string. | ||
""" | ||
norm_elev = art3d._norm_angle(self.elev) | ||
norm_azim = art3d._norm_angle(self.azim) | ||
norm_elev = art3d._norm_angle(self.elev) | ||
norm_roll = art3d._norm_angle(self.roll) | ||
coords = (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, " | ||
f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, " | ||
coords = (f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, " | ||
f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, " | ||
f"roll={norm_roll:.0f}\N{DEGREE SIGN}" | ||
).replace("-", "\N{MINUS SIGN}") | ||
return coords | ||
|
@@ -1561,10 +1611,10 @@ | |
return | ||
|
||
# Convert to quaternion | ||
elev = np.deg2rad(self.elev) | ||
azim = np.deg2rad(self.azim) | ||
elev = np.deg2rad(self.elev) | ||
roll = np.deg2rad(self.roll) | ||
q = _Quaternion.from_cardan_angles(elev, azim, roll) | ||
q = _Quaternion.from_cardan_angles(azim, elev, roll) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not the order that the function API defines. Please be very careful what you change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...well actually, the order is the order that the _Quaternion API defines, and the change is as intended. Since the quaternion class has its own API, it is not bound by my previous mistake to force it to be consistent with the Axes3D class. And furthermore, _Quaternion is an internal class, so I have little hesitation to change it back to the way it was initially, with a logical progression of arguments for |
||
|
||
# Update quaternion - a variation on Ken Shoemake's ARCBALL | ||
current_vec = self._arcball(self._sx/w, self._sy/h) | ||
|
@@ -1573,18 +1623,13 @@ | |
q = dq * q | ||
|
||
# Convert to elev, azim, roll | ||
elev, azim, roll = q.as_cardan_angles() | ||
azim, elev, roll = q.as_cardan_angles() | ||
timhoffm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
azim = np.rad2deg(azim) | ||
elev = np.rad2deg(elev) | ||
roll = np.rad2deg(roll) | ||
vertical_axis = self._axis_names[self._vertical_axis] | ||
self.view_init( | ||
elev=elev, | ||
azim=azim, | ||
roll=roll, | ||
vertical_axis=vertical_axis, | ||
share=True, | ||
) | ||
self.view_init(elev, azim, roll, vertical_axis=vertical_axis, | ||
share=True) | ||
self.stale = True | ||
|
||
# Pan | ||
|
@@ -3662,10 +3707,10 @@ | |
quiversize = np.mean(np.diff(quiversize, axis=0)) | ||
# quiversize is now in Axes coordinates, and to convert back to data | ||
# coordinates, we need to run it through the inverse 3D transform. For | ||
# consistency, this uses a fixed elevation, azimuth, and roll. | ||
# consistency, this uses a fixed azimuth, elevation, and roll. | ||
with cbook._setattr_cm(self, elev=0, azim=0, roll=0): | ||
invM = np.linalg.inv(self.get_proj()) | ||
# elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is | ||
# azim=elev=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is | ||
# 'y' in 3D, hence the 1 index. | ||
quiversize = np.dot(invM, [quiversize, 0, 0, 0])[1] | ||
# Quivers use a fixed 15-degree arrow head, so scale up the length so | ||
|
@@ -4000,7 +4045,7 @@ | |
return q | ||
|
||
@classmethod | ||
def from_cardan_angles(cls, elev, azim, roll): | ||
def from_cardan_angles(cls, azim, elev, roll): | ||
""" | ||
Converts the angles to a quaternion | ||
q = exp((roll/2)*e_x)*exp((elev/2)*e_y)*exp((-azim/2)*e_z) | ||
|
@@ -4027,4 +4072,4 @@ | |
azim = np.arctan2(2*(-qw*qz+qx*qy), qw*qw+qx*qx-qy*qy-qz*qz) | ||
elev = np.arcsin( 2*( qw*qy+qz*qx)/(qw*qw+qx*qx+qy*qy+qz*qz)) # noqa E201 | ||
roll = np.arctan2(2*( qw*qx-qy*qz), qw*qw-qx*qx-qy*qy+qz*qz) # noqa E201 | ||
return elev, azim, roll | ||
return azim, elev, roll |
Uh oh!
There was an error while loading. Please reload this page.