1. 程式人生 > >OpenCV原始碼解析之findContours

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