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/volume.py CHANGED
@@ -34,14 +34,18 @@ The ray is expressed in coordinates local to the volume (i.e. texture
34
34
  coordinates).
35
35
 
36
36
  """
37
+ from __future__ import annotations
38
+
39
+ from typing import Optional
37
40
  from functools import lru_cache
41
+ import warnings
38
42
 
39
- from ._scalable_textures import CPUScaledTexture3D, GPUScaledTextured3D
43
+ from ._scalable_textures import CPUScaledTexture3D, GPUScaledTextured3D, Texture2D
40
44
  from ..gloo import VertexBuffer, IndexBuffer
41
- from ..gloo.texture import should_cast_to_f32
42
45
  from . import Visual
43
46
  from .shaders import Function
44
47
  from ..color import get_colormap
48
+ from ..io import load_spatial_filters
45
49
 
46
50
  import numpy as np
47
51
 
@@ -86,6 +90,8 @@ uniform float gamma;
86
90
  uniform float u_threshold;
87
91
  uniform float u_attenuation;
88
92
  uniform float u_relative_step_size;
93
+ uniform float u_mip_cutoff;
94
+ uniform float u_minip_cutoff;
89
95
 
90
96
  //varyings
91
97
  varying vec3 v_position;
@@ -139,16 +145,16 @@ vec4 calculateColor(vec4 betterColor, vec3 loc, vec3 step)
139
145
 
140
146
  // calculate normal vector from gradient
141
147
  vec3 N; // normal
142
- color1 = $sample( u_volumetex, loc+vec3(-step[0],0.0,0.0) );
143
- color2 = $sample( u_volumetex, loc+vec3(step[0],0.0,0.0) );
148
+ color1 = $get_data(loc+vec3(-step[0],0.0,0.0) );
149
+ color2 = $get_data(loc+vec3(step[0],0.0,0.0) );
144
150
  N[0] = colorToVal(color1) - colorToVal(color2);
145
151
  betterColor = max(max(color1, color2),betterColor);
146
- color1 = $sample( u_volumetex, loc+vec3(0.0,-step[1],0.0) );
147
- color2 = $sample( u_volumetex, loc+vec3(0.0,step[1],0.0) );
152
+ color1 = $get_data(loc+vec3(0.0,-step[1],0.0) );
153
+ color2 = $get_data(loc+vec3(0.0,step[1],0.0) );
148
154
  N[1] = colorToVal(color1) - colorToVal(color2);
149
155
  betterColor = max(max(color1, color2),betterColor);
150
- color1 = $sample( u_volumetex, loc+vec3(0.0,0.0,-step[2]) );
151
- color2 = $sample( u_volumetex, loc+vec3(0.0,0.0,step[2]) );
156
+ color1 = $get_data(loc+vec3(0.0,0.0,-step[2]) );
157
+ color2 = $get_data(loc+vec3(0.0,0.0,step[2]) );
152
158
  N[2] = colorToVal(color1) - colorToVal(color2);
153
159
  betterColor = max(max(color1, color2),betterColor);
154
160
  float gm = length(N); // gradient magnitude
@@ -229,12 +235,10 @@ void main() {
229
235
  // fragment.
230
236
  view_ray = normalize(farpos.xyz - nearpos.xyz);
231
237
 
232
- // Keep track of wheter a surface was found (used for depth)
233
- // Need to do it before raycasting setup because the "plane" mode needs to set
234
- // the depth during that phase.
235
- vec3 surface_point;
236
- bool surface_found = false;
237
-
238
+ // Variables to keep track of where to set the frag depth.
239
+ // frag_depth_point is in data coordinates.
240
+ vec3 frag_depth_point;
241
+
238
242
  // Set up the ray casting
239
243
  // This snippet must define three variables:
240
244
  // vec3 start_loc - the starting location of the ray in texture coordinates
@@ -255,8 +259,9 @@ void main() {
255
259
  vec3 loc = start_loc;
256
260
  int iter = 0;
257
261
 
258
- // Keep track of whether texture has been sampled
259
- int texture_sampled = 0;
262
+ // keep track if the texture is ever sampled; if not, fragment will be discarded
263
+ // this allows us to discard fragments that only traverse clipped parts of the texture
264
+ bool texture_sampled = false;
260
265
 
261
266
  while (iter < nsteps) {
262
267
  for (iter=iter; iter<nsteps; iter++)
@@ -266,9 +271,9 @@ void main() {
266
271
  if (distance_from_clip >= 0)
267
272
  {
268
273
  // Get sample color
269
- vec4 color = $sample(u_volumetex, loc);
274
+ vec4 color = $get_data(loc);
270
275
  float val = color.r;
271
- texture_sampled = 1;
276
+ texture_sampled = true;
272
277
 
273
278
  $in_loop
274
279
  }
@@ -276,28 +281,18 @@ void main() {
276
281
  loc += step;
277
282
  }
278
283
  }
279
-
280
- // discard fragment if texture not sampled
281
- if ( texture_sampled != 1 ) {
284
+
285
+ if (!texture_sampled)
282
286
  discard;
283
- }
284
-
285
- $after_loop
286
287
 
287
- if (surface_found == true) {
288
- // if a surface was found, use it to set the depth buffer
289
- vec4 position2 = vec4(surface_point, 1);
290
- vec4 iproj = $viewtransformf(position2);
291
- iproj.z /= iproj.w;
292
- gl_FragDepth = (iproj.z+1.0)/2.0;
293
- }
294
- else {
295
- gl_FragDepth = gl_FragCoord.z;
296
- }
288
+ $after_loop
297
289
 
290
+ // set frag depth
291
+ vec4 frag_depth_vector = vec4(frag_depth_point, 1);
292
+ vec4 iproj = $viewtransformf(frag_depth_vector);
293
+ iproj.z /= iproj.w;
294
+ gl_FragDepth = (iproj.z+1.0)/2.0;
298
295
  }
299
-
300
-
301
296
  """ # noqa
302
297
 
303
298
  _RAYCASTING_SETUP_VOLUME = """
@@ -323,6 +318,9 @@ _RAYCASTING_SETUP_VOLUME = """
323
318
  vec3 step = ((v_position - front) / u_shape) / f_nsteps;
324
319
  // 0.5 offset needed to get back to correct texture coordinates (vispy#2239)
325
320
  vec3 start_loc = (front + 0.5) / u_shape;
321
+
322
+ // set frag depth to the cube face; this can be overridden by projection snippets
323
+ frag_depth_point = front;
326
324
  """
327
325
 
328
326
  _RAYCASTING_SETUP_PLANE = """
@@ -344,9 +342,8 @@ _RAYCASTING_SETUP_PLANE = """
344
342
  out_of_bounds += float(intersection_tex.z > 1);
345
343
  out_of_bounds += float(intersection_tex.z < 0);
346
344
 
347
- if (out_of_bounds > 0) {
345
+ if (out_of_bounds > 0)
348
346
  discard;
349
- }
350
347
 
351
348
 
352
349
  // Decide how many steps to take
@@ -361,78 +358,125 @@ _RAYCASTING_SETUP_PLANE = """
361
358
  vec3 step = N / u_shape;
362
359
  vec3 start_loc = intersection_tex - ((step * f_nsteps) / 2);
363
360
 
364
- // Set depth value
365
- surface_point = intersection;
366
- surface_found = true;
361
+ // Ensure that frag depth value will be set to plane intersection
362
+ frag_depth_point = intersection;
367
363
  """
368
364
 
369
365
 
370
366
  _MIP_SNIPPETS = dict(
371
367
  before_loop="""
372
- float maxval = -99999.0; // The maximum encountered value
368
+ float maxval = u_mip_cutoff; // The maximum encountered value
373
369
  int maxi = -1; // Where the maximum value was encountered
374
370
  """,
375
371
  in_loop="""
376
- if( val > maxval ) {
372
+ if ( val > maxval ) {
377
373
  maxval = val;
378
374
  maxi = iter;
375
+ if ( maxval >= clim.y ) {
376
+ // stop if no chance of finding a higher maxval
377
+ iter = nsteps;
378
+ }
379
379
  }
380
380
  """,
381
381
  after_loop="""
382
382
  // Refine search for max value, but only if anything was found
383
383
  if ( maxi > -1 ) {
384
- loc = start_loc + step * (float(maxi) - 0.5);
384
+ // Calculate starting location of ray for sampling
385
+ vec3 start_loc_refine = start_loc + step * (float(maxi) - 0.5);
386
+ loc = start_loc_refine;
387
+
388
+ // Variables to keep track of current value and where max was encountered
389
+ vec3 max_loc_tex = start_loc_refine;
390
+
391
+ vec3 small_step = step * 0.1;
385
392
  for (int i=0; i<10; i++) {
386
- maxval = max(maxval, $sample(u_volumetex, loc).r);
387
- loc += step * 0.1;
393
+ float val = $get_data(loc).r;
394
+ if ( val > maxval) {
395
+ maxval = val;
396
+ max_loc_tex = start_loc_refine + (small_step * i);
397
+ }
398
+ loc += small_step;
388
399
  }
400
+ frag_depth_point = max_loc_tex * u_shape;
389
401
  gl_FragColor = applyColormap(maxval);
402
+ } else {
403
+ discard;
390
404
  }
391
405
  """,
392
406
  )
393
407
 
394
408
  _ATTENUATED_MIP_SNIPPETS = dict(
395
409
  before_loop="""
396
- float maxval = -99999.0; // The maximum encountered value
410
+ float maxval = u_mip_cutoff; // The maximum encountered value
397
411
  float sumval = 0.0; // The sum of the encountered values
398
- float scaled = 0.0; // The scaled value
399
- int maxi = 0; // Where the maximum value was encountered
400
- vec3 maxloc = vec3(0.0); // Location where the maximum value was encountered
412
+ float scale = 0.0; // The cumulative attenuation
413
+ int maxi = -1; // Where the maximum value was encountered
414
+ vec3 max_loc_tex = vec3(0.0); // Location where the maximum value was encountered
401
415
  """,
402
416
  in_loop="""
403
- sumval = sumval + val;
404
- scaled = val * exp(-u_attenuation * (sumval - 1) / u_relative_step_size);
405
- if( scaled > maxval ) {
406
- maxval = scaled;
417
+ // Scale and clamp accumulation in `sumval` by contrast limits so that:
418
+ // * attenuation value does not depend on data values
419
+ // * negative values do not amplify instead of attenuate
420
+ sumval = sumval + clamp((val - clim.x) / (clim.y - clim.x), 0.0, 1.0);
421
+ scale = exp(-u_attenuation * (sumval - 1) / u_relative_step_size);
422
+ if( maxval > scale * clim.y ) {
423
+ // stop if no chance of finding a higher maxval
424
+ iter = nsteps;
425
+ } else if( val * scale > maxval ) {
426
+ maxval = val * scale;
407
427
  maxi = iter;
408
- maxloc = loc;
428
+ max_loc_tex = loc;
409
429
  }
410
430
  """,
411
431
  after_loop="""
412
- gl_FragColor = applyColormap(maxval);
432
+ if ( maxi > -1 ) {
433
+ frag_depth_point = max_loc_tex * u_shape;
434
+ gl_FragColor = applyColormap(maxval);
435
+ }
436
+ else {
437
+ discard;
438
+ }
413
439
  """,
414
440
  )
415
441
 
416
442
  _MINIP_SNIPPETS = dict(
417
443
  before_loop="""
418
- float minval = 99999.0; // The minimum encountered value
444
+ float minval = u_minip_cutoff; // The minimum encountered value
419
445
  int mini = -1; // Where the minimum value was encountered
420
446
  """,
421
447
  in_loop="""
422
- if( val < minval ) {
448
+ if ( val < minval ) {
423
449
  minval = val;
424
450
  mini = iter;
451
+ if ( minval <= clim.x ) {
452
+ // stop if no chance of finding a lower minval
453
+ iter = nsteps;
454
+ }
425
455
  }
426
456
  """,
427
457
  after_loop="""
428
458
  // Refine search for min value, but only if anything was found
429
459
  if ( mini > -1 ) {
430
- loc = start_loc + step * (float(mini) - 0.5);
460
+ // Calculate starting location of ray for sampling
461
+ vec3 start_loc_refine = start_loc + step * (float(mini) - 0.5);
462
+ loc = start_loc_refine;
463
+
464
+ // Variables to keep track of current value and where max was encountered
465
+ vec3 min_loc_tex = start_loc_refine;
466
+
467
+ vec3 small_step = step * 0.1;
431
468
  for (int i=0; i<10; i++) {
432
- minval = min(minval, $sample(u_volumetex, loc).r);
433
- loc += step * 0.1;
469
+ float val = $get_data(loc).r;
470
+ if ( val < minval) {
471
+ minval = val;
472
+ min_loc_tex = start_loc_refine + (small_step * i);
473
+ }
474
+ loc += small_step;
434
475
  }
476
+ frag_depth_point = min_loc_tex * u_shape;
435
477
  gl_FragColor = applyColormap(minval);
478
+ } else {
479
+ discard;
436
480
  }
437
481
  """,
438
482
  )
@@ -442,24 +486,24 @@ _TRANSLUCENT_SNIPPETS = dict(
442
486
  vec4 integrated_color = vec4(0., 0., 0., 0.);
443
487
  """,
444
488
  in_loop="""
445
- color = applyColormap(val);
446
- float a1 = integrated_color.a;
447
- float a2 = color.a * (1 - a1);
448
- float alpha = max(a1 + a2, 0.001);
449
-
450
- // Doesn't work.. GLSL optimizer bug?
451
- //integrated_color = (integrated_color * a1 / alpha) +
452
- // (color * a2 / alpha);
453
- // This should be identical but does work correctly:
454
- integrated_color *= a1 / alpha;
455
- integrated_color += color * a2 / alpha;
456
-
457
- integrated_color.a = alpha;
458
-
459
- if( alpha > 0.99 ){
460
- // stop integrating if the fragment becomes opaque
461
- iter = nsteps;
462
- }
489
+ color = applyColormap(val);
490
+ float a1 = integrated_color.a;
491
+ float a2 = color.a * (1 - a1);
492
+ float alpha = max(a1 + a2, 0.001);
493
+
494
+ // Doesn't work.. GLSL optimizer bug?
495
+ //integrated_color = (integrated_color * a1 / alpha) +
496
+ // (color * a2 / alpha);
497
+ // This should be identical but does work correctly:
498
+ integrated_color *= a1 / alpha;
499
+ integrated_color += color * a2 / alpha;
500
+
501
+ integrated_color.a = alpha;
502
+
503
+ if( alpha > 0.99 ){
504
+ // stop integrating if the fragment becomes opaque
505
+ iter = nsteps;
506
+ }
463
507
  """,
464
508
  after_loop="""
465
509
  gl_FragColor = integrated_color;
@@ -485,20 +529,21 @@ _ISO_SNIPPETS = dict(
485
529
  vec4 color3 = vec4(0.0); // final color
486
530
  vec3 dstep = 1.5 / u_shape; // step to sample derivative
487
531
  gl_FragColor = vec4(0.0);
532
+ bool discard_fragment = true;
488
533
  """,
489
534
  in_loop="""
490
535
  if (val > u_threshold-0.2) {
491
536
  // Take the last interval in smaller steps
492
537
  vec3 iloc = loc - step;
493
538
  for (int i=0; i<10; i++) {
494
- color = $sample(u_volumetex, iloc);
539
+ color = $get_data(iloc);
495
540
  if (color.r > u_threshold) {
496
541
  color = calculateColor(color, iloc, dstep);
497
542
  gl_FragColor = applyColormap(color.r);
498
543
 
499
544
  // set the variables for the depth buffer
500
- surface_point = iloc * u_shape;
501
- surface_found = true;
545
+ frag_depth_point = iloc * u_shape;
546
+ discard_fragment = false;
502
547
 
503
548
  iter = nsteps;
504
549
  break;
@@ -508,12 +553,9 @@ _ISO_SNIPPETS = dict(
508
553
  }
509
554
  """,
510
555
  after_loop="""
511
-
512
- if (!surface_found) {
556
+ if (discard_fragment)
513
557
  discard;
514
- }
515
-
516
- """,
558
+ """,
517
559
  )
518
560
 
519
561
 
@@ -535,6 +577,19 @@ _AVG_SNIPPETS = dict(
535
577
  """,
536
578
  )
537
579
 
580
+ _INTERPOLATION_TEMPLATE = """
581
+ #include "misc/spatial-filters.frag"
582
+ vec4 texture_lookup_filtered(vec3 texcoord) {
583
+ // no need to discard out of bounds, already checked during raycasting
584
+ return %s($texture, $shape, texcoord);
585
+ }"""
586
+
587
+ _TEXTURE_LOOKUP = """
588
+ vec4 texture_lookup(vec3 texcoord) {
589
+ // no need to discard out of bounds, already checked during raycasting
590
+ return texture3D($texture, texcoord);
591
+ }"""
592
+
538
593
 
539
594
  class VolumeVisual(Visual):
540
595
  """Displays a 3D Volume
@@ -567,8 +622,16 @@ class VolumeVisual(Visual):
567
622
  gamma : float
568
623
  Gamma to use during colormap lookup. Final color will be cmap(val**gamma).
569
624
  by default: 1.
570
- interpolation : {'linear', 'nearest'}
571
- Selects method of image interpolation.
625
+ interpolation : str
626
+ Selects method of texture interpolation. Makes use of the two hardware
627
+ interpolation methods and the available interpolation methods defined
628
+ in vispy/gloo/glsl/misc/spatial_filters.frag
629
+
630
+ * 'nearest': Default, uses 'nearest' with Texture interpolation.
631
+ * 'linear': uses 'linear' with Texture interpolation.
632
+ * 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'cubic',
633
+ 'catrom', 'mitchell', 'spline16', 'spline36', 'gaussian',
634
+ 'bessel', 'sinc', 'lanczos', 'blackman'
572
635
  texture_format : numpy.dtype | str | None
573
636
  How to store data on the GPU. OpenGL allows for many different storage
574
637
  formats and schemes for the low-level texture data stored in the GPU.
@@ -615,8 +678,6 @@ class VolumeVisual(Visual):
615
678
 
616
679
  """
617
680
 
618
- _interpolation_methods = ['linear', 'nearest']
619
-
620
681
  _rendering_methods = {
621
682
  'mip': _MIP_SNIPPETS,
622
683
  'minip': _MINIP_SNIPPETS,
@@ -637,12 +698,18 @@ class VolumeVisual(Visual):
637
698
  'fragment': _FRAGMENT_SHADER,
638
699
  }
639
700
 
701
+ _func_templates = {
702
+ 'texture_lookup_interpolated': _INTERPOLATION_TEMPLATE,
703
+ 'texture_lookup': _TEXTURE_LOOKUP,
704
+ }
705
+
640
706
  def __init__(self, vol, clim="auto", method='mip', threshold=None,
641
707
  attenuation=1.0, relative_step_size=0.8, cmap='grays',
642
708
  gamma=1.0, interpolation='linear', texture_format=None,
643
709
  raycasting_mode='volume', plane_position=None,
644
710
  plane_normal=None, plane_thickness=1.0, clipping_planes=None,
645
- clipping_planes_coord_system='scene'):
711
+ clipping_planes_coord_system='scene', mip_cutoff=None,
712
+ minip_cutoff=None):
646
713
 
647
714
  tr = ['visual', 'scene', 'document', 'canvas', 'framebuffer', 'render']
648
715
  if clipping_planes_coord_system not in tr:
@@ -661,7 +728,19 @@ class VolumeVisual(Visual):
661
728
  # Create gloo objects
662
729
  self._vertices = VertexBuffer()
663
730
 
731
+ kernel, interpolation_methods = load_spatial_filters()
732
+ self._kerneltex = Texture2D(kernel, interpolation='nearest')
733
+ interpolation_methods, interpolation_fun = self._init_interpolation(
734
+ interpolation_methods)
735
+ self._interpolation_methods = interpolation_methods
736
+ self._interpolation_fun = interpolation_fun
664
737
  self._interpolation = interpolation
738
+ if self._interpolation not in self._interpolation_methods:
739
+ raise ValueError("interpolation must be one of %s" %
740
+ ', '.join(self._interpolation_methods))
741
+ self._data_lookup_fn = None
742
+ self._need_interpolation_update = True
743
+
665
744
  self._texture = self._create_texture(texture_format, vol)
666
745
  # used to store current data for later CPU-side scaling if
667
746
  # texture_format is None
@@ -685,6 +764,8 @@ class VolumeVisual(Visual):
685
764
 
686
765
  # Set params
687
766
  self.raycasting_mode = raycasting_mode
767
+ self.mip_cutoff = mip_cutoff
768
+ self.minip_cutoff = minip_cutoff
688
769
  self.method = method
689
770
  self.relative_step_size = relative_step_size
690
771
  self.threshold = threshold if threshold is not None else vol.mean()
@@ -705,17 +786,41 @@ class VolumeVisual(Visual):
705
786
 
706
787
  self.freeze()
707
788
 
789
+ def _init_interpolation(self, interpolation_methods):
790
+ # create interpolation shader functions for available
791
+ # interpolations
792
+ fun = [Function(self._func_templates['texture_lookup_interpolated'] % (n + '3D'))
793
+ for n in interpolation_methods]
794
+ interpolation_methods = [n.lower() for n in interpolation_methods]
795
+
796
+ interpolation_fun = dict(zip(interpolation_methods, fun))
797
+ interpolation_methods = tuple(sorted(interpolation_methods))
798
+
799
+ # overwrite "nearest" and "linear" spatial-filters
800
+ # with "hardware" interpolation _data_lookup_fn
801
+ hardware_lookup = Function(self._func_templates['texture_lookup'])
802
+ interpolation_fun['nearest'] = hardware_lookup
803
+ interpolation_fun['linear'] = hardware_lookup
804
+ # alias bicubic to cubic (but deprecate)
805
+ interpolation_methods = interpolation_methods + ('bicubic',)
806
+ return interpolation_methods, interpolation_fun
807
+
708
808
  def _create_texture(self, texture_format, data):
709
809
  if texture_format is not None:
710
810
  tex_cls = GPUScaledTextured3D
711
811
  else:
712
812
  tex_cls = CPUScaledTexture3D
713
813
 
814
+ if self._interpolation == 'linear':
815
+ texture_interpolation = 'linear'
816
+ else:
817
+ texture_interpolation = 'nearest'
818
+
714
819
  # clamp_to_edge means any texture coordinates outside of 0-1 should be
715
820
  # clamped to 0 and 1.
716
821
  # NOTE: This doesn't actually set the data in the texture. Only
717
822
  # creates a placeholder texture that will be resized later on.
718
- return tex_cls(data, interpolation=self._interpolation,
823
+ return tex_cls(data, interpolation=texture_interpolation,
719
824
  internalformat=texture_format,
720
825
  format='luminance',
721
826
  wrapping='clamp_to_edge')
@@ -752,11 +857,9 @@ class VolumeVisual(Visual):
752
857
  self._texture.set_clim(clim)
753
858
 
754
859
  # Apply to texture
755
- if should_cast_to_f32(vol.dtype):
756
- vol = vol.astype(np.float32)
757
860
  self._texture.check_data_format(vol)
758
861
  self._last_data = vol
759
- self._texture.scale_and_set_data(vol, copy=copy) # will be efficient if vol is same shape
862
+ self._texture.scale_and_set_data(vol, copy=copy)
760
863
  self.shared_program['clim'] = self._texture.clim_normalized
761
864
  self.shared_program['u_shape'] = (vol.shape[2], vol.shape[1],
762
865
  vol.shape[0])
@@ -767,10 +870,6 @@ class VolumeVisual(Visual):
767
870
  self._need_vertex_update = True
768
871
  self._vol_shape = shape
769
872
 
770
- @property
771
- def interpolation_methods(self):
772
- return self._interpolation_methods
773
-
774
873
  @property
775
874
  def rendering_methods(self):
776
875
  return list(self._rendering_methods)
@@ -828,56 +927,87 @@ class VolumeVisual(Visual):
828
927
  self.update()
829
928
 
830
929
  @property
831
- def interpolation(self):
832
- """The interpolation method to use
833
-
834
- Current options are:
930
+ def interpolation_methods(self):
931
+ return self._interpolation_methods
835
932
 
836
- * linear: this method is appropriate for most volumes as it creates
837
- nice looking visuals.
838
- * nearest: this method is appropriate for volumes with discrete
839
- data where additional interpolation does not make sense.
840
- """
933
+ @property
934
+ def interpolation(self):
935
+ """Get interpolation algorithm name."""
841
936
  return self._interpolation
842
937
 
843
938
  @interpolation.setter
844
- def interpolation(self, interp):
845
- if interp not in self._interpolation_methods:
846
- raise ValueError(
847
- "interpolation must be one of %s"
848
- % ', '.join(self._interpolation_methods)
849
- )
850
- if self._interpolation != interp:
851
- self._interpolation = interp
852
- self._texture.interpolation = self._interpolation
939
+ def interpolation(self, i):
940
+ if i not in self._interpolation_methods:
941
+ raise ValueError("interpolation must be one of %s" %
942
+ ', '.join(self._interpolation_methods))
943
+ if self._interpolation != i:
944
+ self._interpolation = i
945
+ self._need_interpolation_update = True
853
946
  self.update()
854
947
 
948
+ # The interpolation code could be transferred to a dedicated filter
949
+ # function in visuals/filters as discussed in #1051
950
+ def _build_interpolation(self):
951
+ """Rebuild the _data_lookup_fn for different interpolations."""
952
+ interpolation = self._interpolation
953
+ # alias bicubic to cubic
954
+ if interpolation == 'bicubic':
955
+ warnings.warn(
956
+ "'bicubic' interpolation is Deprecated. Use 'cubic' instead.",
957
+ DeprecationWarning,
958
+ stacklevel=2,
959
+ )
960
+ interpolation = 'cubic'
961
+ self._data_lookup_fn = self._interpolation_fun[interpolation]
962
+ try:
963
+ self.shared_program.frag['get_data'] = self._data_lookup_fn
964
+ except Exception as e:
965
+ print(e)
966
+
967
+ # only 'linear' uses 'linear' texture interpolation
968
+ if interpolation == 'linear':
969
+ texture_interpolation = 'linear'
970
+ else:
971
+ # 'nearest' (and also 'linear') doesn't use spatial_filters.frag
972
+ # so u_kernel and shape setting is skipped
973
+ texture_interpolation = 'nearest'
974
+ if interpolation != 'nearest':
975
+ self.shared_program['u_kernel'] = self._kerneltex
976
+ self._data_lookup_fn['shape'] = self._last_data.shape[:3][::-1]
977
+
978
+ if self._texture.interpolation != texture_interpolation:
979
+ self._texture.interpolation = texture_interpolation
980
+
981
+ self._data_lookup_fn['texture'] = self._texture
982
+
983
+ self._need_interpolation_update = False
984
+
855
985
  @staticmethod
856
986
  @lru_cache(maxsize=10)
857
- def _build_clipping_planes_func(n_planes):
987
+ def _build_clipping_planes_glsl(n_planes: int) -> str:
858
988
  """Build the code snippet used to clip the volume based on self.clipping_planes."""
859
989
  func_template = '''
860
990
  float clip_planes(vec3 loc, vec3 vol_shape) {{
861
- vec3 loc_transf = $clip_transform(vec4(loc, 1)).xyz;
862
- float distance_from_clip = 1.0;
991
+ vec3 loc_transf = $clip_transform(vec4(loc * vol_shape, 1)).xyz;
992
+ float distance_from_clip = 3.4e38; // max float
863
993
  {clips};
864
994
  return distance_from_clip;
865
995
  }}
866
996
  '''
867
997
  # the vertex is considered clipped if on the "negative" side of the plane
868
998
  clip_template = '''
869
- vec3 relative_vec{idx} = loc_transf - ( $clipping_plane_pos{idx} / vol_shape );
870
- float distance_from_clip{idx} = dot(relative_vec{idx}, ($clipping_plane_norm{idx} * vol_shape));
999
+ vec3 relative_vec{idx} = loc_transf - $clipping_plane_pos{idx};
1000
+ float distance_from_clip{idx} = dot(relative_vec{idx}, $clipping_plane_norm{idx});
871
1001
  distance_from_clip = min(distance_from_clip{idx}, distance_from_clip);
872
1002
  '''
873
1003
  all_clips = []
874
1004
  for idx in range(n_planes):
875
1005
  all_clips.append(clip_template.format(idx=idx))
876
1006
  formatted_code = func_template.format(clips=''.join(all_clips))
877
- return Function(formatted_code)
1007
+ return formatted_code
878
1008
 
879
1009
  @property
880
- def clipping_planes(self):
1010
+ def clipping_planes(self) -> np.ndarray:
881
1011
  """The set of planes used to clip the volume. Values on the negative side of the normal are discarded.
882
1012
 
883
1013
  Each plane is defined by a position and a normal vector (magnitude is irrelevant). Shape: (n_planes, 2, 3).
@@ -895,12 +1025,12 @@ class VolumeVisual(Visual):
895
1025
  return self._clipping_planes
896
1026
 
897
1027
  @clipping_planes.setter
898
- def clipping_planes(self, value):
1028
+ def clipping_planes(self, value: Optional[np.ndarray]):
899
1029
  if value is None:
900
1030
  value = np.empty([0, 2, 3])
901
1031
  self._clipping_planes = value
902
1032
 
903
- self._clip_func = self._build_clipping_planes_func(len(value))
1033
+ self._clip_func = Function(self._build_clipping_planes_glsl(len(value)))
904
1034
  self.shared_program.frag['clip_with_planes'] = self._clip_func
905
1035
 
906
1036
  self._clip_func['clip_transform'] = self._clip_transform
@@ -910,7 +1040,7 @@ class VolumeVisual(Visual):
910
1040
  self.update()
911
1041
 
912
1042
  @property
913
- def clipping_planes_coord_system(self):
1043
+ def clipping_planes_coord_system(self) -> str:
914
1044
  """
915
1045
  Coordinate system used by the clipping planes (see visuals.transforms.transform_system.py)
916
1046
  """
@@ -962,17 +1092,19 @@ class VolumeVisual(Visual):
962
1092
  (known_methods, method))
963
1093
  self._method = method
964
1094
 
965
- # $sample needs to be unset and re-set, since it's present inside the snippets.
1095
+ # $get_data needs to be unset and re-set, since it's present inside the snippets.
966
1096
  # Program should probably be able to do this automatically
967
- self.shared_program.frag['sample'] = None
1097
+ self.shared_program.frag['get_data'] = None
968
1098
  self.shared_program.frag['raycasting_setup'] = self._raycasting_setup_snippet
969
1099
  self.shared_program.frag['before_loop'] = self._before_loop_snippet
970
1100
  self.shared_program.frag['in_loop'] = self._in_loop_snippet
971
1101
  self.shared_program.frag['after_loop'] = self._after_loop_snippet
972
1102
  self.shared_program.frag['sampler_type'] = self._texture.glsl_sampler_type
973
- self.shared_program.frag['sample'] = self._texture.glsl_sample
974
1103
  self.shared_program.frag['cmap'] = Function(self._cmap.glsl_map)
975
1104
  self.shared_program['texture2D_LUT'] = self.cmap.texture_lut()
1105
+ self.shared_program['u_mip_cutoff'] = self._mip_cutoff
1106
+ self.shared_program['u_minip_cutoff'] = self._minip_cutoff
1107
+ self._need_interpolation_update = True
976
1108
  self.update()
977
1109
 
978
1110
  @property
@@ -1096,6 +1228,40 @@ class VolumeVisual(Visual):
1096
1228
  self.shared_program['u_plane_thickness'] = value
1097
1229
  self.update()
1098
1230
 
1231
+ @property
1232
+ def mip_cutoff(self):
1233
+ """The lower cutoff value for `mip` and `attenuated_mip`.
1234
+
1235
+ When using the `mip` or `attenuated_mip` rendering methods, fragments
1236
+ with values below the cutoff will be discarded.
1237
+ """
1238
+ return self._mip_cutoff
1239
+
1240
+ @mip_cutoff.setter
1241
+ def mip_cutoff(self, value):
1242
+ if value is None:
1243
+ value = np.finfo('float32').min
1244
+ self._mip_cutoff = float(value)
1245
+ self.shared_program['u_mip_cutoff'] = self._mip_cutoff
1246
+ self.update()
1247
+
1248
+ @property
1249
+ def minip_cutoff(self):
1250
+ """The upper cutoff value for `minip`.
1251
+
1252
+ When using the `minip` rendering method, fragments
1253
+ with values above the cutoff will be discarded.
1254
+ """
1255
+ return self._minip_cutoff
1256
+
1257
+ @minip_cutoff.setter
1258
+ def minip_cutoff(self, value):
1259
+ if value is None:
1260
+ value = np.finfo('float32').max
1261
+ self._minip_cutoff = float(value)
1262
+ self.shared_program['u_minip_cutoff'] = self._minip_cutoff
1263
+ self.update()
1264
+
1099
1265
  def _create_vertex_data(self):
1100
1266
  """Create and set positions and texture coords from the given shape
1101
1267
 
@@ -1164,3 +1330,6 @@ class VolumeVisual(Visual):
1164
1330
  def _prepare_draw(self, view):
1165
1331
  if self._need_vertex_update:
1166
1332
  self._create_vertex_data()
1333
+
1334
+ if self._need_interpolation_update:
1335
+ self._build_interpolation()