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-26 10:41:22 +02:00
# include "q3DMASCClassifier.h"
2018-10-26 12:02:23 +02:00
# include "q3DMASCTools.h"
2018-11-04 23:31:48 +01:00
# include "qClassify3DMASCDialog.h"
2019-01-19 21:40:38 +01:00
# include "qTrain3DMASCDialog.h"
2019-01-19 00:49:29 +01:00
# include "q3DMASCCommands.h"
2018-10-22 19:01:30 +02:00
//qCC_db
# include <ccPointCloud.h>
2018-11-04 00:06:47 +01:00
# include <ccProgressDialog.h>
2018-10-22 19:01:30 +02:00
//Qt
# include <QtGui>
# include <QtCore>
# include <QApplication>
2018-11-04 00:06:47 +01:00
# include <QFileDialog>
2019-01-21 00:10:47 +01:00
# include <QMessageBox>
2018-10-25 10:09:38 +02:00
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
2018-11-04 23:31:48 +01:00
//m_classifyAction->setEnabled(selectedEntities.size() == 1 && selectedEntities[0]->isA(CC_TYPES::POINT_CLOUD));
m_classifyAction - > setEnabled ( m_app - > dbRootObject ( ) - > getChildrenNumber ( ) ! = 0 ) ;
2018-10-22 19:01:30 +02:00
}
if ( m_trainAction )
{
2018-11-04 00:06:47 +01:00
//m_trainAction->setEnabled(m_app && m_app->dbRootObject() && m_app->dbRootObject()->getChildrenNumber() != 0); //need some loaded entities to train the classifier!
m_trainAction - > setEnabled ( true ) ;
2018-10-22 19:01:30 +02:00
}
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 ;
}
2018-11-04 23:31:48 +01:00
QString inputFilename ;
{
QSettings settings ;
settings . beginGroup ( " 3DMASC " ) ;
QString inputPath = settings . value ( " FilePath " , QCoreApplication : : applicationDirPath ( ) ) . toString ( ) ;
inputFilename = QFileDialog : : getOpenFileName ( m_app - > getMainWindow ( ) , " Load 3DMASC classifier file " , inputPath , " *.txt " ) ;
if ( inputFilename . isNull ( ) )
{
//process cancelled by the user
return ;
}
settings . setValue ( " FilePath " , QFileInfo ( inputFilename ) . absolutePath ( ) ) ;
settings . endGroup ( ) ;
}
2018-10-26 10:41:22 +02:00
2019-03-26 14:28:27 +01:00
QList < QString > cloudLabels ;
2019-03-25 18:45:28 +01:00
QString corePointsLabel ;
bool filenamesSpecified = false ;
if ( ! masc : : Tools : : LoadClassifierCloudLabels ( inputFilename , cloudLabels , corePointsLabel , filenamesSpecified ) )
2018-11-04 23:31:48 +01:00
{
m_app - > dispToConsole ( " Failed to read classifier file (see Console) " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2018-10-26 10:41:22 +02:00
return ;
2018-11-04 23:31:48 +01:00
}
if ( cloudLabels . empty ( ) )
{
m_app - > dispToConsole ( " Invalid classifier file (no cloud label defined) " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
2019-03-29 13:40:57 +01:00
else if ( cloudLabels . size ( ) > 4 + ( cloudLabels . contains ( " TEST " ) ? 1 : 0 ) )
2018-11-04 23:31:48 +01:00
{
2019-03-29 13:40:57 +01:00
m_app - > dispToConsole ( " This classifier uses more than 4 clouds (the GUI version cannot handle it) " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2018-11-04 23:31:48 +01:00
return ;
}
2018-10-26 10:41:22 +02:00
2018-11-04 23:31:48 +01:00
//now show a dialog where the user will be able to set the cloud roles
Classify3DMASCDialog classifDlg ( m_app ) ;
2019-03-25 18:45:28 +01:00
classifDlg . setCloudRoles ( cloudLabels , corePointsLabel ) ;
2018-11-05 00:11:27 +01:00
classifDlg . classifierFileLineEdit - > setText ( inputFilename ) ;
2019-01-19 00:49:29 +01:00
static bool s_keepAttributes = false ;
classifDlg . keepAttributesCheckBox - > setChecked ( s_keepAttributes ) ;
2019-03-27 15:03:58 +01:00
classifDlg . testCloudComboBox - > hide ( ) ;
classifDlg . testLabel - > hide ( ) ;
2018-11-04 23:31:48 +01:00
if ( ! classifDlg . exec ( ) )
2018-11-04 00:06:47 +01:00
{
2018-11-04 23:31:48 +01:00
//process cancelled by the user
2018-11-04 00:06:47 +01:00
return ;
}
2019-01-19 00:49:29 +01:00
s_keepAttributes = classifDlg . keepAttributesCheckBox - > isChecked ( ) ;
2018-11-04 23:31:48 +01:00
masc : : Tools : : NamedClouds clouds ;
QString mainCloudLabel ;
classifDlg . getClouds ( clouds , mainCloudLabel ) ;
2018-11-04 11:21:50 +01:00
masc : : Feature : : Set features ;
2018-11-04 23:31:48 +01:00
masc : : Classifier classifier ;
if ( ! masc : : Tools : : LoadClassifier ( inputFilename , clouds , features , classifier , m_app - > getMainWindow ( ) ) )
2018-10-22 19:01:30 +02:00
{
return ;
}
2019-03-27 15:03:58 +01:00
if ( ! classifier . isValid ( ) )
2019-03-26 14:28:27 +01:00
{
m_app - > dispToConsole ( " No classifier or invalid classifier " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
2018-10-22 19:01:30 +02:00
2019-03-24 23:16:55 +01:00
if ( clouds . contains ( " TEST " ) )
{
//remove the test cloud (if any)
clouds . remove ( " TEST " ) ;
}
2018-11-04 23:31:48 +01:00
//the 'main cloud' is the cloud that should be classified
masc : : CorePoints corePoints ;
corePoints . origin = corePoints . cloud = clouds [ mainCloudLabel ] ;
2019-03-25 22:21:40 +01:00
corePoints . role = mainCloudLabel ;
2018-10-22 19:01:30 +02:00
2018-11-04 23:31:48 +01:00
//prepare the main cloud
2019-01-20 22:12:20 +01:00
ccProgressDialog progressDlg ( true , m_app - > getMainWindow ( ) ) ;
progressDlg . setAutoClose ( false ) ; //we don't want the progress dialog to 'pop' for each feature
2018-11-04 23:31:48 +01:00
QString error ;
2019-01-19 00:49:29 +01:00
SFCollector generatedScalarFields ;
2019-01-20 22:12:20 +01:00
if ( ! masc : : Tools : : PrepareFeatures ( corePoints , features , error , & progressDlg , & generatedScalarFields ) )
2018-11-04 23:31:48 +01:00
{
m_app - > dispToConsole ( error , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( false ) ;
2018-11-04 23:31:48 +01:00
return ;
}
2019-01-20 22:12:20 +01:00
progressDlg . close ( ) ;
2018-11-04 23:31:48 +01:00
QCoreApplication : : processEvents ( ) ;
2019-01-20 22:12:20 +01:00
progressDlg . setAutoClose ( true ) ; //restore the default behavior of the progress dialog
2018-11-04 23:31:48 +01:00
//apply classifier
{
QString errorMessage ;
2019-03-26 14:28:27 +01:00
masc : : Feature : : Source : : Set featureSources ;
masc : : Feature : : ExtractSources ( features , featureSources ) ;
if ( ! classifier . classify ( featureSources , corePoints . cloud , errorMessage , m_app - > getMainWindow ( ) ) )
2018-11-04 23:31:48 +01:00
{
m_app - > dispToConsole ( errorMessage , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( false ) ;
2018-11-04 23:31:48 +01:00
return ;
}
2019-01-19 00:49:29 +01:00
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( s_keepAttributes ) ;
2018-11-04 23:31:48 +01:00
}
}
2019-01-20 22:12:20 +01:00
struct FeatureSelection
{
FeatureSelection ( masc : : Feature : : Shared f = masc : : Feature : : Shared ( nullptr ) ) : feature ( f ) { }
masc : : Feature : : Shared feature ;
bool selected = true ;
bool prepared = false ;
float importance = std : : numeric_limits < float > : : quiet_NaN ( ) ;
} ;
2018-11-04 23:31:48 +01:00
void q3DMASCPlugin : : doTrainAction ( )
{
//disclaimer accepted?
if ( ! ShowTrainDisclaimer ( m_app ) )
return ;
2018-11-04 00:06:47 +01:00
QString inputFilename ;
{
QSettings settings ;
settings . beginGroup ( " 3DMASC " ) ;
QString inputPath = settings . value ( " FilePath " , QCoreApplication : : applicationDirPath ( ) ) . toString ( ) ;
2018-11-04 23:31:48 +01:00
inputFilename = QFileDialog : : getOpenFileName ( m_app - > getMainWindow ( ) , " Load 3DMASC training file " , inputPath , " *.txt " ) ;
2018-11-04 00:06:47 +01:00
if ( inputFilename . isNull ( ) )
{
//process cancelled by the user
return ;
}
settings . setValue ( " FilePath " , QFileInfo ( inputFilename ) . absolutePath ( ) ) ;
settings . endGroup ( ) ;
}
2019-01-21 00:10:47 +01:00
//load the cloud labels (PC1, PC2, CTX, etc.)
2019-03-26 14:28:27 +01:00
QList < QString > cloudLabels ;
2019-03-25 18:45:28 +01:00
QString corePointsLabel ;
bool filenamesSpecified = false ;
if ( ! masc : : Tools : : LoadClassifierCloudLabels ( inputFilename , cloudLabels , corePointsLabel , filenamesSpecified ) )
2019-01-21 00:10:47 +01:00
{
m_app - > dispToConsole ( " Failed to read classifier file (see Console) " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
if ( cloudLabels . empty ( ) )
{
m_app - > dispToConsole ( " Invalid classifier file (no cloud label defined) " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
static bool s_keepAttributes = false ;
masc : : Tools : : NamedClouds loadedClouds ;
2019-03-25 18:45:28 +01:00
masc : : CorePoints corePoints ;
2019-01-21 00:10:47 +01:00
2019-03-25 18:45:28 +01:00
//if no filename is specified in the training file, we are bound to ask the user to specify them
bool useCloudsFromDB = ( ! filenamesSpecified | | QMessageBox : : question ( m_app - > getMainWindow ( ) , " Use clouds in DB " , " Use clouds in db (yes) or clouds specified in the file(no)? " , QMessageBox : : Yes , QMessageBox : : No ) = = QMessageBox : : Yes ) ;
2019-03-28 21:29:03 +01:00
QString mainCloudLabel = corePointsLabel ;
2019-01-21 00:10:47 +01:00
if ( useCloudsFromDB )
{
2019-03-29 13:40:57 +01:00
if ( cloudLabels . size ( ) > 4 + ( cloudLabels . contains ( " TEST " ) ? 1 : 0 ) )
2019-01-21 00:10:47 +01:00
{
2019-03-29 13:40:57 +01:00
m_app - > dispToConsole ( " This classifier uses more than 4 different clouds (the GUI version cannot handle it) " , ccMainAppInterface : : WRN_CONSOLE_MESSAGE ) ;
2019-01-21 00:10:47 +01:00
return ;
}
//now show a dialog where the user will be able to set the cloud roles
Classify3DMASCDialog classifDlg ( m_app , true ) ;
classifDlg . setWindowTitle ( " 3DMASC Train " ) ;
2019-03-25 18:45:28 +01:00
classifDlg . setCloudRoles ( cloudLabels , corePointsLabel ) ;
2019-01-21 00:10:47 +01:00
classifDlg . classifierFileLineEdit - > setText ( inputFilename ) ;
classifDlg . keepAttributesCheckBox - > setChecked ( s_keepAttributes ) ;
if ( ! classifDlg . exec ( ) )
{
//process cancelled by the user
return ;
}
s_keepAttributes = classifDlg . keepAttributesCheckBox - > isChecked ( ) ;
2019-01-19 21:40:38 +01:00
2019-01-21 00:10:47 +01:00
classifDlg . getClouds ( loadedClouds , mainCloudLabel ) ;
2019-03-25 18:45:28 +01:00
m_app - > dispToConsole ( " Training cloud: " + mainCloudLabel , ccMainAppInterface : : STD_CONSOLE_MESSAGE ) ;
corePoints . origin = loadedClouds [ mainCloudLabel ] ;
2019-03-25 22:21:40 +01:00
corePoints . role = mainCloudLabel ;
2019-01-21 00:10:47 +01:00
}
static masc : : TrainParameters s_params ;
2018-11-04 23:31:48 +01:00
masc : : Feature : : Set features ;
2019-03-28 21:29:03 +01:00
if ( ! masc : : Tools : : LoadTrainingFile ( inputFilename , features , loadedClouds , s_params , & corePoints , m_app - > getMainWindow ( ) ) )
2018-11-04 00:06:47 +01:00
{
2019-01-21 00:10:47 +01:00
m_app - > dispToConsole ( " Failed to load the training file " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2018-10-24 23:14:40 +02:00
return ;
}
2019-03-25 18:45:28 +01:00
if ( ! corePoints . origin )
{
m_app - > dispToConsole ( " Core points not defined " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
2019-03-28 21:29:03 +01:00
if ( mainCloudLabel . isEmpty ( ) )
{
mainCloudLabel = corePoints . role ;
}
2019-04-05 11:15:57 +02:00
if ( ! masc : : Tools : : GetClassificationSF ( corePoints . origin ) )
2019-03-25 18:45:28 +01:00
{
m_app - > dispToConsole ( " Missing 'Classification' field on core points cloud " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
2018-11-04 00:06:47 +01:00
ccHObject * group = new ccHObject ( " 3DMASC " ) ;
2019-01-21 00:10:47 +01:00
if ( ! useCloudsFromDB )
2018-11-04 00:06:47 +01:00
{
2019-01-21 00:10:47 +01:00
//add the loaded clouds to the main DB (so that we don't need to handle them anymore)
for ( masc : : Tools : : NamedClouds : : const_iterator it = loadedClouds . begin ( ) ; it ! = loadedClouds . end ( ) ; + + it )
{
group - > addChild ( it . value ( ) ) ;
}
2018-11-04 00:06:47 +01:00
}
2019-03-25 18:45:28 +01:00
for ( masc : : Tools : : NamedClouds : : const_iterator it = loadedClouds . begin ( ) ; it ! = loadedClouds . end ( ) ; + + it )
{
m_app - > dispToConsole ( it . key ( ) + " = " + it . value ( ) - > getName ( ) , ccMainAppInterface : : STD_CONSOLE_MESSAGE ) ;
}
2019-03-26 14:28:27 +01:00
//test role
ccPointCloud * testCloud = nullptr ;
bool needTestSuite = false ;
masc : : Feature : : Set featuresTest ;
if ( loadedClouds . contains ( " TEST " ) )
{
testCloud = loadedClouds [ " TEST " ] ;
loadedClouds . remove ( " TEST " ) ;
if ( testCloud ! = corePoints . origin & & testCloud ! = corePoints . cloud )
{
//we need a duplicated test suite!!!
needTestSuite = true ;
//replace the main cloud by the test cloud
masc : : Tools : : NamedClouds loadedCloudsTest ;
loadedCloudsTest = loadedClouds ;
loadedCloudsTest [ mainCloudLabel ] = testCloud ;
//simply reload the classification file to create duplicated features
masc : : TrainParameters tempParams ;
if ( ! masc : : Tools : : LoadTrainingFile ( inputFilename , featuresTest , loadedCloudsTest , tempParams ) )
{
m_app - > dispToConsole ( " Failed to load the training file (for test) " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
return ;
}
}
}
2019-01-20 22:12:20 +01:00
//show the training dialog for the first time
Train3DMASCDialog trainDlg ( m_app - > getMainWindow ( ) ) ;
trainDlg . maxDepthSpinBox - > setValue ( s_params . rt . maxDepth ) ;
trainDlg . maxTreeCountSpinBox - > setValue ( s_params . rt . maxTreeCount ) ;
trainDlg . activeVarCountSpinBox - > setValue ( s_params . rt . activeVarCount ) ;
trainDlg . minSampleCountSpinBox - > setValue ( s_params . rt . minSampleCount ) ;
trainDlg . testDataRatioSpinBox - > setValue ( static_cast < int > ( s_params . testDataRatio * 100 ) ) ;
2019-03-26 14:28:27 +01:00
trainDlg . testDataRatioSpinBox - > setEnabled ( testCloud = = nullptr ) ;
2019-01-20 22:12:20 +01:00
//display the loaded features and let the user select the ones to use
trainDlg . setResultText ( " Select features and press 'Run' " ) ;
std : : vector < FeatureSelection > originalFeatures ;
originalFeatures . reserve ( features . size ( ) ) ;
for ( const masc : : Feature : : Shared & f : features )
{
originalFeatures . push_back ( FeatureSelection ( f ) ) ;
trainDlg . addFeature ( f - > toString ( ) , originalFeatures . back ( ) . importance , originalFeatures . back ( ) . selected ) ;
}
2019-03-26 14:28:27 +01:00
std : : vector < FeatureSelection > originalFeaturesTest ;
if ( testCloud & & needTestSuite )
{
originalFeaturesTest . reserve ( featuresTest . size ( ) ) ;
for ( const masc : : Feature : : Shared & f : featuresTest )
{
originalFeaturesTest . push_back ( FeatureSelection ( f ) ) ;
}
}
2019-01-20 22:12:20 +01:00
if ( ! trainDlg . exec ( ) )
{
2019-01-21 00:10:47 +01:00
delete group ;
2019-01-20 22:12:20 +01:00
return ;
}
assert ( ! trainDlg . shouldSaveClassifier ( ) ) ; //the save button should be disabled at this point
//compute the core points (if necessary)
ccProgressDialog progressDlg ( true , m_app - > getMainWindow ( ) ) ;
if ( ! corePoints . prepare ( & progressDlg ) )
2018-11-04 00:06:47 +01:00
{
m_app - > dispToConsole ( " Failed to compute/prepare the core points! " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
delete group ;
return ;
}
2019-03-26 14:28:27 +01:00
2018-11-04 00:06:47 +01:00
if ( corePoints . cloud ! = corePoints . origin )
{
//auto-hide the other clouds
for ( ccPointCloud * pc : loadedClouds )
{
pc - > setEnabled ( false ) ;
}
//set an explicit name for the core points
QString corePointsName = corePoints . origin - > getName ( ) ;
switch ( corePoints . selectionMethod )
{
case masc : : CorePoints : : NONE :
break ;
case masc : : CorePoints : : RANDOM :
corePointsName + = " _SS_Random@ " + QString : : number ( corePoints . selectionParam ) ;
break ;
case masc : : CorePoints : : SPATIAL :
corePointsName + = " _SS_Spatial@ " + QString : : number ( corePoints . selectionParam ) ;
break ;
default :
assert ( false ) ;
}
corePoints . cloud - > setName ( QString ( " Core points (%1) " ) . arg ( corePointsName ) ) ;
group - > addChild ( corePoints . cloud ) ;
}
2019-03-24 23:16:55 +01:00
2019-01-21 00:10:47 +01:00
if ( group - > getChildrenNumber ( ) ! = 0 )
{
m_app - > addToDB ( group ) ;
QCoreApplication : : processEvents ( ) ;
}
else
{
delete group ;
group = nullptr ;
}
2018-11-04 00:06:47 +01:00
2019-01-20 22:12:20 +01:00
//train / test subsets
2019-03-26 14:28:27 +01:00
QSharedPointer < CCLib : : ReferenceCloud > trainSubset , testSubset ;
2019-03-24 23:16:55 +01:00
float previousTestSubsetRatio = - 1.0f ;
2019-03-26 14:28:27 +01:00
SFCollector generatedScalarFields , generatedScalarFieldsTest ;
2019-01-20 22:12:20 +01:00
2019-03-26 14:28:27 +01:00
//we will train + evaluate the classifier, then display the reuslts
//then let the user change parameters and (potentially) start again
2019-01-20 22:12:20 +01:00
for ( int iteration = 0 ; ; + + iteration )
2018-10-24 11:30:39 +02:00
{
2019-01-20 22:12:20 +01:00
//look for selected features
features . clear ( ) ;
masc : : Feature : : Set toPrepare ;
for ( size_t i = 0 ; i < originalFeatures . size ( ) ; + + i )
{
2019-03-27 15:03:58 +01:00
originalFeatures [ i ] . selected = trainDlg . isFeatureSelected ( originalFeatures [ i ] . feature - > toString ( ) ) ;
2018-10-22 19:01:30 +02:00
2019-01-20 22:12:20 +01:00
//if the feature is selected
if ( originalFeatures [ i ] . selected )
{
if ( ! originalFeatures [ i ] . prepared )
{
//we should prepare it first!
toPrepare . push_back ( originalFeatures [ i ] . feature ) ;
}
features . push_back ( originalFeatures [ i ] . feature ) ;
}
}
2018-12-01 14:39:11 +01:00
2019-03-26 14:28:27 +01:00
masc : : Classifier classifier ;
2019-01-20 22:12:20 +01:00
if ( features . empty ( ) )
2019-01-19 21:40:38 +01:00
{
2019-01-20 22:12:20 +01:00
m_app - > dispToConsole ( " No feature selected! " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
}
2019-03-26 14:28:27 +01:00
else
2019-01-20 22:12:20 +01:00
{
2019-03-26 14:28:27 +01:00
//prepare the features (should be done once)
if ( ! toPrepare . empty ( ) )
2019-01-20 22:12:20 +01:00
{
2019-03-26 14:28:27 +01:00
progressDlg . setAutoClose ( false ) ; //we don't want the progress dialog to 'pop' for each feature
QString error ;
if ( ! masc : : Tools : : PrepareFeatures ( corePoints , toPrepare , error , & progressDlg , & generatedScalarFields ) )
{
m_app - > dispToConsole ( error , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( false ) ;
generatedScalarFieldsTest . releaseSFs ( false ) ;
2019-03-26 14:28:27 +01:00
return ;
}
progressDlg . setAutoClose ( true ) ; //restore the default behavior of the progress dialog
progressDlg . hide ( ) ;
QCoreApplication : : processEvents ( ) ;
m_app - > redrawAll ( ) ;
2019-01-20 22:12:20 +01:00
2019-03-26 14:28:27 +01:00
//flag the prepared features as 'prepared' ;)
for ( FeatureSelection & fs : originalFeatures )
{
if ( fs . selected & & ! fs . prepared )
fs . prepared = true ;
}
2019-01-20 22:12:20 +01:00
}
2018-10-26 12:02:23 +02:00
2019-03-26 14:28:27 +01:00
//retrieve parameters
s_params . rt . maxDepth = trainDlg . maxDepthSpinBox - > value ( ) ;
s_params . rt . maxTreeCount = trainDlg . maxTreeCountSpinBox - > value ( ) ;
s_params . rt . activeVarCount = trainDlg . activeVarCountSpinBox - > value ( ) ;
s_params . rt . minSampleCount = trainDlg . minSampleCountSpinBox - > value ( ) ;
float testDataRatio = 0.0f ;
2019-01-20 22:12:20 +01:00
2019-03-26 14:28:27 +01:00
if ( ! testCloud )
2019-03-24 23:16:55 +01:00
{
2019-03-26 14:28:27 +01:00
//we need to generate test subsets
testDataRatio = s_params . testDataRatio = trainDlg . testDataRatioSpinBox - > value ( ) / 100.0f ;
if ( testDataRatio < 0.0f | | testDataRatio > 0.99f )
2019-01-20 22:12:20 +01:00
{
2019-03-26 14:28:27 +01:00
assert ( false ) ;
m_app - > dispToConsole ( " Invalid test data ratio " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
trainSubset . clear ( ) ;
testSubset . clear ( ) ;
}
else if ( previousTestSubsetRatio ! = testDataRatio )
{
if ( ! trainSubset )
trainSubset . reset ( new CCLib : : ReferenceCloud ( corePoints . cloud ) ) ;
trainSubset - > clear ( ) ;
if ( ! testSubset )
testSubset . reset ( new CCLib : : ReferenceCloud ( corePoints . cloud ) ) ;
testSubset - > clear ( ) ;
//randomly select the training points
if ( ! masc : : Tools : : RandomSubset ( corePoints . cloud , testDataRatio , testSubset . data ( ) , trainSubset . data ( ) ) )
{
m_app - > dispToConsole ( " Not enough memory to generate the test subsets " , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( false ) ;
generatedScalarFieldsTest . releaseSFs ( false ) ;
2019-03-26 14:28:27 +01:00
return ;
}
previousTestSubsetRatio = testDataRatio ;
2019-01-20 22:12:20 +01:00
}
2018-11-04 00:06:47 +01:00
}
2019-03-26 14:28:27 +01:00
//extract the sources (after having prepared the features!)
masc : : Feature : : Source : : Set featureSources ;
masc : : Feature : : ExtractSources ( features , featureSources ) ;
2019-01-20 22:12:20 +01:00
//train the classifier
2019-01-19 21:40:38 +01:00
{
2019-01-20 22:12:20 +01:00
QString errorMessage ;
2019-03-26 14:28:27 +01:00
if ( ! classifier . train ( corePoints . cloud ,
s_params . rt ,
featureSources ,
errorMessage ,
trainSubset . data ( ) ,
m_app ,
m_app - > getMainWindow ( )
) )
2019-01-19 21:40:38 +01:00
{
2019-01-20 22:12:20 +01:00
m_app - > dispToConsole ( errorMessage , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( false ) ;
generatedScalarFieldsTest . releaseSFs ( false ) ;
2019-01-19 21:40:38 +01:00
return ;
}
2019-01-20 22:12:20 +01:00
trainDlg . setFirstRunDone ( ) ;
trainDlg . shouldSaveClassifier ( ) ;
2019-01-19 21:40:38 +01:00
}
2019-01-20 22:12:20 +01:00
//test the trained classifier
2019-01-19 21:40:38 +01:00
{
2019-03-26 14:28:27 +01:00
if ( testCloud )
{
//look for selected features
if ( needTestSuite )
{
featuresTest . clear ( ) ;
masc : : Feature : : Set toPrepareTest ;
for ( size_t i = 0 ; i < originalFeaturesTest . size ( ) ; + + i )
{
2019-03-27 15:03:58 +01:00
originalFeaturesTest [ i ] . selected = trainDlg . isFeatureSelected ( originalFeatures [ i ] . feature - > toString ( ) ) ;
2019-03-26 14:28:27 +01:00
//if the feature is selected
if ( originalFeaturesTest [ i ] . selected )
{
if ( ! originalFeaturesTest [ i ] . prepared )
{
//we should prepare it first!
toPrepareTest . push_back ( originalFeaturesTest [ i ] . feature ) ;
}
featuresTest . push_back ( originalFeaturesTest [ i ] . feature ) ;
}
}
//prepare the features and the test cloud
if ( ! toPrepareTest . empty ( ) )
{
progressDlg . setAutoClose ( false ) ; //we don't want the progress dialog to 'pop' for each feature
QString error ;
masc : : CorePoints corePointsTest ;
corePointsTest . cloud = corePointsTest . origin = testCloud ;
corePointsTest . role = mainCloudLabel ;
if ( ! masc : : Tools : : PrepareFeatures ( corePointsTest , toPrepareTest , error , & progressDlg , & generatedScalarFieldsTest ) )
{
m_app - > dispToConsole ( error , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( false ) ;
generatedScalarFieldsTest . releaseSFs ( false ) ;
2019-03-26 14:28:27 +01:00
return ;
}
progressDlg . setAutoClose ( true ) ; //restore the default behavior of the progress dialog
progressDlg . hide ( ) ;
QCoreApplication : : processEvents ( ) ;
m_app - > redrawAll ( ) ;
//flag the prepared features as 'prepared' ;)
for ( FeatureSelection & fs : originalFeaturesTest )
{
if ( fs . selected & & ! fs . prepared )
fs . prepared = true ;
}
}
}
}
2019-01-20 22:12:20 +01:00
masc : : Classifier : : AccuracyMetrics metrics ;
QString errorMessage ;
2019-03-26 14:28:27 +01:00
if ( ! classifier . evaluate ( featureSources ,
testCloud ? testCloud : corePoints . cloud ,
metrics ,
errorMessage ,
testCloud ? nullptr : testSubset . data ( ) ,
" Classification_pred " ,
m_app - > getMainWindow ( ) ) )
2019-01-20 22:12:20 +01:00
{
m_app - > dispToConsole ( errorMessage , ccMainAppInterface : : ERR_CONSOLE_MESSAGE ) ;
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( false ) ;
generatedScalarFieldsTest . releaseSFs ( false ) ;
2019-01-20 22:12:20 +01:00
return ;
}
QString resultText = QString ( " Correct guess = %1 / %2 --> accuracy = %3 " ) . arg ( metrics . goodGuess ) . arg ( metrics . sampleCount ) . arg ( metrics . ratio ) ;
m_app - > dispToConsole ( resultText , ccMainAppInterface : : STD_CONSOLE_MESSAGE ) ;
trainDlg . setResultText ( resultText ) ;
cv : : Mat importanceMat = classifier . getVarImportance ( ) ;
//m_app->dispToConsole(QString("Var importance size = %1 x %2").arg(importanceMat.rows).arg(importanceMat.cols));
assert ( static_cast < int > ( features . size ( ) ) = = importanceMat . rows ) ;
int selectedFeatureIndex = 0 ;
for ( size_t i = 0 ; i < originalFeatures . size ( ) ; + + i )
{
if ( originalFeatures [ i ] . selected )
{
//m_app->dispToConsole(QString("Feature #%1 importance = %2").arg(i + 1).arg(importanceMat.at<float>(i, 0)));
assert ( selectedFeatureIndex < importanceMat . rows ) ;
originalFeatures [ i ] . importance = importanceMat . at < float > ( selectedFeatureIndex , 0 ) ;
+ + selectedFeatureIndex ;
}
else
{
originalFeatures [ i ] . importance = std : : numeric_limits < float > : : quiet_NaN ( ) ;
}
2019-03-27 15:03:58 +01:00
trainDlg . setFeatureImportance ( originalFeatures [ i ] . feature - > toString ( ) , originalFeatures [ i ] . importance ) ;
2019-01-20 22:12:20 +01:00
}
2019-03-25 22:45:39 +01:00
trainDlg . sortByFeatureImportance ( ) ;
2019-01-19 21:40:38 +01:00
}
2018-11-04 23:31:48 +01:00
}
2018-10-26 12:02:23 +02:00
2019-03-26 14:28:27 +01:00
//now wait for the user input
2019-01-20 22:12:20 +01:00
while ( true )
2018-11-04 00:06:47 +01:00
{
2019-01-20 22:12:20 +01:00
if ( ! trainDlg . exec ( ) )
2019-01-19 21:40:38 +01:00
{
2019-01-20 22:12:20 +01:00
//the dialog can be closed
2019-05-03 23:52:33 +02:00
generatedScalarFields . releaseSFs ( s_keepAttributes ) ;
generatedScalarFieldsTest . releaseSFs ( s_keepAttributes ) ;
2019-01-19 21:40:38 +01:00
return ;
}
2019-01-20 22:12:20 +01:00
//if the save button has been clicked
if ( trainDlg . shouldSaveClassifier ( ) )
{
//ask for the output filename
QString outputFilename ;
{
QSettings settings ;
settings . beginGroup ( " 3DMASC " ) ;
QString outputPath = settings . value ( " FilePath " , QCoreApplication : : applicationDirPath ( ) ) . toString ( ) ;
outputFilename = QFileDialog : : getSaveFileName ( m_app - > getMainWindow ( ) , " Save 3DMASC classifier " , outputPath , " *.txt " ) ;
if ( outputFilename . isNull ( ) )
{
//process cancelled by the user
continue ;
}
settings . setValue ( " FilePath " , QFileInfo ( outputFilename ) . absolutePath ( ) ) ;
settings . endGroup ( ) ;
}
//save the classifier
2019-03-26 14:28:27 +01:00
if ( masc : : Tools : : SaveClassifier ( outputFilename , features , mainCloudLabel , classifier , m_app - > getMainWindow ( ) ) )
2019-01-20 22:12:20 +01:00
{
m_app - > dispToConsole ( " Classifier succesfully saved to " + outputFilename , ccMainAppInterface : : STD_CONSOLE_MESSAGE ) ;
trainDlg . setClassifierSaved ( ) ;
}
else
{
m_app - > dispToConsole ( " Failed to save classifier file " ) ;
}
}
else //we will run the classifier another time
{
//stop the local loop
break ;
}
2018-11-04 00:06:47 +01:00
}
2018-10-26 12:02:23 +02:00
2019-01-20 22:12:20 +01:00
//we are going to restart the classification process
2018-11-04 00:06:47 +01:00
}
2018-10-22 19:01:30 +02:00
}
void q3DMASCPlugin : : registerCommands ( ccCommandLineInterface * cmd )
{
if ( ! cmd )
{
assert ( false ) ;
return ;
}
2019-01-19 00:49:29 +01:00
cmd - > registerCommand ( ccCommandLineInterface : : Command : : Shared ( new Command3DMASCClassif ) ) ;
2018-10-22 19:01:30 +02:00
}