QGIS API Documentation  2.9.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  if ( myExpectedImage.isNull() )
181  {
182  qDebug() << "QgsRenderChecker::runTest failed - Could not load expected image from " << mExpectedImageFile;
183  mReport = "<table>"
184  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
185  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
186  "Image File could not be loaded.</td></tr></table>\n";
187  return false;
188  }
189  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
190  //
191  // Now render our layers onto a pixmap
192  //
193  mMapSettings.setBackgroundColor( qRgb( 152, 219, 249 ) );
194  mMapSettings.setFlag( QgsMapSettings::Antialiasing );
195  mMapSettings.setOutputSize( QSize( myExpectedImage.width(), myExpectedImage.height() ) );
196 
197  QTime myTime;
198  myTime.start();
199 
200  QgsMapRendererSequentialJob job( mMapSettings );
201  job.start();
202  job.waitForFinished();
203 
204  mElapsedTime = myTime.elapsed();
205 
206  QImage myImage = job.renderedImage();
207 
208  //
209  // Save the pixmap to disk so the user can make a
210  // visual assessment if needed
211  //
212  mRenderedImageFile = QDir::tempPath() + QDir::separator() +
213  theTestName + "_result.png";
214 
215  myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
216  myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
217  if ( ! myImage.save( mRenderedImageFile, "PNG", 100 ) )
218  {
219  qDebug() << "QgsRenderChecker::runTest failed - Could not save rendered image to " << mRenderedImageFile;
220  mReport = "<table>"
221  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
222  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
223  "Image File could not be saved.</td></tr></table>\n";
224  return false;
225  }
226 
227  //create a world file to go with the image...
228 
229  QFile wldFile( QDir::tempPath() + QDir::separator() + theTestName + "_result.wld" );
230  if ( wldFile.open( QIODevice::WriteOnly ) )
231  {
232  QgsRectangle r = mMapSettings.extent();
233 
234  QTextStream stream( &wldFile );
235  stream << QString( "%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
236  .arg( qgsDoubleToString( mMapSettings.mapUnitsPerPixel() ) )
237  .arg( qgsDoubleToString( -mMapSettings.mapUnitsPerPixel() ) )
238  .arg( qgsDoubleToString( r.xMinimum() + mMapSettings.mapUnitsPerPixel() / 2.0 ) )
239  .arg( qgsDoubleToString( r.yMaximum() - mMapSettings.mapUnitsPerPixel() / 2.0 ) );
240  }
241 
242  return compareImages( theTestName, theMismatchCount );
243 }
244 
245 
246 bool QgsRenderChecker::compareImages( QString theTestName,
247  unsigned int theMismatchCount,
248  QString theRenderedImageFile )
249 {
250  if ( mExpectedImageFile.isEmpty() )
251  {
252  qDebug( "QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
253  mReport = "<table>"
254  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
255  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
256  "Image File not set.</td></tr></table>\n";
257  return false;
258  }
259  if ( ! theRenderedImageFile.isEmpty() )
260  {
261  mRenderedImageFile = theRenderedImageFile;
262  }
263  else if ( mRenderedImageFile.isEmpty() )
264  {
265  qDebug( "QgsRenderChecker::runTest failed - Rendered Image File not set." );
266  mReport = "<table>"
267  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
268  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
269  "Image File not set.</td></tr></table>\n";
270  return false;
271  }
272  //
273  // Load /create the images
274  //
275  QImage myExpectedImage( mExpectedImageFile );
276  QImage myResultImage( mRenderedImageFile );
277  if ( myResultImage.isNull() )
278  {
279  qDebug() << "QgsRenderChecker::runTest failed - Could not load rendered image from " << mRenderedImageFile;
280  mReport = "<table>"
281  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
282  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
283  "Image File could not be loaded.</td></tr></table>\n";
284  return false;
285  }
286  QImage myDifferenceImage( myExpectedImage.width(),
287  myExpectedImage.height(),
288  QImage::Format_RGB32 );
289  QString myDiffImageFile = QDir::tempPath() +
290  QDir::separator() +
291  theTestName + "_result_diff.png";
292  myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
293 
294  //check for mask
295  QString maskImagePath = mExpectedImageFile;
296  maskImagePath.chop( 4 ); //remove .png extension
297  maskImagePath += "_mask.png";
298  QImage* maskImage = new QImage( maskImagePath );
299  bool hasMask = !maskImage->isNull();
300  if ( hasMask )
301  {
302  qDebug( "QgsRenderChecker using mask image" );
303  }
304 
305  //
306  // Set pixel count score and target
307  //
308  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
309  unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
310  //
311  // Set the report with the result
312  //
313  mReport = "<table>";
314  mReport += "<tr><td colspan=2>";
315  mReport += "Test image and result image for " + theTestName + "<br>"
316  "Expected size: " + QString::number( myExpectedImage.width() ).toLocal8Bit() + "w x " +
317  QString::number( myExpectedImage.height() ).toLocal8Bit() + "h (" +
318  QString::number( mMatchTarget ).toLocal8Bit() + " pixels)<br>"
319  "Actual size: " + QString::number( myResultImage.width() ).toLocal8Bit() + "w x " +
320  QString::number( myResultImage.height() ).toLocal8Bit() + "h (" +
321  QString::number( myPixelCount ).toLocal8Bit() + " pixels)";
322  mReport += "</td></tr>";
323  mReport += "<tr><td colspan = 2>\n";
324  mReport += "Expected Duration : <= " + QString::number( mElapsedTimeTarget ) +
325  "ms (0 indicates not specified)<br>";
326  mReport += "Actual Duration : " + QString::number( mElapsedTime ) + "ms<br>";
327 
328  // limit image size in page to something reasonable
329  int imgWidth = 420;
330  int imgHeight = 280;
331  if ( ! myExpectedImage.isNull() )
332  {
333  imgWidth = qMin( myExpectedImage.width(), imgWidth );
334  imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
335  }
336  QString myImagesString = "</td></tr>"
337  "<tr><td>Test Result:</td><td>Expected Result:</td><td>Difference (all blue is good, any red is bad)</td></tr>\n"
338  "<tr><td><img width=" + QString::number( imgWidth ) +
339  " height=" + QString::number( imgHeight ) +
340  " src=\"file://" +
342  "\"></td>\n<td><img width=" + QString::number( imgWidth ) +
343  " height=" + QString::number( imgHeight ) +
344  " src=\"file://" +
346  "\"></td>\n<td><img width=" + QString::number( imgWidth ) +
347  " height=" + QString::number( imgHeight ) +
348  " src=\"file://" +
349  myDiffImageFile +
350  "\"></td>\n</tr>\n</table>";
351 
352  QString prefix;
353  if ( !mControlPathPrefix.isNull() )
354  {
355  prefix = QString( " (prefix %1)" ).arg( mControlPathPrefix );
356  }
357  //
358  // To get the images into CDash
359  //
360  emitDashMessage( "Rendered Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, mRenderedImageFile );
361  emitDashMessage( "Expected Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, mExpectedImageFile );
362  emitDashMessage( "Difference Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, myDiffImageFile );
363 
364  //
365  // Put the same info to debug too
366  //
367 
368  qDebug( "Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
369  qDebug( "Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
370 
371  if ( mMatchTarget != myPixelCount )
372  {
373  qDebug( "Test image and result image for %s are different - FAILING!", theTestName.toLocal8Bit().constData() );
374  mReport += "<tr><td colspan=3>";
375  mReport += "<font color=red>Expected image and result image for " + theTestName + " are different dimensions - FAILING!</font>";
376  mReport += "</td></tr>";
377  mReport += myImagesString;
378  delete maskImage;
379  return false;
380  }
381 
382  //
383  // Now iterate through them counting how many
384  // dissimilar pixel values there are
385  //
386 
387  mMismatchCount = 0;
388  int colorTolerance = ( int ) mColorTolerance;
389  for ( int y = 0; y < myExpectedImage.height(); ++y )
390  {
391  const QRgb* expectedScanline = ( const QRgb* )myExpectedImage.constScanLine( y );
392  const QRgb* resultScanline = ( const QRgb* )myResultImage.constScanLine( y );
393  const QRgb* maskScanline = hasMask ? ( const QRgb* )maskImage->constScanLine( y ) : 0;
394  QRgb* diffScanline = ( QRgb* )myDifferenceImage.scanLine( y );
395 
396  for ( int x = 0; x < myExpectedImage.width(); ++x )
397  {
398  int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
399  int pixelTolerance = qMax( colorTolerance, maskTolerance );
400  if ( pixelTolerance == 255 )
401  {
402  //skip pixel
403  continue;
404  }
405 
406  QRgb myExpectedPixel = expectedScanline[x];
407  QRgb myActualPixel = resultScanline[x];
408  if ( pixelTolerance == 0 )
409  {
410  if ( myExpectedPixel != myActualPixel )
411  {
412  ++mMismatchCount;
413  diffScanline[ x ] = qRgb( 255, 0, 0 );
414  }
415  }
416  else
417  {
418  if ( qAbs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
419  qAbs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
420  qAbs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
421  qAbs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
422  {
423  ++mMismatchCount;
424  diffScanline[ x ] = qRgb( 255, 0, 0 );
425  }
426  }
427  }
428  }
429  //
430  //save the diff image to disk
431  //
432  myDifferenceImage.save( myDiffImageFile );
433  delete maskImage;
434 
435  //
436  // Send match result to debug
437  //
438  qDebug( "%d/%d pixels mismatched (%d allowed)", mMismatchCount, mMatchTarget, theMismatchCount );
439 
440  //
441  // Send match result to report
442  //
443  mReport += "<tr><td colspan=3>" +
444  QString::number( mMismatchCount ) + "/" +
445  QString::number( mMatchTarget ) +
446  " pixels mismatched (allowed threshold: " +
447  QString::number( theMismatchCount ) +
448  ", allowed color component tolerance: " +
449  QString::number( mColorTolerance ) + ")";
450  mReport += "</td></tr>";
451 
452  //
453  // And send it to CDash
454  //
455  emitDashMessage( "Mismatch Count", QgsDartMeasurement::Integer, QString( "%1/%2" ).arg( mMismatchCount ).arg( mMatchTarget ) );
456 
457  bool myAnomalyMatchFlag = isKnownAnomaly( myDiffImageFile );
458 
459  if ( myAnomalyMatchFlag )
460  {
461  mReport += "<tr><td colspan=3>"
462  "Difference image matched a known anomaly - passing test! "
463  "</td></tr>";
464  return true;
465  }
466  else
467  {
468  mReport += "<tr><td colspan=3>"
469  "</td></tr>";
470  emitDashMessage( "No Anomalies Match", QgsDartMeasurement::Text, "Difference image did not match any known anomaly."
471  " If you feel the difference image should be considered an anomaly "
472  "you can do something like this\n"
473  "cp " + myDiffImageFile + " ../tests/testdata/control_images/" + theTestName +
474  "/<imagename>.{wld,png}" );
475  }
476 
477  if ( mMismatchCount <= theMismatchCount )
478  {
479  mReport += "<tr><td colspan = 3>\n";
480  mReport += "Test image and result image for " + theTestName + " are matched<br>";
481  mReport += "</td></tr>";
482  if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < mElapsedTime )
483  {
484  //test failed because it took too long...
485  qDebug( "Test failed because render step took too long" );
486  mReport += "<tr><td colspan = 3>\n";
487  mReport += "<font color=red>Test failed because render step took too long</font>";
488  mReport += "</td></tr>";
489  mReport += myImagesString;
490  return false;
491  }
492  else
493  {
494  mReport += myImagesString;
495  return true;
496  }
497  }
498  else
499  {
500  mReport += "<tr><td colspan = 3>\n";
501  mReport += "<font color=red>Test image and result image for " + theTestName + " are mismatched</font><br>";
502  mReport += "</td></tr>";
503  mReport += myImagesString;
504  return false;
505  }
506 }
const QgsMapSettings & mapSettings()
bridge to QgsMapSettings
A rectangle specified with double values.
Definition: qgsrectangle.h:35
virtual void start() override
Start the rendering job and immediately return.
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
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:319
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.
virtual QImage renderedImage() override
Get a preview/resulting 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)
virtual void waitForFinished() override
Block until the job has finished.
double xMinimum() const
Get the x minimum value (left side of rectangle)
Definition: qgsrectangle.h:183