Skip to content

Commit a07a57e

Browse files
committedOct 1, 2016
Add new script to handle multiple render results from a dash
results page in an interactive way Allows generation and tweaking of mask images while previewing the result
1 parent 67d5e19 commit a07a57e

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed
 

‎scripts/parse_dash_results.py

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
****************************3***********************************************
6+
parse_dash_results.py
7+
---------------------
8+
Date : October 2016
9+
Copyright : (C) 2016 by Nyall Dawson
10+
Email : nyall dot dawson at gmail dot com
11+
***************************************************************************
12+
* *
13+
* This program is free software; you can redistribute it and/or modify *
14+
* it under the terms of the GNU General Public License as published by *
15+
* the Free Software Foundation; either version 2 of the License, or *
16+
* (at your option) any later version. *
17+
* *
18+
***************************************************************************
19+
"""
20+
21+
__author__ = 'Nyall Dawson'
22+
__date__ = 'October 2016'
23+
__copyright__ = '(C) 2016, Nyall Dawson'
24+
# This will get replaced with a git SHA1 when you do a git archive
25+
__revision__ = '$Format:%H$'
26+
27+
import os
28+
import sys
29+
import argparse
30+
import urllib
31+
import re
32+
from bs4 import BeautifulSoup
33+
from PyQt5.QtGui import (
34+
QImage, QColor, qRed, qBlue, qGreen, qAlpha, qRgb, QPixmap)
35+
from PyQt5.QtWidgets import (QDialog,
36+
QApplication,
37+
QLabel,
38+
QVBoxLayout,
39+
QHBoxLayout,
40+
QGridLayout,
41+
QPushButton,
42+
QDoubleSpinBox,
43+
QMessageBox)
44+
import struct
45+
import glob
46+
47+
dash_url = 'http://dash.orfeo-toolbox.org'
48+
49+
50+
def error(msg):
51+
print(msg)
52+
sys.exit(1)
53+
54+
55+
def colorDiff(c1, c2):
56+
redDiff = abs(qRed(c1) - qRed(c2))
57+
greenDiff = abs(qGreen(c1) - qGreen(c2))
58+
blueDiff = abs(qBlue(c1) - qBlue(c2))
59+
alphaDiff = abs(qAlpha(c1) - qAlpha(c2))
60+
return max(redDiff, greenDiff, blueDiff, alphaDiff)
61+
62+
63+
def imageFromPath(path):
64+
if (path[:7] == 'http://' or path[:7] == 'file://'):
65+
# fetch remote image
66+
data = urllib.request.urlopen(path).read()
67+
image = QImage()
68+
image.loadFromData(data)
69+
else:
70+
image = QImage(path)
71+
return image
72+
73+
74+
class ResultHandler(QDialog):
75+
76+
def __init__(self, parent=None):
77+
super(ResultHandler, self).__init__()
78+
self.setWindowTitle('Dash results')
79+
self.control_label = QLabel()
80+
self.rendered_label = QLabel()
81+
self.diff_label = QLabel()
82+
83+
self.mask_label = QLabel()
84+
self.new_mask_label = QLabel()
85+
86+
grid = QGridLayout()
87+
self.test_name_label = QLabel()
88+
grid.addWidget(self.test_name_label, 0, 0)
89+
grid.addWidget(QLabel('Control'), 1, 0)
90+
grid.addWidget(QLabel('Rendered'), 1, 1)
91+
grid.addWidget(QLabel('Difference'), 1, 2)
92+
grid.addWidget(self.control_label, 2, 0)
93+
grid.addWidget(self.rendered_label, 2, 1)
94+
grid.addWidget(self.diff_label, 2, 2)
95+
grid.addWidget(QLabel('Current Mask'), 3, 0)
96+
grid.addWidget(QLabel('New Mask'), 3, 1)
97+
grid.addWidget(self.mask_label, 4, 0)
98+
grid.addWidget(self.new_mask_label, 4, 1)
99+
100+
v_layout = QVBoxLayout()
101+
v_layout.addLayout(grid, 1)
102+
103+
next_image_button = QPushButton()
104+
next_image_button.setText('Skip')
105+
next_image_button.pressed.connect(self.load_next)
106+
107+
self.overload_spin = QDoubleSpinBox()
108+
self.overload_spin.setMinimum(1)
109+
self.overload_spin.setMaximum(255)
110+
self.overload_spin.setValue(1)
111+
112+
preview_mask_button = QPushButton()
113+
preview_mask_button.setText('Preview New Mask')
114+
preview_mask_button.pressed.connect(self.preview_mask)
115+
116+
save_mask_button = QPushButton()
117+
save_mask_button.setText('Save New Mask')
118+
save_mask_button.pressed.connect(self.save_mask)
119+
120+
button_layout = QHBoxLayout()
121+
button_layout.addWidget(next_image_button)
122+
button_layout.addWidget(QLabel('Mask diff multiplier:'))
123+
button_layout.addWidget(self.overload_spin)
124+
button_layout.addWidget(preview_mask_button)
125+
button_layout.addWidget(save_mask_button)
126+
button_layout.addStretch()
127+
v_layout.addLayout(button_layout)
128+
self.setLayout(v_layout)
129+
130+
def closeEvent(self, event):
131+
self.reject()
132+
133+
def parse_url(self, url):
134+
print('Fetching dash results from: {}'.format(url))
135+
page = urllib.request.urlopen(url)
136+
soup = BeautifulSoup(page, "lxml")
137+
138+
# build up list of rendered images
139+
measurement_img = [img for img in soup.find_all('img') if
140+
img.get('alt') and img.get('alt').startswith('Rendered Image')]
141+
142+
images = {}
143+
for img in measurement_img:
144+
m = re.search('Rendered Image (.*?)\s', img.get('alt'))
145+
test_name = m.group(1)
146+
rendered_image = img.get('src')
147+
images[test_name] = '{}/{}'.format(dash_url, rendered_image)
148+
149+
print('found images:\n{}'.format(images))
150+
self.images = images
151+
self.load_next()
152+
153+
def load_next(self):
154+
if not self.images:
155+
# all done
156+
self.accept()
157+
158+
test_name, rendered_image = self.images.popitem()
159+
self.test_name_label.setText(test_name)
160+
control_image = self.get_control_image_path(test_name)
161+
if not control_image:
162+
self.load_next()
163+
return
164+
165+
self.mask_image_path = control_image[:-4] + '_mask.png'
166+
self.load_images(control_image, rendered_image, self.mask_image_path)
167+
168+
def load_images(self, control_image_path, rendered_image_path, mask_image_path):
169+
self.control_image = imageFromPath(control_image_path)
170+
if not self.control_image:
171+
error('Could not read control image {}'.format(control_image_path))
172+
173+
self.rendered_image = imageFromPath(rendered_image_path)
174+
if not self.rendered_image:
175+
error(
176+
'Could not read rendered image {}'.format(rendered_image_path))
177+
if not self.rendered_image.width() == self.control_image.width() or not self.rendered_image.height() == self.control_image.height():
178+
print(
179+
'Size mismatch - control image is {}x{}, rendered image is {}x{}'.format(self.control_image.width(),
180+
self.control_image.height(
181+
),
182+
self.rendered_image.width(
183+
),
184+
self.rendered_image.height()))
185+
186+
max_width = min(
187+
self.rendered_image.width(), self.control_image.width())
188+
max_height = min(
189+
self.rendered_image.height(), self.control_image.height())
190+
191+
# read current mask, if it exist
192+
self.mask_image = imageFromPath(mask_image_path)
193+
if self.mask_image.isNull():
194+
print(
195+
'Mask image does not exist, creating {}'.format(mask_image_path))
196+
self.mask_image = QImage(
197+
self.control_image.width(), self.control_image.height(), QImage.Format_ARGB32)
198+
self.mask_image.fill(QColor(0, 0, 0))
199+
200+
self.diff_image = self.create_diff_image(
201+
self.control_image, self.rendered_image, self.mask_image)
202+
if not self.diff_image:
203+
self.load_next()
204+
return
205+
206+
self.control_label.setPixmap(QPixmap.fromImage(self.control_image))
207+
self.rendered_label.setPixmap(QPixmap.fromImage(self.rendered_image))
208+
self.mask_label.setPixmap(QPixmap.fromImage(self.mask_image))
209+
self.diff_label.setPixmap(QPixmap.fromImage(self.diff_image))
210+
self.preview_mask()
211+
212+
def preview_mask(self):
213+
self.new_mask_image = self.create_mask(
214+
self.control_image, self.rendered_image, self.mask_image, self.overload_spin.value())
215+
self.new_mask_label.setPixmap(QPixmap.fromImage(self.new_mask_image))
216+
217+
def save_mask(self):
218+
self.new_mask_image.save(self.mask_image_path, "png")
219+
self.load_next()
220+
221+
def create_mask(self, control_image, rendered_image, mask_image, overload=1):
222+
max_width = min(rendered_image.width(), control_image.width())
223+
max_height = min(rendered_image.height(), control_image.height())
224+
225+
new_mask_image = QImage(
226+
control_image.width(), control_image.height(), QImage.Format_ARGB32)
227+
new_mask_image.fill(QColor(0, 0, 0))
228+
229+
# loop through pixels in rendered image and compare
230+
mismatch_count = 0
231+
linebytes = max_width * 4
232+
for y in range(max_height):
233+
control_scanline = control_image.constScanLine(
234+
y).asstring(linebytes)
235+
rendered_scanline = rendered_image.constScanLine(
236+
y).asstring(linebytes)
237+
mask_scanline = mask_image.scanLine(y).asstring(linebytes)
238+
239+
for x in range(max_width):
240+
currentTolerance = qRed(
241+
struct.unpack('I', mask_scanline[x * 4:x * 4 + 4])[0])
242+
243+
if currentTolerance == 255:
244+
# ignore pixel
245+
new_mask_image.setPixel(
246+
x, y, qRgb(currentTolerance, currentTolerance, currentTolerance))
247+
continue
248+
249+
expected_rgb = struct.unpack(
250+
'I', control_scanline[x * 4:x * 4 + 4])[0]
251+
rendered_rgb = struct.unpack(
252+
'I', rendered_scanline[x * 4:x * 4 + 4])[0]
253+
difference = min(
254+
255, colorDiff(expected_rgb, rendered_rgb) * overload)
255+
256+
if difference > currentTolerance:
257+
# update mask image
258+
new_mask_image.setPixel(
259+
x, y, qRgb(difference, difference, difference))
260+
mismatch_count += 1
261+
else:
262+
new_mask_image.setPixel(
263+
x, y, qRgb(currentTolerance, currentTolerance, currentTolerance))
264+
return new_mask_image
265+
266+
def get_control_image_path(self, test_name):
267+
if os.path.isfile(test_name):
268+
return path
269+
270+
# else try and find matching test image
271+
script_folder = os.path.dirname(os.path.realpath(sys.argv[0]))
272+
control_images_folder = os.path.join(
273+
script_folder, '../tests/testdata/control_images')
274+
275+
matching_control_images = [x[0]
276+
for x in os.walk(control_images_folder) if test_name in x[0]]
277+
if len(matching_control_images) > 1:
278+
QMessageBox.warning(
279+
self, 'Result', 'Found multiple matching control images for {}'.format(test_name))
280+
return None
281+
elif len(matching_control_images) == 0:
282+
QMessageBox.warning(
283+
self, 'Result', 'No matching control images found for {}'.format(test_name))
284+
return None
285+
286+
found_control_image_path = matching_control_images[0]
287+
288+
# check for a single matching expected image
289+
images = glob.glob(os.path.join(found_control_image_path, '*.png'))
290+
filtered_images = [i for i in images if not i[-9:] == '_mask.png']
291+
if len(filtered_images) > 1:
292+
error(
293+
'Found multiple matching control images for {}'.format(test_name))
294+
elif len(filtered_images) == 0:
295+
error('No matching control images found for {}'.format(test_name))
296+
297+
found_image = filtered_images[0]
298+
print('Found matching control image: {}'.format(found_image))
299+
return found_image
300+
301+
def create_diff_image(self, control_image, rendered_image, mask_image):
302+
# loop through pixels in rendered image and compare
303+
mismatch_count = 0
304+
max_width = min(rendered_image.width(), control_image.width())
305+
max_height = min(rendered_image.height(), control_image.height())
306+
linebytes = max_width * 4
307+
308+
diff_image = QImage(
309+
control_image.width(), control_image.height(), QImage.Format_ARGB32)
310+
diff_image.fill(QColor(152, 219, 249))
311+
312+
for y in range(max_height):
313+
control_scanline = control_image.constScanLine(
314+
y).asstring(linebytes)
315+
rendered_scanline = rendered_image.constScanLine(
316+
y).asstring(linebytes)
317+
mask_scanline = mask_image.scanLine(y).asstring(linebytes)
318+
319+
for x in range(max_width):
320+
currentTolerance = qRed(
321+
struct.unpack('I', mask_scanline[x * 4:x * 4 + 4])[0])
322+
323+
if currentTolerance == 255:
324+
# ignore pixel
325+
continue
326+
327+
expected_rgb = struct.unpack(
328+
'I', control_scanline[x * 4:x * 4 + 4])[0]
329+
rendered_rgb = struct.unpack(
330+
'I', rendered_scanline[x * 4:x * 4 + 4])[0]
331+
difference = colorDiff(expected_rgb, rendered_rgb)
332+
333+
if difference > currentTolerance:
334+
# update mask image
335+
diff_image.setPixel(x, y, qRgb(255, 0, 0))
336+
mismatch_count += 1
337+
338+
if mismatch_count:
339+
return diff_image
340+
else:
341+
print('No mismatches')
342+
return None
343+
344+
345+
def main():
346+
app = QApplication(sys.argv)
347+
348+
parser = argparse.ArgumentParser()
349+
parser.add_argument('dash_url')
350+
args = parser.parse_args()
351+
352+
w = ResultHandler()
353+
w.parse_url(args.dash_url)
354+
w.exec()
355+
356+
if __name__ == '__main__':
357+
main()

0 commit comments

Comments
 (0)
Please sign in to comment.