Home | History | Annotate | Download | only in traincascade
      1 #include "opencv2/core.hpp"
      2 
      3 #include "cascadeclassifier.h"
      4 #include <queue>
      5 
      6 using namespace std;
      7 using namespace cv;
      8 
      9 static const char* stageTypes[] = { CC_BOOST };
     10 static const char* featureTypes[] = { CC_HAAR, CC_LBP, CC_HOG };
     11 
     12 CvCascadeParams::CvCascadeParams() : stageType( defaultStageType ),
     13     featureType( defaultFeatureType ), winSize( cvSize(24, 24) )
     14 {
     15     name = CC_CASCADE_PARAMS;
     16 }
     17 CvCascadeParams::CvCascadeParams( int _stageType, int _featureType ) : stageType( _stageType ),
     18     featureType( _featureType ), winSize( cvSize(24, 24) )
     19 {
     20     name = CC_CASCADE_PARAMS;
     21 }
     22 
     23 //---------------------------- CascadeParams --------------------------------------
     24 
     25 void CvCascadeParams::write( FileStorage &fs ) const
     26 {
     27     string stageTypeStr = stageType == BOOST ? CC_BOOST : string();
     28     CV_Assert( !stageTypeStr.empty() );
     29     fs << CC_STAGE_TYPE << stageTypeStr;
     30     string featureTypeStr = featureType == CvFeatureParams::HAAR ? CC_HAAR :
     31                             featureType == CvFeatureParams::LBP ? CC_LBP :
     32                             featureType == CvFeatureParams::HOG ? CC_HOG :
     33                             0;
     34     CV_Assert( !stageTypeStr.empty() );
     35     fs << CC_FEATURE_TYPE << featureTypeStr;
     36     fs << CC_HEIGHT << winSize.height;
     37     fs << CC_WIDTH << winSize.width;
     38 }
     39 
     40 bool CvCascadeParams::read( const FileNode &node )
     41 {
     42     if ( node.empty() )
     43         return false;
     44     string stageTypeStr, featureTypeStr;
     45     FileNode rnode = node[CC_STAGE_TYPE];
     46     if ( !rnode.isString() )
     47         return false;
     48     rnode >> stageTypeStr;
     49     stageType = !stageTypeStr.compare( CC_BOOST ) ? BOOST : -1;
     50     if (stageType == -1)
     51         return false;
     52     rnode = node[CC_FEATURE_TYPE];
     53     if ( !rnode.isString() )
     54         return false;
     55     rnode >> featureTypeStr;
     56     featureType = !featureTypeStr.compare( CC_HAAR ) ? CvFeatureParams::HAAR :
     57                   !featureTypeStr.compare( CC_LBP ) ? CvFeatureParams::LBP :
     58                   !featureTypeStr.compare( CC_HOG ) ? CvFeatureParams::HOG :
     59                   -1;
     60     if (featureType == -1)
     61         return false;
     62     node[CC_HEIGHT] >> winSize.height;
     63     node[CC_WIDTH] >> winSize.width;
     64     return winSize.height > 0 && winSize.width > 0;
     65 }
     66 
     67 void CvCascadeParams::printDefaults() const
     68 {
     69     CvParams::printDefaults();
     70     cout << "  [-stageType <";
     71     for( int i = 0; i < (int)(sizeof(stageTypes)/sizeof(stageTypes[0])); i++ )
     72     {
     73         cout << (i ? " | " : "") << stageTypes[i];
     74         if ( i == defaultStageType )
     75             cout << "(default)";
     76     }
     77     cout << ">]" << endl;
     78 
     79     cout << "  [-featureType <{";
     80     for( int i = 0; i < (int)(sizeof(featureTypes)/sizeof(featureTypes[0])); i++ )
     81     {
     82         cout << (i ? ", " : "") << featureTypes[i];
     83         if ( i == defaultStageType )
     84             cout << "(default)";
     85     }
     86     cout << "}>]" << endl;
     87     cout << "  [-w <sampleWidth = " << winSize.width << ">]" << endl;
     88     cout << "  [-h <sampleHeight = " << winSize.height << ">]" << endl;
     89 }
     90 
     91 void CvCascadeParams::printAttrs() const
     92 {
     93     cout << "stageType: " << stageTypes[stageType] << endl;
     94     cout << "featureType: " << featureTypes[featureType] << endl;
     95     cout << "sampleWidth: " << winSize.width << endl;
     96     cout << "sampleHeight: " << winSize.height << endl;
     97 }
     98 
     99 bool CvCascadeParams::scanAttr( const string prmName, const string val )
    100 {
    101     bool res = true;
    102     if( !prmName.compare( "-stageType" ) )
    103     {
    104         for( int i = 0; i < (int)(sizeof(stageTypes)/sizeof(stageTypes[0])); i++ )
    105             if( !val.compare( stageTypes[i] ) )
    106                 stageType = i;
    107     }
    108     else if( !prmName.compare( "-featureType" ) )
    109     {
    110         for( int i = 0; i < (int)(sizeof(featureTypes)/sizeof(featureTypes[0])); i++ )
    111             if( !val.compare( featureTypes[i] ) )
    112                 featureType = i;
    113     }
    114     else if( !prmName.compare( "-w" ) )
    115     {
    116         winSize.width = atoi( val.c_str() );
    117     }
    118     else if( !prmName.compare( "-h" ) )
    119     {
    120         winSize.height = atoi( val.c_str() );
    121     }
    122     else
    123         res = false;
    124     return res;
    125 }
    126 
    127 //---------------------------- CascadeClassifier --------------------------------------
    128 
    129 bool CvCascadeClassifier::train( const string _cascadeDirName,
    130                                 const string _posFilename,
    131                                 const string _negFilename,
    132                                 int _numPos, int _numNeg,
    133                                 int _precalcValBufSize, int _precalcIdxBufSize,
    134                                 int _numStages,
    135                                 const CvCascadeParams& _cascadeParams,
    136                                 const CvFeatureParams& _featureParams,
    137                                 const CvCascadeBoostParams& _stageParams,
    138                                 bool baseFormatSave,
    139                                 double acceptanceRatioBreakValue )
    140 {
    141     // Start recording clock ticks for training time output
    142     const clock_t begin_time = clock();
    143 
    144     if( _cascadeDirName.empty() || _posFilename.empty() || _negFilename.empty() )
    145         CV_Error( CV_StsBadArg, "_cascadeDirName or _bgfileName or _vecFileName is NULL" );
    146 
    147     string dirName;
    148     if (_cascadeDirName.find_last_of("/\\") == (_cascadeDirName.length() - 1) )
    149         dirName = _cascadeDirName;
    150     else
    151         dirName = _cascadeDirName + '/';
    152 
    153     numPos = _numPos;
    154     numNeg = _numNeg;
    155     numStages = _numStages;
    156     if ( !imgReader.create( _posFilename, _negFilename, _cascadeParams.winSize ) )
    157     {
    158         cout << "Image reader can not be created from -vec " << _posFilename
    159                 << " and -bg " << _negFilename << "." << endl;
    160         return false;
    161     }
    162     if ( !load( dirName ) )
    163     {
    164         cascadeParams = _cascadeParams;
    165         featureParams = CvFeatureParams::create(cascadeParams.featureType);
    166         featureParams->init(_featureParams);
    167         stageParams = makePtr<CvCascadeBoostParams>();
    168         *stageParams = _stageParams;
    169         featureEvaluator = CvFeatureEvaluator::create(cascadeParams.featureType);
    170         featureEvaluator->init( featureParams, numPos + numNeg, cascadeParams.winSize );
    171         stageClassifiers.reserve( numStages );
    172     }else{
    173         // Make sure that if model parameters are preloaded, that people are aware of this,
    174         // even when passing other parameters to the training command
    175         cout << "---------------------------------------------------------------------------------" << endl;
    176         cout << "Training parameters are pre-loaded from the parameter file in data folder!" << endl;
    177         cout << "Please empty this folder if you want to use a NEW set of training parameters." << endl;
    178         cout << "---------------------------------------------------------------------------------" << endl;
    179     }
    180     cout << "PARAMETERS:" << endl;
    181     cout << "cascadeDirName: " << _cascadeDirName << endl;
    182     cout << "vecFileName: " << _posFilename << endl;
    183     cout << "bgFileName: " << _negFilename << endl;
    184     cout << "numPos: " << _numPos << endl;
    185     cout << "numNeg: " << _numNeg << endl;
    186     cout << "numStages: " << numStages << endl;
    187     cout << "precalcValBufSize[Mb] : " << _precalcValBufSize << endl;
    188     cout << "precalcIdxBufSize[Mb] : " << _precalcIdxBufSize << endl;
    189     cout << "acceptanceRatioBreakValue : " << acceptanceRatioBreakValue << endl;
    190     cascadeParams.printAttrs();
    191     stageParams->printAttrs();
    192     featureParams->printAttrs();
    193 
    194     int startNumStages = (int)stageClassifiers.size();
    195     if ( startNumStages > 1 )
    196         cout << endl << "Stages 0-" << startNumStages-1 << " are loaded" << endl;
    197     else if ( startNumStages == 1)
    198         cout << endl << "Stage 0 is loaded" << endl;
    199 
    200     double requiredLeafFARate = pow( (double) stageParams->maxFalseAlarm, (double) numStages ) /
    201                                 (double)stageParams->max_depth;
    202     double tempLeafFARate;
    203 
    204     for( int i = startNumStages; i < numStages; i++ )
    205     {
    206         cout << endl << "===== TRAINING " << i << "-stage =====" << endl;
    207         cout << "<BEGIN" << endl;
    208 
    209         if ( !updateTrainingSet( tempLeafFARate ) )
    210         {
    211             cout << "Train dataset for temp stage can not be filled. "
    212                     "Branch training terminated." << endl;
    213             break;
    214         }
    215         if( tempLeafFARate <= requiredLeafFARate )
    216         {
    217             cout << "Required leaf false alarm rate achieved. "
    218                     "Branch training terminated." << endl;
    219             break;
    220         }
    221         if( (tempLeafFARate <= acceptanceRatioBreakValue) && (acceptanceRatioBreakValue >= 0) ){
    222             cout << "The required acceptanceRatio for the model has been reached to avoid overfitting of trainingdata. "
    223                     "Branch training terminated." << endl;
    224             break;
    225         }
    226 
    227         Ptr<CvCascadeBoost> tempStage = makePtr<CvCascadeBoost>();
    228         bool isStageTrained = tempStage->train( featureEvaluator,
    229                                                 curNumSamples, _precalcValBufSize, _precalcIdxBufSize,
    230                                                 *stageParams );
    231         cout << "END>" << endl;
    232 
    233         if(!isStageTrained)
    234             break;
    235 
    236         stageClassifiers.push_back( tempStage );
    237 
    238         // save params
    239         if( i == 0)
    240         {
    241             std::string paramsFilename = dirName + CC_PARAMS_FILENAME;
    242             FileStorage fs( paramsFilename, FileStorage::WRITE);
    243             if ( !fs.isOpened() )
    244             {
    245                 cout << "Parameters can not be written, because file " << paramsFilename
    246                         << " can not be opened." << endl;
    247                 return false;
    248             }
    249             fs << FileStorage::getDefaultObjectName(paramsFilename) << "{";
    250             writeParams( fs );
    251             fs << "}";
    252         }
    253         // save current stage
    254         char buf[10];
    255         sprintf(buf, "%s%d", "stage", i );
    256         string stageFilename = dirName + buf + ".xml";
    257         FileStorage fs( stageFilename, FileStorage::WRITE );
    258         if ( !fs.isOpened() )
    259         {
    260             cout << "Current stage can not be written, because file " << stageFilename
    261                     << " can not be opened." << endl;
    262             return false;
    263         }
    264         fs << FileStorage::getDefaultObjectName(stageFilename) << "{";
    265         tempStage->write( fs, Mat() );
    266         fs << "}";
    267 
    268         // Output training time up till now
    269         float seconds = float( clock () - begin_time ) / CLOCKS_PER_SEC;
    270         int days = int(seconds) / 60 / 60 / 24;
    271         int hours = (int(seconds) / 60 / 60) % 24;
    272         int minutes = (int(seconds) / 60) % 60;
    273         int seconds_left = int(seconds) % 60;
    274         cout << "Training until now has taken " << days << " days " << hours << " hours " << minutes << " minutes " << seconds_left <<" seconds." << endl;
    275     }
    276 
    277     if(stageClassifiers.size() == 0)
    278     {
    279         cout << "Cascade classifier can't be trained. Check the used training parameters." << endl;
    280         return false;
    281     }
    282 
    283     save( dirName + CC_CASCADE_FILENAME, baseFormatSave );
    284 
    285     return true;
    286 }
    287 
    288 int CvCascadeClassifier::predict( int sampleIdx )
    289 {
    290     CV_DbgAssert( sampleIdx < numPos + numNeg );
    291     for (vector< Ptr<CvCascadeBoost> >::iterator it = stageClassifiers.begin();
    292         it != stageClassifiers.end(); it++ )
    293     {
    294         if ( (*it)->predict( sampleIdx ) == 0.f )
    295             return 0;
    296     }
    297     return 1;
    298 }
    299 
    300 bool CvCascadeClassifier::updateTrainingSet( double& acceptanceRatio)
    301 {
    302     int64 posConsumed = 0, negConsumed = 0;
    303     imgReader.restart();
    304     int posCount = fillPassedSamples( 0, numPos, true, posConsumed );
    305     if( !posCount )
    306         return false;
    307     cout << "POS count : consumed   " << posCount << " : " << (int)posConsumed << endl;
    308 
    309     int proNumNeg = cvRound( ( ((double)numNeg) * ((double)posCount) ) / numPos ); // apply only a fraction of negative samples. double is required since overflow is possible
    310     int negCount = fillPassedSamples( posCount, proNumNeg, false, negConsumed );
    311     if ( !negCount )
    312         return false;
    313 
    314     curNumSamples = posCount + negCount;
    315     acceptanceRatio = negConsumed == 0 ? 0 : ( (double)negCount/(double)(int64)negConsumed );
    316     cout << "NEG count : acceptanceRatio    " << negCount << " : " << acceptanceRatio << endl;
    317     return true;
    318 }
    319 
    320 int CvCascadeClassifier::fillPassedSamples( int first, int count, bool isPositive, int64& consumed )
    321 {
    322     int getcount = 0;
    323     Mat img(cascadeParams.winSize, CV_8UC1);
    324     for( int i = first; i < first + count; i++ )
    325     {
    326         for( ; ; )
    327         {
    328             bool isGetImg = isPositive ? imgReader.getPos( img ) :
    329                                            imgReader.getNeg( img );
    330             if( !isGetImg )
    331                 return getcount;
    332             consumed++;
    333 
    334             featureEvaluator->setImage( img, isPositive ? 1 : 0, i );
    335             if( predict( i ) == 1.0F )
    336             {
    337                 getcount++;
    338                 printf("%s current samples: %d\r", isPositive ? "POS":"NEG", getcount);
    339                 break;
    340             }
    341         }
    342     }
    343     return getcount;
    344 }
    345 
    346 void CvCascadeClassifier::writeParams( FileStorage &fs ) const
    347 {
    348     cascadeParams.write( fs );
    349     fs << CC_STAGE_PARAMS << "{"; stageParams->write( fs ); fs << "}";
    350     fs << CC_FEATURE_PARAMS << "{"; featureParams->write( fs ); fs << "}";
    351 }
    352 
    353 void CvCascadeClassifier::writeFeatures( FileStorage &fs, const Mat& featureMap ) const
    354 {
    355     featureEvaluator->writeFeatures( fs, featureMap );
    356 }
    357 
    358 void CvCascadeClassifier::writeStages( FileStorage &fs, const Mat& featureMap ) const
    359 {
    360     char cmnt[30];
    361     int i = 0;
    362     fs << CC_STAGES << "[";
    363     for( vector< Ptr<CvCascadeBoost> >::const_iterator it = stageClassifiers.begin();
    364         it != stageClassifiers.end(); it++, i++ )
    365     {
    366         sprintf( cmnt, "stage %d", i );
    367         cvWriteComment( fs.fs, cmnt, 0 );
    368         fs << "{";
    369         (*it)->write( fs, featureMap );
    370         fs << "}";
    371     }
    372     fs << "]";
    373 }
    374 
    375 bool CvCascadeClassifier::readParams( const FileNode &node )
    376 {
    377     if ( !node.isMap() || !cascadeParams.read( node ) )
    378         return false;
    379 
    380     stageParams = makePtr<CvCascadeBoostParams>();
    381     FileNode rnode = node[CC_STAGE_PARAMS];
    382     if ( !stageParams->read( rnode ) )
    383         return false;
    384 
    385     featureParams = CvFeatureParams::create(cascadeParams.featureType);
    386     rnode = node[CC_FEATURE_PARAMS];
    387     if ( !featureParams->read( rnode ) )
    388         return false;
    389     return true;
    390 }
    391 
    392 bool CvCascadeClassifier::readStages( const FileNode &node)
    393 {
    394     FileNode rnode = node[CC_STAGES];
    395     if (!rnode.empty() || !rnode.isSeq())
    396         return false;
    397     stageClassifiers.reserve(numStages);
    398     FileNodeIterator it = rnode.begin();
    399     for( int i = 0; i < min( (int)rnode.size(), numStages ); i++, it++ )
    400     {
    401         Ptr<CvCascadeBoost> tempStage = makePtr<CvCascadeBoost>();
    402         if ( !tempStage->read( *it, featureEvaluator, *stageParams) )
    403             return false;
    404         stageClassifiers.push_back(tempStage);
    405     }
    406     return true;
    407 }
    408 
    409 // For old Haar Classifier file saving
    410 #define ICV_HAAR_SIZE_NAME            "size"
    411 #define ICV_HAAR_STAGES_NAME          "stages"
    412 #define ICV_HAAR_TREES_NAME             "trees"
    413 #define ICV_HAAR_FEATURE_NAME             "feature"
    414 #define ICV_HAAR_RECTS_NAME                 "rects"
    415 #define ICV_HAAR_TILTED_NAME                "tilted"
    416 #define ICV_HAAR_THRESHOLD_NAME           "threshold"
    417 #define ICV_HAAR_LEFT_NODE_NAME           "left_node"
    418 #define ICV_HAAR_LEFT_VAL_NAME            "left_val"
    419 #define ICV_HAAR_RIGHT_NODE_NAME          "right_node"
    420 #define ICV_HAAR_RIGHT_VAL_NAME           "right_val"
    421 #define ICV_HAAR_STAGE_THRESHOLD_NAME   "stage_threshold"
    422 #define ICV_HAAR_PARENT_NAME            "parent"
    423 #define ICV_HAAR_NEXT_NAME              "next"
    424 
    425 void CvCascadeClassifier::save( const string filename, bool baseFormat )
    426 {
    427     FileStorage fs( filename, FileStorage::WRITE );
    428 
    429     if ( !fs.isOpened() )
    430         return;
    431 
    432     fs << FileStorage::getDefaultObjectName(filename) << "{";
    433     if ( !baseFormat )
    434     {
    435         Mat featureMap;
    436         getUsedFeaturesIdxMap( featureMap );
    437         writeParams( fs );
    438         fs << CC_STAGE_NUM << (int)stageClassifiers.size();
    439         writeStages( fs, featureMap );
    440         writeFeatures( fs, featureMap );
    441     }
    442     else
    443     {
    444         //char buf[256];
    445         CvSeq* weak;
    446         if ( cascadeParams.featureType != CvFeatureParams::HAAR )
    447             CV_Error( CV_StsBadFunc, "old file format is used for Haar-like features only");
    448         fs << ICV_HAAR_SIZE_NAME << "[:" << cascadeParams.winSize.width <<
    449             cascadeParams.winSize.height << "]";
    450         fs << ICV_HAAR_STAGES_NAME << "[";
    451         for( size_t si = 0; si < stageClassifiers.size(); si++ )
    452         {
    453             fs << "{"; //stage
    454             /*sprintf( buf, "stage %d", si );
    455             CV_CALL( cvWriteComment( fs, buf, 1 ) );*/
    456             weak = stageClassifiers[si]->get_weak_predictors();
    457             fs << ICV_HAAR_TREES_NAME << "[";
    458             for( int wi = 0; wi < weak->total; wi++ )
    459             {
    460                 int inner_node_idx = -1, total_inner_node_idx = -1;
    461                 queue<const CvDTreeNode*> inner_nodes_queue;
    462                 CvCascadeBoostTree* tree = *((CvCascadeBoostTree**) cvGetSeqElem( weak, wi ));
    463 
    464                 fs << "[";
    465                 /*sprintf( buf, "tree %d", wi );
    466                 CV_CALL( cvWriteComment( fs, buf, 1 ) );*/
    467 
    468                 const CvDTreeNode* tempNode;
    469 
    470                 inner_nodes_queue.push( tree->get_root() );
    471                 total_inner_node_idx++;
    472 
    473                 while (!inner_nodes_queue.empty())
    474                 {
    475                     tempNode = inner_nodes_queue.front();
    476                     inner_node_idx++;
    477 
    478                     fs << "{";
    479                     fs << ICV_HAAR_FEATURE_NAME << "{";
    480                     ((CvHaarEvaluator*)featureEvaluator.get())->writeFeature( fs, tempNode->split->var_idx );
    481                     fs << "}";
    482 
    483                     fs << ICV_HAAR_THRESHOLD_NAME << tempNode->split->ord.c;
    484 
    485                     if( tempNode->left->left || tempNode->left->right )
    486                     {
    487                         inner_nodes_queue.push( tempNode->left );
    488                         total_inner_node_idx++;
    489                         fs << ICV_HAAR_LEFT_NODE_NAME << total_inner_node_idx;
    490                     }
    491                     else
    492                         fs << ICV_HAAR_LEFT_VAL_NAME << tempNode->left->value;
    493 
    494                     if( tempNode->right->left || tempNode->right->right )
    495                     {
    496                         inner_nodes_queue.push( tempNode->right );
    497                         total_inner_node_idx++;
    498                         fs << ICV_HAAR_RIGHT_NODE_NAME << total_inner_node_idx;
    499                     }
    500                     else
    501                         fs << ICV_HAAR_RIGHT_VAL_NAME << tempNode->right->value;
    502                     fs << "}"; // ICV_HAAR_FEATURE_NAME
    503                     inner_nodes_queue.pop();
    504                 }
    505                 fs << "]";
    506             }
    507             fs << "]"; //ICV_HAAR_TREES_NAME
    508             fs << ICV_HAAR_STAGE_THRESHOLD_NAME << stageClassifiers[si]->getThreshold();
    509             fs << ICV_HAAR_PARENT_NAME << (int)si-1 << ICV_HAAR_NEXT_NAME << -1;
    510             fs << "}"; //stage
    511         } /* for each stage */
    512         fs << "]"; //ICV_HAAR_STAGES_NAME
    513     }
    514     fs << "}";
    515 }
    516 
    517 bool CvCascadeClassifier::load( const string cascadeDirName )
    518 {
    519     FileStorage fs( cascadeDirName + CC_PARAMS_FILENAME, FileStorage::READ );
    520     if ( !fs.isOpened() )
    521         return false;
    522     FileNode node = fs.getFirstTopLevelNode();
    523     if ( !readParams( node ) )
    524         return false;
    525     featureEvaluator = CvFeatureEvaluator::create(cascadeParams.featureType);
    526     featureEvaluator->init( featureParams, numPos + numNeg, cascadeParams.winSize );
    527     fs.release();
    528 
    529     char buf[10];
    530     for ( int si = 0; si < numStages; si++ )
    531     {
    532         sprintf( buf, "%s%d", "stage", si);
    533         fs.open( cascadeDirName + buf + ".xml", FileStorage::READ );
    534         node = fs.getFirstTopLevelNode();
    535         if ( !fs.isOpened() )
    536             break;
    537         Ptr<CvCascadeBoost> tempStage = makePtr<CvCascadeBoost>();
    538 
    539         if ( !tempStage->read( node, featureEvaluator, *stageParams ))
    540         {
    541             fs.release();
    542             break;
    543         }
    544         stageClassifiers.push_back(tempStage);
    545     }
    546     return true;
    547 }
    548 
    549 void CvCascadeClassifier::getUsedFeaturesIdxMap( Mat& featureMap )
    550 {
    551     int varCount = featureEvaluator->getNumFeatures() * featureEvaluator->getFeatureSize();
    552     featureMap.create( 1, varCount, CV_32SC1 );
    553     featureMap.setTo(Scalar(-1));
    554 
    555     for( vector< Ptr<CvCascadeBoost> >::const_iterator it = stageClassifiers.begin();
    556         it != stageClassifiers.end(); it++ )
    557         (*it)->markUsedFeaturesInMap( featureMap );
    558 
    559     for( int fi = 0, idx = 0; fi < varCount; fi++ )
    560         if ( featureMap.at<int>(0, fi) >= 0 )
    561             featureMap.ptr<int>(0)[fi] = idx++;
    562 }
    563