Update to 2.0.0 tree from current Fremantle build
[opencv] / src / ml / mlcnn.cpp
diff --git a/src/ml/mlcnn.cpp b/src/ml/mlcnn.cpp
new file mode 100644 (file)
index 0000000..285bb9f
--- /dev/null
@@ -0,0 +1,1675 @@
+/*M///////////////////////////////////////////////////////////////////////////////////////
+//
+//  IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
+//
+//  By downloading, copying, installing or using the software you agree to this license.
+//  If you do not agree to this license, do not download, install,
+//  copy or use the software.
+//
+//
+//                        Intel License Agreement
+//
+// Copyright (C) 2000, Intel Corporation, all rights reserved.
+// Third party copyrights are property of their respective owners.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+//   * Redistribution's of source code must retain the above copyright notice,
+//     this list of conditions and the following disclaimer.
+//
+//   * Redistribution's in binary form must reproduce the above copyright notice,
+//     this list of conditions and the following disclaimer in the documentation
+//     and/or other materials provided with the distribution.
+//
+//   * The name of Intel Corporation may not be used to endorse or promote products
+//     derived from this software without specific prior written permission.
+//
+// This software is provided by the copyright holders and contributors "as is" and
+// any express or implied warranties, including, but not limited to, the implied
+// warranties of merchantability and fitness for a particular purpose are disclaimed.
+// In no event shall the Intel Corporation or contributors be liable for any direct,
+// indirect, incidental, special, exemplary, or consequential damages
+// (including, but not limited to, procurement of substitute goods or services;
+// loss of use, data, or profits; or business interruption) however caused
+// and on any theory of liability, whether in contract, strict liability,
+// or tort (including negligence or otherwise) arising in any way out of
+// the use of this software, even if advised of the possibility of such damage.
+//
+//M*/
+
+#include "_ml.h"
+
+#if 0
+/****************************************************************************************\
+*                         Auxilary functions declarations                                *
+\****************************************************************************************/
+/*---------------------- functions for the CNN classifier ------------------------------*/
+static float icvCNNModelPredict(
+        const CvStatModel* cnn_model,
+        const CvMat* image,
+        CvMat* probs CV_DEFAULT(0) );
+
+static void icvCNNModelUpdate(
+        CvStatModel* cnn_model, const CvMat* images, int tflag,
+        const CvMat* responses, const CvStatModelParams* params,
+        const CvMat* CV_DEFAULT(0), const CvMat* sample_idx CV_DEFAULT(0),
+        const CvMat* CV_DEFAULT(0), const CvMat* CV_DEFAULT(0));
+
+static void icvCNNModelRelease( CvStatModel** cnn_model );
+
+static void icvTrainCNNetwork( CvCNNetwork* network,
+                               const float** images,
+                               const CvMat* responses,
+                               const CvMat* etalons,
+                               int grad_estim_type,
+                               int max_iter,
+                               int start_iter );
+
+/*------------------------- functions for the CNN network ------------------------------*/
+static void icvCNNetworkAddLayer( CvCNNetwork* network, CvCNNLayer* layer );
+static void icvCNNetworkRelease( CvCNNetwork** network );
+
+/* In all layer functions we denote input by X and output by Y, where
+   X and Y are column-vectors, so that
+   length(X)==<n_input_planes>*<input_height>*<input_width>,
+   length(Y)==<n_output_planes>*<output_height>*<output_width>.
+*/
+/*------------------------ functions for convolutional layer ---------------------------*/
+static void icvCNNConvolutionRelease( CvCNNLayer** p_layer );
+
+static void icvCNNConvolutionForward( CvCNNLayer* layer, const CvMat* X, CvMat* Y );
+
+static void icvCNNConvolutionBackward( CvCNNLayer*  layer, int t,
+    const CvMat* X, const CvMat* dE_dY, CvMat* dE_dX );
+
+/*------------------------ functions for sub-sampling layer ----------------------------*/
+static void icvCNNSubSamplingRelease( CvCNNLayer** p_layer );
+
+static void icvCNNSubSamplingForward( CvCNNLayer* layer, const CvMat* X, CvMat* Y );
+
+static void icvCNNSubSamplingBackward( CvCNNLayer*  layer, int t,
+    const CvMat* X, const CvMat* dE_dY, CvMat* dE_dX );
+
+/*------------------------ functions for full connected layer --------------------------*/
+static void icvCNNFullConnectRelease( CvCNNLayer** p_layer );
+
+static void icvCNNFullConnectForward( CvCNNLayer* layer, const CvMat* X, CvMat* Y );
+
+static void icvCNNFullConnectBackward( CvCNNLayer* layer, int,
+    const CvMat*, const CvMat* dE_dY, CvMat* dE_dX );
+
+/****************************************************************************************\
+*                             Functions implementations                                  *
+\****************************************************************************************/
+
+#define ICV_CHECK_CNN_NETWORK(network)                                                  \
+{                                                                                       \
+    CvCNNLayer* first_layer, *layer, *last_layer;                                       \
+    int n_layers, i;                                                                    \
+    if( !network )                                                                      \
+        CV_ERROR( CV_StsNullPtr,                                                        \
+        "Null <network> pointer. Network must be created by user." );                   \
+    n_layers = network->n_layers;                                                       \
+    first_layer = last_layer = network->layers;                                         \
+    for( i = 0, layer = first_layer; i < n_layers && layer; i++ )                       \
+    {                                                                                   \
+        if( !ICV_IS_CNN_LAYER(layer) )                                                  \
+            CV_ERROR( CV_StsNullPtr, "Invalid network" );                               \
+        last_layer = layer;                                                             \
+        layer = layer->next_layer;                                                      \
+    }                                                                                   \
+                                                                                        \
+    if( i == 0 || i != n_layers || first_layer->prev_layer || layer )                   \
+        CV_ERROR( CV_StsNullPtr, "Invalid network" );                                   \
+                                                                                        \
+    if( first_layer->n_input_planes != 1 )                                              \
+        CV_ERROR( CV_StsBadArg, "First layer must contain only one input plane" );      \
+                                                                                        \
+    if( img_size != first_layer->input_height*first_layer->input_width )                \
+        CV_ERROR( CV_StsBadArg, "Invalid input sizes of the first layer" );             \
+                                                                                        \
+    if( params->etalons->cols != last_layer->n_output_planes*                           \
+        last_layer->output_height*last_layer->output_width )                            \
+        CV_ERROR( CV_StsBadArg, "Invalid output sizes of the last layer" );             \
+}
+
+#define ICV_CHECK_CNN_MODEL_PARAMS(params)                                              \
+{                                                                                       \
+    if( !params )                                                                       \
+        CV_ERROR( CV_StsNullPtr, "Null <params> pointer" );                             \
+                                                                                        \
+    if( !ICV_IS_MAT_OF_TYPE(params->etalons, CV_32FC1) )                                \
+        CV_ERROR( CV_StsBadArg, "<etalons> must be CV_32FC1 type" );                    \
+    if( params->etalons->rows != cnn_model->cls_labels->cols )                          \
+        CV_ERROR( CV_StsBadArg, "Invalid <etalons> size" );                             \
+                                                                                        \
+    if( params->grad_estim_type != CV_CNN_GRAD_ESTIM_RANDOM &&                          \
+        params->grad_estim_type != CV_CNN_GRAD_ESTIM_BY_WORST_IMG )                     \
+        CV_ERROR( CV_StsBadArg, "Invalid <grad_estim_type>" );                          \
+                                                                                        \
+    if( params->start_iter < 0 )                                                        \
+        CV_ERROR( CV_StsBadArg, "Parameter <start_iter> must be positive or zero" );    \
+                                                                                        \
+    if( params->max_iter < 1 )                                                \
+        params->max_iter = 1;                                                 \
+}
+
+/****************************************************************************************\
+*                              Classifier functions                                      *
+\****************************************************************************************/
+ML_IMPL CvStatModel*
+cvTrainCNNClassifier( const CvMat* _train_data, int tflag,
+            const CvMat* _responses,
+            const CvStatModelParams* _params,
+            const CvMat*, const CvMat* _sample_idx, const CvMat*, const CvMat* )
+{
+    CvCNNStatModel* cnn_model    = 0;
+    const float** out_train_data = 0;
+    CvMat* responses             = 0;
+
+    CV_FUNCNAME("cvTrainCNNClassifier");
+    __BEGIN__;
+
+    int n_images;
+    int img_size;
+    CvCNNStatModelParams* params = (CvCNNStatModelParams*)_params;
+
+    CV_CALL(cnn_model = (CvCNNStatModel*)cvCreateStatModel(
+        CV_STAT_MODEL_MAGIC_VAL|CV_CNN_MAGIC_VAL, sizeof(CvCNNStatModel),
+        icvCNNModelRelease, icvCNNModelPredict, icvCNNModelUpdate ));
+
+    CV_CALL(cvPrepareTrainData( "cvTrainCNNClassifier",
+        _train_data, tflag, _responses, CV_VAR_CATEGORICAL,
+        0, _sample_idx, false, &out_train_data,
+        &n_images, &img_size, &img_size, &responses,
+        &cnn_model->cls_labels, 0 ));
+
+    ICV_CHECK_CNN_MODEL_PARAMS(params);
+    ICV_CHECK_CNN_NETWORK(params->network);
+
+    cnn_model->network = params->network;
+    CV_CALL(cnn_model->etalons = (CvMat*)cvClone( params->etalons ));
+
+    CV_CALL( icvTrainCNNetwork( cnn_model->network, out_train_data, responses,
+        cnn_model->etalons, params->grad_estim_type, params->max_iter,
+        params->start_iter ));
+
+    __END__;
+
+    if( cvGetErrStatus() < 0 && cnn_model )
+    {
+        cnn_model->release( (CvStatModel**)&cnn_model );
+    }
+    cvFree( &out_train_data );
+    cvReleaseMat( &responses );
+
+    return (CvStatModel*)cnn_model;
+}
+
+/****************************************************************************************/
+static void icvTrainCNNetwork( CvCNNetwork* network,
+                               const float** images,
+                               const CvMat* responses,
+                               const CvMat* etalons,
+                               int grad_estim_type,
+                               int max_iter,
+                               int start_iter )
+{
+    CvMat** X     = 0;
+    CvMat** dE_dX = 0;
+    const int n_layers = network->n_layers;
+    int k;
+
+    CV_FUNCNAME("icvTrainCNNetwork");
+    __BEGIN__;
+
+    CvCNNLayer* first_layer = network->layers;
+    const int img_height = first_layer->input_height;
+    const int img_width  = first_layer->input_width;
+    const int img_size   = img_width*img_height;
+    const int n_images   = responses->cols;
+    CvMat image = cvMat( 1, img_size, CV_32FC1 );
+    CvCNNLayer* layer;
+    int n;
+    CvRNG rng = cvRNG(-1);
+
+    CV_CALL(X = (CvMat**)cvAlloc( (n_layers+1)*sizeof(CvMat*) ));
+    CV_CALL(dE_dX = (CvMat**)cvAlloc( (n_layers+1)*sizeof(CvMat*) ));
+    memset( X, 0, (n_layers+1)*sizeof(CvMat*) );
+    memset( dE_dX, 0, (n_layers+1)*sizeof(CvMat*) );
+
+    CV_CALL(X[0] = cvCreateMat( img_height*img_width,1,CV_32FC1 ));
+    CV_CALL(dE_dX[0] = cvCreateMat( 1, X[0]->rows, CV_32FC1 ));
+    for( k = 0, layer = first_layer; k < n_layers; k++, layer = layer->next_layer )
+    {
+        CV_CALL(X[k+1] = cvCreateMat( layer->n_output_planes*layer->output_height*
+            layer->output_width, 1, CV_32FC1 ));
+        CV_CALL(dE_dX[k+1] = cvCreateMat( 1, X[k+1]->rows, CV_32FC1 ));
+    }
+
+    for( n = 1; n <= max_iter; n++ )
+    {
+        float loss, max_loss = 0;
+        int i;
+        int worst_img_idx = -1;
+        int* right_etal_idx = responses->data.i;
+        CvMat etalon;
+
+        // Find the worst image (which produces the greatest loss) or use the random image
+        if( grad_estim_type == CV_CNN_GRAD_ESTIM_BY_WORST_IMG )
+        {
+            for( i = 0; i < n_images; i++, right_etal_idx++ )
+            {
+                image.data.fl = (float*)images[i];
+                cvTranspose( &image, X[0] );
+                
+                for( k = 0, layer = first_layer; k < n_layers; k++, layer = layer->next_layer )
+                    CV_CALL(layer->forward( layer, X[k], X[k+1] ));
+                
+                cvTranspose( X[n_layers], dE_dX[n_layers] );
+                cvGetRow( etalons, &etalon, *right_etal_idx );
+                loss = (float)cvNorm( dE_dX[n_layers], &etalon );
+                if( loss > max_loss )
+                {
+                    max_loss = loss;
+                    worst_img_idx = i;
+                }
+            }
+        }
+        else
+            worst_img_idx = cvRandInt(&rng) % n_images;
+
+        // Train network on the worst image
+        // 1) Compute the network output on the <image>
+        image.data.fl = (float*)images[worst_img_idx];
+        CV_CALL(cvTranspose( &image, X[0] ));
+
+        for( k = 0, layer = first_layer; k < n_layers - 1; k++, layer = layer->next_layer )
+            CV_CALL(layer->forward( layer, X[k], X[k+1] ));
+        CV_CALL(layer->forward( layer, X[k], X[k+1] ));
+
+        // 2) Compute the gradient
+        cvTranspose( X[n_layers], dE_dX[n_layers] );
+        cvGetRow( etalons, &etalon, responses->data.i[worst_img_idx] );
+        cvSub( dE_dX[n_layers], &etalon, dE_dX[n_layers] );
+
+        // 3) Update weights by the gradient descent
+        for( k = n_layers; k > 0; k--, layer = layer->prev_layer )
+            CV_CALL(layer->backward( layer, n + start_iter, X[k-1], dE_dX[k], dE_dX[k-1] ));
+    }
+
+    __END__;
+
+    for( k = 0; k <= n_layers; k++ )
+    {
+        cvReleaseMat( &X[k] );
+        cvReleaseMat( &dE_dX[k] );
+    }
+    cvFree( &X );
+    cvFree( &dE_dX );
+}
+
+/****************************************************************************************/
+static float icvCNNModelPredict( const CvStatModel* model,
+                                 const CvMat* _image,
+                                 CvMat* probs )
+{
+    CvMat** X       = 0;
+    float* img_data = 0;
+    int n_layers = 0;
+    int best_etal_idx = -1;
+    int k;
+
+    CV_FUNCNAME("icvCNNModelPredict");
+    __BEGIN__;
+
+    CvCNNStatModel* cnn_model = (CvCNNStatModel*)model;
+    CvCNNLayer* first_layer, *layer = 0;
+    int img_height, img_width, img_size;
+    int nclasses, i;
+    float loss, min_loss = FLT_MAX;
+    float* probs_data;
+    CvMat etalon, image;
+
+    if( !CV_IS_CNN(model) )
+        CV_ERROR( CV_StsBadArg, "Invalid model" );
+
+    nclasses = cnn_model->cls_labels->cols;
+    n_layers = cnn_model->network->n_layers;
+    first_layer   = cnn_model->network->layers;
+    img_height = first_layer->input_height;
+    img_width  = first_layer->input_width;
+    img_size   = img_height*img_width;
+
+    cvPreparePredictData( _image, img_size, 0, nclasses, probs, &img_data );
+
+    CV_CALL(X = (CvMat**)cvAlloc( (n_layers+1)*sizeof(CvMat*) ));
+    memset( X, 0, (n_layers+1)*sizeof(CvMat*) );
+
+    CV_CALL(X[0] = cvCreateMat( img_size,1,CV_32FC1 ));
+    for( k = 0, layer = first_layer; k < n_layers; k++, layer = layer->next_layer )
+    {
+        CV_CALL(X[k+1] = cvCreateMat( layer->n_output_planes*layer->output_height*
+            layer->output_width, 1, CV_32FC1 ));
+    }
+
+    image = cvMat( 1, img_size, CV_32FC1, img_data );
+    cvTranspose( &image, X[0] );
+    for( k = 0, layer = first_layer; k < n_layers; k++, layer = layer->next_layer )
+        CV_CALL(layer->forward( layer, X[k], X[k+1] ));
+
+    probs_data = probs ? probs->data.fl : 0;
+    etalon = cvMat( cnn_model->etalons->cols, 1, CV_32FC1, cnn_model->etalons->data.fl );
+    for( i = 0; i < nclasses; i++, etalon.data.fl += cnn_model->etalons->cols )
+    {
+        loss = (float)cvNorm( X[n_layers], &etalon );
+        if( loss < min_loss )
+        {
+            min_loss = loss;
+            best_etal_idx = i;
+        }
+        if( probs )
+            *probs_data++ = -loss;
+    }
+
+    if( probs )
+    {
+        cvExp( probs, probs );
+        CvScalar sum = cvSum( probs );
+        cvConvertScale( probs, probs, 1./sum.val[0] );
+    }
+
+    __END__;
+
+    for( k = 0; k <= n_layers; k++ )
+        cvReleaseMat( &X[k] );
+    cvFree( &X );
+    if( img_data != _image->data.fl )
+        cvFree( &img_data );
+
+    return ((float) ((CvCNNStatModel*)model)->cls_labels->data.i[best_etal_idx]);
+}
+
+/****************************************************************************************/
+static void icvCNNModelUpdate(
+        CvStatModel* _cnn_model, const CvMat* _train_data, int tflag,
+        const CvMat* _responses, const CvStatModelParams* _params,
+        const CvMat*, const CvMat* _sample_idx,
+        const CvMat*, const CvMat* )
+{
+    const float** out_train_data = 0;
+    CvMat* responses             = 0;
+    CvMat* cls_labels            = 0;
+
+    CV_FUNCNAME("icvCNNModelUpdate");
+    __BEGIN__;
+
+    int n_images, img_size, i;
+    CvCNNStatModelParams* params = (CvCNNStatModelParams*)_params;
+    CvCNNStatModel* cnn_model = (CvCNNStatModel*)_cnn_model;
+
+    if( !CV_IS_CNN(cnn_model) )
+        CV_ERROR( CV_StsBadArg, "Invalid model" );
+
+    CV_CALL(cvPrepareTrainData( "cvTrainCNNClassifier",
+        _train_data, tflag, _responses, CV_VAR_CATEGORICAL,
+        0, _sample_idx, false, &out_train_data,
+        &n_images, &img_size, &img_size, &responses,
+        &cls_labels, 0, 0 ));
+
+    ICV_CHECK_CNN_MODEL_PARAMS(params);
+
+    // Number of classes must be the same as when classifiers was created
+    if( !CV_ARE_SIZES_EQ(cls_labels, cnn_model->cls_labels) )
+        CV_ERROR( CV_StsBadArg, "Number of classes must be left unchanged" );
+    for( i = 0; i < cls_labels->cols; i++ )
+    {
+        if( cls_labels->data.i[i] != cnn_model->cls_labels->data.i[i] )
+            CV_ERROR( CV_StsBadArg, "Number of classes must be left unchanged" );
+    }
+
+    CV_CALL( icvTrainCNNetwork( cnn_model->network, out_train_data, responses,
+        cnn_model->etalons, params->grad_estim_type, params->max_iter,
+        params->start_iter ));
+
+    __END__;
+
+    cvFree( &out_train_data );
+    cvReleaseMat( &responses );
+}
+
+/****************************************************************************************/
+static void icvCNNModelRelease( CvStatModel** cnn_model )
+{
+    CV_FUNCNAME("icvCNNModelRelease");
+    __BEGIN__;
+
+    CvCNNStatModel* cnn;
+    if( !cnn_model )
+        CV_ERROR( CV_StsNullPtr, "Null double pointer" );
+
+    cnn = *(CvCNNStatModel**)cnn_model;
+
+    cvReleaseMat( &cnn->cls_labels );
+    cvReleaseMat( &cnn->etalons );
+    cnn->network->release( &cnn->network );
+
+    cvFree( &cnn );
+
+    __END__;
+
+}
+
+/****************************************************************************************\
+*                                 Network functions                                      *
+\****************************************************************************************/
+ML_IMPL CvCNNetwork* cvCreateCNNetwork( CvCNNLayer* first_layer )
+{
+    CvCNNetwork* network = 0;    
+    
+    CV_FUNCNAME( "cvCreateCNNetwork" );
+    __BEGIN__;
+
+    if( !ICV_IS_CNN_LAYER(first_layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    CV_CALL(network = (CvCNNetwork*)cvAlloc( sizeof(CvCNNetwork) ));
+    memset( network, 0, sizeof(CvCNNetwork) );
+
+    network->layers    = first_layer;
+    network->n_layers  = 1;
+    network->release   = icvCNNetworkRelease;
+    network->add_layer = icvCNNetworkAddLayer;
+
+    __END__;
+
+    if( cvGetErrStatus() < 0 && network )
+        cvFree( &network );
+
+    return network;
+
+}
+
+/****************************************************************************************/
+static void icvCNNetworkAddLayer( CvCNNetwork* network, CvCNNLayer* layer )
+{
+    CV_FUNCNAME( "icvCNNetworkAddLayer" );
+    __BEGIN__;
+
+    CvCNNLayer* prev_layer;
+
+    if( network == NULL )
+        CV_ERROR( CV_StsNullPtr, "Null <network> pointer" );
+
+    prev_layer = network->layers;
+    while( prev_layer->next_layer )
+        prev_layer = prev_layer->next_layer;
+
+    if( ICV_IS_CNN_FULLCONNECT_LAYER(layer) )
+    {
+        if( layer->n_input_planes != prev_layer->output_width*prev_layer->output_height*
+            prev_layer->n_output_planes )
+            CV_ERROR( CV_StsBadArg, "Unmatched size of the new layer" );
+        if( layer->input_height != 1 || layer->output_height != 1 ||
+            layer->input_width != 1  || layer->output_width != 1 )
+            CV_ERROR( CV_StsBadArg, "Invalid size of the new layer" );
+    }
+    else if( ICV_IS_CNN_CONVOLUTION_LAYER(layer) || ICV_IS_CNN_SUBSAMPLING_LAYER(layer) )
+    {
+        if( prev_layer->n_output_planes != layer->n_input_planes ||
+        prev_layer->output_height   != layer->input_height ||
+        prev_layer->output_width    != layer->input_width )
+        CV_ERROR( CV_StsBadArg, "Unmatched size of the new layer" );
+    }
+    else
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    layer->prev_layer = prev_layer;
+    prev_layer->next_layer = layer;
+    network->n_layers++;
+
+    __END__;
+}
+
+/****************************************************************************************/
+static void icvCNNetworkRelease( CvCNNetwork** network_pptr )
+{
+    CV_FUNCNAME( "icvReleaseCNNetwork" );
+    __BEGIN__;
+
+    CvCNNetwork* network = 0;
+    CvCNNLayer* layer = 0, *next_layer = 0;
+    int k;
+
+    if( network_pptr == NULL )
+        CV_ERROR( CV_StsBadArg, "Null double pointer" );
+    if( *network_pptr == NULL )
+        return;
+
+    network = *network_pptr;
+    layer = network->layers;
+    if( layer == NULL )
+        CV_ERROR( CV_StsBadArg, "CNN is empty (does not contain any layer)" );
+
+    // k is the number of the layer to be deleted
+    for( k = 0; k < network->n_layers && layer; k++ )
+    {
+        next_layer = layer->next_layer;
+        layer->release( &layer );
+        layer = next_layer;
+    }
+
+    if( k != network->n_layers || layer)
+        CV_ERROR( CV_StsBadArg, "Invalid network" );
+
+    cvFree( &network );
+
+    __END__;
+}
+
+/****************************************************************************************\
+*                                  Layer functions                                       *
+\****************************************************************************************/
+static CvCNNLayer* icvCreateCNNLayer( int layer_type, int header_size,
+    int n_input_planes, int input_height, int input_width,
+    int n_output_planes, int output_height, int output_width,
+    float init_learn_rate, int learn_rate_decrease_type,
+    CvCNNLayerRelease release, CvCNNLayerForward forward, CvCNNLayerBackward backward )
+{
+    CvCNNLayer* layer = 0;
+
+    CV_FUNCNAME("icvCreateCNNLayer");
+    __BEGIN__;
+
+    CV_ASSERT( release && forward && backward )
+    CV_ASSERT( header_size >= sizeof(CvCNNLayer) )
+
+    if( n_input_planes < 1 || n_output_planes < 1 ||
+        input_height   < 1 || input_width < 1 ||
+        output_height  < 1 || output_width < 1 ||
+        input_height < output_height ||
+        input_width  < output_width )
+        CV_ERROR( CV_StsBadArg, "Incorrect input or output parameters" );
+    if( init_learn_rate < FLT_EPSILON )
+        CV_ERROR( CV_StsBadArg, "Initial learning rate must be positive" );
+    if( learn_rate_decrease_type != CV_CNN_LEARN_RATE_DECREASE_HYPERBOLICALLY &&
+        learn_rate_decrease_type != CV_CNN_LEARN_RATE_DECREASE_SQRT_INV &&
+        learn_rate_decrease_type != CV_CNN_LEARN_RATE_DECREASE_LOG_INV )
+        CV_ERROR( CV_StsBadArg, "Invalid type of learning rate dynamics" );
+
+    CV_CALL(layer = (CvCNNLayer*)cvAlloc( header_size ));
+    memset( layer, 0, header_size );
+
+    layer->flags = ICV_CNN_LAYER|layer_type;
+    CV_ASSERT( ICV_IS_CNN_LAYER(layer) )
+
+    layer->n_input_planes = n_input_planes;
+    layer->input_height   = input_height;
+    layer->input_width    = input_width;
+
+    layer->n_output_planes = n_output_planes;
+    layer->output_height   = output_height;
+    layer->output_width    = output_width;
+
+    layer->init_learn_rate = init_learn_rate;
+    layer->learn_rate_decrease_type = learn_rate_decrease_type;
+
+    layer->release  = release;
+    layer->forward  = forward;
+    layer->backward = backward;
+
+    __END__;
+
+    if( cvGetErrStatus() < 0 && layer)
+        cvFree( &layer );
+
+    return layer;
+}
+
+/****************************************************************************************/
+ML_IMPL CvCNNLayer* cvCreateCNNConvolutionLayer(
+    int n_input_planes, int input_height, int input_width,
+    int n_output_planes, int K,
+    float init_learn_rate, int learn_rate_decrease_type,
+    CvMat* connect_mask, CvMat* weights )
+
+{
+    CvCNNConvolutionLayer* layer = 0;
+
+    CV_FUNCNAME("cvCreateCNNConvolutionLayer");
+    __BEGIN__;
+
+    const int output_height = input_height - K + 1;
+    const int output_width = input_width - K + 1;
+
+    if( K < 1 || init_learn_rate <= 0 )
+        CV_ERROR( CV_StsBadArg, "Incorrect parameters" );
+
+    CV_CALL(layer = (CvCNNConvolutionLayer*)icvCreateCNNLayer( ICV_CNN_CONVOLUTION_LAYER,
+        sizeof(CvCNNConvolutionLayer), n_input_planes, input_height, input_width,
+        n_output_planes, output_height, output_width,
+        init_learn_rate, learn_rate_decrease_type,
+        icvCNNConvolutionRelease, icvCNNConvolutionForward, icvCNNConvolutionBackward ));
+
+    layer->K = K;
+    CV_CALL(layer->weights = cvCreateMat( n_output_planes, K*K+1, CV_32FC1 ));
+    CV_CALL(layer->connect_mask = cvCreateMat( n_output_planes, n_input_planes, CV_8UC1));
+
+    if( weights )
+    {
+        if( !ICV_IS_MAT_OF_TYPE( weights, CV_32FC1 ) )
+            CV_ERROR( CV_StsBadSize, "Type of initial weights matrix must be CV_32FC1" );
+        if( !CV_ARE_SIZES_EQ( weights, layer->weights ) )
+            CV_ERROR( CV_StsBadSize, "Invalid size of initial weights matrix" );
+        CV_CALL(cvCopy( weights, layer->weights ));
+    }
+    else
+    {
+        CvRNG rng = cvRNG( 0xFFFFFFFF );
+        cvRandArr( &rng, layer->weights, CV_RAND_UNI, cvRealScalar(-1), cvRealScalar(1) );
+    }
+    if( connect_mask )
+    {
+        if( !ICV_IS_MAT_OF_TYPE( connect_mask, CV_8UC1 ) )
+            CV_ERROR( CV_StsBadSize, "Type of connection matrix must be CV_32FC1" );
+        if( !CV_ARE_SIZES_EQ( connect_mask, layer->connect_mask ) )
+            CV_ERROR( CV_StsBadSize, "Invalid size of connection matrix" );
+        CV_CALL(cvCopy( connect_mask, layer->connect_mask ));
+    }
+    else
+        CV_CALL(cvSet( layer->connect_mask, cvRealScalar(1) ));
+
+    __END__;
+
+    if( cvGetErrStatus() < 0 && layer )
+    {
+        cvReleaseMat( &layer->weights );
+        cvReleaseMat( &layer->connect_mask );
+        cvFree( &layer );
+    }
+    
+    return (CvCNNLayer*)layer;
+}
+
+/****************************************************************************************/
+ML_IMPL CvCNNLayer* cvCreateCNNSubSamplingLayer(
+    int n_input_planes, int input_height, int input_width,
+    int sub_samp_scale, float a, float s,
+    float init_learn_rate, int learn_rate_decrease_type, CvMat* weights )
+
+{
+    CvCNNSubSamplingLayer* layer = 0;
+
+    CV_FUNCNAME("cvCreateCNNSubSamplingLayer");
+    __BEGIN__;
+
+    const int output_height   = input_height/sub_samp_scale;
+    const int output_width    = input_width/sub_samp_scale;
+    const int n_output_planes = n_input_planes;
+
+    if( sub_samp_scale < 1 || a <= 0 || s <= 0)
+        CV_ERROR( CV_StsBadArg, "Incorrect parameters" );
+
+    CV_CALL(layer = (CvCNNSubSamplingLayer*)icvCreateCNNLayer( ICV_CNN_SUBSAMPLING_LAYER,
+        sizeof(CvCNNSubSamplingLayer), n_input_planes, input_height, input_width,
+        n_output_planes, output_height, output_width,
+        init_learn_rate, learn_rate_decrease_type,
+        icvCNNSubSamplingRelease, icvCNNSubSamplingForward, icvCNNSubSamplingBackward ));
+
+    layer->sub_samp_scale  = sub_samp_scale;
+    layer->a               = a;
+    layer->s               = s;
+
+    CV_CALL(layer->sumX =
+        cvCreateMat( n_output_planes*output_width*output_height, 1, CV_32FC1 ));
+    CV_CALL(layer->exp2ssumWX =
+        cvCreateMat( n_output_planes*output_width*output_height, 1, CV_32FC1 ));
+
+    cvZero( layer->sumX );
+    cvZero( layer->exp2ssumWX );
+
+    CV_CALL(layer->weights = cvCreateMat( n_output_planes, 2, CV_32FC1 ));
+    if( weights )
+    {
+        if( !ICV_IS_MAT_OF_TYPE( weights, CV_32FC1 ) )
+            CV_ERROR( CV_StsBadSize, "Type of initial weights matrix must be CV_32FC1" );
+        if( !CV_ARE_SIZES_EQ( weights, layer->weights ) )
+            CV_ERROR( CV_StsBadSize, "Invalid size of initial weights matrix" );
+        CV_CALL(cvCopy( weights, layer->weights ));
+    }
+    else
+    {
+        CvRNG rng = cvRNG( 0xFFFFFFFF );
+        cvRandArr( &rng, layer->weights, CV_RAND_UNI, cvRealScalar(-1), cvRealScalar(1) );
+    }
+
+    __END__;
+
+    if( cvGetErrStatus() < 0 && layer )
+    {
+        cvReleaseMat( &layer->exp2ssumWX );
+        cvFree( &layer );
+    }
+
+    return (CvCNNLayer*)layer;
+}
+
+/****************************************************************************************/
+ML_IMPL CvCNNLayer* cvCreateCNNFullConnectLayer( 
+    int n_inputs, int n_outputs, float a, float s,
+    float init_learn_rate, int learn_rate_decrease_type, CvMat* weights )
+{
+    CvCNNFullConnectLayer* layer = 0;
+
+    CV_FUNCNAME("cvCreateCNNFullConnectLayer");
+    __BEGIN__;
+
+    if( a <= 0 || s <= 0 || init_learn_rate <= 0)
+        CV_ERROR( CV_StsBadArg, "Incorrect parameters" );
+
+    CV_CALL(layer = (CvCNNFullConnectLayer*)icvCreateCNNLayer( ICV_CNN_FULLCONNECT_LAYER,
+        sizeof(CvCNNFullConnectLayer), n_inputs, 1, 1, n_outputs, 1, 1,
+        init_learn_rate, learn_rate_decrease_type,
+        icvCNNFullConnectRelease, icvCNNFullConnectForward, icvCNNFullConnectBackward ));
+
+    layer->a = a;
+    layer->s = s;
+
+    CV_CALL(layer->exp2ssumWX = cvCreateMat( n_outputs, 1, CV_32FC1 ));
+    cvZero( layer->exp2ssumWX );
+
+    CV_CALL(layer->weights = cvCreateMat( n_outputs, n_inputs+1, CV_32FC1 ));
+    if( weights )
+    {
+        if( !ICV_IS_MAT_OF_TYPE( weights, CV_32FC1 ) )
+            CV_ERROR( CV_StsBadSize, "Type of initial weights matrix must be CV_32FC1" );
+        if( !CV_ARE_SIZES_EQ( weights, layer->weights ) )
+            CV_ERROR( CV_StsBadSize, "Invalid size of initial weights matrix" );
+        CV_CALL(cvCopy( weights, layer->weights ));
+    }
+    else
+    {
+        CvRNG rng = cvRNG( 0xFFFFFFFF );
+        cvRandArr( &rng, layer->weights, CV_RAND_UNI, cvRealScalar(-1), cvRealScalar(1) );
+    }
+
+    __END__;
+
+    if( cvGetErrStatus() < 0 && layer )
+    {
+        cvReleaseMat( &layer->exp2ssumWX );
+        cvReleaseMat( &layer->weights );
+        cvFree( &layer );
+    }
+
+    return (CvCNNLayer*)layer;
+}
+
+
+/****************************************************************************************\
+*                           Layer FORWARD functions                                      *
+\****************************************************************************************/
+static void icvCNNConvolutionForward( CvCNNLayer* _layer,
+                                      const CvMat* X,
+                                      CvMat* Y )
+{
+    CV_FUNCNAME("icvCNNConvolutionForward");
+
+    if( !ICV_IS_CNN_CONVOLUTION_LAYER(_layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    {__BEGIN__;
+
+    const CvCNNConvolutionLayer* layer = (CvCNNConvolutionLayer*) _layer;
+
+    const int K = layer->K;
+    const int n_weights_for_Yplane = K*K + 1;
+
+    const int nXplanes = layer->n_input_planes;
+    const int Xheight  = layer->input_height;
+    const int Xwidth   = layer->input_width ;
+    const int Xsize    = Xwidth*Xheight;
+
+    const int nYplanes = layer->n_output_planes;
+    const int Yheight  = layer->output_height;
+    const int Ywidth   = layer->output_width;
+    const int Ysize    = Ywidth*Yheight;
+
+    int xx, yy, ni, no, kx, ky;
+    float *Yplane = 0, *Xplane = 0, *w = 0;
+    uchar* connect_mask_data = 0;
+
+    CV_ASSERT( X->rows == nXplanes*Xsize && X->cols == 1 );
+    CV_ASSERT( Y->rows == nYplanes*Ysize && Y->cols == 1 );
+
+    cvSetZero( Y );
+
+    Yplane = Y->data.fl;
+    connect_mask_data = layer->connect_mask->data.ptr;
+    w = layer->weights->data.fl;
+    for( no = 0; no < nYplanes; no++, Yplane += Ysize, w += n_weights_for_Yplane )
+    {
+        Xplane = X->data.fl;
+        for( ni = 0; ni < nXplanes; ni++, Xplane += Xsize, connect_mask_data++ )
+        {
+            if( *connect_mask_data )
+            {
+                float* Yelem = Yplane;
+
+                // Xheight-K+1 == Yheight && Xwidth-K+1 == Ywidth
+                for( yy = 0; yy < Xheight-K+1; yy++ )
+                {
+                    for( xx = 0; xx < Xwidth-K+1; xx++, Yelem++ )
+                    {
+                        float* templ = Xplane+yy*Xwidth+xx;
+                        float WX = 0;
+                        for( ky = 0; ky < K; ky++, templ += Xwidth-K )
+                        {
+                            for( kx = 0; kx < K; kx++, templ++ )
+                            {
+                                WX += *templ*w[ky*K+kx];
+                            }
+                        }
+                        *Yelem += WX + w[K*K];
+                    }
+                }
+            }
+        }
+    }
+    }__END__;
+}
+
+/****************************************************************************************/
+static void icvCNNSubSamplingForward( CvCNNLayer* _layer,
+                                      const CvMat* X,
+                                      CvMat* Y )
+{
+    CV_FUNCNAME("icvCNNSubSamplingForward");
+
+    if( !ICV_IS_CNN_SUBSAMPLING_LAYER(_layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    {__BEGIN__;
+
+    const CvCNNSubSamplingLayer* layer = (CvCNNSubSamplingLayer*) _layer;
+
+    const int sub_sampl_scale = layer->sub_samp_scale;
+    const int nplanes = layer->n_input_planes;
+
+    const int Xheight = layer->input_height;
+    const int Xwidth  = layer->input_width ;
+    const int Xsize   = Xwidth*Xheight;
+
+    const int Yheight = layer->output_height;
+    const int Ywidth  = layer->output_width;
+    const int Ysize   = Ywidth*Yheight;
+
+    int xx, yy, ni, kx, ky;
+    float* sumX_data = 0, *w = 0;
+    CvMat sumX_sub_col, exp2ssumWX_sub_col;
+
+    CV_ASSERT(X->rows == nplanes*Xsize && X->cols == 1);
+    CV_ASSERT(layer->exp2ssumWX->cols == 1 && layer->exp2ssumWX->rows == nplanes*Ysize);
+
+    // update inner variable layer->exp2ssumWX, which will be used in back-progation
+    cvZero( layer->sumX );
+    cvZero( layer->exp2ssumWX );
+
+    for( ky = 0; ky < sub_sampl_scale; ky++ )
+        for( kx = 0; kx < sub_sampl_scale; kx++ )
+        {
+            float* Xplane = X->data.fl;
+            sumX_data = layer->sumX->data.fl;
+            for( ni = 0; ni < nplanes; ni++, Xplane += Xsize )
+            {
+                for( yy = 0; yy < Yheight; yy++ )
+                    for( xx = 0; xx < Ywidth; xx++, sumX_data++ )
+                        *sumX_data += Xplane[((yy+ky)*Xwidth+(xx+kx))];
+            }
+        }                
+
+    w = layer->weights->data.fl;
+    cvGetRows( layer->sumX, &sumX_sub_col, 0, Ysize );
+    cvGetRows( layer->exp2ssumWX, &exp2ssumWX_sub_col, 0, Ysize );
+    for( ni = 0; ni < nplanes; ni++, w += 2 )
+    {
+        CV_CALL(cvConvertScale( &sumX_sub_col, &exp2ssumWX_sub_col, w[0], w[1] ));
+        sumX_sub_col.data.fl += Ysize;
+        exp2ssumWX_sub_col.data.fl += Ysize;
+    }
+
+    CV_CALL(cvScale( layer->exp2ssumWX, layer->exp2ssumWX, 2.0*layer->s ));
+    CV_CALL(cvExp( layer->exp2ssumWX, layer->exp2ssumWX ));
+    CV_CALL(cvMinS( layer->exp2ssumWX, FLT_MAX, layer->exp2ssumWX ));
+//#ifdef _DEBUG
+    {
+        float* exp2ssumWX_data = layer->exp2ssumWX->data.fl;
+        for( ni = 0; ni < layer->exp2ssumWX->rows; ni++, exp2ssumWX_data++ )
+        {
+            if( *exp2ssumWX_data == FLT_MAX )
+                cvSetErrStatus( 1 );
+        }
+    }
+//#endif
+    // compute the output variable Y == ( a - 2a/(layer->exp2ssumWX + 1))
+    CV_CALL(cvAddS( layer->exp2ssumWX, cvRealScalar(1), Y ));
+    CV_CALL(cvDiv( 0, Y, Y, -2.0*layer->a ));
+    CV_CALL(cvAddS( Y, cvRealScalar(layer->a), Y ));
+
+    }__END__;
+}
+
+/****************************************************************************************/
+static void icvCNNFullConnectForward( CvCNNLayer* _layer, const CvMat* X, CvMat* Y )
+{
+    CV_FUNCNAME("icvCNNFullConnectForward");
+
+    if( !ICV_IS_CNN_FULLCONNECT_LAYER(_layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    {__BEGIN__;
+
+    const CvCNNFullConnectLayer* layer = (CvCNNFullConnectLayer*)_layer;
+    CvMat* weights = layer->weights;
+    CvMat sub_weights, bias;
+
+    CV_ASSERT(X->cols == 1 && X->rows == layer->n_input_planes);
+    CV_ASSERT(Y->cols == 1 && Y->rows == layer->n_output_planes);
+
+    CV_CALL(cvGetSubRect( weights, &sub_weights,
+                          cvRect(0, 0, weights->cols-1, weights->rows )));
+    CV_CALL(cvGetCol( weights, &bias, weights->cols-1));
+
+    // update inner variable layer->exp2ssumWX, which will be used in Back-Propagation
+    CV_CALL(cvGEMM( &sub_weights, X, 2*layer->s, &bias, 2*layer->s, layer->exp2ssumWX ));
+    CV_CALL(cvExp( layer->exp2ssumWX, layer->exp2ssumWX ));
+    CV_CALL(cvMinS( layer->exp2ssumWX, FLT_MAX, layer->exp2ssumWX ));
+//#ifdef _DEBUG
+    {
+        float* exp2ssumWX_data = layer->exp2ssumWX->data.fl;
+        int i;
+        for( i = 0; i < layer->exp2ssumWX->rows; i++, exp2ssumWX_data++ )
+        {
+            if( *exp2ssumWX_data == FLT_MAX )
+                cvSetErrStatus( 1 );
+        }
+    }
+//#endif
+    // compute the output variable Y == ( a - 2a/(layer->exp2ssumWX + 1))
+    CV_CALL(cvAddS( layer->exp2ssumWX, cvRealScalar(1), Y ));
+    CV_CALL(cvDiv( 0, Y, Y, -2.0*layer->a ));
+    CV_CALL(cvAddS( Y, cvRealScalar(layer->a), Y ));
+
+    }__END__;
+}
+
+/****************************************************************************************\
+*                           Layer BACKWARD functions                                     *
+\****************************************************************************************/
+
+/* <dE_dY>, <dE_dX> should be row-vectors.
+   Function computes partial derivatives <dE_dX>
+   of the loss function with respect to the planes components
+   of the previous layer (X).
+   It is a basic function for back propagation method.
+   Input parameter <dE_dY> is the partial derivative of the
+   loss function with respect to the planes components
+   of the current layer. */
+static void icvCNNConvolutionBackward(
+    CvCNNLayer* _layer, int t, const CvMat* X, const CvMat* dE_dY, CvMat* dE_dX )
+{
+    CvMat* dY_dX = 0;
+    CvMat* dY_dW = 0;
+    CvMat* dE_dW = 0;
+
+    CV_FUNCNAME("icvCNNConvolutionBackward");
+
+    if( !ICV_IS_CNN_CONVOLUTION_LAYER(_layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    {__BEGIN__;
+
+    const CvCNNConvolutionLayer* layer = (CvCNNConvolutionLayer*) _layer;
+
+    const int K = layer->K;
+
+    const int n_X_planes     = layer->n_input_planes;
+    const int X_plane_height = layer->input_height;
+    const int X_plane_width  = layer->input_width;
+    const int X_plane_size   = X_plane_height*X_plane_width;
+
+    const int n_Y_planes     = layer->n_output_planes;
+    const int Y_plane_height = layer->output_height;
+    const int Y_plane_width  = layer->output_width;
+    const int Y_plane_size   = Y_plane_height*Y_plane_width;
+
+    int no, ni, yy, xx, ky, kx;
+    int X_idx = 0, Y_idx = 0;
+
+    float *X_plane = 0, *w = 0;
+
+    CvMat* weights = layer->weights;
+
+    CV_ASSERT( t >= 1 );
+    CV_ASSERT( n_Y_planes == weights->rows );
+
+    dY_dX = cvCreateMat( n_Y_planes*Y_plane_size, X->rows, CV_32FC1 );
+    dY_dW = cvCreateMat( dY_dX->rows, weights->cols*weights->rows, CV_32FC1 );
+    dE_dW = cvCreateMat( 1, dY_dW->cols, CV_32FC1 );
+
+    cvZero( dY_dX );
+    cvZero( dY_dW );
+
+    // compute gradient of the loss function with respect to X and W
+    for( no = 0; no < n_Y_planes; no++, Y_idx += Y_plane_size )
+    {
+        w = weights->data.fl + no*(K*K+1);
+        X_idx = 0;
+        X_plane = X->data.fl;
+        for( ni = 0; ni < n_X_planes; ni++, X_plane += X_plane_size )
+        {
+            if( layer->connect_mask->data.ptr[ni*n_Y_planes+no] )
+            {
+                for( yy = 0; yy < X_plane_height - K + 1; yy++ )
+                {
+                    for( xx = 0; xx < X_plane_width - K + 1; xx++ )
+                    {
+                        for( ky = 0; ky < K; ky++ )
+                        {
+                            for( kx = 0; kx < K; kx++ )
+                            {
+                                CV_MAT_ELEM(*dY_dX, float, Y_idx+yy*Y_plane_width+xx,
+                                    X_idx+(yy+ky)*X_plane_width+(xx+kx)) = w[ky*K+kx];
+
+                                // dY_dWi, i=1,...,K*K
+                                CV_MAT_ELEM(*dY_dW, float, Y_idx+yy*Y_plane_width+xx,
+                                    no*(K*K+1)+ky*K+kx) +=
+                                    X_plane[(yy+ky)*X_plane_width+(xx+kx)];
+                            }
+                        }
+                        // dY_dW(K*K+1)==1 because W(K*K+1) is bias
+                        CV_MAT_ELEM(*dY_dW, float, Y_idx+yy*Y_plane_width+xx,
+                            no*(K*K+1)+K*K) += 1;
+                    }
+                }
+            }
+            X_idx += X_plane_size;
+        }
+    }
+
+    CV_CALL(cvMatMul( dE_dY, dY_dW, dE_dW ));
+    CV_CALL(cvMatMul( dE_dY, dY_dX, dE_dX ));
+
+    // update weights
+    {
+        CvMat dE_dW_mat;
+        float eta;
+        if( layer->learn_rate_decrease_type == CV_CNN_LEARN_RATE_DECREASE_LOG_INV )
+            eta = -layer->init_learn_rate/logf(1+(float)t);
+        else if( layer->learn_rate_decrease_type == CV_CNN_LEARN_RATE_DECREASE_SQRT_INV )
+            eta = -layer->init_learn_rate/sqrtf((float)t);
+        else
+            eta = -layer->init_learn_rate/(float)t;
+        cvReshape( dE_dW, &dE_dW_mat, 0, weights->rows );
+        cvScaleAdd( &dE_dW_mat, cvRealScalar(eta), weights, weights );
+    }
+
+    }__END__;
+
+    cvReleaseMat( &dY_dX );
+    cvReleaseMat( &dY_dW );
+    cvReleaseMat( &dE_dW );
+}
+
+/****************************************************************************************/
+static void icvCNNSubSamplingBackward(
+    CvCNNLayer* _layer, int t, const CvMat*, const CvMat* dE_dY, CvMat* dE_dX )
+{
+    // derivative of activation function
+    CvMat* dY_dX_elems = 0; // elements of matrix dY_dX
+    CvMat* dY_dW_elems = 0; // elements of matrix dY_dW
+    CvMat* dE_dW = 0;
+
+    CV_FUNCNAME("icvCNNSubSamplingBackward");
+
+    if( !ICV_IS_CNN_SUBSAMPLING_LAYER(_layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    {__BEGIN__;
+
+    const CvCNNSubSamplingLayer* layer = (CvCNNSubSamplingLayer*) _layer;
+
+    const int Xwidth  = layer->input_width;
+    const int Ywidth  = layer->output_width;
+    const int Yheight = layer->output_height;
+    const int Ysize   = Ywidth * Yheight;
+    const int scale   = layer->sub_samp_scale;
+    const int k_max   = layer->n_output_planes * Yheight;
+
+    int k, i, j, m;
+    float* dY_dX_current_elem = 0, *dE_dX_start = 0, *dE_dW_data = 0, *w = 0;
+    CvMat dy_dw0, dy_dw1;
+    CvMat activ_func_der, sumX_row;
+    CvMat dE_dY_sub_row, dY_dX_sub_col, dy_dw0_sub_row, dy_dw1_sub_row;
+
+    CV_CALL(dY_dX_elems = cvCreateMat( layer->sumX->rows, 1, CV_32FC1 ));
+    CV_CALL(dY_dW_elems = cvCreateMat( 2, layer->sumX->rows, CV_32FC1 ));
+    CV_CALL(dE_dW = cvCreateMat( 1, 2*layer->n_output_planes, CV_32FC1 ));
+
+    // compute derivative of activ.func.
+    // ==<dY_dX_elems> = 4as*(layer->exp2ssumWX)/(layer->exp2ssumWX + 1)^2
+    CV_CALL(cvAddS( layer->exp2ssumWX, cvRealScalar(1), dY_dX_elems ));
+    CV_CALL(cvPow( dY_dX_elems, dY_dX_elems, -2.0 ));
+    CV_CALL(cvMul( dY_dX_elems, layer->exp2ssumWX, dY_dX_elems, 4.0*layer->a*layer->s ));
+
+    // compute <dE_dW>
+    // a) compute <dY_dW_elems>
+    cvReshape( dY_dX_elems, &activ_func_der, 0, 1 );
+    cvGetRow( dY_dW_elems, &dy_dw0, 0 );
+    cvGetRow( dY_dW_elems, &dy_dw1, 1 );
+    CV_CALL(cvCopy( &activ_func_der, &dy_dw0 ));
+    CV_CALL(cvCopy( &activ_func_der, &dy_dw1 ));
+
+    cvReshape( layer->sumX, &sumX_row, 0, 1 );
+    cvMul( &dy_dw0, &sumX_row, &dy_dw0 );
+
+    // b) compute <dE_dW> = <dE_dY>*<dY_dW_elems>
+    cvGetCols( dE_dY, &dE_dY_sub_row, 0, Ysize );
+    cvGetCols( &dy_dw0, &dy_dw0_sub_row, 0, Ysize );
+    cvGetCols( &dy_dw1, &dy_dw1_sub_row, 0, Ysize );
+    dE_dW_data = dE_dW->data.fl;
+    for( i = 0; i < layer->n_output_planes; i++ )
+    {
+        *dE_dW_data++ = (float)cvDotProduct( &dE_dY_sub_row, &dy_dw0_sub_row );
+        *dE_dW_data++ = (float)cvDotProduct( &dE_dY_sub_row, &dy_dw1_sub_row );
+
+        dE_dY_sub_row.data.fl += Ysize;
+        dy_dw0_sub_row.data.fl += Ysize;
+        dy_dw1_sub_row.data.fl += Ysize;
+    }
+
+    // compute <dY_dX> = layer->weights*<dY_dX>
+    w = layer->weights->data.fl;
+    cvGetRows( dY_dX_elems, &dY_dX_sub_col, 0, Ysize );
+    for( i = 0; i < layer->n_input_planes; i++, w++, dY_dX_sub_col.data.fl += Ysize )
+        CV_CALL(cvConvertScale( &dY_dX_sub_col, &dY_dX_sub_col, (float)*w ));
+
+    // compute <dE_dX>
+    CV_CALL(cvReshape( dY_dX_elems, dY_dX_elems, 0, 1 ));
+    CV_CALL(cvMul( dY_dX_elems, dE_dY, dY_dX_elems ));
+
+    dY_dX_current_elem = dY_dX_elems->data.fl;
+    dE_dX_start = dE_dX->data.fl;
+    for( k = 0; k < k_max; k++ )
+    {
+        for( i = 0; i < Ywidth; i++, dY_dX_current_elem++ )
+        {
+            float* dE_dX_current_elem = dE_dX_start;
+            for( j = 0; j < scale; j++, dE_dX_current_elem += Xwidth - scale )
+            {
+                for( m = 0; m < scale; m++, dE_dX_current_elem++ )
+                    *dE_dX_current_elem = *dY_dX_current_elem;
+            }
+            dE_dX_start += scale;
+        }
+        dE_dX_start += Xwidth * (scale - 1);
+    }
+
+    // update weights
+    {
+        CvMat dE_dW_mat, *weights = layer->weights;
+        float eta;
+        if( layer->learn_rate_decrease_type == CV_CNN_LEARN_RATE_DECREASE_LOG_INV )
+            eta = -layer->init_learn_rate/logf(1+(float)t);
+        else if( layer->learn_rate_decrease_type == CV_CNN_LEARN_RATE_DECREASE_SQRT_INV )
+            eta = -layer->init_learn_rate/sqrtf((float)t);
+        else
+            eta = -layer->init_learn_rate/(float)t;
+        cvReshape( dE_dW, &dE_dW_mat, 0, weights->rows );
+        cvScaleAdd( &dE_dW_mat, cvRealScalar(eta), weights, weights );
+    }
+
+    }__END__;
+
+    cvReleaseMat( &dY_dX_elems );
+    cvReleaseMat( &dY_dW_elems );
+    cvReleaseMat( &dE_dW );
+}
+
+/****************************************************************************************/
+/* <dE_dY>, <dE_dX> should be row-vectors.
+   Function computes partial derivatives <dE_dX>, <dE_dW>
+   of the loss function with respect to the planes components
+   of the previous layer (X) and the weights of the current layer (W)
+   and updates weights od the current layer by using <dE_dW>.
+   It is a basic function for back propagation method.
+   Input parameter <dE_dY> is the partial derivative of the
+   loss function with respect to the planes components
+   of the current layer. */
+static void icvCNNFullConnectBackward( CvCNNLayer* _layer,
+                                    int t,
+                                    const CvMat* X,
+                                    const CvMat* dE_dY,
+                                    CvMat* dE_dX )
+{
+    CvMat* dE_dY_activ_func_der = 0;
+    CvMat* dE_dW = 0;
+    
+    CV_FUNCNAME( "icvCNNFullConnectBackward" );
+
+    if( !ICV_IS_CNN_FULLCONNECT_LAYER(_layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    {__BEGIN__;
+
+    const CvCNNFullConnectLayer* layer = (CvCNNFullConnectLayer*)_layer;
+    const int n_outputs = layer->n_output_planes;
+    const int n_inputs  = layer->n_input_planes;
+
+    int i;
+    float* dE_dY_activ_func_der_data;
+    CvMat* weights = layer->weights;
+    CvMat sub_weights, Xtemplate, Xrow, exp2ssumWXrow;
+
+    CV_ASSERT(X->cols == 1 && X->rows == n_inputs);
+    CV_ASSERT(dE_dY->rows == 1 && dE_dY->cols == n_outputs );
+    CV_ASSERT(dE_dX->rows == 1 && dE_dX->cols == n_inputs );
+    
+    // we violate the convetion about vector's orientation because
+    // here is more convenient to make this parameter a row-vector 
+    CV_CALL(dE_dY_activ_func_der = cvCreateMat( 1, n_outputs, CV_32FC1 ));
+    CV_CALL(dE_dW = cvCreateMat( 1, weights->rows*weights->cols, CV_32FC1 ));
+    
+    // 1) compute gradients dE_dX and dE_dW
+    // activ_func_der == 4as*(layer->exp2ssumWX)/(layer->exp2ssumWX + 1)^2
+    CV_CALL(cvReshape( layer->exp2ssumWX, &exp2ssumWXrow, 0, layer->exp2ssumWX->cols ));
+    CV_CALL(cvAddS( &exp2ssumWXrow, cvRealScalar(1), dE_dY_activ_func_der ));
+    CV_CALL(cvPow( dE_dY_activ_func_der, dE_dY_activ_func_der, -2.0 ));
+    CV_CALL(cvMul( dE_dY_activ_func_der, &exp2ssumWXrow, dE_dY_activ_func_der,
+                   4.0*layer->a*layer->s ));
+    CV_CALL(cvMul( dE_dY, dE_dY_activ_func_der, dE_dY_activ_func_der ));
+
+    // sub_weights = d(W*(X|1))/dX
+    CV_CALL(cvGetSubRect( weights, &sub_weights,
+        cvRect(0, 0, weights->cols-1, weights->rows) ));
+    CV_CALL(cvMatMul( dE_dY_activ_func_der, &sub_weights, dE_dX ));
+
+    cvReshape( X, &Xrow, 0, 1 );
+    dE_dY_activ_func_der_data = dE_dY_activ_func_der->data.fl;
+    Xtemplate = cvMat( 1, n_inputs, CV_32FC1, dE_dW->data.fl );
+    for( i = 0; i < n_outputs; i++, Xtemplate.data.fl += n_inputs + 1 )
+    {
+        CV_CALL(cvConvertScale( &Xrow, &Xtemplate, *dE_dY_activ_func_der_data ));
+        Xtemplate.data.fl[n_inputs] = *dE_dY_activ_func_der_data++;
+    }
+
+    // 2) update weights
+    {
+        CvMat dE_dW_mat;
+        float eta;
+        if( layer->learn_rate_decrease_type == CV_CNN_LEARN_RATE_DECREASE_LOG_INV )
+            eta = -layer->init_learn_rate/logf(1+(float)t);
+        else if( layer->learn_rate_decrease_type == CV_CNN_LEARN_RATE_DECREASE_SQRT_INV )
+            eta = -layer->init_learn_rate/sqrtf((float)t);
+        else
+            eta = -layer->init_learn_rate/(float)t;
+        cvReshape( dE_dW, &dE_dW_mat, 0, n_outputs );
+        cvScaleAdd( &dE_dW_mat, cvRealScalar(eta), weights, weights );
+    }
+
+    }__END__;
+
+    cvReleaseMat( &dE_dY_activ_func_der );
+    cvReleaseMat( &dE_dW );
+}
+
+/****************************************************************************************\
+*                           Layer RELEASE functions                                      *
+\****************************************************************************************/
+static void icvCNNConvolutionRelease( CvCNNLayer** p_layer )
+{
+    CV_FUNCNAME("icvCNNConvolutionRelease");
+    __BEGIN__;
+
+    CvCNNConvolutionLayer* layer = 0;
+
+    if( !p_layer )
+        CV_ERROR( CV_StsNullPtr, "Null double pointer" );
+
+    layer = *(CvCNNConvolutionLayer**)p_layer;
+
+    if( !layer )
+        return;
+    if( !ICV_IS_CNN_CONVOLUTION_LAYER(layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    cvReleaseMat( &layer->weights );
+    cvReleaseMat( &layer->connect_mask );
+    cvFree( p_layer );
+
+    __END__;
+}
+
+/****************************************************************************************/
+static void icvCNNSubSamplingRelease( CvCNNLayer** p_layer )
+{
+    CV_FUNCNAME("icvCNNSubSamplingRelease");
+    __BEGIN__;
+
+    CvCNNSubSamplingLayer* layer = 0;
+    
+    if( !p_layer )
+        CV_ERROR( CV_StsNullPtr, "Null double pointer" );
+
+    layer = *(CvCNNSubSamplingLayer**)p_layer;
+
+    if( !layer )
+        return;
+    if( !ICV_IS_CNN_SUBSAMPLING_LAYER(layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    cvReleaseMat( &layer->exp2ssumWX );
+    cvReleaseMat( &layer->weights );
+    cvFree( p_layer );
+
+    __END__;
+}
+
+/****************************************************************************************/
+static void icvCNNFullConnectRelease( CvCNNLayer** p_layer )
+{
+    CV_FUNCNAME("icvCNNFullConnectRelease");
+    __BEGIN__;
+
+    CvCNNFullConnectLayer* layer = 0;
+    
+    if( !p_layer )
+        CV_ERROR( CV_StsNullPtr, "Null double pointer" );
+
+    layer = *(CvCNNFullConnectLayer**)p_layer;
+
+    if( !layer )
+        return;
+    if( !ICV_IS_CNN_FULLCONNECT_LAYER(layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    cvReleaseMat( &layer->exp2ssumWX );
+    cvReleaseMat( &layer->weights );
+    cvFree( p_layer );
+
+    __END__;
+}
+
+/****************************************************************************************\
+*                              Read/Write CNN classifier                                 *
+\****************************************************************************************/
+static int icvIsCNNModel( const void* ptr )
+{
+    return CV_IS_CNN(ptr);
+}
+
+/****************************************************************************************/
+static void icvReleaseCNNModel( void** ptr )
+{
+    CV_FUNCNAME("icvReleaseCNNModel");
+    __BEGIN__;
+
+    if( !ptr )
+        CV_ERROR( CV_StsNullPtr, "NULL double pointer" );
+    CV_ASSERT(CV_IS_CNN(*ptr));
+
+    icvCNNModelRelease( (CvStatModel**)ptr );
+
+    __END__;
+}
+
+/****************************************************************************************/
+static CvCNNLayer* icvReadCNNLayer( CvFileStorage* fs, CvFileNode* node )
+{
+    CvCNNLayer* layer = 0;
+    CvMat* weights    = 0;
+    CvMat* connect_mask = 0;
+
+    CV_FUNCNAME("icvReadCNNLayer");
+    __BEGIN__;
+
+    int n_input_planes, input_height, input_width;
+    int n_output_planes, output_height, output_width;
+    int learn_type, layer_type;
+    float init_learn_rate;
+
+    CV_CALL(n_input_planes  = cvReadIntByName( fs, node, "n_input_planes",  -1 ));
+    CV_CALL(input_height    = cvReadIntByName( fs, node, "input_height",    -1 ));
+    CV_CALL(input_width     = cvReadIntByName( fs, node, "input_width",     -1 ));
+    CV_CALL(n_output_planes = cvReadIntByName( fs, node, "n_output_planes", -1 ));
+    CV_CALL(output_height   = cvReadIntByName( fs, node, "output_height",   -1 ));
+    CV_CALL(output_width    = cvReadIntByName( fs, node, "output_width",    -1 ));
+    CV_CALL(layer_type      = cvReadIntByName( fs, node, "layer_type",      -1 ));
+
+    CV_CALL(init_learn_rate = (float)cvReadRealByName( fs, node, "init_learn_rate", -1 ));
+    CV_CALL(learn_type = cvReadIntByName( fs, node, "learn_rate_decrease_type", -1 ));
+    CV_CALL(weights    = (CvMat*)cvReadByName( fs, node, "weights" ));
+
+    if( n_input_planes < 0  || input_height < 0  || input_width < 0 ||
+        n_output_planes < 0 || output_height < 0 || output_width < 0 ||
+        init_learn_rate < 0 || learn_type < 0 || layer_type < 0 || !weights )
+        CV_ERROR( CV_StsParseError, "" );
+
+    if( layer_type == ICV_CNN_CONVOLUTION_LAYER )
+    {
+        const int K = input_height - output_height + 1;
+        if( K <= 0 || K != input_width - output_width + 1 )
+            CV_ERROR( CV_StsBadArg, "Invalid <K>" );
+
+        CV_CALL(connect_mask = (CvMat*)cvReadByName( fs, node, "connect_mask" ));
+        if( !connect_mask )
+            CV_ERROR( CV_StsParseError, "Missing <connect mask>" );
+
+        CV_CALL(layer = cvCreateCNNConvolutionLayer( 
+            n_input_planes, input_height, input_width, n_output_planes, K,
+            init_learn_rate, learn_type, connect_mask, weights ));
+    }
+    else if( layer_type == ICV_CNN_SUBSAMPLING_LAYER )
+    {
+        float a, s;
+        const int sub_samp_scale = input_height/output_height;
+
+        if( sub_samp_scale <= 0 || sub_samp_scale != input_width/output_width )
+            CV_ERROR( CV_StsBadArg, "Invalid <sub_samp_scale>" );
+
+        CV_CALL(a = (float)cvReadRealByName( fs, node, "a", -1 ));
+        CV_CALL(s = (float)cvReadRealByName( fs, node, "s", -1 ));
+        if( a  < 0 || s  < 0 )
+            CV_ERROR( CV_StsParseError, "Missing <a> or <s>" );
+
+        CV_CALL(layer = cvCreateCNNSubSamplingLayer(
+            n_input_planes, input_height, input_width, sub_samp_scale,
+            a, s, init_learn_rate, learn_type, weights ));
+    }
+    else if( layer_type == ICV_CNN_FULLCONNECT_LAYER )
+    {
+        float a, s;
+        CV_CALL(a = (float)cvReadRealByName( fs, node, "a", -1 ));
+        CV_CALL(s = (float)cvReadRealByName( fs, node, "s", -1 ));
+        if( a  < 0 || s  < 0 )
+            CV_ERROR( CV_StsParseError, "" );
+        if( input_height != 1  || input_width != 1 ||
+            output_height != 1 || output_width != 1 )
+            CV_ERROR( CV_StsBadArg, "" );
+
+        CV_CALL(layer = cvCreateCNNFullConnectLayer( n_input_planes, n_output_planes,
+            a, s, init_learn_rate, learn_type, weights ));
+    }
+    else
+        CV_ERROR( CV_StsBadArg, "Invalid <layer_type>" );
+
+    __END__;
+
+    if( cvGetErrStatus() < 0 && layer )
+        layer->release( &layer );
+
+    cvReleaseMat( &weights );
+    cvReleaseMat( &connect_mask );
+
+    return layer;
+}
+
+/****************************************************************************************/
+static void icvWriteCNNLayer( CvFileStorage* fs, CvCNNLayer* layer )
+{
+    CV_FUNCNAME ("icvWriteCNNLayer");
+    __BEGIN__;
+
+    if( !ICV_IS_CNN_LAYER(layer) )
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    CV_CALL( cvStartWriteStruct( fs, NULL, CV_NODE_MAP, "opencv-ml-cnn-layer" ));
+
+    CV_CALL(cvWriteInt( fs, "n_input_planes",  layer->n_input_planes ));
+    CV_CALL(cvWriteInt( fs, "input_height",    layer->input_height ));
+    CV_CALL(cvWriteInt( fs, "input_width",     layer->input_width ));
+    CV_CALL(cvWriteInt( fs, "n_output_planes", layer->n_output_planes ));
+    CV_CALL(cvWriteInt( fs, "output_height",   layer->output_height ));
+    CV_CALL(cvWriteInt( fs, "output_width",    layer->output_width ));
+    CV_CALL(cvWriteInt( fs, "learn_rate_decrease_type", layer->learn_rate_decrease_type));
+    CV_CALL(cvWriteReal( fs, "init_learn_rate", layer->init_learn_rate ));
+    CV_CALL(cvWrite( fs, "weights", layer->weights ));
+
+    if( ICV_IS_CNN_CONVOLUTION_LAYER( layer ))
+    {
+        CvCNNConvolutionLayer* l = (CvCNNConvolutionLayer*)layer;
+        CV_CALL(cvWriteInt( fs, "layer_type", ICV_CNN_CONVOLUTION_LAYER ));
+        CV_CALL(cvWrite( fs, "connect_mask", l->connect_mask ));
+    }
+    else if( ICV_IS_CNN_SUBSAMPLING_LAYER( layer ) )
+    {
+        CvCNNSubSamplingLayer* l = (CvCNNSubSamplingLayer*)layer;
+        CV_CALL(cvWriteInt( fs, "layer_type", ICV_CNN_SUBSAMPLING_LAYER ));
+        CV_CALL(cvWriteReal( fs, "a", l->a ));
+        CV_CALL(cvWriteReal( fs, "s", l->s ));
+    }
+    else if( ICV_IS_CNN_FULLCONNECT_LAYER( layer ) )
+    {
+        CvCNNFullConnectLayer* l = (CvCNNFullConnectLayer*)layer;
+        CV_CALL(cvWriteInt( fs, "layer_type", ICV_CNN_FULLCONNECT_LAYER ));
+        CV_CALL(cvWriteReal( fs, "a", l->a ));
+        CV_CALL(cvWriteReal( fs, "s", l->s ));
+    }
+    else
+        CV_ERROR( CV_StsBadArg, "Invalid layer" );
+
+    CV_CALL( cvEndWriteStruct( fs )); //"opencv-ml-cnn-layer"
+
+    __END__;
+}
+
+/****************************************************************************************/
+static void* icvReadCNNModel( CvFileStorage* fs, CvFileNode* root_node )
+{
+    CvCNNStatModel* cnn = 0;
+    CvCNNLayer* layer = 0;
+
+    CV_FUNCNAME("icvReadCNNModel");
+    __BEGIN__;
+
+    CvFileNode* node;
+    CvSeq* seq;
+    CvSeqReader reader;
+    int i;
+
+    CV_CALL(cnn = (CvCNNStatModel*)cvCreateStatModel(
+        CV_STAT_MODEL_MAGIC_VAL|CV_CNN_MAGIC_VAL, sizeof(CvCNNStatModel),
+        icvCNNModelRelease, icvCNNModelPredict, icvCNNModelUpdate ));
+
+    CV_CALL(cnn->etalons = (CvMat*)cvReadByName( fs, root_node, "etalons" ));
+    CV_CALL(cnn->cls_labels = (CvMat*)cvReadByName( fs, root_node, "cls_labels" ));
+
+    if( !cnn->etalons || !cnn->cls_labels )
+        CV_ERROR( CV_StsParseError, "No <etalons> or <cls_labels> in CNN model" );
+
+    CV_CALL( node = cvGetFileNodeByName( fs, root_node, "network" ));
+    seq = node->data.seq;
+    if( !CV_NODE_IS_SEQ(node->tag) )
+        CV_ERROR( CV_StsBadArg, "" );
+
+    CV_CALL( cvStartReadSeq( seq, &reader, 0 ));
+    CV_CALL(layer = icvReadCNNLayer( fs, (CvFileNode*)reader.ptr ));
+    CV_CALL(cnn->network = cvCreateCNNetwork( layer ));
+
+    for( i = 1; i < seq->total; i++ )
+    {
+        CV_NEXT_SEQ_ELEM( seq->elem_size, reader );
+        CV_CALL(layer = icvReadCNNLayer( fs, (CvFileNode*)reader.ptr ));
+        CV_CALL(cnn->network->add_layer( cnn->network, layer ));
+    }
+
+    __END__;
+
+    if( cvGetErrStatus() < 0 )
+    {
+        if( cnn ) cnn->release( (CvStatModel**)&cnn );
+        if( layer ) layer->release( &layer );
+    }
+    return (void*)cnn;
+}
+
+/****************************************************************************************/
+static void
+icvWriteCNNModel( CvFileStorage* fs, const char* name,
+                  const void* struct_ptr, CvAttrList )
+                                   
+{
+    CV_FUNCNAME ("icvWriteCNNModel");
+    __BEGIN__;
+
+    CvCNNStatModel* cnn = (CvCNNStatModel*)struct_ptr;
+    int n_layers, i;
+    CvCNNLayer* layer;
+
+    if( !CV_IS_CNN(cnn) )
+        CV_ERROR( CV_StsBadArg, "Invalid pointer" );
+
+    n_layers = cnn->network->n_layers;
+    
+    CV_CALL( cvStartWriteStruct( fs, name, CV_NODE_MAP, CV_TYPE_NAME_ML_CNN ));
+
+    CV_CALL(cvWrite( fs, "etalons", cnn->etalons ));
+    CV_CALL(cvWrite( fs, "cls_labels", cnn->cls_labels ));
+    
+    CV_CALL( cvStartWriteStruct( fs, "network", CV_NODE_SEQ ));
+
+    layer = cnn->network->layers;
+    for( i = 0; i < n_layers && layer; i++, layer = layer->next_layer )
+        CV_CALL(icvWriteCNNLayer( fs, layer ));
+    if( i < n_layers || layer )
+        CV_ERROR( CV_StsBadArg, "Invalid network" );
+
+    CV_CALL( cvEndWriteStruct( fs )); //"network"
+    CV_CALL( cvEndWriteStruct( fs )); //"opencv-ml-cnn"
+
+    __END__;
+}
+
+static int icvRegisterCNNStatModelType()
+{
+    CvTypeInfo info;
+
+    info.header_size = sizeof( info );
+    info.is_instance = icvIsCNNModel;
+    info.release = icvReleaseCNNModel;
+    info.read = icvReadCNNModel;
+    info.write = icvWriteCNNModel;
+    info.clone = NULL;
+    info.type_name = CV_TYPE_NAME_ML_CNN;
+    cvRegisterType( &info );
+
+    return 1;
+} // End of icvRegisterCNNStatModelType
+
+static int cnn = icvRegisterCNNStatModelType();
+
+#endif
+
+// End of file