การสร้างไดอะแกรมโวโรนอยด้วยอัลกอริทึมของ Fortune
(redpenguin101.github.io)- Fortune’s Algorithm สามารถสร้างไดอะแกรมโวโรนอยได้ในเวลา O(n log n) แต่การนำไปใช้งานจริงค่อนข้างซับซ้อน ดังนั้นหากไม่ได้เป็นกรณีที่ต้องสร้างไดอะแกรมขนาดใหญ่ซ้ำ ๆ บ่อยครั้ง การใช้วิธี O(n²) หรือใช้ไลบรารีมักจะใช้งานได้จริงมากกว่า
- ไดอะแกรมโวโรนอยแบ่งระนาบออกเป็นบริเวณที่ใกล้กับ site แต่ละจุดมากที่สุด โดยเส้นขอบเกิดจากจุดที่อยู่ห่างจากสอง site เท่ากัน
- อัลกอริทึมจะคงสถานะของ sweep line ที่เคลื่อนจากซ้ายไปขวา และ beachline ซึ่งเป็นแนวหน้าของส่วนโค้งพาราโบลา แล้วประมวลผลเฉพาะ site event และ circle event
- site event จะแทรกส่วนโค้งใหม่ แบ่งส่วนโค้งเดิม และสร้าง incomplete edge ส่วน circle event จะลบส่วนโค้งตรงกลาง พร้อมทำให้ Voronoi Vertex และ half edge สมบูรณ์
- ในการติดตั้งใช้งานจริง ต้องจัดการทั้ง event queue, beachline, แผนที่ของ incomplete edge และ DCEL ไปพร้อมกัน อีกทั้งการลบ circle event ที่ใช้ไม่ได้แล้วและการเก็บกวาด edge ที่เหลือยังเพิ่มความซับซ้อนอย่างมาก
ความยากของการติดตั้งใช้งานและขอบเขตการนำไปใช้
- Fortune’s Algorithm เป็นอัลกอริทึมสำหรับสร้างไดอะแกรมโวโรนอยในเวลา O(n log n)
- หากเป้าหมายคือการใช้งานจริง ควรประเมินขนาดงานที่ต้องการก่อนมากกว่าจะรีบลงมือเขียนเองทันที
- ถ้าไม่ได้ต้องสร้างไดอะแกรมขนาดใหญ่หลายชุดต่อวินาที การใช้วิธี O(n²) อาจเป็นตัวเลือกที่ง่ายกว่า
- ทางเลือกที่ใช้งานได้จริงยิ่งกว่าคือใช้ ไลบรารี ที่มีอยู่แล้ว
- แม้ผลลัพธ์การทำงานของอัลกอริทึมจะดูน่าสนใจในเชิงภาพ แต่กระบวนการติดตั้งใช้งานจริงนั้นค่อนข้างยากและชวนท้อพอสมควร
แนวคิดพื้นฐานของไดอะแกรมโวโรนอย
- ไดอะแกรมโวโรนอยเป็นวิธีแบ่งระนาบออกเป็นหลายบริเวณ และมักถูกใช้ในการสร้างแผนที่เชิงกระบวนการ
- จุดที่เลือกเป็นข้อมูลนำเข้าจะเรียกว่า site หรือ seed
- cell ที่สอดคล้องกับแต่ละ site คือเซตของจุดบนระนาบที่อยู่ใกล้ site นั้นมากที่สุด
- ขอบเขตของเซลล์ประกอบด้วยจุดที่อยู่ห่างจากสอง site เท่ากัน
- Voronoi Vertex ซึ่งเป็นจุดที่มุมของเซลล์มาบรรจบกัน คือจุดที่อยู่ห่างจากสาม site เท่ากัน
sweep line, beachline, event
- Fortune’s Algorithm ใช้เส้นแนวตั้งที่เคลื่อนจากซ้ายไปขวา เรียกว่า sweep line
- เมื่อ sweep line พบ site จะเกิดส่วนโค้งพาราโบลาที่มี site นั้นเป็นโฟกัส และเมื่อ sweep line เคลื่อนออกห่าง ส่วนโค้งจะใหญ่ขึ้น
- จุดที่ส่วนโค้งจากคนละ site สองเส้นมาพบกันเป็นจุดที่ห่างจากสอง site เท่ากัน จึงกลายเป็นขอบเขตของเซลล์
- เมื่อขอบเขตสองเส้นมาพบกัน จะเกิดจุดยอดของไดอะแกรม
- แนวหน้าของส่วนโค้งที่ยังทำงานอยู่เรียกว่า beachline
- การติดตั้งใช้งานจริงจะไม่เลื่อน sweep line ทีละพิกเซล แต่จะประมวลผลเฉพาะจุดเฉพาะที่คำนวณได้ซึ่งเรียกว่า event
- site event: นิยามจากพิกัดของ site ที่รู้ล่วงหน้า และเมื่อประมวลผลจะมีการเพิ่มส่วนโค้งใหม่ลงใน beachline
- circle event: นิยามจากส่วนโค้งสามเส้นบน beachline และเมื่อประมวลผลจะมีการลบส่วนโค้งหนึ่งเส้น พร้อมสร้าง Voronoi Vertex และ half edge
การหาขอบเขตด้วยพาราโบลา
- ในอัลกอริทึมนี้ พาราโบลาไม่ได้ถูกมองในรูปแบบทั่วไปอย่าง
y = ax^2 + bx + cแต่พิจารณาด้วย locus definition - พาราโบลาถูกนิยามด้วย focus point หนึ่งจุดและ directrix หนึ่งเส้น
- focus point คือ site
- directrix คือ sweep line
- จุดตัดของพาราโบลาสองเส้นที่ใช้ sweep line เดียวกันเป็น directrix จะอยู่ห่างจากสอง site เท่ากัน
- ดังนั้นเมื่อหาจุดตัดของพาราโบลาสองเส้นได้ ก็จะหาตำแหน่งของ equiedge ระหว่างสอง site ได้
- มีการใช้ทั้ง pseudocode สำหรับคำนวณพิกัด x ของพาราโบลา และตัวอย่างที่แสดงว่าจุดตัดของพาราโบลาสองเส้นเคลื่อนที่ไปตามขอบเขตอย่างไรเมื่อ sweep line เปลี่ยนตำแหน่ง
การแทน beachline และการจัดการ site event
- ส่วนโค้งแต่ละเส้นใน beachline สามารถแทนได้ด้วยพิกัดของ site ที่เกี่ยวข้องเพียงอย่างเดียว
- sweep line เป็นค่าที่ใช้ร่วมกันกับทุกส่วนโค้ง
- ในการติดตั้งใช้งานจริง ส่วนโค้งจึงถูกจัดการเป็น พิกัด 2D แทนการสร้างเป็นอ็อบเจ็กต์แยกต่างหาก
- beachline สามารถแทนเป็นลำดับของจุดแบบง่าย ๆ ได้
- ตัวอย่าง:
[arc1, arc2],[arc1, arc2, arc3] - ส่วนโค้งจาก site เดียวกันสามารถปรากฏหลายครั้งบน beachline ได้
- ตัวอย่าง:
[arc1, arc3, arc1, arc2]
- ตัวอย่าง:
- เมื่อเกิด site event จะต้องหาส่วนโค้งบน beachline ที่ถูกตัดโดยเส้นที่ลากไปทางซ้ายจาก site ใหม่ แล้วให้ส่วนโค้งใหม่ แยก ส่วนโค้งนั้น
- หาก site ใหม่
Lไปแบ่งjใน beachline เดิม[.., i, j, k, ..]โครงสร้างจะกลายเป็น[.., i, j, L, j, k, ..] - site จะถูกใส่ในคิวตามลำดับพิกัด x และทุกครั้งที่ประมวลผล จะมีการอัปเดต beachline และ candidate ของ event
circle event และ circumcircle
- หากใน beachline ที่เป็น
[.., i, j, k, ..]มีสถานการณ์ที่ขอบเขตสองเส้นมาบรรจบกัน ส่วนโค้งตรงกลางjจะหายไป - ในกรณีนี้จะมี circumcircle ที่ผ่านทั้งสาม site และจุดศูนย์กลางของวงกลมจะอยู่ห่างจากทั้งสาม site เท่ากัน
- จุดศูนย์กลางของ circumcircle จะกลายเป็น Voronoi Vertex
- circle event จะถูกวางใน event queue โดยอิงจาก circle point ซึ่งเป็นจุดปลายด้านขวาของวงกลม
- หากมี site ใหม่ถูกพบภายในวงกลมก่อนที่ sweep line จะถึง circle point, circle event เดิมจะใช้ไม่ได้
- เพราะ site ใหม่จะเข้าไปแบ่งส่วนโค้งตรงกลางก่อน ทำให้ชุดของสามส่วนโค้งเดิมไม่คงอยู่ต่อไป
- triple เดิม
i, j, kจะหายไป และต้องตรวจสอบ triple ใหม่อย่างi, j, L,L, j, k
incomplete edge และ half edge
- incomplete edge คือเส้นที่ปลายด้านหนึ่งถูกกำหนดแล้ว แต่อีกปลายหนึ่งยังนิยามด้วยจุดตัดของส่วนโค้งพาราโบลาสองเส้น
- เมื่อมีการแทรกส่วนโค้งใหม่จาก site event จะเกิด incomplete edge สองเส้น
- จุดตรึงคือพิกัดที่ส่วนโค้งใหม่มาพบกับ beachline เดิม
- หากส่วนโค้งใหม่
jไปแบ่งส่วนโค้งเดิมiจะเกิด edge ที่สอดคล้องกับจุดตัด[i, j],[j, i]
- เมื่อ incomplete edge สองเส้นชนกันใน circle event จุดชนจะกลายเป็น Voronoi Vertex
- incomplete edge เดิมจะสมบูรณ์เป็น half edge ที่จุดนี้ และจะมีการสร้าง incomplete edge ใหม่ขึ้นระหว่างส่วนโค้งสองเส้นที่กลายเป็นเพื่อนบ้านกันใหม่
เฉพาะวงกลมทวนเข็มนาฬิกาเท่านั้นที่เป็น circle event
- เมื่อ beachline เป็น
[i, j, k, j, i]ทั้งijkและkjiอาจสร้างวงกลมได้ แต่ไม่ได้หมายความว่าทั้งสองจะเป็น circle event ที่ใช้ได้ - กรณีที่ส่วนโค้งตรงกลางหายไปจะเกิดขึ้นเฉพาะด้านที่ขอบเขตบรรจบเข้าหากันจริงเท่านั้น
- ในโปรแกรม จะตัดสิน orientation ของสามจุดด้วย determinant
- หาก determinant เป็นลบ จะเป็นทิศทางทวนเข็มนาฬิกาและนับเป็น circle event
- หาก determinant เป็นบวก จะเป็นทิศทางตามเข็มนาฬิกาและไม่เป็น circle event
- หาก determinant เป็น 0 แสดงว่าสามจุดอยู่บนเส้นตรงเดียวกัน จึงไม่มีวงกลม
ลำดับการทำงานของอัลกอริทึมทั้งหมด
- เรียงลำดับ site นำเข้าตามพิกัด x แล้วใส่ลงคิวเป็น site event
- ดึง event ถัดไปจากคิวออกมาประมวลผลจนกว่าคิวจะว่าง
- การประมวลผล site event:
- ลบ circle event ที่ยังเหลืออยู่ล่วงหน้าซึ่ง site ใหม่เข้าไปอยู่ภายในวงกลม
- หา arc บน beachline ที่ site ใหม่จะเข้าไปแบ่ง
- แทรกส่วนโค้งใหม่เพื่อแบ่งส่วนโค้งเดิม
- เพิ่ม incomplete edge สองเส้น
- ตรวจสอบว่า triple ที่เกิดขึ้นใหม่สามารถสร้าง circle event ได้หรือไม่
- การประมวลผล circle event:
- เพิ่มจุดศูนย์กลางของ circumcircle เป็น Voronoi Vertex
- ลบส่วนโค้งตรงกลางออกจาก beachline
- ลบ circle event ในอนาคตที่กลายเป็นใช้ไม่ได้เพราะส่วนโค้งนี้ถูกลบออก
- ตรวจสอบ triple ของส่วนโค้งที่กลายเป็นเพื่อนบ้านกันใหม่ แล้วเพิ่ม circle event หากมี
- เมื่อคิวว่าง ให้ยืด incomplete edge ที่เหลือไปจนถึงขอบเขตของไดอะแกรม และสร้าง Voronoi Vertex ที่จุดตัดกับขอบเขต
โครงสร้างข้อมูลของเวอร์ชันติดตั้งใช้งานด้วย Odin
- ตัวอย่างการติดตั้งใช้งานเขียนด้วย Odin ซึ่งเป็นภาษาอีกทางเลือกหนึ่งของ C
- โค้ดทั้งหมดอยู่ในรีโพซิทอรี RedPenguin101/voronoi
- ชนิดข้อมูลพื้นฐาน:
V2: จุด 2D ในรูปแบบ[2]intPointPair: คู่ของV2สองค่าEvent: โครงสร้าง{site: bool, a, b, c: V2}
- ความหมายของ
Eventจะแตกต่างกันตามชนิด- ใน site event,
aคือพิกัดของ site ส่วนb,cจะไม่ถูกใช้ - ใน circle event,
a,b,cคือส่วนโค้งสามเส้นบน beachline ที่สร้าง event นั้น
- ใน site event,
- โครงสร้าง
Fortuneจัดการสถานะต่อไปนี้beachline: อาร์เรย์ของV2queue: อาร์เรย์ของEventincomplete_edges: แมปPointPair -> V2vd: DCEL สำหรับเก็บ Voronoi Diagram
ส่วนที่ละไว้หรือทำให้ง่ายลงในการติดตั้งใช้งาน
- แม้ beachline จะถูกแทนด้วยเวกเตอร์ แต่หากต้องการประสิทธิภาพสูงขึ้น binary tree จะเหมาะสมกว่า
- event queue ตามแนวคิดควรเป็น priority queue แต่ในตัวอย่างนี้ใช้วิธีแทรกลงอาร์เรย์แบบเรียงลำดับ
- การทำให้ circle event ใช้ไม่ได้ถูกจัดการด้วยการไล่ตรวจ event ในอนาคตทีละตัว และยังมี TODO ว่าควรหาวิธีที่เร็วกว่า
clean_beachline_edgesคือขั้นตอนตัดส่วนโค้งที่ไม่จำเป็นออกจากปลายทั้งสองด้านของ beachline- ในการติดตั้งใช้งานมีการจัดการกรณียกเว้น เช่น site ที่มีพิกัด x เท่ากัน, กรณี circle point ตรงกับ site, และการชนกันของ reference point
- ขั้นตอนสุดท้ายหลังคิวว่าง ซึ่งต้องเก็บกวาด incomplete edge ที่เหลือ, half edge ที่ไม่มี twin, และ vertex ต่าง ๆ ถูกกล่าวถึงเพียงในระดับการคำนวณทางคณิตศาสตร์อย่างง่าย
การเก็บไดอะแกรมโวโรนอยด้วย DCEL
- โดยทั่วไปไดอะแกรมโวโรนอยจะถูกเก็บด้วย Doubly Connected Edge List (DCEL)
- DCEL เป็นโครงสร้างข้อมูลที่ช่วยให้จัดการ cell-complex ซึ่งประกอบด้วย vertex และ edge ได้สะดวก
- แม้จะเป็นการแทนข้อมูลแบบยึด edge เป็นศูนย์กลาง แต่ก็เก็บข้อมูลของ vertex และ face ไว้ด้วย
- edge ปกติไม่มีทิศทาง แต่ใน DCEL จะเก็บ edge แต่ละเส้นเป็น half edge สองเส้นที่มีทิศทางตรงข้ามกัน
- vertex ที่เก็บใน DCEL ของไดอะแกรมโวโรนอยไม่ใช่ site แต่เป็น Voronoi Vertex
- ปลายทางของ edge
Eสามารถหาได้จากE.twin.originและ face ด้านขวาหาได้จากE.twin.left
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เมื่อก่อนเคยทำ implementation ด้วย ClojureScript ที่แสดงภาพเคลื่อนไหวของการทำงานของอัลกอริทึม Fortune: https://voronoi.ajwerner.net/#/app-diagrams
เป็นอัลกอริทึมที่สวยงามมาก
แต่หลังจากโปรเจ็กต์นั้นก็เริ่มไม่ค่อยชอบ อัลกอริทึม Fortune เท่าไร เพราะมีปัญหาเรื่องเสถียรภาพเชิงตัวเลขของ floating point
ถ้าจุดต่าง ๆ อยู่บนเส้นตรงเดียวกัน หรือเกือบจะอยู่บนเส้นตรงเดียวกันในระดับ floating point ก็อาจพังได้
ถ้าจำไม่ผิด ในจุดนี้ delaunator ดีกว่า: https://github.com/mapbox/delaunator
เห็นลิงก์ implementation “old” อยู่ในหน้าที่อ้างอิง เลยสงสัยว่าเวอร์ชันแอนิเมชันปัจจุบันมีโอกาสจะเปิดเป็น โอเพนซอร์ส ไหม
เมื่อหลายปีก่อนเคยทำ ภาพแสดงผลแบบ 3D นี้ไว้: https://x.com/KangarooPhysics/status/1253336959755251716
มี implementation ภาษา JavaScript ของ Raymond Hill ผู้มีชื่อเสียงจาก uBlock Origin: https://github.com/gorhill/Javascript-Voronoi
ตรงนี้ลองปรับแต่งให้องค์ประกอบขยับได้เล็กน้อย: https://animations.adgent.com/voronoi.html
สงสัยว่าน่าจะเอาวิดีโอเป็นอินพุตแล้วป้อนเข้าอัลกอริทึมที่แสดงผลแบบ Voronoi ได้ไหม
ถ้าถึงขั้นนั้นแล้ว ตามนิยามเคร่งครัดมันอาจไม่ใช่ Voronoi diagram ก็ได้ แต่ก็น่าจะดูเท่มาก
D3.js มี implementation ใหม่: https://github.com/d3/d3-delaunay
ด้านล่างของหน้านั้นมีคำอธิบายเกี่ยวกับ sweep algorithm ที่ใช้ และรายการ implementation ภาษาอื่นนอกเหนือจาก JavaScript
d3-voronoi เดิมกำลังจะเลิกใช้ แต่ยังดูได้ที่นี่: https://github.com/d3/d3-voronoi
ถ้าไม่ได้สนใจเส้นขอบ และแค่อยากระบายแต่ละบริเวณเป็นคนละสี ก็ใช้ flood fill แบบดัดแปลงที่เริ่มจาก seed point ได้
แค่ใส่พิกเซลลงในสแตกเมื่อระยะของสีนั้นสั้นกว่าสีที่ระบายพิกเซลนั้นไว้อยู่แล้วเท่านั้น
ถ้าเรนเดอร์เป็นภาพฉายแบบออร์โธกราฟิก 2D จากด้านบนยอดกรวย z-buffer จะเก็บพิกเซลของยอดที่ใกล้ที่สุดไว้
อาจทำด้วยเชดเดอร์ก็ได้ แต่เดโมกรวย 3D แบบคลาสสิกนั้นเข้าใจและทำตามได้ง่ายมาก
น่าสนใจที่ D3 ย้ายจากอัลกอริทึม Fortune ไปใช้ https://mapbox.github.io/delaunator/
เหตุผลคือ “มันเร็วกว่า d3-voronoi 5~10 เท่า ในการสร้าง Delaunay triangulation หรือ Voronoi diagram, มีความทนทานเชิงตัวเลขมากกว่า, มี Canvas rendering ในตัว, รองรับการเดินกราฟ Delaunay และมีการปรับปรุงอื่น ๆ อีกหลายอย่าง”
โค้ดที่ใช้คำนวณไทล์อยู่ตอนนี้เรียกได้ว่าไร้เดียงสาจนน่าเจ็บปวด
การพูดคุยใหม่: https://github.com/KaliedaRik/Scrawl-canvas/discussions/120
บทความนี้ทำให้ฉันไปค้นดูว่าเดี๋ยวนี้ Steve อยู่ที่ไหน
เคยรู้จักกันเมื่อหลายสิบปีก่อน
บทความที่เกี่ยวข้องและน่าอ่าน: https://news.ycombinator.com/item?id=37998923 - การสร้าง Voronoi diagram และ Delaunay triangulation ใน O(n log n) ด้วยอัลกอริทึม Fortune (2020)
ในบทความก่อนหน้าและการพูดคุยมีสรุปสั้น ๆ ของอัลกอริทึมอื่น ๆ ด้วย
ส่วนตัวแล้วฉันยังคงชอบ Jump Flooding Algorithm มากที่สุด: https://en.wikipedia.org/wiki/Jump_flooding_algorithm