如何在2D地图中检测相连的(但逻辑上不同的)水体?


17

我有一个二维六角形网格图。每个十六进制单元格都有一个高度值,用于确定它是水还是海洋。我正在尝试一种确定和标记水体的好方法。海洋和内海都很容易(使用洪水填充算法)。

但是像地中海这样的水域呢?与较大的水体相连的水体(“海”和“海湾”的区别仅在于洞口的大小)?

这是我要检测的示例(图像中间的蓝色水域,尽管在技术上已连接,但其标签应与左侧较大的海洋体不同): 世界地图

有任何想法吗?

Answers:


10

您要描述的是细分问题。很抱歉,这实际上是一个未解决的问题。但是我为此建议的一种方法是基于图切的算法。Graph-Cut将图像表示为本地连接节点的图。它使用最大流最小割定理福特·富尔克森算法递归地细分图的连接组件,以使两个子组件之间的边界的长度最小。

本质上,您将所有水磁砖都连接到图形中。将权重分配给图形中与相邻水瓦片之间的差异相对应的边缘。我认为在您的情况下,所有权重都可能为1。您将必须使用不同的加权方案才能获得理想的结果。例如,您可能必须增加一些权重,其中包括对惯性运动的邻接。

然后,找到该图的所有连接的组件。这些是明显的海洋/湖泊等。

最后,对于每个连接的组件,递归地细分该组件,以使连接两个新的子组件的边缘的权重最小。递归地细分,直到所有子组件都达到最小大小(即像大海的最大大小一样),或者切割这两个组件的边缘的重量过大。最后,标记所有剩余的连接组件。

在实践中,这样做的目的是在航道之间互相切开海洋,但不要跨越大洋。

这是伪代码:

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

非常感谢你的这篇文章。我从您的榜样中得到了一些合理的信息
Kaelan Cooter 2015年

6

识别单独但相连的水体的一种快速而肮脏的方法是收缩所有水体,并查看是否出现缝隙。

在上面的示例中,我认为除去所有连接有2个或更少的瓷砖(标记为红色)的瓷砖将为您提供理想的效果以及一些边缘噪声。在标记了主体之后,可以将水“流动”到其原始状态,并为现在分离的主体回收取下的瓷砖。

在此处输入图片说明

同样,这是一个快速而肮脏的解决方案,对于后期生产可能还不够好,但是足以“立即使它工作”并继续使用其他功能。


5

我认为这是一个完整的算法,应该会产生良好的效果。

  1. 在水域上进行形态侵蚀 -也就是说,制作地图的副本,在每个地图上将其视为水当它和所有的邻国(或更大的区域,如果你有河流超过一个砖宽)是水。这将导致所有河流完全消失。

    (这会将内陆左侧的岛屿水视为河流。如果这是一个问题,则可以使用另一条规则,例如一个vrinek的答案提出的建议;只要您在此处执行某种“删除河流”步骤。)

  2. 在侵蚀地图中找到相连的水分量,并给每个水分量唯一的标签。(我想您已经知道该怎么做了。)现在,它标记了除了河流和海岸水(侵蚀作用所在的地方)以外的所有内容。

  3. 对于原始地图中的每个水瓦片,在侵蚀的地图中找到存在于相邻水瓦片上的标签,然后:

    • 如果瓷砖本身在腐蚀的地图上有标签,则说明它是海水。在原始地图中给它一个标签。
    • 如果仅找到一个不同的相邻标签,则为岸或河口;给它那个标签。
    • 如果找不到标签,那就是一条河。不要管它。
    • 如果找到多个标签,则这是两个较大实体之间的短暂瓶颈;您可能希望将其视为河流,或者将两个实体合并在一个标签下。

    (请注意,在此步骤中,您必须为侵蚀的地图(您从中读取)和原始的地图(您写入到其中)保留单独的标签网格(或在一个结构中具有两个标签字段),否则将存在迭代-与订单相关的错误。)

  4. 如果您也要唯一地标记单个河流,则在执行上述步骤之后,找到所有未标记水的剩余连接组件并标记它们。


1

遵循vrinek的想法,如何增加土地(或缩小水面),使原本要连接的部分在土地长大后会断开连接?

可以这样完成:

  1. 定义要增加多少土地:1十六进制?2个十六进制?这个值是n

  2. 拜访所有陆地节点,并将所有邻居置入深度 n陆地节点的(写入副本,以免产生无限循环)

  3. 再次运行原始的Floodfill算法,以确定现在已连接的内容和未连接的内容


0

您对海湾的位置有一个大概的认识吗?如果是这样,您可以修改洪水填充以跟踪相邻但未探索的单元格的数量(以及已访问单元格的列表)。它从十六进制映射图中的6开始,并且只要该值下降到某个特定点以下,便表示您正在击中“开孔”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.