vispy 0.9.5__cp38-cp38-win_amd64.whl → 0.14.0__cp38-cp38-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of vispy might be problematic. Click here for more details.
- vispy/app/backends/_glfw.py +2 -2
- vispy/app/backends/_pyglet.py +8 -2
- vispy/app/backends/_qt.py +88 -63
- vispy/app/backends/_wx.py +6 -1
- vispy/app/canvas.py +4 -2
- vispy/app/tests/test_canvas.py +52 -1
- vispy/app/tests/test_context.py +5 -3
- vispy/color/color_array.py +8 -1
- vispy/color/colormap.py +5 -25
- vispy/geometry/meshdata.py +76 -38
- vispy/geometry/rect.py +6 -0
- vispy/geometry/tests/test_meshdata.py +72 -0
- vispy/gloo/buffer.py +12 -0
- vispy/gloo/gl/_constants.py +9 -5
- vispy/gloo/gl/_es2.py +8 -4
- vispy/gloo/gl/_gl2.py +2 -3
- vispy/gloo/gl/_proxy.py +1 -1
- vispy/gloo/gl/_pyopengl2.py +12 -7
- vispy/gloo/gl/tests/test_names.py +3 -0
- vispy/gloo/glir.py +26 -13
- vispy/gloo/program.py +39 -22
- vispy/gloo/tests/test_program.py +9 -2
- vispy/gloo/tests/test_texture.py +19 -2
- vispy/gloo/texture.py +46 -16
- vispy/gloo/wrappers.py +4 -2
- vispy/glsl/build_spatial_filters.py +241 -293
- vispy/glsl/misc/spatial-filters.frag +1299 -254
- vispy/io/_data/spatial-filters.npy +0 -0
- vispy/io/datasets.py +2 -2
- vispy/io/image.py +1 -1
- vispy/io/stl.py +3 -3
- vispy/scene/cameras/base_camera.py +6 -2
- vispy/scene/cameras/panzoom.py +10 -14
- vispy/scene/cameras/perspective.py +6 -0
- vispy/scene/cameras/tests/test_cameras.py +27 -0
- vispy/scene/cameras/tests/test_perspective.py +37 -0
- vispy/scene/cameras/turntable.py +39 -23
- vispy/scene/canvas.py +9 -5
- vispy/scene/events.py +9 -0
- vispy/scene/node.py +19 -2
- vispy/scene/tests/test_canvas.py +30 -1
- vispy/scene/tests/test_visuals.py +113 -0
- vispy/scene/visuals.py +6 -1
- vispy/scene/widgets/viewbox.py +3 -2
- vispy/testing/_runners.py +6 -12
- vispy/testing/_testing.py +3 -4
- vispy/util/check_environment.py +4 -4
- vispy/util/gallery_scraper.py +50 -32
- vispy/util/tests/test_gallery_scraper.py +2 -0
- vispy/util/transforms.py +1 -1
- vispy/util/wrappers.py +1 -1
- vispy/version.py +2 -3
- vispy/visuals/__init__.py +2 -0
- vispy/visuals/_scalable_textures.py +20 -17
- vispy/visuals/collections/array_list.py +3 -3
- vispy/visuals/collections/base_collection.py +1 -1
- vispy/visuals/ellipse.py +1 -1
- vispy/visuals/filters/__init__.py +3 -2
- vispy/visuals/filters/base_filter.py +120 -0
- vispy/visuals/filters/clipping_planes.py +24 -12
- vispy/visuals/filters/markers.py +28 -0
- vispy/visuals/filters/mesh.py +61 -6
- vispy/visuals/filters/tests/test_primitive_picking_filters.py +70 -0
- vispy/visuals/graphs/graph.py +1 -1
- vispy/visuals/image.py +114 -26
- vispy/visuals/image_complex.py +130 -0
- vispy/visuals/instanced_mesh.py +152 -0
- vispy/visuals/isocurve.py +1 -1
- vispy/visuals/line/dash_atlas.py +46 -41
- vispy/visuals/line/line.py +2 -5
- vispy/visuals/markers.py +310 -384
- vispy/visuals/mesh.py +2 -2
- vispy/visuals/shaders/function.py +3 -0
- vispy/visuals/shaders/tests/test_function.py +6 -0
- vispy/visuals/tests/test_axis.py +2 -2
- vispy/visuals/tests/test_image.py +92 -2
- vispy/visuals/tests/test_image_complex.py +36 -0
- vispy/visuals/tests/test_instanced_mesh.py +50 -0
- vispy/visuals/tests/test_markers.py +6 -0
- vispy/visuals/tests/test_mesh.py +17 -0
- vispy/visuals/tests/test_text.py +11 -0
- vispy/visuals/tests/test_volume.py +218 -12
- vispy/visuals/text/_sdf_cpu.cp38-win_amd64.pyd +0 -0
- vispy/visuals/text/_sdf_cpu.pyx +21 -23
- vispy/visuals/text/text.py +9 -3
- vispy/visuals/tube.py +2 -2
- vispy/visuals/visual.py +144 -3
- vispy/visuals/volume.py +300 -131
- {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/LICENSE.txt +1 -1
- {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/METADATA +218 -198
- {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/RECORD +93 -96
- {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/WHEEL +1 -1
- vispy/glsl/antialias/__init__.py +0 -0
- vispy/glsl/arrowheads/__init__.py +0 -0
- vispy/glsl/arrows/__init__.py +0 -0
- vispy/glsl/collections/__init__.py +0 -0
- vispy/glsl/colormaps/__init__.py +0 -0
- vispy/glsl/lines/__init__.py +0 -0
- vispy/glsl/markers/__init__.py +0 -0
- vispy/glsl/math/__init__.py +0 -0
- vispy/glsl/misc/__init__.py +0 -0
- vispy/glsl/transforms/__init__.py +0 -0
- {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/top_level.txt +0 -0
vispy/visuals/filters/mesh.py
CHANGED
|
@@ -7,7 +7,7 @@ import numpy as np
|
|
|
7
7
|
|
|
8
8
|
from vispy.gloo import Texture2D, VertexBuffer
|
|
9
9
|
from vispy.visuals.shaders import Function, Varying
|
|
10
|
-
from vispy.visuals.filters import Filter
|
|
10
|
+
from vispy.visuals.filters import Filter, PrimitivePickingFilter
|
|
11
11
|
from ...color import Color
|
|
12
12
|
|
|
13
13
|
|
|
@@ -177,6 +177,7 @@ void shade() {
|
|
|
177
177
|
vec3 u = dFdx(v_pos_scene.xyz);
|
|
178
178
|
vec3 v = dFdy(v_pos_scene.xyz);
|
|
179
179
|
normal = cross(u, v);
|
|
180
|
+
} else {
|
|
180
181
|
// Note(asnt): The normal calculated above always points in the
|
|
181
182
|
// direction of the camera. Reintroduce the original orientation of the
|
|
182
183
|
// face.
|
|
@@ -387,6 +388,10 @@ class ShadingFilter(Filter):
|
|
|
387
388
|
<https://github.com/vispy/vispy/blob/main/examples/basics/scene/mesh_shading.py>`_
|
|
388
389
|
example script.
|
|
389
390
|
"""
|
|
391
|
+
_shaders = {
|
|
392
|
+
'vertex': shading_vertex_template,
|
|
393
|
+
'fragment': shading_fragment_template,
|
|
394
|
+
}
|
|
390
395
|
|
|
391
396
|
def __init__(self, shading='flat',
|
|
392
397
|
ambient_coefficient=(1, 1, 1, 1),
|
|
@@ -412,8 +417,8 @@ class ShadingFilter(Filter):
|
|
|
412
417
|
|
|
413
418
|
self._enabled = enabled
|
|
414
419
|
|
|
415
|
-
vfunc = Function(
|
|
416
|
-
ffunc = Function(
|
|
420
|
+
vfunc = Function(self._shaders['vertex'])
|
|
421
|
+
ffunc = Function(self._shaders['fragment'])
|
|
417
422
|
|
|
418
423
|
self._normals = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
|
|
419
424
|
vfunc['normal'] = self._normals
|
|
@@ -572,6 +577,29 @@ class ShadingFilter(Filter):
|
|
|
572
577
|
super()._detach(visual)
|
|
573
578
|
|
|
574
579
|
|
|
580
|
+
instanced_shading_vertex_template = shading_vertex_template.replace(
|
|
581
|
+
"$normal",
|
|
582
|
+
"mat3($instance_transform_x, $instance_transform_y, $instance_transform_z) * $normal"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class InstancedShadingFilter(ShadingFilter):
|
|
587
|
+
"""Shading filter modified for use with :class:`~vispy.visuals.InstancedMeshVisual`.
|
|
588
|
+
|
|
589
|
+
See :class:`ShadingFilter` for details and usage.
|
|
590
|
+
"""
|
|
591
|
+
_shaders = {
|
|
592
|
+
'vertex': instanced_shading_vertex_template,
|
|
593
|
+
'fragment': ShadingFilter._shaders['fragment'],
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
def _attach(self, visual):
|
|
597
|
+
super()._attach(visual)
|
|
598
|
+
self.vshader['instance_transform_x'] = visual._instance_transforms_vbos[0]
|
|
599
|
+
self.vshader['instance_transform_y'] = visual._instance_transforms_vbos[1]
|
|
600
|
+
self.vshader['instance_transform_z'] = visual._instance_transforms_vbos[2]
|
|
601
|
+
|
|
602
|
+
|
|
575
603
|
wireframe_vertex_template = """
|
|
576
604
|
varying vec3 v_bc;
|
|
577
605
|
|
|
@@ -729,13 +757,40 @@ class WireframeFilter(Filter):
|
|
|
729
757
|
bc = np.tile(bc[None, ...], (n_faces, 1, 1))
|
|
730
758
|
self._bc.set_data(bc, convert=True)
|
|
731
759
|
|
|
732
|
-
def
|
|
760
|
+
def on_data_updated(self, event):
|
|
733
761
|
self._update_data()
|
|
734
762
|
|
|
735
763
|
def _attach(self, visual):
|
|
736
764
|
super()._attach(visual)
|
|
737
|
-
visual.events.data_updated.connect(self.
|
|
765
|
+
visual.events.data_updated.connect(self.on_data_updated)
|
|
738
766
|
|
|
739
767
|
def _detach(self, visual):
|
|
740
|
-
visual.events.data_updated.disconnect(self.
|
|
768
|
+
visual.events.data_updated.disconnect(self.on_data_updated)
|
|
741
769
|
super()._detach(visual)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
class FacePickingFilter(PrimitivePickingFilter):
|
|
773
|
+
"""Filter used to color mesh faces by a picking ID.
|
|
774
|
+
|
|
775
|
+
Note that the ID color uses the alpha channel, so this may not be used
|
|
776
|
+
with blending enabled.
|
|
777
|
+
|
|
778
|
+
Examples
|
|
779
|
+
--------
|
|
780
|
+
:ref:`sphx_glr_gallery_scene_face_picking.py`
|
|
781
|
+
"""
|
|
782
|
+
|
|
783
|
+
def _get_picking_ids(self):
|
|
784
|
+
if self._visual.mesh_data.is_empty():
|
|
785
|
+
n_faces = 0
|
|
786
|
+
else:
|
|
787
|
+
n_faces = len(self._visual.mesh_data.get_faces())
|
|
788
|
+
|
|
789
|
+
# we only care about the number of faces changing
|
|
790
|
+
if self._n_primitives == n_faces:
|
|
791
|
+
return None
|
|
792
|
+
self._n_primitives = n_faces
|
|
793
|
+
|
|
794
|
+
ids = np.arange(1, n_faces + 1, dtype=np.uint32)
|
|
795
|
+
ids = np.repeat(ids, 3, axis=0) # repeat id for each vertex
|
|
796
|
+
return ids
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) Vispy Development Team. All Rights Reserved.
|
|
3
|
+
# Distributed under the (new) BSD License. See LICENSE.txt for more info.
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from vispy.geometry import create_plane
|
|
7
|
+
from vispy.scene.visuals import Markers, Mesh
|
|
8
|
+
from vispy.testing import requires_application, TestingCanvas
|
|
9
|
+
from vispy.visuals.filters import FacePickingFilter, MarkerPickingFilter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_empty_mesh_face_picking():
|
|
13
|
+
mesh = Mesh()
|
|
14
|
+
filter = FacePickingFilter()
|
|
15
|
+
mesh.attach(filter)
|
|
16
|
+
filter.enabled = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@requires_application()
|
|
20
|
+
def test_mesh_face_picking():
|
|
21
|
+
vertices, faces, _ = create_plane(125, 125)
|
|
22
|
+
vertices = vertices["position"]
|
|
23
|
+
vertices[:, :2] += 125 / 2
|
|
24
|
+
mesh = Mesh(vertices=vertices, faces=faces)
|
|
25
|
+
filter = FacePickingFilter()
|
|
26
|
+
mesh.attach(filter)
|
|
27
|
+
|
|
28
|
+
with TestingCanvas(size=(125, 125)) as c:
|
|
29
|
+
view = c.central_widget.add_view()
|
|
30
|
+
view.add(mesh)
|
|
31
|
+
filter.enabled = True
|
|
32
|
+
mesh.update_gl_state(blend=False)
|
|
33
|
+
picking_render = c.render(bgcolor=(0, 0, 0, 0), alpha=True)
|
|
34
|
+
|
|
35
|
+
# unpack the IDs
|
|
36
|
+
ids = picking_render.view(np.uint32)
|
|
37
|
+
# the plane is made up of two triangles and nearly fills the view
|
|
38
|
+
# pick one point on each triangle
|
|
39
|
+
assert ids[125 // 2, 125 // 4] == 1
|
|
40
|
+
assert ids[125 // 2, 3 * 125 // 4] == 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_empty_markers_picking():
|
|
44
|
+
markers = Markers()
|
|
45
|
+
filter = MarkerPickingFilter()
|
|
46
|
+
markers.attach(filter)
|
|
47
|
+
filter.enabled = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@requires_application()
|
|
51
|
+
def test_markers_picking():
|
|
52
|
+
markers = Markers(
|
|
53
|
+
pos=np.array([[-0.5, -0.5], [0.5, 0.5]]),
|
|
54
|
+
size=5,
|
|
55
|
+
)
|
|
56
|
+
filter = MarkerPickingFilter()
|
|
57
|
+
markers.attach(filter)
|
|
58
|
+
|
|
59
|
+
with TestingCanvas(size=(125, 125)) as c:
|
|
60
|
+
view = c.central_widget.add_view(camera="panzoom")
|
|
61
|
+
view.camera.rect = (-1, -1, 2, 2)
|
|
62
|
+
view.add(markers)
|
|
63
|
+
|
|
64
|
+
filter.enabled = True
|
|
65
|
+
markers.update_gl_state(blend=False)
|
|
66
|
+
picking_render = c.render(bgcolor=(0, 0, 0, 0), alpha=True)
|
|
67
|
+
ids = picking_render.view(np.uint32)
|
|
68
|
+
|
|
69
|
+
assert ids[3 * 125 // 4, 125 // 4] == 1
|
|
70
|
+
assert ids[125 // 4, 3 * 125 // 4] == 2
|
vispy/visuals/graphs/graph.py
CHANGED
|
@@ -63,7 +63,7 @@ class GraphVisual(CompoundVisual):
|
|
|
63
63
|
_arrow_kw_trans = dict(line_color='color', line_width='width')
|
|
64
64
|
_node_kw_trans = dict(node_symbol='symbol', node_size='size',
|
|
65
65
|
border_color='edge_color', border_width='edge_width')
|
|
66
|
-
_node_properties_args = (
|
|
66
|
+
_node_properties_args = ()
|
|
67
67
|
|
|
68
68
|
def __init__(self, adjacency_mat=None, directed=False, layout=None,
|
|
69
69
|
animate=False, line_color=None, line_width=None,
|
vispy/visuals/image.py
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import division
|
|
7
7
|
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
8
10
|
import numpy as np
|
|
9
11
|
|
|
10
12
|
from ..gloo import Texture2D, VertexBuffer
|
|
11
|
-
from ..gloo.texture import should_cast_to_f32
|
|
12
13
|
from ..color import get_colormap
|
|
13
14
|
from .shaders import Function, FunctionChain
|
|
14
15
|
from .transforms import NullTransform
|
|
@@ -129,6 +130,36 @@ _NULL_COLOR_TRANSFORM = 'vec4 pass(vec4 color) { return color; }'
|
|
|
129
130
|
|
|
130
131
|
_C2L_RED = 'float cmap(vec4 color) { return color.r; }'
|
|
131
132
|
|
|
133
|
+
_CUSTOM_FILTER = """
|
|
134
|
+
vec4 texture_lookup(vec2 texcoord) {
|
|
135
|
+
// based on https://gist.github.com/kingbedjed/373c8811efcf1b3a155d29a13c1e5b61
|
|
136
|
+
vec2 tex_pixel = 1 / $shape;
|
|
137
|
+
vec2 kernel_pixel = 1 / $kernel_shape;
|
|
138
|
+
vec2 sampling_corner = texcoord - ($kernel_shape / 2 * tex_pixel);
|
|
139
|
+
|
|
140
|
+
// loop over kernel pixels
|
|
141
|
+
vec2 kernel_pos, tex_pos;
|
|
142
|
+
vec4 color = vec4(0);
|
|
143
|
+
float weight;
|
|
144
|
+
|
|
145
|
+
// offset 0.5 to sample center of pixels
|
|
146
|
+
for (float i = 0.5; i < $kernel_shape.x; i++) {
|
|
147
|
+
for (float j = 0.5; j < $kernel_shape.y; j++) {
|
|
148
|
+
kernel_pos = vec2(i, j) * kernel_pixel;
|
|
149
|
+
tex_pos = sampling_corner + vec2(i, j) * tex_pixel;
|
|
150
|
+
// TODO: allow other edge effects, like mirror or wrap
|
|
151
|
+
if (tex_pos.x >= 0 && tex_pos.y >= 0 && tex_pos.x <= 1 && tex_pos.y <= 1) {
|
|
152
|
+
weight = texture2D($kernel, kernel_pos).r;
|
|
153
|
+
// make sure to clamp or we sample outside
|
|
154
|
+
color += texture2D($texture, clamp(tex_pos, 0, 1)) * weight;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return color;
|
|
160
|
+
}
|
|
161
|
+
"""
|
|
162
|
+
|
|
132
163
|
|
|
133
164
|
class ImageVisual(Visual):
|
|
134
165
|
"""Visual subclass displaying an image.
|
|
@@ -174,15 +205,16 @@ class ImageVisual(Visual):
|
|
|
174
205
|
Gamma to use during colormap lookup. Final color will be cmap(val**gamma).
|
|
175
206
|
by default: 1.
|
|
176
207
|
interpolation : str
|
|
177
|
-
Selects method of
|
|
208
|
+
Selects method of texture interpolation. Makes use of the two hardware
|
|
178
209
|
interpolation methods and the available interpolation methods defined
|
|
179
210
|
in vispy/gloo/glsl/misc/spatial_filters.frag
|
|
180
211
|
|
|
181
|
-
* 'nearest': Default, uses 'nearest' with
|
|
182
|
-
* '
|
|
183
|
-
* 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', '
|
|
212
|
+
* 'nearest': Default, uses 'nearest' with Texture interpolation.
|
|
213
|
+
* 'linear': uses 'linear' with Texture interpolation.
|
|
214
|
+
* 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'cubic',
|
|
184
215
|
'catrom', 'mitchell', 'spline16', 'spline36', 'gaussian',
|
|
185
216
|
'bessel', 'sinc', 'lanczos', 'blackman'
|
|
217
|
+
* 'custom': uses the sampling kernel provided through 'custom_kernel'.
|
|
186
218
|
texture_format: numpy.dtype | str | None
|
|
187
219
|
How to store data on the GPU. OpenGL allows for many different storage
|
|
188
220
|
formats and schemes for the low-level texture data stored in the GPU.
|
|
@@ -204,6 +236,8 @@ class ImageVisual(Visual):
|
|
|
204
236
|
GPU which allows for faster color limit changes. Additionally, when
|
|
205
237
|
32-bit float data is provided it won't be copied before being
|
|
206
238
|
transferred to the GPU.
|
|
239
|
+
custom_kernel: numpy.ndarray
|
|
240
|
+
Kernel used for texture sampling when interpolation is set to 'custom'.
|
|
207
241
|
**kwargs : dict
|
|
208
242
|
Keyword arguments to pass to `Visual`.
|
|
209
243
|
|
|
@@ -220,6 +254,7 @@ class ImageVisual(Visual):
|
|
|
220
254
|
|
|
221
255
|
_func_templates = {
|
|
222
256
|
'texture_lookup_interpolated': _INTERPOLATION_TEMPLATE,
|
|
257
|
+
'texture_lookup_custom': _CUSTOM_FILTER,
|
|
223
258
|
'texture_lookup': _TEXTURE_LOOKUP,
|
|
224
259
|
'clim_float': _APPLY_CLIM_FLOAT,
|
|
225
260
|
'clim': _APPLY_CLIM,
|
|
@@ -231,7 +266,8 @@ class ImageVisual(Visual):
|
|
|
231
266
|
|
|
232
267
|
def __init__(self, data=None, method='auto', grid=(1, 1),
|
|
233
268
|
cmap='viridis', clim='auto', gamma=1.0,
|
|
234
|
-
interpolation='nearest', texture_format=None,
|
|
269
|
+
interpolation='nearest', texture_format=None,
|
|
270
|
+
custom_kernel=np.ones((1, 1)), **kwargs):
|
|
235
271
|
"""Initialize image properties, texture storage, and interpolation methods."""
|
|
236
272
|
self._data = None
|
|
237
273
|
|
|
@@ -287,43 +323,54 @@ class ImageVisual(Visual):
|
|
|
287
323
|
self.clim = clim or "auto" # None -> "auto"
|
|
288
324
|
self.cmap = cmap
|
|
289
325
|
self.gamma = gamma
|
|
326
|
+
self.custom_kernel = custom_kernel
|
|
327
|
+
|
|
290
328
|
if data is not None:
|
|
291
329
|
self.set_data(data)
|
|
292
330
|
self.freeze()
|
|
293
331
|
|
|
294
332
|
def _init_interpolation(self, interpolation_names):
|
|
295
|
-
# create interpolation shader functions for available
|
|
296
|
-
|
|
297
|
-
fun = [Function(self._func_templates['texture_lookup_interpolated'] % n)
|
|
333
|
+
# create interpolation shader functions for available interpolations
|
|
334
|
+
fun = [Function(self._func_templates['texture_lookup_interpolated'] % (n + '2D'))
|
|
298
335
|
for n in interpolation_names]
|
|
299
336
|
interpolation_names = [n.lower() for n in interpolation_names]
|
|
300
337
|
|
|
338
|
+
# add custom filter
|
|
339
|
+
fun.append(Function(self._func_templates['texture_lookup_custom']))
|
|
340
|
+
interpolation_names.append('custom')
|
|
341
|
+
|
|
301
342
|
interpolation_fun = dict(zip(interpolation_names, fun))
|
|
302
343
|
interpolation_names = tuple(sorted(interpolation_names))
|
|
303
344
|
|
|
304
|
-
# overwrite "nearest" and "
|
|
345
|
+
# overwrite "nearest" and "linear" spatial-filters
|
|
305
346
|
# with "hardware" interpolation _data_lookup_fn
|
|
306
347
|
hardware_lookup = Function(self._func_templates['texture_lookup'])
|
|
307
348
|
interpolation_fun['nearest'] = hardware_lookup
|
|
308
|
-
interpolation_fun['
|
|
349
|
+
interpolation_fun['linear'] = hardware_lookup
|
|
350
|
+
# alias bilinear to linear and bicubic to cubic (but deprecate)
|
|
351
|
+
interpolation_names = interpolation_names + ('bilinear', 'bicubic')
|
|
309
352
|
return interpolation_names, interpolation_fun
|
|
310
353
|
|
|
311
|
-
def _init_texture(self, data, texture_format):
|
|
312
|
-
if self._interpolation == '
|
|
354
|
+
def _init_texture(self, data, texture_format, **texture_kwargs):
|
|
355
|
+
if self._interpolation == 'linear':
|
|
313
356
|
texture_interpolation = 'linear'
|
|
314
357
|
else:
|
|
315
358
|
texture_interpolation = 'nearest'
|
|
316
359
|
|
|
317
360
|
if texture_format is None:
|
|
318
361
|
tex = CPUScaledTexture2D(
|
|
319
|
-
data, interpolation=texture_interpolation
|
|
362
|
+
data, interpolation=texture_interpolation,
|
|
363
|
+
**texture_kwargs
|
|
364
|
+
)
|
|
320
365
|
else:
|
|
321
366
|
tex = GPUScaledTexture2D(
|
|
322
367
|
data, internalformat=texture_format,
|
|
323
|
-
interpolation=texture_interpolation
|
|
368
|
+
interpolation=texture_interpolation,
|
|
369
|
+
**texture_kwargs
|
|
370
|
+
)
|
|
324
371
|
return tex
|
|
325
372
|
|
|
326
|
-
def set_data(self, image):
|
|
373
|
+
def set_data(self, image, copy=False):
|
|
327
374
|
"""Set the image data.
|
|
328
375
|
|
|
329
376
|
Parameters
|
|
@@ -333,9 +380,11 @@ class ImageVisual(Visual):
|
|
|
333
380
|
texture_format : str or None
|
|
334
381
|
|
|
335
382
|
"""
|
|
336
|
-
data = np.
|
|
337
|
-
if
|
|
338
|
-
|
|
383
|
+
data = np.array(image, copy=copy)
|
|
384
|
+
if np.iscomplexobj(data):
|
|
385
|
+
raise TypeError(
|
|
386
|
+
"Complex data types not supported. Please use 'ComplexImage' instead"
|
|
387
|
+
)
|
|
339
388
|
# can the texture handle this data?
|
|
340
389
|
self._texture.check_data_format(data)
|
|
341
390
|
if self._data is None or self._data.shape[:2] != data.shape[:2]:
|
|
@@ -443,24 +492,62 @@ class ImageVisual(Visual):
|
|
|
443
492
|
"""Get names of possible interpolation methods."""
|
|
444
493
|
return self._interpolation_names
|
|
445
494
|
|
|
495
|
+
@property
|
|
496
|
+
def custom_kernel(self):
|
|
497
|
+
"""Kernel used by 'custom' interpolation for texture sampling"""
|
|
498
|
+
return self._custom_kernel
|
|
499
|
+
|
|
500
|
+
@custom_kernel.setter
|
|
501
|
+
def custom_kernel(self, value):
|
|
502
|
+
value = np.asarray(value, dtype=np.float32)
|
|
503
|
+
if value.ndim != 2:
|
|
504
|
+
raise ValueError(f'kernel must have 2 dimensions; got {value.ndim}')
|
|
505
|
+
self._custom_kernel = value
|
|
506
|
+
self._custom_kerneltex = Texture2D(value, interpolation='nearest', internalformat='r32f')
|
|
507
|
+
if self._data_lookup_fn is not None and 'kernel' in self._data_lookup_fn:
|
|
508
|
+
self._data_lookup_fn['kernel'] = self._custom_kerneltex
|
|
509
|
+
self._data_lookup_fn['kernel_shape'] = value.shape[::-1]
|
|
510
|
+
self.update()
|
|
511
|
+
|
|
446
512
|
# The interpolation code could be transferred to a dedicated filter
|
|
447
513
|
# function in visuals/filters as discussed in #1051
|
|
448
514
|
def _build_interpolation(self):
|
|
449
515
|
"""Rebuild the _data_lookup_fn for different interpolations."""
|
|
450
516
|
interpolation = self._interpolation
|
|
517
|
+
# alias bilinear to linear
|
|
518
|
+
if interpolation == 'bilinear':
|
|
519
|
+
warnings.warn(
|
|
520
|
+
"'bilinear' interpolation is Deprecated. Use 'linear' instead.",
|
|
521
|
+
DeprecationWarning,
|
|
522
|
+
stacklevel=2,
|
|
523
|
+
)
|
|
524
|
+
interpolation = 'linear'
|
|
525
|
+
# alias bicubic to cubic
|
|
526
|
+
if interpolation == 'bicubic':
|
|
527
|
+
warnings.warn(
|
|
528
|
+
"'bicubic' interpolation is Deprecated. Use 'cubic' instead.",
|
|
529
|
+
DeprecationWarning,
|
|
530
|
+
stacklevel=2,
|
|
531
|
+
)
|
|
532
|
+
interpolation = 'cubic'
|
|
451
533
|
self._data_lookup_fn = self._interpolation_fun[interpolation]
|
|
452
534
|
self.shared_program.frag['get_data'] = self._data_lookup_fn
|
|
453
535
|
|
|
454
|
-
# only '
|
|
455
|
-
if interpolation
|
|
536
|
+
# only 'linear' and 'custom' use 'linear' texture interpolation
|
|
537
|
+
if interpolation in ('linear', 'custom'):
|
|
456
538
|
texture_interpolation = 'linear'
|
|
457
539
|
else:
|
|
458
|
-
# 'nearest' (and also 'bilinear') doesn't use spatial_filters.frag
|
|
459
|
-
# so u_kernel and shape setting is skipped
|
|
460
540
|
texture_interpolation = 'nearest'
|
|
461
|
-
|
|
541
|
+
|
|
542
|
+
# 'nearest' (and also 'linear') doesn't use spatial_filters.frag
|
|
543
|
+
# so u_kernel and shape setting is skipped
|
|
544
|
+
if interpolation not in ('nearest', 'linear'):
|
|
545
|
+
self._data_lookup_fn['shape'] = self._data.shape[:2][::-1]
|
|
546
|
+
if interpolation == 'custom':
|
|
547
|
+
self._data_lookup_fn['kernel'] = self._custom_kerneltex
|
|
548
|
+
self._data_lookup_fn['kernel_shape'] = self._custom_kernel.shape[::-1]
|
|
549
|
+
else:
|
|
462
550
|
self.shared_program['u_kernel'] = self._kerneltex
|
|
463
|
-
self._data_lookup_fn['shape'] = self._data.shape[:2][::-1]
|
|
464
551
|
|
|
465
552
|
if self._texture.interpolation != texture_interpolation:
|
|
466
553
|
self._texture.interpolation = texture_interpolation
|
|
@@ -526,7 +613,8 @@ class ImageVisual(Visual):
|
|
|
526
613
|
except RuntimeError:
|
|
527
614
|
pre_clims = "auto"
|
|
528
615
|
pre_internalformat = self._texture.internalformat
|
|
529
|
-
|
|
616
|
+
# copy was already made on `set_data` if requested
|
|
617
|
+
self._texture.scale_and_set_data(self._data, copy=False)
|
|
530
618
|
post_clims = self._texture.clim_normalized
|
|
531
619
|
post_internalformat = self._texture.internalformat
|
|
532
620
|
# color transform needs rebuilding if the internalformat was changed
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from .image import ImageVisual, _APPLY_CLIM_FLOAT, _APPLY_GAMMA_FLOAT
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .shaders import Function, FunctionChain
|
|
4
|
+
|
|
5
|
+
# In a complex Image, the texture will be rg32f, where:
|
|
6
|
+
# data.r contains the real component
|
|
7
|
+
# data.g contains the imaginary component
|
|
8
|
+
COMPLEX_TRANSFORMS = {
|
|
9
|
+
"real": "float cplx2float(vec4 data) { return data.r; }",
|
|
10
|
+
"imaginary": "float cplx2float(vec4 data) { return data.g; }",
|
|
11
|
+
"magnitude": "float cplx2float(vec4 data) { return length(vec2(data)); }",
|
|
12
|
+
"phase": "float cplx2float(vec4 data) { return atan(data.g, data.r); }",
|
|
13
|
+
}
|
|
14
|
+
CPU_COMPLEX_TRANSFORMS = {
|
|
15
|
+
"magnitude": np.abs,
|
|
16
|
+
"phase": np.angle,
|
|
17
|
+
"real": np.real,
|
|
18
|
+
"imaginary": np.imag,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ComplexImageVisual(ImageVisual):
|
|
23
|
+
""":class:`~vispy.visuals.ImageVisual` subclass displaying a complex-valued image.
|
|
24
|
+
|
|
25
|
+
This class handles complex values by using an rg32f float texture behind the scenes,
|
|
26
|
+
storing the real component in the "r" value and the imaginary in the "g" value.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
data : ndarray
|
|
31
|
+
Complex valued ImageVisual data. Should be a two dimensional array with a dtype
|
|
32
|
+
of np.complex64 or np.complex128.
|
|
33
|
+
complex_mode : str
|
|
34
|
+
The mode used to convert the complex value in each pixel into a scalar:
|
|
35
|
+
* 'real': show only the real component.
|
|
36
|
+
* 'imaginary': show only the imaginary component.
|
|
37
|
+
* 'magnitude': show the magnitude (`np.abs`) of the complex value.
|
|
38
|
+
* 'phase': show the phase (`np.angle`) of the complex value.
|
|
39
|
+
"""
|
|
40
|
+
COMPLEX_MODES = set(COMPLEX_TRANSFORMS)
|
|
41
|
+
|
|
42
|
+
def __init__(self, data=None, complex_mode="magnitude", **kwargs):
|
|
43
|
+
if complex_mode not in self.COMPLEX_MODES:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"complex_mode must be one of %s" % ", ".join(self.COMPLEX_MODES)
|
|
46
|
+
)
|
|
47
|
+
self._data_is_complex = np.iscomplexobj(data)
|
|
48
|
+
self._complex_mode = complex_mode
|
|
49
|
+
|
|
50
|
+
if kwargs.get("clim", "auto") == "auto" and self._data_is_complex:
|
|
51
|
+
kwargs["clim"] = self._calc_complex_clim(data)
|
|
52
|
+
|
|
53
|
+
kwargs["texture_format"] = "r32f" if self._data_is_complex else "r32f"
|
|
54
|
+
if self._data_is_complex:
|
|
55
|
+
data = self._convert_complex_to_float_view(data)
|
|
56
|
+
super().__init__(data=data, **kwargs)
|
|
57
|
+
|
|
58
|
+
def _init_texture(self, data, texture_format, **texture_kwargs):
|
|
59
|
+
texture_kwargs = {}
|
|
60
|
+
if self._data_is_complex:
|
|
61
|
+
texture_kwargs["format"] = "rg"
|
|
62
|
+
return super()._init_texture(data, texture_format, **texture_kwargs)
|
|
63
|
+
|
|
64
|
+
def set_data(self, image):
|
|
65
|
+
data = np.asarray(image)
|
|
66
|
+
if np.iscomplexobj(data):
|
|
67
|
+
# Turn the texture into an rg32f texture
|
|
68
|
+
# where r = 'real' and g = 'imag'
|
|
69
|
+
self._data_is_complex = True
|
|
70
|
+
# FUTURE: Add formal way of defining texture format from set_data
|
|
71
|
+
self._texture._format = "rg"
|
|
72
|
+
data = self._convert_complex_to_float_view(data)
|
|
73
|
+
elif data.ndim == 3 and data.shape[-1] == 2:
|
|
74
|
+
# data was complex but was already converted to 32-bit float
|
|
75
|
+
# should really only occur from __init__
|
|
76
|
+
self._data_is_complex = True
|
|
77
|
+
else:
|
|
78
|
+
self._texture._format = None
|
|
79
|
+
return super().set_data(data)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _convert_complex_to_float_view(complex_arr):
|
|
83
|
+
# turn complex128 into complex64 if needed
|
|
84
|
+
complex64_arr = complex_arr.astype(np.complex64, copy=False)
|
|
85
|
+
float_view_arr = complex64_arr.view(dtype=np.float32).reshape((complex64_arr.shape + (2, )))
|
|
86
|
+
return float_view_arr
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def complex_mode(self):
|
|
90
|
+
return self._data_is_complex and self._complex_mode
|
|
91
|
+
|
|
92
|
+
@complex_mode.setter
|
|
93
|
+
def complex_mode(self, value):
|
|
94
|
+
if value not in self.COMPLEX_MODES:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"complex_mode must be one of %s" % ", ".join(self.COMPLEX_MODES)
|
|
97
|
+
)
|
|
98
|
+
if self._complex_mode != value:
|
|
99
|
+
self._complex_mode = value
|
|
100
|
+
self._need_colortransform_update = True
|
|
101
|
+
self.update()
|
|
102
|
+
|
|
103
|
+
def _build_color_transform(self):
|
|
104
|
+
if self.complex_mode:
|
|
105
|
+
fclim = Function(_APPLY_CLIM_FLOAT)
|
|
106
|
+
fgamma = Function(_APPLY_GAMMA_FLOAT)
|
|
107
|
+
chain = [
|
|
108
|
+
Function(COMPLEX_TRANSFORMS[self.complex_mode]),
|
|
109
|
+
fclim,
|
|
110
|
+
fgamma,
|
|
111
|
+
Function(self.cmap.glsl_map),
|
|
112
|
+
]
|
|
113
|
+
fun = FunctionChain(None, chain)
|
|
114
|
+
fclim["clim"] = self._texture.clim_normalized
|
|
115
|
+
fgamma["gamma"] = self.gamma
|
|
116
|
+
return fun
|
|
117
|
+
return super()._build_color_transform()
|
|
118
|
+
|
|
119
|
+
@ImageVisual.clim.setter
|
|
120
|
+
def clim(self, clim):
|
|
121
|
+
if clim == "auto" and self.complex_mode:
|
|
122
|
+
clim = self._calc_complex_clim()
|
|
123
|
+
super(ComplexImageVisual, type(self)).clim.fset(self, clim)
|
|
124
|
+
|
|
125
|
+
def _calc_complex_clim(self, data=None):
|
|
126
|
+
# it would be nice if this could be done in the scalable texture mixin,
|
|
127
|
+
# but that would require the mixin knowing about the complex mode.
|
|
128
|
+
func = CPU_COMPLEX_TRANSFORMS[self.complex_mode]
|
|
129
|
+
_rendered = func(self._data if data is None else data)
|
|
130
|
+
return (_rendered.min(), _rendered.max())
|