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.

Files changed (103) hide show
  1. vispy/app/backends/_glfw.py +2 -2
  2. vispy/app/backends/_pyglet.py +8 -2
  3. vispy/app/backends/_qt.py +88 -63
  4. vispy/app/backends/_wx.py +6 -1
  5. vispy/app/canvas.py +4 -2
  6. vispy/app/tests/test_canvas.py +52 -1
  7. vispy/app/tests/test_context.py +5 -3
  8. vispy/color/color_array.py +8 -1
  9. vispy/color/colormap.py +5 -25
  10. vispy/geometry/meshdata.py +76 -38
  11. vispy/geometry/rect.py +6 -0
  12. vispy/geometry/tests/test_meshdata.py +72 -0
  13. vispy/gloo/buffer.py +12 -0
  14. vispy/gloo/gl/_constants.py +9 -5
  15. vispy/gloo/gl/_es2.py +8 -4
  16. vispy/gloo/gl/_gl2.py +2 -3
  17. vispy/gloo/gl/_proxy.py +1 -1
  18. vispy/gloo/gl/_pyopengl2.py +12 -7
  19. vispy/gloo/gl/tests/test_names.py +3 -0
  20. vispy/gloo/glir.py +26 -13
  21. vispy/gloo/program.py +39 -22
  22. vispy/gloo/tests/test_program.py +9 -2
  23. vispy/gloo/tests/test_texture.py +19 -2
  24. vispy/gloo/texture.py +46 -16
  25. vispy/gloo/wrappers.py +4 -2
  26. vispy/glsl/build_spatial_filters.py +241 -293
  27. vispy/glsl/misc/spatial-filters.frag +1299 -254
  28. vispy/io/_data/spatial-filters.npy +0 -0
  29. vispy/io/datasets.py +2 -2
  30. vispy/io/image.py +1 -1
  31. vispy/io/stl.py +3 -3
  32. vispy/scene/cameras/base_camera.py +6 -2
  33. vispy/scene/cameras/panzoom.py +10 -14
  34. vispy/scene/cameras/perspective.py +6 -0
  35. vispy/scene/cameras/tests/test_cameras.py +27 -0
  36. vispy/scene/cameras/tests/test_perspective.py +37 -0
  37. vispy/scene/cameras/turntable.py +39 -23
  38. vispy/scene/canvas.py +9 -5
  39. vispy/scene/events.py +9 -0
  40. vispy/scene/node.py +19 -2
  41. vispy/scene/tests/test_canvas.py +30 -1
  42. vispy/scene/tests/test_visuals.py +113 -0
  43. vispy/scene/visuals.py +6 -1
  44. vispy/scene/widgets/viewbox.py +3 -2
  45. vispy/testing/_runners.py +6 -12
  46. vispy/testing/_testing.py +3 -4
  47. vispy/util/check_environment.py +4 -4
  48. vispy/util/gallery_scraper.py +50 -32
  49. vispy/util/tests/test_gallery_scraper.py +2 -0
  50. vispy/util/transforms.py +1 -1
  51. vispy/util/wrappers.py +1 -1
  52. vispy/version.py +2 -3
  53. vispy/visuals/__init__.py +2 -0
  54. vispy/visuals/_scalable_textures.py +20 -17
  55. vispy/visuals/collections/array_list.py +3 -3
  56. vispy/visuals/collections/base_collection.py +1 -1
  57. vispy/visuals/ellipse.py +1 -1
  58. vispy/visuals/filters/__init__.py +3 -2
  59. vispy/visuals/filters/base_filter.py +120 -0
  60. vispy/visuals/filters/clipping_planes.py +24 -12
  61. vispy/visuals/filters/markers.py +28 -0
  62. vispy/visuals/filters/mesh.py +61 -6
  63. vispy/visuals/filters/tests/test_primitive_picking_filters.py +70 -0
  64. vispy/visuals/graphs/graph.py +1 -1
  65. vispy/visuals/image.py +114 -26
  66. vispy/visuals/image_complex.py +130 -0
  67. vispy/visuals/instanced_mesh.py +152 -0
  68. vispy/visuals/isocurve.py +1 -1
  69. vispy/visuals/line/dash_atlas.py +46 -41
  70. vispy/visuals/line/line.py +2 -5
  71. vispy/visuals/markers.py +310 -384
  72. vispy/visuals/mesh.py +2 -2
  73. vispy/visuals/shaders/function.py +3 -0
  74. vispy/visuals/shaders/tests/test_function.py +6 -0
  75. vispy/visuals/tests/test_axis.py +2 -2
  76. vispy/visuals/tests/test_image.py +92 -2
  77. vispy/visuals/tests/test_image_complex.py +36 -0
  78. vispy/visuals/tests/test_instanced_mesh.py +50 -0
  79. vispy/visuals/tests/test_markers.py +6 -0
  80. vispy/visuals/tests/test_mesh.py +17 -0
  81. vispy/visuals/tests/test_text.py +11 -0
  82. vispy/visuals/tests/test_volume.py +218 -12
  83. vispy/visuals/text/_sdf_cpu.cp38-win_amd64.pyd +0 -0
  84. vispy/visuals/text/_sdf_cpu.pyx +21 -23
  85. vispy/visuals/text/text.py +9 -3
  86. vispy/visuals/tube.py +2 -2
  87. vispy/visuals/visual.py +144 -3
  88. vispy/visuals/volume.py +300 -131
  89. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/LICENSE.txt +1 -1
  90. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/METADATA +218 -198
  91. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/RECORD +93 -96
  92. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/WHEEL +1 -1
  93. vispy/glsl/antialias/__init__.py +0 -0
  94. vispy/glsl/arrowheads/__init__.py +0 -0
  95. vispy/glsl/arrows/__init__.py +0 -0
  96. vispy/glsl/collections/__init__.py +0 -0
  97. vispy/glsl/colormaps/__init__.py +0 -0
  98. vispy/glsl/lines/__init__.py +0 -0
  99. vispy/glsl/markers/__init__.py +0 -0
  100. vispy/glsl/math/__init__.py +0 -0
  101. vispy/glsl/misc/__init__.py +0 -0
  102. vispy/glsl/transforms/__init__.py +0 -0
  103. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/top_level.txt +0 -0
@@ -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(shading_vertex_template)
416
- ffunc = Function(shading_fragment_template)
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 on_mesh_data_updated(self, event):
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.on_mesh_data_updated)
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.on_mesh_data_updated)
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
@@ -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 = ('symbol',)
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 image interpolation. Makes use of the two Texture2D
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 Texture2D interpolation.
182
- * 'bilinear': uses 'linear' with Texture2D interpolation.
183
- * 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'bicubic',
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, **kwargs):
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
- # interpolations
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 "bilinear" spatial-filters
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['bilinear'] = hardware_lookup
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 == 'bilinear':
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.asarray(image)
337
- if should_cast_to_f32(data.dtype):
338
- data = data.astype(np.float32)
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 'bilinear' uses 'linear' texture interpolation
455
- if interpolation == 'bilinear':
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
- if interpolation != 'nearest':
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
- self._texture.scale_and_set_data(self._data)
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())