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 }
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:189
QgsRectangle extent() const
returns current extent
QString qgsDoubleToString(const double &a)
Definition: qgis.h:297
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)
unsigned int mColorTolerance
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:184