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 三个事件 wheelEvent
、mouseMoveEvent
和 mouseReleaseEvent
.
要使事情正常进行,您必须 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= 更改相机) ]编辑图)。
否则绘图相机将不会同步。
我需要在同一个 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 三个事件 wheelEvent
、mouseMoveEvent
和 mouseReleaseEvent
.
要使事情正常进行,您必须 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= 更改相机) ]编辑图)。
否则绘图相机将不会同步。