QGIS API Documentation  2.9.0-Master
 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  , 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 }