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

Skip to content

Commit 28e5798

Browse files
authored
Merge pull request #22046 from scottshambaugh/3d_plot_focal_length
Add the ability to change the focal length of the camera for 3D plots
2 parents 4b3d513 + b0135c7 commit 28e5798

File tree

6 files changed

+164
-23
lines changed

6 files changed

+164
-23
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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. The default focal length of 1 corresponds to a
6+
Field of View (FOV) of 90 deg, and is backwards-compatible with existing 3D
7+
plots. An increased focal length between 1 and infinity "flattens" the image,
8+
while a decreased focal length between 1 and 0 exaggerates the perspective and
9+
gives the image more apparent depth.
10+
11+
The focal length can be calculated from a desired FOV via the equation:
12+
13+
.. mathmpl::
14+
15+
focal\_length = 1/\tan(FOV/2)
16+
17+
.. plot::
18+
:include-source: true
19+
20+
from mpl_toolkits.mplot3d import axes3d
21+
import matplotlib.pyplot as plt
22+
fig, axs = plt.subplots(1, 3, subplot_kw={'projection': '3d'},
23+
constrained_layout=True)
24+
X, Y, Z = axes3d.get_test_data(0.05)
25+
focal_lengths = [0.25, 1, 4]
26+
for ax, fl in zip(axs, focal_lengths):
27+
ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
28+
ax.set_proj_type('persp', focal_length=fl)
29+
ax.set_title(f"focal_length = {fl}")
30+
plt.tight_layout()
31+
plt.show()

examples/mplot3d/projections.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
The default focal length of 1 corresponds to a Field of View (FOV) of 90 deg.
11+
An increased focal length between 1 and infinity "flattens" the image, while a
12+
decreased focal length between 1 and 0 exaggerates the perspective and gives
13+
the image more apparent depth. 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+
19+
.. mathmpl::
20+
21+
1 / \tan (FOV / 2)
22+
23+
Or vice versa:
24+
25+
.. mathmpl::
26+
27+
FOV = 2 * \atan (1 / focal length)
28+
29+
"""
30+
31+
from mpl_toolkits.mplot3d import axes3d
32+
import matplotlib.pyplot as plt
33+
34+
35+
fig, axs = plt.subplots(1, 3, subplot_kw={'projection': '3d'})
36+
37+
# Get the test data
38+
X, Y, Z = axes3d.get_test_data(0.05)
39+
40+
# Plot the data
41+
for ax in axs:
42+
ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
43+
44+
# Set the orthographic projection.
45+
axs[0].set_proj_type('ortho') # FOV = 0 deg
46+
axs[0].set_title("'ortho'\nfocal_length = ∞", fontsize=10)
47+
48+
# Set the perspective projections
49+
axs[1].set_proj_type('persp') # FOV = 90 deg
50+
axs[1].set_title("'persp'\nfocal_length = 1 (default)", fontsize=10)
51+
52+
axs[2].set_proj_type('persp', focal_length=0.2) # FOV = 157.4 deg
53+
axs[2].set_title("'persp'\nfocal_length = 0.2", fontsize=10)
54+
55+
plt.show()

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class Axes3D(Axes):
5757
def __init__(
5858
self, fig, rect=None, *args,
5959
elev=30, azim=-60, roll=0, sharez=None, proj_type='persp',
60-
box_aspect=None, computed_zorder=True,
60+
box_aspect=None, computed_zorder=True, focal_length=None,
6161
**kwargs):
6262
"""
6363
Parameters
@@ -104,6 +104,13 @@ def __init__(
104104
This behavior is deprecated in 3.4, the default will
105105
change to False in 3.5. The keyword will be undocumented
106106
and a non-False value will be an error in 3.6.
107+
focal_length : float, default: None
108+
For a projection type of 'persp', the focal length of the virtual
109+
camera. Must be > 0. If None, defaults to 1.
110+
For a projection type of 'ortho', must be set to either None
111+
or infinity (numpy.inf). If None, defaults to infinity.
112+
The focal length can be computed from a desired Field Of View via
113+
the equation: focal_length = 1/tan(FOV/2)
107114
108115
**kwargs
109116
Other optional keyword arguments:
@@ -117,7 +124,7 @@ def __init__(
117124
self.initial_azim = azim
118125
self.initial_elev = elev
119126
self.initial_roll = roll
120-
self.set_proj_type(proj_type)
127+
self.set_proj_type(proj_type, focal_length)
121128
self.computed_zorder = computed_zorder
122129

123130
self.xy_viewLim = Bbox.unit()
@@ -989,18 +996,33 @@ def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z"):
989996
dict(x=0, y=1, z=2), vertical_axis=vertical_axis
990997
)
991998

992-
def set_proj_type(self, proj_type):
999+
def set_proj_type(self, proj_type, focal_length=None):
9931000
"""
9941001
Set the projection type.
9951002
9961003
Parameters
9971004
----------
9981005
proj_type : {'persp', 'ortho'}
999-
"""
1000-
self._projection = _api.check_getitem({
1001-
'persp': proj3d.persp_transformation,
1002-
'ortho': proj3d.ortho_transformation,
1003-
}, proj_type=proj_type)
1006+
The projection type.
1007+
focal_length : float, default: None
1008+
For a projection type of 'persp', the focal length of the virtual
1009+
camera. Must be > 0. If None, defaults to 1.
1010+
The focal length can be computed from a desired Field Of View via
1011+
the equation: focal_length = 1/tan(FOV/2)
1012+
"""
1013+
_api.check_in_list(['persp', 'ortho'], proj_type=proj_type)
1014+
if proj_type == 'persp':
1015+
if focal_length is None:
1016+
focal_length = 1
1017+
elif focal_length <= 0:
1018+
raise ValueError(f"focal_length = {focal_length} must be "
1019+
"greater than 0")
1020+
self._focal_length = focal_length
1021+
elif proj_type == 'ortho':
1022+
if focal_length not in (None, np.inf):
1023+
raise ValueError(f"focal_length = {focal_length} must be "
1024+
f"None for proj_type = {proj_type}")
1025+
self._focal_length = np.inf
10041026

10051027
def _roll_to_vertical(self, arr):
10061028
"""Roll arrays to match the different vertical axis."""
@@ -1056,8 +1078,21 @@ def get_proj(self):
10561078
V = np.zeros(3)
10571079
V[self._vertical_axis] = -1 if abs(elev_rad) > 0.5 * np.pi else 1
10581080

1059-
viewM = proj3d.view_transformation(eye, R, V, roll_rad)
1060-
projM = self._projection(-self._dist, self._dist)
1081+
# Generate the view and projection transformation matrices
1082+
if self._focal_length == np.inf:
1083+
# Orthographic projection
1084+
viewM = proj3d.view_transformation(eye, R, V, roll_rad)
1085+
projM = proj3d.ortho_transformation(-self._dist, self._dist)
1086+
else:
1087+
# Perspective projection
1088+
# Scale the eye dist to compensate for the focal length zoom effect
1089+
eye_focal = R + self._dist * ps * self._focal_length
1090+
viewM = proj3d.view_transformation(eye_focal, R, V, roll_rad)
1091+
projM = proj3d.persp_transformation(-self._dist,
1092+
self._dist,
1093+
self._focal_length)
1094+
1095+
# Combine all the transformation matrices to get the final projection
10611096
M0 = np.dot(viewM, worldM)
10621097
M = np.dot(projM, M0)
10631098
return M
@@ -1120,7 +1155,7 @@ def cla(self):
11201155
pass
11211156

11221157
self._autoscaleZon = True
1123-
if self._projection is proj3d.ortho_transformation:
1158+
if self._focal_length == np.inf:
11241159
self._zmargin = rcParams['axes.zmargin']
11251160
else:
11261161
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: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,7 @@ def _test_proj_make_M():
871871
V = np.array([0, 0, 1])
872872
roll = 0
873873
viewM = proj3d.view_transformation(E, R, V, roll)
874-
perspM = proj3d.persp_transformation(100, -100)
874+
perspM = proj3d.persp_transformation(100, -100, 1)
875875
M = np.dot(perspM, viewM)
876876
return M
877877

@@ -1036,6 +1036,22 @@ def test_unautoscale(axis, auto):
10361036
np.testing.assert_array_equal(get_lim(), (-0.5, 0.5))
10371037

10381038

1039+
def test_axes3d_focal_length_checks():
1040+
fig = plt.figure()
1041+
ax = fig.add_subplot(projection='3d')
1042+
with pytest.raises(ValueError):
1043+
ax.set_proj_type('persp', focal_length=0)
1044+
with pytest.raises(ValueError):
1045+
ax.set_proj_type('ortho', focal_length=1)
1046+
1047+
1048+
@mpl3d_image_comparison(['axes3d_focal_length.png'], remove_text=False)
1049+
def test_axes3d_focal_length():
1050+
fig, axs = plt.subplots(1, 2, subplot_kw={'projection': '3d'})
1051+
axs[0].set_proj_type('persp', focal_length=np.inf)
1052+
axs[1].set_proj_type('persp', focal_length=0.15)
1053+
1054+
10391055
@mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False)
10401056
def test_axes3d_ortho():
10411057
fig = plt.figure()

0 commit comments

Comments
 (0)