2018-10-22 19:01:30 +02:00
//##########################################################################
//# #
//# CLOUDCOMPARE PLUGIN: q3DMASC #
//# #
//# This program is free software; you can redistribute it and/or modify #
//# it under the terms of the GNU General Public License as published by #
//# the Free Software Foundation; version 2 or later of the License. #
//# #
//# This program is distributed in the hope that it will be useful, #
//# but WITHOUT ANY WARRANTY; without even the implied warranty of #
//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
//# GNU General Public License for more details. #
//# #
//# COPYRIGHT: Dimitri Lague / CNRS / UEB #
//# #
//##########################################################################
# include "q3DMASC.h"
//local
# include "q3DMASCDisclaimerDialog.h"
2018-10-25 10:09:38 +02:00
# include "Features.h"
2018-10-22 19:01:30 +02:00
//qCC_db
# include <ccPointCloud.h>
2018-10-25 10:09:38 +02:00
//qCC_io
# include <LASFields.h>
2018-10-22 19:01:30 +02:00
//Qt
# include <QtGui>
# include <QtCore>
# include <QApplication>
# include <QMessageBox>
# include <QStringList>
2018-10-25 10:09:38 +02:00
//OpenCV
# include <opencv2/ml.hpp>
2018-10-22 19:01:30 +02:00
q3DMASCPlugin : : q3DMASCPlugin ( QObject * parent /*=0*/ )
: QObject ( parent )
, ccStdPluginInterface ( " :/CC/plugin/q3DMASCPlugin/info.json " )
, m_classifyAction ( 0 )
, m_trainAction ( 0 )
{
}
void q3DMASCPlugin : : onNewSelection ( const ccHObject : : Container & selectedEntities )
{
if ( m_classifyAction )
{
//classification: only one point cloud
m_classifyAction - > setEnabled ( selectedEntities . size ( ) = = 1 & & selectedEntities [ 0 ] - > isA ( CC_TYPES : : POINT_CLOUD ) ) ;
}
if ( m_trainAction )
{
m_trainAction - > setEnabled ( m_app & & m_app - > dbRootObject ( ) & & m_app - > dbRootObject ( ) - > getChildrenNumber ( ) ! = 0 ) ; //need some loaded entities to train the classifier!
}
m_selectedEntities = selectedEntities ;
}
QList < QAction * > q3DMASCPlugin : : getActions ( )
{
QList < QAction * > group ;
if ( ! m_trainAction )
{
m_trainAction = new QAction ( " Train classifier " , this ) ;
m_trainAction - > setToolTip ( " Train classifier " ) ;
m_trainAction - > setIcon ( QIcon ( QString : : fromUtf8 ( " :/CC/plugin/q3DMASCPlugin/iconCreate.png " ) ) ) ;
connect ( m_trainAction , SIGNAL ( triggered ( ) ) , this , SLOT ( doTrainAction ( ) ) ) ;
}
group . push_back ( m_trainAction ) ;
if ( ! m_classifyAction )
{
m_classifyAction = new QAction ( " Classify " , this ) ;
m_classifyAction - > setToolTip ( " Classify cloud " ) ;
m_classifyAction - > setIcon ( QIcon ( QString : : fromUtf8 ( " :/CC/plugin/q3DMASCPlugin/iconClassify.png " ) ) ) ;
connect ( m_classifyAction , SIGNAL ( triggered ( ) ) , this , SLOT ( doClassifyAction ( ) ) ) ;
}
group . push_back ( m_classifyAction ) ;
return group ;
}
void q3DMASCPlugin : : doClassifyAction ( )
{
if ( ! m_app )
{
assert ( false ) ;
return ;
}
//disclaimer accepted?
if ( ! ShowClassifyDisclaimer ( m_app ) )
{
return ;
}
if ( m_selectedEntities . empty ( ) | | ! m_selectedEntities . front ( ) - > isA ( CC_TYPES : : POINT_CLOUD ) )
{
m_app - > dispToConsole ( " Select one and only one point cloud! " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
ccPointCloud * cloud = static_cast < ccPointCloud * > ( m_selectedEntities . front ( ) ) ;
2018-10-24 11:30:39 +02:00
//look for the classification field
int classifSFIdx = cloud - > getScalarFieldIndexByName ( LAS_FIELD_NAMES [ LAS_CLASSIFICATION ] ) ; //LAS_FIELD_NAMES[LAS_CLASSIFICATION] = "Classification"
if ( ! classifSFIdx )
{
m_app - > dispToConsole ( " Missing 'Classification' field " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
CCLib : : ScalarField * classifSF = cloud - > getScalarField ( classifSFIdx ) ;
if ( ! classifSF | | classifSF - > size ( ) < cloud - > size ( ) )
{
assert ( false ) ;
return ;
}
2018-10-22 19:01:30 +02:00
RTParams params ;
2018-10-24 23:14:40 +02:00
if ( params . testDataRatio < 0 | | params . testDataRatio > 0.99f )
{
m_app - > dispToConsole ( " Invalid test data ratio " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
2018-10-25 13:06:02 +02:00
std : : vector < Feature : : Shared > features ;
features . push_back ( Feature : : Shared ( new PointFeature ( cloud , PointFeature : : Z , Feature : : DimZ , " Z " ) ) ) ;
features . push_back ( Feature : : Shared ( new PointFeature ( cloud , PointFeature : : Intensity , Feature : : ScalarField , " Intensity " ) ) ) ;
2018-10-24 11:30:39 +02:00
2018-10-24 23:14:40 +02:00
int totalSampleCount = static_cast < int > ( cloud - > size ( ) ) ;
int testSampleCount = static_cast < int > ( floor ( totalSampleCount * params . testDataRatio ) ) ;
2018-10-25 10:09:38 +02:00
int sampleCount = totalSampleCount - testSampleCount ;
2018-10-24 11:30:39 +02:00
int attributesPerSample = static_cast < int > ( features . size ( ) ) ;
2018-10-22 19:01:30 +02:00
2018-10-24 23:14:40 +02:00
m_app - > dispToConsole ( QString ( " [3DMASC] Training data: %1 samples with %2 feature(s) / %3 test samples " ) . arg ( sampleCount ) . arg ( attributesPerSample ) . arg ( testSampleCount ) , ccMainAppInterface : : STD_CONSOLE_MESSAGE ) ;
//choose the sample indexes
std : : vector < bool > isSample ;
{
try
{
isSample . resize ( totalSampleCount , true ) ;
}
catch ( const std : : bad_alloc & )
{
m_app - > dispToConsole ( " Not enough memory " , ccMainAppInterface : : STD_CONSOLE_MESSAGE ) ;
return ;
}
2018-10-25 10:09:38 +02:00
int randomCount = 0 ;
2018-10-25 21:59:00 +02:00
int randIndex = 0 ;
2018-10-24 23:14:40 +02:00
while ( randomCount < testSampleCount )
{
2018-10-25 21:59:00 +02:00
randIndex = ( ( randIndex + std : : rand ( ) ) % totalSampleCount ) ;
2018-10-24 23:14:40 +02:00
if ( isSample [ randIndex ] )
{
isSample [ randIndex ] = false ;
+ + randomCount ;
}
}
}
2018-10-22 19:01:30 +02:00
//NUMBER_OF_TRAINING_SAMPLES = number of points
//ATTRIBUTES_PER_SAMPLE = number of scalar fields
2018-10-24 11:30:39 +02:00
cv : : Mat training_data , train_labels ;
2018-10-24 23:14:40 +02:00
cv : : Mat test_data , test_labels ;
2018-10-24 11:30:39 +02:00
try
{
training_data . create ( sampleCount , attributesPerSample , CV_32FC1 ) ;
2018-10-25 21:59:00 +02:00
train_labels . create ( sampleCount , 1 , CV_32FC1 ) ;
2018-10-24 23:14:40 +02:00
test_data . create ( testSampleCount , attributesPerSample , CV_32FC1 ) ;
2018-10-25 21:59:00 +02:00
test_labels . create ( testSampleCount , 1 , CV_32FC1 ) ;
2018-10-24 11:30:39 +02:00
}
catch ( const cv : : Exception & cvex )
{
ccLog : : Error ( cvex . msg . c_str ( ) ) ;
return ;
}
//fill the classification labels vector
{
2018-10-24 23:14:40 +02:00
unsigned sampleIndex = 0 ;
unsigned testSampleIndex = 0 ;
2018-10-24 11:30:39 +02:00
for ( unsigned i = 0 ; i < cloud - > size ( ) ; + + i )
{
ScalarType pointClass = classifSF - > getValue ( i ) ;
int iClass = static_cast < int > ( pointClass ) ;
if ( iClass < 0 | | iClass > 255 )
{
m_app - > dispToConsole ( " Classification values out of range (0-255) " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
2018-10-24 23:14:40 +02:00
if ( isSample [ i ] )
{
2018-10-25 21:59:00 +02:00
train_labels . at < float > ( sampleIndex + + ) = static_cast < unsigned char > ( iClass ) ;
2018-10-24 23:14:40 +02:00
}
else
{
2018-10-25 21:59:00 +02:00
test_labels . at < float > ( testSampleIndex + + ) = static_cast < unsigned char > ( iClass ) ;
2018-10-24 23:14:40 +02:00
}
2018-10-24 11:30:39 +02:00
}
2018-10-25 21:59:00 +02:00
assert ( sampleIndex + testSampleIndex = = totalSampleCount ) ;
2018-10-24 11:30:39 +02:00
}
//fill the training data matrix
for ( int fIndex = 0 ; fIndex < attributesPerSample ; + + fIndex )
{
QScopedPointer < IScalarFieldWrapper > source ( nullptr ) ;
2018-10-25 16:42:02 +02:00
const Feature : : Shared & f = features [ fIndex ] ;
switch ( f - > source )
2018-10-24 11:30:39 +02:00
{
case Feature : : ScalarField :
{
2018-10-25 16:42:02 +02:00
int sfIdx = cloud - > getScalarFieldIndexByName ( qPrintable ( f - > sourceName ) ) ;
2018-10-24 11:30:39 +02:00
if ( sfIdx > = 0 )
{
source . reset ( new ScalarFieldWrapper ( cloud - > getScalarField ( sfIdx ) ) ) ;
}
else
{
2018-10-25 16:42:02 +02:00
ccLog : : Error ( QString ( " Internal error: unknwon scalar field '%1' " ) . arg ( f - > sourceName ) ) ;
2018-10-24 11:30:39 +02:00
return ;
}
}
break ;
case Feature : : DimX :
source . reset ( new DimScalarFieldWrapper ( cloud , DimScalarFieldWrapper : : DimX ) ) ;
break ;
case Feature : : DimY :
source . reset ( new DimScalarFieldWrapper ( cloud , DimScalarFieldWrapper : : DimY ) ) ;
break ;
case Feature : : DimZ :
source . reset ( new DimScalarFieldWrapper ( cloud , DimScalarFieldWrapper : : DimZ ) ) ;
break ;
case Feature : : Red :
source . reset ( new ColorScalarFieldWrapper ( cloud , ColorScalarFieldWrapper : : Red ) ) ;
break ;
case Feature : : Green :
source . reset ( new ColorScalarFieldWrapper ( cloud , ColorScalarFieldWrapper : : Green ) ) ;
break ;
case Feature : : Blue :
source . reset ( new ColorScalarFieldWrapper ( cloud , ColorScalarFieldWrapper : : Blue ) ) ;
break ;
}
if ( ! source | | ! source - > isValid ( ) )
{
assert ( false ) ;
2018-10-25 16:42:02 +02:00
ccLog : : Error ( QString ( " Internal error: invalid source '%1' " ) . arg ( f - > sourceName ) ) ;
2018-10-24 11:30:39 +02:00
}
2018-10-24 23:14:40 +02:00
unsigned sampleIndex = 0 ;
unsigned testSampleIndex = 0 ;
2018-10-24 11:30:39 +02:00
for ( unsigned i = 0 ; i < cloud - > size ( ) ; + + i )
{
double value = source - > pointValue ( i ) ;
2018-10-24 23:14:40 +02:00
if ( isSample [ i ] )
{
2018-10-25 21:59:00 +02:00
assert ( sampleIndex < sampleCount ) ;
2018-10-24 23:14:40 +02:00
training_data . at < float > ( sampleIndex + + , fIndex ) = static_cast < float > ( value ) ;
}
else
{
2018-10-25 21:59:00 +02:00
assert ( testSampleIndex < testSampleCount ) ;
2018-10-24 23:14:40 +02:00
test_data . at < float > ( testSampleIndex + + , fIndex ) = static_cast < float > ( value ) ;
}
2018-10-24 11:30:39 +02:00
}
2018-10-25 21:59:00 +02:00
assert ( sampleIndex + testSampleIndex = = totalSampleCount ) ;
2018-10-24 11:30:39 +02:00
}
2018-10-22 19:01:30 +02:00
cv : : Ptr < cv : : ml : : RTrees > rtrees ;
rtrees = cv : : ml : : RTrees : : create ( ) ;
rtrees - > setMaxDepth ( params . maxDepth ) ;
rtrees - > setMinSampleCount ( params . minSampleCount ) ;
rtrees - > setCalculateVarImportance ( params . calcVarImportance ) ;
rtrees - > setActiveVarCount ( params . activeVarCount ) ;
cv : : TermCriteria terminationCriteria ( cv : : TermCriteria : : MAX_ITER , params . maxTreeCount , std : : numeric_limits < double > : : epsilon ( ) ) ;
rtrees - > setTermCriteria ( terminationCriteria ) ;
//rtrees->setRegressionAccuracy(0);
//rtrees->setUseSurrogates(false);
//rtrees->setMaxCategories(params.maxCategories); //not important?
//rtrees->setPriors(cv::Mat());
2018-10-25 21:59:00 +02:00
try
{
rtrees - > train ( training_data , cv : : ml : : ROW_SAMPLE , train_labels ) ;
}
catch ( const cv : : Exception & cvex )
{
ccLog : : Error ( cvex . msg . c_str ( ) ) ;
return ;
}
catch ( const std : : exception & stdex )
{
ccLog : : Error ( stdex . what ( ) ) ;
return ;
}
catch ( . . . )
{
ccLog : : Error ( " Unknown error " ) ;
return ;
}
2018-10-22 19:01:30 +02:00
2018-10-24 23:14:40 +02:00
if ( ! rtrees - > isTrained ( ) )
{
//an error occurred?
return ;
}
//estimate the efficiency of the classiier
{
int goodGuessCount = 0 ;
for ( int j = 0 ; j < testSampleCount ; + + j )
{
2018-10-25 21:59:00 +02:00
if ( rtrees - > predict ( test_data . row ( j ) ) = = test_labels . at < float > ( j ) )
2018-10-24 23:14:40 +02:00
{
+ + goodGuessCount ;
}
}
float acc = static_cast < float > ( goodGuessCount ) / testSampleCount ;
m_app - > dispToConsole ( QString ( " Correct = %1 / %2 --> Accuracy = %3 " ) . arg ( goodGuessCount ) . arg ( testSampleCount ) . arg ( acc ) , ccMainAppInterface : : STD_CONSOLE_MESSAGE ) ;
}
2018-10-25 21:59:00 +02:00
//save the classifier
QString outputFilename = QCoreApplication : : applicationDirPath ( ) + " /classifier.yaml " ;
ccLog : : Print ( " Classifier file saved to: " + outputFilename ) ;
rtrees - > save ( outputFilename . toStdString ( ) ) ;
2018-10-22 19:01:30 +02:00
}
//OpenCV
void q3DMASCPlugin : : doTrainAction ( )
{
//disclaimer accepted?
if ( ! ShowTrainDisclaimer ( m_app ) )
return ;
//if (m_selectedEntities.size() != 2
// || !m_selectedEntities[0]->isA(CC_TYPES::POINT_CLOUD)
// || !m_selectedEntities[1]->isA(CC_TYPES::POINT_CLOUD))
//{
// m_app->dispToConsole("Select two point clouds!",ccMainAppInterface::ERR_CONSOLE_MESSAGE);
// return;
//}
//
//ccPointCloud* cloud1 = static_cast<ccPointCloud*>(m_selectedEntities[0]);
//ccPointCloud* cloud2 = static_cast<ccPointCloud*>(m_selectedEntities[1]);
}
void q3DMASCPlugin : : registerCommands ( ccCommandLineInterface * cmd )
{
if ( ! cmd )
{
assert ( false ) ;
return ;
}
//cmd->registerCommand(ccCommandLineInterface::Command::Shared(new CommandCanupoClassif));
}