QGIS API Documentation  2.2.0-Valmiera
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
qgsrenderchecker.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsrenderchecker.cpp
3  --------------------------------------
4  Date : 18 Jan 2008
5  Copyright : (C) 2008 by Tim Sutton
6  Email : tim @ linfiniti.com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
16 #include "qgsrenderchecker.h"
17 #include "qgis.h"
18 
19 #include <QColor>
20 #include <QPainter>
21 #include <QImage>
22 #include <QTime>
23 #include <QCryptographicHash>
24 #include <QByteArray>
25 #include <QDebug>
26 #include <QBuffer>
27 
29  mReport( "" ),
30  mMatchTarget( 0 ),
31  mpMapRenderer( NULL ),
32  mElapsedTime( 0 ),
33  mRenderedImageFile( "" ),
34  mExpectedImageFile( "" ),
35  mMismatchCount( 0 ),
36  mColorTolerance( 0 ),
37  mElapsedTimeTarget( 0 ),
38  mControlPathPrefix( "" )
39 {
40 
41 }
42 
44 {
45  QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
46  QString myControlImageDir = myDataDir + QDir::separator() + "control_images" +
47  QDir::separator() + mControlPathPrefix;
48  return myControlImageDir;
49 }
50 
51 void QgsRenderChecker::setControlName( const QString theName )
52 {
53  mControlName = theName;
54  mExpectedImageFile = controlImagePath() + theName + QDir::separator()
55  + theName + ".png";
56 }
57 
58 QString QgsRenderChecker::imageToHash( QString theImageFile )
59 {
60  QImage myImage;
61  myImage.load( theImageFile );
62  QByteArray myByteArray;
63  QBuffer myBuffer( &myByteArray );
64  myImage.save( &myBuffer, "PNG" );
65  QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
66  QCryptographicHash myHash( QCryptographicHash::Md5 );
67  myHash.addData( myImageString.toUtf8() );
68  return myHash.result().toHex().constData();
69 }
70 
71 bool QgsRenderChecker::isKnownAnomaly( QString theDiffImageFile )
72 {
73  QString myControlImageDir = controlImagePath() + mControlName
74  + QDir::separator();
75  QDir myDirectory = QDir( myControlImageDir );
76  QStringList myList;
77  QString myFilename = "*";
78  myList = myDirectory.entryList( QStringList( myFilename ),
79  QDir::Files | QDir::NoSymLinks );
80  //remove the control file from the list as the anomalies are
81  //all files except the control file
82  myList.removeAt( myList.indexOf( QFileInfo( mExpectedImageFile ).fileName() ) );
83 
84  QString myImageHash = imageToHash( theDiffImageFile );
85 
86 
87  for ( int i = 0; i < myList.size(); ++i )
88  {
89  QString myFile = myList.at( i );
90  mReport += "<tr><td colspan=3>"
91  "Checking if " + myFile + " is a known anomaly.";
92  mReport += "</td></tr>";
93  QString myAnomalyHash = imageToHash( controlImagePath() + mControlName
94  + QDir::separator() + myFile );
95  QString myHashMessage = QString(
96  "Checking if anomaly %1 (hash %2)<br>" )
97  .arg( myFile )
98  .arg( myAnomalyHash );
99  myHashMessage += QString( "&nbsp; matches %1 (hash %2)" )
100  .arg( theDiffImageFile )
101  .arg( myImageHash );
102  //foo CDash
103  QString myMeasureMessage = "<DartMeasurement name=\"Anomaly check"
104  "\" type=\"text/text\">" + myHashMessage +
105  "</DartMeasurement>";
106  qDebug() << myMeasureMessage;
107  mReport += "<tr><td colspan=3>" + myHashMessage + "</td></tr>";
108  if ( myImageHash == myAnomalyHash )
109  {
110  mReport += "<tr><td colspan=3>"
111  "Anomaly found! " + myFile;
112  mReport += "</td></tr>";
113  return true;
114  }
115  }
116  mReport += "<tr><td colspan=3>"
117  "No anomaly found! ";
118  mReport += "</td></tr>";
119  return false;
120 }
121 
122 bool QgsRenderChecker::runTest( QString theTestName,
123  unsigned int theMismatchCount )
124 {
125  if ( mExpectedImageFile.isEmpty() )
126  {
127  qDebug( "QgsRenderChecker::runTest failed - Expected Image File not set." );
128  mReport = "<table>"
129  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
130  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
131  "Image File not set.</td></tr></table>\n";
132  return false;
133  }
134  //
135  // Load the expected result pixmap
136  //
137  QImage myExpectedImage( mExpectedImageFile );
138  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
139  //
140  // Now render our layers onto a pixmap
141  //
142  QImage myImage( myExpectedImage.width(),
143  myExpectedImage.height(),
144  QImage::Format_RGB32 );
145  myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
146  myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
147  myImage.fill( qRgb( 152, 219, 249 ) );
148  QPainter myPainter( &myImage );
149  myPainter.setRenderHint( QPainter::Antialiasing );
151  myExpectedImage.width(),
152  myExpectedImage.height() ),
153  myExpectedImage.logicalDpiX() );
154  QTime myTime;
155  myTime.start();
156  mpMapRenderer->render( &myPainter );
157  mElapsedTime = myTime.elapsed();
158  myPainter.end();
159  //
160  // Save the pixmap to disk so the user can make a
161  // visual assessment if needed
162  //
163  mRenderedImageFile = QDir::tempPath() + QDir::separator() +
164  theTestName + "_result.png";
165  myImage.save( mRenderedImageFile, "PNG", 100 );
166 
167  //create a world file to go with the image...
168 
169  QFile wldFile( QDir::tempPath() + QDir::separator() + theTestName + "_result.wld" );
170  if ( wldFile.open( QIODevice::WriteOnly ) )
171  {
173 
174  QTextStream stream( &wldFile );
175  stream << QString( "%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
178  .arg( qgsDoubleToString( r.xMinimum() + mpMapRenderer->mapUnitsPerPixel() / 2.0 ) )
179  .arg( qgsDoubleToString( r.yMaximum() - mpMapRenderer->mapUnitsPerPixel() / 2.0 ) );
180  }
181 
182  return compareImages( theTestName, theMismatchCount );
183 }
184 
185 
186 bool QgsRenderChecker::compareImages( QString theTestName,
187  unsigned int theMismatchCount,
188  QString theRenderedImageFile )
189 {
190  if ( mExpectedImageFile.isEmpty() )
191  {
192  qDebug( "QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
193  mReport = "<table>"
194  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
195  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
196  "Image File not set.</td></tr></table>\n";
197  return false;
198  }
199  if ( ! theRenderedImageFile.isEmpty() )
200  {
201  mRenderedImageFile = theRenderedImageFile;
202  }
203  if ( mRenderedImageFile.isEmpty() )
204  {
205  qDebug( "QgsRenderChecker::runTest failed - Rendered Image File not set." );
206  mReport = "<table>"
207  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
208  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
209  "Image File not set.</td></tr></table>\n";
210  return false;
211  }
212  //
213  // Load /create the images
214  //
215  QImage myExpectedImage( mExpectedImageFile );
216  QImage myResultImage( mRenderedImageFile );
217  QImage myDifferenceImage( myExpectedImage.width(),
218  myExpectedImage.height(),
219  QImage::Format_RGB32 );
220  QString myDiffImageFile = QDir::tempPath() + QDir::separator() +
221  QDir::separator() +
222  theTestName + "_result_diff.png";
223  myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
224 
225  //
226  // Set pixel count score and target
227  //
228  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
229  unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
230  //
231  // Set the report with the result
232  //
233  mReport = "<table>";
234  mReport += "<tr><td colspan=2>";
235  mReport += "Test image and result image for " + theTestName + "<br>"
236  "Expected size: " + QString::number( myExpectedImage.width() ).toLocal8Bit() + "w x " +
237  QString::number( myExpectedImage.height() ).toLocal8Bit() + "h (" +
238  QString::number( mMatchTarget ).toLocal8Bit() + " pixels)<br>"
239  "Actual size: " + QString::number( myResultImage.width() ).toLocal8Bit() + "w x " +
240  QString::number( myResultImage.height() ).toLocal8Bit() + "h (" +
241  QString::number( myPixelCount ).toLocal8Bit() + " pixels)";
242  mReport += "</td></tr>";
243  mReport += "<tr><td colspan = 2>\n";
244  mReport += "Expected Duration : <= " + QString::number( mElapsedTimeTarget ) +
245  "ms (0 indicates not specified)<br>";
246  mReport += "Actual Duration : " + QString::number( mElapsedTime ) + "ms<br>";
247 
248  // limit image size in page to something reasonable
249  int imgWidth = 420;
250  int imgHeight = 280;
251  if ( ! myExpectedImage.isNull() )
252  {
253  imgWidth = qMin( myExpectedImage.width(), imgWidth );
254  imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
255  }
256  QString myImagesString = "</td></tr>"
257  "<tr><td>Test Result:</td><td>Expected Result:</td><td>Difference (all blue is good, any red is bad)</td></tr>\n"
258  "<tr><td><img width=" + QString::number( imgWidth ) +
259  " height=" + QString::number( imgHeight ) +
260  " src=\"file://" +
262  "\"></td>\n<td><img width=" + QString::number( imgWidth ) +
263  " height=" + QString::number( imgHeight ) +
264  " src=\"file://" +
266  "\"></td>\n<td><img width=" + QString::number( imgWidth ) +
267  " height=" + QString::number( imgHeight ) +
268  " src=\"file://" +
269  myDiffImageFile +
270  "\"></td>\n</tr>\n</table>";
271  //
272  // To get the images into CDash
273  //
274  QString myDashMessage = "<DartMeasurementFile name=\"Rendered Image " + theTestName + "\""
275  " type=\"image/png\">" + mRenderedImageFile +
276  "</DartMeasurementFile>\n"
277  "<DartMeasurementFile name=\"Expected Image " + theTestName + "\" type=\"image/png\">" +
278  mExpectedImageFile + "</DartMeasurementFile>\n"
279  "<DartMeasurementFile name=\"Difference Image " + theTestName + "\" type=\"image/png\">" +
280  myDiffImageFile + "</DartMeasurementFile>\n";
281  qDebug( ) << myDashMessage;
282 
283  //
284  // Put the same info to debug too
285  //
286 
287  qDebug( "Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
288  qDebug( "Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
289 
290  if ( mMatchTarget != myPixelCount )
291  {
292  qDebug( "Test image and result image for %s are different - FAILING!", theTestName.toLocal8Bit().constData() );
293  mReport += "<tr><td colspan=3>";
294  mReport += "<font color=red>Expected image and result image for " + theTestName + " are different dimensions - FAILING!</font>";
295  mReport += "</td></tr>";
296  mReport += myImagesString;
297  return false;
298  }
299 
300  //
301  // Now iterate through them counting how many
302  // dissimilar pixel values there are
303  //
304 
305  mMismatchCount = 0;
306  int colorTolerance = ( int ) mColorTolerance;
307  for ( int x = 0; x < myExpectedImage.width(); ++x )
308  {
309  for ( int y = 0; y < myExpectedImage.height(); ++y )
310  {
311  QRgb myExpectedPixel = myExpectedImage.pixel( x, y );
312  QRgb myActualPixel = myResultImage.pixel( x, y );
313  if ( mColorTolerance == 0 )
314  {
315  if ( myExpectedPixel != myActualPixel )
316  {
317  ++mMismatchCount;
318  myDifferenceImage.setPixel( x, y, qRgb( 255, 0, 0 ) );
319  }
320  }
321  else
322  {
323  if ( qAbs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > colorTolerance ||
324  qAbs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > colorTolerance ||
325  qAbs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > colorTolerance ||
326  qAbs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > colorTolerance )
327  {
328  ++mMismatchCount;
329  myDifferenceImage.setPixel( x, y, qRgb( 255, 0, 0 ) );
330  }
331  }
332  }
333  }
334  //
335  //save the diff image to disk
336  //
337  myDifferenceImage.save( myDiffImageFile );
338 
339  //
340  // Send match result to debug
341  //
342  qDebug( "%d/%d pixels mismatched", mMismatchCount, mMatchTarget );
343 
344  //
345  // Send match result to report
346  //
347  mReport += "<tr><td colspan=3>" +
348  QString::number( mMismatchCount ) + "/" +
349  QString::number( mMatchTarget ) +
350  " pixels mismatched (allowed threshold: " +
351  QString::number( theMismatchCount ) +
352  ", allowed color component tolerance: " +
353  QString::number( mColorTolerance ) + ")";
354  mReport += "</td></tr>";
355 
356  //
357  // And send it to CDash
358  //
359  myDashMessage = "<DartMeasurement name=\"Mismatch Count "
360  "\" type=\"numeric/integer\">" +
361  QString::number( mMismatchCount ) + "/" +
362  QString::number( mMatchTarget ) +
363  "</DartMeasurement>";
364  qDebug( ) << myDashMessage;
365 
366  bool myAnomalyMatchFlag = isKnownAnomaly( myDiffImageFile );
367 
368  if ( myAnomalyMatchFlag )
369  {
370  mReport += "<tr><td colspan=3>"
371  "Difference image matched a known anomaly - passing test! "
372  "</td></tr>";
373  return true;
374  }
375  else
376  {
377  QString myMessage = "Difference image did not match any known anomaly.";
378  mReport += "<tr><td colspan=3>"
379  "</td></tr>";
380  QString myMeasureMessage = "<DartMeasurement name=\"No Anomalies Match"
381  "\" type=\"text/text\">" + myMessage +
382  " If you feel the difference image should be considered an anomaly "
383  "you can do something like this\n"
384  "cp " + myDiffImageFile + " ../tests/testdata/control_images/" + theTestName +
385  "/<imagename>.{wld,png}"
386  "</DartMeasurement>";
387  qDebug() << myMeasureMessage;
388  }
389 
390  if ( mMismatchCount <= theMismatchCount )
391  {
392  mReport += "<tr><td colspan = 3>\n";
393  mReport += "Test image and result image for " + theTestName + " are matched<br>";
394  mReport += "</td></tr>";
396  {
397  //test failed because it took too long...
398  qDebug( "Test failed because render step took too long" );
399  mReport += "<tr><td colspan = 3>\n";
400  mReport += "<font color=red>Test failed because render step took too long</font>";
401  mReport += "</td></tr>";
402  mReport += myImagesString;
403  return false;
404  }
405  else
406  {
407  mReport += myImagesString;
408  return true;
409  }
410  }
411  else
412  {
413  mReport += "<tr><td colspan = 3>\n";
414  mReport += "<font color=red>Test image and result image for " + theTestName + " are mismatched</font><br>";
415  mReport += "</td></tr>";
416  mReport += myImagesString;
417  return false;
418  }
419 }