我有一个二维六角形网格图。每个十六进制单元格都有一个高度值,用于确定它是水还是海洋。我正在尝试一种确定和标记水体的好方法。海洋和内海都很容易(使用洪水填充算法)。
但是像地中海这样的水域呢?与较大的水体相连的水体(“海”和“海湾”的区别仅在于洞口的大小)?
这是我要检测的示例(图像中间的蓝色水域,尽管在技术上已连接,但其标签应与左侧较大的海洋体不同):
有任何想法吗?
我有一个二维六角形网格图。每个十六进制单元格都有一个高度值,用于确定它是水还是海洋。我正在尝试一种确定和标记水体的好方法。海洋和内海都很容易(使用洪水填充算法)。
但是像地中海这样的水域呢?与较大的水体相连的水体(“海”和“海湾”的区别仅在于洞口的大小)?
这是我要检测的示例(图像中间的蓝色水域,尽管在技术上已连接,但其标签应与左侧较大的海洋体不同):
有任何想法吗?
Answers:
您要描述的是细分问题。很抱歉,这实际上是一个未解决的问题。但是我为此建议的一种方法是基于图切的算法。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也会导致中央海面也被分成两半。
我认为这是一个完整的算法,应该会产生良好的效果。
在水域上进行形态侵蚀 -也就是说,制作地图的副本,在每个地图上仅将其视为水当它和所有的邻国(或更大的区域,如果你有河流超过一个砖宽)是水。这将导致所有河流完全消失。
(这会将内陆左侧的岛屿水视为河流。如果这是一个问题,则可以使用另一条规则,例如一个vrinek的答案提出的建议;只要您在此处执行某种“删除河流”步骤。)
在侵蚀地图中找到相连的水分量,并给每个水分量唯一的标签。(我想您已经知道该怎么做了。)现在,它标记了除了河流和海岸水(侵蚀作用所在的地方)以外的所有内容。
对于原始地图中的每个水瓦片,在侵蚀的地图中找到存在于相邻水瓦片上的标签,然后:
(请注意,在此步骤中,您必须为侵蚀的地图(您从中读取)和原始的地图(您写入到其中)保留单独的标签网格(或在一个结构中具有两个标签字段),否则将存在迭代-与订单相关的错误。)
如果您也要唯一地标记单个河流,则在执行上述步骤之后,找到所有未标记水的剩余连接组件并标记它们。