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
vispy/visuals/mesh.py CHANGED
@@ -283,8 +283,8 @@ class MeshVisual(Visual):
283
283
  clim_func = 'float cmap(float val) { return (val - $cmin) / ($cmax - $cmin); }'
284
284
  if colors.ndim == 2 and colors.shape[1] == 1:
285
285
  fun = Function(clim_func)
286
- fun['cmin'] = self.clim[0]
287
- fun['cmax'] = self.clim[1]
286
+ fun['cmin'] = self._clim_values[0]
287
+ fun['cmax'] = self._clim_values[1]
288
288
  fun = FunctionChain(None, [fun, Function(self.cmap.glsl_map)])
289
289
  else:
290
290
  fun = Function(null_color_transform)
@@ -309,6 +309,9 @@ class Function(ShaderObject):
309
309
  """
310
310
  return FunctionCall(self, args)
311
311
 
312
+ def __contains__(self, key):
313
+ return key in self.template_vars
314
+
312
315
  # Public API methods
313
316
 
314
317
  @property
@@ -241,6 +241,12 @@ def test_function_basics():
241
241
  assert_equal(fun.name, 'main')
242
242
  assert len(fun.template_vars) == 2
243
243
  assert_in('foo', fun.template_vars)
244
+
245
+ # Test that `var in fun` syntax works as well
246
+ assert 'foo' in fun
247
+ assert 'bar' in fun
248
+ assert 'baz' not in fun
249
+
244
250
  assert_in('bar', fun.template_vars)
245
251
 
246
252
  # Test setting verbatim expressions
@@ -50,12 +50,12 @@ def test_rotation_angle():
50
50
  view.camera.elevation = 90.
51
51
 
52
52
  assert_allclose(axis1._rotation_angle, 0)
53
- assert_allclose(axis2._rotation_angle, -30)
53
+ assert_allclose(axis2._rotation_angle, -30, rtol=1e-3)
54
54
 
55
55
  view.camera.elevation = 45.
56
56
 
57
57
  assert_allclose(axis1._rotation_angle, 0)
58
- assert_allclose(axis2._rotation_angle, -22.207653)
58
+ assert_allclose(axis2._rotation_angle, -22.207653, rtol=1e-3)
59
59
 
60
60
  view.camera.fov = 20.
61
61
 
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from unittest import mock
3
3
 
4
- from vispy.scene.visuals import Image
4
+ from vispy.scene import Image, PanZoomCamera
5
5
  from vispy.testing import (requires_application, TestingCanvas,
6
6
  run_tests_if_main, IS_CI)
7
7
  from vispy.testing.image_tester import assert_image_approved, downsample
@@ -76,7 +76,7 @@ def _get_orig_and_new_clims(input_dtype):
76
76
  def test_image_clims_and_gamma(input_dtype, texture_format, num_channels,
77
77
  clim_on_init, data_on_init):
78
78
  """Test image visual with clims and gamma on shader."""
79
- size = (40, 40)
79
+ size = (80, 80)
80
80
  if texture_format == '__dtype__':
81
81
  texture_format = input_dtype
82
82
  shape = size + (num_channels,) if num_channels > 0 else size
@@ -297,4 +297,94 @@ def test_change_clim_float(dtype, init_clim):
297
297
  assert np.allclose(rendered1, rendered2)
298
298
 
299
299
 
300
+ @requires_application()
301
+ def test_image_interpolation():
302
+ """Test different interpolations"""
303
+ size = (81, 81)
304
+ data = np.array([[0, 1]], dtype=int)
305
+ left = (40, 0)
306
+ right = (40, 80)
307
+ center_left = (40, 39)
308
+ center = (40, 40)
309
+ center_right = (40, 41)
310
+ white = (255, 255, 255, 255)
311
+ black = (0, 0, 0, 255)
312
+ gray = (128, 128, 128, 255)
313
+
314
+ with TestingCanvas(size=size[::-1], bgcolor="w") as c:
315
+ view = c.central_widget.add_view(border_width=0)
316
+ view.camera = PanZoomCamera((0, 0, 2, 1))
317
+ image = Image(data=data, cmap='grays',
318
+ parent=view.scene)
319
+
320
+ # needed to properly initialize the canvas
321
+ render = c.render()
322
+
323
+ image.interpolation = 'nearest'
324
+ render = c.render()
325
+ assert np.allclose(render[left], black)
326
+ assert np.allclose(render[right], white)
327
+ assert np.allclose(render[center_left], black)
328
+ assert np.allclose(render[center_right], white)
329
+
330
+ image.interpolation = 'bilinear'
331
+ render = c.render()
332
+ assert np.allclose(render[left], black)
333
+ assert np.allclose(render[right], white)
334
+ assert np.allclose(render[center], gray, atol=5) # we just want gray, this is not quantitative
335
+
336
+ image.interpolation = 'custom'
337
+ image.custom_kernel = np.array([[0]]) # no sampling
338
+ render = c.render()
339
+ assert np.allclose(render[left], black)
340
+ assert np.allclose(render[right], black)
341
+ assert np.allclose(render[center], black)
342
+
343
+ image.custom_kernel = np.array([[1]]) # same as linear
344
+ render = c.render()
345
+ assert np.allclose(render[left], black)
346
+ assert np.allclose(render[right], white)
347
+ assert np.allclose(render[center], gray, atol=5) # we just want gray, this is not quantitative
348
+
349
+
350
+ @requires_application()
351
+ def test_image_set_data_different_dtype():
352
+ size = (80, 80)
353
+ data = np.array([[0, 127]], dtype=np.int8)
354
+ left = (40, 10)
355
+ right = (40, 70)
356
+ white = (255, 255, 255, 255)
357
+ black = (0, 0, 0, 255)
358
+
359
+ with TestingCanvas(size=size[::-1], bgcolor="w") as c:
360
+ view = c.central_widget.add_view()
361
+ view.camera = PanZoomCamera((0, 0, 2, 1))
362
+ image = Image(data=data, cmap='grays', clim=[0, 127],
363
+ parent=view.scene)
364
+
365
+ render = c.render()
366
+ assert np.allclose(render[left], black)
367
+ assert np.allclose(render[right], white)
368
+
369
+ # same data as float should change nothing
370
+ image.set_data(data.astype(np.float32))
371
+ render = c.render()
372
+ assert np.allclose(render[left], black)
373
+ assert np.allclose(render[right], white)
374
+
375
+ # something inverted, different dtype
376
+ new_data = np.array([[127, 0]], dtype=np.float16)
377
+ image.set_data(new_data)
378
+ render = c.render()
379
+ assert np.allclose(render[left], white)
380
+ assert np.allclose(render[right], black)
381
+
382
+ # out of bounds should clip (2000 > 127)
383
+ new_data = np.array([[0, 2000]], dtype=np.float64)
384
+ image.set_data(new_data)
385
+ render = c.render()
386
+ assert np.allclose(render[left], black)
387
+ assert np.allclose(render[right], white)
388
+
389
+
300
390
  run_tests_if_main()
@@ -0,0 +1,36 @@
1
+ from vispy.scene.visuals import ComplexImage
2
+ from vispy.visuals.image_complex import CPU_COMPLEX_TRANSFORMS
3
+ from vispy.testing import requires_application, TestingCanvas
4
+ from vispy.testing.image_tester import downsample
5
+
6
+ import numpy as np
7
+ import pytest
8
+
9
+ from vispy.testing.rendered_array_tester import compare_render
10
+
11
+
12
+ # we add the np.float32 case to test that ComplexImage falls back to Image behavior
13
+ # if the data is not complex
14
+ @requires_application()
15
+ @pytest.mark.parametrize("input_dtype", [np.complex64, np.complex128, np.float32])
16
+ @pytest.mark.parametrize("complex_mode", ["magnitude", "real", "imaginary", "phase"])
17
+ def test_image_complex(input_dtype, complex_mode):
18
+ """Test rendering of complex-valued image data."""
19
+ shape = (40, 40)
20
+ np.random.seed(0)
21
+ data = np.random.random(shape).astype(input_dtype)
22
+ if np.iscomplexobj(data):
23
+ data.imag = np.random.random(shape)
24
+
25
+ with TestingCanvas(size=shape, bgcolor="w") as c:
26
+ ComplexImage(data, cmap="grays", complex_mode=complex_mode, parent=c.scene)
27
+ # render to canvas
28
+ rendered = c.render()
29
+ shape_ratio = rendered.shape[0] // data.shape[0]
30
+ rendered = downsample(rendered, shape_ratio, axis=(0, 1))
31
+
32
+ # perform (auto-clim) rendering on cpu
33
+ exp = CPU_COMPLEX_TRANSFORMS[complex_mode](data) if np.iscomplexobj(data) else data
34
+ exp -= exp.min()
35
+ exp /= exp.max()
36
+ compare_render(exp, rendered)
@@ -0,0 +1,50 @@
1
+ import numpy as np
2
+ from vispy import scene, use
3
+
4
+ from vispy.testing import (TestingCanvas, requires_application,
5
+ run_tests_if_main, requires_pyopengl)
6
+
7
+
8
+ def setup_module(module):
9
+ use(gl='gl+')
10
+
11
+
12
+ def teardown_module(module):
13
+ use(gl='gl2')
14
+
15
+
16
+ @requires_pyopengl()
17
+ @requires_application()
18
+ def test_mesh_with_vertex_values():
19
+ size = (80, 60)
20
+ with TestingCanvas(size=size) as c:
21
+ use(gl='gl+')
22
+ vert = np.array([[0, 0, 0], [0, 30, 0], [40, 0, 0]])
23
+ faces = np.array([0, 1, 2])
24
+ pos = np.array([[0, 0, 0], [80, 60, 0]])
25
+ # identity and rotate 180
26
+ trans = np.array([
27
+ [
28
+ [1, 0, 0],
29
+ [0, 1, 0],
30
+ [0, 0, 1],
31
+ ],
32
+ [
33
+ [-1, 0, 0],
34
+ [0, -1, 0],
35
+ [0, 0, 1],
36
+ ],
37
+ ])
38
+ colors = ['red', 'blue']
39
+ mesh = scene.visuals.InstancedMesh(
40
+ vertices=vert, faces=faces, instance_positions=pos, instance_transforms=trans, instance_colors=colors
41
+ )
42
+ v = c.central_widget.add_view(border_width=0)
43
+ v.add(mesh)
44
+ render = c.render()
45
+ assert np.allclose(render[10, 10], (255, 0, 0, 255))
46
+ assert np.allclose(render[-10, -10], (0, 0, 255, 255))
47
+ assert np.allclose(render[30, 40], (0, 0, 0, 255))
48
+
49
+
50
+ run_tests_if_main()
@@ -45,4 +45,10 @@ def test_markers_edge_width():
45
45
  with pytest.raises(ValueError):
46
46
  marker.set_data(pos=data, edge_width_rel=edge_width - 1, edge_width=None)
47
47
 
48
+
49
+ def test_empty_markers_symbol():
50
+ markers = Markers()
51
+ markers.symbol = 'o'
52
+
53
+
48
54
  run_tests_if_main()
@@ -31,6 +31,23 @@ def test_mesh_color():
31
31
  np.testing.assert_allclose(vertices['position'], new_vertices)
32
32
 
33
33
 
34
+ @requires_pyopengl()
35
+ @requires_application()
36
+ def test_mesh_with_vertex_values():
37
+ size = (45, 40)
38
+ with TestingCanvas(size=size) as c:
39
+ v = c.central_widget.add_view(border_width=0)
40
+ vertices = np.array([(0, 0, 0), (0, 0, 1), (1, 0, 0)], dtype=float)
41
+ faces = np.array([(0, 1, 2)])
42
+ mesh = scene.visuals.Mesh(
43
+ vertices=vertices,
44
+ faces=faces,
45
+ vertex_values=np.ones(len(vertices)),
46
+ )
47
+ v.add(mesh)
48
+ c.render()
49
+
50
+
34
51
  @requires_pyopengl()
35
52
  @requires_application()
36
53
  @pytest.mark.parametrize('shading', [None, 'flat', 'smooth'])
@@ -81,4 +81,15 @@ def test_face_bold_italic():
81
81
  assert font1 is font4
82
82
 
83
83
 
84
+ def test_text_depth_test():
85
+ t = Text(depth_test=False)
86
+ assert not t._vshare.gl_state["depth_test"]
87
+
88
+ t = Text(depth_test=True)
89
+ assert t._vshare.gl_state["depth_test"]
90
+
91
+ t = Text() # Default is false
92
+ assert not t._vshare.gl_state["depth_test"]
93
+
94
+
84
95
  run_tests_if_main()
@@ -238,7 +238,7 @@ def _make_test_data(shape, input_dtype):
238
238
  @requires_pyopengl()
239
239
  def test_set_data_does_not_change_input():
240
240
  # Create volume
241
- V = scene.visuals.Volume(np.zeros((20, 20, 20)))
241
+ V = scene.visuals.Volume(np.zeros((20, 20, 20), dtype=np.float32))
242
242
 
243
243
  # calling Volume.set_data() should NOT alter the values of the input array
244
244
  # regardless of data type
@@ -249,12 +249,36 @@ def test_set_data_does_not_change_input():
249
249
  V.set_data(vol_copy, clim=(0, 200))
250
250
  assert np.allclose(vol, vol_copy)
251
251
 
252
- # for those using float32 who want to avoid the copy operation,
253
- # using set_data() with `copy=False` should be expected to alter the data.
254
- vol2 = np.array(vol, dtype='float32', copy=True)
252
+ # dtype has to be the same as the one used to init the texture, or it will
253
+ # be first coerced to the same dtype as the init
254
+
255
+ vol2 = np.array(vol, dtype=np.float32, copy=True)
255
256
  assert np.allclose(vol, vol2)
257
+ # we explicitly create a copy when data would be altered by the texture,
258
+ # no matter what the user asks, so the data outside should never change
256
259
  V.set_data(vol2, clim=(0, 200), copy=False)
257
- assert not np.allclose(vol, vol2)
260
+ assert np.allclose(vol, vol2)
261
+
262
+
263
+ @requires_pyopengl()
264
+ def test_set_data_changes_shape():
265
+ dtype = np.float32
266
+ # Create initial volume
267
+ V = scene.visuals.Volume(np.zeros((20, 20, 20), dtype=dtype))
268
+
269
+ # Sending new three dimensional data of different shape should alter volume shape
270
+ vol = np.zeros((25, 25, 10), dtype=dtype)
271
+ V.set_data(vol)
272
+ assert V._vol_shape == (25, 25, 10)
273
+
274
+ # Sending data of dimension other than 3 should raise a ValueError
275
+ vol2 = np.zeros((20, 20), dtype=dtype)
276
+ with pytest.raises(ValueError):
277
+ V.set_data(vol2)
278
+
279
+ vol2 = np.zeros((20, 20, 20, 20), dtype=dtype)
280
+ with pytest.raises(ValueError):
281
+ V.set_data(vol2)
258
282
 
259
283
 
260
284
  @requires_pyopengl()
@@ -296,17 +320,17 @@ def test_changing_cmap():
296
320
  @requires_pyopengl()
297
321
  @requires_application()
298
322
  def test_plane_depth():
299
- with TestingCanvas(size=(40, 40)) as c:
323
+ with TestingCanvas(size=(80, 80)) as c:
300
324
  v = c.central_widget.add_view(border_width=0)
301
325
  v.camera = 'arcball'
302
326
  v.camera.fov = 0
303
- v.camera.center = (20, 20, 20)
304
- v.camera.scale_factor = 40.0
327
+ v.camera.center = (40, 40, 40)
328
+ v.camera.scale_factor = 80.0
305
329
 
306
330
  # two planes at 45 degrees relative to the camera. If depth is set correctly, we should see one half
307
331
  # of the screen red and the other half white
308
332
  scene.visuals.Volume(
309
- np.ones((40, 40, 40), dtype=np.uint8),
333
+ np.ones((80, 80, 80), dtype=np.uint8),
310
334
  interpolation="nearest",
311
335
  clim=(0, 1),
312
336
  cmap="grays",
@@ -316,7 +340,7 @@ def test_plane_depth():
316
340
  )
317
341
 
318
342
  scene.visuals.Volume(
319
- np.ones((40, 40, 40), dtype=np.uint8),
343
+ np.ones((80, 80, 80), dtype=np.uint8),
320
344
  interpolation="nearest",
321
345
  clim=(0, 1),
322
346
  cmap="reds",
@@ -327,10 +351,192 @@ def test_plane_depth():
327
351
 
328
352
  # render with grays colormap
329
353
  rendered = c.render()
330
- left = rendered[20, 10]
331
- right = rendered[20, 30]
354
+ left = rendered[40, 20]
355
+ right = rendered[40, 60]
332
356
  assert np.array_equal(left, [255, 0, 0, 255])
333
357
  assert np.array_equal(right, [255, 255, 255, 255])
334
358
 
335
359
 
360
+ @requires_pyopengl()
361
+ @requires_application()
362
+ def test_volume_depth():
363
+ """Check that depth setting is properly performed for the volume visual
364
+
365
+ Render a volume with a blue ball in front of a red plane in front of a
366
+ blue plane, checking that the output image contains both red and blue pixels.
367
+ """
368
+ # A blue strip behind a red strip
369
+ # If depth is set correctly, we should see only red pixels
370
+ # the screen
371
+ blue_vol = np.zeros((80, 80, 80), dtype=np.uint8)
372
+ blue_vol[:, -1, :] = 1 # back plane blue
373
+ blue_vol[30:50, 30:50, 30:50] = 1 # blue in center
374
+
375
+ red_vol = np.zeros((80, 80, 80), dtype=np.uint8)
376
+ red_vol[:, -5, :] = 1 # red plane in front of blue plane
377
+
378
+ with TestingCanvas(size=(80, 80)) as c:
379
+ v = c.central_widget.add_view(border_width=0)
380
+ v.camera = 'arcball'
381
+ v.camera.fov = 0
382
+ v.camera.center = (40, 40, 40)
383
+ v.camera.scale_factor = 80.0
384
+
385
+ scene.visuals.Volume(
386
+ red_vol,
387
+ interpolation="nearest",
388
+ clim=(0, 1),
389
+ cmap="reds",
390
+ parent=v.scene,
391
+ )
392
+
393
+ scene.visuals.Volume(
394
+ blue_vol,
395
+ interpolation="nearest",
396
+ clim=(0, 1),
397
+ cmap="blues",
398
+ parent=v.scene,
399
+ )
400
+
401
+ # render
402
+ rendered = c.render()
403
+ reds = np.sum(rendered[:, :, 0])
404
+ greens = np.sum(rendered[:, :, 1])
405
+ blues = np.sum(rendered[:, :, 2])
406
+ assert reds > 0
407
+ np.testing.assert_allclose(greens, 0)
408
+ assert blues > 0
409
+
410
+
411
+ @requires_pyopengl()
412
+ @requires_application()
413
+ def test_mip_cutoff():
414
+ """
415
+ Ensure fragments are properly discarded based on the mip_cutoff
416
+ for the mip and attenuated_mip rendering methods
417
+ """
418
+ with TestingCanvas(size=(80, 80)) as c:
419
+ v = c.central_widget.add_view(border_width=0)
420
+ v.camera = 'arcball'
421
+ v.camera.fov = 0
422
+ v.camera.center = (40, 40, 40)
423
+ v.camera.scale_factor = 80.0
424
+
425
+ vol = scene.visuals.Volume(
426
+ np.ones((80, 80, 80), dtype=np.uint8),
427
+ interpolation="nearest",
428
+ clim=(0, 1),
429
+ cmap="grays",
430
+ parent=v.scene,
431
+ )
432
+
433
+ # we should see white
434
+ rendered = c.render()
435
+ assert np.array_equal(rendered[40, 40], [255, 255, 255, 255])
436
+
437
+ vol.mip_cutoff = 10
438
+ # we should see black
439
+ rendered = c.render()
440
+ assert np.array_equal(rendered[40, 40], [0, 0, 0, 255])
441
+
442
+ # repeat for attenuated_mip
443
+ vol.method = 'attenuated_mip'
444
+ vol.mip_cutoff = None
445
+
446
+ # we should see white
447
+ rendered = c.render()
448
+ assert np.array_equal(rendered[40, 40], [255, 255, 255, 255])
449
+
450
+ vol.mip_cutoff = 10
451
+ # we should see black
452
+ rendered = c.render()
453
+ assert np.array_equal(rendered[40, 40], [0, 0, 0, 255])
454
+
455
+
456
+ @requires_pyopengl()
457
+ @requires_application()
458
+ def test_minip_cutoff():
459
+ """
460
+ Ensure fragments are properly discarded based on the minip_cutoff
461
+ for the minip rendering method
462
+ """
463
+ with TestingCanvas(size=(80, 80)) as c:
464
+ v = c.central_widget.add_view(border_width=0)
465
+ v.camera = 'arcball'
466
+ v.camera.fov = 0
467
+ v.camera.center = (40, 40, 40)
468
+ v.camera.scale_factor = 120.0
469
+
470
+ # just surface of the cube is ones, but it should win over the twos inside
471
+ data = np.ones((80, 80, 80), dtype=np.uint8)
472
+ data[1:-1, 1:-1, 1:-1] = 2
473
+
474
+ vol = scene.visuals.Volume(
475
+ data,
476
+ interpolation="nearest",
477
+ method='minip',
478
+ clim=(0, 2),
479
+ cmap="grays",
480
+ parent=v.scene,
481
+ )
482
+
483
+ # we should see gray (half of cmap)
484
+ rendered = c.render()
485
+ assert np.array_equal(rendered[40, 40], [128, 128, 128, 255])
486
+
487
+ # discard fragments above -10 (everything)
488
+ vol.minip_cutoff = -10
489
+ # we should see black
490
+ rendered = c.render()
491
+ assert np.array_equal(rendered[40, 40], [0, 0, 0, 255])
492
+
493
+
494
+ @requires_pyopengl()
495
+ @requires_application()
496
+ def test_volume_set_data_different_dtype():
497
+ size = (80, 80)
498
+ data = np.array([[[0, 127]]], dtype=np.int8)
499
+ left = (40, 10)
500
+ right = (40, 70)
501
+ white = (255, 255, 255, 255)
502
+ black = (0, 0, 0, 255)
503
+
504
+ with TestingCanvas(size=size[::-1], bgcolor="w") as c:
505
+ view = c.central_widget.add_view()
506
+ view.camera = 'arcball'
507
+ view.camera.fov = 0
508
+ view.camera.center = 0.5, 0, 0
509
+ view.camera.scale_factor = 2
510
+ volume = scene.visuals.Volume(
511
+ data,
512
+ cmap='grays',
513
+ clim=[0, 127],
514
+ parent=view.scene
515
+ )
516
+
517
+ render = c.render()
518
+ assert np.allclose(render[left], black)
519
+ assert np.allclose(render[right], white)
520
+
521
+ # same data as float should change nothing
522
+ volume.set_data(data.astype(np.float32))
523
+ render = c.render()
524
+ assert np.allclose(render[left], black)
525
+ assert np.allclose(render[right], white)
526
+
527
+ # something inverted, different dtype
528
+ new_data = np.array([[[127, 0]]], dtype=np.float16)
529
+ volume.set_data(new_data)
530
+ render = c.render()
531
+ assert np.allclose(render[left], white)
532
+ assert np.allclose(render[right], black)
533
+
534
+ # out of bounds should clip (2000 > 127)
535
+ new_data = np.array([[[0, 2000]]], dtype=np.float64)
536
+ volume.set_data(new_data)
537
+ render = c.render()
538
+ assert np.allclose(render[left], black)
539
+ assert np.allclose(render[right], white)
540
+
541
+
336
542
  run_tests_if_main()
@@ -1,5 +1,4 @@
1
- # -*- coding: utf-8 -*-
2
-
1
+ # cython: language_level=3, boundscheck=False, cdivision=True, wraparound=False, initializedcheck=False, nonecheck=False
3
2
  # A Cython implementation of the "eight-points signed sequential Euclidean
4
3
  # distance transform algorithm" (8SSEDT)
5
4
 
@@ -18,13 +17,14 @@ ctypedef np.complex64_t DTYPE_ct
18
17
  cdef DTYPE_ct MAX_VAL = (1e6 + 1e6j)
19
18
 
20
19
 
21
- @cython.boundscheck(False) # designed to stay within bounds
22
- @cython.wraparound(False) # we don't use negative indexing
23
20
  def _calc_distance_field(np.ndarray[DTYPE_t, ndim=2] pixels,
24
21
  int w, int h, DTYPE_t sp_f):
25
22
  # initialize grids
26
- cdef np.ndarray[DTYPE_ct, ndim=2] g0 = np.zeros((h, w), dtype_c)
27
- cdef np.ndarray[DTYPE_ct, ndim=2] g1 = np.zeros((h, w), dtype_c)
23
+ cdef np.ndarray[DTYPE_ct, ndim=2] g0_arr = np.zeros((h, w), dtype_c)
24
+ cdef np.ndarray[DTYPE_ct, ndim=2] g1_arr = np.zeros((h, w), dtype_c)
25
+ cdef DTYPE_ct[:, ::1] g0 = g0_arr
26
+ cdef DTYPE_ct[:, ::1] g1 = g1_arr
27
+ cdef DTYPE_t[:, :] pixels_view = pixels
28
28
  cdef Py_ssize_t y, x
29
29
  for y in range(h):
30
30
  g0[y, 0] = MAX_VAL
@@ -32,9 +32,9 @@ def _calc_distance_field(np.ndarray[DTYPE_t, ndim=2] pixels,
32
32
  g1[y, 0] = MAX_VAL
33
33
  g1[y, w-1] = MAX_VAL
34
34
  for x in range(1, w-1):
35
- if pixels[y, x] > 0:
35
+ if pixels_view[y, x] > 0:
36
36
  g0[y, x] = MAX_VAL
37
- if pixels[y, x] < 1:
37
+ if pixels_view[y, x] < 1:
38
38
  g1[y, x] = MAX_VAL
39
39
  for x in range(w):
40
40
  g0[0, x] = MAX_VAL
@@ -50,37 +50,35 @@ def _calc_distance_field(np.ndarray[DTYPE_t, ndim=2] pixels,
50
50
  cdef DTYPE_t r_sp_f_2 = 1. / (sp_f * 2.)
51
51
  for y in range(1, h-1):
52
52
  for x in range(1, w-1):
53
- pixels[y, x] = sqrt(dist(g0[y, x])) - sqrt(dist(g1[y, x]))
54
- if pixels[y, x] < 0:
55
- pixels[y, x] = (pixels[y, x] + sp_f) * r_sp_f_2
53
+ pixels_view[y, x] = sqrt(dist(g0[y, x])) - sqrt(dist(g1[y, x]))
54
+ if pixels_view[y, x] < 0:
55
+ pixels_view[y, x] = (pixels_view[y, x] + sp_f) * r_sp_f_2
56
56
  else:
57
- pixels[y, x] = 0.5 + pixels[y, x] * r_sp_f_2
58
- pixels[y, x] = max(min(pixels[y, x], 1), 0)
57
+ pixels_view[y, x] = 0.5 + pixels_view[y, x] * r_sp_f_2
58
+ pixels_view[y, x] = max(min(pixels_view[y, x], 1), 0)
59
59
 
60
60
 
61
- @cython.boundscheck(False) # designed to stay within bounds
62
- @cython.wraparound(False) # we don't use negative indexing
63
- cdef Py_ssize_t compare(DTYPE_ct *cell, DTYPE_ct xy, DTYPE_t *current):
61
+ cdef inline Py_ssize_t compare(DTYPE_ct *cell, DTYPE_ct xy, DTYPE_t *current) noexcept nogil:
64
62
  cdef DTYPE_t val = dist(xy)
65
63
  if val < current[0]:
66
64
  cell[0] = xy
67
65
  current[0] = val
68
66
 
69
67
 
70
- @cython.boundscheck(False) # designed to stay within bounds
71
- @cython.wraparound(False) # we don't use negative indexing
72
- cdef DTYPE_t dist(DTYPE_ct val):
68
+ cdef DTYPE_t dist(DTYPE_ct val) noexcept nogil:
73
69
  return val.real*val.real + val.imag*val.imag
74
70
 
75
71
 
76
- @cython.boundscheck(False) # designed to stay within bounds
77
- @cython.wraparound(False) # we don't use negative indexing
78
- cdef void _propagate(np.ndarray[DTYPE_ct, ndim=2] grid):
72
+ cdef void _propagate(DTYPE_ct[:, :] grid) noexcept nogil:
79
73
  cdef Py_ssize_t height = grid.shape[0]
80
74
  cdef Py_ssize_t width = grid.shape[1]
81
75
  cdef Py_ssize_t y, x
82
76
  cdef DTYPE_t current
83
- cdef DTYPE_ct a0=-1, a1=-1j, a2=-1-1j, a3=1-1j
77
+ cdef DTYPE_ct a0, a1, a2, a3
78
+ a0 = -1
79
+ a1 = -1j
80
+ a2 = -1 - 1j
81
+ a3 = 1 - 1j
84
82
  cdef DTYPE_ct b0=1
85
83
  cdef DTYPE_ct c0=1, c1=1j, c2=-1+1j, c3=1+1j
86
84
  cdef DTYPE_ct d0=-1