QGIS API Documentation  2.4.0-Chugiak
 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 
18 #include "qgis.h"
20 
21 #include <QColor>
22 #include <QPainter>
23 #include <QImage>
24 #include <QTime>
25 #include <QCryptographicHash>
26 #include <QByteArray>
27 #include <QDebug>
28 #include <QBuffer>
29 
31  mReport( "" ),
32  mMatchTarget( 0 ),
33  mElapsedTime( 0 ),
34  mRenderedImageFile( "" ),
35  mExpectedImageFile( "" ),
36  mMismatchCount( 0 ),
37  mColorTolerance( 0 ),
38  mElapsedTimeTarget( 0 ),
39  mControlPathPrefix( "" )
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 
72 {
73  mMapSettings = thepMapRenderer->mapSettings();
74 }
75 
77 {
78  mMapSettings = mapSettings;
79 }
80 
81 bool QgsRenderChecker::isKnownAnomaly( QString theDiffImageFile )
82 {
83  QString myControlImageDir = controlImagePath() + mControlName
84  + QDir::separator();
85  QDir myDirectory = QDir( myControlImageDir );
86  QStringList myList;
87  QString myFilename = "*";
88  myList = myDirectory.entryList( QStringList( myFilename ),
89  QDir::Files | QDir::NoSymLinks );
90  //remove the control file from the list as the anomalies are
91  //all files except the control file
92  myList.removeAt( myList.indexOf( QFileInfo( mExpectedImageFile ).fileName() ) );
93 
94  QString myImageHash = imageToHash( theDiffImageFile );
95 
96 
97  for ( int i = 0; i < myList.size(); ++i )
98  {
99  QString myFile = myList.at( i );
100  mReport += "<tr><td colspan=3>"
101  "Checking if " + myFile + " is a known anomaly.";
102  mReport += "</td></tr>";
103  QString myAnomalyHash = imageToHash( controlImagePath() + mControlName
104  + QDir::separator() + myFile );
105  QString myHashMessage = QString(
106  "Checking if anomaly %1 (hash %2)<br>" )
107  .arg( myFile )
108  .arg( myAnomalyHash );
109  myHashMessage += QString( "&nbsp; matches %1 (hash %2)" )
110  .arg( theDiffImageFile )
111  .arg( myImageHash );
112  //foo CDash
113  QString myMeasureMessage = "<DartMeasurement name=\"Anomaly check"
114  "\" type=\"text/text\">" + myHashMessage +
115  "</DartMeasurement>";
116  qDebug() << myMeasureMessage;
117  mReport += "<tr><td colspan=3>" + myHashMessage + "</td></tr>";
118  if ( myImageHash == myAnomalyHash )
119  {
120  mReport += "<tr><td colspan=3>"
121  "Anomaly found! " + myFile;
122  mReport += "</td></tr>";
123  return true;
124  }
125  }
126  mReport += "<tr><td colspan=3>"
127  "No anomaly found! ";
128  mReport += "</td></tr>";
129  return false;
130 }
131 
132 bool QgsRenderChecker::runTest( QString theTestName,
133  unsigned int theMismatchCount )
134 {
135  if ( mExpectedImageFile.isEmpty() )
136  {
137  qDebug( "QgsRenderChecker::runTest failed - Expected Image File not set." );
138  mReport = "<table>"
139  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
140  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
141  "Image File not set.</td></tr></table>\n";
142  return false;
143  }
144  //
145  // Load the expected result pixmap
146  //
147  QImage myExpectedImage( mExpectedImageFile );
148  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
149  //
150  // Now render our layers onto a pixmap
151  //
152  mMapSettings.setBackgroundColor( qRgb( 152, 219, 249 ) );
154  mMapSettings.setOutputSize( QSize( myExpectedImage.width(), myExpectedImage.height() ) );
155 
156  QTime myTime;
157  myTime.start();
158 
160  job.start();
161  job.waitForFinished();
162 
163  mElapsedTime = myTime.elapsed();
164 
165  QImage myImage = job.renderedImage();
166 
167  //
168  // Save the pixmap to disk so the user can make a
169  // visual assessment if needed
170  //
171  mRenderedImageFile = QDir::tempPath() + QDir::separator() +
172  theTestName + "_result.png";
173 
174  myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
175  myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
176  myImage.save( mRenderedImageFile, "PNG", 100 );
177 
178  //create a world file to go with the image...
179 
180  QFile wldFile( QDir::tempPath() + QDir::separator() + theTestName + "_result.wld" );
181  if ( wldFile.open( QIODevice::WriteOnly ) )
182  {
184 
185  QTextStream stream( &wldFile );
186  stream << QString( "%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
190  .arg( qgsDoubleToString( r.yMaximum() - mMapSettings.mapUnitsPerPixel() / 2.0 ) );
191  }
192 
193  return compareImages( theTestName, theMismatchCount );
194 }
195 
196 
197 bool QgsRenderChecker::compareImages( QString theTestName,
198  unsigned int theMismatchCount,
199  QString theRenderedImageFile )
200 {
201  if ( mExpectedImageFile.isEmpty() )
202  {
203  qDebug( "QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
204  mReport = "<table>"
205  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
206  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
207  "Image File not set.</td></tr></table>\n";
208  return false;
209  }
210  if ( ! theRenderedImageFile.isEmpty() )
211  {
212  mRenderedImageFile = theRenderedImageFile;
213  }
214  if ( mRenderedImageFile.isEmpty() )
215  {
216  qDebug( "QgsRenderChecker::runTest failed - Rendered Image File not set." );
217  mReport = "<table>"
218  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
219  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
220  "Image File not set.</td></tr></table>\n";
221  return false;
222  }
223  //
224  // Load /create the images
225  //
226  QImage myExpectedImage( mExpectedImageFile );
227  QImage myResultImage( mRenderedImageFile );
228  QImage myDifferenceImage( myExpectedImage.width(),
229  myExpectedImage.height(),
230  QImage::Format_RGB32 );
231  QString myDiffImageFile = QDir::tempPath() + QDir::separator() +
232  QDir::separator() +
233  theTestName + "_result_diff.png";
234  myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
235 
236  //
237  // Set pixel count score and target
238  //
239  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
240  unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
241  //
242  // Set the report with the result
243  //
244  mReport = "<table>";
245  mReport += "<tr><td colspan=2>";
246  mReport += "Test image and result image for " + theTestName + "<br>"
247  "Expected size: " + QString::number( myExpectedImage.width() ).toLocal8Bit() + "w x " +
248  QString::number( myExpectedImage.height() ).toLocal8Bit() + "h (" +
249  QString::number( mMatchTarget ).toLocal8Bit() + " pixels)<br>"
250  "Actual size: " + QString::number( myResultImage.width() ).toLocal8Bit() + "w x " +
251  QString::number( myResultImage.height() ).toLocal8Bit() + "h (" +
252  QString::number( myPixelCount ).toLocal8Bit() + " pixels)";
253  mReport += "</td></tr>";
254  mReport += "<tr><td colspan = 2>\n";
255  mReport += "Expected Duration : <= " + QString::number( mElapsedTimeTarget ) +
256  "ms (0 indicates not specified)<br>";
257  mReport += "Actual Duration : " + QString::number( mElapsedTime ) + "ms<br>";
258 
259  // limit image size in page to something reasonable
260  int imgWidth = 420;
261  int imgHeight = 280;
262  if ( ! myExpectedImage.isNull() )
263  {
264  imgWidth = qMin( myExpectedImage.width(), imgWidth );
265  imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
266  }
267  QString myImagesString = "</td></tr>"
268  "<tr><td>Test Result:</td><td>Expected Result:</td><td>Difference (all blue is good, any red is bad)</td></tr>\n"
269  "<tr><td><img width=" + QString::number( imgWidth ) +
270  " height=" + QString::number( imgHeight ) +
271  " src=\"file://" +
273  "\"></td>\n<td><img width=" + QString::number( imgWidth ) +
274  " height=" + QString::number( imgHeight ) +
275  " src=\"file://" +
277  "\"></td>\n<td><img width=" + QString::number( imgWidth ) +
278  " height=" + QString::number( imgHeight ) +
279  " src=\"file://" +
280  myDiffImageFile +
281  "\"></td>\n</tr>\n</table>";
282  //
283  // To get the images into CDash
284  //
285  QString myDashMessage = "<DartMeasurementFile name=\"Rendered Image " + theTestName + "\""
286  " type=\"image/png\">" + mRenderedImageFile +
287  "</DartMeasurementFile>\n"
288  "<DartMeasurementFile name=\"Expected Image " + theTestName + "\" type=\"image/png\">" +
289  mExpectedImageFile + "</DartMeasurementFile>\n"
290  "<DartMeasurementFile name=\"Difference Image " + theTestName + "\" type=\"image/png\">" +
291  myDiffImageFile + "</DartMeasurementFile>\n";
292  qDebug( ) << myDashMessage;
293 
294  //
295  // Put the same info to debug too
296  //
297 
298  qDebug( "Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
299  qDebug( "Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
300 
301  if ( mMatchTarget != myPixelCount )
302  {
303  qDebug( "Test image and result image for %s are different - FAILING!", theTestName.toLocal8Bit().constData() );
304  mReport += "<tr><td colspan=3>";
305  mReport += "<font color=red>Expected image and result image for " + theTestName + " are different dimensions - FAILING!</font>";
306  mReport += "</td></tr>";
307  mReport += myImagesString;
308  return false;
309  }
310 
311  //
312  // Now iterate through them counting how many
313  // dissimilar pixel values there are
314  //
315 
316  mMismatchCount = 0;
317  int colorTolerance = ( int ) mColorTolerance;
318  for ( int x = 0; x < myExpectedImage.width(); ++x )
319  {
320  for ( int y = 0; y < myExpectedImage.height(); ++y )
321  {
322  QRgb myExpectedPixel = myExpectedImage.pixel( x, y );
323  QRgb myActualPixel = myResultImage.pixel( x, y );
324  if ( mColorTolerance == 0 )
325  {
326  if ( myExpectedPixel != myActualPixel )
327  {
328  ++mMismatchCount;
329  myDifferenceImage.setPixel( x, y, qRgb( 255, 0, 0 ) );
330  }
331  }
332  else
333  {
334  if ( qAbs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > colorTolerance ||
335  qAbs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > colorTolerance ||
336  qAbs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > colorTolerance ||
337  qAbs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > colorTolerance )
338  {
339  ++mMismatchCount;
340  myDifferenceImage.setPixel( x, y, qRgb( 255, 0, 0 ) );
341  }
342  }
343  }
344  }
345  //
346  //save the diff image to disk
347  //
348  myDifferenceImage.save( myDiffImageFile );
349 
350  //
351  // Send match result to debug
352  //
353  qDebug( "%d/%d pixels mismatched (%d allowed)", mMismatchCount, mMatchTarget, theMismatchCount );
354 
355  //
356  // Send match result to report
357  //
358  mReport += "<tr><td colspan=3>" +
359  QString::number( mMismatchCount ) + "/" +
360  QString::number( mMatchTarget ) +
361  " pixels mismatched (allowed threshold: " +
362  QString::number( theMismatchCount ) +
363  ", allowed color component tolerance: " +
364  QString::number( mColorTolerance ) + ")";
365  mReport += "</td></tr>";
366 
367  //
368  // And send it to CDash
369  //
370  myDashMessage = "<DartMeasurement name=\"Mismatch Count "
371  "\" type=\"numeric/integer\">" +
372  QString::number( mMismatchCount ) + "/" +
373  QString::number( mMatchTarget ) +
374  "</DartMeasurement>";
375  qDebug( ) << myDashMessage;
376 
377  bool myAnomalyMatchFlag = isKnownAnomaly( myDiffImageFile );
378 
379  if ( myAnomalyMatchFlag )
380  {
381  mReport += "<tr><td colspan=3>"
382  "Difference image matched a known anomaly - passing test! "
383  "</td></tr>";
384  return true;
385  }
386  else
387  {
388  QString myMessage = "Difference image did not match any known anomaly.";
389  mReport += "<tr><td colspan=3>"
390  "</td></tr>";
391  QString myMeasureMessage = "<DartMeasurement name=\"No Anomalies Match"
392  "\" type=\"text/text\">" + myMessage +
393  " If you feel the difference image should be considered an anomaly "
394  "you can do something like this\n"
395  "cp " + myDiffImageFile + " ../tests/testdata/control_images/" + theTestName +
396  "/<imagename>.{wld,png}"
397  "</DartMeasurement>";
398  qDebug() << myMeasureMessage;
399  }
400 
401  if ( mMismatchCount <= theMismatchCount )
402  {
403  mReport += "<tr><td colspan = 3>\n";
404  mReport += "Test image and result image for " + theTestName + " are matched<br>";
405  mReport += "</td></tr>";
407  {
408  //test failed because it took too long...
409  qDebug( "Test failed because render step took too long" );
410  mReport += "<tr><td colspan = 3>\n";
411  mReport += "<font color=red>Test failed because render step took too long</font>";
412  mReport += "</td></tr>";
413  mReport += myImagesString;
414  return false;
415  }
416  else
417  {
418  mReport += myImagesString;
419  return true;
420  }
421  }
422  else
423  {
424  mReport += "<tr><td colspan = 3>\n";
425  mReport += "<font color=red>Test image and result image for " + theTestName + " are mismatched</font><br>";
426  mReport += "</td></tr>";
427  mReport += myImagesString;
428  return false;
429  }
430 }
const QgsMapSettings & mapSettings()
bridge to QgsMapSettings
A rectangle specified with double values.
Definition: qgsrectangle.h:35
virtual void waitForFinished()
Block until the job has finished.
double yMaximum() const
Get the y maximum value (top side of rectangle)
Definition: qgsrectangle.h:194
QString qgsDoubleToString(const double &a)
Definition: qgis.h:316
virtual QImage renderedImage()
Get a preview/resulting image.
Q_DECL_DEPRECATED void setMapRenderer(QgsMapRenderer *thepMapRenderer)
bool runTest(QString theTestName, unsigned int theMismatchCount=0)
Test using renderer to generate the image to be compared.
A non GUI class for rendering a map layer set onto a QPainter.
void setMapSettings(const QgsMapSettings &mapSettings)
void setFlag(Flag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
The QgsMapSettings class contains configuration for rendering of the map.
unsigned int mMismatchCount
QString controlImagePath() const
QString imageToHash(QString theImageFile)
Get an md5 hash that uniquely identifies an image.
Enable anti-aliasin for map rendering.
double mapUnitsPerPixel() const
Return the distance in geographical coordinates that equals to one pixel in the map.
unsigned int mMatchTarget
Job implementation that renders everything sequentially in one thread.
QgsMapSettings mMapSettings
void setBackgroundColor(const QColor &color)
Set the background color of the map.
unsigned int mColorTolerance
bool isKnownAnomaly(QString theDiffImageFile)
Get a list of all the anomalies.
void setOutputSize(const QSize &size)
Set the size of the resulting map image.
QgsRectangle extent() const
Return geographical coordinates of the rectangle that should be rendered.
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:189
virtual void start()
Start the rendering job and immediately return.