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