То, что вы описываете, это проблема сегментации . Мне жаль говорить, что это на самом деле нерешенная проблема. Но один метод, который я бы порекомендовал для этого - алгоритм на основе Graph-Cut . Graph-Cut представляет изображение в виде графа локально связанных узлов. Он рекурсивно подразделяет связанные компоненты графа таким образом, что граница между двумя подкомпонентами имеет минимальную длину с использованием теоремы Max-flow-min-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);
РЕДАКТИРОВАТЬ : Кстати, вот что алгоритм будет делать с вашим примером с минимальным размером моря около 40, с максимальным размером разреза 1, если все веса ребер равны 1:
Играя с параметрами, вы можете получить разные результаты. Например, максимальный размер разреза, равный 3, приведет к тому, что из основных морей будет вырезано гораздо больше заливов, а море № 1 разделится пополам с севера на юг. Минимальный размер моря 20 приведет к тому, что центральное море также будет разделено пополам.