QGIS API Documentation  2.11.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 
30 static int renderCounter = 0;
31 
33  : mReport( "" )
34  , mMatchTarget( 0 )
35  , mElapsedTime( 0 )
36  , mRenderedImageFile( "" )
37  , mExpectedImageFile( "" )
38  , mMismatchCount( 0 )
39  , mColorTolerance( 0 )
40  , mMaxSizeDifferenceX( 0 )
41  , mMaxSizeDifferenceY( 0 )
42  , mElapsedTimeTarget( 0 )
43  , mBufferDashMessages( false )
44 {
45 }
46 
48 {
49  QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
50  QString myControlImageDir = myDataDir + "/control_images/" + mControlPathPrefix;
51  return myControlImageDir;
52 }
53 
55 {
56  mControlName = theName;
57  mExpectedImageFile = controlImagePath() + theName + "/" + mControlPathSuffix + theName + ".png";
58 }
59 
61 {
62  QImage myImage;
63  myImage.load( theImageFile );
64  QByteArray myByteArray;
65  QBuffer myBuffer( &myByteArray );
66  myImage.save( &myBuffer, "PNG" );
67  QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
68  QCryptographicHash myHash( QCryptographicHash::Md5 );
69  myHash.addData( myImageString.toUtf8() );
70  return myHash.result().toHex().constData();
71 }
72 
74 {
75  mMapSettings = thepMapRenderer->mapSettings();
76 }
77 
79 {
80  mMapSettings = mapSettings;
81 }
82 
84 {
85  // create a 2x2 checker-board image
86  uchar pixDataRGB[] = { 255, 255, 255, 255,
87  127, 127, 127, 255,
88  127, 127, 127, 255,
89  255, 255, 255, 255
90  };
91 
92  QImage img( pixDataRGB, 2, 2, 8, QImage::Format_ARGB32 );
93  QPixmap pix = QPixmap::fromImage( img.scaled( 20, 20 ) );
94 
95  // fill image with texture
96  QBrush brush;
97  brush.setTexture( pix );
98  QPainter p( image );
99  p.setRenderHint( QPainter::Antialiasing, false );
100  p.fillRect( QRect( 0, 0, image->width(), image->height() ), brush );
101  p.end();
102 }
103 
105 {
106  QString myControlImageDir = controlImagePath() + mControlName + "/";
107  QDir myDirectory = QDir( myControlImageDir );
108  QStringList myList;
109  QString myFilename = "*";
110  myList = myDirectory.entryList( QStringList( myFilename ),
111  QDir::Files | QDir::NoSymLinks );
112  //remove the control file from the list as the anomalies are
113  //all files except the control file
114  myList.removeAt( myList.indexOf( QFileInfo( mExpectedImageFile ).fileName() ) );
115 
116  QString myImageHash = imageToHash( theDiffImageFile );
117 
118 
119  for ( int i = 0; i < myList.size(); ++i )
120  {
121  QString myFile = myList.at( i );
122  mReport += "<tr><td colspan=3>"
123  "Checking if " + myFile + " is a known anomaly.";
124  mReport += "</td></tr>";
125  QString myAnomalyHash = imageToHash( controlImagePath() + mControlName + "/" + 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 
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() + "/" + theTestName + "_result.png";
213 
214  myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
215  myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
216  if ( ! myImage.save( mRenderedImageFile, "PNG", 100 ) )
217  {
218  qDebug() << "QgsRenderChecker::runTest failed - Could not save rendered image to " << mRenderedImageFile;
219  mReport = "<table>"
220  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
221  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
222  "Image File could not be saved.</td></tr></table>\n";
223  return false;
224  }
225 
226  //create a world file to go with the image...
227 
228  QFile wldFile( QDir::tempPath() + "/" + theTestName + "_result.wld" );
229  if ( wldFile.open( QIODevice::WriteOnly ) )
230  {
231  QgsRectangle r = mMapSettings.extent();
232 
233  QTextStream stream( &wldFile );
234  stream << QString( "%1\r\n0 \r\n0 \r\n%2\r\n%3\r\n%4\r\n" )
235  .arg( qgsDoubleToString( mMapSettings.mapUnitsPerPixel() ) )
236  .arg( qgsDoubleToString( -mMapSettings.mapUnitsPerPixel() ) )
237  .arg( qgsDoubleToString( r.xMinimum() + mMapSettings.mapUnitsPerPixel() / 2.0 ) )
238  .arg( qgsDoubleToString( r.yMaximum() - mMapSettings.mapUnitsPerPixel() / 2.0 ) );
239  }
240 
241  return compareImages( theTestName, theMismatchCount );
242 }
243 
244 
246  unsigned int theMismatchCount,
247  QString theRenderedImageFile )
248 {
249  if ( mExpectedImageFile.isEmpty() )
250  {
251  qDebug( "QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
252  mReport = "<table>"
253  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
254  "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
255  "Image File not set.</td></tr></table>\n";
256  return false;
257  }
258  if ( ! theRenderedImageFile.isEmpty() )
259  {
260 #ifndef Q_OS_WIN
261  mRenderedImageFile = theRenderedImageFile;
262 #else
263  mRenderedImageFile = theRenderedImageFile.replace( "\\", "/" );
264 #endif
265  }
266 
267  if ( mRenderedImageFile.isEmpty() )
268  {
269  qDebug( "QgsRenderChecker::runTest failed - Rendered Image File not set." );
270  mReport = "<table>"
271  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
272  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
273  "Image File not set.</td></tr></table>\n";
274  return false;
275  }
276 
277  //
278  // Load /create the images
279  //
280  QImage myExpectedImage( mExpectedImageFile );
281  QImage myResultImage( mRenderedImageFile );
282  if ( myResultImage.isNull() )
283  {
284  qDebug() << "QgsRenderChecker::runTest failed - Could not load rendered image from " << mRenderedImageFile;
285  mReport = "<table>"
286  "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
287  "<tr><td>Nothing rendered</td>\n<td>Failed because Rendered "
288  "Image File could not be loaded.</td></tr></table>\n";
289  return false;
290  }
291  QImage myDifferenceImage( myExpectedImage.width(),
292  myExpectedImage.height(),
293  QImage::Format_RGB32 );
294  QString myDiffImageFile = QDir::tempPath() + "/" + theTestName + "_result_diff.png";
295  myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
296 
297  //check for mask
298  QString maskImagePath = mExpectedImageFile;
299  maskImagePath.chop( 4 ); //remove .png extension
300  maskImagePath += "_mask.png";
301  QImage* maskImage = new QImage( maskImagePath );
302  bool hasMask = !maskImage->isNull();
303  if ( hasMask )
304  {
305  qDebug( "QgsRenderChecker using mask image" );
306  }
307 
308  //
309  // Set pixel count score and target
310  //
311  mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
312  unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
313  //
314  // Set the report with the result
315  //
316  mReport = QString( "<script src=\"file://%1/../renderchecker.js\"></script>\n" ).arg( TEST_DATA_DIR );
317  mReport += "<table>";
318  mReport += "<tr><td colspan=2>";
319  mReport += QString( "<tr><td colspan=2>"
320  "Test image and result image for %1<br>"
321  "Expected size: %2 w x %3 h (%4 pixels)<br>"
322  "Actual size: %5 w x %6 h (%7 pixels)"
323  "</td></tr>" )
324  .arg( theTestName )
325  .arg( myExpectedImage.width() ).arg( myExpectedImage.height() ).arg( mMatchTarget )
326  .arg( myResultImage.width() ).arg( myResultImage.height() ).arg( myPixelCount );
327  mReport += QString( "<tr><td colspan=2>\n"
328  "Expected Duration : <= %1 (0 indicates not specified)<br>"
329  "Actual Duration : %2 ms<br></td></tr>" )
330  .arg( mElapsedTimeTarget )
331  .arg( mElapsedTime );
332 
333  // limit image size in page to something reasonable
334  int imgWidth = 420;
335  int imgHeight = 280;
336  if ( ! myExpectedImage.isNull() )
337  {
338  imgWidth = qMin( myExpectedImage.width(), imgWidth );
339  imgHeight = myExpectedImage.height() * imgWidth / myExpectedImage.width();
340  }
341 
342  QString myImagesString = QString(
343  "<tr>"
344  "<td colspan=2>Compare actual and expected result</td>"
345  "<td>Difference (all blue is good, any red is bad)</td>"
346  "</tr>\n<tr>"
347  "<td colspan=2 id=\"td-%1-%7\"></td>\n"
348  "<td align=center><img width=%5 height=%6 src=\"file://%2\"></td>\n"
349  "</tr>"
350  "</table>\n"
351  "<script>\naddComparison(\"td-%1-%7\",\"file://%3\",\"file://%4\",%5,%6);\n</script>\n" )
352  .arg( theTestName )
353  .arg( myDiffImageFile )
356  .arg( imgWidth ).arg( imgHeight )
357  .arg( renderCounter++ );
358 
359  QString prefix;
360  if ( !mControlPathPrefix.isNull() )
361  {
362  prefix = QString( " (prefix %1)" ).arg( mControlPathPrefix );
363  }
364  //
365  // To get the images into CDash
366  //
367  emitDashMessage( "Rendered Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, mRenderedImageFile );
368  emitDashMessage( "Expected Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, mExpectedImageFile );
369 
370  //
371  // Put the same info to debug too
372  //
373 
374  qDebug( "Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
375  qDebug( "Actual size: %dw x %dh", myResultImage.width(), myResultImage.height() );
376 
377  if ( mMatchTarget != myPixelCount )
378  {
379  qDebug( "Test image and result image for %s are different dimensions", theTestName.toLocal8Bit().constData() );
380 
381  if ( qAbs( myExpectedImage.width() - myResultImage.width() ) > mMaxSizeDifferenceX ||
382  qAbs( myExpectedImage.height() - myResultImage.height() ) > mMaxSizeDifferenceY )
383  {
384  mReport += "<tr><td colspan=3>";
385  mReport += "<font color=red>Expected image and result image for " + theTestName + " are different dimensions - FAILING!</font>";
386  mReport += "</td></tr>";
387  mReport += myImagesString;
388  delete maskImage;
389  return false;
390  }
391  else
392  {
393  mReport += "<tr><td colspan=3>";
394  mReport += "Expected image and result image for " + theTestName + " are different dimensions, but within tolerance";
395  mReport += "</td></tr>";
396  }
397  }
398 
399  //
400  // Now iterate through them counting how many
401  // dissimilar pixel values there are
402  //
403 
404  int maxHeight = qMin( myExpectedImage.height(), myResultImage.height() );
405  int maxWidth = qMin( myExpectedImage.width(), myResultImage.width() );
406 
407  mMismatchCount = 0;
408  int colorTolerance = ( int ) mColorTolerance;
409  for ( int y = 0; y < maxHeight; ++y )
410  {
411  const QRgb* expectedScanline = ( const QRgb* )myExpectedImage.constScanLine( y );
412  const QRgb* resultScanline = ( const QRgb* )myResultImage.constScanLine( y );
413  const QRgb* maskScanline = hasMask ? ( const QRgb* )maskImage->constScanLine( y ) : 0;
414  QRgb* diffScanline = ( QRgb* )myDifferenceImage.scanLine( y );
415 
416  for ( int x = 0; x < maxWidth; ++x )
417  {
418  int maskTolerance = hasMask ? qRed( maskScanline[ x ] ) : 0;
419  int pixelTolerance = qMax( colorTolerance, maskTolerance );
420  if ( pixelTolerance == 255 )
421  {
422  //skip pixel
423  continue;
424  }
425 
426  QRgb myExpectedPixel = expectedScanline[x];
427  QRgb myActualPixel = resultScanline[x];
428  if ( pixelTolerance == 0 )
429  {
430  if ( myExpectedPixel != myActualPixel )
431  {
432  ++mMismatchCount;
433  diffScanline[ x ] = qRgb( 255, 0, 0 );
434  }
435  }
436  else
437  {
438  if ( qAbs( qRed( myExpectedPixel ) - qRed( myActualPixel ) ) > pixelTolerance ||
439  qAbs( qGreen( myExpectedPixel ) - qGreen( myActualPixel ) ) > pixelTolerance ||
440  qAbs( qBlue( myExpectedPixel ) - qBlue( myActualPixel ) ) > pixelTolerance ||
441  qAbs( qAlpha( myExpectedPixel ) - qAlpha( myActualPixel ) ) > pixelTolerance )
442  {
443  ++mMismatchCount;
444  diffScanline[ x ] = qRgb( 255, 0, 0 );
445  }
446  }
447  }
448  }
449  //
450  //save the diff image to disk
451  //
452  myDifferenceImage.save( myDiffImageFile );
453  emitDashMessage( "Difference Image " + theTestName + prefix, QgsDartMeasurement::ImagePng, myDiffImageFile );
454  delete maskImage;
455 
456  //
457  // Send match result to debug
458  //
459  qDebug( "%d/%d pixels mismatched (%d allowed)", mMismatchCount, mMatchTarget, theMismatchCount );
460 
461  //
462  // Send match result to report
463  //
464  mReport += QString( "<tr><td colspan=3>%1/%2 pixels mismatched (allowed threshold: %3, allowed color component tolerance: %4)</td></tr>" )
465  .arg( mMismatchCount ).arg( mMatchTarget ).arg( theMismatchCount ).arg( mColorTolerance );
466 
467  //
468  // And send it to CDash
469  //
470  emitDashMessage( "Mismatch Count", QgsDartMeasurement::Integer, QString( "%1/%2" ).arg( mMismatchCount ).arg( mMatchTarget ) );
471 
472  if ( mMismatchCount <= theMismatchCount )
473  {
474  mReport += "<tr><td colspan = 3>\n";
475  mReport += "Test image and result image for " + theTestName + " are matched<br>";
476  mReport += "</td></tr>";
477  if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < mElapsedTime )
478  {
479  //test failed because it took too long...
480  qDebug( "Test failed because render step took too long" );
481  mReport += "<tr><td colspan = 3>\n";
482  mReport += "<font color=red>Test failed because render step took too long</font>";
483  mReport += "</td></tr>";
484  mReport += myImagesString;
485  return false;
486  }
487  else
488  {
489  mReport += myImagesString;
490  return true;
491  }
492  }
493 
494  bool myAnomalyMatchFlag = isKnownAnomaly( myDiffImageFile );
495  if ( myAnomalyMatchFlag )
496  {
497  mReport += "<tr><td colspan=3>"
498  "Difference image matched a known anomaly - passing test! "
499  "</td></tr>";
500  return true;
501  }
502 
503  mReport += "<tr><td colspan=3></td></tr>";
504  emitDashMessage( "Image mismatch", QgsDartMeasurement::Text, "Difference image did not match any known anomaly or mask."
505  " If you feel the difference image should be considered an anomaly "
506  "you can do something like this\n"
507  "cp '" + myDiffImageFile + "' " + controlImagePath() + mControlName +
508  "/\nIf it should be included in the mask run\n"
509  "scripts/generate_test_mask_image.py '" + mExpectedImageFile + "' '" + mRenderedImageFile + "'\n" );
510 
511  mReport += "<tr><td colspan = 3>\n";
512  mReport += "<font color=red>Test image and result image for " + theTestName + " are mismatched</font><br>";
513  mReport += "</td></tr>";
514  mReport += myImagesString;
515  return false;
516 }
const QgsMapSettings & mapSettings()
bridge to QgsMapSettings
A rectangle specified with double values.
Definition: qgsrectangle.h:35
void setDotsPerMeterX(int x)
void setDotsPerMeterY(int y)
bool load(QIODevice *device, const char *format)
virtual void start() override
Start the rendering job and immediately return.
bool end()
QString & fill(QChar ch, int size)
void fillRect(const QRectF &rectangle, const QBrush &brush)
const uchar * constScanLine(int i) const
void setRenderHint(RenderHint hint, bool on)
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
bool save(const QString &fileName, const char *format, int quality) const
const T & at(int i) const
void removeAt(int i)
QPixmap fromImage(const QImage &image, QFlags< Qt::ImageConversionFlag > flags)
int dotsPerMeterX() const
int dotsPerMeterY() const
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.
bool isNull() const
A non GUI class for rendering a map layer set onto a QPainter.
void chop(int n)
void setMapSettings(const QgsMapSettings &mapSettings)
int size() const
bool isNull() const
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.
int elapsed() const
QString fromUtf8(const char *str, int size)
QString tempPath()
QString controlImagePath() const
int width() const
QString imageToHash(QString theImageFile)
Get an md5 hash that uniquely identifies an image.
bool isEmpty() const
Enable anti-aliasin for map rendering.
const char * constData() const
double mapUnitsPerPixel() const
Return the distance in geographical coordinates that equals to one pixel in the map.
unsigned int mMatchTarget
virtual bool open(QFlags< QIODevice::OpenModeFlag > mode)
static int renderCounter
QByteArray toLocal8Bit() const
QString qgsDoubleToString(const double &a, const int &precision=17)
Definition: qgis.h:339
void setTexture(const QPixmap &pixmap)
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.
QString & replace(int position, int n, QChar after)
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.
QStringList entryList(QFlags< QDir::Filter > filters, QFlags< QDir::SortFlag > sort) const
QgsRectangle extent() const
Return geographical coordinates of the rectangle that should be rendered.
char * data()
void start()
int height() const
int indexOf(const QRegExp &rx, int from) const
bool compareImages(QString theTestName, unsigned int theMismatchCount=0, QString theRenderedImageFile="")
Test using two arbitary images (map renderer will not be used)
QByteArray toBase64() const
virtual void waitForFinished() override
Block until the job has finished.
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
double xMinimum() const
Get the x minimum value (left side of rectangle)
Definition: qgsrectangle.h:187
QImage scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const
QByteArray toUtf8() const