Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e8a3f57

Browse files
Specify the focal length for 3d plots
Create a gallery example showing the different proj_type options Typo Test fix, example fix, and linting What's new, example fix Merge conflict Offset zooming of focal length Code review comments, consolidate projection functions Try and fix zooming Try to fix the focal length zooming Update example Cleanup example cleanup more example tweaks more example tweak swap plot order Enforce a positive focal length focal lentgh tests flake8 linting docstring tweak
1 parent 7457cb7 commit e8a3f57

File tree

6 files changed

+145
-23
lines changed

6 files changed

+145
-23
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Give the 3D camera a custom focal length
2+
----------------------------------------
3+
4+
Users can now better mimic real-world cameras by specifying the focal length of
5+
the virtual camera in 3D plots. An increasing focal length between 1 and
6+
+infinity "flattens" the image, while a decreasing focal length between 1 and 0
7+
exaggerates the perspective and gives the image more apparent depth. The
8+
default focal length of 1 corresponds to a Field of View (FOV) of 90 deg, and
9+
is backwards-compatible with existing 3D plots.
10+
11+
The focal length can be calculated from a desired FOV via the equation:
12+
| ``focal_length = 1/tan(FOV/2)``
13+
14+
.. plot::
15+
:include-source: true
16+
17+
from mpl_toolkits.mplot3d import axes3d
18+
import matplotlib.pyplot as plt
19+
fig, axs = plt.subplots(1, 3, subplot_kw={'projection': '3d'})
20+
X, Y, Z = axes3d.get_test_data(0.05)
21+
focal_lengths = [0.25, 1, 4]
22+
for ax, fl in zip(axs, focal_lengths):
23+
ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
24+
ax.set_proj_type('persp', focal_length=fl)
25+
ax.set_title(f"focal_length = {fl}")
26+
plt.tight_layout()
27+
plt.show()

examples/mplot3d/projections.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
========================
3+
3D plot projection types
4+
========================
5+
6+
Demonstrates the different camera projections for 3D plots, and the effects of
7+
changing the focal length for a perspective projection. Note that matplotlib
8+
corrects for the 'zoom' effect of changing the focal length.
9+
10+
An increasing focal length between 1 and +infinity "flattens" the image, while
11+
a decreasing focal length between 1 and 0 exaggerates the perspective and gives
12+
the image more apparent depth. The default focal length of 1 corresponds to a
13+
Field of View (FOV) of 90 deg. In the limiting case, a focal length of
14+
+infinity corresponds to an orthographic projection after correction of the
15+
zoom effect.
16+
17+
You can calculate focal length from a FOV via the equation:
18+
focal_length = 1/tan(FOV/2)
19+
20+
Or vice versa:
21+
FOV = 2*atan(1/focal_length)
22+
"""
23+
24+
from mpl_toolkits.mplot3d import axes3d
25+
import matplotlib.pyplot as plt
26+
27+
28+
fig, axs = plt.subplots(1, 3, subplot_kw={'projection': '3d'})
29+
30+
# Get the test data
31+
X, Y, Z = axes3d.get_test_data(0.05)
32+
33+
# Plot the data
34+
for ax in axs:
35+
ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
36+
37+
# Set the orthographic projection.
38+
axs[0].set_proj_type('ortho') # FOV = 0 deg
39+
axs[0].set_title("'ortho'\nfocal_length = +∞", fontsize=10)
40+
41+
# Set the perspective projections
42+
axs[1].set_proj_type('persp') # FOV = 90 deg
43+
axs[1].set_title("'persp'\nfocal_length = 1 (default)", fontsize=10)
44+
45+
axs[2].set_proj_type('persp', focal_length=0.2) # FOV = 157.4 deg
46+
axs[2].set_title("'persp'\nfocal_length = 0.2", fontsize=10)
47+
48+
plt.show()

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class Axes3D(Axes):
5656
def __init__(
5757
self, fig, rect=None, *args,
5858
elev=30, azim=-60, roll=0, sharez=None, proj_type='persp',
59-
box_aspect=None, computed_zorder=True,
59+
box_aspect=None, computed_zorder=True, focal_length=None,
6060
**kwargs):
6161
"""
6262
Parameters
@@ -103,6 +103,11 @@ def __init__(
103103
This behavior is deprecated in 3.4, the default will
104104
change to False in 3.5. The keyword will be undocumented
105105
and a non-False value will be an error in 3.6.
106+
focal_length : float, default: None
107+
For a projection type of 'persp', the focal length of the virtual
108+
camera. Must be > 0. If None, defaults to 1.
109+
The focal length can be computed from a desired Field Of View via
110+
the equation: focal_length = 1/tan(FOV/2)
106111
107112
**kwargs
108113
Other optional keyword arguments:
@@ -116,7 +121,7 @@ def __init__(
116121
self.initial_azim = azim
117122
self.initial_elev = elev
118123
self.initial_roll = roll
119-
self.set_proj_type(proj_type)
124+
self.set_proj_type(proj_type, focal_length)
120125
self.computed_zorder = computed_zorder
121126

122127
self.xy_viewLim = Bbox.unit()
@@ -1027,18 +1032,36 @@ def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z"):
10271032
dict(x=0, y=1, z=2), vertical_axis=vertical_axis
10281033
)
10291034

1030-
def set_proj_type(self, proj_type):
1035+
def set_proj_type(self, proj_type, focal_length=None):
10311036
"""
10321037
Set the projection type.
10331038
10341039
Parameters
10351040
----------
10361041
proj_type : {'persp', 'ortho'}
1037-
"""
1038-
self._projection = _api.check_getitem({
1039-
'persp': proj3d.persp_transformation,
1040-
'ortho': proj3d.ortho_transformation,
1041-
}, proj_type=proj_type)
1042+
The projection type.
1043+
focal_length : float, default: None
1044+
For a projection type of 'persp', the focal length of the virtual
1045+
camera. Must be > 0. If None, defaults to 1.
1046+
The focal length can be computed from a desired Field Of View via
1047+
the equation: focal_length = 1/tan(FOV/2)
1048+
"""
1049+
if proj_type == 'persp':
1050+
if focal_length is None:
1051+
self.focal_length = 1
1052+
else:
1053+
if focal_length <= 0:
1054+
raise ValueError(f"focal_length = {focal_length} must be" +
1055+
" greater than 0")
1056+
self.focal_length = focal_length
1057+
elif proj_type == 'ortho':
1058+
if focal_length not in (None, np.inf):
1059+
raise ValueError(f"focal_length = {focal_length} must be" +
1060+
f"None for proj_type = {proj_type}")
1061+
self.focal_length = np.inf
1062+
else:
1063+
raise ValueError(f"proj_type = {proj_type} must be in" +
1064+
f"{'persp', 'ortho'}")
10421065

10431066
def _roll_to_vertical(self, arr):
10441067
"""Roll arrays to match the different vertical axis."""
@@ -1094,8 +1117,21 @@ def get_proj(self):
10941117
V = np.zeros(3)
10951118
V[self._vertical_axis] = -1 if abs(elev_rad) > 0.5 * np.pi else 1
10961119

1097-
viewM = proj3d.view_transformation(eye, R, V, roll_rad)
1098-
projM = self._projection(-self.dist, self.dist)
1120+
# Generate the view and projection transformation matrices
1121+
if self.focal_length == np.inf:
1122+
# Orthographic projection
1123+
viewM = proj3d.view_transformation(eye, R, V, roll_rad)
1124+
projM = proj3d.ortho_transformation(-self.dist, self.dist)
1125+
else:
1126+
# Perspective projection
1127+
# Scale the eye dist to compensate for the focal length zoom effect
1128+
eye_focal = R + self.dist * ps * self.focal_length
1129+
viewM = proj3d.view_transformation(eye_focal, R, V, roll_rad)
1130+
projM = proj3d.persp_transformation(-self.dist,
1131+
self.dist,
1132+
self.focal_length)
1133+
1134+
# Combine all the transformation matrices to get the final projection
10991135
M0 = np.dot(viewM, worldM)
11001136
M = np.dot(projM, M0)
11011137
return M
@@ -1158,7 +1194,7 @@ def cla(self):
11581194
pass
11591195

11601196
self._autoscaleZon = True
1161-
if self._projection is proj3d.ortho_transformation:
1197+
if self.focal_length == np.inf:
11621198
self._zmargin = rcParams['axes.zmargin']
11631199
else:
11641200
self._zmargin = 0.

lib/mpl_toolkits/mplot3d/proj3d.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,23 +90,27 @@ def view_transformation(E, R, V, roll):
9090
return np.dot(Mr, Mt)
9191

9292

93-
def persp_transformation(zfront, zback):
94-
a = (zfront+zback)/(zfront-zback)
95-
b = -2*(zfront*zback)/(zfront-zback)
96-
return np.array([[1, 0, 0, 0],
97-
[0, 1, 0, 0],
98-
[0, 0, a, b],
99-
[0, 0, -1, 0]])
93+
def persp_transformation(zfront, zback, focal_length):
94+
e = focal_length
95+
a = 1 # aspect ratio
96+
b = (zfront+zback)/(zfront-zback)
97+
c = -2*(zfront*zback)/(zfront-zback)
98+
proj_matrix = np.array([[e, 0, 0, 0],
99+
[0, e/a, 0, 0],
100+
[0, 0, b, c],
101+
[0, 0, -1, 0]])
102+
return proj_matrix
100103

101104

102105
def ortho_transformation(zfront, zback):
103106
# note: w component in the resulting vector will be (zback-zfront), not 1
104107
a = -(zfront + zback)
105108
b = -(zfront - zback)
106-
return np.array([[2, 0, 0, 0],
107-
[0, 2, 0, 0],
108-
[0, 0, -2, 0],
109-
[0, 0, a, b]])
109+
proj_matrix = np.array([[2, 0, 0, 0],
110+
[0, 2, 0, 0],
111+
[0, 0, -2, 0],
112+
[0, 0, a, b]])
113+
return proj_matrix
110114

111115

112116
def _proj_transform_vec(vec, M):

lib/mpl_toolkits/tests/test_mplot3d.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,7 @@ def _test_proj_make_M():
872872
V = np.array([0, 0, 1])
873873
roll = 0
874874
viewM = proj3d.view_transformation(E, R, V, roll)
875-
perspM = proj3d.persp_transformation(100, -100)
875+
perspM = proj3d.persp_transformation(100, -100, 1)
876876
M = np.dot(perspM, viewM)
877877
return M
878878

@@ -1037,6 +1037,13 @@ def test_unautoscale(axis, auto):
10371037
np.testing.assert_array_equal(get_lim(), (-0.5, 0.5))
10381038

10391039

1040+
@mpl3d_image_comparison(['axes3d_focal_length.png'], remove_text=False)
1041+
def test_axes3d_focal_length():
1042+
fig, axs = plt.subplots(1, 2, subplot_kw={'projection': '3d'})
1043+
axs[0].set_proj_type('persp', focal_length=np.inf)
1044+
axs[1].set_proj_type('persp', focal_length=0.15)
1045+
1046+
10401047
@mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False)
10411048
def test_axes3d_ortho():
10421049
fig = plt.figure()

0 commit comments

Comments
 (0)