/*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.
//
//
//                           License Agreement
//                For Open Source Computer Vision Library
//
// Copyright (C) 2000-2008, Intel Corporation, all rights reserved.
// Copyright (C) 2009, Willow Garage Inc., 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 the copyright holders 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 "precomp.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/ml.hpp"
#include <limits>
#include <fstream>
#include <queue>

#if defined _MSC_VER && _MSC_VER == 1500
    typedef int int_fast32_t;
#else
    #ifndef INT32_MAX
    #ifndef __STDC_LIMIT_MACROS
    #define __STDC_LIMIT_MACROS
    #endif
    #include <stdint.h>
    #endif
#endif

namespace cv
{
namespace text
{

using namespace cv::ml;
using namespace std;
using namespace cv::ml;

// Deletes a tree of ERStat regions starting at root. Used only
// internally to this implementation.
static void deleteERStatTree(ERStat* root) {
    queue<ERStat*> to_delete;
    to_delete.push(root);
    while (!to_delete.empty()) {
        ERStat* n = to_delete.front();
        to_delete.pop();
        ERStat* c = n->child;
        if (c != NULL) {
            to_delete.push(c);
            ERStat* sibling = c->next;
            while (sibling != NULL) {
                to_delete.push(sibling);
                sibling = sibling->next;
            }
        }
        delete n;
    }
}

ERStat::ERStat(int init_level, int init_pixel, int init_x, int init_y) : pixel(init_pixel),
               level(init_level), area(0), perimeter(0), euler(0), probability(1.0),
               parent(0), child(0), next(0), prev(0), local_maxima(0),
               max_probability_ancestor(0), min_probability_ancestor(0)
{
    rect = Rect(init_x,init_y,1,1);
    raw_moments[0] = 0.0;
    raw_moments[1] = 0.0;
    central_moments[0] = 0.0;
    central_moments[1] = 0.0;
    central_moments[2] = 0.0;
    crossings = makePtr<deque<int> >();
    crossings->push_back(0);
}


// derivative classes


// the classes implementing the interface for the 1st and 2nd stages of Neumann and Matas algorithm
class CV_EXPORTS ERFilterNM : public ERFilter
{
public:
    //Constructor
    ERFilterNM();
    //Destructor
    ~ERFilterNM() CV_OVERRIDE {}

    float minProbability;
    bool  nonMaxSuppression;
    float minProbabilityDiff;

    // the key method. Takes image on input, vector of ERStat is output for the first stage,
    // input/output - for the second one.
    void run( InputArray image, vector<ERStat>& regions ) CV_OVERRIDE;

protected:
    int thresholdDelta;
    float maxArea;
    float minArea;

    Ptr<ERFilter::Callback> classifier;

    // count of the rejected/accepted regions
    int num_rejected_regions;
    int num_accepted_regions;

public:

    // set/get methods to set the algorithm properties,
    void setCallback(const Ptr<ERFilter::Callback>& cb) CV_OVERRIDE;
    void setThresholdDelta(int thresholdDelta) CV_OVERRIDE;
    void setMinArea(float minArea) CV_OVERRIDE;
    void setMaxArea(float maxArea) CV_OVERRIDE;
    void setMinProbability(float minProbability) CV_OVERRIDE;
    void setMinProbabilityDiff(float minProbabilityDiff) CV_OVERRIDE;
    void setNonMaxSuppression(bool nonMaxSuppression) CV_OVERRIDE;
    int  getNumRejected() CV_OVERRIDE;

private:
    // pointer to the input/output regions vector
    vector<ERStat> *regions;
    // image mask used for feature calculations
    Mat region_mask;

    // extract the component tree and store all the ER regions
    void er_tree_extract( InputArray image );
    // accumulate a pixel into an ER
    void er_add_pixel( ERStat *parent, int x, int y, int non_boundary_neighbours,
                       int non_boundary_neighbours_horiz,
                       int d_C1, int d_C2, int d_C3 );
    // merge an ER with its nested parent
    void er_merge( ERStat *parent, ERStat *child );
    // copy extracted regions into the output vector
    ERStat* er_save( ERStat *er, ERStat *parent, ERStat *prev );
    // recursively walk the tree and filter (remove) regions using the callback classifier
    ERStat* er_tree_filter( InputArray image, ERStat *stat, ERStat *parent, ERStat *prev );
    // recursively walk the tree selecting only regions with local maxima probability
    ERStat* er_tree_nonmax_suppression( ERStat *er, ERStat *parent, ERStat *prev );
};


// default 1st stage classifier
class CV_EXPORTS ERClassifierNM1 CV_FINAL : public ERFilter::Callback
{
public:
    //Constructor
    ERClassifierNM1(const string& filename);
    // Destructor
    ~ERClassifierNM1() CV_OVERRIDE {}

    // The classifier must return probability measure for the region.
    double eval(const ERStat& stat) CV_OVERRIDE;

private:
    Ptr<Boost> boost;
};

// default 2nd stage classifier
class CV_EXPORTS ERClassifierNM2 CV_FINAL : public ERFilter::Callback
{
public:
    //constructor
    ERClassifierNM2(const string& filename);
    // Destructor
    ~ERClassifierNM2() CV_OVERRIDE {}

    // The classifier must return probability measure for the region.
    double eval(const ERStat& stat) CV_OVERRIDE;

private:
    Ptr<Boost> boost;
};





// default constructor
ERFilterNM::ERFilterNM()
{
    thresholdDelta = 1;
    minArea = 0.;
    maxArea = 1.;
    minProbability = 0.;
    nonMaxSuppression = false;
    minProbabilityDiff = 1.;
    num_accepted_regions = 0;
    num_rejected_regions = 0;
}

// the key method. Takes image on input, vector of ERStat is output for the first stage,
// input/output for the second one.
void ERFilterNM::run( InputArray image, vector<ERStat>& _regions )
{

    // assert correct image type
    CV_Assert( image.getMat().type() == CV_8UC1 );

    regions = &_regions;
    region_mask = Mat::zeros(image.getMat().rows+2, image.getMat().cols+2, CV_8UC1);

    // if regions vector is empty we must extract the entire component tree
    if ( regions->size() == 0 )
    {
        er_tree_extract( image );
        if (nonMaxSuppression)
        {
            vector<ERStat> aux_regions;
            regions->swap(aux_regions);
            regions->reserve(aux_regions.size());
            er_tree_nonmax_suppression( &aux_regions.front(), NULL, NULL );
            aux_regions.clear();
        }
    }
    else // if regions vector is already filled we'll just filter the current regions
    {
        // the tree root must have no parent
        CV_Assert( regions->front().parent == NULL );

        vector<ERStat> aux_regions;
        regions->swap(aux_regions);
        regions->reserve(aux_regions.size());
        er_tree_filter( image, &aux_regions.front(), NULL, NULL );
        aux_regions.clear();
    }
}

// extract the component tree and store all the ER regions
// uses the algorithm described in
// Linear time maximally stable extremal regions, D Nistér, H Stewénius – ECCV 2008
void ERFilterNM::er_tree_extract( InputArray image )
{

    Mat src = image.getMat();
    // assert correct image type
    CV_Assert( src.type() == CV_8UC1 );

    if (thresholdDelta > 1)
    {
        src = (src / thresholdDelta) -1;
    }

    const unsigned char * image_data = src.data;
    int width = src.cols, height = src.rows;

    // the component stack
    vector<ERStat*> er_stack;

    // the quads for Euler's number calculation
    // quads[2][2] and quads[2][3] are never used.
    // The four lowest bits in each quads[i][j] correspond to the 2x2 binary patterns
    // Q_1, Q_2, Q_3 in the Neumann and Matas CVPR 2012 paper
    // (see in page 4 at the end of first column).
    // Q_1 and Q_2 have four patterns, while Q_3 has only two.
    const int quads[3][4] =
    {
        { 1<<3                 ,          1<<2          ,                 1<<1   ,                       1<<0 },
        {     (1<<2)|(1<<1)|(1),   (1<<3)|    (1<<1)|(1),   (1<<3)|(1<<2)|    (1),   (1<<3)|(1<<2)|(1<<1)     },
        {     (1<<2)|(1<<1)    ,   (1<<3)|           (1),            /*unused*/-1,               /*unused*/-1 }
    };

    // masks to know if a pixel is accessible and if it has been already added to some region
    vector<bool> accessible_pixel_mask(width * height);
    vector<bool> accumulated_pixel_mask(width * height);

    // heap of boundary pixels
    vector<int> boundary_pixes[256];
    vector<int> boundary_edges[256];

    // add a dummy-component before start
    er_stack.push_back(new ERStat);

    // we'll look initially for all pixels with grey-level lower than a grey-level higher than any allowed in the image
    int threshold_level = (255/thresholdDelta)+1;

    // starting from the first pixel (0,0)
    int current_pixel = 0;
    int current_edge = 0;
    int current_level = image_data[0];
    accessible_pixel_mask[0] = true;

    bool push_new_component = true;

    for (;;) {

        int x = current_pixel % width;
        int y = current_pixel / width;

        // push a component with current level in the component stack
        if (push_new_component)
            er_stack.push_back(new ERStat(current_level, current_pixel, x, y));
        push_new_component = false;

        // explore the (remaining) edges to the neighbors to the current pixel
        for ( ; current_edge < 4; current_edge++)
        {

            int neighbour_pixel = current_pixel;

            switch (current_edge)
            {
                    case 0: if (x < width - 1) neighbour_pixel = current_pixel + 1;  break;
                    case 1: if (y < height - 1) neighbour_pixel = current_pixel + width; break;
                    case 2: if (x > 0) neighbour_pixel = current_pixel - 1; break;
                    default: if (y > 0) neighbour_pixel = current_pixel - width; break;
            }

            // if neighbour is not accessible, mark it accessible and retrieve its grey-level value
            if ( !accessible_pixel_mask[neighbour_pixel] && (neighbour_pixel != current_pixel) )
            {

                int neighbour_level = image_data[neighbour_pixel];
                accessible_pixel_mask[neighbour_pixel] = true;

                // if neighbour level is not lower than current level add neighbour to the boundary heap
                if (neighbour_level >= current_level)
                {

                    boundary_pixes[neighbour_level].push_back(neighbour_pixel);
                    boundary_edges[neighbour_level].push_back(0);

                    // if neighbour level is lower than our threshold_level set threshold_level to neighbour level
                    if (neighbour_level < threshold_level)
                        threshold_level = neighbour_level;

                }
                else // if neighbour level is lower than current add current_pixel (and next edge)
                     // to the boundary heap for later processing
                {

                    boundary_pixes[current_level].push_back(current_pixel);
                    boundary_edges[current_level].push_back(current_edge + 1);

                    // if neighbour level is lower than threshold_level set threshold_level to neighbour level
                    if (current_level < threshold_level)
                        threshold_level = current_level;

                    // consider the new pixel and its grey-level as current pixel
                    current_pixel = neighbour_pixel;
                    current_edge = 0;
                    current_level = neighbour_level;

                    // and push a new component
                    push_new_component = true;
                    break;
                }
            }

        } // else neighbour was already accessible

        if (push_new_component) continue;


        // once here we can add the current pixel to the component at the top of the stack
        // but first we find how many of its neighbours are part of the region boundary (needed for
        // perimeter and crossings calc.) and the increment in quads counts for Euler's number calc.
        int non_boundary_neighbours = 0;
        int non_boundary_neighbours_horiz = 0;

        int quad_before[4] = {0,0,0,0};
        int quad_after[4] = {0,0,0,0};
        quad_after[0] = 1<<1;
        quad_after[1] = 1<<3;
        quad_after[2] = 1<<2;
        quad_after[3] = 1;

        for (int edge = 0; edge < 8; edge++)
        {
            int neighbour4 = -1;
            int neighbour8 = -1;
            int cell = 0;
            switch (edge)
            {
                    case 0: if (x < width - 1) { neighbour4 = neighbour8 = current_pixel + 1;} cell = 5; break;
                    case 1: if ((x < width - 1)&&(y < height - 1)) { neighbour8 = current_pixel + 1 + width;} cell = 8; break;
                    case 2: if (y < height - 1) { neighbour4 = neighbour8 = current_pixel + width;} cell = 7; break;
                    case 3: if ((x > 0)&&(y < height - 1)) { neighbour8 = current_pixel - 1 + width;} cell = 6; break;
                    case 4: if (x > 0) { neighbour4 = neighbour8 = current_pixel - 1;} cell = 3; break;
                    case 5: if ((x > 0)&&(y > 0)) { neighbour8 = current_pixel - 1 - width;} cell = 0; break;
                    case 6: if (y > 0) { neighbour4 = neighbour8 = current_pixel - width;} cell = 1; break;
                    default: if ((x < width - 1)&&(y > 0)) { neighbour8 = current_pixel + 1 - width;} cell = 2; break;
            }
            if ((neighbour4 != -1)&&(accumulated_pixel_mask[neighbour4])&&(image_data[neighbour4]<=image_data[current_pixel]))
            {
                non_boundary_neighbours++;
                if ((edge == 0) || (edge == 4))
                    non_boundary_neighbours_horiz++;
            }

            int pix_value = image_data[current_pixel] + 1;
            if (neighbour8 != -1)
            {
                if (accumulated_pixel_mask[neighbour8])
                    pix_value = image_data[neighbour8];
            }

            if (pix_value<=image_data[current_pixel])
            {
                switch(cell)
                {
                    case 0:
                        quad_before[3] = quad_before[3] | (1<<3);
                        quad_after[3]  = quad_after[3]  | (1<<3);
                        break;
                    case 1:
                        quad_before[3] = quad_before[3] | (1<<2);
                        quad_after[3]  = quad_after[3]  | (1<<2);
                        quad_before[0] = quad_before[0] | (1<<3);
                        quad_after[0]  = quad_after[0]  | (1<<3);
                        break;
                    case 2:
                        quad_before[0] = quad_before[0] | (1<<2);
                        quad_after[0]  = quad_after[0]  | (1<<2);
                        break;
                    case 3:
                        quad_before[3] = quad_before[3] | (1<<1);
                        quad_after[3]  = quad_after[3]  | (1<<1);
                        quad_before[2] = quad_before[2] | (1<<3);
                        quad_after[2]  = quad_after[2]  | (1<<3);
                        break;
                    case 5:
                        quad_before[0] = quad_before[0] | (1);
                        quad_after[0]  = quad_after[0]  | (1);
                        quad_before[1] = quad_before[1] | (1<<2);
                        quad_after[1]  = quad_after[1]  | (1<<2);
                        break;
                    case 6:
                        quad_before[2] = quad_before[2] | (1<<1);
                        quad_after[2]  = quad_after[2]  | (1<<1);
                        break;
                    case 7:
                        quad_before[2] = quad_before[2] | (1);
                        quad_after[2]  = quad_after[2]  | (1);
                        quad_before[1] = quad_before[1] | (1<<1);
                        quad_after[1]  = quad_after[1]  | (1<<1);
                        break;
                    default:
                        quad_before[1] = quad_before[1] | (1);
                        quad_after[1]  = quad_after[1]  | (1);
                        break;
                }
            }

        }

        int C_before[3] = {0, 0, 0};
        int C_after[3] = {0, 0, 0};

        for (int p=0; p<3; p++)
        {
            for (int q=0; q<4; q++)
            {
                if ( (quad_before[0] == quads[p][q]) && ((p<2)||(q<2)) )
                    C_before[p]++;
                if ( (quad_before[1] == quads[p][q]) && ((p<2)||(q<2)) )
                    C_before[p]++;
                if ( (quad_before[2] == quads[p][q]) && ((p<2)||(q<2)) )
                    C_before[p]++;
                if ( (quad_before[3] == quads[p][q]) && ((p<2)||(q<2)) )
                    C_before[p]++;

                if ( (quad_after[0] == quads[p][q]) && ((p<2)||(q<2)) )
                    C_after[p]++;
                if ( (quad_after[1] == quads[p][q]) && ((p<2)||(q<2)) )
                    C_after[p]++;
                if ( (quad_after[2] == quads[p][q]) && ((p<2)||(q<2)) )
                    C_after[p]++;
                if ( (quad_after[3] == quads[p][q]) && ((p<2)||(q<2)) )
                    C_after[p]++;
            }
        }

        int d_C1 = C_after[0]-C_before[0];
        int d_C2 = C_after[1]-C_before[1];
        int d_C3 = C_after[2]-C_before[2];

        er_add_pixel(er_stack.back(), x, y, non_boundary_neighbours, non_boundary_neighbours_horiz, d_C1, d_C2, d_C3);
        accumulated_pixel_mask[current_pixel] = true;

        // if we have processed all the possible threshold levels (the hea is empty) we are done!
        if (threshold_level == (255/thresholdDelta)+1)
        {

            // save the extracted regions into the output vector
            regions->reserve(num_accepted_regions+1);
            er_save(er_stack.back(), NULL, NULL);

            // clean memory
            for (size_t r=0; r<er_stack.size(); r++)
            {
                ERStat *stat = er_stack.at(r);
                if (stat->crossings)
                {
                    stat->crossings.release();
                }
                deleteERStatTree(stat);
            }
            er_stack.clear();

            return;
        }


        // pop the heap of boundary pixels
        current_pixel = boundary_pixes[threshold_level].back();
        boundary_pixes[threshold_level].erase(boundary_pixes[threshold_level].end()-1);
        current_edge  = boundary_edges[threshold_level].back();
        boundary_edges[threshold_level].erase(boundary_edges[threshold_level].end()-1);

        for (; threshold_level < (255/thresholdDelta)+1; threshold_level++)
            if (!boundary_pixes[threshold_level].empty())
                break;

        int new_level = image_data[current_pixel];

        // if the new pixel has higher grey value than the current one
        if (new_level != current_level) {

            current_level = new_level;

            // process components on the top of the stack until we reach the higher grey-level
            while (er_stack.back()->level < new_level)
            {
                ERStat* er = er_stack.back();
                er_stack.erase(er_stack.end()-1);

                if (new_level < er_stack.back()->level)
                {
                    er_stack.push_back(new ERStat(new_level, current_pixel, current_pixel%width, current_pixel/width));
                    er_merge(er_stack.back(), er);
                    break;
                }

                er_merge(er_stack.back(), er);
            }

        }

    }
}

// accumulate a pixel into an ER
void ERFilterNM::er_add_pixel(ERStat *parent, int x, int y, int non_border_neighbours,
                                                            int non_border_neighbours_horiz,
                                                            int d_C1, int d_C2, int d_C3)
{
    parent->area++;
    parent->perimeter += 4 - 2*non_border_neighbours;

    if (parent->crossings->size()>0)
    {
        if (y<parent->rect.y) parent->crossings->push_front(2);
        else if (y>parent->rect.br().y-1) parent->crossings->push_back(2);
        else {
            parent->crossings->at(y - parent->rect.y) += 2-2*non_border_neighbours_horiz;
        }
    } else {
        parent->crossings->push_back(2);
    }

    parent->euler += (d_C1 - d_C2 + 2*d_C3) / 4;

    int new_x1 = min(parent->rect.x,x);
    int new_y1 = min(parent->rect.y,y);
    int new_x2 = max(parent->rect.br().x-1,x);
    int new_y2 = max(parent->rect.br().y-1,y);
    parent->rect.x = new_x1;
    parent->rect.y = new_y1;
    parent->rect.width  = new_x2-new_x1+1;
    parent->rect.height = new_y2-new_y1+1;

    parent->raw_moments[0] += x;
    parent->raw_moments[1] += y;

    parent->central_moments[0] += x * x;
    parent->central_moments[1] += x * y;
    parent->central_moments[2] += y * y;
}

// merge an ER with its nested parent
void ERFilterNM::er_merge(ERStat *parent, ERStat *child)
{

    parent->area += child->area;

    parent->perimeter += child->perimeter;


    for (int i=parent->rect.y; i<=min(parent->rect.br().y-1,child->rect.br().y-1); i++)
        if (i-child->rect.y >= 0)
            parent->crossings->at(i-parent->rect.y) += child->crossings->at(i-child->rect.y);

    for (int i=parent->rect.y-1; i>=child->rect.y; i--)
        if (i-child->rect.y < (int)child->crossings->size())
            parent->crossings->push_front(child->crossings->at(i-child->rect.y));
        else
            parent->crossings->push_front(0);

    for (int i=parent->rect.br().y; i<child->rect.y; i++)
        parent->crossings->push_back(0);

    for (int i=max(parent->rect.br().y,child->rect.y); i<=child->rect.br().y-1; i++)
        parent->crossings->push_back(child->crossings->at(i-child->rect.y));

    parent->euler += child->euler;

    int new_x1 = min(parent->rect.x,child->rect.x);
    int new_y1 = min(parent->rect.y,child->rect.y);
    int new_x2 = max(parent->rect.br().x-1,child->rect.br().x-1);
    int new_y2 = max(parent->rect.br().y-1,child->rect.br().y-1);
    parent->rect.x = new_x1;
    parent->rect.y = new_y1;
    parent->rect.width  = new_x2-new_x1+1;
    parent->rect.height = new_y2-new_y1+1;

    parent->raw_moments[0] += child->raw_moments[0];
    parent->raw_moments[1] += child->raw_moments[1];

    parent->central_moments[0] += child->central_moments[0];
    parent->central_moments[1] += child->central_moments[1];
    parent->central_moments[2] += child->central_moments[2];

    vector<int> m_crossings;
    m_crossings.push_back(child->crossings->at((int)(child->rect.height)/6));
    m_crossings.push_back(child->crossings->at((int)3*(child->rect.height)/6));
    m_crossings.push_back(child->crossings->at((int)5*(child->rect.height)/6));
    sort(m_crossings.begin(), m_crossings.end());
    child->med_crossings = (float)m_crossings.at(1);

    // free unnecessary mem
    child->crossings.release();

    // recover the original grey-level
    child->level = child->level*thresholdDelta;

    // before saving calculate P(child|character) and filter if possible
    if (classifier != NULL)
    {
        child->probability = classifier->eval(*child);
    }

    if ( (((classifier!=NULL)?(child->probability >= minProbability):true)||(nonMaxSuppression)) &&
         ((child->area >= (minArea*region_mask.rows*region_mask.cols)) &&
          (child->area <= (maxArea*region_mask.rows*region_mask.cols)) &&
          (child->rect.width > 2) && (child->rect.height > 2)) )
    {

        num_accepted_regions++;

        child->next = parent->child;
        if (parent->child)
            parent->child->prev = child;
        parent->child = child;
        child->parent = parent;

    } else {

        num_rejected_regions++;

        if (child->prev !=NULL)
            child->prev->next = child->next;

        ERStat *new_child = child->child;
        if (new_child != NULL)
        {
            while (new_child->next != NULL)
                new_child = new_child->next;
            new_child->next = parent->child;
            if (parent->child)
                parent->child->prev = new_child;
            parent->child   = child->child;
            child->child->parent = parent;
        }

        // free mem
        if(child->crossings)
        {
            child->crossings.release();
        }
        delete(child);
    }

}

// copy extracted regions into the output vector
ERStat* ERFilterNM::er_save( ERStat *er, ERStat *parent, ERStat *prev )
{

    regions->push_back(*er);

    regions->back().parent = parent;
    if (prev != NULL)
    {
      prev->next = &(regions->back());
    }
    else if (parent != NULL)
      parent->child = &(regions->back());

    ERStat *old_prev = NULL;
    ERStat *this_er  = &regions->back();

    if (this_er->parent == NULL)
    {
       this_er->probability = 0;
    }

    if (nonMaxSuppression)
    {
        if (this_er->parent == NULL)
        {
            this_er->max_probability_ancestor = this_er;
            this_er->min_probability_ancestor = this_er;
        }
        else
        {
            this_er->max_probability_ancestor = (this_er->probability > parent->max_probability_ancestor->probability)? this_er :  parent->max_probability_ancestor;

            this_er->min_probability_ancestor = (this_er->probability < parent->min_probability_ancestor->probability)? this_er :  parent->min_probability_ancestor;

            if ( (this_er->max_probability_ancestor->probability > minProbability) && (this_er->max_probability_ancestor->probability - this_er->min_probability_ancestor->probability > minProbabilityDiff))
            {
              this_er->max_probability_ancestor->local_maxima = true;
              if ((this_er->max_probability_ancestor == this_er) && (this_er->parent->local_maxima))
              {
                this_er->parent->local_maxima = false;
              }
            }
            else if (this_er->probability < this_er->parent->probability)
            {
              this_er->min_probability_ancestor = this_er;
            }
            else if (this_er->probability > this_er->parent->probability)
            {
              this_er->max_probability_ancestor = this_er;
            }


        }
    }

    for (ERStat * child = er->child; child; child = child->next)
    {
        old_prev = er_save(child, this_er, old_prev);
    }

    return this_er;
}

// recursively walk the tree and filter (remove) regions using the callback classifier
ERStat* ERFilterNM::er_tree_filter ( InputArray image, ERStat * stat, ERStat *parent, ERStat *prev )
{
    // assert correct image type
    CV_Assert( image.type() == CV_8UC1 );

    Mat src = image.getMat();

    //Fill the region and calculate 2nd stage features
    Mat region = region_mask(Rect(stat->rect.tl(), stat->rect.br() + Point(2,2)));
    region = Scalar(0);
    int newMaskVal = 255;
    int flags = 4 + (newMaskVal << 8) + FLOODFILL_FIXED_RANGE + FLOODFILL_MASK_ONLY;
    Rect rect;

    floodFill( src(stat->rect),
               region, Point(stat->pixel%src.cols, stat->pixel/src.cols) - stat->rect.tl(),
               Scalar(255), &rect, Scalar(stat->level), Scalar(0), flags );
    region = region(Rect(1, 1, rect.width, rect.height));

    vector<vector<Point> > contours;
    vector<Point> contour_poly;
    vector<Vec4i> hierarchy;
    findContours( region, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0) );
    //TODO check epsilon parameter of approxPolyDP (set empirically) : we want more precision
    //     if the region is very small because otherwise we'll loose all the convexities
    approxPolyDP( Mat(contours[0]), contour_poly, (float)min(rect.width,rect.height)/17, true );

    bool was_convex = false;
    int  num_inflexion_points = 0;

    for (int p = 0 ; p<(int)contour_poly.size(); p++)
    {
        int p_prev = p-1;
        int p_next = p+1;
        if (p_prev == -1)
            p_prev = (int)contour_poly.size()-1;
        if (p_next == (int)contour_poly.size())
            p_next = 0;

        double angle_next = atan2((double)(contour_poly[p_next].y-contour_poly[p].y),
                                  (double)(contour_poly[p_next].x-contour_poly[p].x));
        double angle_prev = atan2((double)(contour_poly[p_prev].y-contour_poly[p].y),
                                  (double)(contour_poly[p_prev].x-contour_poly[p].x));
        if ( angle_next < 0 )
            angle_next = 2.*CV_PI + angle_next;

        double angle = (angle_next - angle_prev);
        if (angle > 2.*CV_PI)
            angle = angle - 2.*CV_PI;
        else if (angle < 0)
            angle = 2.*CV_PI + abs(angle);

        if (p>0)
        {
            if ( ((angle > CV_PI)&&(!was_convex)) || ((angle < CV_PI)&&(was_convex)) )
                num_inflexion_points++;
        }
        was_convex = (angle > CV_PI);

    }

    floodFill(region, Point(0,0), Scalar(255), 0);
    int holes_area = region.cols*region.rows-countNonZero(region);

    int hull_area = 0;

    {

        vector<Point> hull;
        convexHull(contours[0], hull, false);
        hull_area = (int)contourArea(hull);
    }


    stat->hole_area_ratio = (float)holes_area / stat->area;
    stat->convex_hull_ratio = (float)hull_area / (float)contourArea(contours[0]);
    stat->num_inflexion_points = (float)num_inflexion_points;


    // calculate P(child|character) and filter if possible
    if ( (classifier != NULL) && (stat->parent != NULL) )
    {
        stat->probability = classifier->eval(*stat);
    }

    if ( ( ((classifier != NULL)?(stat->probability >= minProbability):true) &&
          ((stat->area >= minArea*region_mask.rows*region_mask.cols) &&
           (stat->area <= maxArea*region_mask.rows*region_mask.cols)) ) ||
        (stat->parent == NULL) )
    {

        num_accepted_regions++;
        regions->push_back(*stat);

        regions->back().parent = parent;
        regions->back().next   = NULL;
        regions->back().child  = NULL;

        if (prev != NULL)
            prev->next = &(regions->back());
        else if (parent != NULL)
            parent->child = &(regions->back());

        ERStat *old_prev = NULL;
        ERStat *this_er  = &regions->back();

        for (ERStat * child = stat->child; child; child = child->next)
        {
            old_prev = er_tree_filter(image, child, this_er, old_prev);
        }

        return this_er;

    } else {

        num_rejected_regions++;

        ERStat *old_prev = prev;

        for (ERStat * child = stat->child; child; child = child->next)
        {
            old_prev = er_tree_filter(image, child, parent, old_prev);
        }

        return old_prev;
    }

}

// recursively walk the tree selecting only regions with local maxima probability
ERStat* ERFilterNM::er_tree_nonmax_suppression ( ERStat * stat, ERStat *parent, ERStat *prev )
{

    if ( ( stat->local_maxima ) || ( stat->parent == NULL ) )
    {

        regions->push_back(*stat);

        regions->back().parent = parent;
        regions->back().next   = NULL;
        regions->back().child  = NULL;

        if (prev != NULL)
            prev->next = &(regions->back());
        else if (parent != NULL)
            parent->child = &(regions->back());

        ERStat *old_prev = NULL;
        ERStat *this_er  = &regions->back();

        for (ERStat * child = stat->child; child; child = child->next)
        {
            old_prev = er_tree_nonmax_suppression( child, this_er, old_prev );
        }

        return this_er;

    } else {

        num_rejected_regions++;
        num_accepted_regions--;

        ERStat *old_prev = prev;

        for (ERStat * child = stat->child; child; child = child->next)
        {
            old_prev = er_tree_nonmax_suppression( child, parent, old_prev );
        }

        return old_prev;
    }

}

void ERFilterNM::setCallback(const Ptr<ERFilter::Callback>& cb)
{
    classifier = cb;
}

void ERFilterNM::setMinArea(float _minArea)
{
    CV_Assert( (_minArea >= 0) && (_minArea < maxArea) );
    minArea = _minArea;
    return;
}

void ERFilterNM::setMaxArea(float _maxArea)
{
    CV_Assert(_maxArea <= 1);
    CV_Assert(minArea < _maxArea);
    maxArea = _maxArea;
    return;
}

void ERFilterNM::setThresholdDelta(int _thresholdDelta)
{
    CV_Assert( (_thresholdDelta > 0) && (_thresholdDelta <= 128) );
    thresholdDelta = _thresholdDelta;
    return;
}

void ERFilterNM::setMinProbability(float _minProbability)
{
    CV_Assert( (_minProbability >= 0.0) && (_minProbability <= 1.0) );
    minProbability = _minProbability;
    return;
}

void ERFilterNM::setMinProbabilityDiff(float _minProbabilityDiff)
{
    CV_Assert( (_minProbabilityDiff >= 0.0) && (_minProbabilityDiff <= 1.0) );
    minProbabilityDiff = _minProbabilityDiff;
    return;
}

void ERFilterNM::setNonMaxSuppression(bool _nonMaxSuppression)
{
    nonMaxSuppression = _nonMaxSuppression;
    return;
}

int ERFilterNM::getNumRejected()
{
    return num_rejected_regions;
}




// load default 1st stage classifier if found
ERClassifierNM1::ERClassifierNM1(const string& filename)
{

    if (ifstream(filename.c_str()))
    {
        boost = StatModel::load<Boost>( filename.c_str() );
        if( boost.empty() )
        {
            cout << "Could not read the classifier " << filename.c_str() << endl;
            CV_Error(Error::StsBadArg, "Could not read the default classifier!");
        }
    }
    else
        CV_Error(Error::StsBadArg, "Default classifier file not found!");
}

double ERClassifierNM1::eval(const ERStat& stat)
{
    //Classify
    Mat sample = (Mat_<float>(1,4) <<  (float)(stat.rect.width)/(stat.rect.height), // aspect ratio
                     sqrt((float)(stat.area))/stat.perimeter, // compactness
                     (float)(1-stat.euler), //number of holes
                     stat.med_crossings);

    float votes = boost->predict( sample, noArray(), DTrees::PREDICT_SUM | StatModel::RAW_OUTPUT);

    // Logistic Correction returns a probability value (in the range(0,1))
    return (double)1-(double)1/(1+exp(-2*votes));
}


// load default 2nd stage classifier if found
ERClassifierNM2::ERClassifierNM2(const string& filename)
{
    if (ifstream(filename.c_str()))
    {
        boost = StatModel::load<Boost>( filename.c_str() );
        if( boost.empty() )
        {
            cout << "Could not read the classifier " << filename.c_str() << endl;
            CV_Error(Error::StsBadArg, "Could not read the default classifier!");
        }
    }
    else
        CV_Error(Error::StsBadArg, "Default classifier file not found!");
}

double ERClassifierNM2::eval(const ERStat& stat)
{
    //Classify
    Mat sample = (Mat_<float>(1,7) << (float)(stat.rect.width)/(stat.rect.height), // aspect ratio
                     sqrt((float)(stat.area))/stat.perimeter, // compactness
                     (float)(1-stat.euler), //number of holes
                     stat.med_crossings, stat.hole_area_ratio,
                     stat.convex_hull_ratio, stat.num_inflexion_points);

    float votes = boost->predict( sample, noArray(), DTrees::PREDICT_SUM | StatModel::RAW_OUTPUT);

    // Logistic Correction returns a probability value (in the range(0,1))
    return (double)1-(double)1/(1+exp(-2*votes));
}


/*!
    Create an Extremal Region Filter for the 1st stage classifier of N&M algorithm
    Neumann L., Matas J.: Real-Time Scene Text Localization and Recognition, CVPR 2012

    The component tree of the image is extracted by a threshold increased step by step
    from 0 to 255, incrementally computable descriptors (aspect_ratio, compactness,
    number of holes, and number of horizontal crossings) are computed for each ER
    and used as features for a classifier which estimates the class-conditional
    probability P(er|character). The value of P(er|character) is tracked using the inclusion
    relation of ER across all thresholds and only the ERs which correspond to local maximum
    of the probability P(er|character) are selected (if the local maximum of the
    probability is above a global limit pmin and the difference between local maximum and
    local minimum is greater than minProbabilityDiff).

    \param  cb                Callback with the classifier.
                              default classifier can be implicitly load with function loadClassifierNM1()
                              from file in samples/cpp/trained_classifierNM1.xml
    \param  thresholdDelta    Threshold step in subsequent thresholds when extracting the component tree
    \param  minArea           The minimum area (% of image size) allowed for retrieved ER's
    \param  minArea           The maximum area (% of image size) allowed for retrieved ER's
    \param  minProbability    The minimum probability P(er|character) allowed for retrieved ER's
    \param  nonMaxSuppression Whenever non-maximum suppression is done over the branch probabilities
    \param  minProbability    The minimum probability difference between local maxima and local minima ERs
*/
Ptr<ERFilter> createERFilterNM1(const Ptr<ERFilter::Callback>& cb, int thresholdDelta,
                                float minArea, float maxArea, float minProbability,
                                bool nonMaxSuppression, float minProbabilityDiff)
{

    CV_Assert( (minProbability >= 0.) && (minProbability <= 1.) );
    CV_Assert( (minArea < maxArea) && (minArea >=0.) && (maxArea <= 1.) );
    CV_Assert( (thresholdDelta >= 0) && (thresholdDelta <= 128) );
    CV_Assert( (minProbabilityDiff >= 0.) && (minProbabilityDiff <= 1.) );

    Ptr<ERFilterNM> filter = makePtr<ERFilterNM>();

    filter->setCallback(cb);

    filter->setThresholdDelta(thresholdDelta);
    filter->setMinArea(minArea);
    filter->setMaxArea(maxArea);
    filter->setMinProbability(minProbability);
    filter->setNonMaxSuppression(nonMaxSuppression);
    filter->setMinProbabilityDiff(minProbabilityDiff);
    return (Ptr<ERFilter>)filter;
}

/*!
    Create an Extremal Region Filter for the 2nd stage classifier of N&M algorithm
    Neumann L., Matas J.: Real-Time Scene Text Localization and Recognition, CVPR 2012

    In the second stage, the ERs that passed the first stage are classified into character
    and non-character classes using more informative but also more computationally expensive
    features. The classifier uses all the features calculated in the first stage and the following
    additional features: hole area ratio, convex hull ratio, and number of outer inflexion points.

    \param  cb             Callback with the classifier
                           default classifier can be implicitly load with function loadClassifierNM1()
                           from file in samples/cpp/trained_classifierNM2.xml
    \param  minProbability The minimum probability P(er|character) allowed for retreived ER's
*/
Ptr<ERFilter> createERFilterNM2(const Ptr<ERFilter::Callback>& cb, float minProbability)
{

    CV_Assert( (minProbability >= 0.) && (minProbability <= 1.) );

    Ptr<ERFilterNM> filter = makePtr<ERFilterNM>();

    filter->setCallback(cb);

    filter->setMinProbability(minProbability);
    return (Ptr<ERFilter>)filter;
}

Ptr<ERFilter> createERFilterNM1(const String& filename, int _thresholdDelta,
                                float _minArea, float _maxArea, float _minProbability,
                                bool _nonMaxSuppression, float _minProbabilityDiff) {
    return createERFilterNM1(loadClassifierNM1(filename), _thresholdDelta, _minArea, _maxArea, _minProbability, _nonMaxSuppression, _minProbabilityDiff);

}

Ptr<ERFilter> createERFilterNM2(const String& filename, float _minProbability) {
    return createERFilterNM2(loadClassifierNM2(filename), _minProbability);
}

/*!
    Allow to implicitly load the default classifier when creating an ERFilter object.
    The function takes as parameter the XML or YAML file with the classifier model
    (e.g. trained_classifierNM1.xml) returns a pointer to ERFilter::Callback.
*/
Ptr<ERFilter::Callback> loadClassifierNM1(const String& filename)

{
    return makePtr<ERClassifierNM1>(filename);
}

/*!
    Allow to implicitly load the default classifier when creating an ERFilter object.
    The function takes as parameter the XML or YAML file with the classifier model
    (e.g. trained_classifierNM2.xml) returns a pointer to ERFilter::Callback.
*/
Ptr<ERFilter::Callback> loadClassifierNM2(const String& filename)
{
    return makePtr<ERClassifierNM2>(filename);
}

// dummy classifier
class ERDummyClassifier CV_FINAL : public ERFilter::Callback
{
public:
    //Constructor
    ERDummyClassifier() {}
    // Destructor
    ~ERDummyClassifier() CV_OVERRIDE {}

    // The classifier must return probability measure for the region.
    double eval(const ERStat& s) CV_OVERRIDE {if (s.area ==0) return (double)0.0; return (double)1.0;}
};

/* Create a dummy classifier that accepts all regions */
Ptr<ERFilter::Callback> loadDummyClassifier();
Ptr<ERFilter::Callback> loadDummyClassifier()

{
    return makePtr<ERDummyClassifier>();
}

/* ------------------------------------------------------------------------------------*/
/* -------------------------------- Compute Channels NM -------------------------------*/
/* ------------------------------------------------------------------------------------*/


void  get_gradient_magnitude(Mat& _grey_img, Mat& _gradient_magnitude);

void get_gradient_magnitude(Mat& _grey_img, Mat& _gradient_magnitude)
{
    Mat C = Mat_<float>(_grey_img);

    Mat kernel = (Mat_<float>(1,3) << -1,0,1);
    Mat grad_x;
    filter2D(C, grad_x, -1, kernel, Point(-1,-1), 0, BORDER_DEFAULT);

    Mat kernel2 = (Mat_<float>(3,1) << -1,0,1);
    Mat grad_y;
    filter2D(C, grad_y, -1, kernel2, Point(-1,-1), 0, BORDER_DEFAULT);

    magnitude( grad_x, grad_y, _gradient_magnitude);
}


/*!
    Compute the different channels to be processed independently in the N&M algorithm
    Neumann L., Matas J.: Real-Time Scene Text Localization and Recognition, CVPR 2012

    In N&M algorithm, the combination of intensity (I), hue (H), saturation (S), and gradient
    magnitude channels (Grad) are used in order to obtain high localization recall.
    This implementation also the alternative combination of red (R), green (G), blue (B),
    lightness (L), and gradient magnitude (Grad).

    \param  _src           Source image. Must be RGB CV_8UC3.
    \param  _channels      Output vector<Mat> where computed channels are stored.
    \param  _mode          Mode of operation. Currently the only available options are
                           ERFILTER_NM_RGBLGrad and ERFILTER_NM_IHSGrad.

*/
void computeNMChannels(InputArray _src, CV_OUT OutputArrayOfArrays _channels, int _mode)
{

    CV_Assert( ( _mode == ERFILTER_NM_RGBLGrad ) || ( _mode == ERFILTER_NM_IHSGrad ) );

    Mat src = _src.getMat();
    if( src.empty() )
    {
        _channels.release();
        return;
    }

    // assert RGB image
    CV_Assert(src.type() == CV_8UC3);

    if (_mode == ERFILTER_NM_IHSGrad)
    {
        _channels.create( 4, 1, src.depth());

        Mat hsv;
        cvtColor(src, hsv, COLOR_RGB2HSV);
        vector<Mat> channelsHSV;
        split(hsv, channelsHSV);

        for (int i = 0; i < src.channels(); i++)
        {
            _channels.create(src.rows, src.cols, CV_8UC1, i);
            Mat channel = _channels.getMat(i);
            channelsHSV.at(i).copyTo(channel);
        }

        Mat grey;
        cvtColor(src, grey, COLOR_RGB2GRAY);
        Mat gradient_magnitude = Mat_<float>(grey.size());
        get_gradient_magnitude( grey, gradient_magnitude);
        gradient_magnitude.convertTo(gradient_magnitude, CV_8UC1);

        _channels.create(src.rows, src.cols, CV_8UC1, 3);
        Mat channelGrad = _channels.getMat(3);
        gradient_magnitude.copyTo(channelGrad);

    } else if (_mode == ERFILTER_NM_RGBLGrad) {

        _channels.create( 5, 1, src.depth());

        vector<Mat> channelsRGB;
        split(src, channelsRGB);
        for (int i = 0; i < src.channels(); i++)
        {
            _channels.create(src.rows, src.cols, CV_8UC1, i);
            Mat channel = _channels.getMat(i);
            channelsRGB.at(i).copyTo(channel);
        }

        Mat hls;
        cvtColor(src, hls, COLOR_RGB2HLS);
        vector<Mat> channelsHLS;
        split(hls, channelsHLS);

        _channels.create(src.rows, src.cols, CV_8UC1, 3);
        Mat channelL = _channels.getMat(3);
        channelsHLS.at(1).copyTo(channelL);

        Mat grey;
        cvtColor(src, grey, COLOR_RGB2GRAY);
        Mat gradient_magnitude = Mat_<float>(grey.size());
        get_gradient_magnitude( grey, gradient_magnitude);
        gradient_magnitude.convertTo(gradient_magnitude, CV_8UC1);

        _channels.create(src.rows, src.cols, CV_8UC1, 4);
        Mat channelGrad = _channels.getMat(4);
        gradient_magnitude.copyTo(channelGrad);
    }
}



/* ------------------------------------------------------------------------------------*/
/* -------------------------------- ER Grouping Algorithm -----------------------------*/
/* ------------------------------------------------------------------------------------*/


/*  NFA approximation functions */

// ln(10)
#ifndef M_LN10
#define M_LN10     2.30258509299404568401799145468436421
#endif
// Doubles relative error factor
#define RELATIVE_ERROR_FACTOR 100.0

// Compare doubles by relative error.
static int double_equal(double a, double b)
{
    double abs_diff,aa,bb,abs_max;

    /* trivial case */
    if( a == b ) return true;

    abs_diff = fabs(a-b);
    aa = fabs(a);
    bb = fabs(b);
    abs_max = aa > bb ? aa : bb;

    /* DBL_MIN is the smallest normalized number, thus, the smallest
       number whose relative error is bounded by DBL_EPSILON. For
       smaller numbers, the same quantization steps as for DBL_MIN
       are used. Then, for smaller numbers, a meaningful "relative"
       error should be computed by dividing the difference by DBL_MIN. */
    if( abs_max < DBL_MIN ) abs_max = DBL_MIN;

    /* equal if relative error <= factor x eps */
    return (abs_diff / abs_max) <= (RELATIVE_ERROR_FACTOR * DBL_EPSILON);
}


/*
     Computes the natural logarithm of the absolute value of
     the gamma function of x using the Lanczos approximation.
     See http://www.rskey.org/gamma.htm
*/
static double log_gamma_lanczos(double x)
{
    static double q[7] = { 75122.6331530, 80916.6278952, 36308.2951477,
                           8687.24529705, 1168.92649479, 83.8676043424,
                           2.50662827511 };
    double a = (x+0.5) * log(x+5.5) - (x+5.5);
    double b = 0.0;
    int n;

    for(n=0;n<7;n++)
    {
        a -= log( x + (double) n );
        b += q[n] * pow( x, (double) n );
    }
    return a + log(b);
}

/*
     Computes the natural logarithm of the absolute value of
     the gamma function of x using Windschitl method.
     See http://www.rskey.org/gamma.htm
*/
static double log_gamma_windschitl(double x)
{
    return 0.918938533204673 + (x-0.5)*log(x) - x
           + 0.5*x*log( x*sinh(1/x) + 1/(810.0*pow(x,6.0)) );
}

/*
     Computes the natural logarithm of the absolute value of
     the gamma function of x. When x>15 use log_gamma_windschitl(),
     otherwise use log_gamma_lanczos().
*/
#define log_gamma(x) ((x)>15.0?log_gamma_windschitl(x):log_gamma_lanczos(x))

// Size of the table to store already computed inverse values.
#define TABSIZE 100000

/*
     Computes -log10(NFA).
     NFA stands for Number of False Alarms:
*/
static double NFA(int n, int k, double p, double logNT)
{
    static double inv[TABSIZE];   /* table to keep computed inverse values */
    double tolerance = 0.1;       /* an error of 10% in the result is accepted */
    double log1term,term,bin_term,mult_term,bin_tail,err,p_term;
    int i;

    if (p<=0)
        p = std::numeric_limits<double>::min();
    if (p>=1)
        p = 1 - std::numeric_limits<double>::epsilon();

    /* check parameters */
    if( n<0 || k<0 || k>n || p<=0.0 || p>=1.0 )
    {
        CV_Error(Error::StsBadArg, "erGrouping wrong n, k or p values in NFA call!");
    }

    /* trivial cases */
    if( n==0 || k==0 ) return -logNT;
    if( n==k ) return -logNT - (double) n * log10(p);

    /* probability term */
    p_term = p / (1.0-p);

    /* compute the first term of the series */
    log1term = log_gamma( (double) n + 1.0 ) - log_gamma( (double) k + 1.0 )
               - log_gamma( (double) (n-k) + 1.0 )
               + (double) k * log(p) + (double) (n-k) * log(1.0-p);
    term = exp(log1term);

    /* in some cases no more computations are needed */
    if( double_equal(term,0.0) )              /* the first term is almost zero */
    {
        if( (double) k > (double) n * p )     /* at begin or end of the tail?  */
            return -log1term / M_LN10 - logNT;  /* end: use just the first term  */
        else
            return -logNT;                      /* begin: the tail is roughly 1  */
    }

    /* compute more terms if needed */
    bin_tail = term;
    for(i=k+1;i<=n;i++)
    {
        bin_term = (double) (n-i+1) * ( i<TABSIZE ?
                    ( inv[i]!=0.0 ? inv[i] : ( inv[i] = 1.0 / (double) i ) ) :
                    1.0 / (double) i );

        mult_term = bin_term * p_term;
        term *= mult_term;
        bin_tail += term;
        if(bin_term<1.0)
        {
            err = term * ( ( 1.0 - pow( mult_term, (double) (n-i+1) ) ) /
                           (1.0-mult_term) - 1.0 );
            if( err < tolerance * fabs(-log10(bin_tail)-logNT) * bin_tail ) break;
        }
    }
    return -log10(bin_tail) - logNT;
}


// Minibox : smallest enclosing box of a set of n points in d dimensions

class Minibox {
private:
    vector<float> edge_begin;
    vector<float> edge_end;
    bool   initialized;

public:
    // creates an empty box
    Minibox();

    // copies p to the internal point set
    void check_in (vector<float> *p);

    // returns the volume of the box
    long double volume();
};

Minibox::Minibox()
{
    initialized = false;
}

void Minibox::check_in (vector<float> *p)
{
    if(!initialized) for (int i=0; i<(int)p->size(); i++)
    {
        edge_begin.push_back(p->at(i));
        edge_end.push_back(p->at(i)+0.00000000000000001f);
        initialized = true;
    }
    else for (int i=0; i<(int)p->size(); i++)
    {
        edge_begin.at(i) = min(p->at(i),edge_begin.at(i));
        edge_end.at(i) = max(p->at(i),edge_end.at(i));
    }
}

long double Minibox::volume ()
{
    long double volume_ = 1;
    for (int i=0; i<(int)edge_begin.size(); i++)
    {
        volume_ = volume_ * (edge_end.at(i) - edge_begin.at(i));
    }
    return (volume_);
}


#define MAX_GROUP_ELEMENTS 50


/*  Hierarchical Clustering classes and functions */


// Hierarchical Clustering linkage variants
enum method_codes
{
    METHOD_METR_SINGLE           = 0,
    METHOD_METR_AVERAGE          = 1
};

#ifndef INT32_MAX
#define MAX_INDEX 0x7fffffffL
#else
#define MAX_INDEX INT32_MAX
#endif

// A node in the hierarchical clustering algorithm
struct node {
    int_fast32_t node1, node2;
    double dist;

    inline friend bool operator< (const node a, const node b)
    {
        // Numbers are always smaller than NaNs.
        return a.dist < b.dist || (a.dist==a.dist && b.dist!=b.dist);
    }
};

// self-destructing array pointer
template <typename type>
class auto_array_ptr {
private:
    type * ptr;
public:
    auto_array_ptr() { ptr = NULL; }
    template <typename index>
    auto_array_ptr(index const size) { init(size); }
    template <typename index, typename value>
    auto_array_ptr(index const size, value const val) { init(size, val); }

    ~auto_array_ptr()
    {
        delete [] ptr;
    }
    void free() {
        delete [] ptr;
        ptr = NULL;
    }
    template <typename index>
    void init(index const size)
    {
        ptr = new type [size];
    }
    template <typename index, typename value>
    void init(index const size, value const val)
    {
        init(size);
        for (index i=0; i<size; i++) ptr[i] = val;
    }
    inline operator type *() const { return ptr; }
};

// The result of the hierarchical clustering algorithm
class cluster_result {
private:
    auto_array_ptr<node> Z;
    int_fast32_t pos;

public:
    cluster_result(const int_fast32_t size): Z(size)
    {
        pos = 0;
    }

    void append(const int_fast32_t node1, const int_fast32_t node2, const double dist)
    {
        Z[pos].node1 = node1;
        Z[pos].node2 = node2;
        Z[pos].dist  = dist;
        pos++;
    }

    node * operator[] (const int_fast32_t idx) const { return Z + idx; }

    void sqrt() const
    {
        for (int_fast32_t i=0; i<pos; i++)
            Z[i].dist = ::sqrt(Z[i].dist);
    }

    void sqrt(const double) const  // ignore the argument
    {
        sqrt();
    }
};

// Class for a doubly linked list
class doubly_linked_list {
public:
    int_fast32_t start;
    auto_array_ptr<int_fast32_t> succ;

private:
    auto_array_ptr<int_fast32_t> pred;

public:
    doubly_linked_list(const int_fast32_t size): succ(size+1), pred(size+1)
    {
        for (int_fast32_t i=0; i<size; i++)
        {
            pred[i+1] = i;
            succ[i] = i+1;
        }
        start = 0;
    }

    void remove(const int_fast32_t idx)
    {
        // Remove an index from the list.
        if (idx==start)
        {
            start = succ[idx];
        } else {
            succ[pred[idx]] = succ[idx];
            pred[succ[idx]] = pred[idx];
        }
        succ[idx] = 0; // Mark as inactive
    }

    bool is_inactive(int_fast32_t idx) const
    {
        return (succ[idx]==0);
    }
};

// Indexing functions
// D is the upper triangular part of a symmetric (NxN)-matrix
// We require r_ < c_ !
#define D_(r_,c_) ( D[(static_cast<ptrdiff_t>(2*N-3-(r_))*(r_)>>1)+(c_)-1] )
// Z is an ((N-1)x4)-array
#define Z_(_r, _c) (Z[(_r)*4 + (_c)])

/*
   Lookup function for a union-find data structure.

   The function finds the root of idx by going iteratively through all
   parent elements until a root is found. An element i is a root if
   nodes[i] is zero. To make subsequent searches faster, the entry for
   idx and all its parents is updated with the root element.
*/
class union_find {
private:
    auto_array_ptr<int_fast32_t> parent;
    int_fast32_t nextparent;

public:
    void init(const int_fast32_t size)
    {
        parent.init(2*size-1, 0);
        nextparent = size;
    }

    int_fast32_t Find (int_fast32_t idx) const
    {
        if (parent[idx] !=0 ) // a -> b
        {
            int_fast32_t p = idx;
            idx = parent[idx];
            if (parent[idx] !=0 ) // a -> b -> c
            {
                do
                {
                    idx = parent[idx];
                } while (parent[idx] != 0);
                do
                {
                    int_fast32_t tmp = parent[p];
                    parent[p] = idx;
                    p = tmp;
                } while (parent[p] != idx);
            }
        }
        return idx;
    }

    void Union (const int_fast32_t node1, const int_fast32_t node2)
    {
        parent[node1] = parent[node2] = nextparent++;
    }
};

#if 0
/* Functions for the update of the dissimilarity array */

inline static void f_single( double * const b, const double a )
{
    if (*b > a) *b = a;
}
inline static void f_average( double * const b, const double a, const double s, const double t)
{
    *b = s*a + t*(*b);
}

/*
     This is the NN-chain algorithm.

     N: integer
     D: condensed distance matrix N*(N-1)/2
     Z2: output data structure
*/
template <const unsigned char method, typename t_members>
static void NN_chain_core(const int_fast32_t N, double * const D, t_members * const members, cluster_result & Z2)
{
    int_fast32_t i;

    auto_array_ptr<int_fast32_t> NN_chain(N);
    int_fast32_t NN_chain_tip = 0;

    int_fast32_t idx1, idx2;

    double size1, size2;
    doubly_linked_list active_nodes(N);

    double min;

    for (int_fast32_t j=0; j<N-1; j++)
    {
        if (NN_chain_tip <= 3)
        {
            NN_chain[0] = idx1 = active_nodes.start;
            NN_chain_tip = 1;

            idx2 = active_nodes.succ[idx1];
            min = D_(idx1,idx2);

            for (i=active_nodes.succ[idx2]; i<N; i=active_nodes.succ[i])
            {
                if (D_(idx1,i) < min)
                {
                    min = D_(idx1,i);
                    idx2 = i;
                }
            }
        }  // a: idx1   b: idx2
        else {
            NN_chain_tip -= 3;
            idx1 = NN_chain[NN_chain_tip-1];
            idx2 = NN_chain[NN_chain_tip];
            min = idx1<idx2 ? D_(idx1,idx2) : D_(idx2,idx1);
        }  // a: idx1   b: idx2

        do {
            NN_chain[NN_chain_tip] = idx2;

            for (i=active_nodes.start; i<idx2; i=active_nodes.succ[i])
            {
                // Need double_equal check because of some numerical imprecision
                // in construction of D_.
                if (D_(i,idx2) < min && !double_equal(D_(i,idx2), min))
                {
                    min = D_(i,idx2);
                    idx1 = i;
                }
            }
            for (i=active_nodes.succ[idx2]; i<N; i=active_nodes.succ[i])
            {
                if (D_(idx2,i) < min && !double_equal(D_(idx2,i), min))
                {
                    min = D_(idx2,i);
                    idx1 = i;
                }
            }

            idx2 = idx1;
            idx1 = NN_chain[NN_chain_tip++];

        } while (idx2 != NN_chain[NN_chain_tip-2]);

        Z2.append(idx1, idx2, min);

        if (idx1>idx2)
        {
            int_fast32_t tmp = idx1;
            idx1 = idx2;
            idx2 = tmp;
        }

        //if ( method == METHOD_METR_AVERAGE )
        {
            size1 = static_cast<double>(members[idx1]);
            size2 = static_cast<double>(members[idx2]);
            members[idx2] += members[idx1];
        }

        // Remove the smaller index from the valid indices (active_nodes).
        active_nodes.remove(idx1);

        switch (method) {
            case METHOD_METR_SINGLE:
                /*
                 Single linkage.
                */
                // Update the distance matrix in the range [start, idx1).
                for (i=active_nodes.start; i<idx1; i=active_nodes.succ[i])
                    f_single(&D_(i, idx2), D_(i, idx1) );
                // Update the distance matrix in the range (idx1, idx2).
                for (; i<idx2; i=active_nodes.succ[i])
                    f_single(&D_(i, idx2), D_(idx1, i) );
                // Update the distance matrix in the range (idx2, N).
                for (i=active_nodes.succ[idx2]; i<N; i=active_nodes.succ[i])
                    f_single(&D_(idx2, i), D_(idx1, i) );
                break;

            case METHOD_METR_AVERAGE:
            {
                /*
                Average linkage.
                */
                // Update the distance matrix in the range [start, idx1).
                double s = size1/(size1+size2);
                double t = size2/(size1+size2);
                for (i=active_nodes.start; i<idx1; i=active_nodes.succ[i])
                    f_average(&D_(i, idx2), D_(i, idx1), s, t );
                // Update the distance matrix in the range (idx1, idx2).
                for (; i<idx2; i=active_nodes.succ[i])
                    f_average(&D_(i, idx2), D_(idx1, i), s, t );
                // Update the distance matrix in the range (idx2, N).
                for (i=active_nodes.succ[idx2]; i<N; i=active_nodes.succ[i])
                    f_average(&D_(idx2, i), D_(idx1, i), s, t );
                break;
            }
        }
    }
}
#endif

/*
   Clustering methods for vector data
*/

template <typename t_dissimilarity>
static void MST_linkage_core_vector(const int_fast32_t N,
                                    t_dissimilarity & dist,
                                    cluster_result & Z2) {
/*
     Hierarchical clustering using the minimum spanning tree

     N: integer, number of data points
     dist: function pointer to the metric
     Z2: output data structure
*/
    int_fast32_t i;
    int_fast32_t idx2;
    doubly_linked_list active_nodes(N);
    auto_array_ptr<double> d(N);

    int_fast32_t prev_node;
    double min;

    // first iteration
    idx2 = 1;
    min = d[1] = dist(0,1);
    for (i=2; min!=min && i<N; i++) // eliminate NaNs if possible
    {
        min = d[i] = dist(0,i);
        idx2 = i;
    }

    for ( ; i<N; i++)
    {
        d[i] = dist(0,i);
        if (d[i] < min)
        {
            min = d[i];
            idx2 = i;
        }
    }

    Z2.append(0, idx2, min);

    for (int_fast32_t j=1; j<N-1; j++)
    {
        prev_node = idx2;
        active_nodes.remove(prev_node);

        idx2 = active_nodes.succ[0];
        min = d[idx2];

        for (i=idx2; min!=min && i<N; i=active_nodes.succ[i]) // eliminate NaNs if possible
        {
            min = d[i] = dist(i, prev_node);
            idx2 = i;
        }

        for ( ; i<N; i=active_nodes.succ[i])
        {
            double tmp = dist(i, prev_node);
            if (d[i] > tmp)
                d[i] = tmp;
            if (d[i] < min)
            {
                min = d[i];
                idx2 = i;
            }
        }
        Z2.append(prev_node, idx2, min);
    }
}

class linkage_output {
private:
    double * Z;
    int_fast32_t pos;

public:
    linkage_output(double * const _Z)
    {
         this->Z = _Z;
         pos = 0;
    }

    void append(const int_fast32_t node1, const int_fast32_t node2, const double dist, const double size)
    {
         if (node1<node2)
         {
                Z[pos++] = static_cast<double>(node1);
                Z[pos++] = static_cast<double>(node2);
         } else {
                Z[pos++] = static_cast<double>(node2);
                Z[pos++] = static_cast<double>(node1);
         }
         Z[pos++] = dist;
         Z[pos++] = size;
    }
};


/*
    Generate the specific output format for a dendrogram from the
    clustering output.

    The list of merging steps can be sorted or unsorted.
*/

// The size of a node is either 1 (a single point) or is looked up from
// one of the clusters.
#define size_(r_) ( ((r_<N) ? 1 : Z_(r_-N,3)) )

static void generate_dendrogram(double * const Z, cluster_result & Z2, const int_fast32_t N)
{
    // The array "nodes" is a union-find data structure for the cluster
    // identities (only needed for unsorted cluster_result input).
    union_find nodes;
    stable_sort(Z2[0], Z2[N-1]);
    nodes.init(N);

    linkage_output output(Z);
    int_fast32_t node1, node2;

    for (int_fast32_t i=0; i<N-1; i++) {
         // Get two data points whose clusters are merged in step i.
         // Find the cluster identifiers for these points.
         node1 = nodes.Find(Z2[i]->node1);
         node2 = nodes.Find(Z2[i]->node2);
         // Merge the nodes in the union-find data structure by making them
         // children of a new node.
         nodes.Union(node1, node2);
         output.append(node1, node2, Z2[i]->dist, size_(node1)+size_(node2));
    }
}

/*
     Clustering on vector data
*/

enum {
    // metrics
    METRIC_EUCLIDEAN       =  0,
    METRIC_CITYBLOCK       =  1,
    METRIC_SEUCLIDEAN      =  2,
    METRIC_SQEUCLIDEAN     =  3
};

/*
    This class handles all the information about the dissimilarity
    computation.
*/
class dissimilarity {
protected:
    double * Xa;
    auto_array_ptr<double> Xnew;
    ptrdiff_t dim; // size_t saves many statis_cast<> in products
    int_fast32_t N;
    int_fast32_t * members;
    void (cluster_result::*postprocessfn) (const double) const;
    double postprocessarg;

    double (dissimilarity::*distfn) (const int_fast32_t, const int_fast32_t) const;

    auto_array_ptr<double> precomputed;

    double * V;
    const double * V_data;

public:
    dissimilarity (double * const _Xa, int _Num, int _dim,
                   int_fast32_t * const _members,
                   const unsigned char method,
                   const unsigned char metric,
                   bool temp_point_array)
                   : Xa(_Xa),
                     dim(_dim),
                     N(_Num),
                     members(_members),
                     postprocessfn(NULL),
                     V(NULL)
    {
        switch (method) {
            case METHOD_METR_SINGLE: // only single linkage allowed here but others may come...
            default:
                postprocessfn = NULL; // default
                switch (metric)
                {
                    case METRIC_EUCLIDEAN:
                        set_euclidean();
                        break;
                    case METRIC_SEUCLIDEAN:
                    case METRIC_SQEUCLIDEAN:
                        distfn = &dissimilarity::sqeuclidean;
                        break;
                    case METRIC_CITYBLOCK:
                        set_cityblock();
                        break;
                }
        }

        if (temp_point_array)
        {
            Xnew.init((N-1)*dim);
        }
    }

    ~dissimilarity()
    {
        free(V);
    }

    inline double operator () (const int_fast32_t i, const int_fast32_t j) const
    {
        return (this->*distfn)(i,j);
    }

    inline double X (const int_fast32_t i, const int_fast32_t j) const
    {
        return Xa[i*dim+j];
    }

    inline bool Xb (const int_fast32_t i, const int_fast32_t j) const
    {
        return  reinterpret_cast<bool *>(Xa)[i*dim+j];
    }

    inline double * Xptr(const int_fast32_t i, const int_fast32_t j) const
    {
        return Xa+i*dim+j;
    }

    void postprocess(cluster_result & Z2) const
    {
        if (postprocessfn!=NULL)
        {
            (Z2.*postprocessfn)(postprocessarg);
        }
    }

    double sqeuclidean(const int_fast32_t i, const int_fast32_t j) const
    {
        double sum = 0;
        double const * Pi = Xa+i*dim;
        double const * Pj = Xa+j*dim;
        for (int_fast32_t k=0; k<dim; k++)
        {
            double diff = Pi[k] - Pj[k];
            sum += diff*diff;
        }
        return sum;
    }

private:

    void set_euclidean()
    {
        distfn = &dissimilarity::sqeuclidean;
        postprocessfn = &cluster_result::sqrt;
    }

    void set_cityblock()
    {
        distfn = &dissimilarity::cityblock;
    }

    double seuclidean(const int_fast32_t i, const int_fast32_t j) const
    {
        double sum = 0;
        for (int_fast32_t k=0; k<dim; k++)
        {
            double diff = X(i,k)-X(j,k);
            sum += diff*diff/V_data[k];
        }
        return sum;
    }

    double cityblock(const int_fast32_t i, const int_fast32_t j) const
    {
        double sum = 0;
        for (int_fast32_t k=0; k<dim; k++)
        {
            sum += fabs(X(i,k)-X(j,k));
        }
        return sum;
    }
};

/*Clustering for the "stored data approach": the input are points in a vector space.*/
static int linkage_vector(double *X, int N, int dim, double * Z, unsigned char method, unsigned char metric)
{

    CV_Assert(N >=1);
    CV_Assert(N <= MAX_INDEX/4);
    CV_Assert(dim >=1);

    try
    {
        cluster_result Z2(N-1);
        auto_array_ptr<int_fast32_t> members;
        dissimilarity dist(X, N, dim, members, method, metric, false);
        MST_linkage_core_vector(N, dist, Z2);
        dist.postprocess(Z2);
        generate_dendrogram(Z, Z2, N);
    } // try
    catch (const bad_alloc&)
    {
        CV_Error(Error::StsNoMem, "Not enough Memory for erGrouping hierarchical clustering structures!");
    }
    catch(const exception&)
    {
        CV_Error(Error::StsError, "Uncaught exception in erGrouping!");
    }
    catch(...)
    {
        CV_Error(Error::StsError, "C++ exception (unknown reason) in erGrouping!");
    }
    return 0;
}

// ERFeatures structure stores additional features for a given ERStat instance
struct ERFeatures
{
    int area;
    Point center;
    Rect  rect;
    float intensity_mean;  ///< mean intensity of the whole region
    float intensity_std;  ///< intensity standard deviation of the whole region
    float boundary_intensity_mean;  ///< mean intensity of the boundary of the region
    float boundary_intensity_std;  ///< intensity standard deviation of the boundary of the region
    double stroke_mean;  ///< mean stroke width approximation of the whole region
    double stroke_std;  ///< stroke standard deviation of the whole region
    double gradient_mean;  ///< mean gradient magnitude of the whole region
    double gradient_std;  ///< gradient magnitude standard deviation of the whole region
    double axial_ratio;
    double convex_hull_ratio;
    int convexities;
    double hu_moments[7];
};

/*  Maximal Meaningful Clusters Detection */

struct HCluster{
    int num_elem;           // number of elements
    vector<int> elements;   // elements (contour ID)
    int nfa;                // the number of false alarms for this merge
    float dist;             // distance of the merge
    float dist_ext;         // distance where this merge will merge with another
    long double volume;     // volume of the bounding sphere (or bounding box)
    long double volume_ext; // volume of the sphere(or box) + envolvent empty space
    vector<vector<float> > points; // nD points in this cluster
    bool max_meaningful;    // is this merge max meaningful ?
    vector<int> max_in_branch; // otherwise which merges are the max_meaningful in this branch
    int min_nfa_in_branch;  // min nfa detected within the chilhood
    int node1;
    int node2;
    double probability;      //the probability of this group of being a text group
};

class MaxMeaningfulClustering
{
public:
    unsigned char method_;
    unsigned char metric_;

    /// Constructor.
    MaxMeaningfulClustering(unsigned char _method, unsigned char _metric, vector<ERFeatures> &_regions,
                            Size _imsize, const string &filename, double _minProbability);

    void operator()(double *data, unsigned int num, int dim, unsigned char method,
                    unsigned char metric, vector< vector<int> > *meaningful_clusters);

    MaxMeaningfulClustering & operator=(const MaxMeaningfulClustering &a);

private:
    double minProbability;
    Ptr<Boost> group_boost;
    vector<ERFeatures> &regions;
    Size imsize;

    /// Helper functions
    void build_merge_info(double *dendogram, double *data, int num, int dim, bool use_full_merge_rule,
                          vector<HCluster> *merge_info, vector< vector<int> > *meaningful_clusters);

    /// Calculate the Number of False Alarms
    int nfa(float sigma, int k, int N);

    /// Calculate the probability of a group being a text group
    double probability(vector<int> &elements);

};

MaxMeaningfulClustering::MaxMeaningfulClustering(unsigned char _method, unsigned char _metric, vector<ERFeatures> &_regions,
                                                 Size _imsize, const string &filename, double _minProbability):
                                                 method_(_method), metric_(_metric), regions(_regions), imsize(_imsize)
{

    minProbability = _minProbability;

    if (ifstream(filename.c_str()))
    {
        group_boost = StatModel::load<Boost>( filename.c_str() );
        if( group_boost.empty() )
        {
            cout << "Could not read the classifier " << filename.c_str() << endl;
            CV_Error(Error::StsBadArg, "Could not read the default classifier!");
        }
    }
    else
        CV_Error(Error::StsBadArg, "erGrouping: Default classifier file not found!");
}


void MaxMeaningfulClustering::operator()(double *data, unsigned int num, int dim, unsigned char method,
                                         unsigned char metric, vector< vector<int> > *meaningful_clusters)
{

    double *Z = (double*)malloc(((num-1)*4) * sizeof(double)); // we need 4 floats foreach sample merge.
    if (Z == NULL)
        CV_Error(Error::StsNoMem, "Not enough Memory for erGrouping hierarchical clustering structures!");

    linkage_vector(data, (int)num, dim, Z, method, metric);

    vector<HCluster> merge_info;
    build_merge_info(Z, data, (int)num, dim, false, &merge_info, meaningful_clusters);

    free(Z);
    merge_info.clear();
}

void MaxMeaningfulClustering::build_merge_info(double *Z, double *X, int N, int dim,
                                               bool use_full_merge_rule,
                                               vector<HCluster> *merge_info,
                                               vector< vector<int> > *meaningful_clusters)
{

    // walk the whole dendrogram
    for (int i=0; i<(N-1)*4; i=i+4)
    {
        HCluster cluster;
        cluster.num_elem = (int)Z[i+3]; //number of elements

        int node1  = (int)Z[i];
        int node2  = (int)Z[i+1];
        float dist = (float)Z[i+2];

        if (node1<N)
        {
            vector<float> point;
            for (int n=0; n<dim; n++)
                point.push_back((float)X[node1*dim+n]);
            cluster.points.push_back(point);
            cluster.elements.push_back((int)node1);
        }
        else
        {
            for (int ii=0; ii<(int)merge_info->at(node1-N).points.size(); ii++)
            {
                cluster.points.push_back(merge_info->at(node1-N).points[ii]);
                cluster.elements.push_back(merge_info->at(node1-N).elements[ii]);
            }
            //update the extended volume of node1 using the dist where this cluster merge with another
            merge_info->at(node1-N).dist_ext = dist;
        }
        if (node2<N)
        {
            vector<float> point;
            for (int n=0; n<dim; n++)
                point.push_back((float)X[node2*dim+n]);
            cluster.points.push_back(point);
            cluster.elements.push_back((int)node2);
        }
        else
        {
            for (int ii=0; ii<(int)merge_info->at(node2-N).points.size(); ii++)
            {
                cluster.points.push_back(merge_info->at(node2-N).points[ii]);
                cluster.elements.push_back(merge_info->at(node2-N).elements[ii]);
            }

            //update the extended volume of node2 using the dist where this cluster merge with another
            merge_info->at(node2-N).dist_ext = dist;
        }

        Minibox mb;
        for (int ii=0; ii<(int)cluster.points.size(); ii++)
        {
            mb.check_in(&cluster.points.at(ii));
        }

        cluster.dist   = dist;
        cluster.volume = mb.volume();
        if (cluster.volume >= 1)
            cluster.volume = 0.999999;
        if (cluster.volume == 0)
            cluster.volume = 0.001;

        cluster.volume_ext=1;

        if (node1>=N)
        {
            merge_info->at(node1-N).volume_ext = cluster.volume;
        }
        if (node2>=N)
        {
            merge_info->at(node2-N).volume_ext = cluster.volume;
        }

        cluster.node1 = node1;
        cluster.node2 = node2;

        merge_info->push_back(cluster);

    }

    for (int i=0; i<(int)merge_info->size(); i++)
    {

        merge_info->at(i).nfa = nfa((float)merge_info->at(i).volume,
                                    merge_info->at(i).num_elem, N);

        merge_info->at(i).probability = probability(merge_info->at(i).elements);
        int node1 = merge_info->at(i).node1;
        int node2 = merge_info->at(i).node2;

        if ((node1<N)&&(node2<N))
        {
            // both nodes are individual samples (nfa=1) : each cluster is max.
            merge_info->at(i).max_meaningful = true;
            merge_info->at(i).max_in_branch.push_back(i);
            merge_info->at(i).min_nfa_in_branch = merge_info->at(i).nfa;
        } else {
            if ((node1>=N)&&(node2>=N))
            {
                // both nodes are "sets" : we must evaluate the merging condition
                if ( ( ( (use_full_merge_rule) &&
                         ((merge_info->at(i).nfa < merge_info->at(node1-N).nfa + merge_info->at(node2-N).nfa) &&
                         (merge_info->at(i).nfa < min(merge_info->at(node1-N).min_nfa_in_branch,
                                                    merge_info->at(node2-N).min_nfa_in_branch))) ) ||
                       ( (!use_full_merge_rule) &&
                         ((merge_info->at(i).nfa < min(merge_info->at(node1-N).min_nfa_in_branch,
                                                     merge_info->at(node2-N).min_nfa_in_branch))) ) )
                     && (merge_info->at(i).probability > minProbability) )
                {
                    merge_info->at(i).max_meaningful = true;
                    merge_info->at(i).max_in_branch.push_back(i);
                    merge_info->at(i).min_nfa_in_branch = merge_info->at(i).nfa;
                    for (int k =0; k<(int)merge_info->at(node1-N).max_in_branch.size(); k++)
                        merge_info->at(merge_info->at(node1-N).max_in_branch.at(k)).max_meaningful = false;
                    for (int k =0; k<(int)merge_info->at(node2-N).max_in_branch.size(); k++)
                        merge_info->at(merge_info->at(node2-N).max_in_branch.at(k)).max_meaningful = false;
                } else {
                    merge_info->at(i).max_meaningful = false;
                    merge_info->at(i).max_in_branch.insert(merge_info->at(i).max_in_branch.end(),
                                                           merge_info->at(node1-N).max_in_branch.begin(),
                                                           merge_info->at(node1-N).max_in_branch.end());
                    merge_info->at(i).max_in_branch.insert(merge_info->at(i).max_in_branch.end(),
                                                           merge_info->at(node2-N).max_in_branch.begin(),
                                                           merge_info->at(node2-N).max_in_branch.end());

                    if (merge_info->at(i).nfa < min(merge_info->at(node1-N).min_nfa_in_branch,
                                                    merge_info->at(node2-N).min_nfa_in_branch))

                        merge_info->at(i).min_nfa_in_branch = merge_info->at(i).nfa;
                    else
                        merge_info->at(i).min_nfa_in_branch = min(merge_info->at(node1-N).min_nfa_in_branch,
                                                                  merge_info->at(node2-N).min_nfa_in_branch);
                }
            } else {

                //one of the nodes is a "set" and the other is an individual sample : check merging condition
                if (node1>=N)
                {
                    if ((merge_info->at(i).nfa < merge_info->at(node1-N).nfa + 1) &&
                        (merge_info->at(i).nfa<merge_info->at(node1-N).min_nfa_in_branch) &&
                        (merge_info->at(i).probability > minProbability))
                    {
                        merge_info->at(i).max_meaningful = true;
                        merge_info->at(i).max_in_branch.push_back(i);
                        merge_info->at(i).min_nfa_in_branch = merge_info->at(i).nfa;
                        for (int k =0; k<(int)merge_info->at(node1-N).max_in_branch.size(); k++)
                            merge_info->at(merge_info->at(node1-N).max_in_branch.at(k)).max_meaningful = false;
                    } else {
                        merge_info->at(i).max_meaningful = false;
                        merge_info->at(i).max_in_branch.insert(merge_info->at(i).max_in_branch.end(),
                                                               merge_info->at(node1-N).max_in_branch.begin(),
                                                               merge_info->at(node1-N).max_in_branch.end());
                        merge_info->at(i).min_nfa_in_branch = min(merge_info->at(i).nfa,
                                                                  merge_info->at(node1-N).min_nfa_in_branch);
                    }
                } else {
                    if ((merge_info->at(i).nfa < merge_info->at(node2-N).nfa + 1) &&
                        (merge_info->at(i).nfa<merge_info->at(node2-N).min_nfa_in_branch) &&
                        (merge_info->at(i).probability > minProbability))
                    {
                        merge_info->at(i).max_meaningful = true;
                        merge_info->at(i).max_in_branch.push_back(i);
                        merge_info->at(i).min_nfa_in_branch = merge_info->at(i).nfa;
                        for (int k =0; k<(int)merge_info->at(node2-N).max_in_branch.size(); k++)
                            merge_info->at(merge_info->at(node2-N).max_in_branch.at(k)).max_meaningful = false;
                    } else {
                        merge_info->at(i).max_meaningful = false;
                        merge_info->at(i).max_in_branch.insert(merge_info->at(i).max_in_branch.end(),
                        merge_info->at(node2-N).max_in_branch.begin(),
                        merge_info->at(node2-N).max_in_branch.end());
                        merge_info->at(i).min_nfa_in_branch = min(merge_info->at(i).nfa,
                        merge_info->at(node2-N).min_nfa_in_branch);
                    }
                }
            }
        }
    }

    for (int i=0; i<(int)merge_info->size(); i++)
    {
        if (merge_info->at(i).max_meaningful)
        {
            vector<int> cluster;
            for (int k=0; k<(int)merge_info->at(i).elements.size();k++)
                cluster.push_back(merge_info->at(i).elements.at(k));
            meaningful_clusters->push_back(cluster);
        }
    }

}


int MaxMeaningfulClustering::nfa(float sigma, int k, int N)
{
    // use an approximation for the nfa calculations (faster)
    return -1*(int)NFA( N, k, (double) sigma, 0);
}


// utility functions for MST
static bool edge_comp (Vec4f i,Vec4f j)
{
    Point a = Point(cvRound(i[0]), cvRound(i[1]));
    Point b = Point(cvRound(i[2]), cvRound(i[3]));
    double edist_i = norm(a-b);
    a = Point(cvRound(j[0]), cvRound(j[1]));
    b = Point(cvRound(j[2]), cvRound(j[3]));
    double edist_j = norm(a-b);
    return (edist_i>edist_j);
}

static int find_vertex(vector<vector<Point> > &forest, Point &p)
{
  for (int i=0; i<(int)forest.size(); i++)
  {
    for (int j=0; j<(int)forest[i].size(); j++)
    {
        if (forest[i][j] == p)
            return i;
    }
  }
  return -1;
}

static int getAngleABC( Point a, Point b, Point c )
{
    Point ab = Point( b.x - a.x, b.y - a.y );
    Point cb = Point( b.x - c.x, b.y - c.y );

    // dot product
    float dot = (float)(ab.x * cb.x + ab.y * cb.y);

    // length square of both vectors
    float abSqr = (float)(ab.x * ab.x + ab.y * ab.y);
    float cbSqr = (float)(cb.x * cb.x + cb.y * cb.y);

    // square of cosine of the needed angle
    float cosSqr = dot * dot / abSqr / cbSqr;

    // this is a known trigonometric equality:
    // cos(alpha * 2) = [ cos(alpha) ]^2 * 2 - 1
    float cos2 = 2 * cosSqr - 1;

    // Here's the only invocation of the heavy function.
    // It's a good idea to check explicitly if cos2 is within [-1 .. 1] range

    const float pi = 3.141592f;

    float alpha2 =
        (cos2 <= -1) ? pi :
        (cos2 >= 1) ? 0 :
        acosf(cos2);

    float rslt = alpha2 / 2;

    float rs = (float)(rslt * 180. / pi);


    // Now revolve the ambiguities.
    // 1. If dot product of two vectors is negative - the angle is definitely
    // above 90 degrees. Still we have no information regarding the sign of the angle.

    // NOTE: This ambiguity is the consequence of our method: calculating the cosine
    // of the double angle. This allows us to get rid of calling sqrt.

    if (dot < 0)
        rs = 180 - rs;

    // 2. Determine the sign. For this we'll use the Determinant of two vectors.
    float det = (float)(ab.x * cb.y - ab.y * cb.y);
    if (det < 0)
        rs = -rs;

    return abs((int) floor(rs + 0.5));
}

double MaxMeaningfulClustering::probability(vector<int> &cluster)
{

    if (cluster.size()>MAX_GROUP_ELEMENTS)
        return 0.;

    vector<float> sample;
    sample.push_back((float)cluster.size());

    Mat diameters      ( (int)cluster.size(), 1, CV_32F, 1 );
    Mat strokes        ( (int)cluster.size(), 1, CV_32F, 1 );
    Mat gradients      ( (int)cluster.size(), 1, CV_32F, 1 );
    Mat fg_intensities ( (int)cluster.size(), 1, CV_32F, 1 );
    Mat bg_intensities ( (int)cluster.size(), 1, CV_32F, 1 );
    Mat axial_ratios   ( (int)cluster.size(), 1, CV_32F, 1 );
    Mat chull_ratios   ( (int)cluster.size(), 1, CV_32F, 1 );
    Mat convexities    ( (int)cluster.size(), 1, CV_32F, 1 );
    Subdiv2D subdiv(Rect(0,0,imsize.width,imsize.height));
    vector< vector<Point> > forest(cluster.size());
    float maxAvgOverlap = 0;

    for (int i=(int)cluster.size()-1; i>=0; i--)
    {

        diameters.at<float>(i,0)      = (float)max(regions.at(cluster.at(i)).rect.width,regions.at(cluster.at(i)).rect.height);
        strokes.at<float>(i,0)        = (float)regions.at(cluster.at(i)).stroke_mean;
        gradients.at<float>(i,0)      = (float)regions.at(cluster.at(i)).gradient_mean;
        fg_intensities.at<float>(i,0) = (float)regions.at(cluster.at(i)).intensity_mean;
        bg_intensities.at<float>(i,0) = (float)regions.at(cluster.at(i)).boundary_intensity_mean;
        axial_ratios.at<float>(i,0)   = (float)regions.at(cluster.at(i)).axial_ratio;
        chull_ratios.at<float>(i,0)   = (float)regions.at(cluster.at(i)).convex_hull_ratio;
        convexities.at<float>(i,0)    = (float)regions.at(cluster.at(i)).convexities;

        Point2f fp((float)regions.at(cluster.at(i)).rect.x+(regions.at(cluster.at(i)).rect.width/2),
                   (float)regions.at(cluster.at(i)).rect.y+(regions.at(cluster.at(i)).rect.height/2));
        subdiv.insert(fp);
        forest[i].push_back(Point((int)fp.x,(int)fp.y));
        float avgOverlap = 0;
        for (int j=0; j<(int)cluster.size(); j++)
        {
            if (j!=i)
            {
                Rect intersection = regions.at(cluster.at(i)).rect & regions.at(cluster.at(j)).rect;
                int area_intersection = intersection.width * intersection.height;
                int area_i = regions.at(cluster.at(i)).rect.width * regions.at(cluster.at(i)).rect.height;
                int area_j = regions.at(cluster.at(j)).rect.width * regions.at(cluster.at(j)).rect.height;
                if (area_intersection > 0)
                {
                    float overlap = (float)area_intersection / min(area_i, area_j);
                    avgOverlap += overlap;
                }
            }
        }
        avgOverlap = avgOverlap / (cluster.size()-1);
        if (avgOverlap > maxAvgOverlap)
            maxAvgOverlap = avgOverlap;
    }

    Scalar mean,std;
    meanStdDev( diameters, mean, std );
    sample.push_back((float)(std[0]/mean[0])); float diameter_mean = (float)mean[0];
    meanStdDev( strokes, mean, std );
    sample.push_back((float)(std[0]/mean[0]));
    meanStdDev( gradients, mean, std );
    sample.push_back((float)std[0]);
    meanStdDev( fg_intensities, mean, std );
    sample.push_back((float)std[0]);
    meanStdDev( bg_intensities, mean, std );
    sample.push_back((float)std[0]);

    /* begin Kruskal algorithm to find the MST */
    vector<Vec4f> edgeList;
    subdiv.getEdgeList(edgeList);
    sort (edgeList.begin(), edgeList.end(), edge_comp);

    vector<Vec4f> mst_edges;
    vector<float> edge_distances;

    for( int k = (int)edgeList.size()-1; k>=0; k-- )
    {
        Vec4f e = edgeList[k];
        Point pt0 = Point(cvRound(e[0]), cvRound(e[1]));
        Point pt1 = Point(cvRound(e[2]), cvRound(e[3]));
        int tree_pt0  = find_vertex(forest, pt0);
        int tree_pt1  = find_vertex(forest, pt1);
        if (((pt0.x>0)&&(pt0.x<imsize.width)&&(pt0.y>0)&&(pt0.y<imsize.height) &&
             (pt1.x>0)&&(pt1.x<imsize.width)&&(pt1.y>0)&&(pt1.y<imsize.height)) &&
                (tree_pt0 != tree_pt1))
        {
            mst_edges.push_back(e);
            forest[tree_pt0].insert(forest[tree_pt0].begin(),forest[tree_pt1].begin(),forest[tree_pt1].end());
            forest.erase(forest.begin()+tree_pt1);
            edge_distances.push_back((float)norm(pt0-pt1));
        }
        if (mst_edges.size() == cluster.size()-1)
            break;
    }

    //cout << "mst has " << mst_edges.size() << " edges " << endl;

    /* End Kruskal algorithm */

    vector<float> angles;
    for (size_t k=0; k<mst_edges.size(); k++)
    {
        Vec4f q = mst_edges[k];
        Point q_pt0 = Point(cvRound(q[0]), cvRound(q[1]));
        Point q_pt1 = Point(cvRound(q[2]), cvRound(q[3]));
        for (size_t j=k+1; j<mst_edges.size(); j++)
        {
            Vec4f t = mst_edges[j];
            Point t_pt0 = Point(cvRound(t[0]), cvRound(t[1]));
            Point t_pt1 = Point(cvRound(t[2]), cvRound(t[3]));
            if(q_pt0 == t_pt0)
                angles.push_back((float)getAngleABC(q_pt1, q_pt0 , t_pt1));
            if(q_pt0 == t_pt1)
                angles.push_back((float)getAngleABC(q_pt1, q_pt0 , t_pt0));
            if(q_pt1 == t_pt0)
                angles.push_back((float)getAngleABC(q_pt0, q_pt1 , t_pt1));
            if(q_pt1 == t_pt1)
                angles.push_back((float)getAngleABC(q_pt0, q_pt1 , t_pt0));
        }
    }
    //cout << "we have " << angles.size() << " angles " << endl;
    //for (int kk=0; kk<angles.size(); kk++)
    //  cout << angles[kk] << " ";
    //cout << endl;
    if (angles.empty() || edge_distances.empty())
        return 0;
    meanStdDev( angles, mean, std );
    sample.push_back((float)std[0]);
    sample.push_back((float)mean[0]);
    meanStdDev( edge_distances, mean, std );
    sample.push_back((float)(std[0]/mean[0]));
    sample.push_back((float)(mean[0]/diameter_mean));

    meanStdDev( axial_ratios, mean, std );
    sample.push_back((float)mean[0]);
    sample.push_back((float)std[0]);

    /// Calculate average shape self-similarity
    double avg_shape_match = 0;
    double eps = 1.e-5;
    int num_matches = 0, sma, smb;
    for (size_t i=0; i<cluster.size(); i++)
    {
        for (size_t j=i+1; j<cluster.size(); j++)
        {
            for (int h=0; h<7; h++)
            {
                double ama = fabs( regions[cluster[i]].hu_moments[h] );
                double amb = fabs( regions[cluster[j]].hu_moments[h] );

                if( regions[cluster[i]].hu_moments[h] > 0 )
                    sma = 1;
                else if( regions[cluster[i]].hu_moments[h] < 0 )
                    sma = -1;
                else
                    sma = 0;
                if( regions[cluster[j]].hu_moments[h] > 0 )
                    smb = 1;
                else if( regions[cluster[j]].hu_moments[h] < 0 )
                    smb = -1;
                else
                    smb = 0;

                if( ama > eps && amb > eps )
                {
                    ama = 1. / (sma * log10( ama ));
                    amb = 1. / (smb * log10( amb ));
                    avg_shape_match += fabs( -ama + amb );
                }
            }
            num_matches++;
        }
    }

    sample.push_back((float)(avg_shape_match/num_matches));

    sample.push_back(maxAvgOverlap);

    meanStdDev( chull_ratios, mean, std );
    sample.push_back((float)mean[0]);
    sample.push_back((float)std[0]);

    meanStdDev( convexities, mean, std );
    sample.push_back((float)mean[0]);
    sample.push_back((float)std[0]);

    float votes_group = group_boost->predict( Mat(sample), noArray(), DTrees::PREDICT_SUM | StatModel::RAW_OUTPUT);

    return (double)1-(double)1/(1+exp(-2*votes_group));
}


/* fast thinning for stroke width calculation */
bool guo_hall_thinning(const Mat1b & img, Mat& skeleton);

bool guo_hall_thinning(const Mat1b & img, Mat& skeleton)
{

  uchar* img_ptr = img.data;
  uchar* skel_ptr = skeleton.data;

  for (int row = 0; row < img.rows; ++row)
  {
    for (int col = 0; col < img.cols; ++col)
    {
      if (*img_ptr)
      {
        int key = row * img.cols + col;
        if ((col > 0 && *img_ptr != img.data[key - 1]) ||
            (col < img.cols-1 && *img_ptr != img.data[key + 1]) ||
            (row > 0 && *img_ptr != img.data[key - img.cols]) ||
            (row < img.rows-1 && *img_ptr != img.data[key + img.cols]))
        {
          *skel_ptr = 255;
        }
        else
        {
          *skel_ptr = 128;
        }
      }
      img_ptr++;
      skel_ptr++;
    }
  }

  int max_iters = 10000;
  int niters = 0;
  bool changed = false;

  /// list of keys to set to 0 at the end of the iteration
  deque<int> cols_to_set;
  deque<int> rows_to_set;

  while (changed && niters < max_iters)
  {
    changed = false;
    for (unsigned short iter = 0; iter < 2; ++iter)
    {
      uchar *skeleton_ptr = skeleton.data;
      rows_to_set.clear();
      cols_to_set.clear();
      // for each point in skeleton, check if it needs to be changed
      for (int row = 0; row < skeleton.rows; ++row)
      {
        for (int col = 0; col < skeleton.cols; ++col)
        {
          if (*skeleton_ptr++ == 255)
          {
            bool p2, p3, p4, p5, p6, p7, p8, p9;
            p2 = (skeleton.data[(row-1) * skeleton.cols + col]) > 0;
            p3 = (skeleton.data[(row-1) * skeleton.cols + col+1]) > 0;
            p4 = (skeleton.data[row     * skeleton.cols + col+1]) > 0;
            p5 = (skeleton.data[(row+1) * skeleton.cols + col+1]) > 0;
            p6 = (skeleton.data[(row+1) * skeleton.cols + col]) > 0;
            p7 = (skeleton.data[(row+1) * skeleton.cols + col-1]) > 0;
            p8 = (skeleton.data[row     * skeleton.cols + col-1]) > 0;
            p9 = (skeleton.data[(row-1) * skeleton.cols + col-1]) > 0;

            int C  = (!p2 && (p3 || p4)) + (!p4 && (p5 || p6)) +
                    (!p6 && (p7 || p8)) + (!p8 && (p9 || p2));
            int N1 = (p9 || p2) + (p3 || p4) + (p5 || p6) + (p7 || p8);
            int N2 = (p2 || p3) + (p4 || p5) + (p6 || p7) + (p8 || p9);
            int N  = N1 < N2 ? N1 : N2;
            int m  = iter == 0 ? ((p6 || p7 || !p9) && p8) : ((p2 || p3 || !p5) && p4);

            if ((C == 1) && (N >= 2) && (N <= 3) && (m == 0))
            {
              cols_to_set.push_back(col);
              rows_to_set.push_back(row);
            }
          }
        }
      }

      // set all points in rows_to_set (of skel)
      unsigned int rows_to_set_size = (unsigned int)rows_to_set.size();
      for (unsigned int pt_idx = 0; pt_idx < rows_to_set_size; ++pt_idx)
      {
        if (!changed)
          changed = (skeleton.data[rows_to_set[pt_idx] * skeleton.cols + cols_to_set[pt_idx]]) > 0;

        int key = rows_to_set[pt_idx] * skeleton.cols + cols_to_set[pt_idx];
        skeleton.data[key] = 0;
        if (cols_to_set[pt_idx] > 0 && skeleton.data[key - 1] == 128) // left
            skeleton.data[key - 1] = 255;
        if (cols_to_set[pt_idx] < skeleton.cols-1 && skeleton.data[key + 1] == 128) // right
            skeleton.data[key + 1] = 255;
        if (rows_to_set[pt_idx] > 0 && skeleton.data[key - skeleton.cols] == 128) // up
            skeleton.data[key - skeleton.cols] = 255;
        if (rows_to_set[pt_idx] < skeleton.rows-1 && skeleton.data[key + skeleton.cols] == 128) // down
            skeleton.data[key + skeleton.cols] = 255;
      }

      if ((niters++) >= max_iters) // we have done!
        break;
    }
  }

  skeleton = (skeleton != 0);
  return true;
}


static float extract_features(Mat &grey, Mat& channel, vector<ERStat> &regions, vector<ERFeatures> &features)
{
    // assert correct image type
    CV_Assert(( channel.type() == CV_8UC1 ) && ( grey.type() == CV_8UC1 ));
    CV_Assert( channel.size() == grey.size() );

    CV_Assert( !regions.empty() );
    CV_Assert( features.empty() );

    Mat gradient_magnitude = Mat_<double>(grey.size());
    get_gradient_magnitude( grey, gradient_magnitude);

    Mat region_mask = Mat::zeros(grey.rows+2, grey.cols+2, CV_8UC1);

    float max_stroke = 0;

    for (int r=0; r<(int)regions.size(); r++)
    {
        ERFeatures f;
        ERStat *stat = &regions.at(r);

        f.area = stat->area;
        f.rect = stat->rect;
        f.center = Point(f.rect.x+(f.rect.width/2),f.rect.y+(f.rect.height/2));

        if (regions.at(r).parent != NULL)
        {

            //Fill the region and calculate features
            Mat region = region_mask(Rect(stat->rect.tl(),
                                          stat->rect.br() + Point(2,2)));
            region = Scalar(0);
            int newMaskVal = 255;
            int flags = 4 + (newMaskVal << 8) + FLOODFILL_FIXED_RANGE + FLOODFILL_MASK_ONLY;

            floodFill( channel(stat->rect),
                       region, Point(stat->pixel%channel.cols - stat->rect.x, stat->pixel/channel.cols - stat->rect.y),
                       Scalar(255), NULL, Scalar(stat->level), Scalar(0), flags );
            Mat rect_mask = region_mask(Rect(stat->rect.x+1,stat->rect.y+1,stat->rect.width,stat->rect.height));


            Scalar mean,std;
            meanStdDev( grey(stat->rect), mean, std, rect_mask);
            f.intensity_mean = (float)mean[0];
            f.intensity_std  = (float)std[0];

            Mat tmp,bw;
            rect_mask.copyTo(bw);
            distanceTransform(bw, tmp, DIST_L1,3); //L1 gives distance in round integers while L2 floats

            // Add border because if region span all the image size skeleton will crash
            copyMakeBorder(bw, bw, 5, 5, 5, 5, BORDER_CONSTANT, Scalar(0));
            Mat skeleton = Mat::zeros(bw.size(),CV_8UC1);
            guo_hall_thinning(bw,skeleton);
            Mat mask;
            skeleton(Rect(5,5,bw.cols-10,bw.rows-10)).copyTo(mask);
            bw(Rect(5,5,bw.cols-10,bw.rows-10)).copyTo(bw);
            meanStdDev(tmp,mean,std,mask);
            f.stroke_mean = mean[0];
            f.stroke_std  = std[0];

            if (f.stroke_mean > max_stroke)
                max_stroke = (float)f.stroke_mean;

            Mat element = getStructuringElement( MORPH_RECT, Size(5, 5), Point(2, 2) );
            dilate(rect_mask, tmp, element);
            absdiff(tmp, rect_mask, tmp);

            meanStdDev( grey(stat->rect), mean, std, tmp);
            f.boundary_intensity_mean = (float)mean[0];
            f.boundary_intensity_std  = (float)std[0];

            Mat tmp2;
            dilate(rect_mask, tmp, element);
            erode (rect_mask, tmp2, element);
            absdiff(tmp, tmp2, tmp);

            meanStdDev( gradient_magnitude(stat->rect), mean, std, tmp);
            f.gradient_mean = mean[0];
            f.gradient_std  = std[0];

            copyMakeBorder(bw, bw, 5, 5, 5, 5, BORDER_CONSTANT, Scalar(0));

            vector<vector<Point> > contours0;
            vector<Vec4i> hierarchy;
            findContours( bw, contours0, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);

            RotatedRect rrect = minAreaRect(contours0.at(0));

            f.axial_ratio = max(rrect.size.width, rrect.size.height) / min(rrect.size.width, rrect.size.height);

            Moments mu = moments(contours0.at(0));
            HuMoments (mu, f.hu_moments);

            vector<Point> hull;
            convexHull(contours0[0],hull);
            f.convex_hull_ratio = (float)contourArea(hull)/contourArea(contours0[0]);
            vector<Vec4i> cx;
            vector<int> hull_idx;
            //TODO check epsilon parameter of approxPolyDP (set empirically) : we want more precision
            //     if the region is very small because otherwise we'll loose all the convexities
            approxPolyDP( Mat(contours0[0]), contours0[0], (float)min(rrect.size.width,rrect.size.height)/17, true );
            convexHull(contours0[0],hull_idx,false,false);
            f.convexities = 0;
            if (hull_idx.size()>2)
                if (contours0[0].size()>3)
                    convexityDefects(contours0[0],hull_idx,cx);
            f.convexities = (int)cx.size();

            rect_mask = Scalar(0);

        } else {

            f.intensity_mean = 0;
            f.intensity_std  = 0;

            f.stroke_mean = 0;
            f.stroke_std  = 0;

            f.boundary_intensity_mean = 0;
            f.boundary_intensity_std  = 0;

            f.gradient_mean = 0;
            f.gradient_std  = 0;
        }

        features.push_back(f);
    }

    return max_stroke;
}


/*!
    Find groups of Extremal Regions that are organized as text blocks. This function implements
    the grouping algorithm described in:
    Gomez L. and Karatzas D.: A Fast Hierarchical Method for Multi-script and Arbitrary Oriented
                              Scene Text Extraction, arXiv:1407.7504 [cs.CV].
    Gomez L. and Karatzas D.: Multi-script Text Extraction from Natural Scenes, ICDAR 2013.

    \param  _image         Original RGB image from wich the regions were extracted.
    \param  _src           Vector of sinle channel images CV_8UC1 from wich the regions were extracted.
    \param  regions        Vector of ER's retrieved from the ERFilter algorithm from each channel
    \param  groups         The output of the algorithm are stored in this parameter as list of indexes to provided regions.
    \param  text_boxes     The output of the algorithm are stored in this parameter as list of rectangles.
    \param  filename       The XML or YAML file with the classifier model (e.g. trained_classifier_erGrouping.xml)
    \param  minProbability The minimum probability for accepting a group
*/
static void erGroupingGK(InputArray _image, InputArrayOfArrays _src, vector<vector<ERStat> > &regions, vector<vector<Vec2i> > &groups,  vector<Rect> &text_boxes, const string& filename, float minProbability)
{

    CV_Assert( _image.getMat().type() == CV_8UC3 );
    // TODO assert correct vector<Mat>

    Mat image = _image.getMat();
    Mat grey;
    cvtColor(image, grey, COLOR_BGR2GRAY);

    vector<Mat> src;
    _src.getMatVector(src);

    CV_Assert ( !src.empty() );
    CV_Assert ( src.size() == regions.size() );

    if (!text_boxes.empty())
    {
        text_boxes.clear();
    }

    for (int c=0; c<(int)src.size(); c++)
    {
        Mat channel = src.at(c);

        // assert correct image type
        CV_Assert( channel.type() == CV_8UC1 );

        //CV_Assert( !regions.at(c).empty() );

        if ( regions.at(c).size() < 3 )
            continue;


        vector<vector<int> > meaningful_clusters;
        vector<ERFeatures> features;
        float max_stroke = extract_features(grey, channel, regions.at(c), features);



        // Find the Max. Meaningful Clusters in the learned feature space

        unsigned int N = (unsigned int)regions.at(c).size();
        int dim = 7; //dimensionality of feature space
        double *data = (double*)malloc(dim*N * sizeof(double));
        if (data == NULL)
            CV_Error(Error::StsNoMem, "Not enough Memory for erGrouping hierarchical clustering structures!");

        //Learned weights
        float weight_param1 = 1.00f;
        float weight_param2 = 0.65f;
        float weight_param3 = 0.65f;
        float weight_param4 = 0.49f;
        float weight_param5 = 0.67f;
        float weight_param6 = 0.91f;

        int count = 0;
        for (int i=0; i<(int)regions.at(c).size(); i++)
        {
            data[count] = (double)features.at(i).center.x/channel.cols*weight_param1;
            data[count+1] = (double)features.at(i).center.y/channel.rows*weight_param1;
            data[count+2] = (double)features.at(i).intensity_mean/255*weight_param2;
            data[count+3] = (double)features.at(i).boundary_intensity_mean/255*weight_param3;
            data[count+4] = (double)max(features.at(i).rect.height,features.at(i).rect.width)/
                                    max(channel.rows,channel.cols)*weight_param5;
            data[count+5] = (double)features.at(i).stroke_mean/max_stroke*weight_param6;
            data[count+6] = (double)features.at(i).gradient_mean/255*weight_param4;

            count = count+dim;
        }

        MaxMeaningfulClustering   mm_clustering(METHOD_METR_SINGLE, METRIC_SEUCLIDEAN, features, Size(channel.cols,channel.rows), filename, minProbability);
        mm_clustering(data, N, dim, METHOD_METR_SINGLE, METRIC_SEUCLIDEAN, &meaningful_clusters);

        free(data);

        for (size_t k=0; k<meaningful_clusters.size(); k++)
        {
            if (meaningful_clusters[k].size()>2)
            {
                Rect group_rect = features[meaningful_clusters[k][0]].rect;
                vector<Vec2i> group;
                group.push_back(Vec2i(c,meaningful_clusters[k][0]));
                for (size_t l=1; l<meaningful_clusters[k].size(); l++)
                {
                    group_rect = group_rect | features[meaningful_clusters[k][l]].rect;
                    group.push_back(Vec2i(c,meaningful_clusters[k][l]));
                }
                text_boxes.push_back(group_rect);
                groups.push_back(group);
            }
        }

        //getLines(img, &regions, &final_clusters, line_rects, line_regions, multi_oriented);

        meaningful_clusters.clear();
        features.clear();
    }
}

/**************************************/
/*Exhaustive Search grouping algorithm*/
/**************************************/

//threshold values for the Exhaustive Search algorithm (learned from training dataset)
#define PAIR_MIN_HEIGHT_RATIO     0.4
#define PAIR_MIN_CENTROID_ANGLE - 0.85
#define PAIR_MAX_CENTROID_ANGLE   0.85
#define PAIR_MIN_REGION_DIST    - 0.4
#define PAIR_MAX_REGION_DIST      2.2
#define PAIR_MAX_INTENSITY_DIST   111
#define PAIR_MAX_AB_DIST          54

#define TRIPLET_MAX_DIST          0.9
#define TRIPLET_MAX_SLOPE         0.3

#define SEQUENCE_MAX_TRIPLET_DIST 0.45
#define SEQUENCE_MIN_LENGHT       4

// struct line_estimates
// Represents a line estimate (as above) for an ER's group
// i.e.: slope and intercept of 2 top and 2 bottom lines
struct line_estimates
{
    float top1_a0;
    float top1_a1;
    float top2_a0;
    float top2_a1;
    float bottom1_a0;
    float bottom1_a1;
    float bottom2_a0;
    float bottom2_a1;
    int x_min;
    int x_max;
    int h_max;
    bool operator==(const line_estimates& e) const
    {
        return ( (top1_a0 == e.top1_a0) && (top1_a1 == e.top1_a1) && (top2_a0 == e.top2_a0) &&
        (top2_a1 == e.top2_a1) && (bottom1_a0 == e.bottom1_a0) && (bottom1_a1 == e.bottom1_a1) &&
        (bottom2_a0 == e.bottom2_a0) && (bottom2_a1 == e.bottom2_a1) && (x_min == e.x_min) &&
        (x_max == e.x_max) && (h_max == e.h_max) );
    }
};

// distanceLinesEstimates
// Calculates the distance between two line estimates defined as the largest
// normalized vertical difference of their top/bottom lines at their boundary points
// out float distance
float distanceLinesEstimates(line_estimates &a, line_estimates &b);

float distanceLinesEstimates(line_estimates &a, line_estimates &b)
{
    CV_Assert( (a.h_max != 0) && ( b.h_max != 0));

    if (a == b)
        return 0.0f;

    int x_min = min(a.x_min, b.x_min);
    int x_max = max(a.x_max, b.x_max);
    int h_max = max(a.h_max, b.h_max);

    float dist_top = FLT_MAX, dist_bottom = FLT_MAX;
    for (int i=0; i<2; i++)
    {
        float top_a0, top_a1, bottom_a0, bottom_a1;
        if (i == 0)
        {
            top_a0 = a.top1_a0;
            top_a1 = a.top1_a1;
            bottom_a0 = a.bottom1_a0;
            bottom_a1 = a.bottom1_a1;
        } else {
            top_a0 = a.top2_a0;
            top_a1 = a.top2_a1;
            bottom_a0 = a.bottom2_a0;
            bottom_a1 = a.bottom2_a1;
        }
        for (int j=0; j<2; j++)
        {
            float top_b0, top_b1, bottom_b0, bottom_b1;
            if (j==0)
            {
                top_b0 = b.top1_a0;
                top_b1 = b.top1_a1;
                bottom_b0 = b.bottom1_a0;
                bottom_b1 = b.bottom1_a1;
            } else {
                top_b0 = b.top2_a0;
                top_b1 = b.top2_a1;
                bottom_b0 = b.bottom2_a0;
                bottom_b1 = b.bottom2_a1;
            }

            float x_min_dist = abs((top_a0+x_min*top_a1) - (top_b0+x_min*top_b1));
            float x_max_dist = abs((top_a0+x_max*top_a1) - (top_b0+x_max*top_b1));
            dist_top    = min(dist_top, max(x_min_dist,x_max_dist)/h_max);

            x_min_dist  = abs((bottom_a0+x_min*bottom_a1) - (bottom_b0+x_min*bottom_b1));
            x_max_dist  = abs((bottom_a0+x_max*bottom_a1) - (bottom_b0+x_max*bottom_b1));
            dist_bottom = min(dist_bottom, max(x_min_dist,x_max_dist)/h_max);
        }
    }
    return max(dist_top, dist_bottom);
}

// struct region_pair
// Represents a pair of ER's
struct region_pair
{
    Vec2i a;
    Vec2i b;
    region_pair (Vec2i _a, Vec2i _b) : a(_a), b(_b) {}
    bool operator==(const region_pair& p1) const
    {
        return ( (p1.a == a) && (p1.b == b) );
    }
};

// struct region_triplet
// Represents a triplet of ER's
struct region_triplet
{
    Vec2i a;
    Vec2i b;
    Vec2i c;
    line_estimates estimates;
    region_triplet (Vec2i _a, Vec2i _b, Vec2i _c) : a(_a), b(_b), c(_c) {}
    bool operator==(const region_triplet& t1) const
    {
        return ( (t1.a == a) && (t1.b == b) && (t1.c == c) );
    }
};

// struct region_sequence
// Represents a sequence of more than three ER's
struct region_sequence
{
    vector<region_triplet> triplets;
    region_sequence (region_triplet t)
    {
        triplets.push_back(t);
    }
    region_sequence () {}
};

// Evaluates if a pair of regions is valid or not
// using thresholds learned on training (defined above)
bool isValidPair(Mat &grey, Mat& lab, Mat& mask, vector<Mat> &channels, vector< vector<ERStat> >& regions, Vec2i idx1, Vec2i idx2);

// Evaluates if a set of 3 regions is valid or not
// using thresholds learned on training (defined above)
bool isValidTriplet(vector< vector<ERStat> >& regions, region_pair pair1, region_pair pair2, region_triplet &triplet);

// Evaluates if a set of more than 3 regions is valid or not
// using thresholds learned on training (defined above)
bool isValidSequence(region_sequence &sequence1, region_sequence &sequence2);

// Check if two sequences share a region in common
bool haveCommonRegion(region_sequence &sequence1, region_sequence &sequence2);
// Check if two triplets share a region in common
bool haveCommonRegion(region_triplet &t1, region_triplet &t2);

// Takes as input the set of ER's extracted by ERFilter
// then finds for all valid pairs and triplets.
// in regions the set of ER's extracted by ERFilter
// in _src the channels from which the ER's were extracted
// out sets of regions, each one represents a possible text line
void erGroupingNM(InputArray _img, InputArrayOfArrays _src, vector< vector<ERStat> >& regions,  vector< vector<Vec2i> >& groups, vector<Rect> &boxes, bool do_feedback_loop);

// Fit line from two points
// out a0 is the intercept
// out a1 is the slope
void fitLine(Point p1, Point p2, float &a0, float &a1);

// Fit line from three points using Ordinary Least Squares
// out a0 is the intercept
// out a1 is the slope
void fitLineOLS(Point p1, Point p2, Point p3, float &a0, float &a1);

// Fit line from three points using (heuristic) Least-Median of Squares
// out a0 is the intercept
// out a1 is the slope
// returns the error of the single point that doesn't fit the line
float fitLineLMS(Point p1, Point p2, Point p3, float &a0, float &a1);

// Fit a line_estimate to a group of 3 regions
// out triplet.estimates is updated with the new line estimates
bool fitLineEstimates(vector< vector<ERStat> > &regions, region_triplet &triplet);

// Fit line from two points
// out a0 is the intercept
// out a1 is the slope
void fitLine(Point p1, Point p2, float &a0, float &a1)
{
    CV_Assert ( p1.x != p2.x );

    a1 = (float)(p2.y - p1.y) / (p2.x - p1.x);
    a0 = a1 * -1 * p1.x + p1.y;
}

// Fit line from three points using Ordinary Least Squares
// out a0 is the intercept
// out a1 is the slope
void fitLineOLS(Point p1, Point p2, Point p3, float &a0, float &a1)
{
    float sumx  = (float)(p1.x + p2.x + p3.x);
    float sumy  = (float)(p1.y + p2.y + p3.y);
    float sumxy = (float)(p1.x*p1.y + p2.x*p2.y + p3.x*p3.y);
    float sumx2 = (float)(p1.x*p1.x + p2.x*p2.x + p3.x*p3.x);

    // line coefficients
    a0=(float)(sumy*sumx2-sumx*sumxy) / (3*sumx2-sumx*sumx);
    a1=(float)(3*sumxy-sumx*sumy) / (3*sumx2-sumx*sumx);
}

// Fit line from three points using (heuristic) Least-Median of Squares
// out a0 is the intercept
// out a1 is the slope
// returns the error of the single point that doesn't fit the line
float fitLineLMS(Point p1, Point p2, Point p3, float &a0, float &a1)
{
    //if this is not changed the line is not valid
    a0 = -1;
    a1 = 0;

    //Least-Median of Squares does not make sense with only three points
    //because any line passing by two of them has median_error = 0
    //So we'll take the one with smaller slope
    float l_a0, l_a1, best_slope=FLT_MAX, err=0;

    if (p1.x != p2.x)
    {
        fitLine(p1,p2,l_a0,l_a1);;
        if (abs(l_a1) < best_slope)
        {
            best_slope = abs(l_a1);
            a0 = l_a0;
            a1 = l_a1;
            err = (p3.y - (a0+a1*p3.x));
        }
    }


    if (p1.x != p3.x)
    {
        fitLine(p1,p3,l_a0,l_a1);
        if (abs(l_a1) < best_slope)
        {
            best_slope = abs(l_a1);
            a0 = l_a0;
            a1 = l_a1;
            err = (p2.y - (a0+a1*p2.x));
        }
    }


    if (p2.x != p3.x)
    {
        fitLine(p2,p3,l_a0,l_a1);
        if (abs(l_a1) < best_slope)
        {
            best_slope = abs(l_a1);
            a0 = l_a0;
            a1 = l_a1;
            err = (p1.y - (a0+a1*p1.x));
        }
    }
    return err;

}

// Fit a line_estimate to a group of 3 regions
// out triplet.estimates is updated with the new line estimates
bool fitLineEstimates(vector< vector<ERStat> > &regions, region_triplet &triplet)
{
    vector<Rect> char_boxes;
    char_boxes.push_back(regions[triplet.a[0]][triplet.a[1]].rect);
    char_boxes.push_back(regions[triplet.b[0]][triplet.b[1]].rect);
    char_boxes.push_back(regions[triplet.c[0]][triplet.c[1]].rect);

    triplet.estimates.x_min = min(min(char_boxes[0].tl().x,char_boxes[1].tl().x), char_boxes[2].tl().x);
    triplet.estimates.x_max = max(max(char_boxes[0].br().x,char_boxes[1].br().x), char_boxes[2].br().x);
    triplet.estimates.h_max = max(max(char_boxes[0].height,char_boxes[1].height), char_boxes[2].height);

    // Fit one bottom line
    float err = fitLineLMS(char_boxes[0].br(), char_boxes[1].br(), char_boxes[2].br(),
                           triplet.estimates.bottom1_a0, triplet.estimates.bottom1_a1);

    if ((triplet.estimates.bottom1_a0 == -1) && (triplet.estimates.bottom1_a1 == 0))
        return false;

    // Slope for all lines must be the same
    triplet.estimates.bottom2_a1 = triplet.estimates.bottom1_a1;
    triplet.estimates.top1_a1    = triplet.estimates.bottom1_a1;
    triplet.estimates.top2_a1    = triplet.estimates.bottom1_a1;

    if (abs(err) > (float)triplet.estimates.h_max/6)
    {
        // We need two different bottom lines
        triplet.estimates.bottom2_a0 = triplet.estimates.bottom1_a0 + err;
    }
    else
    {
        // Second bottom line is the same
        triplet.estimates.bottom2_a0 = triplet.estimates.bottom1_a0;
    }

    // Fit one top line within the two (Y)-closer coordinates
    int d_12 = abs(char_boxes[0].tl().y - char_boxes[1].tl().y);
    int d_13 = abs(char_boxes[0].tl().y - char_boxes[2].tl().y);
    int d_23 = abs(char_boxes[1].tl().y - char_boxes[2].tl().y);
    if ((d_12<d_13) && (d_12<d_23))
    {
        Point p = Point((char_boxes[0].tl().x + char_boxes[1].tl().x)/2,
                        (char_boxes[0].tl().y + char_boxes[1].tl().y)/2);
        triplet.estimates.top1_a0 = triplet.estimates.bottom1_a0 +
                (p.y - (triplet.estimates.bottom1_a0+p.x*triplet.estimates.bottom1_a1));
        p = char_boxes[2].tl();
        err = (p.y - (triplet.estimates.top1_a0+p.x*triplet.estimates.top1_a1));
    }
    else if (d_13<d_23)
    {
        Point p = Point((char_boxes[0].tl().x + char_boxes[2].tl().x)/2,
                        (char_boxes[0].tl().y + char_boxes[2].tl().y)/2);
        triplet.estimates.top1_a0 = triplet.estimates.bottom1_a0 +
                (p.y - (triplet.estimates.bottom1_a0+p.x*triplet.estimates.bottom1_a1));
        p = char_boxes[1].tl();
        err = (p.y - (triplet.estimates.top1_a0+p.x*triplet.estimates.top1_a1));
    }
    else
    {
        Point p = Point((char_boxes[1].tl().x + char_boxes[2].tl().x)/2,
                        (char_boxes[1].tl().y + char_boxes[2].tl().y)/2);
        triplet.estimates.top1_a0 = triplet.estimates.bottom1_a0 +
                (p.y - (triplet.estimates.bottom1_a0+p.x*triplet.estimates.bottom1_a1));
        p = char_boxes[0].tl();
        err = (p.y - (triplet.estimates.top1_a0+p.x*triplet.estimates.top1_a1));
    }

    if (abs(err) > (float)triplet.estimates.h_max/6)
    {
        // We need two different top lines
        triplet.estimates.top2_a0 = triplet.estimates.top1_a0 + err;
    }
    else
    {
        // Second top line is the same
        triplet.estimates.top2_a0 = triplet.estimates.top1_a0;
    }

    return true;
}


// Evaluates if a pair of regions is valid or not
// using thresholds learned on training (defined above)
bool isValidPair(Mat &grey, Mat &lab, Mat &mask, vector<Mat> &channels, vector< vector<ERStat> >& regions, Vec2i idx1, Vec2i idx2)
{
    Rect minarearect  = regions[idx1[0]][idx1[1]].rect | regions[idx2[0]][idx2[1]].rect;

    // Overlapping regions are not valid pair in any case
    if ( (minarearect == regions[idx1[0]][idx1[1]].rect) ||
         (minarearect == regions[idx2[0]][idx2[1]].rect) )
        return false;

    ERStat *i, *j;
    if (regions[idx1[0]][idx1[1]].rect.x < regions[idx2[0]][idx2[1]].rect.x)
    {
        i = &regions[idx1[0]][idx1[1]];
        j = &regions[idx2[0]][idx2[1]];
    } else {
        i = &regions[idx2[0]][idx2[1]];
        j = &regions[idx1[0]][idx1[1]];
    }

    if (j->rect.x == i->rect.x)
        return false;

    float height_ratio = (float)min(i->rect.height,j->rect.height) /
                                max(i->rect.height,j->rect.height);

    Point center_i(i->rect.x+i->rect.width/2, i->rect.y+i->rect.height/2);
    Point center_j(j->rect.x+j->rect.width/2, j->rect.y+j->rect.height/2);
    float centroid_angle = (float)atan2((float)(center_j.y-center_i.y), (float)(center_j.x-center_i.x));

    int avg_width = (i->rect.width + j->rect.width) / 2;
    float norm_distance = (float)(j->rect.x-(i->rect.x+i->rect.width))/avg_width;

    if (( height_ratio   < PAIR_MIN_HEIGHT_RATIO) ||
        ( centroid_angle < PAIR_MIN_CENTROID_ANGLE) ||
        ( centroid_angle > PAIR_MAX_CENTROID_ANGLE) ||
        ( norm_distance  < PAIR_MIN_REGION_DIST) ||
        ( norm_distance  > PAIR_MAX_REGION_DIST))
        return false;

    if ((i->parent == NULL)||(j->parent == NULL)) // deprecate the root region
      return false;

    i = &regions[idx1[0]][idx1[1]];
    j = &regions[idx2[0]][idx2[1]];

    Mat region = mask(Rect(i->rect.tl(),
                           i->rect.br()+ Point(2,2)));
    region = Scalar(0);

    int newMaskVal = 255;
    int flags = 4 + (newMaskVal << 8) + FLOODFILL_FIXED_RANGE + FLOODFILL_MASK_ONLY;

    floodFill( channels[idx1[0]](i->rect),
               region, Point(i->pixel%grey.cols, i->pixel/grey.cols) - i->rect.tl(),
               Scalar(255), NULL, Scalar(i->level), Scalar(0), flags);
    Mat rect_mask = mask(Rect(i->rect.x+1,i->rect.y+1,i->rect.width,i->rect.height));

    Scalar mean,std;
    meanStdDev(grey(i->rect),mean,std,rect_mask);
    int grey_mean1 = (int)mean[0];
    meanStdDev(lab(i->rect),mean,std,rect_mask);
    float a_mean1 = (float)mean[1];
    float b_mean1 = (float)mean[2];

    region = mask(Rect(j->rect.tl(), j->rect.br()+ Point(2,2)));
    region = Scalar(0);

    floodFill( channels[idx2[0]](j->rect),
               region, Point(j->pixel%grey.cols, j->pixel/grey.cols) - j->rect.tl(),
               Scalar(255), NULL, Scalar(j->level), Scalar(0), flags);
    rect_mask = mask(Rect(j->rect.x+1,j->rect.y+1,j->rect.width,j->rect.height));

    meanStdDev(grey(j->rect),mean,std,rect_mask);
    int grey_mean2 = (int)mean[0];
    meanStdDev(lab(j->rect),mean,std,rect_mask);
    float a_mean2 = (float)mean[1];
    float b_mean2 = (float)mean[2];

    if (abs(grey_mean1-grey_mean2) > PAIR_MAX_INTENSITY_DIST)
      return false;

    if (sqrt(pow(a_mean1-a_mean2,2)+pow(b_mean1-b_mean2,2)) > PAIR_MAX_AB_DIST)
      return false;



    return true;
}

// Evaluates if a set of 3 regions is valid or not
// using thresholds learned on training (defined above)
bool isValidTriplet(vector< vector<ERStat> >& regions, region_pair pair1, region_pair pair2, region_triplet &triplet)
{

    if (pair1 == pair2)
        return false;

    // At least one region in common is needed
    if ( (pair1.a == pair2.a)||(pair1.a == pair2.b)||(pair1.b == pair2.a)||(pair1.b == pair2.b) )
    {

        //fill the indexes in the output tripled (sorted)
        if (pair1.a == pair2.a)
        {
            if ((regions[pair1.b[0]][pair1.b[1]].rect.x <= regions[pair1.a[0]][pair1.a[1]].rect.x) &&
                    (regions[pair2.b[0]][pair2.b[1]].rect.x <= regions[pair1.a[0]][pair1.a[1]].rect.x))
                return false;
            if ((regions[pair1.b[0]][pair1.b[1]].rect.x >= regions[pair1.a[0]][pair1.a[1]].rect.x) &&
                    (regions[pair2.b[0]][pair2.b[1]].rect.x >= regions[pair1.a[0]][pair1.a[1]].rect.x))
                return false;

            triplet.a = (regions[pair1.b[0]][pair1.b[1]].rect.x <
                         regions[pair2.b[0]][pair2.b[1]].rect.x)? pair1.b : pair2.b;
            triplet.b = pair1.a;
            triplet.c = (regions[pair1.b[0]][pair1.b[1]].rect.x >
                         regions[pair2.b[0]][pair2.b[1]].rect.x)? pair1.b : pair2.b;

        } else if (pair1.a == pair2.b) {
            if ((regions[pair1.b[0]][pair1.b[1]].rect.x <= regions[pair1.a[0]][pair1.a[1]].rect.x) &&
                    (regions[pair2.a[0]][pair2.a[1]].rect.x <= regions[pair1.a[0]][pair1.a[1]].rect.x))
                return false;
            if ((regions[pair1.b[0]][pair1.b[1]].rect.x >= regions[pair1.a[0]][pair1.a[1]].rect.x) &&
                    (regions[pair2.a[0]][pair2.a[1]].rect.x >= regions[pair1.a[0]][pair1.a[1]].rect.x))
                return false;

            triplet.a = (regions[pair1.b[0]][pair1.b[1]].rect.x <
                         regions[pair2.a[0]][pair2.a[1]].rect.x)? pair1.b : pair2.a;
            triplet.b = pair1.a;
            triplet.c = (regions[pair1.b[0]][pair1.b[1]].rect.x >
                         regions[pair2.a[0]][pair2.a[1]].rect.x)? pair1.b : pair2.a;

        } else if (pair1.b == pair2.a) {
            if ((regions[pair1.a[0]][pair1.a[1]].rect.x <= regions[pair1.b[0]][pair1.b[1]].rect.x) &&
                    (regions[pair2.b[0]][pair2.b[1]].rect.x <= regions[pair1.b[0]][pair1.b[1]].rect.x))
                return false;
            if ((regions[pair1.a[0]][pair1.a[1]].rect.x >= regions[pair1.b[0]][pair1.b[1]].rect.x) &&
                    (regions[pair2.b[0]][pair2.b[1]].rect.x >= regions[pair1.b[0]][pair1.b[1]].rect.x))
                return false;

            triplet.a = (regions[pair1.a[0]][pair1.a[1]].rect.x <
                         regions[pair2.b[0]][pair2.b[1]].rect.x)? pair1.a : pair2.b;
            triplet.b = pair1.b;
            triplet.c = (regions[pair1.a[0]][pair1.a[1]].rect.x >
                         regions[pair2.b[0]][pair2.b[1]].rect.x)? pair1.a : pair2.b;

        } else if (pair1.b == pair2.b) {
            if ((regions[pair1.a[0]][pair1.a[1]].rect.x <= regions[pair1.b[0]][pair1.b[1]].rect.x) &&
                    (regions[pair2.a[0]][pair2.a[1]].rect.x <= regions[pair1.b[0]][pair1.b[1]].rect.x))
                return false;
            if ((regions[pair1.a[0]][pair1.a[1]].rect.x >= regions[pair1.b[0]][pair1.b[1]].rect.x) &&
                    (regions[pair2.a[0]][pair2.a[1]].rect.x >= regions[pair1.b[0]][pair1.b[1]].rect.x))
                return false;

            triplet.a = (regions[pair1.a[0]][pair1.a[1]].rect.x <
                         regions[pair2.a[0]][pair2.a[1]].rect.x)? pair1.a : pair2.a;
            triplet.b = pair1.b;
            triplet.c = (regions[pair1.a[0]][pair1.a[1]].rect.x >
                         regions[pair2.a[0]][pair2.a[1]].rect.x)? pair1.a : pair2.a;

        }



        if ( (regions[triplet.a[0]][triplet.a[1]].rect.x == regions[triplet.b[0]][triplet.b[1]].rect.x) &&
             (regions[triplet.a[0]][triplet.a[1]].rect.x == regions[triplet.c[0]][triplet.c[1]].rect.x) )
            return false;

        if ( (regions[triplet.a[0]][triplet.a[1]].rect.br().x == regions[triplet.b[0]][triplet.b[1]].rect.br().x) &&
             (regions[triplet.a[0]][triplet.a[1]].rect.br().x == regions[triplet.c[0]][triplet.c[1]].rect.br().x) )
            return false;


        if (!fitLineEstimates(regions, triplet))
            return false;

        if ( (triplet.estimates.bottom1_a0 < triplet.estimates.top1_a0) ||
             (triplet.estimates.bottom1_a0 < triplet.estimates.top2_a0) ||
             (triplet.estimates.bottom2_a0 < triplet.estimates.top1_a0) ||
             (triplet.estimates.bottom2_a0 < triplet.estimates.top2_a0) )
            return false;

        int central_height = (int)min(triplet.estimates.bottom1_a0, triplet.estimates.bottom2_a0) -
                             (int)max(triplet.estimates.top1_a0,triplet.estimates.top2_a0);
        int top_height     = (int)abs(triplet.estimates.top1_a0 - triplet.estimates.top2_a0);
        int bottom_height  = (int)abs(triplet.estimates.bottom1_a0 - triplet.estimates.bottom2_a0);

        if (central_height == 0)
            return false;

        float top_height_ratio    = (float)top_height/central_height;
        float bottom_height_ratio = (float)bottom_height/central_height;

        if ( (top_height_ratio > TRIPLET_MAX_DIST) || (bottom_height_ratio > TRIPLET_MAX_DIST) )
            return false;

        if (abs(triplet.estimates.bottom1_a1) > TRIPLET_MAX_SLOPE)
            return false;

        return true;
    }

    return false;
}

// Evaluates if a set of more than 3 regions is valid or not
// using thresholds learned on training (defined above)
bool isValidSequence(region_sequence &sequence1, region_sequence &sequence2)
{
    for (size_t i=0; i<sequence2.triplets.size(); i++)
    {
        for (size_t j=0; j<sequence1.triplets.size(); j++)
        {
            if ((distanceLinesEstimates(sequence2.triplets[i].estimates,
                                       sequence1.triplets[j].estimates) < SEQUENCE_MAX_TRIPLET_DIST) &&
                ((float)max((sequence2.triplets[i].estimates.x_min-sequence1.triplets[j].estimates.x_max),
                            (sequence1.triplets[j].estimates.x_min-sequence2.triplets[i].estimates.x_max))/
                        max(sequence2.triplets[i].estimates.h_max,sequence1.triplets[j].estimates.h_max) < 3*PAIR_MAX_REGION_DIST))
                return true;
        }
    }

    return false;
}

// Check if two triplets share a region in common
bool haveCommonRegion(region_triplet &t1, region_triplet &t2)
{
    if ((t1.a==t2.a) || (t1.a==t2.b) || (t1.a==t2.c) ||
        (t1.b==t2.a) || (t1.b==t2.b) || (t1.b==t2.c) ||
        (t1.c==t2.a) || (t1.c==t2.b) || (t1.c==t2.c))
      return true;

    return false;
}

// Check if two sequences share a region in common
bool haveCommonRegion(region_sequence &sequence1, region_sequence &sequence2)
{
    for (size_t i=0; i<sequence2.triplets.size(); i++)
    {
        for (size_t j=0; j<sequence1.triplets.size(); j++)
        {
            if (haveCommonRegion(sequence2.triplets[i], sequence1.triplets[j]))
                return true;
        }
    }

    return false;
}

bool sort_couples (Vec3i i,Vec3i j);
bool sort_couples (Vec3i i,Vec3i j) { return (i[0]<j[0]); }

/*!
    Find groups of Extremal Regions that are organized as text lines. This function implements
    the grouping algorithm described in:
    Neumann L., Matas J.: Real-Time Scene Text Localization and Recognition, CVPR 2012
    Neumann L., Matas J.: A method for text localization and detection, ACCV 2010

    \param  _img           Original RGB image from wich the regions were extracted.
    \param  _src           Vector of sinle channel images CV_8UC1 from wich the regions were extracted.
    \param  regions        Vector of ER's retrieved from the ERFilter algorithm from each channel
    \param  out_groups     The output of the algorithm are stored in this parameter as list of indexes to provided regions.
    \param  out_boxes      The output of the algorithm are stored in this parameter as list of rectangles.
    \param  do_feedback    Whenever the grouping algorithm uses a feedback loop to recover missing regions in a line.
*/

void erGroupingNM(InputArray _img, InputArrayOfArrays _src, vector< vector<ERStat> >& regions,
                  vector< vector<Vec2i> >& out_groups, vector<Rect>& out_boxes, bool do_feedback_loop)
{

    vector<Mat> src;
    _src.getMatVector(src);

    CV_Assert ( !src.empty() );
    //CV_Assert ( src.size() == regions.size() );
    size_t num_channels = src.size();

    Mat img = _img.getMat();

    //process each channel independently
    for(size_t c=0; c<num_channels; c++)
    {
        //store indices to regions in a single vector
        vector< Vec2i > all_regions;
        for(size_t r=0; r<regions[c].size(); r++)
        {
            all_regions.push_back(Vec2i((int)c,(int)r));
        }

        vector< region_pair > valid_pairs;
        Mat mask = Mat::zeros(img.rows+2, img.cols+2, CV_8UC1);
        Mat grey,lab;
        cvtColor(img, lab, COLOR_RGB2Lab);
        cvtColor(img, grey, COLOR_RGB2GRAY);

        //check every possible pair of regions
        for (size_t i=0; i<all_regions.size(); i++)
        {
            vector<int> i_siblings;
            int first_i_sibling_idx = (int)valid_pairs.size();
            for (size_t j=i+1; j<all_regions.size(); j++)
            {
                // check height ratio, centroid angle and region distance normalized by region width
                // fall within a given interval
                if (isValidPair(grey, lab, mask, src, regions, all_regions[i],all_regions[j]))
                {
                    bool isCycle = false;
                    for (size_t k=0; k<i_siblings.size(); k++)
                    {
                      if (isValidPair(grey, lab, mask, src, regions, all_regions[j],all_regions[i_siblings[k]]))
                      {
                        // choose as sibling the closer and not the first that was "paired" with i
                        Point i_center = Point( regions[all_regions[i][0]][all_regions[i][1]].rect.x +
                                                regions[all_regions[i][0]][all_regions[i][1]].rect.width/2,
                                                regions[all_regions[i][0]][all_regions[i][1]].rect.y +
                                                regions[all_regions[i][0]][all_regions[i][1]].rect.height/2 );
                        Point j_center = Point( regions[all_regions[j][0]][all_regions[j][1]].rect.x +
                                                regions[all_regions[j][0]][all_regions[j][1]].rect.width/2,
                                                regions[all_regions[j][0]][all_regions[j][1]].rect.y +
                                                regions[all_regions[j][0]][all_regions[j][1]].rect.height/2 );
                        Point k_center = Point( regions[all_regions[i_siblings[k]][0]][all_regions[i_siblings[k]][1]].rect.x +
                                                regions[all_regions[i_siblings[k]][0]][all_regions[i_siblings[k]][1]].rect.width/2,
                                                regions[all_regions[i_siblings[k]][0]][all_regions[i_siblings[k]][1]].rect.y +
                                                regions[all_regions[i_siblings[k]][0]][all_regions[i_siblings[k]][1]].rect.height/2 );

                        if ( norm(i_center - j_center) < norm(i_center - k_center) )
                        {
                          valid_pairs[first_i_sibling_idx+k] = region_pair(all_regions[i],all_regions[j]);
                          i_siblings[k] = (int)j;
                        }
                        isCycle = true;
                        break;
                      }
                    }
                    if (!isCycle)
                    {
                      valid_pairs.push_back(region_pair(all_regions[i],all_regions[j]));
                      i_siblings.push_back((int)j);
                      //cout << "Valid pair (" << all_regions[i][0] << ","  << all_regions[i][1] << ") (" << all_regions[j][0] << ","  << all_regions[j][1] << ")" << endl;
                    }
                }
            }
        }

        //cout << "GroupingNM : detected " << valid_pairs.size() << " valid pairs" << endl;

        vector< region_triplet > valid_triplets;

        //check every possible triplet of regions
        for (size_t i=0; i<valid_pairs.size(); i++)
        {
            for (size_t j=i+1; j<valid_pairs.size(); j++)
            {
                // check collinearity rules
                region_triplet valid_triplet(Vec2i(0,0),Vec2i(0,0),Vec2i(0,0));
                if (isValidTriplet(regions, valid_pairs[i],valid_pairs[j], valid_triplet))
                {
                    valid_triplets.push_back(valid_triplet);
                    //cout << "Valid triplet (" << valid_triplet.a[1] << "," <<  valid_triplet.b[1] << "," <<  valid_triplet.c[1] << ")" << endl;
                }
            }
        }

        //cout << "GroupingNM : detected " << valid_triplets.size() << " valid triplets" << endl;

        vector<region_sequence> valid_sequences;
        vector<region_sequence> pending_sequences;

        for (size_t i=0; i<valid_triplets.size(); i++)
        {
            pending_sequences.push_back(region_sequence(valid_triplets[i]));
        }


        for (size_t i=0; i<pending_sequences.size(); i++)
        {
            bool expanded = false;
            for (size_t j=i+1; j<pending_sequences.size(); j++)
            {
                if (isValidSequence(pending_sequences[i], pending_sequences[j]))
                {
                    expanded = true;
                    pending_sequences[i].triplets.insert(pending_sequences[i].triplets.begin(), pending_sequences[j].triplets.begin(), pending_sequences[j].triplets.end());
                    pending_sequences.erase(pending_sequences.begin()+j);
                    j--;
                }
            }
            if (expanded)
            {
                valid_sequences.push_back(pending_sequences[i]);
            }
        }

        // remove a sequence if one its regions is already grouped within a longer seq
        for (size_t i=0; i<valid_sequences.size(); i++)
        {
            for (size_t j=i+1; j<valid_sequences.size(); j++)
            {
              if (haveCommonRegion(valid_sequences[i],valid_sequences[j]))
              {
                if (valid_sequences[i].triplets.size() < valid_sequences[j].triplets.size())
                {
                  valid_sequences.erase(valid_sequences.begin()+i);
                  i--;
                  break;
                }
                else
                {
                  valid_sequences.erase(valid_sequences.begin()+j);
                  j--;
                }
              }
            }
        }


        //cout << "GroupingNM : detected " << valid_sequences.size() << " sequences." << endl;

        if (do_feedback_loop)
        {

            //Feedback loop of detected lines to region extraction ... tries to recover mismatches in the region decomposition step by extracting regions in the neighbourhood of a valid sequence and checking if they are consistent with its line estimates
            Ptr<ERFilter> er_filter = createERFilterNM1(loadDummyClassifier(),1,0.005f,0.3f,0.f,false);
            for (int i=0; i<(int)valid_sequences.size(); i++)
            {
                vector<Point> bbox_points;

                for (size_t j=0; j<valid_sequences[i].triplets.size(); j++)
                {
                    bbox_points.push_back(regions[valid_sequences[i].triplets[j].a[0]][valid_sequences[i].triplets[j].a[1]].rect.tl());
                    bbox_points.push_back(regions[valid_sequences[i].triplets[j].a[0]][valid_sequences[i].triplets[j].a[1]].rect.br());
                    bbox_points.push_back(regions[valid_sequences[i].triplets[j].b[0]][valid_sequences[i].triplets[j].b[1]].rect.tl());
                    bbox_points.push_back(regions[valid_sequences[i].triplets[j].b[0]][valid_sequences[i].triplets[j].b[1]].rect.br());
                    bbox_points.push_back(regions[valid_sequences[i].triplets[j].c[0]][valid_sequences[i].triplets[j].c[1]].rect.tl());
                    bbox_points.push_back(regions[valid_sequences[i].triplets[j].c[0]][valid_sequences[i].triplets[j].c[1]].rect.br());
                }

                Rect rect = boundingRect(bbox_points);
                rect.x = max(rect.x-10,0);
                rect.y = max(rect.y-10,0);
                rect.width = min(rect.width+20,src[c].cols-rect.x);
                rect.height = min(rect.height+20,src[c].rows-rect.y);

                vector<ERStat> aux_regions;
                Mat tmp;
                src[c](rect).copyTo(tmp);
                er_filter->run(tmp, aux_regions);

                for(size_t r=0; r<aux_regions.size(); r++)
                {
                    if ((aux_regions[r].rect.y == 0)||(aux_regions[r].rect.br().y >= tmp.rows))
                      continue;

                    aux_regions[r].rect   = aux_regions[r].rect + Point(rect.x,rect.y);
                    aux_regions[r].pixel  = ((aux_regions[r].pixel/tmp.cols)+rect.y)*src[c].cols + (aux_regions[r].pixel%tmp.cols) + rect.x;
                    bool overlaps = false;
                    for (size_t j=0; j<valid_sequences[i].triplets.size(); j++)
                    {
                        Rect minarearect_a  = regions[valid_sequences[i].triplets[j].a[0]][valid_sequences[i].triplets[j].a[1]].rect | aux_regions[r].rect;
                        Rect minarearect_b  = regions[valid_sequences[i].triplets[j].b[0]][valid_sequences[i].triplets[j].b[1]].rect | aux_regions[r].rect;
                        Rect minarearect_c  = regions[valid_sequences[i].triplets[j].c[0]][valid_sequences[i].triplets[j].c[1]].rect | aux_regions[r].rect;

                        // Overlapping regions are not valid pair in any case
                        if ( (minarearect_a == aux_regions[r].rect) ||
                             (minarearect_b == aux_regions[r].rect) ||
                             (minarearect_c == aux_regions[r].rect) ||
                             (minarearect_a == regions[valid_sequences[i].triplets[j].a[0]][valid_sequences[i].triplets[j].a[1]].rect) ||
                             (minarearect_b == regions[valid_sequences[i].triplets[j].b[0]][valid_sequences[i].triplets[j].b[1]].rect) ||
                             (minarearect_c == regions[valid_sequences[i].triplets[j].c[0]][valid_sequences[i].triplets[j].c[1]].rect) )

                        {
                            overlaps = true;
                            break;
                        }
                    }
                    if (!overlaps)
                    {
                        //now check if it has at least one valid pair
                        vector<Vec3i> left_couples, right_couples;
                        regions[c].push_back(aux_regions[r]);
                        for (size_t j=0; j<valid_sequences[i].triplets.size(); j++)
                        {
                            if (isValidPair(grey, lab, mask, src, regions, valid_sequences[i].triplets[j].a, Vec2i((int)c,(int)(regions[c].size())-1)))
                            {
                                if (regions[valid_sequences[i].triplets[j].a[0]][valid_sequences[i].triplets[j].a[1]].rect.x > aux_regions[r].rect.x)
                                    right_couples.push_back(Vec3i(regions[valid_sequences[i].triplets[j].a[0]][valid_sequences[i].triplets[j].a[1]].rect.x - aux_regions[r].rect.x, valid_sequences[i].triplets[j].a[0],valid_sequences[i].triplets[j].a[1]));
                                else
                                    left_couples.push_back(Vec3i(aux_regions[r].rect.x - regions[valid_sequences[i].triplets[j].a[0]][valid_sequences[i].triplets[j].a[1]].rect.x, valid_sequences[i].triplets[j].a[0],valid_sequences[i].triplets[j].a[1]));
                            }
                            if (isValidPair(grey, lab, mask, src, regions, valid_sequences[i].triplets[j].b, Vec2i((int)c,(int)(regions[c].size())-1)))
                            {
                                if (regions[valid_sequences[i].triplets[j].b[0]][valid_sequences[i].triplets[j].b[1]].rect.x > aux_regions[r].rect.x)
                                    right_couples.push_back(Vec3i(regions[valid_sequences[i].triplets[j].b[0]][valid_sequences[i].triplets[j].b[1]].rect.x - aux_regions[r].rect.x, valid_sequences[i].triplets[j].b[0],valid_sequences[i].triplets[j].b[1]));
                                else
                                    left_couples.push_back(Vec3i(aux_regions[r].rect.x - regions[valid_sequences[i].triplets[j].b[0]][valid_sequences[i].triplets[j].b[1]].rect.x, valid_sequences[i].triplets[j].b[0],valid_sequences[i].triplets[j].b[1]));
                            }
                            if (isValidPair(grey, lab, mask, src, regions, valid_sequences[i].triplets[j].c, Vec2i((int)c,(int)(regions[c].size())-1)))
                            {
                                if (regions[valid_sequences[i].triplets[j].c[0]][valid_sequences[i].triplets[j].c[1]].rect.x > aux_regions[r].rect.x)
                                    right_couples.push_back(Vec3i(regions[valid_sequences[i].triplets[j].c[0]][valid_sequences[i].triplets[j].c[1]].rect.x - aux_regions[r].rect.x, valid_sequences[i].triplets[j].c[0],valid_sequences[i].triplets[j].c[1]));
                                else
                                    left_couples.push_back(Vec3i(aux_regions[r].rect.x - regions[valid_sequences[i].triplets[j].c[0]][valid_sequences[i].triplets[j].c[1]].rect.x, valid_sequences[i].triplets[j].c[0],valid_sequences[i].triplets[j].c[1]));
                            }
                        }

                        //make it part of a triplet and check if line estimates is consistent with the sequence
                        vector<region_triplet> new_valid_triplets;
                        if(!left_couples.empty() && !right_couples.empty())
                        {
                            sort(left_couples.begin(), left_couples.end(), sort_couples);
                            sort(right_couples.begin(), right_couples.end(), sort_couples);
                            region_pair pair1(Vec2i(left_couples[0][1],left_couples[0][2]),Vec2i((int)c,(int)(regions[c].size())-1));
                            region_pair pair2(Vec2i((int)c,(int)(regions[c].size())-1), Vec2i(right_couples[0][1],right_couples[0][2]));
                            region_triplet triplet(Vec2i(0,0),Vec2i(0,0),Vec2i(0,0));
                            if (isValidTriplet(regions, pair1, pair2, triplet))
                            {
                                new_valid_triplets.push_back(triplet);
                            }
                        }
                        else if (right_couples.size() >= 2)
                        {
                            sort(right_couples.begin(), right_couples.end(), sort_couples);
                            region_pair pair1(Vec2i((int)c,(int)(regions[c].size())-1), Vec2i(right_couples[0][1],right_couples[0][2]));
                            region_pair pair2(Vec2i(right_couples[0][1],right_couples[0][2]), Vec2i(right_couples[1][1],right_couples[1][2]));
                            region_triplet triplet(Vec2i(0,0),Vec2i(0,0),Vec2i(0,0));
                            if (isValidTriplet(regions, pair1, pair2, triplet))
                            {
                                new_valid_triplets.push_back(triplet);
                            }
                        }
                        else if (left_couples.size() >=2)
                        {
                            sort(left_couples.begin(), left_couples.end(), sort_couples);
                            region_pair pair1(Vec2i(left_couples[1][1],left_couples[1][2]), Vec2i(left_couples[0][1],left_couples[0][2]));
                            region_pair pair2(Vec2i(left_couples[0][1],left_couples[0][2]),Vec2i((int)c,(int)(regions[c].size())-1));
                            region_triplet triplet(Vec2i(0,0),Vec2i(0,0),Vec2i(0,0));
                            if (isValidTriplet(regions, pair1, pair2, triplet))
                            {
                                new_valid_triplets.push_back(triplet);
                            }
                        }
                        else
                        {
                            // no possible triplet found
                            continue;
                        }

                        //check if line estimates is consistent with the sequence
                        for (size_t t=0; t<new_valid_triplets.size(); t++)
                        {
                            region_sequence sequence(new_valid_triplets[t]);
                            if (isValidSequence(valid_sequences[i],sequence))
                            {
                                valid_sequences[i].triplets.push_back(new_valid_triplets[t]);
                            }

                        }
                    }
                }
            }

        }


        // Prepare the sequences for output
        for (size_t i=0; i<valid_sequences.size(); i++)
        {
            vector<Point> bbox_points;
            vector<Vec2i> group_regions;

            for (size_t j=0; j<valid_sequences[i].triplets.size(); j++)
            {
                size_t prev_size = group_regions.size();
                if(find(group_regions.begin(), group_regions.end(), valid_sequences[i].triplets[j].a) == group_regions.end())
                  group_regions.push_back(valid_sequences[i].triplets[j].a);
                if(find(group_regions.begin(), group_regions.end(), valid_sequences[i].triplets[j].b) == group_regions.end())
                  group_regions.push_back(valid_sequences[i].triplets[j].b);
                if(find(group_regions.begin(), group_regions.end(), valid_sequences[i].triplets[j].c) == group_regions.end())
                  group_regions.push_back(valid_sequences[i].triplets[j].c);

                for (size_t k=prev_size; k<group_regions.size(); k++)
                {
                    bbox_points.push_back(regions[group_regions[k][0]][group_regions[k][1]].rect.tl());
                    bbox_points.push_back(regions[group_regions[k][0]][group_regions[k][1]].rect.br());
                }
            }

            out_groups.push_back(group_regions);
            out_boxes.push_back(boundingRect(bbox_points));

        }
    }

}

void erGrouping(InputArray image, InputArrayOfArrays channels, vector<vector<ERStat> > &regions,  vector<vector<Vec2i> > &groups,  vector<Rect> &groups_rects, int method, const string& filename, float minProbability)
{
    CV_Assert( image.getMat().type() == CV_8UC3 );
    CV_Assert( !channels.empty() );
    CV_Assert( !((method == ERGROUPING_ORIENTATION_ANY) && (filename.empty())) );

    switch (method)
    {
        case ERGROUPING_ORIENTATION_HORIZ:
            erGroupingNM(image, channels, regions, groups, groups_rects, true);
            break;
        case ERGROUPING_ORIENTATION_ANY:
            erGroupingGK(image, channels, regions, groups, groups_rects, filename, minProbability);
            break;
    }

}

void erGrouping(InputArray image, InputArray channel, vector<vector<Point> > contours, CV_OUT std::vector<Rect> &groups_rects, int method, const String& filename, float minProbability)
{
    CV_Assert( image.getMat().type() == CV_8UC3 );
    CV_Assert( channel.getMat().type() == CV_8UC1 );
    CV_Assert( !((method == ERGROUPING_ORIENTATION_ANY) && (filename.empty())) );

    vector<Mat> channels;
    channels.push_back(channel.getMat());
    vector<vector<ERStat> > regions;
    MSERsToERStats(channel, contours, regions);
    regions.pop_back();
    std::vector<std::vector<Vec2i> > groups;

    erGrouping(image, channels, regions,  groups,  groups_rects, method, filename, minProbability);
}

/*!
 * MSERsToERStats function converts MSER contours (vector<Point>) to ERStat regions.
 * It takes as input the contours provided by the OpenCV MSER feature detector and returns as output two vectors
 * of ERStats. MSER output contains both MSER+ and MSER- regions in a single vector<Point>, the function separates
 * them in two different vectors (this is the ERStats where extracted from two different channels).
 * */
void MSERsToERStats(InputArray image, vector<vector<Point> > &contours, vector<vector<ERStat> > &mser_regions)
{

  CV_Assert(!contours.empty());
  Mat grey = image.getMat();
  // assert correct image type
  CV_Assert( grey.type() == CV_8UC1 );
  if (!mser_regions.empty())
    mser_regions.clear();

  //MSER output contains both MSER+ and MSER- regions in a single vector but we want them separated
  mser_regions.resize(2);

  //Append "fake" root region to simulate a tree structure (needed for grouping)
  ERStat fake_root;
  mser_regions[0].push_back(fake_root);
  mser_regions[1].push_back(fake_root);

  Mat mask = Mat::zeros(grey.rows, grey.cols, CV_8UC1);
  Mat mtmp = Mat::zeros(grey.rows, grey.cols, CV_8UC1);
  for (int i=0; i<(int)contours.size(); i++)
  {

    ERStat cser;
    cser.area = (int)contours[i].size();
    cser.rect = boundingRect(contours[i]);

    float avg_intensity = 0;
    const vector<Point>& r = contours[i];
    for ( int j = 0; j < (int)r.size(); j++ )
    {
      Point pt = r[j];
      mask.at<unsigned char>(pt) = 255;
      avg_intensity += (float)grey.at<unsigned char>(pt)/(int)r.size();
    }

    double min, max;
    Point min_loc, max_loc;
    minMaxLoc(grey(cser.rect), &min, &max, &min_loc, &max_loc, mask(cser.rect));

    Mat element = getStructuringElement( MORPH_RECT, Size(5,5), Point(2,2) );
    dilate( mask(cser.rect), mtmp(cser.rect), element );
    absdiff( mtmp(cser.rect), mask(cser.rect), mtmp(cser.rect) );

    Scalar mean,std;
    meanStdDev(grey(cser.rect), mean, std, mtmp(cser.rect) );

    if (avg_intensity < mean[0])
    {
      cser.level  = (int)max;
      cser.pixel  = (max_loc.y+cser.rect.y)*grey.cols+max_loc.x+cser.rect.x;
      cser.parent = &(mser_regions[0][0]);
      mser_regions[0].push_back(cser);
    }
    else
    {
      cser.level  = 255-(int)min;
      cser.pixel  = (min_loc.y+cser.rect.y)*grey.cols+min_loc.x+cser.rect.x;
      cser.parent = &(mser_regions[1][0]);
      mser_regions[1].push_back(cser);
    }

    mask(cser.rect) = 0;
    mtmp(cser.rect) = 0;
  }
}

// Utility function for scripting
void detectRegions(InputArray image, const Ptr<ERFilter>& er_filter1, const Ptr<ERFilter>& er_filter2, CV_OUT vector< vector<Point> >& regions)
{
    // assert correct image type
    CV_Assert( image.type() == CV_8UC1 );
    // at least one ERFilter must be passed
    CV_Assert( !er_filter1.empty() );

    vector<ERStat> ers;

    er_filter1->run(image, ers);

    if (!er_filter2.empty())
    {
      er_filter2->run(image, ers);
    }

    //Convert each ER to vector<Point> and push it to output regions
    const Mat src = image.getMat();
    for (size_t i=1; i < ers.size(); i++) //start from 1 to deprecate root region
    {
      ERStat* stat = &ers[i];

      //Fill the region and calculate 2nd stage features
      Mat region_mask(Size(stat->rect.width + 2, stat->rect.height + 2), CV_8UC1, Scalar(0));
      Mat region = region_mask(Rect(1, 1, stat->rect.width, stat->rect.height));

      int newMaskVal = 255;
      int flags = 4 + (newMaskVal << 8) + FLOODFILL_FIXED_RANGE + FLOODFILL_MASK_ONLY;

      const Point seed_pt(stat->pixel%src.cols, stat->pixel/src.cols);
      uchar seed_v = src.at<uchar>(seed_pt);
      CV_Assert((int)seed_v <= stat->level);

      floodFill( src(stat->rect),
                 region_mask,
                 seed_pt - stat->rect.tl(),
                 Scalar(255), NULL, Scalar(/*stat->level*/255), Scalar(0), flags );

      vector<vector<Point> > contours;
      vector<Vec4i> hierarchy;
      findContours( region, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE, stat->rect.tl() );

      regions.push_back(contours[0]);
    }
}


void detectRegions(InputArray image, const Ptr<ERFilter>& er_filter1, const Ptr<ERFilter>& er_filter2,
                                           CV_OUT std::vector<Rect> &groups_rects,
                                           int method,
                                           const String& filename,
                                           float minProbability)
{
    // assert correct image type
    CV_Assert( image.type() == CV_8UC3 );

    CV_Assert( !er_filter1.empty() );
    CV_Assert( !er_filter2.empty() );

    // Extract channels to be processed individually
    vector<Mat> channels;

    Mat grey;
    cvtColor(image,grey,COLOR_RGB2GRAY);

    // here we are only using grey channel
    channels.push_back(grey);
    channels.push_back(255-grey);

    vector<vector<ERStat> > regions(channels.size());

    // Apply the default cascade classifier to each independent channel (could be done in parallel)
    for (int c=0; c<(int)channels.size(); c++)
    {
        er_filter1->run(channels[c], regions[c]);
        er_filter2->run(channels[c], regions[c]);
    }
   // Detect character groups
    vector< vector<Vec2i> > nm_region_groups;
    erGrouping(image, channels, regions, nm_region_groups, groups_rects, method, filename, minProbability);
}

}
}
