Skip to content

Commit a2476b7

Browse files
authored
Merge pull request #1290 from maharshi-gor/analyze-snapshot-tests
MNT: Added analyze-snapshot test cases.
2 parents 8f0a11f + fff84d1 commit a2476b7

17 files changed

Lines changed: 504 additions & 296 deletions

docs/examples/viz_solar_system.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,4 @@ def update_playback_logic(show_manager_obj=None):
279279

280280
showm.start()
281281

282-
showm.snapshot("viz_solar_system_animation.png")
282+
showm.snapshot(fname="viz_solar_system_animation.png")

fury/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ from .colormap import (
154154
_xyz2rgb as _xyz2rgb,
155155
boys2rgb as boys2rgb,
156156
cc as cc,
157+
colors_to_uint8 as colors_to_uint8,
157158
# colormap_lookup_table as colormap_lookup_table,
158159
create_colormap as create_colormap,
159160
distinguishable_colormap as distinguishable_colormap,

fury/actor/tests/_helpers.py

Lines changed: 144 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,122 @@
66

77
from fury import actor, window
88

9+
# --- Shared real inputs for visibility/snapshot tests -----------------------
10+
11+
CENTERS = np.array([[0.0, 0.0, 0.0]], dtype=np.float32)
12+
LINES = [np.array([[0, 0, 0], [1, 1, 1], [2, 0, 0]], dtype=np.float32)]
13+
14+
15+
def gradient_image(n=64):
16+
"""A non-uniform 2D image so the rendered frame is never flat."""
17+
ramp = np.linspace(0.0, 1.0, n, dtype=np.float32)
18+
return np.outer(ramp, ramp)
19+
20+
21+
def scalar_volume(n=32):
22+
"""A non-uniform 3D scalar volume (deterministic)."""
23+
x, y, z = np.mgrid[0:n, 0:n, 0:n]
24+
return ((x + y + z) % 17).astype(np.float32)
25+
26+
27+
def roi_volume(n=20):
28+
"""A binary volume with a solid central cube (for contour actors)."""
29+
data = np.zeros((n, n, n), dtype=np.float32)
30+
data[5:15, 5:15, 5:15] = 1.0
31+
return data
32+
33+
34+
def label_volume(n=20):
35+
"""A labeled volume with one nonzero region."""
36+
data = np.zeros((n, n, n), dtype=int)
37+
data[5:15, 5:15, 5:15] = 1
38+
return data
39+
40+
41+
def peak_dirs(n=8):
42+
"""A (X, Y, Z, 3, 3) field of unit peak directions."""
43+
dirs = np.zeros((n, n, n, 3, 3), dtype=np.float32)
44+
dirs[..., 0, :] = (1.0, 0.0, 0.0)
45+
dirs[..., 1, :] = (0.0, 1.0, 0.0)
46+
dirs[..., 2, :] = (0.0, 0.0, 1.0)
47+
return dirs
48+
49+
50+
def vector_field_data(n=8):
51+
"""A (X, Y, Z, 3) field of unit vectors."""
52+
field = np.zeros((n, n, n, 3), dtype=np.float32)
53+
field[...] = (1.0, 0.0, 0.0)
54+
return field
55+
56+
57+
def sph_coeffs(n=4, ncoeff=15):
58+
"""SH coefficients with only the l=0 term -> isotropic visible glyphs."""
59+
coeffs = np.zeros((n, n, n, ncoeff), dtype=np.float32)
60+
coeffs[..., 0] = 1.0
61+
return coeffs
62+
63+
64+
def triangle_mesh():
65+
"""A single-triangle (vertices, faces) pair for surface actors."""
66+
verts = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]], dtype=np.float32)
67+
faces = np.array([[0, 1, 2]], dtype=np.int32)
68+
return verts, faces
69+
70+
71+
# The full set of renderable actors keyed by id. Each value is a factory that
72+
# builds a fresh actor (with real inputs) on every call.
73+
ACTOR_FACTORIES = {
74+
# Planar / polyhedron / curved mesh primitives (centers only).
75+
"square": lambda: actor.square(CENTERS),
76+
"triangle": lambda: actor.triangle(CENTERS),
77+
"star": lambda: actor.star(CENTERS),
78+
"disk": lambda: actor.disk(CENTERS),
79+
"ring": lambda: actor.ring(CENTERS),
80+
"box": lambda: actor.box(CENTERS),
81+
"tetrahedron": lambda: actor.tetrahedron(CENTERS),
82+
"icosahedron": lambda: actor.icosahedron(CENTERS),
83+
"triangularprism": lambda: actor.triangularprism(CENTERS),
84+
"pentagonalprism": lambda: actor.pentagonalprism(CENTERS),
85+
"octagonalprism": lambda: actor.octagonalprism(CENTERS),
86+
"rhombicuboctahedron": lambda: actor.rhombicuboctahedron(CENTERS),
87+
"frustum": lambda: actor.frustum(CENTERS),
88+
"superquadric": lambda: actor.superquadric(CENTERS),
89+
"cylinder": lambda: actor.cylinder(CENTERS),
90+
"cone": lambda: actor.cone(CENTERS),
91+
"arrow": lambda: actor.arrow(CENTERS),
92+
"sphere": lambda: actor.sphere(CENTERS),
93+
"ellipsoid": lambda: actor.ellipsoid(CENTERS),
94+
# Points / billboards.
95+
"point": lambda: actor.point(CENTERS),
96+
"marker": lambda: actor.marker(CENTERS),
97+
"billboard": lambda: actor.billboard(CENTERS),
98+
"billboard_sphere": lambda: actor.billboard_sphere(CENTERS),
99+
# Axes (no required args).
100+
"axes": lambda: actor.axes(),
101+
# Text / image.
102+
"text": lambda: actor.text("FURY"),
103+
"image": lambda: actor.image(gradient_image()),
104+
# Line / streamline family.
105+
"line": lambda: actor.line(LINES),
106+
"streamlines": lambda: actor.streamlines(LINES),
107+
"streamtube": lambda: actor.streamtube(LINES),
108+
"line_projection": lambda: actor.line_projection(LINES),
109+
# Surface.
110+
"surface": lambda: actor.surface(*triangle_mesh()),
111+
# Volume / field / glyph slicers (return single actors or groups; both are
112+
# hidden by setting ``.visible`` on the returned object).
113+
"data_slicer": lambda: actor.data_slicer(scalar_volume()),
114+
"volume_slicer": lambda: actor.volume_slicer(scalar_volume()),
115+
"peaks_slicer": lambda: actor.peaks_slicer(peak_dirs()),
116+
"vector_field": lambda: actor.vector_field(vector_field_data()),
117+
"vector_field_slicer": lambda: actor.vector_field_slicer(vector_field_data()),
118+
"sph_glyph": lambda: actor.sph_glyph(sph_coeffs()),
119+
# Contours (need a solid block to produce geometry).
120+
"contour_from_volume": lambda: actor.contour_from_volume(roi_volume()),
121+
"contour_from_roi": lambda: actor.contour_from_roi(roi_volume()),
122+
"contour_from_label": lambda: actor.contour_from_label(label_volume()),
123+
}
124+
9125

10126
def random_png(width, height):
11127
"""
@@ -55,43 +171,42 @@ def validate_actors(actor_type="actor_name", prim_count=1, **kwargs):
55171
if actor_type == "line":
56172
return
57173

58-
fname = f"{actor_type}_test.png"
59-
window.snapshot(scene=scene, fname=fname)
60-
61-
img = Image.open(fname)
62-
img_array = np.array(img)
63-
64-
mean_r, mean_g, mean_b, _mean_a = np.mean(
65-
img_array.reshape(-1, img_array.shape[2]), axis=0
66-
)
174+
# Default material: the actor renders and red dominates the image.
175+
arr = window.snapshot(scene=scene, fname=None, return_array=True)
176+
report = window.analyze_snapshot(arr, find_objects=True)
177+
assert report.objects >= 1
67178

179+
mean_r, mean_g, mean_b, _mean_a = np.mean(arr.reshape(-1, arr.shape[2]), axis=0)
68180
assert mean_r > mean_b and mean_r > mean_g
181+
assert 0 < mean_r < 255
69182

70-
middle_pixel = img_array[img_array.shape[0] // 2, img_array.shape[1] // 2]
71-
r, g, b, a = middle_pixel
72-
assert r > g and r > b
73-
assert g == b
183+
# Hidden: nothing renders.
184+
get_actor.visible = False
185+
report = window.analyze_snapshot(
186+
window.snapshot(scene=scene, fname=None, return_array=True), find_objects=True
187+
)
188+
assert report.objects == 0
74189
scene.remove(get_actor)
75190

191+
# Basic (flat) material: exact red is present, then absent once hidden.
76192
typ_actor_1 = getattr(actor, actor_type)
77193
get_actor_1 = typ_actor_1(**{**kwargs, "material": "basic"})
78194
scene.add(get_actor_1)
79-
fname_1 = f"{actor_type}_test_1.png"
80-
window.snapshot(scene=scene, fname=fname_1)
81-
img = Image.open(fname_1)
82-
img_array = np.array(img)
83195

84-
mean_r, mean_g, mean_b, _mean_a = np.mean(
85-
img_array.reshape(-1, img_array.shape[2]), axis=0
196+
report = window.analyze_snapshot(
197+
window.snapshot(scene=scene, fname=None, return_array=True),
198+
colors=(255, 0, 0),
199+
find_objects=True,
86200
)
87-
88-
assert mean_r > mean_b and mean_r > mean_g
89-
assert 0 < mean_r < 255
90-
assert mean_g == 0 and mean_b == 0
91-
92-
middle_pixel = img_array[img_array.shape[0] // 2, img_array.shape[1] // 2]
93-
r, g, b, a = middle_pixel
94-
assert r > g and r > b
95-
assert g == 0 and b == 0
96-
assert r == 255
201+
assert report.objects >= 1
202+
assert report.colors_found == [True]
203+
204+
get_actor_1.visible = False
205+
report = window.analyze_snapshot(
206+
window.snapshot(scene=scene, fname=None, return_array=True),
207+
colors=(255, 0, 0),
208+
find_objects=True,
209+
)
210+
assert report.objects == 0
211+
assert report.colors_found == [False]
97212
scene.remove(get_actor_1)

fury/actor/tests/test_billboard.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@
44
Simple tests for billboard creation and basic rendering functionality.
55
"""
66

7-
from __future__ import annotations
8-
9-
import os
10-
import tempfile
11-
127
import numpy as np
138
import numpy.testing as npt
149

@@ -72,9 +67,7 @@ def test_basic_billboard(interactive: bool = False):
7267
window.show([bb_render])
7368

7469
# Test snapshot creation
75-
tmp_fd, tmp_file = tempfile.mkstemp(suffix="_bb.png")
76-
os.close(tmp_fd)
77-
arr = window.snapshot(scene=scene, fname=tmp_file, return_array=True)
70+
arr = window.snapshot(scene=scene, fname=None, return_array=True)
7871

7972
# Basic checks: array exists and has expected shape
8073
assert arr is not None
@@ -85,8 +78,6 @@ def test_basic_billboard(interactive: bool = False):
8578
_assert_red_visible(arr)
8679

8780
scene.clear()
88-
if tmp_file and os.path.exists(tmp_file):
89-
os.remove(tmp_file)
9081

9182

9283
def test_billboard_camera_facing():
@@ -101,9 +92,7 @@ def test_billboard_camera_facing():
10192
scene.add(bb)
10293

10394
# Test from default camera position
104-
tmp_fd1, tmp_file1 = tempfile.mkstemp(suffix="_bb_front.png")
105-
os.close(tmp_fd1)
106-
arr1 = window.snapshot(scene=scene, fname=tmp_file1, return_array=True)
95+
arr1 = window.snapshot(scene=scene, fname=None, return_array=True)
10796

10897
# Billboard should be visible from front view
10998
data1 = np.asarray(arr1)
@@ -119,11 +108,7 @@ def test_billboard_camera_facing():
119108
assert arr1 is not None
120109
assert green_pixels1 > 0, "Billboard should be visible with green pixels"
121110

122-
# Cleanup
123111
scene.clear()
124-
for tmp_file in [tmp_file1]:
125-
if tmp_file and os.path.exists(tmp_file):
126-
os.remove(tmp_file)
127112

128113

129114
def test_rectangular_billboards():
@@ -200,8 +185,7 @@ def test_billboard_sphere(interactive: bool = False):
200185
if interactive: # pragma: no cover
201186
window.show(scene)
202187

203-
tmp_file = tempfile.mktemp(suffix="_bbs.png")
204-
arr = window.snapshot(scene=scene, fname=tmp_file, return_array=True)
188+
arr = window.snapshot(scene=scene, fname=None, return_array=True)
205189
assert arr is not None
206190
_assert_red_visible(arr)
207191

@@ -212,8 +196,6 @@ def test_billboard_sphere(interactive: bool = False):
212196
assert positive_red.max() > positive_red.min()
213197

214198
scene.clear()
215-
if tmp_file and os.path.exists(tmp_file):
216-
os.remove(tmp_file)
217199

218200

219201
def _assert_red_visible(image):
@@ -276,7 +258,7 @@ def test_billboard_bounding_box_camera_framing():
276258
)
277259
scene.add(bb)
278260

279-
arr = window.snapshot(scene=scene, return_array=True)
261+
arr = window.snapshot(scene=scene, fname=None, return_array=True)
280262
assert arr is not None
281263

282264
_assert_red_visible(arr)

fury/actor/tests/test_core.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from PIL import Image
21
import numpy as np
32
import pytest
43

@@ -181,13 +180,10 @@ def test_axes():
181180

182181
assert axes_actor.prim_count == 3
183182

184-
fname = "axes_test.png"
185-
window.snapshot(scene=scene, fname=fname)
186-
img = Image.open(fname)
187-
img_array = np.array(img)
188-
mean_r, mean_g, mean_b, _mean_a = np.mean(
189-
img_array.reshape(-1, img_array.shape[2]), axis=0
190-
)
183+
arr = window.snapshot(scene=scene, fname=None, return_array=True)
184+
report = window.analyze_snapshot(arr, find_objects=True)
185+
assert report.objects >= 1
186+
mean_r, mean_g, mean_b, _mean_a = np.mean(arr.reshape(-1, arr.shape[2]), axis=0)
191187
assert np.isclose(mean_r, mean_g, atol=0.02)
192188
assert 0 < mean_r < 255
193189
assert 0 < mean_g < 255
@@ -264,9 +260,7 @@ def test_actor_from_primitive_transparency_visual(sphere_prim):
264260

265261
opaque = actor_from_primitive(vertices, faces, centers, colors=colors)
266262
scene.add(opaque)
267-
fname = "transparency_opaque_test.png"
268-
window.snapshot(scene=scene, fname=fname)
269-
img_array_op = np.array(Image.open(fname))
263+
img_array_op = window.snapshot(scene=scene, fname=None, return_array=True)
270264
mid = img_array_op[img_array_op.shape[0] // 2, img_array_op.shape[1] // 2]
271265
assert mid[0] > mid[1] and mid[0] > mid[2]
272266
scene.remove(opaque)
@@ -275,9 +269,7 @@ def test_actor_from_primitive_transparency_visual(sphere_prim):
275269
vertices, faces, centers, colors=colors, opacity=0.5
276270
)
277271
scene.add(transparent)
278-
fname = "transparency_semi_test.png"
279-
window.snapshot(scene=scene, fname=fname)
280-
img_array_tr = np.array(Image.open(fname))
272+
img_array_tr = window.snapshot(scene=scene, fname=None, return_array=True)
281273
mean_r_tr, mean_g_tr, mean_b_tr, _ = np.mean(
282274
img_array_tr.reshape(-1, img_array_tr.shape[2]), axis=0
283275
)

fury/actor/tests/test_curved.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import math
22

3-
from PIL import Image
43
import numpy as np
54
import pytest
65

@@ -78,18 +77,14 @@ def test_streamtube():
7877
tube_actor = actor.streamtube(lines=lines, colors=colors)
7978
scene.add(tube_actor)
8079

81-
fname = "streamtube_test.png"
82-
window.snapshot(scene=scene, fname=fname)
83-
img = Image.open(fname)
84-
img_array = np.array(img)
85-
86-
mean_r, mean_g, mean_b, _ = np.mean(
87-
img_array.reshape(-1, img_array.shape[2]), axis=0
88-
)
80+
arr = window.snapshot(scene=scene, fname=None, return_array=True)
81+
report = window.analyze_snapshot(arr, find_objects=True)
82+
assert report.objects >= 1
8983

84+
mean_r, mean_g, mean_b, _ = np.mean(arr.reshape(-1, arr.shape[2]), axis=0)
9085
assert mean_r > mean_g and mean_r > mean_b
9186

92-
middle_pixel = img_array[img_array.shape[0] // 2, img_array.shape[1] // 2]
87+
middle_pixel = arr[arr.shape[0] // 2, arr.shape[1] // 2]
9388
r, g, b, a = middle_pixel
9489
assert r > g and r > b
9590

0 commit comments

Comments
 (0)