手繪多邊形切割/裁剪(附C#代碼實現)

來源:互聯網
上載者:User

標籤:

本實現主要參考了發表於2003年《軟體學報》的《一個有效多邊形裁剪演算法》(劉勇奎,高雲,黃有群)這篇論文,所使用的理論與演算法大都基於本文,對論文中部分闡述進行了詳細解釋,並提取了論文中一些重要的理論加以匯總。另外對於論文描述無法處理的一些情況也進行了試探性的分析。

 

多邊形裁剪用於裁剪掉被裁剪多邊形(又稱為實體多邊形,後文用S表示)位於視窗(又稱為裁剪多邊形,後文用C表示)之外的部分。裁剪的結果多邊形是由實體多邊形位於裁剪多邊形內的邊界和裁剪多邊形位於實體多邊形內的邊界組成的。見

圖1

後文將以這個圖來描述裁剪過程,這個圖形比原論文給出的圖形更具代表性,最主要就在於C1這一點,論文中的圖形,切割多邊形沒有在實體多邊形中的點。所以,第一次看的人往往容易誤以為切割多邊形與交點組成的列表沒有作用。

中所示,切割多邊形與實體多邊形分別為C1C2C3C4和S1S2S3S4S5(由於兩個多邊形需要同向,需要逆置切割多邊形為C1C4C3C2,這個逆置也是有條件的,需要第一個交點保持不變,具體可以見代碼實現),我們要得到的結果是C1-I6->I3-I4->I5(-C1)和S4->I1-I2(->S4)這兩個結果多邊形(其中用"-"表示切割多邊形在實體多邊形中的邊,用"->"表示實體多邊形在切割多邊形中的邊)

 

演算法的實現分為三個階段:

階段1:計算第一個交點,並由第一個交點兩多邊形相互的進出性判斷兩個多邊形是否同向,如果不同向,將切割多邊形反向來使兩個多邊形方向一致。

階段2:依次使用實體多邊形每條邊去切割裁剪多邊形並將交點以正確的順序分別插入到兩個多邊形的鏈表中。

階段3:遍曆交點列表,獲得最終的結果。

後文將按照這3步來介紹相關的實現要點和代碼中主要方法。最後在文章末尾還提到一些特殊情況的處理。

 

階段1

階段1要判斷2個多邊形是否同向(同向的重要性在後文有描述),其中很重要的一點就是求切割的交點,當然求交點在第二階段也是很重要的,這裡介紹了求交點第二階段就不再介紹了。

(註:判斷多邊形是順時針還是逆時針的方法有很多,本文的C#代碼將採用原論文中的方法)

原論文中描述的方法基於一個定理:

定理1:如果兩個相交多邊形的邊的取向相同(均為順時針或逆時針方向),則對一個多邊形是進點的交點對另一個多邊形必是出點。

如果兩個多邊形的方向相同,對其中一個多邊形求出一個進點或出點以後,另一個多邊形的進出性也就確定了。這樣,求出交點時只需標記其中一個多邊形對另一個多邊形的進出性即可。從另一個多邊形角度看的進出性相反。

判斷兩個多邊形是否同向,需要判斷交點的進出性。同一個交點對於兩個多邊形的進出性不同,則兩個多邊形是同向;否則,兩個多邊形方向相反。

 

交點進出性的判定(排除切線與多邊形在頂點處相交或與多邊形一條邊重合的情況。)

當一條直線切割多邊形時,與多邊形相交的第一個點是必是進點,第二個點必是出點,如此進點與出點迴圈出現。

當一個多邊形的邊切割另一個多邊形時,多邊形上所有交點的進出性也是交替出現。

 

通過上面的分析可知,整個實現過程中一個十分關鍵的操作就是求線段(階段1中是求直線與多邊形交點)與多邊形的交點(即用線切割多邊形)。並在這個過程中判斷交點對於被切割多邊形的進出性。

 

求交點

這裡介紹一下原論文中給出的名為錯切變化法的求交點方法。這種方法基於兩直線進行錯切變換後交點的x值不變來實現,通過把切線變為斜率為0的直線來使求交點的過程簡化。假設切割線段為(x1,y1)、(x2,y2),被切割多邊形由v1, v2 … vn構成。求交點過程可以描述如下:

1. 求斜率d,d是切線的斜率,將(x1,y1)、(x2,y2)按照這個斜率進行變化後可以得到一條水平直線,我們設這條水平直線為y=yc,則有如下方程組:

 

 

帶入可得,有

 

說明,原論文說這裡需要x1<x2(看代碼實現可知x1=x2 x1>x2都是特殊處理的)。x1≠x2和y1≠y2也是需要滿足的,當x1=x2時表示切線是垂直線,應在垂直方向進行切割,先求交點的座標y。如果y1=y2則不用錯切過程,直接用切線切割即可。保證x1<x2是為了保證交點插入實體多邊形的順序正確。

是一個例子,黑色的線是錯切前的線,綠色的線是錯切後的線,實線是切線,虛線是多邊形上一條被切割的線:

圖2

 

2. 將多邊形上每個點vi(xi, yi)的y座標按公式進行錯切變化得到yi‘:

 

3. 對錯切後多邊形的每條邊((xi,yi‘),(xi+1,yi+1‘))與切線求交點的x座標Ixj:

 

如,對於前面圖中的例子,帶入xi, xi+1可得Ix=6.8。

接著,通過反錯切就可以得到Iy,公式:

 

所以

 

經計算,Iy=4.6。最終座標系映像:

 圖3

在多邊形切割處理中有一個需要特別注意的問題,就是一個多邊形的頂點落在另一個多邊形的邊上的情況。這種情況會影響交點的進出性判斷,從而可能會導致錯誤的結果。這種點與邊重合的情況可以在上述使用線切割多邊形的過程中處理。我們分幾種情況討論:

1. 切線的點落在多邊形的一條邊上

可以很好的說明這種情況:

圖4

當通過計算,我們發現交點座標的x值Ix與切線的座標x的最大或最小值相等時就發生了這種情況。

如下面的兩組多邊形都是這種情況:

圖5

如果不考慮第二種情況,我們只需將這些座標的x值Ix與切線的座標x的最大或最小值相等的點不算在交點(實交點和虛交點都不統計這種情況)內即可忽略這種情況,即我們假設那個點不相交。但這種處理對第二種情況不適用,我們需要知道這個特殊的交點的進出性來決定遇到這個交點後是沿著實體多邊形還是切割多邊形繼續前進。

對於這種情況,只需將座標的x值Ix與切線的座標x的最大或最小值相等的交點作為實交點即可。

2. 多邊形邊的一點落在切線上

如所示:

圖6

當經過錯切計算後,錯切後的多邊形的點的座標的y值等於yc的話,表示出現了這種情況。由於被切的多邊形的邊不用計算交點的進出性(當然判斷兩個多邊形方向時是個例外,也就是說判斷兩個多邊形方向時需要選擇一個一般化的交點),只需忽略這種多邊形的邊,不拿它們與切線計算交點即可(簡單說,這種交點可以被忽略)。

如使用上述方法可以正確處理:

圖7

 

3. 邊重合的情況

以上兩種情況的處理,由於我們沒有改變多邊形結點的座標,所以不會對最終的結果產生錯誤的影響,是可以接受的解決方案。對於兩個多邊形有邊重合的情況(兩多邊形結點重合除外,這個問題後面說),如這樣兩個多邊形(其中實體多邊形為實線,虛線的為切割多邊形,後文例子同),通過上面的方法可以正確處理,且不需要特意去判斷重合,我們可以將其作為情況2的一個特例:

圖8

 

4. 當前文提到的情況1和2同時發生時,就是多邊形節點重合的情況,體現在座標系中為如下這樣:

圖9

看2個這種情況的典型例子:

圖10

對於這種情況的處理方法,當經過錯切計算後,錯切後的多邊形的點的座標的y值等於yc的話,且交點的座標的x值正好等於切線端點的x座標,即發生當前所述情況,則我們挑多邊形邊上的一個點認為其有交點,而另一個點認為沒有交點。還是用的例子來解

如果現使用I1I2依次切割多邊形的邊C1C2, C2C3, …我們假設如果多邊形邊上的第二點(對於C1C2,C2C3第二點分別為C2,C3,另外,使用第一點效果也是一樣的)出現情況4時,我們認為其存在實交點。即使用I1I2切 C1C2,我們認為有一個交點,使用I1I2 切C2C3時根據上述規則它們沒有交點。這樣這種情況就得到正確的處理。

 

結果上面的介紹,對於各種交點計算都已可以處理,然後按偶奇的順序來標記進出性也很容易了。下面來看第二階段。

 

階段2

上面介紹的方法已經可以得到交點與交點的進出性。並根據進出性判斷兩個多邊形的方向,對於不同的向的,需要將其中一個多邊形反向。下面是整個演算法的另一個重點,用實體多邊形的邊切割裁剪多邊形並將交點插入兩個多邊形的的鏈表的過程。

1. 先把切割多邊形與實體多邊形連成如下兩個鏈表(前文已處理為方向一致)。

圖11

2. 用實體多邊形S的每條邊依次切割切割多邊形C,並把交點依次插入實體多邊形S與切割多邊形C的鏈表中,這個過程中同時標記該交點實體多邊形對於切割多邊形的進出性。

開始執行時,先使用S1S5切割切割多邊形C,會產生I6, I3兩個交點,可以使用S1、I6、I3和S5這幾個點x座標的位置關係來作為順序進行插入。這樣依次用S中每條邊切割C並插入鏈表,最終得到如下模型:

圖12

下面的圖可以更好的看出每個鏈表各自的情況(注意,交點相對不同的多邊形進出性是相反的,這也是多邊形同向的主要特徵):

圖13

這樣完成切割以及交點插入後就可以進入階段3了。

 

階段3

在有了上面兩個鏈表後,遍曆實體多邊形和裁剪多邊形鏈表來來獲得輸出多邊形鏈表。遍曆過程由實體多邊形對於裁剪多邊形為進點的這樣一個交點開始(需要used為0)。如第一個這樣的交點為I6。

從該進點到實體多邊形鏈表中的下一個交點(一定為出點,對於圖中這個點為I3)之間的所有實體多邊形上的點全部作為結果多邊形(下面的圖中以紅色箭頭表示這樣一個產生結果的過程,即沿著I6的NextS方向,對應圖中實心三角箭頭所指方向前進)。另外每遍曆一個結點或交點就將其used置為1,下同。

由於I3是實體多邊形對於裁剪多邊行的出點,所以下面的過程不能繼續沿著實體多邊形的邊繼續,需要轉向裁剪多邊形的方向(即沿著I3的Next2方向,對應圖中三角箭頭方向)繼續找點,直到遇到下一個交點(這個交點必然是實體多邊形對於裁剪多邊形的進點)。(此處論文中提到由於I3的NextC方向是裁剪多邊形相對於實體多邊形的進點,所以沿著NextC方向繼續,正是因為兩個多邊形同向,由於定理1,必然可以保證I3的NextC方向是裁剪多邊形相對於實體多邊形的進點,所以這就是兩個多邊形同向的重要性。)把這些點順序加入輸出多邊形。

重複這個過程(即遇到進點就沿NextS走,遇到出點就沿NextC走,另外注意,主要這個過程中遇到Vertex(不管是實體多邊形的,還是裁剪多邊形的),一定是沿著其Next走)直到遇到當前結果多邊形起始的那個交點(對於這個例子即I6)。這樣就輸出了一個結果多邊形。

在上面處理結束後,如果實體多邊形的鏈表中還有used為0的交點,說明還有其它的結果多邊形。找到第一個used為0的進點,然後繼續上文所述的過程,得到剩餘的結果多邊形。直到所有used為1結束。

展示了連接點的過程:

圖14

另一個版本:

圖15

這樣就可以得到結果了。

 

本文參考論文所實現的演算法支援一般多邊形,包括但不限於凹多邊形等,而且經過簡單修改還可以求多邊形的並或差,有一定的應用範圍。在輸出多邊形的過程中當遇到進點(實體多邊形相對於切割多邊形)時沿著切割多邊形的方向(即交點的NextC方向),而遇到出點(實體多邊形相對於切割多邊形)時沿著實體多邊形的方向(即交點的NextS方向),即可得到多邊形的"並"。

要得到多邊形的"差",只需使兩個多邊形開始階段2時方向相反即可(即求交集需要多邊形方向相同,求差集需要方向相反)。

對於有孔洞的多邊形的切割,論文中給出了一種思路,但是具體實現太複雜,這裡就沒有實現。有興趣的童鞋自己去研究下吧。基本的原理就是把有空多邊形的外側邊方向和切割多邊形保持一致,內側邊和外側邊方向相反,這樣在內側邊與切割多邊形切割時,交點的進出性可以保持正確。

 

由於本文使用的演算法需要由交點起遍曆多邊形鏈表,所以要求切割多邊形與實體多邊形必須有實際交點(區別於虛焦點,即一個多邊形邊的延長線與另一個多邊形的交點)。

下面補充討論下兩個沒有實際相交的多邊形。

首先看幾組例子:

圖16

在這種情況的處理下,仍可以處理凹多邊形,將不被如下方案支援。上面幾種情況,(a)切割後,結果多邊形即為切割多邊形。而(b)情況切割後結果即為實體多邊形。對於(c),結果為空白。

經過前文使用實體多邊形的邊切切割多邊行的過程後,如果交點個數為0,則說明出現了的情況。對於這三種情況的區分可以使用如下方法:

取切割多邊形中的一個點,如果位於實體多邊形內則屬於情況(a)

取實體多邊形中的一個點,如果位於切割多邊形內則屬於情況(b)

如果以上兩種情況都不成立則是情況(c)

這種判斷對如下這種嵌套,但同時存在邊重合的情況也適用(需要在判斷時,如果點在多邊形的邊上也算作在多邊形內部。同時這也依賴於如果兩個多邊形邊重合就認為它們沒有交點這個假設):

圖17

這裡涉及到一個問題是,如果一個多邊形嵌套另一個多邊形,如何來判斷嵌套。由於當一個多邊形嵌套在另一個多邊形中時,這個多邊形其中的任一頂點也就在另一個多邊形內部。所以可以把一個多邊形是否包含另一個多邊形的問題轉為一個多邊形是否包含一個點的問題。

對於點是否在多邊形中的問題,有一個經典方法可用於判斷,由這個點向任意方向做一條射線(代碼實現中是向右做水平射線,實現起來最簡單),如果射線與多邊形有奇數個交點,說明點在多邊形內部,反之(包括沒有交點)則說明點在多邊形外部。具體可見代碼實現部分。

 

 

代碼實現:

採用原論文中描述的方法判斷兩個多變形是否同向和擷取第一個交點應該同時完成,不過我實現寫不出在通過一次切割即構成鏈表又能判斷出進出性/多邊形方向的代碼,所以下面的實現中判斷方向的切割與構成鏈表的切割相分離(即階段1和階段2把第一個交點計算了2次,第一次只是用於方向的判斷)。 

 

演算法使用的資料結構

這個演算法中使用2個鏈表來分別表示切割多邊形與實體多邊形。

其中結點/交點的資料結構:

public abstract class VertexBase{    public double X { get; set; }    public double Y { get; set; }    public string Name { get; set; }    public void SetXY(double x, double y)    {        X = x;        Y = y;    }    public Point ToPoint()    {        return new Point(X,Y);    }}public class Vertex : VertexBase{    [DebuggerNonUserCode]    public Vertex(double x, double y)    {        X = x;        Y = y;    }    public VertexBase Next { get; set; }}public class Intersection : VertexBase{    [DebuggerNonUserCode]    public Intersection(double x, double y)    {        X = x;        Y = y;    }    public CrossInOut CrossDi { get; set; }    public bool Used { get; set; }    public VertexBase NextS { get; set; }    public VertexBase NextC { get; set; }}

Vertex中的Next用於表示下一個節點或交點的引用(可能是Vertex也可能是Intersection),Intersection中的NextS和NextC分別儲存交點在S鏈表中和C鏈表中的下一個節點或交點的引用。Intersection中的Used記錄輸出結果的過程中該交點是否被輸出,CrossDi表示該交點處S多邊形相對於C多邊形的進出性。

解決方案中ArbitraryPolygonCut.cs檔案包含了演算法核心的代碼(篇幅原因不列出代碼了,後面有Github連結,自行下載查看吧),其中

List<List<VertexBase>> Cut(List<VertexBase> listS, List<VertexBase> listC)

是演算法的主入口函數,裡面的注釋包含了幾個階段的標識,階段一最主要的切割並判斷方向的函數為:

Tuple<CrossInOut, int, bool> CutByLineForCrossDi(VertexBase v1, VertexBase v2, List<VertexBase> list, bool withIdx, int line2Idx = 0)

階段二切割並連結節點的函數為:

List<Intersection> CutByLine(Vertex s1, Vertex s2, LinkedList<VertexBase> linkC)

上面2個切割函數還有2個Vertical結尾的函數,用於處理切線是垂直的情況。

 

對於完全不相交的情況,使用

List<VertexBase> ProcessNoCross(List<VertexBase> listS, List<VertexBase> listC)

函數來處理,其中調用

bool IsVertexInPolygon(VertexBase v, List<VertexBase> list)

來進行點是否在多邊形內的判斷。

 

代碼附帶一個測試程式,裡面附帶了部分儲存好的測試圖形(.cut結尾的檔案),其包含了文檔中涵蓋的幾種情況。也可以通過程式建立自己的圖形來測試。

最後放上一個圖:

 

代碼下載:

Github

 

代碼可以用於任意項目中,但本實現的測試不完善,用於生產情境請再次測試。如果用於出版的文檔請標明來源。

轉載本文請保留連結。

手繪多邊形切割/裁剪(附C#代碼實現)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.