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