Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7.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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -33,19 +33,19 @@ | ||
angle_norm = (angle + 180) % 360 - 180 | ||
# Cycle through a full rotation of elevation, then azimuth, roll, and all | ||
azim =elev = roll = 0 | ||
if angle <= 360: | ||
elev = angle_norm | ||
elif angle <= 360*2: | ||
azim = angle_norm | ||
elif angle <= 360*3: | ||
roll = angle_norm | ||
else: | ||
azim =elev = roll = angle_norm | ||
# Update the axis view and title | ||
ax.view_init(elev=elev, azim=azim, roll=roll) | ||
timhoffm marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
plt.title('Azimuth: %d°,Elevation: %d°, Roll: %d°' % (azim, elev, roll)) | ||
plt.draw() | ||
plt.pause(.001) |
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. Theazimuth, 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") | ||
Contributor
| ||
# (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[1], azim=angles[0], roll=angles[2]) | ||
axd[plane].set_box_aspect(None, zoom=1.25) | ||
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( | ||
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 theazimuth,elevation, androll 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 azimuth and | ||
elevation angles can be used: | ||
========== ==== ==== | ||
view planeazim elev | ||
========== ==== ==== | ||
XY-90 90 | ||
XZ-900 | ||
YZ 0 0 | ||
-XY90-90 | ||
-XZ 90 0 | ||
-YZ1800 | ||
========== ==== ==== | ||
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 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.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 | ||
# 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. | ||
azim_rad = np.deg2rad(self.azim) | ||
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, | ||
) | ||
def clear(self): | ||
# docstring inherited. | ||
@@ -1392,8 +1442,8 @@ | ||
# docstring inherited | ||
props, (elev, azim, roll) = view | ||
self.set(**props) | ||
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_azim = art3d._norm_angle(self.azim) | ||
norm_elev = art3d._norm_angle(self.elev) | ||
norm_roll = art3d._norm_angle(self.roll) | ||
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 | ||
azim = np.deg2rad(self.azim) | ||
elev = np.deg2rad(self.elev) | ||
roll = np.deg2rad(self.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 orderis 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 | ||
azim, elev, roll = q.as_cardan_angles() | ||
timhoffm marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
azim = np.rad2deg(azim) | ||
elev = np.rad2deg(elev) | ||
roll = np.rad2deg(roll) | ||
vertical_axis = self._axis_names[self._vertical_axis] | ||
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 fixedazimuth, elevation, and roll. | ||
with cbook._setattr_cm(self, elev=0, azim=0, roll=0): | ||
invM = np.linalg.inv(self.get_proj()) | ||
# 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,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 | ||
returnazim, elev, roll |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.