2018-10-26 10:41:22 +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 "q3DMASCClassifier.h"
//Local
# include "ScalarFieldWrappers.h"
2019-04-05 11:15:57 +02:00
# include "q3DMASCTools.h"
2018-10-26 10:41:22 +02:00
//qCC_db
# include <ccPointCloud.h>
2018-11-04 23:31:48 +01:00
# include <ccScalarField.h>
# include <ccProgressDialog.h>
2018-10-26 10:41:22 +02:00
# include <ccLog.h>
2019-03-23 20:42:01 +01:00
//qPDALIO
2020-06-04 00:32:27 +02:00
# include "../../core/IO/qPDALIO/include/LASFields.h"
2018-10-26 10:41:22 +02:00
2019-01-20 22:12:20 +01:00
//qCC_plugins
# include <ccMainAppInterface.h>
2018-10-26 10:41:22 +02:00
//Qt
# include <QCoreApplication>
# include <QProgressDialog>
2018-11-04 11:56:13 +01:00
# include <QtConcurrent>
2018-10-26 10:41:22 +02:00
2023-01-18 23:57:59 +01:00
# include "qTrain3DMASCDialog.h"
2022-12-14 11:37:29 +01:00
# include "confusionmatrix.h"
2018-10-26 10:41:22 +02:00
using namespace masc ;
Classifier : : Classifier ( )
{
}
bool Classifier : : isValid ( ) const
{
2019-03-29 13:40:29 +01:00
return ( m_rtrees & & m_rtrees - > isClassifier ( ) & & m_rtrees - > isTrained ( ) ) ;
2018-10-26 10:41:22 +02:00
}
2019-03-26 14:28:27 +01:00
static IScalarFieldWrapper : : Shared GetSource ( const Feature : : Source & fs , const ccPointCloud * cloud )
2018-10-26 12:02:23 +02:00
{
2019-03-23 20:42:01 +01:00
IScalarFieldWrapper : : Shared source ( nullptr ) ;
2018-10-26 10:41:22 +02:00
2019-03-26 14:28:27 +01:00
switch ( fs . type )
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
case Feature : : Source : : ScalarField :
2018-10-26 12:02:23 +02:00
{
2019-03-27 15:03:58 +01:00
assert ( ! fs . name . isEmpty ( ) ) ;
2019-03-26 14:28:27 +01:00
int sfIdx = cloud - > getScalarFieldIndexByName ( qPrintable ( fs . name ) ) ;
2018-10-26 12:02:23 +02:00
if ( sfIdx > = 0 )
{
source . reset ( new ScalarFieldWrapper ( cloud - > getScalarField ( sfIdx ) ) ) ;
}
else
{
2019-03-26 14:28:27 +01:00
ccLog : : Warning ( QObject : : tr ( " Internal error: unknwon scalar field '%1' " ) . arg ( fs . name ) ) ;
2019-03-23 20:42:01 +01:00
return IScalarFieldWrapper : : Shared ( nullptr ) ;
2018-10-26 12:02:23 +02:00
}
}
break ;
2019-03-26 14:28:27 +01:00
case Feature : : Source : : DimX :
2018-10-26 12:02:23 +02:00
source . reset ( new DimScalarFieldWrapper ( cloud , DimScalarFieldWrapper : : DimX ) ) ;
break ;
2019-03-26 14:28:27 +01:00
case Feature : : Source : : DimY :
2018-10-26 12:02:23 +02:00
source . reset ( new DimScalarFieldWrapper ( cloud , DimScalarFieldWrapper : : DimY ) ) ;
break ;
2019-03-26 14:28:27 +01:00
case Feature : : Source : : DimZ :
2018-10-26 12:02:23 +02:00
source . reset ( new DimScalarFieldWrapper ( cloud , DimScalarFieldWrapper : : DimZ ) ) ;
break ;
2019-03-26 14:28:27 +01:00
case Feature : : Source : : Red :
2018-10-26 12:02:23 +02:00
source . reset ( new ColorScalarFieldWrapper ( cloud , ColorScalarFieldWrapper : : Red ) ) ;
break ;
2019-03-26 14:28:27 +01:00
case Feature : : Source : : Green :
2018-10-26 12:02:23 +02:00
source . reset ( new ColorScalarFieldWrapper ( cloud , ColorScalarFieldWrapper : : Green ) ) ;
break ;
2019-03-26 14:28:27 +01:00
case Feature : : Source : : Blue :
2018-10-26 12:02:23 +02:00
source . reset ( new ColorScalarFieldWrapper ( cloud , ColorScalarFieldWrapper : : Blue ) ) ;
break ;
}
return source ;
}
2019-03-26 14:28:27 +01:00
bool Classifier : : classify ( const Feature : : Source : : Set & featureSources ,
ccPointCloud * cloud ,
QString & errorMessage ,
QWidget * parentWidget /*=nullptr*/
)
2018-11-04 23:31:48 +01:00
{
if ( ! cloud )
{
assert ( false ) ;
errorMessage = QObject : : tr ( " Invalid input " ) ;
return false ;
}
2019-03-29 13:40:29 +01:00
if ( ! isValid ( ) )
2018-11-04 23:31:48 +01:00
{
2019-03-29 13:40:29 +01:00
errorMessage = QObject : : tr ( " Invalid classifier " ) ;
2018-11-04 23:31:48 +01:00
return false ;
}
2019-03-26 14:28:27 +01:00
if ( featureSources . empty ( ) )
2018-11-04 23:31:48 +01:00
{
2019-03-26 14:28:27 +01:00
errorMessage = QObject : : tr ( " Training method called without any feature (source)?! " ) ;
2018-11-04 23:31:48 +01:00
return false ;
}
//look for the classification field
2020-06-04 00:32:27 +02:00
CCCoreLib : : ScalarField * classificationSF = Tools : : GetClassificationSF ( cloud ) ;
2022-12-08 09:19:52 +01:00
// add a ccConfidence value if needed
2022-12-14 11:37:29 +01:00
int cvConfidenceIdx = cloud - > getScalarFieldIndexByName ( " Classification_confidence " ) ;
2022-12-09 17:45:08 +01:00
if ( cvConfidenceIdx > 0 ) // if the scalar field exists, delete it
cloud - > deleteScalarField ( cvConfidenceIdx ) ;
else
2022-12-14 11:37:29 +01:00
cvConfidenceIdx = cloud - > addScalarField ( " Classification_confidence " ) ;
2022-12-08 09:19:52 +01:00
CCCoreLib : : ScalarField * cvConfidenceSF = cloud - > getScalarField ( cvConfidenceIdx ) ;
2019-03-27 16:48:19 +01:00
2022-12-14 11:37:29 +01:00
ccScalarField * classifSFBackup = nullptr ;
2019-03-27 16:48:19 +01:00
if ( classificationSF )
{
//save previous classification field (if any)
int sfIdx = cloud - > getScalarFieldIndexByName ( " Classification_prev " ) ;
2022-12-09 17:45:08 +01:00
if ( sfIdx > 0 )
2019-03-27 16:48:19 +01:00
cloud - > deleteScalarField ( sfIdx ) ;
try
{
2022-12-14 11:37:29 +01:00
classifSFBackup = new ccScalarField ( * static_cast < ccScalarField * > ( classificationSF ) ) ;
2019-03-27 22:12:51 +01:00
classifSFBackup - > setName ( " Classification_prev " ) ;
2019-03-27 16:48:19 +01:00
cloud - > addScalarField ( classifSFBackup ) ;
}
catch ( const std : : bad_alloc )
{
ccLog : : Warning ( " Not enough memory to backup the previous classification SF! " ) ;
}
}
else
2018-11-04 23:31:48 +01:00
{
2019-03-27 16:48:19 +01:00
//create the classification SF
2018-11-04 23:31:48 +01:00
ccScalarField * _classificationSF = new ccScalarField ( LAS_FIELD_NAMES [ LAS_CLASSIFICATION ] ) ;
if ( ! _classificationSF - > resizeSafe ( cloud - > size ( ) ) )
{
_classificationSF - > release ( ) ;
errorMessage = QObject : : tr ( " Not enough memory " ) ;
return false ;
}
2019-05-03 21:38:02 +02:00
cloud - > addScalarField ( _classificationSF ) ;
2018-11-04 23:31:48 +01:00
classificationSF = _classificationSF ;
}
assert ( classificationSF ) ;
classificationSF - > fill ( 0 ) ; //0 = no classification?
int sampleCount = static_cast < int > ( cloud - > size ( ) ) ;
2019-03-26 14:28:27 +01:00
int attributesPerSample = static_cast < int > ( featureSources . size ( ) ) ;
2018-11-04 23:31:48 +01:00
ccLog : : Print ( QObject : : tr ( " [3DMASC] Classifying %1 points with %2 feature(s) " ) . arg ( sampleCount ) . arg ( attributesPerSample ) ) ;
//create the field wrappers
2019-03-23 20:42:01 +01:00
std : : vector < IScalarFieldWrapper : : Shared > wrappers ;
2018-11-04 23:31:48 +01:00
{
wrappers . reserve ( attributesPerSample ) ;
for ( int fIndex = 0 ; fIndex < attributesPerSample ; + + fIndex )
{
2019-03-26 14:28:27 +01:00
const Feature : : Source & fs = featureSources [ fIndex ] ;
2018-11-04 23:31:48 +01:00
2019-03-26 14:28:27 +01:00
IScalarFieldWrapper : : Shared source = GetSource ( fs , cloud ) ;
2018-11-04 23:31:48 +01:00
if ( ! source | | ! source - > isValid ( ) )
{
assert ( false ) ;
2019-03-26 14:28:27 +01:00
errorMessage = QObject : : tr ( " Internal error: invalid source '%1' " ) . arg ( fs . name ) ;
2018-11-04 23:31:48 +01:00
return false ;
}
wrappers . push_back ( source ) ;
}
}
QScopedPointer < ccProgressDialog > pDlg ;
if ( parentWidget )
{
pDlg . reset ( new ccProgressDialog ( parentWidget ) ) ;
pDlg - > setLabelText ( QString ( " Classify (%1 points) " ) . arg ( sampleCount ) ) ;
pDlg - > show ( ) ;
QCoreApplication : : processEvents ( ) ;
}
2020-06-04 00:32:27 +02:00
CCCoreLib : : NormalizedProgress nProgress ( pDlg . data ( ) , cloud - > size ( ) ) ;
2018-11-04 23:31:48 +01:00
2018-11-24 18:41:14 +01:00
bool success = true ;
2022-12-14 11:37:29 +01:00
int numberOfTrees = m_rtrees - > getRoots ( ) . size ( ) ;
2019-03-29 10:10:42 +01:00
# ifndef _DEBUG
# if defined(_OPENMP)
# pragma omp parallel for
# endif
# endif
for ( int i = 0 ; i < static_cast < int > ( cloud - > size ( ) ) ; + + i )
2018-11-04 23:31:48 +01:00
{
2019-03-29 10:10:42 +01:00
//allocate the data matrix
cv : : Mat test_data ;
try
{
test_data . create ( 1 , attributesPerSample , CV_32FC1 ) ;
}
catch ( const cv : : Exception & cvex )
{
errorMessage = cvex . msg . c_str ( ) ;
2019-03-29 10:34:17 +01:00
success = false ;
break ;
2019-03-29 10:10:42 +01:00
}
2018-11-04 23:31:48 +01:00
for ( int fIndex = 0 ; fIndex < attributesPerSample ; + + fIndex )
{
double value = wrappers [ fIndex ] - > pointValue ( i ) ;
test_data . at < float > ( 0 , fIndex ) = static_cast < float > ( value ) ;
}
2019-03-29 13:40:29 +01:00
float predictedClass = m_rtrees - > predict ( test_data . row ( 0 ) , cv : : noArray ( ) , cv : : ml : : DTrees : : PREDICT_MAX_VOTE ) ;
classificationSF - > setValue ( i , static_cast < int > ( predictedClass ) ) ;
2022-12-08 09:19:52 +01:00
// compute the confidence
cv : : Mat result ;
m_rtrees - > getVotes ( test_data , result , cv : : ml : : DTrees : : PREDICT_MAX_VOTE ) ;
int classIndex = - 1 ;
for ( int col = 0 ; col < result . cols ; col + + )
2022-12-14 11:37:29 +01:00
if ( predictedClass = = result . at < int > ( 0 , col ) )
2022-12-08 09:19:52 +01:00
{
classIndex = col ;
break ;
}
if ( classIndex ! = - 1 )
2022-12-14 11:37:29 +01:00
{
float nbVotes = result . at < int > ( 1 , classIndex ) ;
cvConfidenceSF - > setValue ( i , static_cast < ScalarType > ( nbVotes / numberOfTrees ) ) ;
}
2022-12-08 09:19:52 +01:00
else
cvConfidenceSF - > setValue ( i , CCCoreLib : : NAN_VALUE ) ;
2018-11-04 23:31:48 +01:00
if ( pDlg & & ! nProgress . oneStep ( ) )
{
//process cancelled by the user
2018-11-24 18:41:14 +01:00
success = false ;
break ;
2018-11-04 23:31:48 +01:00
}
}
classificationSF - > computeMinAndMax ( ) ;
2022-12-14 11:37:29 +01:00
cvConfidenceSF - > computeMinAndMax ( ) ;
2018-11-04 23:31:48 +01:00
2019-03-25 18:45:28 +01:00
//show the classification field by default
{
int classifSFIdx = cloud - > getScalarFieldIndexByName ( classificationSF - > getName ( ) ) ;
cloud - > setCurrentDisplayedScalarField ( classifSFIdx ) ;
cloud - > showSF ( true ) ;
}
2018-11-24 18:41:14 +01:00
if ( parentWidget & & cloud - > getDisplay ( ) )
{
cloud - > getDisplay ( ) - > redraw ( ) ;
QCoreApplication : : processEvents ( ) ;
}
2023-01-18 23:57:59 +01:00
ConfusionMatrix * confusionMatrix = new ConfusionMatrix ( * classifSFBackup , * classificationSF , parentWidget ) ;
2022-12-14 11:37:29 +01:00
2018-11-24 18:41:14 +01:00
return success ;
2018-11-04 23:31:48 +01:00
}
2019-03-26 14:28:27 +01:00
bool Classifier : : evaluate ( const Feature : : Source : : Set & featureSources ,
ccPointCloud * testCloud ,
AccuracyMetrics & metrics ,
QString & errorMessage ,
2023-01-18 23:57:59 +01:00
Train3DMASCDialog & train3DMASCDialog ,
2020-06-04 00:32:27 +02:00
CCCoreLib : : ReferenceCloud * testSubset /*=nullptr=*/ ,
2019-03-26 14:28:27 +01:00
QString outputSFName /*=QString()*/ ,
QWidget * parentWidget /*=nullptr*/ )
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
if ( ! testCloud )
{
//invalid input
assert ( false ) ;
errorMessage = QObject : : tr ( " Invalid input cloud " ) ;
return false ;
}
2018-10-26 12:02:23 +02:00
metrics . sampleCount = metrics . goodGuess = 0 ;
metrics . ratio = 0.0f ;
if ( ! m_rtrees | | ! m_rtrees - > isTrained ( ) )
{
errorMessage = QObject : : tr ( " Classifier hasn't been trained yet " ) ;
return false ;
}
2019-03-26 14:28:27 +01:00
if ( featureSources . empty ( ) )
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
errorMessage = QObject : : tr ( " Training method called without any feature (source)?! " ) ;
2018-10-26 12:02:23 +02:00
return false ;
}
2019-03-26 14:28:27 +01:00
if ( testSubset & & testSubset - > getAssociatedCloud ( ) ! = testCloud )
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
errorMessage = QObject : : tr ( " Invalid test subset (associated point cloud is different) " ) ;
2018-10-26 10:41:22 +02:00
return false ;
}
//look for the classification field
2020-06-04 00:32:27 +02:00
CCCoreLib : : ScalarField * classifSF = Tools : : GetClassificationSF ( testCloud ) ;
2019-03-26 14:28:27 +01:00
if ( ! classifSF | | classifSF - > size ( ) < testCloud - > size ( ) )
2018-10-26 10:41:22 +02:00
{
assert ( false ) ;
2019-03-26 14:28:27 +01:00
errorMessage = QObject : : tr ( " Missing/invalid 'Classification' field on input cloud " ) ;
2018-10-26 10:41:22 +02:00
return false ;
}
2020-06-04 00:32:27 +02:00
CCCoreLib : : ScalarField * outputSF = nullptr ;
2019-03-26 14:28:27 +01:00
if ( ! outputSFName . isEmpty ( ) )
{
int outSFIndex = testCloud - > getScalarFieldIndexByName ( qPrintable ( outputSFName ) ) ;
if ( outSFIndex < 0 )
{
ccScalarField * _outputSF = new ccScalarField ( qPrintable ( outputSFName ) ) ;
if ( ! _outputSF - > resizeSafe ( testCloud - > size ( ) ) )
{
errorMessage = QObject : : tr ( " Not enough memory to create output scalar field " ) ;
_outputSF - > release ( ) ;
return false ;
}
testCloud - > addScalarField ( _outputSF ) ;
outputSF = _outputSF ;
}
else
{
outputSF = testCloud - > getScalarField ( outSFIndex ) ;
}
2020-06-04 00:32:27 +02:00
outputSF - > fill ( CCCoreLib : : NAN_VALUE ) ;
2019-03-26 14:28:27 +01:00
outputSF - > computeMinAndMax ( ) ;
}
unsigned testSampleCount = ( testSubset ? testSubset - > size ( ) : testCloud - > size ( ) ) ;
int attributesPerSample = static_cast < int > ( featureSources . size ( ) ) ;
2018-10-26 12:02:23 +02:00
ccLog : : Print ( QObject : : tr ( " [3DMASC] Testing data: %1 samples with %2 feature(s) " ) . arg ( testSampleCount ) . arg ( attributesPerSample ) ) ;
//allocate the data matrix
cv : : Mat test_data ;
try
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
test_data . create ( static_cast < int > ( testSampleCount ) , attributesPerSample , CV_32FC1 ) ;
2018-10-26 12:02:23 +02:00
}
catch ( const cv : : Exception & cvex )
{
errorMessage = cvex . msg . c_str ( ) ;
2018-10-26 10:41:22 +02:00
return false ;
}
2018-11-04 23:31:48 +01:00
QScopedPointer < ccProgressDialog > pDlg ;
if ( parentWidget )
{
pDlg . reset ( new ccProgressDialog ( parentWidget ) ) ;
pDlg - > setLabelText ( QString ( " Evaluating the classifier on %1 points " ) . arg ( testSampleCount ) ) ;
pDlg - > show ( ) ;
QCoreApplication : : processEvents ( ) ;
}
2020-06-04 00:32:27 +02:00
CCCoreLib : : NormalizedProgress nProgress ( pDlg . data ( ) , testSampleCount ) ;
2018-11-04 23:31:48 +01:00
2018-10-26 12:02:23 +02:00
//fill the data matrix
for ( int fIndex = 0 ; fIndex < attributesPerSample ; + + fIndex )
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
const Feature : : Source & fs = featureSources [ fIndex ] ;
IScalarFieldWrapper : : Shared source = GetSource ( fs , testCloud ) ;
2018-10-26 12:02:23 +02:00
if ( ! source | | ! source - > isValid ( ) )
2018-10-26 10:41:22 +02:00
{
2018-10-26 12:02:23 +02:00
assert ( false ) ;
2019-03-26 14:28:27 +01:00
errorMessage = QObject : : tr ( " Internal error: invalid source '%1' " ) . arg ( fs . name ) ;
2018-10-26 10:41:22 +02:00
return false ;
}
2019-03-26 14:28:27 +01:00
for ( unsigned i = 0 ; i < testSampleCount ; + + i )
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
unsigned pointIndex = ( testSubset ? testSubset - > getPointGlobalIndex ( i ) : i ) ;
2018-10-26 12:02:23 +02:00
double value = source - > pointValue ( pointIndex ) ;
test_data . at < float > ( i , fIndex ) = static_cast < float > ( value ) ;
}
}
2023-01-18 23:57:59 +01:00
2018-11-04 23:31:48 +01:00
//estimate the efficiency of the classifier
2023-01-18 23:57:59 +01:00
std : : vector < ScalarType > actualClass ( testSampleCount ) ;
std : : vector < ScalarType > predictectedClass ( testSampleCount ) ;
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
metrics . sampleCount = testSampleCount ;
2018-10-26 12:02:23 +02:00
metrics . goodGuess = 0 ;
2019-03-26 14:28:27 +01:00
for ( unsigned i = 0 ; i < testSampleCount ; + + i )
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
unsigned pointIndex = ( testSubset ? testSubset - > getPointGlobalIndex ( i ) : i ) ;
2018-10-26 12:02:23 +02:00
ScalarType pointClass = classifSF - > getValue ( pointIndex ) ;
int iClass = static_cast < int > ( pointClass ) ;
//if (iClass < 0 || iClass > 255)
//{
// errorMessage = QObject::tr("Classification values out of range (0-255)");
// return false;
//}
2019-03-29 13:40:29 +01:00
float fPredictedClass = m_rtrees - > predict ( test_data . row ( i ) , cv : : noArray ( ) , cv : : ml : : DTrees : : PREDICT_MAX_VOTE ) ;
int iPredictedClass = static_cast < int > ( fPredictedClass ) ;
2023-01-18 23:57:59 +01:00
actualClass . at ( i ) = iClass ;
predictectedClass . at ( i ) = iPredictedClass ;
2019-03-26 14:28:27 +01:00
if ( iPredictedClass = = iClass )
2018-10-26 10:41:22 +02:00
{
2018-10-26 12:02:23 +02:00
+ + metrics . goodGuess ;
2018-10-26 10:41:22 +02:00
}
2019-03-26 14:28:27 +01:00
if ( outputSF )
{
outputSF - > setValue ( pointIndex , static_cast < ScalarType > ( iPredictedClass ) ) ;
}
2018-11-04 23:31:48 +01:00
if ( pDlg & & ! nProgress . oneStep ( ) )
{
//process cancelled by the user
return false ;
}
2018-10-26 10:41:22 +02:00
}
2018-10-26 12:02:23 +02:00
2019-03-26 14:28:27 +01:00
if ( outputSF )
outputSF - > computeMinAndMax ( ) ;
2018-10-26 12:02:23 +02:00
metrics . ratio = static_cast < float > ( metrics . goodGuess ) / metrics . sampleCount ;
}
2023-01-18 23:57:59 +01:00
std : : unique_ptr < ConfusionMatrix > confusionMatrix ( new ConfusionMatrix ( actualClass , predictectedClass ) ) ;
train3DMASCDialog . deleteLaterConfusionMatrix ( confusionMatrix ) ;
2023-01-17 16:43:57 +01:00
2018-10-26 12:02:23 +02:00
return true ;
}
2018-11-04 11:56:13 +01:00
bool Classifier : : train ( const ccPointCloud * cloud ,
const RandomTreesParams & params ,
2019-03-26 14:28:27 +01:00
const Feature : : Source : : Set & featureSources ,
2018-11-04 11:56:13 +01:00
QString & errorMessage ,
2020-06-04 00:32:27 +02:00
CCCoreLib : : ReferenceCloud * trainSubset /*=nullptr*/ ,
2019-01-20 22:12:20 +01:00
ccMainAppInterface * app /*=nullptr*/ ,
2018-11-04 11:56:13 +01:00
QWidget * parentWidget /*=nullptr*/ )
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
if ( featureSources . empty ( ) )
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
errorMessage = QObject : : tr ( " Training method called without any feature (source)?! " ) ;
2018-10-26 12:02:23 +02:00
return false ;
2018-10-26 10:41:22 +02:00
}
2018-11-04 11:21:50 +01:00
if ( ! cloud )
2018-10-26 12:02:23 +02:00
{
2018-11-04 11:21:50 +01:00
errorMessage = QObject : : tr ( " Invalid input cloud " ) ;
2018-10-26 12:02:23 +02:00
return false ;
}
if ( trainSubset & & trainSubset - > getAssociatedCloud ( ) ! = cloud )
{
errorMessage = QObject : : tr ( " Invalid train subset (associated point cloud is different) " ) ;
return false ;
}
//look for the classification field
2020-06-04 00:32:27 +02:00
CCCoreLib : : ScalarField * classifSF = Tools : : GetClassificationSF ( cloud ) ;
2018-10-26 12:02:23 +02:00
if ( ! classifSF | | classifSF - > size ( ) < cloud - > size ( ) )
{
assert ( false ) ;
2019-03-25 18:45:28 +01:00
errorMessage = QObject : : tr ( " Missing/invalid 'Classification' field on input cloud " ) ;
2018-10-26 12:02:23 +02:00
return false ;
}
int sampleCount = static_cast < int > ( trainSubset ? trainSubset - > size ( ) : cloud - > size ( ) ) ;
2019-03-26 14:28:27 +01:00
int attributesPerSample = static_cast < int > ( featureSources . size ( ) ) ;
2018-10-26 12:02:23 +02:00
2019-01-20 22:12:20 +01:00
if ( app )
{
app - > dispToConsole ( QString ( " [3DMASC] Training data: %1 samples with %2 feature(s) " ) . arg ( sampleCount ) . arg ( attributesPerSample ) ) ;
}
2018-10-26 10:41:22 +02:00
cv : : Mat training_data , train_labels ;
try
{
training_data . create ( sampleCount , attributesPerSample , CV_32FC1 ) ;
train_labels . create ( sampleCount , 1 , CV_32FC1 ) ;
}
catch ( const cv : : Exception & cvex )
{
errorMessage = cvex . msg . c_str ( ) ;
return false ;
}
//fill the classification labels vector
{
2018-10-26 12:02:23 +02:00
for ( int i = 0 ; i < sampleCount ; + + i )
2018-10-26 10:41:22 +02:00
{
2018-10-26 12:02:23 +02:00
int pointIndex = ( trainSubset ? static_cast < int > ( trainSubset - > getPointGlobalIndex ( i ) ) : i ) ;
ScalarType pointClass = classifSF - > getValue ( pointIndex ) ;
2018-10-26 10:41:22 +02:00
int iClass = static_cast < int > ( pointClass ) ;
2018-10-26 12:02:23 +02:00
//if (iClass < 0 || iClass > 255)
//{
// errorMessage = QObject::tr("Classification values out of range (0-255)");
// return false;
//}
2018-10-26 10:41:22 +02:00
2018-10-26 12:02:23 +02:00
train_labels . at < float > ( i ) = static_cast < unsigned char > ( iClass ) ;
2018-10-26 10:41:22 +02:00
}
}
//fill the training data matrix
for ( int fIndex = 0 ; fIndex < attributesPerSample ; + + fIndex )
{
2019-03-26 14:28:27 +01:00
const Feature : : Source & fs = featureSources [ fIndex ] ;
2018-10-26 10:41:22 +02:00
2019-03-26 14:28:27 +01:00
IScalarFieldWrapper : : Shared source = GetSource ( fs , cloud ) ;
2018-10-26 10:41:22 +02:00
if ( ! source | | ! source - > isValid ( ) )
{
assert ( false ) ;
2019-03-26 14:28:27 +01:00
errorMessage = QObject : : tr ( " Internal error: invalid source '%1' " ) . arg ( fs . name ) ;
2018-10-26 10:41:22 +02:00
return false ;
}
2018-10-26 12:02:23 +02:00
for ( int i = 0 ; i < sampleCount ; + + i )
2018-10-26 10:41:22 +02:00
{
2018-10-26 12:02:23 +02:00
int pointIndex = ( trainSubset ? static_cast < int > ( trainSubset - > getPointGlobalIndex ( i ) ) : i ) ;
double value = source - > pointValue ( pointIndex ) ;
training_data . at < float > ( i , fIndex ) = static_cast < float > ( value ) ;
2018-10-26 10:41:22 +02:00
}
}
2018-11-04 11:56:13 +01:00
QScopedPointer < QProgressDialog > pDlg ;
if ( parentWidget )
{
pDlg . reset ( new QProgressDialog ( parentWidget ) ) ;
pDlg - > setRange ( 0 , 0 ) ; //infinite loop
pDlg - > setLabelText ( " Training classifier " ) ;
pDlg - > show ( ) ;
2018-11-04 23:31:48 +01:00
QCoreApplication : : processEvents ( ) ;
2018-11-04 11:56:13 +01:00
}
2018-10-26 10:41:22 +02:00
m_rtrees = cv : : ml : : RTrees : : create ( ) ;
2018-10-26 12:02:23 +02:00
m_rtrees - > setMaxDepth ( params . maxDepth ) ;
m_rtrees - > setMinSampleCount ( params . minSampleCount ) ;
2019-03-29 13:40:29 +01:00
m_rtrees - > setRegressionAccuracy ( 0 ) ;
2022-02-24 16:44:59 +01:00
// If true then surrogate splits will be built. These splits allow to work with missing data and compute variable importance correctly. Default value is false.
2019-03-29 13:40:29 +01:00
m_rtrees - > setUseSurrogates ( false ) ;
m_rtrees - > setPriors ( cv : : Mat ( ) ) ;
//m_rtrees->setMaxCategories(params.maxCategories); //not important?
2019-01-20 13:57:27 +01:00
m_rtrees - > setCalculateVarImportance ( true ) ;
2018-10-26 12:02:23 +02:00
m_rtrees - > setActiveVarCount ( params . activeVarCount ) ;
2022-10-20 08:52:55 +02:00
cv : : TermCriteria terminationCriteria ( cv : : TermCriteria : : MAX_ITER , params . maxTreeCount , std : : numeric_limits < double > : : epsilon ( ) ) ;
2018-10-26 10:41:22 +02:00
m_rtrees - > setTermCriteria ( terminationCriteria ) ;
2018-11-04 11:56:13 +01:00
2022-10-20 08:52:55 +02:00
ccLog : : Warning ( " [Classifier::train] cv::getNumThreads " + QString : : number ( cv : : getNumThreads ( ) ) ) ;
2022-02-22 07:53:46 +01:00
2018-11-04 11:56:13 +01:00
QFuture < bool > future = QtConcurrent : : run ( [ & ] ( )
2018-10-26 10:41:22 +02:00
{
2018-11-04 11:56:13 +01:00
// Code in this block will run in another thread
try
{
2022-10-20 08:52:55 +02:00
ccLog : : Warning ( " [QFuture] cv::getNumThreads " + QString : : number ( cv : : getNumThreads ( ) ) ) ;
2019-03-29 13:40:29 +01:00
cv : : Mat sampleIndexes = cv : : Mat : : zeros ( 1 , training_data . rows , CV_8U ) ;
2022-12-08 09:19:52 +01:00
// cv::Mat trainSamples = sampleIndexes.colRange(0, sampleCount);
// trainSamples.setTo(cv::Scalar::all(1));
2019-03-29 13:40:29 +01:00
cv : : Mat varTypes ( training_data . cols + 1 , 1 , CV_8U ) ;
varTypes . setTo ( cv : : Scalar : : all ( cv : : ml : : VAR_ORDERED ) ) ;
varTypes . at < uchar > ( training_data . cols ) = cv : : ml : : VAR_CATEGORICAL ;
2022-12-08 09:19:52 +01:00
cv : : Ptr < cv : : ml : : TrainData > trainData = cv : : ml : : TrainData : : create ( training_data , cv : : ml : : ROW_SAMPLE , train_labels , /* samples layout responses */
cv : : noArray ( ) , sampleIndexes , /* varIdx sampleIdx */
cv : : noArray ( ) , varTypes ) ; // sampleWeights varType
2019-03-29 13:40:29 +01:00
bool success = m_rtrees - > train ( trainData ) ;
if ( ! success | | ! m_rtrees - > isClassifier ( ) )
{
errorMessage = " Training failed " ;
return false ;
}
2018-11-04 11:56:13 +01:00
}
catch ( const cv : : Exception & cvex )
{
m_rtrees . release ( ) ;
errorMessage = cvex . msg . c_str ( ) ;
return false ;
}
catch ( const std : : exception & stdex )
{
errorMessage = stdex . what ( ) ;
return false ;
}
catch ( . . . )
{
errorMessage = QObject : : tr ( " Unknown error " ) ;
return false ;
}
2022-12-08 09:19:52 +01:00
2018-11-04 11:56:13 +01:00
return true ;
} ) ;
while ( ! future . isFinished ( ) )
2018-10-26 10:41:22 +02:00
{
2018-11-04 11:56:13 +01:00
# if defined(CC_WINDOWS)
: : Sleep ( 500 ) ;
# else
usleep ( 500 * 1000 ) ;
# endif
if ( pDlg )
{
if ( pDlg - > wasCanceled ( ) )
{
future . cancel ( ) ;
break ;
}
pDlg - > setValue ( pDlg - > value ( ) + 1 ) ;
}
QCoreApplication : : processEvents ( ) ;
2018-10-26 10:41:22 +02:00
}
2018-11-04 11:56:13 +01:00
if ( pDlg )
2018-10-26 10:41:22 +02:00
{
2018-11-04 11:56:13 +01:00
pDlg - > close ( ) ;
QCoreApplication : : processEvents ( ) ;
2018-10-26 10:41:22 +02:00
}
2018-11-04 11:56:13 +01:00
if ( future . isCanceled ( ) | | ! future . result ( ) | | ! m_rtrees - > isTrained ( ) )
2018-10-26 10:41:22 +02:00
{
errorMessage = QObject : : tr ( " Training failed for an unknown reason... " ) ;
m_rtrees . release ( ) ;
return false ;
}
return true ;
}
bool Classifier : : toFile ( QString filename , QWidget * parentWidget /*=nullptr*/ ) const
{
if ( ! m_rtrees )
{
ccLog : : Warning ( QObject : : tr ( " Classifier hasn't been trained, can't save it " ) ) ;
return false ;
}
//save the classifier
QProgressDialog pDlg ( parentWidget ) ;
pDlg . setRange ( 0 , 0 ) ; //infinite loop
pDlg . setLabelText ( QObject : : tr ( " Saving classifier " ) ) ;
pDlg . show ( ) ;
QCoreApplication : : processEvents ( ) ;
2018-12-01 14:39:11 +01:00
cv : : String cvFilename = filename . toStdString ( ) ;
m_rtrees - > save ( cvFilename ) ;
2018-10-26 10:41:22 +02:00
pDlg . close ( ) ;
QCoreApplication : : processEvents ( ) ;
2018-12-01 14:39:11 +01:00
ccLog : : Print ( " Classifier file saved to: " + QString : : fromStdString ( cvFilename ) ) ;
2018-10-26 10:41:22 +02:00
return true ;
}
bool Classifier : : fromFile ( QString filename , QWidget * parentWidget /*=nullptr*/ )
{
//load the classifier
2018-11-05 00:11:27 +01:00
QScopedPointer < QProgressDialog > pDlg ;
if ( parentWidget )
{
pDlg . reset ( new QProgressDialog ( parentWidget ) ) ;
pDlg - > setRange ( 0 , 0 ) ; //infinite loop
pDlg - > setLabelText ( QObject : : tr ( " Loading classifier " ) ) ;
pDlg - > show ( ) ;
QCoreApplication : : processEvents ( ) ;
}
try
{
m_rtrees = cv : : ml : : RTrees : : load ( filename . toStdString ( ) ) ;
}
catch ( const cv : : Exception & cvex )
{
ccLog : : Warning ( cvex . msg . c_str ( ) ) ;
ccLog : : Error ( " Failed to load file: " + filename ) ;
return false ;
}
2018-10-26 10:41:22 +02:00
2018-11-05 00:11:27 +01:00
if ( pDlg )
{
pDlg - > close ( ) ;
QCoreApplication : : processEvents ( ) ;
}
2018-10-26 10:41:22 +02:00
2019-03-29 13:40:29 +01:00
if ( m_rtrees - > empty ( ) | | ! m_rtrees - > isClassifier ( ) )
{
ccLog : : Error ( QObject : : tr ( " Loaded classifier is invalid " ) ) ;
return false ;
}
else if ( ! m_rtrees - > isTrained ( ) )
2018-10-26 10:41:22 +02:00
{
ccLog : : Warning ( QObject : : tr ( " Loaded classifier doesn't seem to be trained " ) ) ;
}
return true ;
}