Come posso rilevare corpi idrici collegati (ma logicamente distinti) in una mappa 2D?


17

Ho una mappa a griglia esagonale 2D. Ogni cella esadecimale ha un valore di altezza utilizzato per determinare se si tratta di acqua o oceano. Sto cercando di pensare a un buon modo per determinare ed etichettare i corpi idrici. Oceani e mari interni sono facili (usando un algoritmo di riempimento di alluvione).

Ma che dire di specchi d'acqua come il Mediterraneo ? Corpi d'acqua che sono attaccati a quelli più grandi (dove "mari" e "golfi" differiscono solo per le dimensioni dell'apertura)?

Ecco un esempio di ciò che sto cercando di rilevare (il corpo blu dell'acqua nel mezzo dell'immagine, che dovrebbe essere etichettato in modo diverso dal più grande corpo di oceano sulla sinistra, nonostante sia tecnicamente collegato): mappa del mondo

Qualche idea?

Risposte:


10

Quello che stai descrivendo è il problema di segmentazione . Mi dispiace dire che in realtà è un problema irrisolto. Ma un metodo che consiglierei per questo è un algoritmo basato sul taglio grafico . Graph-Cut rappresenta l'immagine come un grafico di nodi connessi localmente. Suddivide ricorsivamente i componenti collegati del grafico in modo tale che il bordo tra i due sottocomponenti sia di lunghezza minima utilizzando il teorema Max-flow-min-cut e l' algoritmo Ford Fulkerson .

In sostanza, si collegano tutte le tessere dell'acqua in un grafico. Assegnare pesi ai bordi nel grafico che corrispondono alle differenze tra le piastrelle adiacenti dell'acqua. Penso che nel tuo caso, tutti i pesi potrebbero essere 1. Dovrai giocare con diversi schemi di ponderazione per ottenere un risultato desiderabile. Ad esempio, potresti dover aggiungere un po 'di peso che include l'adiacenza alle coste.

Quindi, trova tutti i componenti collegati del grafico. Questi sono ovvi mari / laghi e così via.

Infine, per ogni componente collegato, suddividere ricorsivamente il componente in modo tale che i bordi che collegano i due nuovi sottocomponenti abbiano un peso minimo . Continua a suddividere in modo ricorsivo fino a quando tutti i sottocomponenti raggiungono una dimensione minima (cioè come la dimensione massima di un mare) o se i bordi che tagliano i due componenti hanno un peso troppo elevato. Infine, etichettare tutti i componenti collegati che rimangono.

In pratica ciò che farà è tagliare i mari gli uni dagli altri ai canali, ma non attraversare grandi sponde di oceani.

Eccolo in pseudocodice:

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);

EDIT : A proposito, ecco cosa farebbe l'algoritmo con il tuo esempio con una dimensione del mare minima impostata su circa 40, con una dimensione di taglio massima di 1, se tutti i pesi del bordo sono 1:

Imgur

Giocando con i parametri, è possibile ottenere risultati diversi. Una dimensione massima di taglio di 3, ad esempio, comporterebbe molte più baie scavate dai mari principali e il mare n. 1 sarebbe suddiviso in metà nord e sud. Una dimensione minima del mare di 20 comporterebbe anche la divisione del mare centrale a metà.


sembra potente. sicuramente pensato di indurre.
v.oddou,

Grazie mille per questo post. Sono riuscito a ottenere qualcosa di ragionevole dal tuo esempio
Kaelan Cooter,

6

Un modo rapido e sporco per identificare un corpo idrico separato ma collegato sarebbe quello di ridurre tutti i corpi idrici e vedere se compaiono lacune.

Nell'esempio sopra penso che la rimozione di tutte le piastrelle d'acqua che hanno 2 o meno piastrelle d'acqua collegate (contrassegnate in rosso) fornirà il risultato desiderabile più un po 'di rumore di bordo. Dopo aver etichettato i corpi, puoi "far fluire" l'acqua al suo stato originale e recuperare le tessere rimosse per i corpi ora separati.

inserisci qui la descrizione dell'immagine

Ancora una volta, questa è una soluzione rapida e sporca, potrebbe non essere abbastanza buona per le fasi successive della produzione, ma basterà "farlo funzionare per ora" e passare ad altre funzionalità.


5

Ecco un algoritmo completo che penso dovrebbe produrre buoni risultati.

  1. Esegui l' erosione morfologica nell'area dell'acqua, ovvero crea una copia della mappa su cui ogni tessera è considerata acqua solo se essa e tutti i suoi vicini (o un'area più grande, se hai fiumi più larghi di una tessera) sono acqua . Ciò comporterà la scomparsa totale di tutti i fiumi.

    (Ciò considererà i fiumi dell'isola nella parte sinistra del tuo mare interno come fiumi. Se questo è un problema, potresti usare una regola diversa come quella proposta dalla risposta di Vrinek ; i seguenti passaggi funzioneranno comunque finché fare una sorta di passaggio "elimina fiumi" qui.)

  2. Trova i componenti idrici collegati della mappa erosa e assegna a ognuno un'etichetta unica . (Presumo che tu sappia già come farlo.) Questo ora identifica tutto tranne i fiumi e le acque costiere (dove l'erosione ha avuto un effetto).

  3. Per ogni piastrella d'acqua nella mappa originale , trova le etichette presenti sul vicino tessere acqua nella mappa erosa e quindi:

    • Se la tessera stessa ha un'etichetta nella mappa erosa, allora è acqua di mare; dagli l'etichetta nella mappa originale.
    • Se trovi solo un'etichetta vicina distinta, allora è la riva o la foce del fiume; dagli quell'etichetta.
    • Se non trovi etichette, allora è un fiume; lascialo da solo.
    • Se trovi più etichette, si tratta di un breve collo di bottiglia tra due corpi più grandi; potresti voler considerarlo come un fiume o combinare i due corpi sotto un'unica etichetta.

    (Nota che per questo passaggio devi mantenere griglie separate di etichette (o avere due campi etichetta in una struttura) per la mappa erosa (da cui leggi) e l'originale (da cui scrivi), o ci sarà iterazione- errori dipendenti dall'ordine.)

  4. Se si desidera etichettare in modo univoco anche i singoli fiumi, quindi dopo i passaggi precedenti, trovare tutti i restanti componenti collegati di acqua senza etichetta ed etichettarli.


1

Seguendo l'idea di vrinek, che ne dici di coltivare la terra (o restringere l'acqua) in modo che parti che verrebbero originariamente collegate verrebbero disconnesse dopo che la terra è cresciuta?

Questo potrebbe essere fatto in questo modo:

  1. Definisci quanto vuoi far crescere la terra: 1 esagono? 2 esagoni? Questo valore èn

  2. Visita tutti i nodi di terra e imposta tutti i loro vicini in profondità nsui nodi di terra (scrivi su una copia, in modo da non ottenere un ciclo infinito)

  3. Esegui nuovamente l'algoritmo di flooding originale per determinare cosa è ora connesso e cosa no


0

Hai una vaga idea di dove sia il golfo? In tal caso, puoi modificare il riempimento dell'inondazione per tenere traccia del numero di celle vicine ma inesplorate (insieme a un elenco di celle visitate). Inizia con 6 in una mappa esadecimale e ogni volta che quel valore scende al di sotto di un certo punto, allora sai che stai colpendo una "apertura".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.