From e8a3f57a9238efc3e4f540f59bd80533d9923dd0 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> Date: Thu, 23 Dec 2021 14:41:43 -0500 Subject: [PATCH 1/4] 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 --- .../next_whats_new/3d_plot_focal_length.rst | 27 ++++++++ examples/mplot3d/projections.py | 48 +++++++++++++++ lib/mpl_toolkits/mplot3d/axes3d.py | 58 ++++++++++++++---- lib/mpl_toolkits/mplot3d/proj3d.py | 26 ++++---- .../test_mplot3d/axes3d_focal_length.png | Bin 0 -> 64566 bytes lib/mpl_toolkits/tests/test_mplot3d.py | 9 ++- 6 files changed, 145 insertions(+), 23 deletions(-) create mode 100644 doc/users/next_whats_new/3d_plot_focal_length.rst create mode 100644 examples/mplot3d/projections.py create mode 100644 lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_focal_length.png diff --git a/doc/users/next_whats_new/3d_plot_focal_length.rst b/doc/users/next_whats_new/3d_plot_focal_length.rst new file mode 100644 index 000000000000..fe9e5dd1e10a --- /dev/null +++ b/doc/users/next_whats_new/3d_plot_focal_length.rst @@ -0,0 +1,27 @@ +Give the 3D camera a custom focal length +---------------------------------------- + +Users can now better mimic real-world cameras by specifying the focal length of +the virtual camera in 3D plots. An increasing focal length between 1 and ++infinity "flattens" the image, while a decreasing focal length between 1 and 0 +exaggerates the perspective and gives the image more apparent depth. The +default focal length of 1 corresponds to a Field of View (FOV) of 90 deg, and +is backwards-compatible with existing 3D plots. + +The focal length can be calculated from a desired FOV via the equation: +| ``focal_length = 1/tan(FOV/2)`` + +.. plot:: + :include-source: true + + from mpl_toolkits.mplot3d import axes3d + import matplotlib.pyplot as plt + fig, axs = plt.subplots(1, 3, subplot_kw={'projection': '3d'}) + X, Y, Z = axes3d.get_test_data(0.05) + focal_lengths = [0.25, 1, 4] + for ax, fl in zip(axs, focal_lengths): + ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) + ax.set_proj_type('persp', focal_length=fl) + ax.set_title(f"focal_length = {fl}") + plt.tight_layout() + plt.show() diff --git a/examples/mplot3d/projections.py b/examples/mplot3d/projections.py new file mode 100644 index 000000000000..dbec121a1435 --- /dev/null +++ b/examples/mplot3d/projections.py @@ -0,0 +1,48 @@ +""" +======================== +3D plot projection types +======================== + +Demonstrates the different camera projections for 3D plots, and the effects of +changing the focal length for a perspective projection. Note that matplotlib +corrects for the 'zoom' effect of changing the focal length. + +An increasing focal length between 1 and +infinity "flattens" the image, while +a decreasing focal length between 1 and 0 exaggerates the perspective and gives +the image more apparent depth. The default focal length of 1 corresponds to a +Field of View (FOV) of 90 deg. In the limiting case, a focal length of ++infinity corresponds to an orthographic projection after correction of the +zoom effect. + +You can calculate focal length from a FOV via the equation: +focal_length = 1/tan(FOV/2) + +Or vice versa: +FOV = 2*atan(1/focal_length) +""" + +from mpl_toolkits.mplot3d import axes3d +import matplotlib.pyplot as plt + + +fig, axs = plt.subplots(1, 3, subplot_kw={'projection': '3d'}) + +# Get the test data +X, Y, Z = axes3d.get_test_data(0.05) + +# Plot the data +for ax in axs: + ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) + +# Set the orthographic projection. +axs[0].set_proj_type('ortho') # FOV = 0 deg +axs[0].set_title("'ortho'\nfocal_length = +∞", fontsize=10) + +# Set the perspective projections +axs[1].set_proj_type('persp') # FOV = 90 deg +axs[1].set_title("'persp'\nfocal_length = 1 (default)", fontsize=10) + +axs[2].set_proj_type('persp', focal_length=0.2) # FOV = 157.4 deg +axs[2].set_title("'persp'\nfocal_length = 0.2", fontsize=10) + +plt.show() diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index a82ab4455113..661f6233d486 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -56,7 +56,7 @@ class Axes3D(Axes): def __init__( self, fig, rect=None, *args, elev=30, azim=-60, roll=0, sharez=None, proj_type='persp', - box_aspect=None, computed_zorder=True, + box_aspect=None, computed_zorder=True, focal_length=None, **kwargs): """ Parameters @@ -103,6 +103,11 @@ def __init__( This behavior is deprecated in 3.4, the default will change to False in 3.5. The keyword will be undocumented and a non-False value will be an error in 3.6. + focal_length : float, default: None + For a projection type of 'persp', the focal length of the virtual + camera. Must be > 0. If None, defaults to 1. + The focal length can be computed from a desired Field Of View via + the equation: focal_length = 1/tan(FOV/2) **kwargs Other optional keyword arguments: @@ -116,7 +121,7 @@ def __init__( self.initial_azim = azim self.initial_elev = elev self.initial_roll = roll - self.set_proj_type(proj_type) + self.set_proj_type(proj_type, focal_length) self.computed_zorder = computed_zorder self.xy_viewLim = Bbox.unit() @@ -1027,18 +1032,36 @@ def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z"): dict(x=0, y=1, z=2), vertical_axis=vertical_axis ) - def set_proj_type(self, proj_type): + def set_proj_type(self, proj_type, focal_length=None): """ Set the projection type. Parameters ---------- proj_type : {'persp', 'ortho'} - """ - self._projection = _api.check_getitem({ - 'persp': proj3d.persp_transformation, - 'ortho': proj3d.ortho_transformation, - }, proj_type=proj_type) + The projection type. + focal_length : float, default: None + For a projection type of 'persp', the focal length of the virtual + camera. Must be > 0. If None, defaults to 1. + The focal length can be computed from a desired Field Of View via + the equation: focal_length = 1/tan(FOV/2) + """ + if proj_type == 'persp': + if focal_length is None: + self.focal_length = 1 + else: + if focal_length <= 0: + raise ValueError(f"focal_length = {focal_length} must be" + + " greater than 0") + self.focal_length = focal_length + elif proj_type == 'ortho': + if focal_length not in (None, np.inf): + raise ValueError(f"focal_length = {focal_length} must be" + + f"None for proj_type = {proj_type}") + self.focal_length = np.inf + else: + raise ValueError(f"proj_type = {proj_type} must be in" + + f"{'persp', 'ortho'}") def _roll_to_vertical(self, arr): """Roll arrays to match the different vertical axis.""" @@ -1094,8 +1117,21 @@ def get_proj(self): V = np.zeros(3) V[self._vertical_axis] = -1 if abs(elev_rad) > 0.5 * np.pi else 1 - viewM = proj3d.view_transformation(eye, R, V, roll_rad) - projM = self._projection(-self.dist, self.dist) + # Generate the view and projection transformation matrices + if self.focal_length == np.inf: + # Orthographic projection + viewM = proj3d.view_transformation(eye, R, V, roll_rad) + projM = proj3d.ortho_transformation(-self.dist, self.dist) + else: + # Perspective projection + # Scale the eye dist to compensate for the focal length zoom effect + eye_focal = R + self.dist * ps * self.focal_length + viewM = proj3d.view_transformation(eye_focal, R, V, roll_rad) + projM = proj3d.persp_transformation(-self.dist, + self.dist, + self.focal_length) + + # Combine all the transformation matrices to get the final projection M0 = np.dot(viewM, worldM) M = np.dot(projM, M0) return M @@ -1158,7 +1194,7 @@ def cla(self): pass self._autoscaleZon = True - if self._projection is proj3d.ortho_transformation: + if self.focal_length == np.inf: self._zmargin = rcParams['axes.zmargin'] else: self._zmargin = 0. diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index c7c2f93230be..7aa088d57e35 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -90,23 +90,27 @@ def view_transformation(E, R, V, roll): return np.dot(Mr, Mt) -def persp_transformation(zfront, zback): - a = (zfront+zback)/(zfront-zback) - b = -2*(zfront*zback)/(zfront-zback) - return np.array([[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, a, b], - [0, 0, -1, 0]]) +def persp_transformation(zfront, zback, focal_length): + e = focal_length + a = 1 # aspect ratio + b = (zfront+zback)/(zfront-zback) + c = -2*(zfront*zback)/(zfront-zback) + proj_matrix = np.array([[e, 0, 0, 0], + [0, e/a, 0, 0], + [0, 0, b, c], + [0, 0, -1, 0]]) + return proj_matrix def ortho_transformation(zfront, zback): # note: w component in the resulting vector will be (zback-zfront), not 1 a = -(zfront + zback) b = -(zfront - zback) - return np.array([[2, 0, 0, 0], - [0, 2, 0, 0], - [0, 0, -2, 0], - [0, 0, a, b]]) + proj_matrix = np.array([[2, 0, 0, 0], + [0, 2, 0, 0], + [0, 0, -2, 0], + [0, 0, a, b]]) + return proj_matrix def _proj_transform_vec(vec, M): diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_focal_length.png b/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_focal_length.png new file mode 100644 index 0000000000000000000000000000000000000000..1d61e0a0c0f63f73e41eacb10a8f81b5e0a09f1e GIT binary patch literal 64566 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 literal 0 HcmV?d00001 diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index 6141fe8dc079..0b9079808e0a 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -872,7 +872,7 @@ def _test_proj_make_M(): V = np.array([0, 0, 1]) roll = 0 viewM = proj3d.view_transformation(E, R, V, roll) - perspM = proj3d.persp_transformation(100, -100) + perspM = proj3d.persp_transformation(100, -100, 1) M = np.dot(perspM, viewM) return M @@ -1037,6 +1037,13 @@ def test_unautoscale(axis, auto): np.testing.assert_array_equal(get_lim(), (-0.5, 0.5)) +@mpl3d_image_comparison(['axes3d_focal_length.png'], remove_text=False) +def test_axes3d_focal_length(): + fig, axs = plt.subplots(1, 2, subplot_kw={'projection': '3d'}) + axs[0].set_proj_type('persp', focal_length=np.inf) + axs[1].set_proj_type('persp', focal_length=0.15) + + @mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False) def test_axes3d_ortho(): fig = plt.figure() From c1e5e0adf210be1192548b0c7133f675559c542a Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Wed, 5 Jan 2022 08:03:33 -0700 Subject: [PATCH 2/4] Code review changes Make focal_length a private attr linting --- .../next_whats_new/3d_plot_focal_length.rst | 7 ++++-- examples/mplot3d/projections.py | 17 +++++++++----- lib/mpl_toolkits/mplot3d/axes3d.py | 22 +++++++++---------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/doc/users/next_whats_new/3d_plot_focal_length.rst b/doc/users/next_whats_new/3d_plot_focal_length.rst index fe9e5dd1e10a..07666f6f4adb 100644 --- a/doc/users/next_whats_new/3d_plot_focal_length.rst +++ b/doc/users/next_whats_new/3d_plot_focal_length.rst @@ -3,13 +3,16 @@ Give the 3D camera a custom focal length Users can now better mimic real-world cameras by specifying the focal length of the virtual camera in 3D plots. An increasing focal length between 1 and -+infinity "flattens" the image, while a decreasing focal length between 1 and 0 +infinity "flattens" the image, while a decreasing focal length between 1 and 0 exaggerates the perspective and gives the image more apparent depth. The default focal length of 1 corresponds to a Field of View (FOV) of 90 deg, and is backwards-compatible with existing 3D plots. The focal length can be calculated from a desired FOV via the equation: -| ``focal_length = 1/tan(FOV/2)`` + +.. mathmpl:: + + focal\_length = 1/tan(FOV/2) .. plot:: :include-source: true diff --git a/examples/mplot3d/projections.py b/examples/mplot3d/projections.py index dbec121a1435..1432d3e25f12 100644 --- a/examples/mplot3d/projections.py +++ b/examples/mplot3d/projections.py @@ -7,18 +7,25 @@ changing the focal length for a perspective projection. Note that matplotlib corrects for the 'zoom' effect of changing the focal length. -An increasing focal length between 1 and +infinity "flattens" the image, while +An increasing focal length between 1 and infinity "flattens" the image, while a decreasing focal length between 1 and 0 exaggerates the perspective and gives the image more apparent depth. The default focal length of 1 corresponds to a Field of View (FOV) of 90 deg. In the limiting case, a focal length of -+infinity corresponds to an orthographic projection after correction of the +infinity corresponds to an orthographic projection after correction of the zoom effect. You can calculate focal length from a FOV via the equation: -focal_length = 1/tan(FOV/2) + +.. mathmpl:: + + 1 / \tan (FOV / 2) Or vice versa: -FOV = 2*atan(1/focal_length) + +.. mathmpl:: + + FOV = 2 * \atan (1 / focal length) + """ from mpl_toolkits.mplot3d import axes3d @@ -36,7 +43,7 @@ # Set the orthographic projection. axs[0].set_proj_type('ortho') # FOV = 0 deg -axs[0].set_title("'ortho'\nfocal_length = +∞", fontsize=10) +axs[0].set_title("'ortho'\nfocal_length = ∞", fontsize=10) # Set the perspective projections axs[1].set_proj_type('persp') # FOV = 90 deg diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 661f6233d486..b8d6b4344d97 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1046,22 +1046,20 @@ def set_proj_type(self, proj_type, focal_length=None): The focal length can be computed from a desired Field Of View via the equation: focal_length = 1/tan(FOV/2) """ + _api.check_in_list(['persp', 'ortho'], proj_type=proj_type) if proj_type == 'persp': if focal_length is None: - self.focal_length = 1 + self._focal_length = 1 else: if focal_length <= 0: - raise ValueError(f"focal_length = {focal_length} must be" + + raise ValueError(f"focal_length = {focal_length} must be" " greater than 0") - self.focal_length = focal_length + self._focal_length = focal_length elif proj_type == 'ortho': if focal_length not in (None, np.inf): - raise ValueError(f"focal_length = {focal_length} must be" + + raise ValueError(f"focal_length = {focal_length} must be" f"None for proj_type = {proj_type}") - self.focal_length = np.inf - else: - raise ValueError(f"proj_type = {proj_type} must be in" + - f"{'persp', 'ortho'}") + self._focal_length = np.inf def _roll_to_vertical(self, arr): """Roll arrays to match the different vertical axis.""" @@ -1118,18 +1116,18 @@ def get_proj(self): V[self._vertical_axis] = -1 if abs(elev_rad) > 0.5 * np.pi else 1 # Generate the view and projection transformation matrices - if self.focal_length == np.inf: + if self._focal_length == np.inf: # Orthographic projection viewM = proj3d.view_transformation(eye, R, V, roll_rad) projM = proj3d.ortho_transformation(-self.dist, self.dist) else: # Perspective projection # Scale the eye dist to compensate for the focal length zoom effect - eye_focal = R + self.dist * ps * self.focal_length + eye_focal = R + self.dist * ps * self._focal_length viewM = proj3d.view_transformation(eye_focal, R, V, roll_rad) projM = proj3d.persp_transformation(-self.dist, self.dist, - self.focal_length) + self._focal_length) # Combine all the transformation matrices to get the final projection M0 = np.dot(viewM, worldM) @@ -1194,7 +1192,7 @@ def cla(self): pass self._autoscaleZon = True - if self.focal_length == np.inf: + if self._focal_length == np.inf: self._zmargin = rcParams['axes.zmargin'] else: self._zmargin = 0. From 7bd9bd9d70ec5fc67b7766cfa6255c63a0da67eb Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Mon, 17 Jan 2022 19:47:03 -0700 Subject: [PATCH 3/4] More code review changes and tests --- lib/mpl_toolkits/mplot3d/axes3d.py | 4 +++- lib/mpl_toolkits/tests/test_mplot3d.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 74a3960318d7..d21e6713e07a 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -107,6 +107,8 @@ def __init__( focal_length : float, default: None For a projection type of 'persp', the focal length of the virtual camera. Must be > 0. If None, defaults to 1. + For a projection type of 'ortho', must be set to either None + or infinity (numpy.inf). If None, defaults to infinity. The focal length can be computed from a desired Field Of View via the equation: focal_length = 1/tan(FOV/2) @@ -1019,7 +1021,7 @@ def set_proj_type(self, proj_type, focal_length=None): self._focal_length = focal_length elif proj_type == 'ortho': if focal_length not in (None, np.inf): - raise ValueError(f"focal_length = {focal_length} must be" + raise ValueError(f"focal_length = {focal_length} must be " f"None for proj_type = {proj_type}") self._focal_length = np.inf diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index ced299567108..0c1682132380 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -1036,6 +1036,15 @@ def test_unautoscale(axis, auto): np.testing.assert_array_equal(get_lim(), (-0.5, 0.5)) +def test_axes3d_focal_length_checks(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + with pytest.raises(ValueError): + ax.set_proj_type('persp', focal_length=0) + with pytest.raises(ValueError): + ax.set_proj_type('ortho', focal_length=1) + + @mpl3d_image_comparison(['axes3d_focal_length.png'], remove_text=False) def test_axes3d_focal_length(): fig, axs = plt.subplots(1, 2, subplot_kw={'projection': '3d'}) From b0135c76257926a21833b40ba82e132af0558a97 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Tue, 18 Jan 2022 10:04:19 -0700 Subject: [PATCH 4/4] Additional code review changes --- doc/users/next_whats_new/3d_plot_focal_length.rst | 15 ++++++++------- examples/mplot3d/projections.py | 10 +++++----- lib/mpl_toolkits/mplot3d/axes3d.py | 11 +++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/doc/users/next_whats_new/3d_plot_focal_length.rst b/doc/users/next_whats_new/3d_plot_focal_length.rst index 07666f6f4adb..9422faa71546 100644 --- a/doc/users/next_whats_new/3d_plot_focal_length.rst +++ b/doc/users/next_whats_new/3d_plot_focal_length.rst @@ -2,24 +2,25 @@ Give the 3D camera a custom focal length ---------------------------------------- Users can now better mimic real-world cameras by specifying the focal length of -the virtual camera in 3D plots. An increasing focal length between 1 and -infinity "flattens" the image, while a decreasing focal length between 1 and 0 -exaggerates the perspective and gives the image more apparent depth. The -default focal length of 1 corresponds to a Field of View (FOV) of 90 deg, and -is backwards-compatible with existing 3D plots. +the virtual camera in 3D plots. The default focal length of 1 corresponds to a +Field of View (FOV) of 90 deg, and is backwards-compatible with existing 3D +plots. An increased focal length between 1 and infinity "flattens" the image, +while a decreased focal length between 1 and 0 exaggerates the perspective and +gives the image more apparent depth. The focal length can be calculated from a desired FOV via the equation: .. mathmpl:: - focal\_length = 1/tan(FOV/2) + focal\_length = 1/\tan(FOV/2) .. plot:: :include-source: true from mpl_toolkits.mplot3d import axes3d import matplotlib.pyplot as plt - fig, axs = plt.subplots(1, 3, subplot_kw={'projection': '3d'}) + fig, axs = plt.subplots(1, 3, subplot_kw={'projection': '3d'}, + constrained_layout=True) X, Y, Z = axes3d.get_test_data(0.05) focal_lengths = [0.25, 1, 4] for ax, fl in zip(axs, focal_lengths): diff --git a/examples/mplot3d/projections.py b/examples/mplot3d/projections.py index 1432d3e25f12..0368ef68de9e 100644 --- a/examples/mplot3d/projections.py +++ b/examples/mplot3d/projections.py @@ -4,13 +4,13 @@ ======================== Demonstrates the different camera projections for 3D plots, and the effects of -changing the focal length for a perspective projection. Note that matplotlib +changing the focal length for a perspective projection. Note that Matplotlib corrects for the 'zoom' effect of changing the focal length. -An increasing focal length between 1 and infinity "flattens" the image, while -a decreasing focal length between 1 and 0 exaggerates the perspective and gives -the image more apparent depth. The default focal length of 1 corresponds to a -Field of View (FOV) of 90 deg. In the limiting case, a focal length of +The default focal length of 1 corresponds to a Field of View (FOV) of 90 deg. +An increased focal length between 1 and infinity "flattens" the image, while a +decreased focal length between 1 and 0 exaggerates the perspective and gives +the image more apparent depth. In the limiting case, a focal length of infinity corresponds to an orthographic projection after correction of the zoom effect. diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index d21e6713e07a..35f81a4628d9 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1013,12 +1013,11 @@ def set_proj_type(self, proj_type, focal_length=None): _api.check_in_list(['persp', 'ortho'], proj_type=proj_type) if proj_type == 'persp': if focal_length is None: - self._focal_length = 1 - else: - if focal_length <= 0: - raise ValueError(f"focal_length = {focal_length} must be" - " greater than 0") - self._focal_length = focal_length + focal_length = 1 + elif focal_length <= 0: + raise ValueError(f"focal_length = {focal_length} must be " + "greater than 0") + self._focal_length = focal_length elif proj_type == 'ortho': if focal_length not in (None, np.inf): raise ValueError(f"focal_length = {focal_length} must be "