http://www.cnblogs.com/xiaotie/archive/2010/11/08/1871424.html
在圖像分析中,經常需要判斷圖像分割所得到的區域之間的關系。通常情況,我們通過八鄰接外輪廓(准確說法是擴展邊緣,但這樣又得費半天口舌解釋什麼是擴展邊緣)來描述一個區域並對區域進行標注,如:
很容易判斷兩個區域是否相鄰(掃描區域的內外邊緣像素,如果相鄰的像素具有不同的標注值,則為鄰居),卻較難判斷一個區域是否在另一個區域的內部。
如上圖中,通過相鄰像素的標注值的不同,可以得出A和B互為鄰居,A和C互為鄰居,卻很難知道B和C中,哪個是在A的內部。下面設計算法進行判斷。
====
如果B在A的內部,則B的外輪廓上的每個點在A的外輪廓的內部或邊緣上;如此一來,問題就簡化為判斷點是否在多邊形的邊緣或內部。
判斷點是否在多邊形內部有一個很經典的算法:從該點向任意一方畫射線,數該射線與多邊形的邊的交點數量,如果為奇數則在多邊形內部,如果為偶數則在多邊形的外部。
這個算法有兩個特例:
(1)射線和多邊形的邊重合(下圖a,b)
(2)射線經過多邊形的頂點(下圖c,d)
顯然,(a)應該算0個交點 ,(b)應該算1個交點,(c)應該算0個交點,(d)應該算1個交點。
總體上來說,這個算法要考慮到幾種特殊情況,還是比較繁瑣的。下面,針對本文的應用來簡化該算法。
數字圖像是離散的,通過邊界跟蹤可以得到全部的輪廓點。
上圖是一個輪廓及待判斷點。從該點向X軸畫一個射線,與9個輪廓點相交。如果將輪廓的任意兩個相鄰點的連線作為多邊形的一邊的話,很不幸,全部交點都是特殊情況。這裡假定輪廓點的排列是有序的,也就是說,是有方向的,只考察輪廓點和它一前一後兩個輪廓點之間的關系,則有下面幾類情況:
這裡對經典的點在多邊形內部判斷算法進行變形:
(1)如果經過A類中的中間點,則算為 0.5 或 –0.5 個交點;
(2)如果經過B類的中間點,算作1個或-1個交點;
(3)如果經過C類的中間點,算作0個交點。
計算結果——如果交點數加起來是奇數,則點在輪廓的內部,否則在外部。為了避免浮點計算,將交點個數乘於2,即A類的算1個或-1個交點,B類的算2個或-2個交點,C類的算0個交點。交點總數是4的倍數則在輪廓外部,否則,則在內部。
為什麼是1個或-1個,2個或-2個呢?這裡有方向問題。假設箭頭是從下向上的,為-1,箭頭是從上往下的,算1,如果箭頭是水平的,算0. 這樣計算的話,則上圖中A類的2種情況分別為1個、1個交點,B類的2種情況分別為2個、-2個交點,C類的2種情況全為0. 如此以來,完全滿足前面點在多變形內部的經典算法對幾種特殊情況的處理。
為每一個輪廓點賦予一個分值Score,這個分值只與它(Current)和前後兩點(Prev,Next)有關,和其它任何點無關。因此,這個分值是靜態的,不變化的。我們可以把它計算出來緩存在散列表中。
private void ComputeExtendContourPointXScoreDic()
{
List<Point> points = this.ExtendContourPoints;
if (points.Count < 3) return;
int count = points.Count;
for (int i = 0; i < count; i++)
{
Point current = points[i];
Point prev = points[(i + count - 1) % count];
Point next = points[(i + 1) % count];
int score = current.Y < prev.Y ? 1 : current.Y > prev.Y ? -1 : 0;
score += next.Y < current.Y ? 1 : next.Y > current.Y ? -1 : 0;
_extendContourPointXScoreDic[current.GetHashCode32()] = score;
}
}
更進一步,score = prev.Y – next.Y:
private void ComputeExtendContourPointXScoreDic()
{
List<Point> points = this.ExtendContourPoints;
if (points.Count < 3) return;
int count = points.Count;
for (int i = 0; i < count; i++)
{
_extendContourPointXScoreDic[points[i].GetHashCode32()] = points[(i + count - 1) % count].Y - points[(i + 1) % count].Y;
}
}
_extendContourPointXScoreDic 是一個散列表,儲存了輪廓點的Score。因為一般的圖像不會特別大,我為Point添加了一個擴展方法GetHashCode32()來獲得散列值:
public static int GetHashCode32(this Point p)
{
return p.Y * Int16.MaxValue + p.X;
}
這個散列表還有一個用途——如果某點的散列值在散列表內,則該點在外輪廓上。
判斷點是否在輪廓的內部,只需要向左或向右掃描即可。比較向左和向右的掃描長度,選擇最短的掃描路線,將掃描所經過的輪廓點的散列值加起來,就是交點數量。該交點數量如果是4的倍數,則代表點在輪廓外,否則,則在輪廓內。
在掃描的過程中,向左移一點或向右移一點,對應的點的散列值減1或加1,因此,可以省掉移動的過程,用散列值的變化來表示移動。這樣又可以加速計算。如果已知A、B兩個區域的 Rectangle,可以先判斷,如果B的Rectangle不在A的Rectangle內部,則B一定不在A的內部。這樣也可以節省不少計算。
====
參考資料:Udi Manber. 算法引論——一種創造性方法. http://www.china-pub.com/26775 P.189。