2016-10-01 14:29:43 +10:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
2020-05-05 23:28:36 +02:00
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2016-10-01 14:29:43 +10:00
parse_dash_results . py
- - - - - - - - - - - - - - - - - - - - -
Date : October 2016
Copyright : ( C ) 2016 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* This program is free software ; you can redistribute it and / or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation ; either version 2 of the License , or *
* ( at your option ) any later version . *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
"""
2017-09-25 10:26:37 +02:00
from __future__ import print_function
from future import standard_library
standard_library . install_aliases ( )
from builtins import range
2016-10-01 14:29:43 +10:00
__author__ = ' Nyall Dawson '
__date__ = ' October 2016 '
__copyright__ = ' (C) 2016, Nyall Dawson '
import os
import sys
import argparse
2017-09-25 10:26:37 +02:00
import urllib . request
import urllib . parse
import urllib . error
2016-10-01 14:29:43 +10:00
import re
2020-01-22 15:37:46 +01:00
import json
2020-08-02 13:53:56 +02:00
from PyQt5 . QtCore import ( Qt )
2016-10-01 14:29:43 +10:00
from PyQt5 . QtGui import (
QImage , QColor , qRed , qBlue , qGreen , qAlpha , qRgb , QPixmap )
from PyQt5 . QtWidgets import ( QDialog ,
QApplication ,
QLabel ,
QVBoxLayout ,
QHBoxLayout ,
QGridLayout ,
QPushButton ,
QDoubleSpinBox ,
2017-09-25 23:55:16 +02:00
QWidget ,
2020-07-30 08:18:24 +10:00
QScrollArea ,
QLayout ,
QDialogButtonBox ,
QListWidget )
2020-07-30 18:11:52 +02:00
import termcolor
2016-10-01 14:29:43 +10:00
import struct
import glob
2019-07-15 16:05:41 +02:00
dash_url = ' https://cdash.orfeo-toolbox.org '
2016-10-01 14:29:43 +10:00
def error ( msg ) :
2020-07-30 18:11:52 +02:00
print ( termcolor . colored ( msg , ' red ' ) )
2016-10-01 14:29:43 +10:00
sys . exit ( 1 )
def colorDiff ( c1 , c2 ) :
redDiff = abs ( qRed ( c1 ) - qRed ( c2 ) )
greenDiff = abs ( qGreen ( c1 ) - qGreen ( c2 ) )
blueDiff = abs ( qBlue ( c1 ) - qBlue ( c2 ) )
alphaDiff = abs ( qAlpha ( c1 ) - qAlpha ( c2 ) )
return max ( redDiff , greenDiff , blueDiff , alphaDiff )
def imageFromPath ( path ) :
2017-01-30 18:43:56 +10:00
if ( path [ : 8 ] == ' https:// ' or path [ : 7 ] == ' file:// ' ) :
2016-10-01 14:29:43 +10:00
# fetch remote image
2020-08-02 13:54:23 +02:00
print ( ' Fetching remote ( {} ) ' . format ( path ) )
2016-10-01 14:29:43 +10:00
data = urllib . request . urlopen ( path ) . read ( )
image = QImage ( )
image . loadFromData ( data )
else :
2020-08-02 13:54:23 +02:00
print ( ' Using local ( {} ) ' . format ( path ) )
2016-10-01 14:29:43 +10:00
image = QImage ( path )
return image
2020-07-30 08:18:24 +10:00
class SelectReferenceImageDialog ( QDialog ) :
def __init__ ( self , parent , test_name , images ) :
super ( ) . __init__ ( parent )
self . setWindowTitle ( ' Select reference image ' )
2020-08-02 13:53:56 +02:00
self . setWindowFlags ( Qt . Window )
2020-07-30 08:18:24 +10:00
self . button_box = QDialogButtonBox ( QDialogButtonBox . Ok | QDialogButtonBox . Cancel )
self . button_box . accepted . connect ( self . accept )
self . button_box . rejected . connect ( self . reject )
layout = QVBoxLayout ( )
layout . addWidget ( QLabel ( ' Found multiple matching reference images for {} ' . format ( test_name ) ) )
self . list = QListWidget ( )
layout . addWidget ( self . list , 1 )
layout . addWidget ( self . button_box )
self . setLayout ( layout )
for image in images :
self . list . addItem ( image )
def selected_image ( self ) :
return self . list . currentItem ( ) . text ( )
2016-10-01 14:29:43 +10:00
class ResultHandler ( QDialog ) :
def __init__ ( self , parent = None ) :
2020-08-02 13:53:56 +02:00
super ( ) . __init__ ( parent )
2016-10-01 14:29:43 +10:00
self . setWindowTitle ( ' Dash results ' )
2020-08-02 13:53:56 +02:00
self . setWindowFlags ( Qt . Window )
2016-10-01 14:29:43 +10:00
self . control_label = QLabel ( )
self . rendered_label = QLabel ( )
self . diff_label = QLabel ( )
self . mask_label = QLabel ( )
self . new_mask_label = QLabel ( )
2017-09-25 23:55:16 +02:00
self . scrollArea = QScrollArea ( )
self . widget = QWidget ( )
2016-10-01 14:29:43 +10:00
self . test_name_label = QLabel ( )
2017-09-25 23:55:16 +02:00
grid = QGridLayout ( )
2016-10-01 14:29:43 +10:00
grid . addWidget ( self . test_name_label , 0 , 0 )
grid . addWidget ( QLabel ( ' Control ' ) , 1 , 0 )
grid . addWidget ( QLabel ( ' Rendered ' ) , 1 , 1 )
grid . addWidget ( QLabel ( ' Difference ' ) , 1 , 2 )
grid . addWidget ( self . control_label , 2 , 0 )
grid . addWidget ( self . rendered_label , 2 , 1 )
grid . addWidget ( self . diff_label , 2 , 2 )
grid . addWidget ( QLabel ( ' Current Mask ' ) , 3 , 0 )
grid . addWidget ( QLabel ( ' New Mask ' ) , 3 , 1 )
grid . addWidget ( self . mask_label , 4 , 0 )
grid . addWidget ( self . new_mask_label , 4 , 1 )
2020-07-30 08:18:24 +10:00
grid . setSizeConstraint ( QLayout . SetFixedSize )
2016-10-01 14:29:43 +10:00
2017-09-25 23:55:16 +02:00
self . widget . setLayout ( grid )
self . scrollArea . setWidget ( self . widget )
2016-10-01 14:29:43 +10:00
v_layout = QVBoxLayout ( )
2017-09-25 23:55:16 +02:00
v_layout . addWidget ( self . scrollArea , 1 )
2016-10-01 14:29:43 +10:00
next_image_button = QPushButton ( )
next_image_button . setText ( ' Skip ' )
next_image_button . pressed . connect ( self . load_next )
self . overload_spin = QDoubleSpinBox ( )
self . overload_spin . setMinimum ( 1 )
self . overload_spin . setMaximum ( 255 )
self . overload_spin . setValue ( 1 )
2017-09-27 17:09:26 +02:00
self . overload_spin . valueChanged . connect ( lambda : save_mask_button . setEnabled ( False ) )
2016-10-01 14:29:43 +10:00
preview_mask_button = QPushButton ( )
preview_mask_button . setText ( ' Preview New Mask ' )
preview_mask_button . pressed . connect ( self . preview_mask )
2017-09-27 17:09:26 +02:00
preview_mask_button . pressed . connect ( lambda : save_mask_button . setEnabled ( True ) )
2016-10-01 14:29:43 +10:00
save_mask_button = QPushButton ( )
save_mask_button . setText ( ' Save New Mask ' )
save_mask_button . pressed . connect ( self . save_mask )
2020-08-02 13:55:14 +02:00
add_ref_image_button = QPushButton ( )
add_ref_image_button . setText ( ' Add Reference Image ' )
add_ref_image_button . pressed . connect ( self . add_reference_image )
2016-10-01 14:29:43 +10:00
button_layout = QHBoxLayout ( )
button_layout . addWidget ( next_image_button )
button_layout . addWidget ( QLabel ( ' Mask diff multiplier: ' ) )
button_layout . addWidget ( self . overload_spin )
button_layout . addWidget ( preview_mask_button )
button_layout . addWidget ( save_mask_button )
2020-08-02 13:55:14 +02:00
button_layout . addWidget ( add_ref_image_button )
2016-10-01 14:29:43 +10:00
button_layout . addStretch ( )
v_layout . addLayout ( button_layout )
self . setLayout ( v_layout )
def closeEvent ( self , event ) :
self . reject ( )
def parse_url ( self , url ) :
2020-01-22 15:37:46 +01:00
parts = urllib . parse . urlsplit ( url )
apiurl = urllib . parse . urlunsplit ( ( parts . scheme , parts . netloc , ' /api/v1/testDetails.php ' , parts . query , parts . fragment ) )
print ( ' Fetching dash results from api: {} ' . format ( apiurl ) )
page = urllib . request . urlopen ( apiurl )
content = json . loads ( page . read ( ) . decode ( ' utf-8 ' ) )
2016-10-01 14:29:43 +10:00
# build up list of rendered images
2020-01-22 15:37:46 +01:00
measurement_img = [ img for img in content [ ' test ' ] [ ' images ' ] if img [ ' role ' ] . startswith ( ' Rendered Image ' ) ]
2016-10-01 14:29:43 +10:00
images = { }
for img in measurement_img :
2020-05-05 23:43:36 +02:00
m = re . search ( r ' Rendered Image (.*?)( \ s|$) ' , img [ ' role ' ] )
2016-10-01 14:29:43 +10:00
test_name = m . group ( 1 )
2020-01-22 15:37:46 +01:00
rendered_image = ' displayImage.php?imgid= {} ' . format ( img [ ' imgid ' ] )
2016-10-01 14:29:43 +10:00
images [ test_name ] = ' {} / {} ' . format ( dash_url , rendered_image )
2017-09-25 23:55:44 +02:00
if images :
2020-07-30 18:11:52 +02:00
print ( ' Found images: \n ' )
for title , url in images . items ( ) :
print ( ' ' + termcolor . colored ( title , attrs = [ ' bold ' ] ) + ' : ' + url )
2017-09-25 23:55:44 +02:00
else :
2020-07-30 18:11:52 +02:00
print ( termcolor . colored ( ' No images found \n ' , ' yellow ' ) )
2016-10-01 14:29:43 +10:00
self . images = images
self . load_next ( )
def load_next ( self ) :
if not self . images :
# all done
self . accept ( )
2017-09-25 23:56:21 +02:00
exit ( 0 )
2016-10-01 14:29:43 +10:00
test_name , rendered_image = self . images . popitem ( )
self . test_name_label . setText ( test_name )
2020-07-30 18:11:52 +02:00
print ( termcolor . colored ( ' \n ' + test_name , attrs = [ ' bold ' ] ) )
2016-10-01 14:29:43 +10:00
control_image = self . get_control_image_path ( test_name )
if not control_image :
self . load_next ( )
return
self . mask_image_path = control_image [ : - 4 ] + ' _mask.png '
self . load_images ( control_image , rendered_image , self . mask_image_path )
def load_images ( self , control_image_path , rendered_image_path , mask_image_path ) :
self . control_image = imageFromPath ( control_image_path )
if not self . control_image :
error ( ' Could not read control image {} ' . format ( control_image_path ) )
self . rendered_image = imageFromPath ( rendered_image_path )
if not self . rendered_image :
error (
' Could not read rendered image {} ' . format ( rendered_image_path ) )
if not self . rendered_image . width ( ) == self . control_image . width ( ) or not self . rendered_image . height ( ) == self . control_image . height ( ) :
print (
' Size mismatch - control image is {} x {} , rendered image is {} x {} ' . format ( self . control_image . width ( ) ,
self . control_image . height (
) ,
self . rendered_image . width (
) ,
2017-01-22 21:10:23 +10:00
self . rendered_image . height ( ) ) )
2016-10-01 14:29:43 +10:00
max_width = min (
self . rendered_image . width ( ) , self . control_image . width ( ) )
max_height = min (
self . rendered_image . height ( ) , self . control_image . height ( ) )
# read current mask, if it exist
self . mask_image = imageFromPath ( mask_image_path )
if self . mask_image . isNull ( ) :
print (
' Mask image does not exist, creating {} ' . format ( mask_image_path ) )
self . mask_image = QImage (
self . control_image . width ( ) , self . control_image . height ( ) , QImage . Format_ARGB32 )
self . mask_image . fill ( QColor ( 0 , 0 , 0 ) )
self . diff_image = self . create_diff_image (
self . control_image , self . rendered_image , self . mask_image )
if not self . diff_image :
self . load_next ( )
return
self . control_label . setPixmap ( QPixmap . fromImage ( self . control_image ) )
2020-07-30 08:18:24 +10:00
self . control_label . setFixedSize ( self . control_image . size ( ) )
2016-10-01 14:29:43 +10:00
self . rendered_label . setPixmap ( QPixmap . fromImage ( self . rendered_image ) )
2020-07-30 08:18:24 +10:00
self . rendered_label . setFixedSize ( self . rendered_image . size ( ) )
2016-10-01 14:29:43 +10:00
self . mask_label . setPixmap ( QPixmap . fromImage ( self . mask_image ) )
2020-07-30 08:18:24 +10:00
self . mask_label . setFixedSize ( self . mask_image . size ( ) )
2016-10-01 14:29:43 +10:00
self . diff_label . setPixmap ( QPixmap . fromImage ( self . diff_image ) )
2020-07-30 08:18:24 +10:00
self . diff_label . setFixedSize ( self . diff_image . size ( ) )
2016-10-01 14:29:43 +10:00
self . preview_mask ( )
def preview_mask ( self ) :
self . new_mask_image = self . create_mask (
self . control_image , self . rendered_image , self . mask_image , self . overload_spin . value ( ) )
self . new_mask_label . setPixmap ( QPixmap . fromImage ( self . new_mask_image ) )
2020-07-30 08:18:24 +10:00
self . new_mask_label . setFixedSize ( self . new_mask_image . size ( ) )
2016-10-01 14:29:43 +10:00
def save_mask ( self ) :
self . new_mask_image . save ( self . mask_image_path , " png " )
self . load_next ( )
2020-08-02 13:55:14 +02:00
def add_reference_image ( self ) :
if os . path . abspath ( self . control_images_base_path ) == os . path . abspath ( self . found_control_image_path ) :
images = glob . glob ( os . path . join ( self . found_control_image_path , ' *.png ' ) )
default_path = os . path . join ( self . found_control_image_path , ' set1 ' )
os . makedirs ( default_path )
for image in images :
imgname = os . path . basename ( image )
os . rename ( image , os . path . join ( default_path , imgname ) )
for i in range ( 2 , 100 ) :
new_path = os . path . join ( self . control_images_base_path , ' set ' + str ( i ) )
if not os . path . exists ( new_path ) :
break
else :
raise RuntimeError ( ' Could not find a suitable directory for another set of reference images ' )
os . makedirs ( new_path )
control_image_name = os . path . basename ( self . found_image )
self . rendered_image . save ( os . path . join ( new_path , control_image_name ) )
self . load_next ( )
2016-10-01 14:29:43 +10:00
def create_mask ( self , control_image , rendered_image , mask_image , overload = 1 ) :
max_width = min ( rendered_image . width ( ) , control_image . width ( ) )
max_height = min ( rendered_image . height ( ) , control_image . height ( ) )
new_mask_image = QImage (
control_image . width ( ) , control_image . height ( ) , QImage . Format_ARGB32 )
new_mask_image . fill ( QColor ( 0 , 0 , 0 ) )
# loop through pixels in rendered image and compare
mismatch_count = 0
linebytes = max_width * 4
for y in range ( max_height ) :
control_scanline = control_image . constScanLine (
y ) . asstring ( linebytes )
rendered_scanline = rendered_image . constScanLine (
y ) . asstring ( linebytes )
mask_scanline = mask_image . scanLine ( y ) . asstring ( linebytes )
for x in range ( max_width ) :
currentTolerance = qRed (
struct . unpack ( ' I ' , mask_scanline [ x * 4 : x * 4 + 4 ] ) [ 0 ] )
if currentTolerance == 255 :
# ignore pixel
new_mask_image . setPixel (
x , y , qRgb ( currentTolerance , currentTolerance , currentTolerance ) )
continue
expected_rgb = struct . unpack (
' I ' , control_scanline [ x * 4 : x * 4 + 4 ] ) [ 0 ]
rendered_rgb = struct . unpack (
' I ' , rendered_scanline [ x * 4 : x * 4 + 4 ] ) [ 0 ]
difference = min (
2020-08-02 13:55:27 +02:00
255 , int ( colorDiff ( expected_rgb , rendered_rgb ) * overload ) )
2016-10-01 14:29:43 +10:00
if difference > currentTolerance :
# update mask image
new_mask_image . setPixel (
x , y , qRgb ( difference , difference , difference ) )
mismatch_count + = 1
else :
new_mask_image . setPixel (
x , y , qRgb ( currentTolerance , currentTolerance , currentTolerance ) )
return new_mask_image
def get_control_image_path ( self , test_name ) :
if os . path . isfile ( test_name ) :
2020-05-05 23:43:36 +02:00
return test_name
2016-10-01 14:29:43 +10:00
# else try and find matching test image
script_folder = os . path . dirname ( os . path . realpath ( sys . argv [ 0 ] ) )
control_images_folder = os . path . join (
script_folder , ' ../tests/testdata/control_images ' )
matching_control_images = [ x [ 0 ]
2020-07-30 18:34:28 +02:00
for x in os . walk ( control_images_folder ) if test_name + ' / ' in x [ 0 ] or x [ 0 ] . endswith ( test_name ) ]
2020-08-02 13:55:14 +02:00
self . control_images_base_path = os . path . commonprefix ( matching_control_images )
2016-10-01 14:29:43 +10:00
if len ( matching_control_images ) > 1 :
2020-07-30 18:11:52 +02:00
for item in matching_control_images :
print ( ' - ' + item )
2020-07-30 08:18:24 +10:00
dlg = SelectReferenceImageDialog ( self , test_name , matching_control_images )
if not dlg . exec_ ( ) :
return None
2020-08-02 13:55:14 +02:00
self . found_control_image_path = dlg . selected_image ( )
2016-10-01 14:29:43 +10:00
elif len ( matching_control_images ) == 0 :
2020-07-30 18:11:52 +02:00
print ( termcolor . colored ( ' No matching control images found for {} ' . format ( test_name ) , ' yellow ' ) )
2016-10-01 14:29:43 +10:00
return None
2020-07-30 08:18:24 +10:00
else :
2020-08-02 13:55:14 +02:00
self . found_control_image_path = matching_control_images [ 0 ]
2016-10-01 14:29:43 +10:00
# check for a single matching expected image
2020-08-02 13:55:14 +02:00
images = glob . glob ( os . path . join ( self . found_control_image_path , ' *.png ' ) )
2016-10-01 14:29:43 +10:00
filtered_images = [ i for i in images if not i [ - 9 : ] == ' _mask.png ' ]
if len ( filtered_images ) > 1 :
error (
' Found multiple matching control images for {} ' . format ( test_name ) )
elif len ( filtered_images ) == 0 :
error ( ' No matching control images found for {} ' . format ( test_name ) )
2020-08-02 13:55:14 +02:00
self . found_image = filtered_images [ 0 ]
print ( ' Found matching control image: {} ' . format ( self . found_image ) )
return self . found_image
2016-10-01 14:29:43 +10:00
def create_diff_image ( self , control_image , rendered_image , mask_image ) :
# loop through pixels in rendered image and compare
mismatch_count = 0
max_width = min ( rendered_image . width ( ) , control_image . width ( ) )
max_height = min ( rendered_image . height ( ) , control_image . height ( ) )
linebytes = max_width * 4
diff_image = QImage (
control_image . width ( ) , control_image . height ( ) , QImage . Format_ARGB32 )
diff_image . fill ( QColor ( 152 , 219 , 249 ) )
for y in range ( max_height ) :
control_scanline = control_image . constScanLine (
y ) . asstring ( linebytes )
rendered_scanline = rendered_image . constScanLine (
y ) . asstring ( linebytes )
mask_scanline = mask_image . scanLine ( y ) . asstring ( linebytes )
for x in range ( max_width ) :
currentTolerance = qRed (
struct . unpack ( ' I ' , mask_scanline [ x * 4 : x * 4 + 4 ] ) [ 0 ] )
if currentTolerance == 255 :
# ignore pixel
continue
expected_rgb = struct . unpack (
' I ' , control_scanline [ x * 4 : x * 4 + 4 ] ) [ 0 ]
rendered_rgb = struct . unpack (
' I ' , rendered_scanline [ x * 4 : x * 4 + 4 ] ) [ 0 ]
difference = colorDiff ( expected_rgb , rendered_rgb )
if difference > currentTolerance :
# update mask image
diff_image . setPixel ( x , y , qRgb ( 255 , 0 , 0 ) )
mismatch_count + = 1
if mismatch_count :
return diff_image
else :
2020-07-30 18:11:52 +02:00
print ( termcolor . colored ( ' No mismatches ' , ' green ' ) )
2016-10-01 14:29:43 +10:00
return None
def main ( ) :
app = QApplication ( sys . argv )
2020-01-22 15:53:01 +01:00
parser = argparse . ArgumentParser (
2020-05-05 23:28:36 +02:00
description = ''' A tool to automatically update test image masks based on results submitted to cdash.
2016-10-01 14:29:43 +10:00
2020-05-06 06:37:34 +02:00
It will take local control images from the QGIS source and rendered images from test results
on cdash to create a mask .
2020-01-23 07:01:48 +01:00
2020-05-06 06:37:34 +02:00
When using it , carefully check , that the rendered images from the test results are acceptable and
that the new masks will only mask regions on the image that indeed allow for variation .
2020-01-23 07:01:48 +01:00
2020-05-06 06:37:34 +02:00
If the resulting mask is too tolerant , consider adding a new control image next to the existing one .
''' )
2020-01-23 07:01:48 +01:00
2020-05-05 23:28:36 +02:00
parser . add_argument ( ' dash_url ' , help = ' URL to a dash result with images. E.g. https://cdash.orfeo-toolbox.org/testDetails.php?test=15052561&build=27712 ' )
args = parser . parse_args ( )
2020-01-22 15:53:01 +01:00
2020-05-05 23:28:36 +02:00
w = ResultHandler ( )
2016-10-01 14:29:43 +10:00
w . parse_url ( args . dash_url )
2017-03-04 19:41:23 +01:00
w . exec_ ( )
2016-10-01 14:29:43 +10:00
if __name__ == ' __main__ ' :
main ( )