otbTrainVectorClassifier.cxx 21 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
/*=========================================================================
 Program:   ORFEO Toolbox
 Language:  C++
 Date:      $Date$
 Version:   $Revision$


 Copyright (c) Centre National d'Etudes Spatiales. All rights reserved.
 See OTBCopyright.txt for details.


 This software is distributed WITHOUT ANY WARRANTY; without even
 the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 PURPOSE.  See the above copyright notices for more information.

 =========================================================================*/
#include "otbWrapperApplication.h"
#include "otbWrapperApplicationFactory.h"

#include "otbLearningApplicationBase.h"

#include "otbOGRDataSourceWrapper.h"
#include "otbOGRFeatureWrapper.h"
#include "otbStatisticsXMLFileWriter.h"

#include "itkVariableLengthVector.h"
#include "otbStatisticsXMLFileReader.h"

#include "itkListSample.h"
#include "otbShiftScaleSampleListFilter.h"

// Validation
#include "otbConfusionMatrixCalculator.h"

35 36 37
#include <algorithm>
#include <locale>

38 39 40 41
namespace otb
{
namespace Wrapper
{
42 43 44 45 46 47 48

/** Utility function to negate std::isalnum */
bool IsNotAlphaNum(char c)
  {
  return !std::isalnum(c);
  }

49 50 51 52
class TrainVectorClassifier : public LearningApplicationBase<float,int>
{
public:
  typedef TrainVectorClassifier Self;
53
  typedef LearningApplicationBase<float, int> Superclass;
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
  typedef itk::SmartPointer<Self> Pointer;
  typedef itk::SmartPointer<const Self> ConstPointer;
  itkNewMacro(Self)

  itkTypeMacro(Self, Superclass)

  typedef Superclass::SampleType              SampleType;
  typedef Superclass::ListSampleType          ListSampleType;
  typedef Superclass::TargetListSampleType    TargetListSampleType;
  typedef Superclass::SampleImageType         SampleImageType;
  
  typedef double ValueType;
  typedef itk::VariableLengthVector<ValueType> MeasurementType;

  typedef otb::StatisticsXMLFileReader<SampleType> StatisticsReader;

  typedef otb::Statistics::ShiftScaleSampleListFilter<ListSampleType, ListSampleType> ShiftScaleFilterType;

  // Estimate performance on validation sample
  typedef otb::ConfusionMatrixCalculator<TargetListSampleType, TargetListSampleType> ConfusionMatrixCalculatorType;
  typedef ConfusionMatrixCalculatorType::ConfusionMatrixType ConfusionMatrixType;
  typedef ConfusionMatrixCalculatorType::MapOfIndicesType MapOfIndicesType;
  typedef ConfusionMatrixCalculatorType::ClassLabelType ClassLabelType;

private:
  void DoInit()
  {
81 82 83 84 85 86 87 88 89
    SetName("TrainVectorClassifier");
    SetDescription("Train a classifier based on labeled geometries and a list of features to consider.");

    SetDocName("Train Vector Classifier");
    SetDocLongDescription("This application trains a classifier based on "
      "labeled geometries and a list of features to consider for classification.");
    SetDocLimitations(" ");
    SetDocAuthors("OTB Team");
    SetDocSeeAlso(" ");
90 91 92 93 94
   
    //Group IO
    AddParameter(ParameterType_Group, "io", "Input and output data");
    SetParameterDescription("io", "This group of parameters allows setting input and output data.");

95
    AddParameter(ParameterType_InputVectorDataList, "io.vd", "Input Vector Data");
96
    SetParameterDescription("io.vd", "Input geometries used for training (note : all geometries from the layer will be used)");
97 98 99 100 101 102 103 104 105 106 107 108

    AddParameter(ParameterType_InputFilename, "io.stats", "Input XML image statistics file");
    MandatoryOff("io.stats");
    SetParameterDescription("io.stats", "XML file containing mean and variance of each feature.");

    AddParameter(ParameterType_OutputFilename, "io.confmatout", "Output confusion matrix");
    SetParameterDescription("io.confmatout", "Output file containing the confusion matrix (.csv format).");
    MandatoryOff("io.confmatout");

    AddParameter(ParameterType_OutputFilename, "io.out", "Output model");
    SetParameterDescription("io.out", "Output file containing the model estimated (.txt format).");

109 110
    AddParameter(ParameterType_ListView,  "feat", "Field names for training features.");
    SetParameterDescription("feat","List of field names in the input vector data to be used as features for training.");
111

112
    AddParameter(ParameterType_ListView,"cfield","Field containing the class id for supervision");
113 114
    SetParameterDescription("cfield","Field containing the class id for supervision. "
      "Only geometries with this field available will be taken into account.");
115 116
    SetListViewSingleSelectionMode("cfield",true);
      
117
    AddParameter(ParameterType_Int, "layer", "Layer Index");
118
    SetParameterDescription("layer", "Index of the layer to use in the input vector file.");
119 120
    MandatoryOff("layer");
    SetDefaultParameterInt("layer",0);
121

122 123 124
    AddParameter(ParameterType_Group, "valid", "Validation data");
    SetParameterDescription("valid", "This group of parameters defines validation data.");

125
    AddParameter(ParameterType_InputVectorDataList, "valid.vd", "Validation Vector Data");
126 127 128 129 130
    SetParameterDescription("valid.vd", "Geometries used for validation "
      "(must contain the same fields used for training, all geometries from the layer will be used)");
    MandatoryOff("valid.vd");

    AddParameter(ParameterType_Int, "valid.layer", "Layer Index");
131
    SetParameterDescription("valid.layer", "Index of the layer to use in the validation vector file.");
132 133 134 135 136 137 138
    MandatoryOff("valid.layer");
    SetDefaultParameterInt("valid.layer",0);

    // Add parameters for the classifier choice
    Superclass::DoInit();

    AddRANDParameter();
139 140 141 142
    // Doc example parameter settings
    SetDocExampleParameterValue("io.vd", "vectorData.shp");
    SetDocExampleParameterValue("io.stats", "meanVar.xml");
    SetDocExampleParameterValue("io.out", "svmModel.svm");
143
    SetDocExampleParameterValue("feat", "perimeter  area  width");
144 145 146 147 148 149 150
    SetDocExampleParameterValue("cfield", "predicted");
  }

  void DoUpdateParameters()
  {
    if ( HasValue("io.vd") )
      {
151
      std::vector<std::string> vectorFileList = GetParameterStringList("io.vd");
152
      ogr::DataSource::Pointer ogrDS =
153
        ogr::DataSource::New(vectorFileList[0], ogr::DataSource::Modes::Read);
154 155 156 157
      ogr::Layer layer = ogrDS->GetLayer(this->GetParameterInt("layer"));
      ogr::Feature feature = layer.ogr().GetNextFeature();

      ClearChoices("feat");
158 159
      ClearChoices("cfield");
      
160 161 162 163
      for(int iField=0; iField<feature.ogr().GetFieldCount(); iField++)
        {
        std::string key, item = feature.ogr().GetFieldDefnRef(iField)->GetNameRef();
        key = item;
164 165
        std::string::iterator end = std::remove_if(key.begin(),key.end(),IsNotAlphaNum);
        std::transform(key.begin(), end, key.begin(), tolower);
166 167 168
        
        OGRFieldType fieldType = feature.ogr().GetFieldDefnRef(iField)->GetType();
        
169
        if(fieldType == OFTInteger ||  ogr::version_proxy::IsOFTInteger64(fieldType) || fieldType == OFTReal)
170 171 172 173
          {
          std::string tmpKey="feat."+key.substr(0, end - key.begin());
          AddChoice(tmpKey,item);
          }
174
        if(fieldType == OFTInteger || ogr::version_proxy::IsOFTInteger64(fieldType))
175 176 177 178
          {
          std::string tmpKey="cfield."+key.substr(0, end - key.begin());
          AddChoice(tmpKey,item);
          }
179
        }
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
      }
  }


void LogConfusionMatrix(ConfusionMatrixCalculatorType* confMatCalc)
{
  ConfusionMatrixCalculatorType::ConfusionMatrixType matrix = confMatCalc->GetConfusionMatrix();

  // Compute minimal width
  size_t minwidth = 0;

  for (unsigned int i = 0; i < matrix.Rows(); i++)
    {
    for (unsigned int j = 0; j < matrix.Cols(); j++)
      {
      std::ostringstream os;
      os << matrix(i, j);
      size_t size = os.str().size();

      if (size > minwidth)
        {
        minwidth = size;
        }
      }
    }

  MapOfIndicesType mapOfIndices = confMatCalc->GetMapOfIndices();

  MapOfIndicesType::const_iterator it = mapOfIndices.begin();
  MapOfIndicesType::const_iterator end = mapOfIndices.end();

  for (; it != end; ++it)
    {
    std::ostringstream os;
    os << "[" << it->second << "]";

    size_t size = os.str().size();
    if (size > minwidth)
      {
      minwidth = size;
      }
    }

  // Generate matrix string, with 'minwidth' as size specifier
  std::ostringstream os;

  // Header line
  for (size_t i = 0; i < minwidth; ++i)
    os << " ";
  os << " ";

  it = mapOfIndices.begin();
  end = mapOfIndices.end();
  for (; it != end; ++it)
    {
    os << "[" << it->second << "]" << " ";
    }

  os << std::endl;

  // Each line of confusion matrix
  for (unsigned int i = 0; i < matrix.Rows(); i++)
    {
    ConfusionMatrixCalculatorType::ClassLabelType label = mapOfIndices[i];
    os << "[" << std::setw(minwidth - 2) << label << "]" << " ";
    for (unsigned int j = 0; j < matrix.Cols(); j++)
      {
      os << std::setw(minwidth) << matrix(i, j) << " ";
      }
    os << std::endl;
    }

  otbAppLogINFO("Confusion matrix (rows = reference labels, columns = produced labels):\n" << os.str());
}


256
void DoExecute()
257
  {
258 259 260 261
  typedef int LabelPixelType;
  typedef itk::FixedArray<LabelPixelType,1> LabelSampleType;
  typedef itk::Statistics::ListSample <LabelSampleType> LabelListSampleType;

262 263
  // Prepare selected field names (their position may change between two inputs)
  std::vector<int> selectedIdx = GetSelectedItems("feat");
264 265 266 267
  std::vector<int> selectedCFieldIdx = GetSelectedItems("cfield");
  
  if(selectedIdx.empty())
    {
268
    otbAppLogFATAL(<<"No features have been selected to train the classifier on!");
269 270 271 272
    }
  
  if(selectedCFieldIdx.empty())
    {
273
    otbAppLogFATAL(<<"No field has been selected for data labelling!");
274 275
    }

276 277
  const unsigned int nbFeatures = selectedIdx.size();
  std::vector<std::string> fieldNames = GetChoiceNames("feat");
278
  std::vector<std::string> cFieldNames = GetChoiceNames("cfield");
279 280 281 282 283
  std::vector<std::string> selectedNames(nbFeatures);
  for (unsigned int i=0 ; i<nbFeatures ; i++)
    {
    selectedNames[i] = fieldNames[selectedIdx[i]];
    }
284 285 286

  std::string selectedCFieldName = cFieldNames[selectedCFieldIdx.front()];

287 288 289 290 291 292 293 294 295 296 297
  std::vector<int> featureFieldIndex(nbFeatures, -1);
  int cFieldIndex = -1;

  // List of available fields
  std::ostringstream oss;
  for (unsigned int i=0 ; i<fieldNames.size() ; i++)
    {
    if (i) oss << ", ";
    oss << fieldNames[i];
    }
  std::string availableFields(oss.str());
298 299 300 301 302 303

  // Statistics for shift/scale
  MeasurementType meanMeasurementVector;
  MeasurementType stddevMeasurementVector;
  if (HasValue("io.stats") && IsParameterEnabled("io.stats"))
    {
304
    StatisticsReader::Pointer statisticsReader = StatisticsReader::New();
305
    std::string XMLfile = GetParameterString("io.stats");
306
    statisticsReader->SetFileName(XMLfile);
307 308 309 310 311 312 313 314 315 316
    meanMeasurementVector = statisticsReader->GetStatisticVectorByName("mean");
    stddevMeasurementVector = statisticsReader->GetStatisticVectorByName("stddev");
    }
  else
    {
    meanMeasurementVector.SetSize(nbFeatures);
    meanMeasurementVector.Fill(0.);
    stddevMeasurementVector.SetSize(nbFeatures);
    stddevMeasurementVector.Fill(1.);
    }
317

318 319 320
  ListSampleType::Pointer input = ListSampleType::New();
  LabelListSampleType::Pointer target = LabelListSampleType::New();
  input->SetMeasurementVectorSize(nbFeatures);
321

322 323
  std::vector<std::string> vectorFileList = GetParameterStringList("io.vd");
  for (unsigned int k=0 ; k<vectorFileList.size() ; k++)
324
    {
325 326 327 328 329 330 331 332 333 334 335
    otbAppLogINFO("Reading input vector file "<<k+1<<"/"<<vectorFileList.size());
    ogr::DataSource::Pointer source = ogr::DataSource::New(vectorFileList[k], ogr::DataSource::Modes::Read);
    ogr::Layer layer = source->GetLayer(this->GetParameterInt("layer"));
    ogr::Feature feature = layer.ogr().GetNextFeature();
    bool goesOn = feature.addr() != 0;
    if (!goesOn)
      {
      otbAppLogWARNING("The layer "<<GetParameterInt("layer")<<" of "
        <<vectorFileList[k]<<" is empty, input is skipped.");
      continue;
      }
336

337 338
    // Check all needed fields are present :
    //   - check class field
339
    cFieldIndex = feature.ogr().GetFieldIndex(selectedCFieldName.c_str());
340
    if (cFieldIndex < 0)
341
      otbAppLogFATAL("The field name for class label ("<<selectedCFieldName
342 343 344
        <<") has not been found in the input vector file! Choices are "<< availableFields);
    //   - check feature fields
    for (unsigned int i=0 ; i<nbFeatures ; i++)
345
      {
346 347 348 349
      featureFieldIndex[i] = feature.ogr().GetFieldIndex(selectedNames[i].c_str());
      if (featureFieldIndex[i] < 0)
        otbAppLogFATAL("The field name for feature "<<selectedNames[i]
        <<" has not been found in the input vector file! Choices are "<< availableFields);
350 351
      }

352
    while(goesOn)
353
      {
354 355 356 357 358 359
      if(feature.ogr().IsFieldSet(cFieldIndex))
        {
        MeasurementType mv;
        mv.SetSize(nbFeatures);
        for(unsigned int idx=0; idx < nbFeatures; ++idx)
          mv[idx] = feature.ogr().GetFieldAsDouble(featureFieldIndex[idx]);
360

361 362 363 364 365
        input->PushBack(mv);
        target->PushBack(feature.ogr().GetFieldAsInteger(cFieldIndex));
        }
      feature = layer.ogr().GetNextFeature();
      goesOn = feature.addr() != 0;
366 367
      }
    }
368

369 370 371 372 373
  ShiftScaleFilterType::Pointer trainingShiftScaleFilter = ShiftScaleFilterType::New();
  trainingShiftScaleFilter->SetInput(input);
  trainingShiftScaleFilter->SetShifts(meanMeasurementVector);
  trainingShiftScaleFilter->SetScales(stddevMeasurementVector);
  trainingShiftScaleFilter->Update();
374

375 376
  ListSampleType::Pointer trainingListSample= trainingShiftScaleFilter->GetOutput();
  TargetListSampleType::Pointer trainingLabeledListSample = target;
377 378 379 380 381 382 383 384 385 386 387

  //--------------------------
  // Estimate model
  //--------------------------
  this->Train(trainingListSample,trainingLabeledListSample,GetParameterString("io.out"));

  //--------------------------
  // Performances estimation
  //--------------------------
  ListSampleType::Pointer validationListSample=ListSampleType::New();
  TargetListSampleType::Pointer validationLabeledListSample = TargetListSampleType::New();
388 389

  // Import validation data
390
  if (HasValue("valid.vd") && IsParameterEnabled("valid.vd"))
391 392 393 394
    {
    input = ListSampleType::New();
    target = LabelListSampleType::New();
    input->SetMeasurementVectorSize(nbFeatures);
395 396 397

    std::vector<std::string> validFileList = this->GetParameterStringList("valid.vd");
    for (unsigned int k=0 ; k<validFileList.size() ; k++)
398
      {
399 400 401 402 403 404
      otbAppLogINFO("Reading validation vector file "<<k+1<<"/"<<validFileList.size());
      ogr::DataSource::Pointer source = ogr::DataSource::New(validFileList[k], ogr::DataSource::Modes::Read);
      ogr::Layer layer = source->GetLayer(this->GetParameterInt("valid.layer"));
      ogr::Feature feature = layer.ogr().GetNextFeature();
      bool goesOn = feature.addr() != 0;
      if (!goesOn)
405
        {
406 407 408 409
        otbAppLogWARNING("The layer "<<GetParameterInt("valid.layer")<<" of "
          <<validFileList[k]<<" is empty, input is skipped.");
        continue;
        }
410

411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
      // Check all needed fields are present :
      //   - check class field
      cFieldIndex = feature.ogr().GetFieldIndex(GetParameterString("cfield").c_str());
      if (cFieldIndex < 0)
        otbAppLogFATAL("The field name for class label ("<<GetParameterString("cfield")
          <<") has not been found in the input vector file! Choices are "<< availableFields);
      //   - check feature fields
      for (unsigned int i=0 ; i<nbFeatures ; i++)
        {
        featureFieldIndex[i] = feature.ogr().GetFieldIndex(selectedNames[i].c_str());
        if (featureFieldIndex[i] < 0)
          otbAppLogFATAL("The field name for feature "<<selectedNames[i]
          <<" has not been found in the input vector file! Choices are "<< availableFields);
        }

      while(goesOn)
        {
        if(feature.ogr().IsFieldSet(cFieldIndex))
          {
          MeasurementType mv;
          mv.SetSize(nbFeatures);
          for(unsigned int idx=0; idx < nbFeatures; ++idx)
            mv[idx] = feature.ogr().GetFieldAsDouble(featureFieldIndex[idx]);

          input->PushBack(mv);
          target->PushBack(feature.ogr().GetFieldAsInteger(cFieldIndex));
          }
        feature = layer.ogr().GetNextFeature();
        goesOn = feature.addr() != 0;
440 441 442 443 444 445 446 447 448 449 450 451
        }
      }

    ShiftScaleFilterType::Pointer validShiftScaleFilter = ShiftScaleFilterType::New();
    validShiftScaleFilter->SetInput(input);
    validShiftScaleFilter->SetShifts(meanMeasurementVector);
    validShiftScaleFilter->SetScales(stddevMeasurementVector);
    validShiftScaleFilter->Update();

    validationListSample = validShiftScaleFilter->GetOutput();
    validationLabeledListSample = target;
    }
452 453
 
  //Test the input validation set size
454 455 456
  TargetListSampleType::Pointer predictedList = TargetListSampleType::New();
  ListSampleType::Pointer performanceListSample;
  TargetListSampleType::Pointer performanceLabeledListSample;
457 458 459 460 461 462 463 464 465 466 467 468
  if(validationLabeledListSample->Size() != 0)
    {
    performanceListSample = validationListSample;
    performanceLabeledListSample = validationLabeledListSample;
    }
  else
    {
    otbAppLogWARNING("The validation set is empty. The performance estimation is done using the input training set in this case.");
    performanceListSample = trainingListSample;
    performanceLabeledListSample = trainingLabeledListSample;
    }

469
  this->Classify(performanceListSample, predictedList, GetParameterString("io.out"));
470

471
  ConfusionMatrixCalculatorType::Pointer confMatCalc = ConfusionMatrixCalculatorType::New();
472

473 474 475 476 477
  otbAppLogINFO("Predicted list size : " << predictedList->Size());
  otbAppLogINFO("ValidationLabeledListSample size : " << performanceLabeledListSample->Size());
  confMatCalc->SetReferenceLabels(performanceLabeledListSample);
  confMatCalc->SetProducedLabels(predictedList);
  confMatCalc->Compute();
478

479 480
  otbAppLogINFO("training performances");
  LogConfusionMatrix(confMatCalc);
481

482 483 484
  for (unsigned int itClasses = 0; itClasses < confMatCalc->GetNumberOfClasses(); itClasses++)
    {
    ConfusionMatrixCalculatorType::ClassLabelType classLabel = confMatCalc->GetMapOfIndices()[itClasses];
485

486 487 488 489 490 491
    otbAppLogINFO("Precision of class [" << classLabel << "] vs all: " << confMatCalc->GetPrecisions()[itClasses]);
    otbAppLogINFO("Recall of class    [" << classLabel << "] vs all: " << confMatCalc->GetRecalls()[itClasses]);
    otbAppLogINFO(
      "F-score of class   [" << classLabel << "] vs all: " << confMatCalc->GetFScores()[itClasses] << "\n");
    }
  otbAppLogINFO("Global performance, Kappa index: " << confMatCalc->GetKappaIndex());
492 493


494 495 496
  if (this->HasValue("io.confmatout"))
    {
    // Writing the confusion matrix in the output .CSV file
497

498 499
    MapOfIndicesType::iterator itMapOfIndicesValid, itMapOfIndicesPred;
    ClassLabelType labelValid = 0;
500

501 502
    ConfusionMatrixType confusionMatrix = confMatCalc->GetConfusionMatrix();
    MapOfIndicesType mapOfIndicesValid = confMatCalc->GetMapOfIndices();
503

504
    unsigned int nbClassesPred = mapOfIndicesValid.size();
505

506 507 508 509 510 511
    /////////////////////////////////////////////
    // Filling the 2 headers for the output file
    const std::string commentValidStr = "#Reference labels (rows):";
    const std::string commentPredStr = "#Produced labels (columns):";
    const char separatorChar = ',';
    std::ostringstream ossHeaderValidLabels, ossHeaderPredLabels;
512

513 514 515
    // Filling ossHeaderValidLabels and ossHeaderPredLabels for the output file
    ossHeaderValidLabels << commentValidStr;
    ossHeaderPredLabels << commentPredStr;
516

517
    itMapOfIndicesValid = mapOfIndicesValid.begin();
518

519 520 521 522
    while (itMapOfIndicesValid != mapOfIndicesValid.end())
      {
      // labels labelValid of mapOfIndicesValid are already sorted in otbConfusionMatrixCalculator
      labelValid = itMapOfIndicesValid->second;
523

524
      otbAppLogINFO("mapOfIndicesValid[" << itMapOfIndicesValid->first << "] = " << labelValid);
525

526 527
      ossHeaderValidLabels << labelValid;
      ossHeaderPredLabels << labelValid;
528

529
      ++itMapOfIndicesValid;
530

531 532 533 534
      if (itMapOfIndicesValid != mapOfIndicesValid.end())
        {
        ossHeaderValidLabels << separatorChar;
        ossHeaderPredLabels << separatorChar;
535
        }
536 537 538 539 540 541
      else
        {
        ossHeaderValidLabels << std::endl;
        ossHeaderPredLabels << std::endl;
        }
      }
542

543 544 545 546
    std::ofstream outFile;
    outFile.open(this->GetParameterString("io.confmatout").c_str());
    outFile << std::fixed;
    outFile.precision(10);
547

548 549 550 551 552
    /////////////////////////////////////
    // Writing the 2 headers
    outFile << ossHeaderValidLabels.str();
    outFile << ossHeaderPredLabels.str();
    /////////////////////////////////////
553

554
    unsigned int indexLabelValid = 0, indexLabelPred = 0;
555

556 557 558
    for (itMapOfIndicesValid = mapOfIndicesValid.begin(); itMapOfIndicesValid != mapOfIndicesValid.end(); ++itMapOfIndicesValid)
      {
      indexLabelPred = 0;
559

560 561 562 563 564
      for (itMapOfIndicesPred = mapOfIndicesValid.begin(); itMapOfIndicesPred != mapOfIndicesValid.end(); ++itMapOfIndicesPred)
        {
        // Writing the confusion matrix (sorted in otbConfusionMatrixCalculator) in the output file
        outFile << confusionMatrix(indexLabelValid, indexLabelPred);
        if (indexLabelPred < (nbClassesPred - 1))
565
          {
566
          outFile << separatorChar;
567
          }
568 569 570 571 572
        else
          {
          outFile << std::endl;
          }
        ++indexLabelPred;
573 574
        }

575 576
      ++indexLabelValid;
      }
577

578 579 580
    outFile.close();
    } // END if (this->HasValue("io.confmatout"))
  }
581 582 583 584 585 586

};
}
}

OTB_APPLICATION_EXPORT(otb::Wrapper::TrainVectorClassifier)