1212import argparse
1313import sys
1414import os
15- from skimage .io import imread
15+ import numpy as np
16+ from base64 import b64encode
17+ from skimage .io import imread , imsave
1618from skimage .metrics import structural_similarity
19+ from skimage .color import rgb2gray , gray2rgb
20+ from plotly .subplots import make_subplots
21+ import plotly .graph_objects as go
22+
23+ def compare_images (
24+ baseline_file : str ,
25+ output_file : str ,
26+ expect_fail : bool = False ,
27+ CUTOFF_SSIM : float = 0.999
28+ ) -> bool :
1729
18- # Below are key commands that are passed to the -keys command-line argument for
19- # glvis in order to perform testing on raw mesh/grid function data (i.e. non-
20- # streams).
21- #
22- # Currently not in use.
23- test_cases = {
24- "magnify" : "*****" ,
25- "axes1" : "a" ,
26- "axes2" : "aa" ,
27- "mesh1" : "m" ,
28- "mesh2" : "mm" ,
29- "cut_plane" : "i" ,
30- "cut_plane_rotate" : "iyyyy" ,
31- "cut_plane_rotate_back" : "iyyyyYYYY" ,
32- "cut_plane_transl" : "izzzz" ,
33- "cut_plane_transl_back" : "izzzzZZZZ" ,
34- "orient2d_1" : "R" ,
35- "orient2d_2" : "RR" ,
36- "orient2d_3" : "RRR" ,
37- "orient2d_4" : "RRRR" ,
38- "orient2d_5" : "RRRRR" ,
39- "orient2d_6" : "RRRRRR" ,
40- "orient3d" : "Rr" ,
41- "perspective" : "j" ,
42- }
43-
44- screenshot_keys = "Sq"
45- screenshot_file = "GLVis_s01.png"
46-
47- cutoff_ssim = 0.999
48-
49- def compare_images (baseline_file , output_file , expect_fail = False ):
5030 # Try to open output image
5131 output_img = imread (output_file )
5232 if output_img is None :
@@ -62,7 +42,7 @@ def compare_images(baseline_file, output_file, expect_fail=False):
6242 # Compare images with SSIM metrics. For two exactly-equal images, SSIM=1.0.
6343 # We set a cutoff of 0.999 to account for possible differences in rendering.
6444 ssim = structural_similarity (baseline_img , output_img , channel_axis = 2 )
65- if ssim < cutoff_ssim :
45+ if ssim < CUTOFF_SSIM :
6646 if expect_fail :
6747 print ("[PASS] Differences were detected in the control case." )
6848 else :
@@ -72,92 +52,120 @@ def compare_images(baseline_file, output_file, expect_fail=False):
7252 print ("[FAIL] Differences were not detected in the control case." )
7353 else :
7454 print ("[PASS] Images match." )
75- print (" actual ssim = {}, cutoff = {}" .format (ssim , cutoff_ssim ))
76- return ssim >= cutoff_ssim if not expect_fail else ssim < cutoff_ssim
77-
78- # Function to test a given glvis command with a variety of key-based commands.
79- # Not currently in use.
80- def test_case (exec_path , exec_args , baseline , t_group , t_name , cmd ):
81- print ("Testing {0}:{1}..." .format (t_group , t_name ))
82- full_screenshot_cmd = cmd + screenshot_keys
83- cmd = "{0} {1} -k \" {2}\" " .format (exec_path , exec_args , full_screenshot_cmd )
84- print ("Exec: {}" .format (cmd ))
85- ret = os .system (cmd + " > /dev/null 2>&1" )
86- if ret != 0 :
87- print ("[FAIL] GLVis exited with error code {}." .format (ret ))
88- return False
89- if not os .path .exists (t_group ):
90- os .mkdir (t_group )
91- output_name = "{0}/{1}.png" .format (t_group , t_name )
55+ print (f" actual ssim = { ssim } , cutoff = { CUTOFF_SSIM } " )
56+ return ssim >= CUTOFF_SSIM if not expect_fail else ssim < CUTOFF_SSIM
57+
58+ def color_distance (I1 : np .array , I2 : np .array ) -> dict [str , np .array ]:
59+ """
60+ L2-norm in rgb space. There are better ways but this is probably good enough.
61+ """
62+ NORM_CONSTANT = (3 * (255 ** 2 ))** 0.5 # max distance
63+ l2norm = lambda x : np .linalg .norm (x , ord = 2 , axis = 2 )
64+ delta = l2norm (I2 .astype (int )- I1 .astype (int )) / NORM_CONSTANT # output is NxM [0,1]
65+ # now we scale to [0,255] and cast as uint8 so it is a "proper" image
66+ Idiff_abs = (delta * 255 ).astype (np .uint8 )
67+ # get relative version
68+ Idiff_rel = (Idiff_abs / Idiff_abs .max () * 255 ).astype (np .uint8 )
69+ return {'abs' : Idiff_abs ,
70+ 'rel' : Idiff_rel ,}
71+
72+ def generate_image_diffs (
73+ image1_filename : str ,
74+ image2_filename : str ,
75+ absdiff_filename : str ,
76+ reldiff_filename : str ,
77+ ) -> None :
78+ # Images are read as NxMx3 [uint8] arrays from [0,255]
79+ I1 = imread (image1_filename )
80+ I2 = imread (image2_filename )
81+ # Get the image diffs (abs and rel)
82+ Idiffs = color_distance (I1 , I2 ) # output is NxM [0,1]
83+ # Save 3-channel image to file
84+ imsave (absdiff_filename , gray2rgb (Idiffs ['abs' ]))
85+ imsave (reldiff_filename , gray2rgb (Idiffs ['rel' ]))
86+
87+ # For the source= argument in plotly
88+ def _get_image_src (filename ):
89+ with open (filename , "rb" ) as f :
90+ image_bytes = b64encode (f .read ()).decode ()
91+ return f"data:image/png;base64,{ image_bytes } "
92+
93+ def image_comparison_plot (
94+ image_filenames : list [str ],
95+ image_names : list [str ], # for subtitles
96+ output_filename : str ,
97+ ):
98+ """
99+ Illustrate results as an interactive plotly figure (html)
100+ """
101+ assert len (image_filenames ) == len (image_names )
102+ n = len (image_filenames )
103+ fig = make_subplots (rows = 1 , cols = n ,
104+ shared_xaxes = True ,
105+ shared_yaxes = True ,
106+ subplot_titles = image_names )
107+ for idx , filename in enumerate (image_filenames ):
108+ fig .add_trace (go .Image (source = _get_image_src (filename )), 1 , idx + 1 )
109+ fig .update_xaxes (matches = 'x' , showticklabels = False , showgrid = False , zeroline = False )
110+ fig .update_yaxes (matches = 'y' , showticklabels = False , showgrid = False , zeroline = False )
111+ fig .write_html (output_filename , include_plotlyjs = 'cdn' )
112+
113+ def test_stream (
114+ exec_path : str ,
115+ exec_args : str ,
116+ save_file : str ,
117+ baseline : str
118+ ) -> bool :
92119
93- ret = os .system ("mv {0} {1}" .format (screenshot_file , output_name ))
94- if ret != 0 :
95- print ("[FAIL] Could not move output image: exit code {}." .format (ret ))
96- return False
97-
98- if baseline :
99- baseline_name = "{0}/test.{1}.png" .format (baseline , test_name )
100- return compare_images (baseline_name , output_name )
101- else :
102- print ("[IGNORE] No baseline exists to compare against." )
103- return True
104-
105- def test_stream (exec_path , exec_args , save_file , baseline ):
106120 if exec_args is None :
107121 exec_args = ""
108- test_name = os .path .basename (save_file )
109- print ("Testing {}..." .format (save_file ))
122+ print (f"Testing { save_file } ..." )
123+ test_name = os .path .basename (save_file ).replace (".saved" , "" ) # e.g. "ex3"
124+ output_dir = f"outputs/{ test_name } "
125+ os .makedirs (output_dir , exist_ok = True )
110126
111127 # Create new stream file with command to screenshot and close
112128 stream_data = None
113129 with open (save_file ) as in_f :
114130 stream_data = in_f .read ()
115131
116- output_name = "test.{}.png" .format (test_name )
117- output_name_fail = "test.fail.{}.png" .format (test_name )
132+ output_name = f"{ output_dir } /test.nominal.{ test_name } .png"
133+ output_name_fail = f"{ output_dir } /test.zoom.{ test_name } .png"
134+ absdiff_name = f"{ output_dir } /test.nominal.absdiff.{ test_name } .png"
135+ reldiff_name = f"{ output_dir } /test.nominal.reldiff.{ test_name } .png"
118136 tmp_file = "test.saved"
119137 with open (tmp_file , 'w' ) as out_f :
120138 out_f .write (stream_data )
121139 out_f .write ("\n window_size 800 600" )
122- out_f .write ("\n screenshot {}" . format ( output_name ) )
140+ out_f .write (f "\n screenshot { output_name } " )
123141 # Zooming in should create some difference in the images
124142 out_f .write ("\n keys *" )
125- out_f .write ("\n screenshot {}" . format ( output_name_fail ) )
143+ out_f .write (f "\n screenshot { output_name_fail } " )
126144 out_f .write ("\n keys q" )
127145
128146 # Run GLVis with modified stream file
129- cmd = "{0 } {1 } -saved {2}" . format ( exec_path , exec_args , tmp_file )
130- print ("Exec: {}" . format ( cmd ) )
147+ cmd = f" { exec_path } { exec_args } -saved { tmp_file } "
148+ print (f "Exec: { cmd } " )
131149 ret = os .system (cmd )
132150 if ret != 0 :
133- print ("[FAIL] GLVis exited with error code {}." . format ( ret ) )
151+ print (f "[FAIL] GLVis exited with error code { ret } ." )
134152 return False
135153
136154 if baseline :
137- baseline_name = "{0 }/test.{1}. png". format ( baseline , test_name )
155+ baseline_name = f" { baseline } /test.{ test_name } .saved. png"
138156 test_baseline = compare_images (baseline_name , output_name )
139- test_control = compare_images (baseline_name , output_name_fail ,
140- expect_fail = True )
157+ generate_image_diffs (baseline_name , output_name , absdiff_name , reldiff_name )
158+ # Generate an interactive html plot, only if the test fails
159+ # if not test_baseline:
160+ image_comparison_plot ([baseline_name , output_name , reldiff_name ],
161+ ["Baseline" , "Test Output" , "Normalized Diff" ],
162+ reldiff_name .replace (".png" , ".html" ))
163+ test_control = compare_images (baseline_name , output_name_fail , expect_fail = True )
141164 return (test_baseline and test_control )
142165 else :
143166 print ("[IGNORE] No baseline exists to compare against." )
144167 return True
145168
146- def test_cmd (exec_path , exec_args , tgroup , baseline ):
147- try :
148- os .remove (screenshot_file )
149- except OSError :
150- pass
151- all_tests_passed = True
152- for testname , cmds in test_cases .items ():
153- result = test_case (exec_path , exec_args , baseline , tgroup , testname , cmds )
154- all_tests_passed = all_tests_passed and result
155-
156- if all_tests_passed :
157- print ("All tests passed." )
158- else :
159- sys .exit (1 )
160-
161169if __name__ == "__main__" :
162170 parser = argparse .ArgumentParser ()
163171 parser .add_argument ("-s" , "--save_stream" , help = "Path to a GLVis saved stream file." )
@@ -166,9 +174,13 @@ def test_cmd(exec_path, exec_args, tgroup, baseline):
166174 parser .add_argument ("-n" , "--group_name" , help = "Name of the test group." )
167175 parser .add_argument ("-b" , "--baseline" , help = "Path to test baseline." )
168176 args = parser .parse_args ()
177+
178+ # Make a directory for storing test outputs
179+ os .makedirs ("outputs" , exist_ok = True )
180+ # Run tests
169181 if args .save_stream is not None :
170182 result = test_stream (args .exec_cmd , args .exec_args , args .save_stream , args .baseline )
171183 if not result :
172184 sys .exit (1 )
173185 else :
174- test_cmd ( args . exec_cmd , args . exec_args , args . group_name , args . baseline )
186+ raise Exception ( "--save_stream must be specified. test_cmd() is unused. Import from `test_cmd.py`" )
0 commit comments