Skip to content

Commit 4f4d92d

Browse files
committed
renderdiff: add viewer for image differences
- Modify the compare script to output more details of a comparison. This will include the source/golden directory, the comparison directory (the new renderings), and a file path to difference images if the golden does not match the rendered image. - The image_diff script can now output a TIFF that is the difference of two input TIFFs. - Add a viewer for examining the differences between rendered output and golden images. - The viewer consists of a simple server of web API endpoints for querying difference results (along with rendered images in TIFF). - And a web-based (html + lit-element) UI for looking at the rendered images and differences.
1 parent 361ba2a commit 4f4d92d

File tree

11 files changed

+821
-37
lines changed

11 files changed

+821
-37
lines changed

test/renderdiff/generate.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ function start_render_() {
3434
fi
3535
done
3636
fi
37-
mkdir -p ${OUTPUT_DIR}
3837
CXX=`which clang++` CC=`which clang` ./build.sh -f -X ${MESA_DIR} -p desktop debug gltf_viewer
3938
}
4039

@@ -54,6 +53,6 @@ start_render_ && \
5453
python3 ${RENDERDIFF_TEST_DIR}/src/render.py \
5554
--gltf_viewer="$(pwd)/out/cmake-debug/samples/gltf_viewer" \
5655
--test=${RENDERDIFF_TEST_DIR}/tests/presubmit.json \
57-
--output_dir=${OUTPUT_DIR} \
56+
--output_dir=${RENDER_OUTPUT_DIR} \
5857
--opengl_lib=${MESA_LIB_DIR} && \
5958
end_render_

test/renderdiff/src/compare.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,54 @@
44
import pprint
55
import json
66

7-
from utils import execute, ArgParseImpl, important_print
8-
from image_diff import same_image
7+
from utils import execute, ArgParseImpl, important_print, mkdir_p
8+
from image_diff import same_image, output_image_diff
99
from results import RESULT_OK, RESULT_FAILED, RESULT_MISSING
1010

11-
def _compare_goldens(base_dir, comparison_dir):
12-
render_results = {}
13-
base_files = glob.glob(os.path.join(base_dir, "./**/*.tif"))
14-
for golden_file in base_files:
15-
base_fname = os.path.abspath(golden_file)
16-
test_case = base_fname.replace(f'{os.path.abspath(base_dir)}/', '')
17-
comp_fname = os.path.abspath(os.path.join(comparison_dir, test_case))
18-
if not os.path.exists(comp_fname):
19-
print(f'file name not found: {comp_fname}')
20-
render_results[test_case] = RESULT_MISSING
21-
continue
22-
if not same_image(base_fname, comp_fname):
23-
render_results[test_case] = RESULT_FAILED
24-
else:
25-
render_results[test_case] = RESULT_OK
26-
return render_results
11+
def _compare_goldens(base_dir, comparison_dir, out_dir=None):
12+
all_files = glob.glob(os.path.join(base_dir, "./**/*.tif"), recursive=True)
13+
test_dirs = set(os.path.abspath(os.path.dirname(f)).replace(os.path.abspath(base_dir) + '/', '') \
14+
for f in all_files)
15+
all_results = []
16+
for test_dir in test_dirs:
17+
results_meta = {}
18+
results = []
19+
output_test_dir = None if not out_dir else os.path.join(out_dir, test_dir)
20+
if output_test_dir:
21+
mkdir_p(output_test_dir)
22+
base_test_dir = os.path.abspath(os.path.join(base_dir, test_dir))
23+
comp_test_dir = os.path.abspath(os.path.join(comparison_dir, test_dir))
24+
results_meta['base_dir'] = base_test_dir
25+
results_meta['comparison_dir'] = comp_test_dir
26+
for golden_file in \
27+
glob.glob(os.path.join(base_test_dir, "*.tif")):
28+
base_fname = os.path.abspath(golden_file)
29+
test_case = base_fname.replace(f'{base_test_dir}/', '')
30+
comp_fname = os.path.join(comp_test_dir, test_case)
31+
result = {
32+
'name': test_case,
33+
}
34+
if not os.path.exists(comp_fname):
35+
print(f'file name not found: {comp_fname}')
36+
result['result'] = RESULT_MISSING
37+
elif not same_image(base_fname, comp_fname):
38+
result['result'] = RESULT_FAILED
39+
if output_test_dir:
40+
# just the file name
41+
diff_fname = f"{test_case.replace('.tif', '_diff.tif')}"
42+
output_image_diff(base_fname, comp_fname, os.path.join(output_test_dir, diff_fname))
43+
result['diff'] = diff_fname
44+
else:
45+
result['result'] = RESULT_OK
46+
results.append(result)
47+
if output_test_dir:
48+
results_meta['results'] = results
49+
output_fname = os.path.join(output_test_dir, "compare_results.json")
50+
with open(output_fname, 'w') as f:
51+
f.write(json.dumps(results_meta, indent=2))
52+
important_print(f'Written comparison results for {test_dir} to \n {output_fname}')
53+
all_results += results
54+
return all_results
2755

2856
if __name__ == '__main__':
2957
parser = ArgParseImpl()
@@ -39,14 +67,9 @@ def _compare_goldens(base_dir, comparison_dir):
3967
dest = os.path.join(os.getcwd(), './out/renderdiff_tests')
4068
assert os.path.exists(dest), f"Destination folder={dest} does not exist."
4169

42-
results = _compare_goldens(args.src, dest)
70+
results = _compare_goldens(args.src, dest, out_dir=args.out)
4371

44-
if args.out:
45-
assert os.path.exists(arg.out), f"Output folder={dest} does not exist."
46-
with open(os.path.join(args.out, "compare_results.json", 'w')) as f:
47-
f.write(json.dumps(results))
48-
49-
failed = [f" {k}" for k in results.keys() if results[k] != RESULT_OK]
72+
failed = [f" {k['name']}" for k in results if k['result'] != RESULT_OK]
5073
success_count = len(results) - len(failed)
5174
important_print(f'Successfully compared {success_count} / {len(results)} images' +
5275
('\nFailed:\n' + ('\n'.join(failed)) if len(failed) > 0 else ''))

test/renderdiff/src/image_diff.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
import tifffile
16-
import numpy
16+
import numpy as np
1717

1818
def same_image(tiff_file_a, tiff_file_b):
1919
try:
@@ -27,7 +27,7 @@ def same_image(tiff_file_a, tiff_file_b):
2727
return False
2828

2929
# numpy.array_equal() checks if two arrays have the same shape and elements.
30-
if numpy.array_equal(img1_data, img2_data):
30+
if np.array_equal(img1_data, img2_data):
3131
return True
3232
else:
3333
return False
@@ -41,3 +41,27 @@ def same_image(tiff_file_a, tiff_file_b):
4141
except Exception as e:
4242
print(f"An unexpected error occurred: {e}")
4343
return False
44+
45+
def output_image_diff(tiff_file_a, tiff_file_b, output_path):
46+
try:
47+
img1_data = tifffile.imread(tiff_file_a)
48+
img2_data = tifffile.imread(tiff_file_b)
49+
50+
# If the dimensions (height, width, number of channels, number of pages/frames)
51+
# are different, the images are not the same.
52+
if img1_data.shape != img2_data.shape:
53+
raise RuntimeError(f"Images {tiff_file_a} and {tiff_file_b} have different shapes: {img1_data.shape} vs {img2_data.shape}")
54+
55+
diff_img = np.abs(img1_data.astype(np.int16) - img2_data.astype(np.int16))
56+
diff_img = diff_img.astype(np.uint8)
57+
tifffile.imwrite(output_path, diff_img)
58+
59+
except FileNotFoundError:
60+
print(f"Error: One or both files not found ('{file_path1}', '{file_path2}').")
61+
return False
62+
except tifffile.TiffFileError as e:
63+
print(f"Error: One or both files are not valid TIFF files or could not be read. Details: {e}")
64+
return False
65+
except Exception as e:
66+
print(f"An unexpected error occurred: {e}")
67+
return False

test/renderdiff/src/preamble.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
# Sets up the environment for scripts in test/renderdiff/
1818

19-
OUTPUT_DIR="$(pwd)/out/renderdiff_tests"
19+
RENDER_OUTPUT_DIR="$(pwd)/out/renderdiff/renders"
20+
DIFF_OUTPUT_DIR="$(pwd)/out/renderdiff/diffs"
21+
GOLDEN_OUTPUT_DIR="$(pwd)/out/renderdiff/goldens"
2022
RENDERDIFF_TEST_DIR="$(pwd)/test/renderdiff"
2123
MESA_DIR="$(pwd)/mesa/out/"
2224
VENV_DIR="$(pwd)/venv"
@@ -33,6 +35,7 @@ else
3335
fi
3436

3537
function start_() {
38+
mkdir -p ${RENDER_OUTPUT_DIR} ${DIFF_OUTPUT_DIR} ${GOLDEN_OUTPUT_DIR}
3639
if [[ "$GITHUB_WORKFLOW" ]]; then
3740
set -ex
3841
fi

test/renderdiff/src/render.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def _render_test_config(gltf_viewer,
102102
vk_icd=args.vk_icd)
103103

104104
with open(f'{output_dir}/render_results.json', 'w') as f:
105-
f.write(json.dumps(results))
105+
f.write(json.dumps(results, indent=2))
106106

107107
shutil.copy2(args.test, f'{output_dir}/test.json')
108108

test/renderdiff/src/viewer.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright (C) 2025 The Android Open Source Project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import sys
17+
import flask
18+
import pathlib
19+
import json
20+
21+
from utils import ArgParseImpl
22+
23+
from flask import Flask, request, make_response, send_from_directory
24+
25+
DIR = pathlib.Path(__file__).parent.absolute()
26+
HTML_DIR = os.path.join(DIR, "viewer_html")
27+
28+
def _create_app(config):
29+
app = Flask(__name__)
30+
31+
client_config = config.copy()
32+
base_dir = client_config['base_dir']
33+
comparison_dir = client_config['comparison_dir']
34+
diff_dir = client_config['diff_dir']
35+
36+
del client_config['base_dir']
37+
del client_config['comparison_dir']
38+
del client_config['diff_dir']
39+
40+
@app.route('/r/', methods=['GET'])
41+
def get_r():
42+
return json.dumps(client_config)
43+
44+
@app.route('/g/<path:filepath>', methods=['GET'])
45+
def get_g(filepath):
46+
return send_from_directory(base_dir, filepath)
47+
48+
@app.route('/d/<path:filepath>', methods=['GET'])
49+
def get_d(filepath):
50+
return send_from_directory(diff_dir, filepath)
51+
52+
@app.route('/c/<path:filepath>', methods=['GET'])
53+
def get_c(filepath):
54+
return send_from_directory(comparison_dir, filepath)
55+
56+
@app.route('/<path:filepath>')
57+
def get_static_file(filepath):
58+
return send_from_directory(HTML_DIR, filepath)
59+
60+
@app.route('/')
61+
def get_index():
62+
return send_from_directory(HTML_DIR, 'index.html')
63+
64+
app.url_map.strict_slashes = False
65+
return app
66+
67+
if __name__ == '__main__':
68+
PORT = 8901
69+
parser = ArgParseImpl()
70+
parser.add_argument('--diff', help='Diff directory', required=True)
71+
args, _ = parser.parse_known_args(sys.argv[1:])
72+
73+
with open(os.path.join(args.diff, 'compare_results.json'), 'r') as f:
74+
config = json.loads(f.read())
75+
config['diff_dir'] = os.path.abspath(args.diff)
76+
77+
app = _create_app(config)
78+
from waitress import serve
79+
80+
print(f'Point your browser to http://localhost:{PORT} to see the diff results')
81+
serve(app, host="127.0.0.1", port=PORT)

0 commit comments

Comments
 (0)