OpenCV源码解析之findContours

说明:openCv的contours是分级的,其寻边理论依据(方式)参考suzuki的论文《Topological structural analysis of digitized binary images by border following》。

Contour 的寻边模式 Mode

openCV通过一个矩阵来管理等级,矩阵的元素表示方法是:[Next, Previous, First_Child, Parent]

RETR_LIST:列出所有的边,没有父子层级之分(全部边缘都为1级)。
在这种模式下,这8条边建立的等级关系为
[ 1, -1, -1, -1], [ 2, 0, -1, -1],  [ 3, 1, -1, -1],  [ 4, 2, -1, -1],  
[ 5, 3, -1, -1], [ 6, 4, -1, -1], [ 7, 5, -1, -1], [-1, 6, -1, -1]
例如边“0”,其同等级的Next是边“1”,前一个不存在(-1),没有First_Child, Parent,这两个参数也都设为-1,所以第0个元素是
[1,-1,-1,-1];边“1”,其同等级的Next是边“2”,前一个边是“0”,没有First_Child, Parent,两个-1,所以第1个元素是[2,0,-1,-1];依次类推。

RETR_EXTERNAL:列出最外面的边(如物体的外边框),不管被包围的内环或边(如物体的孔洞)。

RETR_CCOMP:只取2个层级的边,如下图,只把边(粉红色)分为两个层(绿色),标记为绿色的(1)顶层和(2)次层。

上面图中,这9条边建立的等级关系为
[ 3, -1, 1, -1],    [ 2, -1, -1, 0],    [-1, 1, -1, 0],    [ 5, 0, 4, -1],    [-1, -1, -1, 3],
[ 7, 3, 6, -1],    [-1, -1, -1, 5],    [ 8, 5, -1, -1],    [-1, 7, -1, -1]
例如第"0"边,其相邻的Next是边"3", Previous不存在(-1),First-child=边"1",Parent不存在(-1),所以其相应的元素为
[3, -1, 1, -1],其余元素依此规则类推。

RETR_TREE:返回所有的边及层级关系,

这9条边建立的等级关系为
[ 7, -1, 1, -1],    [-1, -1, 2, 0],    [-1, -1, 3, 1],    [-1, -1, 4, 2],    [-1, -1, 5, 3],
[ 6, -1, -1, 4],    [-1, 5, -1, 4],    [ 8, 0, -1, -1],   [-1, 7, -1, -1]
 

注意cvDrawContours

findContours往往和drawContours配合使用,
cvDrawContours 函数第5个参数为 max_level,等级的含义,从前面可以知道,只有提取有等级的轮廓时候(提取模式设为 CV_RETR_CCOMP或CV_RETR_TREE)这个参数才有意义。

MAX_SIZE
static const int MAX_SIZE = 16;
MAX_SIZE=16是为了节省计算量。因为在最差的情况下,向左或向右旋转遍历(x,y)周边的8个像素时(如下图所示),8连通需要计算8次,如果采用最大值是8,则每次都要采用计算更大的越界检查来判断序号是否在0~7之间。

CV_INIT_3X3_DELTAS
/* initializes 8-element array for fast access to 3x3 neighborhood of a pixel */
#define CV_INIT_3X3_DELTAS( deltas, step, nch ) \
((deltas)[0] = (nch), (deltas)[1] = -(step) + (nch), \
(deltas)[2] = -(step), (deltas)[3] = -(step) - (nch), \
(deltas)[4] = -(nch), (deltas)[5] = (step) - (nch), \
(deltas)[6] = (step), (deltas)[7] = (step) + (nch))

CV_INIT_3X3_DELTAS是完成下面这个偏移量计算的,比如点a(x,y)b(x+1,y),此时对应deltas(0),即在内存中,假设已经知道了a点的位置,直接使用b=a+deltas(0)即可得到b点的序号。nch是偏移步进距离,这里是1,如果是2,就会对应更外面一层,如图中黄色层。

icvCodeDeltas
static const CvPoint icvCodeDeltas[8] =
{ CvPoint(1, 0), CvPoint(1, -1), CvPoint(0, -1), CvPoint(-1, -1), CvPoint(-1, 0), CvPoint(-1, 1), CvPoint(0, 1), CvPoint(1, 1) };

icvCodeDeltas 描述的是下面这个关第,对照上图序号,比如icvCodeDeltas(序号0).x = 1, icvCodeDeltas(序号0).y = 0, 表示在图片中序号为0的像素相对于中心像素(x,y)的相对位置偏移量为(1,0)。

参数
- nbd, number of border

源码

static void
icvFetchContourEx( schar*               ptr,
                   int                  step,
                   CvPoint              pt,
                   CvSeq*               contour,
                   int  _method,
                   int                  nbd,
                   CvRect*              _rect )
{
    int         deltas[MAX_SIZE];
    CvSeqWriter writer;
    schar        *i0 = ptr, *i1, *i3, *i4 = NULL;
    CvRect      rect;
    int         prev_s = -1, s, s_end;
    int         method = _method - 1;

    CV_DbgAssert( (unsigned) _method <= CV_CHAIN_APPROX_SIMPLE );
    CV_DbgAssert( 1 < nbd && nbd < 128 );

    /* initialize local state */
    CV_INIT_3X3_DELTAS( deltas, step, 1 );
    memcpy( deltas + 8, deltas, 8 * sizeof( deltas[0] ));

    /* initialize writer */
    cvStartAppendToSeq( contour, &writer );

    if( method < 0 )
        ((CvChain *)contour)->origin = pt;

    rect.x = rect.width = pt.x;
    rect.y = rect.height = pt.y;

    s_end = s = CV_IS_SEQ_HOLE( contour ) ? 0 : 4;  // hole从quad 0开始,外边界从quad 4开始

    do
    {
        s = (s - 1) & 7;
        i1 = i0 + deltas[s];
    }
    while( *i1 == 0 && s != s_end );

    if( s == s_end )            /* single pixel domain */
    {
        *i0 = (schar) (nbd | 0x80);
        if( method >= 0 )
        {
            CV_WRITE_SEQ_ELEM( pt, writer );
        }
    }
    else
    {
        i3 = i0;

        prev_s = s ^ 4;

        /* follow border */
        for( ;; )
        {
            CV_Assert(i3 != NULL);
            s_end = s;
            s = std::min(s, MAX_SIZE - 1);

            while( s < MAX_SIZE - 1 )
            {
                i4 = i3 + deltas[++s];
                CV_Assert(i4 != NULL);
                if( *i4 != 0 )
                    break;
            }
            s &= 7;

            /* check "right" bound */
            if( (unsigned) (s - 1) < (unsigned) s_end ) // 该条件表示,外轮廓最右边的标记改为NBD的负值。
            { // 这个条件是为了避免轮廓右边的部分被再次当做初始点。遇到负值的像素点是不判断它是否为一个新轮廓的起始点的,确保一个轮廓只扫描一次。
                *i3 = (schar) (nbd | 0x80);
            }
            else if( *i3 == 1 )
            {
                *i3 = (schar) nbd;
            }

            if( method < 0 )
            {
                schar _s = (schar) s;
                CV_WRITE_SEQ_ELEM( _s, writer );
            }
            else if( s != prev_s || method == 0 )
            {
                CV_WRITE_SEQ_ELEM( pt, writer );
            }

            if( s != prev_s )
            {
                /* update bounds */
                if( pt.x < rect.x )
                    rect.x = pt.x;
                else if( pt.x > rect.width )
                    rect.width = pt.x;

                if( pt.y < rect.y )
                    rect.y = pt.y;
                else if( pt.y > rect.height )
                    rect.height = pt.y;
            }

            prev_s = s;
            pt.x += icvCodeDeltas[s].x;
            pt.y += icvCodeDeltas[s].y;

            if( i4 == i0 && i3 == i1 )  break;

            i3 = i4;
            s = (s + 4) & 7;
        }                       /* end of border following loop */
    }

    rect.width -= rect.x - 1;
    rect.height -= rect.y - 1;

    cvEndWriteSeq( &writer );

    if( _method != CV_CHAIN_CODE )
        ((CvContour*)contour)->rect = rect;

    CV_DbgAssert( (writer.seq->total == 0 && writer.seq->first == 0) ||
            writer.seq->total > writer.seq->first->count ||
            (writer.seq->first->prev == writer.seq->first &&
             writer.seq->first->next == writer.seq->first) );

    if( _rect )  *_rect = rect;
}

未完待续……

参考
[1] https://docs.opencv.org/trunk/d9/d8b/tutorial_py_contours_hierarchy.html

猜你喜欢

转载自blog.csdn.net/tanmx219/article/details/84973542