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