QGIS API Documentation  master-6227475
src/core/qgsrenderchecker.cpp
Go to the documentation of this file.
00001 /***************************************************************************
00002      qgsrenderchecker.cpp
00003      --------------------------------------
00004     Date                 : 18 Jan 2008
00005     Copyright            : (C) 2008 by Tim Sutton
00006     Email                : tim @ linfiniti.com
00007  ***************************************************************************
00008  *                                                                         *
00009  *   This program is free software; you can redistribute it and/or modify  *
00010  *   it under the terms of the GNU General Public License as published by  *
00011  *   the Free Software Foundation; either version 2 of the License, or     *
00012  *   (at your option) any later version.                                   *
00013  *                                                                         *
00014  ***************************************************************************/
00015 
00016 #include "qgsrenderchecker.h"
00017 
00018 #include <QColor>
00019 #include <QPainter>
00020 #include <QImage>
00021 #include <QTime>
00022 #include <QCryptographicHash>
00023 #include <QByteArray>
00024 #include <QDebug>
00025 #include <QBuffer>
00026 
00027 QgsRenderChecker::QgsRenderChecker( ) :
00028     mReport( "" ),
00029     mExpectedImageFile( "" ),
00030     mRenderedImageFile( "" ),
00031     mMismatchCount( 0 ),
00032     mMatchTarget( 0 ),
00033     mElapsedTime( 0 ),
00034     mElapsedTimeTarget( 0 ),
00035     mpMapRenderer( NULL ),
00036     mControlPathPrefix( "" )
00037 {
00038 
00039 }
00040 
00041 QString QgsRenderChecker::controlImagePath() const
00042 {
00043   QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
00044   QString myControlImageDir = myDataDir + QDir::separator() + "control_images" +
00045                               QDir::separator() + mControlPathPrefix;
00046   return myControlImageDir;
00047 }
00048 
00049 void QgsRenderChecker::setControlName( const QString theName )
00050 {
00051   mControlName = theName;
00052   mExpectedImageFile = controlImagePath() + theName + QDir::separator()
00053                        + theName + ".png";
00054 }
00055 
00056 QString QgsRenderChecker::imageToHash( QString theImageFile )
00057 {
00058   QImage myImage;
00059   myImage.load( theImageFile );
00060   QByteArray myByteArray;
00061   QBuffer myBuffer( &myByteArray );
00062   myImage.save( &myBuffer, "PNG" );
00063   QString myImageString = QString::fromUtf8( myByteArray.toBase64().data() );
00064   QCryptographicHash myHash( QCryptographicHash::Md5 );
00065   myHash.addData( myImageString.toUtf8() );
00066   return myHash.result().toHex().constData();
00067 }
00068 
00069 bool QgsRenderChecker::isKnownAnomaly( QString theDiffImageFile )
00070 {
00071   QString myControlImageDir = controlImagePath() + mControlName
00072                               + QDir::separator();
00073   QDir myDirectory = QDir( myControlImageDir );
00074   QStringList myList;
00075   QString myFilename = "*";
00076   myList = myDirectory.entryList( QStringList( myFilename ),
00077                                   QDir::Files | QDir::NoSymLinks );
00078   //remove the control file from teh list as the anomalies are
00079   //all files except the control file
00080   myList.removeAt( myList.indexOf( mExpectedImageFile ) );
00081 
00082   QString myImageHash = imageToHash( theDiffImageFile );
00083 
00084 
00085   for ( int i = 0; i < myList.size(); ++i )
00086   {
00087     QString myFile = myList.at( i );
00088     mReport += "<tr><td colspan=3>"
00089                "Checking if " + myFile + " is a known anomaly.";
00090     mReport += "</td></tr>";
00091     QString myAnomalyHash = imageToHash( controlImagePath() + mControlName
00092                                          + QDir::separator() + myFile );
00093     QString myHashMessage = QString(
00094                               "Checking if anomaly %1 (hash %2)" )
00095                             .arg( myFile )
00096                             .arg( myAnomalyHash );
00097     myHashMessage += QString( " matches %1 (hash %2)" )
00098                      .arg( theDiffImageFile )
00099                      .arg( myImageHash );
00100     //foo CDash
00101     QString myMeasureMessage = "<DartMeasurement name=\"Anomaly check"
00102                                "\" type=\"text/text\">" +  myHashMessage +
00103                                "</DartMeasurement>";
00104     qDebug() << myMeasureMessage;
00105     mReport += "<tr><td colspan=3>" + myHashMessage + "</td></tr>";
00106     if ( myImageHash == myAnomalyHash )
00107     {
00108       mReport += "<tr><td colspan=3>"
00109                  "Anomaly found! " + myFile;
00110       mReport += "</td></tr>";
00111       return true;
00112     }
00113   }
00114   mReport += "<tr><td colspan=3>"
00115              "No anomaly found! ";
00116   mReport += "</td></tr>";
00117   return false;
00118 }
00119 
00120 bool QgsRenderChecker::runTest( QString theTestName,
00121                                 unsigned int theMismatchCount )
00122 {
00123   if ( mExpectedImageFile.isEmpty() )
00124   {
00125     qDebug( "QgsRenderChecker::runTest failed - Expected Image File not set." );
00126     mReport = "<table>"
00127               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
00128               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
00129               "Image File not set.</td></tr></table>\n";
00130     return false;
00131   }
00132   //
00133   // Load the expected result pixmap
00134   //
00135   QImage myExpectedImage( mExpectedImageFile );
00136   mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
00137   //
00138   // Now render our layers onto a pixmap
00139   //
00140   QImage myImage( myExpectedImage.width(),
00141                   myExpectedImage.height(),
00142                   QImage::Format_RGB32 );
00143   myImage.setDotsPerMeterX( myExpectedImage.dotsPerMeterX() );
00144   myImage.setDotsPerMeterY( myExpectedImage.dotsPerMeterY() );
00145   myImage.fill( qRgb( 152, 219, 249 ) );
00146   QPainter myPainter( &myImage );
00147   myPainter.setRenderHint( QPainter::Antialiasing );
00148   mpMapRenderer->setOutputSize( QSize(
00149                                   myExpectedImage.width(),
00150                                   myExpectedImage.height() ),
00151                                 myExpectedImage.logicalDpiX() );
00152   QTime myTime;
00153   myTime.start();
00154   mpMapRenderer->render( &myPainter );
00155   mElapsedTime = myTime.elapsed();
00156   myPainter.end();
00157   //
00158   // Save the pixmap to disk so the user can make a
00159   // visual assessment if needed
00160   //
00161   mRenderedImageFile = QDir::tempPath() + QDir::separator() +
00162                        theTestName + "_result.png";
00163   myImage.save( mRenderedImageFile, "PNG", 100 );
00164   return compareImages( theTestName, theMismatchCount );
00165 
00166 }
00167 
00168 
00169 bool QgsRenderChecker::compareImages( QString theTestName,
00170                                       unsigned int theMismatchCount,
00171                                       QString theRenderedImageFile )
00172 {
00173   if ( mExpectedImageFile.isEmpty() )
00174   {
00175     qDebug( "QgsRenderChecker::runTest failed - Expected Image (control) File not set." );
00176     mReport = "<table>"
00177               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
00178               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
00179               "Image File not set.</td></tr></table>\n";
00180     return false;
00181   }
00182   if ( ! theRenderedImageFile.isEmpty() )
00183   {
00184     mRenderedImageFile = theRenderedImageFile;
00185   }
00186   if ( mRenderedImageFile.isEmpty() )
00187   {
00188     qDebug( "QgsRenderChecker::runTest failed - Rendered Image File not set." );
00189     mReport = "<table>"
00190               "<tr><td>Test Result:</td><td>Expected Result:</td></tr>\n"
00191               "<tr><td>Nothing rendered</td>\n<td>Failed because Expected "
00192               "Image File not set.</td></tr></table>\n";
00193     return false;
00194   }
00195   //
00196   // Load /create the images
00197   //
00198   QImage myExpectedImage( mExpectedImageFile );
00199   QImage myResultImage( mRenderedImageFile );
00200   QImage myDifferenceImage( myExpectedImage.width(),
00201                             myExpectedImage.height(),
00202                             QImage::Format_RGB32 );
00203   QString myDiffImageFile = QDir::tempPath() + QDir::separator() +
00204                             QDir::separator()  +
00205                             theTestName + "_result_diff.png";
00206   myDifferenceImage.fill( qRgb( 152, 219, 249 ) );
00207 
00208   //
00209   // Set pixel count score and target
00210   //
00211   mMatchTarget = myExpectedImage.width() * myExpectedImage.height();
00212   unsigned int myPixelCount = myResultImage.width() * myResultImage.height();
00213   //
00214   // Set the report with the result
00215   //
00216   mReport = "<table>";
00217   mReport += "<tr><td colspan=2>";
00218   mReport += "Test image and result image for " + theTestName + "<br>"
00219              "Expected size: " + QString::number( myExpectedImage.width() ).toLocal8Bit() + "w x " +
00220              QString::number( myExpectedImage.width() ).toLocal8Bit() + "h (" +
00221              QString::number( mMatchTarget ).toLocal8Bit() + " pixels)<br>"
00222              "Actual   size: " + QString::number( myResultImage.width() ).toLocal8Bit() + "w x " +
00223              QString::number( myResultImage.width() ).toLocal8Bit() + "h (" +
00224              QString::number( myPixelCount ).toLocal8Bit() + " pixels)";
00225   mReport += "</td></tr>";
00226   mReport += "<tr><td colspan = 2>\n";
00227   mReport += "Expected Duration : <= " + QString::number( mElapsedTimeTarget ) +
00228              "ms (0 indicates not specified)<br>";
00229   mReport += "Actual Duration :  " + QString::number( mElapsedTime ) + "ms<br>";
00230   QString myImagesString = "</td></tr>"
00231                            "<tr><td>Test Result:</td><td>Expected Result:</td><td>Difference (all blue is good, any red is bad)</td></tr>\n"
00232                            "<tr><td><img src=\"file://" +
00233                            mRenderedImageFile +
00234                            "\"></td>\n<td><img src=\"file://" +
00235                            mExpectedImageFile +
00236                            "\"></td><td><img src=\"file://" +
00237                            myDiffImageFile  +
00238                            "\"></td>\n</tr>\n</table>";
00239   //
00240   // To get the images into CDash
00241   //
00242   QString myDashMessage = "<DartMeasurementFile name=\"Rendered Image " + theTestName + "\""
00243                           " type=\"image/png\">" + mRenderedImageFile +
00244                           "</DartMeasurementFile>"
00245                           "<DartMeasurementFile name=\"Expected Image " + theTestName + "\" type=\"image/png\">" +
00246                           mExpectedImageFile + "</DartMeasurementFile>"
00247                           "<DartMeasurementFile name=\"Difference Image " + theTestName + "\" type=\"image/png\">" +
00248                           myDiffImageFile + "</DartMeasurementFile>";
00249   qDebug( ) << myDashMessage;
00250 
00251   //
00252   // Put the same info to debug too
00253   //
00254 
00255   qDebug( "Expected size: %dw x %dh", myExpectedImage.width(), myExpectedImage.height() );
00256   qDebug( "Actual   size: %dw x %dh", myResultImage.width(), myResultImage.height() );
00257 
00258   if ( mMatchTarget != myPixelCount )
00259   {
00260     qDebug( "Test image and result image for %s are different - FAILING!", theTestName.toLocal8Bit().constData() );
00261     mReport += "<tr><td colspan=3>";
00262     mReport += "<font color=red>Expected image and result image for " + theTestName + " are different dimensions - FAILING!</font>";
00263     mReport += "</td></tr>";
00264     mReport += myImagesString;
00265     return false;
00266   }
00267 
00268   //
00269   // Now iterate through them counting how many
00270   // dissimilar pixel values there are
00271   //
00272 
00273   mMismatchCount = 0;
00274   for ( int x = 0; x < myExpectedImage.width(); ++x )
00275   {
00276     for ( int y = 0; y < myExpectedImage.height(); ++y )
00277     {
00278       QRgb myExpectedPixel = myExpectedImage.pixel( x, y );
00279       QRgb myActualPixel = myResultImage.pixel( x, y );
00280       if ( myExpectedPixel != myActualPixel )
00281       {
00282         ++mMismatchCount;
00283         myDifferenceImage.setPixel( x, y, qRgb( 255, 0, 0 ) );
00284       }
00285     }
00286   }
00287   //
00288   //save the diff image to disk
00289   //
00290   myDifferenceImage.save( myDiffImageFile );
00291 
00292   //
00293   // Send match result to debug
00294   //
00295   qDebug( "%d/%d pixels mismatched", mMismatchCount, mMatchTarget );
00296 
00297   //
00298   // Send match result to report
00299   //
00300   mReport += "<tr><td colspan=3>" +
00301              QString::number( mMismatchCount ) + "/" +
00302              QString::number( mMatchTarget ) +
00303              " pixels mismatched (allowed threshold: " +
00304              QString::number( theMismatchCount ) + ")";
00305   mReport += "</td></tr>";
00306 
00307   //
00308   // And send it to CDash
00309   //
00310   myDashMessage = "<DartMeasurement name=\"Mismatch Count "
00311                   "\" type=\"numeric/integer\">" +
00312                   QString::number( mMismatchCount ) + "/" +
00313                   QString::number( mMatchTarget ) +
00314                   "</DartMeasurement>";
00315   qDebug( ) << myDashMessage;
00316 
00317   bool myAnomalyMatchFlag = isKnownAnomaly( myDiffImageFile );
00318 
00319   if ( myAnomalyMatchFlag )
00320   {
00321     mReport += "<tr><td colspan=3>"
00322                "Difference image matched a known anomaly - passing test! "
00323                "</td></tr>";
00324     return true;
00325   }
00326   else
00327   {
00328     QString myMessage = "Difference image did not match any known anomaly.";
00329     mReport += "<tr><td colspan=3>"
00330                "</td></tr>";
00331     QString myMeasureMessage = "<DartMeasurement name=\"No Anomalies Match"
00332                                "\" type=\"text/text\">" +  myMessage +
00333                                " If you feel the difference image should be considered an anomaly "
00334                                "you can do something like this\n"
00335                                "cp " + myDiffImageFile  + " ../tests/testdata/control_images/" + theTestName +
00336                                "/<imagename>.png"
00337                                "</DartMeasurement>";
00338     qDebug() << myMeasureMessage;
00339   }
00340 
00341   if ( mMismatchCount <= theMismatchCount )
00342   {
00343     mReport += "<tr><td colspan = 3>\n";
00344     mReport += "Test image and result image for " + theTestName + " are matched<br>";
00345     mReport += "</td></tr>";
00346     if ( mElapsedTimeTarget != 0 && mElapsedTimeTarget < mElapsedTime )
00347     {
00348       //test failed because it took too long...
00349       qDebug( "Test failed because render step took too long" );
00350       mReport += "<tr><td colspan = 3>\n";
00351       mReport += "<font color=red>Test failed because render step took too long</font>";
00352       mReport += "</td></tr>";
00353       mReport += myImagesString;
00354       return false;
00355     }
00356     else
00357     {
00358       mReport += myImagesString;
00359       return true;
00360     }
00361   }
00362   else
00363   {
00364     mReport += "<tr><td colspan = 3>\n";
00365     mReport += "<font color=red>Test image and result image for " + theTestName + " are mismatched</font><br>";
00366     mReport += "</td></tr>";
00367     mReport += myImagesString;
00368     return false;
00369   }
00370 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines