2Dマップで接続された(ただし論理的に異なる)水域を検出するにはどうすればよいですか?


17

2D六角形のグリッドマップがあります。各六角形セルには、水か海かを判断するために使用される高さの値があります。私は、水域を特定してラベルを付ける良い方法を考えています。海洋と内海は簡単です(洪水充填アルゴリズムを使用)。

しかし、地中海のような水域はどうですか?大きなものに付着している水域(「海」と「湾」は開口部の大きさだけが異なる)

ここに私が検出しようとしているものの例があります(画像の中央の青い水域は、技術的に接続されているにもかかわらず、左の大きな海域とは異なるラベルが付けられているはずです): 世界地図

何か案は?

回答:


10

あなたが説明しているのはセグメンテーション問題です。実際には未解決の問題だと言ってすみません。しかし、これに推奨する方法の1つは、Graph-Cutベースのアルゴリズムです。Graph-Cutは、ローカルに接続されたノードのグラフとして画像を表します。Max-flow-min-cut定理Ford Fulkersonアルゴリズムを使用して、2つのサブコンポーネント間の境界の長さが最小になるように、グラフの接続されたコンポーネントを再帰的に分割します。

基本的に、すべての水タイルをグラフに接続します。隣接する水タイル間の差に対応するグラフのエッジに重みを割り当てます。あなたの場合、すべての重みが1になる可能性があると思います。望ましい結果を得るには、異なる重みスキームで遊ぶ必要があります。たとえば、海岸への隣接を含む重みを追加する必要がある場合があります。

次に、グラフのすべての接続コンポーネントを見つけます。これらは明らかな海/湖などです。

最後に、接続されたコンポーネントごとに、2つの新しいサブコンポーネントを接続するエッジの重み最小になるように、コンポーネントを再帰的に細分割します。すべてのサブコンポーネントが最小サイズ(海の最大サイズなど)に達するまで、または2つのコンポーネントを切断するエッジの重量が大きすぎる場合は、再帰的に分割し続けます。最後に、残っているすべての接続コンポーネントにラベルを付けます。

これが実際に行うことは、チャネルで海を切り離すことですが、大規模な海を横切ることはありません。

ここに擬似コードがあります:

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

編集:ところで、すべてのエッジの重みが1の場合、アルゴリズムは最小海サイズを約40に設定し、最大カットサイズを1にして、あなたの例で行うことです

イグル

パラメーターを操作することにより、さまざまな結果を得ることができます。たとえば、最大カットサイズが3の場合、より多くのベイがメインの海から切り出され、海#1は北と南の半分に細分化されます。最小の海のサイズが20の場合、中央の海​​も半分に分割されます。


強力なようです。間違いなく誘導と思った。
v.oddou 14

この投稿をありがとうございます。私はあなたの例から、合理的な何かを得るために管理
Kaelanクーター

6

接続されている別個の水域を迅速かつ汚い方法で識別するには、すべての水域を縮小し、隙間が表示されるかどうかを確認します。

上記の例では、2つ以下の水タイルが接続されているすべての水タイル(赤でマーク)を削除すると、望ましい結果とエッジノイズが得られると思います。ボディにラベルを付けた後、水を元の状態に「流し」、現在分離されているボディの除去されたタイルを再生できます。

ここに画像の説明を入力してください

繰り返しになりますが、これは迅速で汚れたソリューションであり、生産の後の段階には十分ではないかもしれませんが、「これを今すぐ動作させて」他の機能に移るだけで十分です。


5

良い結果が得られると思う完全なアルゴリズムを次に示します。

  1. 実行形態学的浸食、各タイルは水を考慮した上でマップのコピーを作成する-水域での唯一の(あなたが1枚のタイルより広い川を持っている場合は、より大きな面積、)それとその近隣諸国のすべてが水である場合に。これにより、すべての川が完全に消えます。

    (これにより、内海の左側の島水は川と見なされます。これが問題である場合は、代わりにvrinekの答えが提案するような別のルールを使用できます。以下の手順は、ここに何らかの「川の削除」ステップがあります。)

  2. 侵食されたマップの接続された水成分を見つけ、それぞれに一意のラベルを付けます。(これを行う方法を既に知っていると思います。)これで、川と海岸水(侵食が影響した場所)以外のすべてにラベルが付けられました。

  3. 元のマップの各水タイルについて、侵食されたマップの隣接する水タイルに存在するラベルを見つけてから、次の操作を行います。

    • タイル自体が侵食されたマップにラベルを持っている場合、それは海中水です。元のマップにそのラベルを付けます。
    • 隣接するラベルが1つだけ見つかった場合、それは海岸または河口です。そのラベルを付けます。
    • ラベルが見つからない場合は、川です。ほっといて。
    • 複数のラベルが見つかった場合、それは2つの大きなボディ間の短いボトルネックです。川のように考えるか、1つのラベルの下で2つのボディを組み合わせることができます。

    (このステップでは、侵食されたマップ(読み取り元)と元のマップ(書き込み先)のラベルのグリッドを別々に保持する(または1つの構造内に2つのラベルフィールドを持つ)必要があります。順序依存エラー。)

  4. 個々の川にも一意にラベルを付けたい場合は、上記の手順の後、ラベル付けされていない水の残りの接続コンポーネントをすべて見つけて、ラベル付けします。


1

vrinekのアイデアに従って、土地を成長(または水を収縮)させて、土地が成長した後に元々接続されていた部分が切断されるようにしますか?

これは次のように実行できます。

  1. 土地をどれだけ成長させたいかを定義します:1ヘクス?2ヘクス?この値はn

  2. すべての土地ノードを訪問し、すべての隣接nノードを深さまで土地ノードに設定します(無限ループを取得しないように、コピーに書き込みます)

  3. 元のフラッドフィルアルゴリズムを再度実行して、現在接続されているものと接続されていないものを判別します


0

湾がどこにあるのか大まかな考えはありますか?その場合、隣接するが未探索のセルの数を(訪問したセルのリストとともに)追跡するように塗りつぶしを変更できます。16進マップでは6から始まり、その値が特定のポイントを下回ると、「開始」に達していることがわかります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.