Link pyqtgraph 中两个 3D 子图的相机位置

Link cameras' positions of two 3D subplots in pyqtgraph

我需要在同一个 window 中绘制两个 3D 表面图,我想“link”它们的相机位置,这样如果用户移动视图第一个子图,第二个相应更新,以保持相同的视图方向。
我可以在 matplotlib 中成功实现此结果,如 this answer:

中所述
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm


N = 101
x = np.linspace(-10, 10, N)
y = np.linspace(-10, 10, N)

X, Y = np.meshgrid(x, y)

Z1 = np.sin(X) + np.cos(Y)
Z2 = np.cos(np.sqrt(X**2 + Y**2))


fig = plt.figure(figsize = plt.figaspect(0.5))

ax1 = fig.add_subplot(1, 2, 1, projection = '3d')
surf1 = ax1.plot_surface(X, Y, Z1, rstride = 1, cstride = 1, cmap = cm.jet, linewidth = 0, antialiased = False)

ax2 = fig.add_subplot(1, 2, 2, projection = '3d')
surf2 = ax2.plot_surface(X, Y, Z2, rstride = 1, cstride = 1, cmap = cm.jet, linewidth = 0, antialiased = False)


def on_move(event):
    if event.inaxes == ax1:
        if ax1.button_pressed in ax1._rotate_btn:
            ax2.view_init(elev = ax1.elev, azim = ax1.azim)
        elif ax1.button_pressed in ax1._zoom_btn:
            ax2.set_xlim3d(ax1.get_xlim3d())
            ax2.set_ylim3d(ax1.get_ylim3d())
            ax2.set_zlim3d(ax1.get_zlim3d())
    elif event.inaxes == ax2:
        if ax2.button_pressed in ax2._rotate_btn:
            ax1.view_init(elev = ax2.elev, azim = ax2.azim)
        elif ax2.button_pressed in ax2._zoom_btn:
            ax1.set_xlim3d(ax2.get_xlim3d())
            ax1.set_ylim3d(ax2.get_ylim3d())
            ax1.set_zlim3d(ax2.get_zlim3d())
    else:
        return
    fig.canvas.draw_idle()


c1 = fig.canvas.mpl_connect('motion_notify_event', on_move)

plt.show()

但是,我必须使用 pyqtgraph 来执行此任务。我可以绘制两个表面,但我不知道如何“link”两个子图的相机位置。

import numpy as np
import pyqtgraph as pg
import pyqtgraph.opengl as gl
from matplotlib.cm import get_cmap
from pyqtgraph.Qt import QtGui


N = 101
x = np.linspace(-10, 10, N)
y = np.linspace(-10, 10, N)
cmap = get_cmap('jet')


X, Y = np.meshgrid(x, y)


Z1 = np.sin(X) + np.cos(Y)

Z1_min = np.min(Z1)
Z1_max = np.max(Z1)
colors1 = cmap((Z1 - Z1_min)/(Z1_max - Z1_min))


Z2 = np.cos(np.sqrt(X**2 + Y**2))

Z2_min = np.min(Z2)
Z2_max = np.max(Z2)
colors2 = cmap((Z2 - Z2_min)/(Z2_max - Z2_min))


win = pg.GraphicsLayoutWidget(show = True, size = (1000, 500))
layoutgb = QtGui.QGridLayout()
win.setLayout(layoutgb)

glvw1 = gl.GLViewWidget()
surf1 = gl.GLSurfacePlotItem(x = x, y = y, z = Z1, colors = colors1)
glvw1.addItem(surf1)
glvw1.sizeHint = lambda: pg.QtCore.QSize(100, 500)
layoutgb.addWidget(glvw1, 0, 0)

glvw2 = gl.GLViewWidget()
surf2 = gl.GLSurfacePlotItem(x = x, y = y, z = Z2, colors = colors2)
glvw2.addItem(surf2)
glvw2.sizeHint = lambda: pg.QtCore.QSize(100, 500)
layoutgb.addWidget(glvw2, 0, 2)

QtGui.QApplication.instance().exec_()

要实现您的需求,您必须将“相机”与所有 linked 图同步。
我选择覆盖 gl.GLViewWidget (SyncedCameraViewWidget) 和 re-implement 三个事件 wheelEventmouseMoveEventmouseReleaseEvent.

要使事情正常进行,您必须 sync_camera_with 使用另一个 SyncedCameraViewWidget。
从技术上讲,您可以在那里使用纯 gl.GLViewWidget。
我喜欢灵活性,所以我不会强制在那里传递另一个 SyncedCameraViewWidget。
这就是为什么你必须 link PlotA -> PlotB 和 PlotB -> PlotA。
否则,当您更改 PlotB 的相机时,它不会更改 PlotA 相机。

这是您使用 SyncedCameraViewWidget 的代码:

from typing import List

import numpy as np
import pyqtgraph as pg
import pyqtgraph.opengl as gl
from matplotlib.cm import get_cmap
from pyqtgraph.Qt import QtGui


class SyncedCameraViewWidget(gl.GLViewWidget):

    def __init__(self, parent=None, devicePixelRatio=None, rotationMethod='euler'):
        self.linked_views: List[gl.GLViewWidget] = []
        super().__init__(parent, devicePixelRatio, rotationMethod)

    def wheelEvent(self, ev):
        """Update view on zoom event"""
        super().wheelEvent(ev)
        self._update_views()

    def mouseMoveEvent(self, ev):
        """Update view on move event"""
        super().mouseMoveEvent(ev)
        self._update_views()

    def mouseReleaseEvent(self, ev):
        """Update view on move event"""
        super().mouseReleaseEvent(ev)
        self._update_views()

    def _update_views(self):
        """Take camera parameters and sync with all views"""
        camera_params = self.cameraParams()
        # Remove rotation, we can't update all params at once (Azimuth and Elevation)
        camera_params["rotation"] = None
        for view in self.linked_views:
            view.setCameraParams(**camera_params)

    def sync_camera_with(self, view: gl.GLViewWidget):
        """Add view to sync camera with"""
        self.linked_views.append(view)


N = 101
x = np.linspace(-10, 10, N)
y = np.linspace(-10, 10, N)
cmap = get_cmap('jet')

X, Y = np.meshgrid(x, y)

Z1 = np.sin(X) + np.cos(Y)

Z1_min = np.min(Z1)
Z1_max = np.max(Z1)
colors1 = cmap((Z1 - Z1_min) / (Z1_max - Z1_min))

Z2 = np.cos(np.sqrt(X ** 2 + Y ** 2))

Z2_min = np.min(Z2)
Z2_max = np.max(Z2)
colors2 = cmap((Z2 - Z2_min) / (Z2_max - Z2_min))

win = pg.GraphicsLayoutWidget(show=True, size=(1000, 500))
layoutgb = QtGui.QGridLayout()
win.setLayout(layoutgb)

glvw1 = SyncedCameraViewWidget()
surf1 = gl.GLSurfacePlotItem(x=x, y=y, z=Z1, colors=colors1)
glvw1.addItem(surf1)
layoutgb.addWidget(glvw1, 0, 0)

glvw2 = SyncedCameraViewWidget()
surf2 = gl.GLSurfacePlotItem(x=x, y=y, z=Z2, colors=colors2)
glvw2.addItem(surf2)
layoutgb.addWidget(glvw2, 0, 2)

glvw1.sync_camera_with(glvw2)
glvw2.sync_camera_with(glvw1)

QtGui.QApplication.instance().exec_()

请注意,在 Event 方法中,您必须先在 gl.GLViewWidget 中执行 Event(在主图中更改相机),然后才执行 _update_views()(为所有 [=34= 更改相机) ]编辑图)。
否则绘图相机将不会同步。