ฉันจะตรวจจับแหล่งน้ำ (แต่แตกต่างอย่างมีเหตุผล) ในแผนที่ 2 มิติได้อย่างไร


17

ฉันมีแผนที่กริดหกเหลี่ยมแบบ 2D แต่ละเซลล์ฐานสิบหกมีค่าความสูงที่ใช้ในการตรวจสอบว่าเป็นน้ำหรือมหาสมุทร ฉันพยายามคิดวิธีที่ดีในการกำหนดและปิดฉลากแหล่งน้ำ มหาสมุทรและทะเลในประเทศนั้นง่าย (โดยใช้อัลกอริธึมเติมน้ำท่วม)

แต่สิ่งที่เกี่ยวกับแหล่งน้ำเช่นทะเลเมดิเตอร์เรเนียน ? แหล่งน้ำที่ติดอยู่กับที่มีขนาดใหญ่กว่า (ที่ "ทะเล" และ "อ่าว" แตกต่างกันไปตามขนาดของช่องเปิด)?

นี่คือตัวอย่างของสิ่งที่ฉันพยายามตรวจจับ (เนื้อน้ำสีน้ำเงินที่อยู่ตรงกลางของรูปภาพซึ่งควรติดป้ายต่างจากร่างกายมหาสมุทรขนาดใหญ่ทางด้านซ้ายแม้ว่าจะมีการเชื่อมต่อทางเทคนิค): แผนที่โลก

ความคิดใด ๆ

คำตอบ:


10

สิ่งที่คุณกำลังอธิบายคือปัญหาการแบ่งกลุ่ม ฉันขอโทษที่ต้องบอกว่ามันเป็นปัญหาที่แก้ไม่ได้จริงๆ แต่วิธีหนึ่งที่ฉันอยากจะแนะนำสำหรับเรื่องนี้คืออัลกอริทึมที่ใช้กราฟ-Cut Graph-Cut แสดงภาพเป็นกราฟของโหนดที่เชื่อมต่อในเครื่อง มันแบ่งย่อยส่วนประกอบที่เชื่อมต่อของกราฟแบบซ้ำ ๆ เพื่อให้เส้นแบ่งระหว่างองค์ประกอบย่อยทั้งสองมีความยาวน้อยที่สุดโดยใช้ทฤษฎีบท Max-flow-min-cutและอัลกอริทึมของFord Fulkerson

โดยพื้นฐานแล้วคุณเชื่อมต่อแผ่นน้ำทั้งหมดลงในกราฟ กำหนดน้ำหนักให้กับขอบในกราฟซึ่งสอดคล้องกับความแตกต่างระหว่างแผ่นน้ำที่อยู่ติดกัน ฉันคิดว่าในกรณีของคุณน้ำหนักทั้งหมดอาจเป็น 1 คุณจะต้องเล่นกับแผนการถ่วงน้ำหนักที่แตกต่างกันเพื่อให้ได้ผลลัพธ์ที่ต้องการ คุณอาจต้องเพิ่มน้ำหนักที่รวมถึงคำคุณศัพท์ไปยังชายฝั่ง

จากนั้นค้นหาส่วนประกอบที่เชื่อมต่อทั้งหมดของกราฟ เหล่านี้เป็นทะเล / ทะเลสาบที่เห็นได้ชัดและอื่น ๆ

สุดท้ายสำหรับแต่ละองค์ประกอบที่เกี่ยวโยงกันซ้ำแบ่งองค์ประกอบดังกล่าวที่ขอบเชื่อมต่อใหม่ทั้งสององค์ประกอบย่อยมีน้ำหนักต่ำสุด แบ่งย่อยแบบวนซ้ำเรื่อย ๆ จนกว่าส่วนประกอบย่อยทั้งหมดจะมีขนาดเล็กที่สุด (เช่นขนาดสูงสุดของทะเล) หรือหากขอบที่ตัดทั้งสององค์ประกอบมีน้ำหนักสูงเกินไป สุดท้ายติดฉลากส่วนประกอบที่เชื่อมต่อทั้งหมดที่เหลืออยู่

ในทางปฏิบัติสิ่งนี้จะตัดทะเลออกจากกันในช่องทาง แต่ไม่ครอบคลุมมหาสมุทรขนาดใหญ่

นี่คือใน pseudocode:

function SegmentGraphCut(Map worldMap, int minimumSeaSize, int maximumCutSize)
    Graph graph = new Graph();
    // First, build the graph from the world map.
    foreach Cell cell in worldMap:
        // The graph only contains water nodes
        if not cell.IsWater():
            continue;

        graph.AddNode(cell);

        // Connect every water node to its neighbors
        foreach Cell neighbor in cell.neighbors:
            if not neighbor.IsWater():
                continue;
            else:  
                // The weight of an edge between water nodes should be related 
                // to how "similar" the waters are. What that means is up to you. 
                // The point is to avoid dividing bodies of water that are "similar"
                graph.AddEdge(cell, neighbor, ComputeWeight(cell, neighbor));

   // Now, subdivide all of the connected components recursively:
   List<Graph> components = graph.GetConnectedComponents();

   // The seas will be added to this list
   List<Graph> seas = new List<Graph>();
   foreach Graph component in components:
       GraphCutRecursive(component, minimumSeaSize, maximumCutSize, seas);


// Recursively subdivides a component using graph cut until all subcomponents are smaller 
// than a minimum size, or all cuts are greater than a maximum cut size
function GraphCutRecursive(Graph component, int minimumSeaSize, int maximumCutSize, List<Graph> seas):
    // If the component is too small, we're done. This corresponds to a small lake,
    // or a small sea or bay
    if(component.size() <= minimumSeaSize):
        seas.Add(component);
        return;

    // Divide the component into two subgraphs with a minimum border cut between them
    // probably using the Ford-Fulkerson algorithm
    [Graph subpartA, Graph subpartB, List<Edge> cut] = GetMinimumCut(component);

    // If the cut is too large, we're done. This corresponds to a huge, bulky ocean
    // that can't be further subdivided
    if (GetTotalWeight(cut) > maximumCutSize):
        seas.Add(component);
        return;
    else:
        // Subdivide each of the new subcomponents
        GraphCutRecursive(subpartA, minimumSeaSize, maximumCutSize);
        GraphCutRecursive(subpartB, minimumSeaSize, maximumCutSize);

แก้ไข : โดยวิธีต่อไปนี้เป็นสิ่งที่อัลกอริทึมจะทำกับตัวอย่างของคุณด้วยขนาดน้ำทะเลขั้นต่ำที่ตั้งไว้ที่ประมาณ 40 โดยมีขนาดการตัดสูงสุดที่ 1 ถ้าน้ำหนักขอบทั้งหมดเป็น 1:

Imgur

ด้วยการเล่นกับพารามิเตอร์คุณจะได้รับผลลัพธ์ที่แตกต่าง ยกตัวอย่างเช่นขนาดตัดสูงสุด 3 จะส่งผลให้ช่องอื่น ๆ อีกมากมายได้รับการแกะสลักออกจากทะเลหลักและทะเล # 1 จะได้รับการแบ่งย่อยในครึ่งเหนือและใต้ ขนาดต่ำสุดของทะเลที่ 20 จะส่งผลให้ทะเลกลางได้รับการแบ่งครึ่งเช่นกัน


ดูเหมือนว่ามีประสิทธิภาพ คิดอย่างแน่นอนทำให้เกิด
v.oddou

ขอบคุณมากสำหรับโพสต์นี้ ฉันจัดการเพื่อให้ได้สิ่งที่เหมาะสมจากตัวอย่างของคุณ
Kaelan Cooter

6

วิธีที่รวดเร็วและสกปรกในการระบุแหล่งน้ำแยก แต่เชื่อมต่อกันคือการย่อส่วนแหล่งน้ำทั้งหมดและดูว่ามีช่องว่างหรือไม่

ในตัวอย่างข้างต้นฉันคิดว่าการถอดแผ่นน้ำทั้งหมดที่มีแผ่นน้ำ 2 แผ่นหรือน้อยกว่าเชื่อมต่อกับพวกเขา (ทำเครื่องหมายสีแดง) จะช่วยให้คุณได้ผลลัพธ์ที่พึงประสงค์พร้อมกับเสียงรบกวนจากขอบ หลังจากที่คุณติดป้ายกำกับร่างกายคุณสามารถ "ไหล" น้ำไปยังสถานะดั้งเดิมและเรียกคืนไพ่ที่ถูกลบออกสำหรับวัตถุที่แยกต่างหากในขณะนี้

ป้อนคำอธิบายรูปภาพที่นี่

อีกครั้งนี่เป็นวิธีแก้ปัญหาที่รวดเร็วและสกปรกมันอาจจะไม่ดีพอสำหรับการผลิตในระยะหลัง แต่พอเพียงที่จะ "ทำให้มันใช้งานได้ในตอนนี้" และย้ายไปใช้คุณสมบัติอื่น ๆ


5

นี่คืออัลกอริทึมที่สมบูรณ์ที่ฉันคิดว่าควรให้ผลลัพธ์ที่ดี

  1. ทำการพังทลายของสัณฐานวิทยาบนพื้นที่น้ำนั่นคือทำสำเนาแผนที่ซึ่งแต่ละแผ่นนั้นถือว่าเป็นน้ำเฉพาะเมื่อมันและเพื่อนบ้านทั้งหมดของมัน (หรือพื้นที่ขนาดใหญ่ถ้าคุณมีแม่น้ำที่กว้างกว่าหนึ่งแผ่น) เป็นน้ำ . ซึ่งจะทำให้แม่น้ำทั้งหมดหายไปอย่างสิ้นเชิง

    (สิ่งนี้จะพิจารณาว่าน้ำเกาะที่อยู่ทางด้านซ้ายของทะเลภายในของคุณเป็นแม่น้ำหากเป็นปัญหาคุณสามารถใช้กฎที่แตกต่างกันเช่นคำตอบของข้อเสนอ vrinekแทนขั้นตอนต่อไปนี้จะยังคงทำงานตราบเท่าที่คุณ มีขั้นตอน“ ลบแม่น้ำ” บางส่วนที่นี่)

  2. ค้นหาส่วนประกอบของน้ำที่เชื่อมต่อกันของแผนที่ที่ถูกกัดเซาะและให้ฉลากแต่ละอันที่ไม่ซ้ำกัน (ฉันคิดว่าคุณรู้วิธีการทำเช่นนี้แล้ว) ตอนนี้ติดป้ายทุกอย่างยกเว้นแม่น้ำและน้ำชายฝั่ง (ซึ่งการกัดเซาะมีผลกระทบ)

  3. สำหรับแผ่นน้ำแต่ละแผ่นในแผนที่ดั้งเดิมให้ค้นหาป้ายชื่อที่อยู่บนแผ่นน้ำใกล้เคียงในแผนที่ที่ถูกกัดเซาะแล้ว:

    • หากตัวกระเบื้องมีฉลากอยู่ในแผนที่ที่กัดเซาะแสดงว่าเป็นน้ำกลางทะเล ให้ป้ายกำกับนั้นในแผนที่ดั้งเดิม
    • หากคุณพบป้ายกำกับที่อยู่ติดกันเพียงป้ายเดียวนั่นคือฝั่งหรือปากแม่น้ำ ให้มันฉลาก
    • หากคุณไม่พบป้ายกำกับแสดงว่าเป็นแม่น้ำ ทิ้งไว้คนเดียว
    • หากคุณพบป้ายกำกับหลายป้ายแสดงว่าเป็นคอขวดสั้น ๆ ระหว่างเนื้อความใหญ่สองรายการ คุณอาจต้องการพิจารณาว่ามันเหมือนแม่น้ำหรือรวมร่างทั้งสองไว้ใต้ฉลากเดียว

    (โปรดทราบว่าสำหรับขั้นตอนนี้คุณต้องเก็บกริดของป้ายกำกับแยกต่างหาก (หรือมีสองช่องป้ายกำกับในโครงสร้างเดียว) สำหรับแผนที่ที่กัดเซาะ (ที่คุณอ่านจาก) และต้นฉบับ (ที่คุณเขียนถึง) หรือจะมีการวนซ้ำ - ข้อผิดพลาดขึ้นอยู่กับคำสั่งซื้อ)

  4. หากคุณต้องการติดฉลากแม่น้ำแต่ละสายโดยเฉพาะด้วยหลังจากทำตามขั้นตอนด้านบนแล้วให้ค้นหาส่วนประกอบที่เชื่อมต่อที่เหลือทั้งหมดของน้ำที่ไม่มีป้ายกำกับและติดป้ายแม่น้ำเหล่านั้น


1

ตามความคิดของ vrinek วิธีการเกี่ยวกับการเติบโตของที่ดิน (หรือการหดตัวของน้ำ) ดังนั้นชิ้นส่วนที่คุณจะเชื่อมต่อในตอนแรกจะถูกตัดการเชื่อมต่อหลังจากที่ที่ดินเติบโตขึ้น?

สิ่งนี้สามารถทำได้ดังนี้:

  1. กำหนดว่าคุณต้องการปลูกที่ดินมากแค่ไหน: 1 hex 2 hexes หรือไม่ ค่านี้คือn

  2. เยี่ยมชมโหนดที่ดินทั้งหมดและตั้งค่าเพื่อนบ้านทั้งหมดของตนให้ลึกnถึงโหนดที่ดิน (เขียนถึงสำเนาเพื่อไม่ให้วนซ้ำไม่สิ้นสุด)

  3. เรียกใช้อัลกอริทึม Floodfill เดิมของคุณอีกครั้งเพื่อตรวจสอบสิ่งที่เชื่อมต่อในขณะนี้และสิ่งที่ไม่


0

คุณมีความคิดคร่าวๆว่าอ่าวอยู่ที่ไหน? ถ้าเป็นเช่นนั้นคุณสามารถแก้ไขการเติมน้ำท่วมของคุณเพื่อติดตามจำนวนของเซลล์ข้างเคียง แต่ไม่ได้สำรวจ (พร้อมกับรายการของเซลล์ที่เยี่ยมชม) มันเริ่มต้นด้วย 6 ในแผนที่เลขฐานสิบหกและเมื่อใดก็ตามที่ค่านั้นลดลงต่ำกว่าจุดหนึ่งคุณจะรู้ว่าคุณกำลังกด "เปิด"

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.