我想在 vispy 中绘制曲面图,同时也显示 2D 图像来代替白色背景。
我使用 vispy 的 Draw a SurfacePlot 示例作为基础。我尝试的第一件事就是向视图中添加一个
scene.visuals.Image
对象,但这最终将图像渲染为 3D 场景中的对象之一:
canvas = scene.SceneCanvas(keys='interactive', bgcolor='w')
background_texture = scene.visuals.Image(np.random.rand(512, 512, 3).astype(np.float32))
view = canvas.central_widget.add_view()
view.camera = scene.TurntableCamera(up='z', fov=60)
view.add(background_texture)
...
由于图像需要在后台渲染,我认为它需要自己的 2D 视图,在其上渲染
SurfacePlot
的 3D 场景,所以我为图像创建了一个新视图:
canvas = scene.SceneCanvas(keys='interactive', bgcolor='w')
background_texture = scene.visuals.Image(np.random.rand(512, 512, 3).astype(np.float32))
viewbg = canvas.central_widget.add_view()
viewbg.add(background_texture)
view = canvas.central_widget.add_view()
view.camera = scene.TurntableCamera(up='z', fov=60)
...
但是,图像会渲染在曲面图的顶部,但轴标签除外,轴标签会渲染在图像的顶部。重新排序
view
和 viewbg
代码,以便先创建 view
不会产生任何影响;这只会让事情变得更糟,因为那样我也失去了与表面图交互的能力。
有没有办法在图像之上渲染整个 3D 场景?
感谢@djhoese的指导,我能够想出一些解决方案。首先,背景图像必须位于场景图中的
view
之外,以免它成为视图 TurntableCamera
显示的 3D 空间的一部分。接下来是实际显示 3D 场景背后的图像的问题。
最干净的方法是首先绘制图像,然后在绘制 3D 场景之前清除 OpenGL 深度缓冲区。这将确保场景绘制在图像之上。最简单的方法是子类化
scene.Image
并在绘制后添加对 gloo.clear(color=False, depth=True)
的调用:
class BackgroundImage(scene.Image):
def draw(self):
super().draw()
gloo.clear(color=False, depth=True)
确保
BackgroundImage
在其他任何内容之前渲染。完整代码如下:
import sys
import numpy as np
from vispy import app, scene, gloo
from vispy.util.filter import gaussian_filter
class BackgroundImage(scene.Image):
def draw(self):
super().draw()
gloo.clear(color=False, depth=True)
canvas = scene.SceneCanvas(keys='interactive', bgcolor='w')
view_bg = canvas.central_widget.add_view(camera=scene.PanZoomCamera())
view_bg.order = float("-inf") # For good measure
background_image = BackgroundImage(np.random.rand(512, 512, 3).astype(np.float32), parent=view_bg.scene)
view_bg.camera.set_range(margin=0)
view = canvas.central_widget.add_view()
view.camera = scene.TurntableCamera(up='z', fov=60)
# Simple surface plot example
# x, y values are not specified, so assumed to be 0:50
z = np.random.normal(size=(250, 250), scale=200)
z[100, 100] += 50000
z = gaussian_filter(z, (10, 10))
p1 = scene.visuals.SurfacePlot(z=z, color=(0.3, 0.3, 1, 1))
p1.transform = scene.transforms.MatrixTransform()
p1.transform.scale([1/249., 1/249., 1/249.])
p1.transform.translate([-0.5, -0.5, 0])
view.add(p1)
# p1._update_data() # cheating.
# cf = scene.filters.ZColormapFilter('fire', zrange=(z.max(), z.min()))
# p1.attach(cf)
xax = scene.Axis(pos=[[-0.5, -0.5], [0.5, -0.5]], tick_direction=(0, -1),
font_size=16, axis_color='k', tick_color='k', text_color='k',
parent=view.scene)
xax.transform = scene.STTransform(translate=(0, 0, -0.2))
yax = scene.Axis(pos=[[-0.5, -0.5], [-0.5, 0.5]], tick_direction=(-1, 0),
font_size=16, axis_color='k', tick_color='k', text_color='k',
parent=view.scene)
yax.transform = scene.STTransform(translate=(0, 0, -0.2))
# Add a 3D axis to keep us oriented
axis = scene.visuals.XYZAxis(parent=view.scene)
if __name__ == '__main__':
canvas.show()
if sys.flags.interactive == 0:
app.run()
A
PanZoomCamera
与 background_image
一起使用可确保缩放图像以适合整个画布。您将无法与该相机交互,因为带有 TurntableCamera
的视图绘制在顶部,因此它需要聚焦。
如果您想绘制任何场景,而不仅仅是另一个场景背景中的
Image
,您可以实现一个Node
子类,在绘制其子级后清除深度缓冲区。最好从 Widget
继承,因为它提供了最多的功能:
class Background(scene.Widget):
"""Node that clears the depth buffer after drawing its children."""
ClearDepth = type("ClearDepthBuffer", (scene.Node,), {'draw': lambda self: gloo.clear(color=False, depth=True)})
def __init__(self, *args, **kwargs):
self._last = None
super().__init__(*args, **kwargs)
self._last = self.ClearDepth(parent=self)
self._last.order = float("inf")
assert self._children.pop() == self._last
@property
def children(self):
return super().children + [self._last] if self._last is not None else super().children
确保
Background
实际上是先于其他内容绘制的。使用以下方法的完整表面绘图代码:
import sys
import numpy as np
from vispy import app, scene, gloo
from vispy.util.filter import gaussian_filter
class Background(scene.Widget):
"""Node that clears the depth buffer after drawing its children."""
ClearDepth = type("ClearDepthBuffer", (scene.Node,), {'draw': lambda self: gloo.clear(color=False, depth=True)})
def __init__(self, *args, **kwargs):
self._last = None
super().__init__(*args, **kwargs)
self._last = self.ClearDepth(parent=self)
self._last.order = float("inf")
assert self._children.pop() == self._last
@property
def children(self):
return super().children + [self._last] if self._last is not None else super().children
canvas = scene.SceneCanvas(keys='interactive', bgcolor='w')
background = canvas.central_widget.add_widget(Background())
background.order = float("-inf") # For good measure
view_bg = background.add_view(camera=scene.PanZoomCamera())
background_image = scene.Image(np.random.rand(512, 512, 3).astype(np.float32), parent=view_bg.scene)
view_bg.camera.set_range(margin=0)
view = canvas.central_widget.add_view()
view.camera = scene.TurntableCamera(up='z', fov=60)
print(canvas.scene.describe_tree())
# Simple surface plot example
# x, y values are not specified, so assumed to be 0:50
z = np.random.normal(size=(250, 250), scale=200)
z[100, 100] += 50000
z = gaussian_filter(z, (10, 10))
p1 = scene.visuals.SurfacePlot(z=z, color=(0.3, 0.3, 1, 1))
p1.transform = scene.transforms.MatrixTransform()
p1.transform.scale([1/249., 1/249., 1/249.])
p1.transform.translate([-0.5, -0.5, 0])
view.add(p1)
# p1._update_data() # cheating.
# cf = scene.filters.ZColormapFilter('fire', zrange=(z.max(), z.min()))
# p1.attach(cf)
xax = scene.Axis(pos=[[-0.5, -0.5], [0.5, -0.5]], tick_direction=(0, -1),
font_size=16, axis_color='k', tick_color='k', text_color='k',
parent=view.scene)
xax.transform = scene.STTransform(translate=(0, 0, -0.2))
yax = scene.Axis(pos=[[-0.5, -0.5], [-0.5, 0.5]], tick_direction=(-1, 0),
font_size=16, axis_color='k', tick_color='k', text_color='k',
parent=view.scene)
yax.transform = scene.STTransform(translate=(0, 0, -0.2))
# Add a 3D axis to keep us oriented
axis = scene.visuals.XYZAxis(parent=view.scene)
if __name__ == '__main__':
canvas.show()
if sys.flags.interactive == 0:
app.run()
在使用
add_widget
或 add_view
的子类时,请确保使用 Widget
和 Widget
方法,以便正确处理画布事件。这种方法的巧妙之处在于,您可以连续渲染多个 Background
实例,每个实例都渲染在前一个实例之上,为您提供多层背景,其中每个背景可以是任意 2D 或 3D场景。
清除 OpenGL 深度缓冲区的另一种方法是平移
Image
,使其 z
坐标尽可能接近 1.0
,但实际上不等于 1.0
。不过,这种方法不适用于 PanZoomCamera
,因此您还必须处理图像的缩放,以便在调整画布大小后它适合整个画布。在初始画布设置期间也会调用 resize
事件。完整代码如下:
import sys
import numpy as np
from vispy import app, scene
from vispy.util.filter import gaussian_filter
from vispy.visuals.transforms import STTransform
canvas = scene.SceneCanvas(keys='interactive', bgcolor='w')
background_image = scene.Image(np.random.rand(512, 512, 3).astype(np.float32), parent=canvas.central_widget)
canvas.events.resize.connect(lambda _: background_image.__setattr__('transform', STTransform(
translate=(0, 0, 0.999999970197677556793536268742172978818416595458984374),
scale=tuple(c / b for c, b in zip(canvas.size, background_image.size))
)))
view = canvas.central_widget.add_view()
view.camera = scene.TurntableCamera(up='z', fov=60)
# Simple surface plot example
# x, y values are not specified, so assumed to be 0:50
z = np.random.normal(size=(250, 250), scale=200)
z[100, 100] += 50000
z = gaussian_filter(z, (10, 10))
p1 = scene.visuals.SurfacePlot(z=z, color=(0.3, 0.3, 1, 1))
p1.transform = scene.transforms.MatrixTransform()
p1.transform.scale([1/249., 1/249., 1/249.])
p1.transform.translate([-0.5, -0.5, 0])
view.add(p1)
# p1._update_data() # cheating.
# cf = scene.filters.ZColormapFilter('fire', zrange=(z.max(), z.min()))
# p1.attach(cf)
xax = scene.Axis(pos=[[-0.5, -0.5], [0.5, -0.5]], tick_direction=(0, -1),
font_size=16, axis_color='k', tick_color='k', text_color='k',
parent=view.scene)
xax.transform = scene.STTransform(translate=(0, 0, -0.2))
yax = scene.Axis(pos=[[-0.5, -0.5], [-0.5, 0.5]], tick_direction=(-1, 0),
font_size=16, axis_color='k', tick_color='k', text_color='k',
parent=view.scene)
yax.transform = scene.STTransform(translate=(0, 0, -0.2))
# Add a 3D axis to keep us oriented
axis = scene.visuals.XYZAxis(parent=view.scene)
if __name__ == '__main__':
canvas.show()
if sys.flags.interactive == 0:
app.run()
通过这种方法,您不必担心在 3D 场景之前绘制
Image
。任何绘图顺序都有效。如果图像未显示,则其 z
坐标可能会向上舍入为 1.0
,因此请尝试删除 translate
的 STTransform
参数中的几位小数。 0.9999
及以上的值应该足够好。我刚刚找到了可以放入 z
坐标中的最大可能值,该值将为我显示图像。