來玩個連連看,如果給你一張 Voronoi 圖,每個相鄰的細胞彼此之間,若以直線連接細胞核,會如何呢?試試看吧!
如果你耐心地做完連連看了,應該可以畫出以下的一組紅色三角形:
這組紅色三角形中的每個三角形,若找出其各自的外接圓,很神奇地,每個圓中不會包含任何的點,來隨便找兩個鄰接三角形的外接圓看看:
從上圖中也可以觀察到,鄰接三角形外接圓的兩個交點,正好就是細胞核的位置,兩個圓心的相連線,正好是 Voronoi 細胞的邊,這不難理解,兩個細胞核連接成兩圓的中垂線,圓心的相連線會平分中垂線,細胞核與圓心的相連線間自然是等距。
反過來看,如果你有一堆隨機散佈的點,若一開始有辦法將這些點連接成這樣的一組三角形,每個三角形的外接圓不包含其他點:
那麼,找出相鄰三角形的外接圓,將圓心連接起來,就可以構成 Voronoi 圖形:
對於一堆隨機散佈的點,得到這樣的一組三角形,是數學家 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;
}
circumcircle
的 triangle
必須是逆時針順序的頂點座標,以 p5.Vector
實例表示,傳回的物件中,center
是圓心座標,radius
半徑,那麼 rr
是什麼?因為求半徑需要開根號,因而在比較半徑與距離時會有誤差,若在意這個誤差,後續在比較點是否在圓內時,可以直接比較點與圓心的平方距離是否小於半徑的平方距離,避免開根號的誤差。
當然,就畫圓來說,還是會使用半徑,例如,底下會隨機產生三角形的三個頂點,畫出三角形與外接圓: