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 }
QgsMapRenderer * mpMapRenderer
A rectangle specified with double values.
Definition: qgsrectangle.h:35
void render(QPainter *painter, double *forceWidthScale=0)
starts rendering @ param forceWidthScale Force a specific scale factor for line widths and marker siz...
double yMaximum() const
Get the y maximum value (top side of rectangle)
Definition: qgsrectangle.h:185
QgsRectangle extent() const
returns current extent
QString qgsDoubleToString(const double &a)
Definition: qgis.h:250
bool runTest(QString theTestName, unsigned int theMismatchCount=0)
Test using renderer to generate the image to be compared.
unsigned int mMismatchCount
QString controlImagePath() const
double mapUnitsPerPixel() const
QString imageToHash(QString theImageFile)
Get an md5 hash that uniquely identifies an image.
unsigned int mMatchTarget
void setOutputSize(QSize size, int dpi)
bool isKnownAnomaly(QString theDiffImageFile)
Get a list of all the anomalies.
void setControlName(const QString theName)
Base directory name for the control image (with control image path suffixed) the path to the image wi...
bool compareImages(QString theTestName, unsigned int theMismatchCount=0, QString theRenderedImageFile="")
Test using two arbitary images (map renderer will not be used)
double xMinimum() const
Get the x minimum value (left side of rectangle)
Definition: qgsrectangle.h:180