QGIS API Documentation  2.9.0-Master
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 
363  //
364  // Put the same info to debug too
365  //
366 
367  qDebug( "Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
368  qDebug( "Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
369 
370  if ( mMatchTarget != myPixelCount )
371  {
372  qDebug( "Test image and result image for %s are different - FAILING!", theTestName.toLocal8Bit().constData() );
373  mReport += "<tr><td colspan=3>";
374  mReport += "<font color=red>Expected image and result image for " + theTestName + " are different dimensions - FAILING!</font>";
375  mReport += "</td></tr>";
376  mReport += myImagesString;
377  delete maskImage;
378  return false;
379  }
380 
381  //
382  // Now iterate through them counting how many
383  // dissimilar pixel values there are
384  //
385 
386  mMismatchCount = 0;
387  int colorTolerance = ( int ) mColorTolerance;
388  for ( int y = 0; y < myExpectedImage.height(); ++y )
389  {
390  const QRgb* expectedScanline = ( const QRgb* )myExpectedImage.constScanLine( y );
391  const QRgb* resultScanline = ( const QRgb* )myResultImage.constScanLine( y );
392  const QRgb* maskScanline = hasMask ? ( const QRgb* )maskImage->constScanLine( y ) : 0;
393  QRgb* diffScanline = ( QRgb* )myDifferenceImage.scanLine( y );
394 
395  for ( int x = 0; x < myExpectedImage.width(); ++x )
396  {
397  int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
398  int pixelTolerance = qMax( colorTolerance, maskTolerance );
399  if ( pixelTolerance == 255 )
400  {
401  //skip pixel
402  continue;
403  }
404 
405  QRgb myExpectedPixel = expectedScanline[x];
406  QRgb myActualPixel = resultScanline[x];
407  if ( pixelTolerance == 0 )
408  {
409  if ( myExpectedPixel != myActualPixel )
410  {
411  ++mMismatchCount;
412  diffScanline[ x ] = qRgb( 255, 0, 0 );
413  }
414  }
415  else
416  {
417  if ( qAbs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
418  qAbs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
419  qAbs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
420  qAbs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
421  {
422  ++mMismatchCount;
423  diffScanline[ x ] = qRgb( 255, 0, 0 );
424  }
425  }
426  }
427  }
428  //
429  //save the diff image to disk
430  //
431  myDifferenceImage.save( myDiffImageFile );
432  emitDashMessage( "Difference Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, 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:192
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:339
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:187