QGIS API Documentation  2.7.0-Master
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules 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  mBufferDashMessages( false )
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() + mControlPathSuffix
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 void QgsRenderChecker::drawBackground( QImage* image )
82 {
83  // create a 2x2 checker-board image
84  uchar pixDataRGB[] = { 255, 255, 255, 255,
85  127, 127, 127, 255,
86  127, 127, 127, 255,
87  255, 255, 255, 255
88  };
89 
90  QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
91  QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
92 
93  // fill image with texture
94  QBrush brush;
95  brush.setTexture( pix );
96  QPainter p( image );
97  p.setRenderHint( QPainter::Antialiasing, false );
98  p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
99  p.end();
100 }
101 
102 bool QgsRenderChecker::isKnownAnomaly( QString theDiffImageFile )
103 {
104  QString myControlImageDir = controlImagePath() + mControlName
105  + QDir::separator();
106  QDir myDirectory = QDir( myControlImageDir );
107  QStringList myList;
108  QString myFilename = "*";
109  myList = myDirectory.entryList( QStringList( myFilename ),
110  QDir::Files | QDir::NoSymLinks );
111  //remove the control file from the list as the anomalies are
112  //all files except the control file
113  myList.removeAt( myList.indexOf( QFileInfo( mExpectedImageFile ).fileName() ) );
114 
115  QString myImageHash = imageToHash( theDiffImageFile );
116 
117 
118  for ( int i = 0; i < myList.size(); ++i )
119  {
120  QString myFile = myList.at( i );
121  mReport += "<tr><td colspan=3>"
122  "Checking if " + myFile + " is a known anomaly.";
123  mReport += "</td></tr>";
124  QString myAnomalyHash = imageToHash( controlImagePath() + mControlName
125  + QDir::separator() + myFile );
126  QString myHashMessage = QString(
127  "Checking if anomaly %1 (hash %2)<br>" )
128  .arg( myFile )
129  .arg( myAnomalyHash );
130  myHashMessage += QString( "&nbsp; matches %1 (hash %2)" )
131  .arg( theDiffImageFile )
132  .arg( myImageHash );
133  //foo CDash
134  emitDashMessage( "Anomaly check", QgsDartMeasurement::Text, myHashMessage );
135 
136  mReport += "<tr><td colspan=3>" + myHashMessage + "</td></tr>";
137  if ( myImageHash == myAnomalyHash )
138  {
139  mReport += "<tr><td colspan=3>"
140  "Anomaly found! " + myFile;
141  mReport += "</td></tr>";
142  return true;
143  }
144  }
145  mReport += "<tr><td colspan=3>"
146  "No anomaly found! ";
147  mReport += "</td></tr>";
148  return false;
149 }
150 
151 void QgsRenderChecker::emitDashMessage( const QgsDartMeasurement& dashMessage )
152 {
153  if ( mBufferDashMessages )
154  mDashMessages << dashMessage;
155  else
156  dashMessage.send();
157 }
158 
159 void QgsRenderChecker::emitDashMessage( const QString& name, QgsDartMeasurement::Type type, const QString& value )
160 {
161  emitDashMessage( QgsDartMeasurement( name, type, value ) );
162 }
163 
164 bool QgsRenderChecker::runTest( QString theTestName,
165  unsigned int theMismatchCount )
166 {
167  if ( mExpectedImageFile.isEmpty() )
168  {
169  qDebug( "QgsRenderChecker::runTest failed - Expected Image File not set." );
170  mReport = "<table>"
171  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
172  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
173  "Image File not set.</td></tr></table>\n";
174  return false;
175  }
176  //
177  // Load the expected result pixmap
178  //
179  QImage myExpectedImage( mExpectedImageFile );
180  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
181  //
182  // Now render our layers onto a pixmap
183  //
184  mMapSettings.setBackgroundColor( qRgb( 152, 219, 249 ) );
185  mMapSettings.setFlag( QgsMapSettings::Antialiasing );
186  mMapSettings.setOutputSize( QSize( myExpectedImage.width(), myExpectedImage.height() ) );
187 
188  QTime myTime;
189  myTime.start();
190 
191  QgsMapRendererSequentialJob job( mMapSettings );
192  job.start();
193  job.waitForFinished();
194 
195  mElapsedTime = myTime.elapsed();
196 
197  QImage myImage = job.renderedImage();
198 
199  //
200  // Save the pixmap to disk so the user can make a
201  // visual assessment if needed
202  //
203  mRenderedImageFile = QDir::tempPath() + QDir::separator() +
204  theTestName + "_result.png";
205 
206  myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
207  myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
208  myImage.save( mRenderedImageFile, "PNG", 100 );
209 
210  //create a world file to go with the image...
211 
212  QFile wldFile( QDir::tempPath() + QDir::separator() + theTestName + "_result.wld" );
213  if ( wldFile.open( QIODevice::WriteOnly ) )
214  {
215  QgsRectangle r = mMapSettings.extent();
216 
217  QTextStream stream( &wldFile );
218  stream << QString( "%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
219  .arg( qgsDoubleToString( mMapSettings.mapUnitsPerPixel() ) )
220  .arg( qgsDoubleToString( -mMapSettings.mapUnitsPerPixel() ) )
221  .arg( qgsDoubleToString( r.xMinimum() + mMapSettings.mapUnitsPerPixel() / 2.0 ) )
222  .arg( qgsDoubleToString( r.yMaximum() - mMapSettings.mapUnitsPerPixel() / 2.0 ) );
223  }
224 
225  return compareImages( theTestName, theMismatchCount );
226 }
227 
228 
229 bool QgsRenderChecker::compareImages( QString theTestName,
230  unsigned int theMismatchCount,
231  QString theRenderedImageFile )
232 {
233  if ( mExpectedImageFile.isEmpty() )
234  {
235  qDebug( "QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
236  mReport = "<table>"
237  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
238  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
239  "Image File not set.</td></tr></table>\n";
240  return false;
241  }
242  if ( ! theRenderedImageFile.isEmpty() )
243  {
244  mRenderedImageFile = theRenderedImageFile;
245  }
246  if ( mRenderedImageFile.isEmpty() )
247  {
248  qDebug( "QgsRenderChecker::runTest failed - Rendered Image File not set." );
249  mReport = "<table>"
250  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
251  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
252  "Image File not set.</td></tr></table>\n";
253  return false;
254  }
255  //
256  // Load /create the images
257  //
258  QImage myExpectedImage( mExpectedImageFile );
259  QImage myResultImage( mRenderedImageFile );
260  QImage myDifferenceImage( myExpectedImage.width(),
261  myExpectedImage.height(),
262  QImage::Format_RGB32 );
263  QString myDiffImageFile = QDir::tempPath() + QDir::separator() +
264  QDir::separator() +
265  theTestName + "_result_diff.png";
266  myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
267 
268  //
269  // Set pixel count score and target
270  //
271  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
272  unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
273  //
274  // Set the report with the result
275  //
276  mReport = "<table>";
277  mReport += "<tr><td colspan=2>";
278  mReport += "Test image and result image for " + theTestName + "<br>"
279  "Expected size: " + QString::number( myExpectedImage.width() ).toLocal8Bit() + "w x " +
280  QString::number( myExpectedImage.height() ).toLocal8Bit() + "h (" +
281  QString::number( mMatchTarget ).toLocal8Bit() + " pixels)<br>"
282  "Actual size: " + QString::number( myResultImage.width() ).toLocal8Bit() + "w x " +
283  QString::number( myResultImage.height() ).toLocal8Bit() + "h (" +
284  QString::number( myPixelCount ).toLocal8Bit() + " pixels)";
285  mReport += "</td></tr>";
286  mReport += "<tr><td colspan = 2>\n";
287  mReport += "Expected Duration : <= " + QString::number( mElapsedTimeTarget ) +
288  "ms (0 indicates not specified)<br>";
289  mReport += "Actual Duration : " + QString::number( mElapsedTime ) + "ms<br>";
290 
291  // limit image size in page to something reasonable
292  int imgWidth = 420;
293  int imgHeight = 280;
294  if ( ! myExpectedImage.isNull() )
295  {
296  imgWidth = qMin( myExpectedImage.width(), imgWidth );
297  imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
298  }
299  QString myImagesString = "</td></tr>"
300  "<tr><td>Test Result:</td><td>Expected Result:</td><td>Difference (all blue is good, any red is bad)</td></tr>\n"
301  "<tr><td><img width=" + QString::number( imgWidth ) +
302  " height=" + QString::number( imgHeight ) +
303  " src=\"file://" +
305  "\"></td>\n<td><img width=" + QString::number( imgWidth ) +
306  " height=" + QString::number( imgHeight ) +
307  " src=\"file://" +
309  "\"></td>\n<td><img width=" + QString::number( imgWidth ) +
310  " height=" + QString::number( imgHeight ) +
311  " src=\"file://" +
312  myDiffImageFile +
313  "\"></td>\n</tr>\n</table>";
314 
315  QString prefix;
316  if ( !mControlPathPrefix.isNull() )
317  {
318  prefix = QString( " (prefix %1)" ).arg( mControlPathPrefix );
319  }
320  //
321  // To get the images into CDash
322  //
323  emitDashMessage( "Rendered Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, mRenderedImageFile );
324  emitDashMessage( "Expected Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, mExpectedImageFile );
325  emitDashMessage( "Difference Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, myDiffImageFile );
326 
327  //
328  // Put the same info to debug too
329  //
330 
331  qDebug( "Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
332  qDebug( "Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
333 
334  if ( mMatchTarget != myPixelCount )
335  {
336  qDebug( "Test image and result image for %s are different - FAILING!", theTestName.toLocal8Bit().constData() );
337  mReport += "<tr><td colspan=3>";
338  mReport += "<font color=red>Expected image and result image for " + theTestName + " are different dimensions - FAILING!</font>";
339  mReport += "</td></tr>";
340  mReport += myImagesString;
341  return false;
342  }
343 
344  //
345  // Now iterate through them counting how many
346  // dissimilar pixel values there are
347  //
348 
349  mMismatchCount = 0;
350  int colorTolerance = ( int ) mColorTolerance;
351  for ( int x = 0; x < myExpectedImage.width(); ++x )
352  {
353  for ( int y = 0; y < myExpectedImage.height(); ++y )
354  {
355  QRgb myExpectedPixel = myExpectedImage.pixel( x, y );
356  QRgb myActualPixel = myResultImage.pixel( x, y );
357  if ( mColorTolerance == 0 )
358  {
359  if ( myExpectedPixel != myActualPixel )
360  {
361  ++mMismatchCount;
362  myDifferenceImage.setPixel( x, y, qRgb( 255, 0, 0 ) );
363  }
364  }
365  else
366  {
367  if ( qAbs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > colorTolerance ||
368  qAbs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > colorTolerance ||
369  qAbs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > colorTolerance ||
370  qAbs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > colorTolerance )
371  {
372  ++mMismatchCount;
373  myDifferenceImage.setPixel( x, y, qRgb( 255, 0, 0 ) );
374  }
375  }
376  }
377  }
378  //
379  //save the diff image to disk
380  //
381  myDifferenceImage.save( myDiffImageFile );
382 
383  //
384  // Send match result to debug
385  //
386  qDebug( "%d/%d pixels mismatched (%d allowed)", mMismatchCount, mMatchTarget, theMismatchCount );
387 
388  //
389  // Send match result to report
390  //
391  mReport += "<tr><td colspan=3>" +
392  QString::number( mMismatchCount ) + "/" +
393  QString::number( mMatchTarget ) +
394  " pixels mismatched (allowed threshold: " +
395  QString::number( theMismatchCount ) +
396  ", allowed color component tolerance: " +
397  QString::number( mColorTolerance ) + ")";
398  mReport += "</td></tr>";
399 
400  //
401  // And send it to CDash
402  //
403  emitDashMessage( "Mismatch Count", QgsDartMeasurement::Integer, QString( "%1/%2" ).arg( mMismatchCount ).arg( mMatchTarget ) );
404 
405  bool myAnomalyMatchFlag = isKnownAnomaly( myDiffImageFile );
406 
407  if ( myAnomalyMatchFlag )
408  {
409  mReport += "<tr><td colspan=3>"
410  "Difference image matched a known anomaly - passing test! "
411  "</td></tr>";
412  return true;
413  }
414  else
415  {
416  mReport += "<tr><td colspan=3>"
417  "</td></tr>";
418  emitDashMessage( "No Anomalies Match", QgsDartMeasurement::Text, "Difference image did not match any known anomaly."
419  " If you feel the difference image should be considered an anomaly "
420  "you can do something like this\n"
421  "cp " + myDiffImageFile + " ../tests/testdata/control_images/" + theTestName +
422  "/<imagename>.{wld,png}" );
423  }
424 
425  if ( mMismatchCount <= theMismatchCount )
426  {
427  mReport += "<tr><td colspan = 3>\n";
428  mReport += "Test image and result image for " + theTestName + " are matched<br>";
429  mReport += "</td></tr>";
430  if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < mElapsedTime )
431  {
432  //test failed because it took too long...
433  qDebug( "Test failed because render step took too long" );
434  mReport += "<tr><td colspan = 3>\n";
435  mReport += "<font color=red>Test failed because render step took too long</font>";
436  mReport += "</td></tr>";
437  mReport += myImagesString;
438  return false;
439  }
440  else
441  {
442  mReport += myImagesString;
443  return true;
444  }
445  }
446  else
447  {
448  mReport += "<tr><td colspan = 3>\n";
449  mReport += "<font color=red>Test image and result image for " + theTestName + " are mismatched</font><br>";
450  mReport += "</td></tr>";
451  mReport += myImagesString;
452  return false;
453  }
454 }
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.
void setControlName(const QString &theName)
Base directory name for the control image (with control image path suffixed) the path to the image wi...
double yMaximum() const
Get the y maximum value (top side of rectangle)
Definition: qgsrectangle.h:188
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.
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
QString qgsDoubleToString(const double &a, const int &precision=17)
Definition: qgis.h:317
static void drawBackground(QImage *image)
Draws a checkboard pattern for image backgrounds, so that transparency is visible without requiring a...
Job implementation that renders everything sequentially in one thread.
void setBackgroundColor(const QColor &color)
Set the background color of the map.
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.
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:183
virtual void start()
Start the rendering job and immediately return.