Delaunay 三角分割(一)


來玩個連連看,如果給你一張 Voronoi 圖,每個相鄰的細胞彼此之間,若以直線連接細胞核,會如何呢?試試看吧!

Delaunay 三角分割(一)

如果你耐心地做完連連看了,應該可以畫出以下的一組紅色三角形:

Delaunay 三角分割(一)

這組紅色三角形中的每個三角形,若找出其各自的外接圓,很神奇地,每個圓中不會包含任何的點,來隨便找兩個鄰接三角形的外接圓看看:

Delaunay 三角分割(一)

從上圖中也可以觀察到,鄰接三角形外接圓的兩個交點,正好就是細胞核的位置,兩個圓心的相連線,正好是 Voronoi 細胞的邊,這不難理解,兩個細胞核連接成兩圓的中垂線,圓心的相連線會平分中垂線,細胞核與圓心的相連線間自然是等距。

反過來看,如果你有一堆隨機散佈的點,若一開始有辦法將這些點連接成這樣的一組三角形,每個三角形的外接圓不包含其他點:

Delaunay 三角化(一)

那麼,找出相鄰三角形的外接圓,將圓心連接起來,就可以構成 Voronoi 圖形:

Delaunay 三角化(一)

對於一堆隨機散佈的點,得到這樣的一組三角形,是數學家 Delaunay 在 1934 提出的一種三角分割,為了紀念他在這個領域的貢獻,這樣的三角分割,就稱為 Delaunay 三角分割。

Delaunay 三角分割後的三角形,外接圓並不會包含其他的點,這也代表了,三角形中也不會有其他點,這就意謂著,可以針對三角形中的資訊分析而不會重複,因此 Delaunay 三角分割在臉部辨識、地理資料分析等領域,都有著重要的應用。

在演算法方面,想求得 Delaunay 三角分割,推薦的演算法之一是 Bowyer–Watson 演算,在後續的文件中,再來看看這個演算法的步驟。

這一篇文件就先做個準備工作,顯然地,你會需要從三角形的頂點取得外接圓,具體來說,只要計算出圓心與半徑就可以了,數學上如何求這個外接圓,就直接參考維基百科〈外接圓〉條目吧!底下是 p5.js 的實現:

function circumcircle(triangle) {
    const [p1, p2, p3] = triangle;
    const v1 = p5.Vector.sub(p2, p1);
    const v2 = p5.Vector.sub(p3, p2);
    const d1 = p5.Vector.add(p2, p1).mult(0.5).dot(v1);
    const d2 = p5.Vector.add(p3, p2).mult(0.5).dot(v2);
    const det = -p5.Vector.cross(v1, v2).z;
    if(det !== 0) {
        const x = (d2* v1.y - d1 * v2.y) / det;
        const y = (d1* v2.x - d2 * v1.x) / det;
        const center = createVector(x, y);
        const v = p5.Vector.sub(p1, center);
        const radius = v.mag();
        // 平方距離,避免開根號的誤差
        const rr = v.x * v.x + v.y * v.y; 
        return {center, radius, rr};
    }
    return null;    
}

circumcircletriangle 必須是逆時針順序的頂點座標,以 p5.Vector 實例表示,傳回的物件中,center 是圓心座標,radius 半徑,那麼 rr 是什麼?因為求半徑需要開根號,因而在比較半徑與距離時會有誤差,若在意這個誤差,後續在比較點是否在圓內時,可以直接比較點與圓心的平方距離是否小於半徑的平方距離,避免開根號的誤差。

當然,就畫圓來說,還是會使用半徑,例如,底下會隨機產生三角形的三個頂點,畫出三角形與外接圓: