Я согласен с остальными, это был удивительно трудный вызов. Частично из-за необходимости иметь смежные пиксели одного и того же типа региона, но также из-за эстетической задачи сделать регионы похожими на карту стран.
Вот моя попытка ... это ужасно неэффективно, но, кажется, дает разумный результат. Продолжая тенденцию использования общего ввода для целей сравнения:
Параметры: 380 260 233 420 1300 3511 4772 5089 9507 22107 25117 26744
Параметры: 380 260 8 5 6 7 8 4 5 6 7 9 4 6 9 5 8 7 5
Темный век Камелота 213 307 1 1 1
Мой более крупный пример: (640 480 6 1 7 2 9 3 4 5 6 1 9 8 7 44 3 1 9 4 5 6 7 2 3 4 9 3 4 5 9 8 7 5 6 1 2 1 2 1 2 6 7 8 9 63 3)
Пример с большим количеством стран: 640 480 6 1 7 2 9 3 4 5 6 1 9 8 7 44 3 1 9 4 5 6 7 2 3 4 9 3 4 5 9 8 7 5 6 1 2 1 2 1 2 6 7 8 9 63 5 33 11 88 2 7 9 5 6 2 5 7
package GenerateRealisticMaps;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import javax.imageio.ImageIO;
public class GenerateRealisticMaps
{
private static final Random rand = new Random(3);
private static final Color[] paletteizedColours = new Color[100];
// create colour palette
static
{
paletteizedColours[0] = new Color(0xFF000000);
for (int i = 1; i < paletteizedColours.length; i++)
{
paletteizedColours[i] = Color.getHSBColor(rand.nextFloat(), rand.nextFloat(), 0.5f + rand.nextFloat() * 0.4f);
}
}
/**
* Represents a pixel that is the boundary of a region
* @author default
*
*/
public static class BoundaryPixel
{
public BoundaryPixel(int x, int y, int otherRegionId)
{
super();
this.x = x;
this.y = y;
this.otherRegionId = otherRegionId;
}
int x;
int y;
int otherRegionId;
}
/**
* Group of adjacent pixels that represent a region (i.e. a country in the map)
* @author default
*
*/
public static class Region
{
static private int masterId = 0;
Region(int desiredSize)
{
this.desiredSize = desiredSize;
id = ++masterId;
}
int desiredSize;
int size = 0;
int id;
List<BoundaryPixel> boundary = new ArrayList<GenerateRealisticMaps.BoundaryPixel>();
}
/**
* Container of regions
* @author default
*
*/
public static class Regions
{
List<Region> regionList = new ArrayList<GenerateRealisticMaps.Region>();
Map<Integer, Region> regionMap = new HashMap<Integer, GenerateRealisticMaps.Region>();
}
public static void main(String[] args) throws IOException
{
int width = Integer.parseInt(args[0]);
int height = Integer.parseInt(args[1]);
int[] s = new int[args.length - 2];
// read in the region weights
int sum = 0;
for (int i = 0; i < args.length - 2; i++)
{
sum += s[i] = Integer.parseInt(args[i + 2]);
}
int totalPixels = width * height;
double multiplier = ((double) totalPixels) / sum;
// convert region weights to pixel counts
int runningCount = 0;
for (int i = 0; i < s.length - 1; i++)
{
runningCount += s[i] = (int) (multiplier * s[i]);
}
s[s.length - 1] = totalPixels - runningCount;
Regions regions = new Regions();
int[][] map = new int[width][height];
// initialise region starting pixels
for (int v : s)
{
Region region = new Region(v);
regions.regionList.add(region);
regions.regionMap.put(region.id, region);
int x;
int y;
do
{
x = rand.nextInt(width);
y = rand.nextInt(height);
} while (map[x][y] != 0);
map[x][y] = region.id;
region.size++;
}
// initialise a "height" map that provides cost to claim a unclaimed region. This allows for more natural shaped countries
int[][] heightMap = new int[width][height];
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
heightMap[i][j] = rand.nextInt(50);
}
}
boolean equal = false;
// main loop
do
{
growRegions(map, heightMap, width, height, regions);
// determine whether regions have reached their desired size
equal = true;
for (Region region : regions.regionList)
{
equal = equal && region.size == region.desiredSize;
}
if (equal)
{
HashMap<Integer, Set<Integer>> commonIsolatedRegions = new HashMap<Integer, Set<Integer>>();
int isolatedRegionId = 0;
int[][] isolatedRegions = new int[width][height];
List<Integer> isolatedRegionSize = new ArrayList<Integer>();
isolatedRegionSize.add(-1); // add dummy entry at index 0 since region ids start at 1
// go though each pixel and attempt to identify an isolated region from that point if it as not
// yet been identified... i.e. an enclosed area.
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
if (isolatedRegions[i][j] == 0)
{
isolatedRegionId++;
Point point = new Point(i, j);
int size = identifyEnclosedArea(map, isolatedRegions, width, height, point, isolatedRegionId);
// add this isolated region id to the group of isolated regions associated with the region at this pixel
Set<Integer> isolatedRegionSet = commonIsolatedRegions.get(map[i][j]);
if (isolatedRegionSet == null)
{
isolatedRegionSet = new HashSet<Integer>();
commonIsolatedRegions.put(map[i][j], isolatedRegionSet);
}
isolatedRegionSet.add(isolatedRegionId);
isolatedRegionSize.add(size);
}
}
}
// only keep the largest isolated region in each group. Mark the other members in the group areas as unclaimed.
for (Region region : regions.regionList)
{
Set<Integer> isolatedRegionSet = commonIsolatedRegions.get(region.id);
// find the largest isolatedRegion mapped to this region
int largestIsolatedRegionId = -1;
int largestIsolatedRegionSize = -1;
for (Integer isolatedRegionIdentifier : isolatedRegionSet)
{
if (isolatedRegionSize.get(isolatedRegionIdentifier) > largestIsolatedRegionSize)
{
largestIsolatedRegionSize = isolatedRegionSize.get(isolatedRegionIdentifier);
largestIsolatedRegionId = isolatedRegionIdentifier;
}
}
// remove the largest isolated region (i.e. retain those pixels)
isolatedRegionSet.remove(largestIsolatedRegionId);
if (isolatedRegionSet.size() > 0)
{
equal = false;
// for all remaining isolated regions mapped to this region, convert to unclaimed areas.
for (Integer isolatedRegionIdentifier : isolatedRegionSet)
{
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
if (isolatedRegions[i][j] == isolatedRegionIdentifier)
map[i][j] = 0;
}
}
}
}
}
}
} while (!equal);
saveOutputImage("out.final.png", map);
}
/**
* Renders and saves the output image
*
* @param filename
* @param map
* @throws IOException
*/
public static void saveOutputImage(String filename, int[][] map) throws IOException
{
final int scale = 1;
final int width = map.length;
final int height = map[0].length;
BufferedImage image = new BufferedImage(width * scale, height * scale, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) image.getGraphics();
for (int j = 0; j < height; j++)
{
for (int i = 0; i < width; i++)
{
g.setColor(paletteizedColours[map[i][j]]);
g.fillRect(i * scale, j * scale, scale, scale);
}
}
ImageIO.write(image, "png", new File(filename));
}
/**
* Grows the regions of the world. Firstly by unclaimed cells and then by distributing cells amongst the regions.
*
* @param map
* cell to region map
* @param heightMap
* the "height" cost of unclaimed cells. Used to give more natural shapes.
* @param width
* @param height
* @param regions
*/
public static void growRegions(int[][] map, int[][] heightMap, int width, int height, Regions regions)
{
// reset region sizes
for (Region region : regions.regionList)
{
region.size = 0;
region.boundary.clear();
}
// populate corners with adjacent pixel region id... these pixels cannot ever be "grown" into.
map[0][0] = map[1][0];
map[width - 1][0] = map[width - 1][5];
map[width - 1][height - 1] = map[width - 2][height - 1];
map[0][height - 1] = map[1][height - 1];
int i, x, y, dx = 0, dy = 0, currHeight, currentId = -1, pixelRegionId;
Region currRegion = null;
;
// calculate initial region sizes
for (y = 0; y < height; y++)
{
for (x = 0; x < width; x++)
{
if (map[x][y] > 0)
regions.regionMap.get(map[x][y]).size++;
}
}
// expand regions into surrounding unclaimed pixels.
// construct a list of region boundary pixels in the process.
for (y = 1; y < height - 1; y++)
{
for (x = 1; x < width - 1; x++)
{
int cellId = map[x][y];
if (cellId > 0)
{
if (cellId != currentId)
{
currRegion = regions.regionMap.get(map[x][y]);
currentId = currRegion.id;
}
currHeight = heightMap[x][y]++;
for (i = 0; i < 4; i++)
{
switch (i)
{
case 0:
dx = x - 1;
dy = y;
break;
case 1:
dx = x + 1;
dy = y;
break;
case 2:
dx = x;
dy = y - 1;
break;
case 3:
dx = x;
dy = y + 1;
break;
}
pixelRegionId = map[dx][dy];
switch (pixelRegionId)
{
// unclaimed cell...
case 0:
if (heightMap[dx][dy] < currHeight)
{
map[dx][dy] = currRegion.id;
currRegion.size++;
}
break;
// claimed cell...
default:
if (pixelRegionId != currRegion.id)
{
currRegion.boundary.add(new BoundaryPixel(dx, dy, pixelRegionId));
}
break;
}
}
}
}
}
HashMap<Integer, List<BoundaryPixel>> neighbourBorders = new HashMap<Integer, List<BoundaryPixel>>();
// for all regions...
for (Region region : regions.regionList)
{
// that are less than the desired size...
if (region.size < region.desiredSize)
{
neighbourBorders.clear();
// identify the boundary segment per neighbour of the region
for (BoundaryPixel boundaryPixel : region.boundary)
{
List<BoundaryPixel> neighbourBorderSegment = neighbourBorders.get(boundaryPixel.otherRegionId);
if (neighbourBorderSegment == null)
{
neighbourBorderSegment = new ArrayList<GenerateRealisticMaps.BoundaryPixel>();
neighbourBorders.put(boundaryPixel.otherRegionId, neighbourBorderSegment);
}
neighbourBorderSegment.add(boundaryPixel);
}
out:
// for each neighbour...
for (int id : neighbourBorders.keySet())
{
Region neighbourRegion = regions.regionMap.get(id);
int surplusPixelCount = neighbourRegion.size - neighbourRegion.desiredSize;
// that has surplus pixels...
if (surplusPixelCount > 0)
{
// and convert the border segment pixels to the current region...
List<BoundaryPixel> neighbourBorderSegment = neighbourBorders.get(id);
int index = 0;
while (surplusPixelCount-- > 0 && index < neighbourBorderSegment.size())
{
BoundaryPixel boundaryPixel = neighbourBorderSegment.get(index++);
map[boundaryPixel.x][boundaryPixel.y] = region.id;
region.size++;
regions.regionMap.get(boundaryPixel.otherRegionId).size--;
// until we reach the desired size...
if (region.size == region.desiredSize)
break out;
}
}
}
}
// if region contains more pixels than desired...
else if (region.size > region.desiredSize)
{
// and the region has neighbours
if (region.boundary.size() > 0)
{
// choose a neighbour to off load extra pixels to
Region neighbour = regions.regionMap.get(region.boundary.remove(rand.nextInt(region.boundary.size())).otherRegionId);
ArrayList<BoundaryPixel> adjustedBoundary = new ArrayList<>();
// iterate over the boundary neighbour's boundary pixels...
for (BoundaryPixel boundaryPixel : neighbour.boundary)
{
// and then for those pixels which are of the current region, convert to the neighbour region
if (boundaryPixel.otherRegionId == region.id)
{
map[boundaryPixel.x][boundaryPixel.y] = neighbour.id;
neighbour.size++;
region.size--;
// stop when we reach the region's desired size.
if (region.size == region.desiredSize)
break;
}
else
{
adjustedBoundary.add(boundaryPixel);
}
}
neighbour.boundary = adjustedBoundary;
}
}
}
}
/**
* identifies the area, starting at the given point, in which adjacent pixels are of the same region id.
*
* @param map
* @param isolatedRegionMap
* cells identifying which area that the corresponding map cell belongs
* @param width
* @param height
* @param point
* the starting point of the area to be identified
* @param isolatedRegionId
* the id of the region to assign cells with
* @return the size of the identified area
*/
private static int identifyEnclosedArea(int[][] map, int[][] isolatedRegionMap, int width, int height, Point point, final int isolatedRegionId)
{
ArrayList<Point> stack = new ArrayList<Point>();
final int EXPECTED_REGION_ID = map[point.x][point.y];
stack.add(point);
int size = 0;
while (stack.size() > 0)
{
Point p = stack.remove(stack.size() - 1);
int x = p.x;
int y = p.y;
if (y < 0 || y > height - 1 || x < 0 || x > width - 1 || isolatedRegionMap[x][y] > 0)
continue;
int val = map[x][y];
if (val == EXPECTED_REGION_ID)
{
isolatedRegionMap[x][y] = isolatedRegionId;
size++;
stack.add(new Point(x + 1, y));
stack.add(new Point(x - 1, y));
stack.add(new Point(x, y + 1));
stack.add(new Point(x, y - 1));
}
}
return size;
}
}
Пояснение (из комментариев)
Алгоритм довольно прост: сначала инициализируйте карту со случайными весами, выберите случайные начальные пиксели для каждого из регионов страны. Во-вторых, «увеличивайте» каждую область, пытаясь претендовать на невостребованные смежные пиксели. Это происходит, когда вес текущего пикселя превышает невостребованный вес.
Каждый пиксель в области увеличивает свой вес в каждом цикле роста. Кроме того, если у области есть соседи, то если текущая рассматриваемая область имеет меньше пикселей, чем нужно, то она будет отбирать пиксели у своего соседа, если у соседа больше пикселей, чем нужно. Если текущая область имеет больше пикселей, чем ее сосед, то она случайным образом выбирает соседа и затем передает все лишние пиксели этому соседу. Когда все регионы имеют правильный размер, тогда происходит третий этап, чтобы идентифицировать и преобразовать любые регионы, которые были разделены и больше не являются непрерывными.
Сохраняется только самое большое разделение области, а остальные разделения преобразуются в невостребованные пиксели, и вторая фаза начинается снова. Это повторяется до тех пор, пока все пиксели в области не будут смежными, и все области не будут иметь правильный размер.
Этот вызов удивительно сложен. Я написал генератор карт на Python, используя Pygame. Программа превращает цветную область в свободное пространство, и в результате получается изображение, которое может выглядеть как карта (если вы щуритесь).
Мой алгоритм не всегда дополняет страны, так как в оставшейся области может не хватить места, но я подумал, что это привело к интересному эффекту, и я больше не буду тратить на него больше времени. Оставшиеся нечетные синие пятна можно считать большими озерами, а пятнистые синие элементы между странами - это реки, которые обозначают границу (это особенность, а не ошибка!).
Чтобы сравнить с Super Chafouin, я использовал их примеры параметров.
Параметры: 380 260 233 420 1300 3511 4772 5089 9507 22107 25117 26744
Параметры: 380 260 8 5 6 7 8 4 5 6 7 9 4 6 9 5 8 7 5
Темный век Камелота (213 307 1 1 1)
Мой более крупный пример: (640 480 6 1 7 2 9 3 4 5 6 1 9 8 7 44 3 1 9 4 5 6 7 2 3 4 9 3 4 5 9 8 7 5 6 1 2 1 2 1 2 6 7 8 9 63 3)
Этот пример немного похож на Восточную Европу?
Пример с большим количеством стран: 640 480 6 1 7 2 9 3 4 5 6 1 9 8 7 44 3 1 9 4 5 6 7 2 3 4 9 3 4 5 9 8 7 5 6 1 2 1 2 1 2 6 7 8 9 63 5 33 11 88 2 7 9 5 6 2 5 7
Я изменил генератор цветов в этом примере,
colors = [(80+ri(100), 80+ri(100), 80+ri(100)) for c in counts]
чтобы получить более мягкий (и похожий на карту) диапазон.Код Python:
источник
"any pixel in the region can be reached from any other by staying within the region and only moving orthogonally"
. Я вижу отдельные пиксели?Давайте будем ленивы и адаптируем мой ответ на этот вопрос !
Алгоритм вычисляет «путь змеи», начиная с верхнего левого угла, который заполняет весь прямоугольник. Змея может идти только вверх, вниз, влево, вправо.
Путь змеи идет по первому цвету, затем второму цвету и т. Д. С учетом процентного соотношения цветов
Этот алгоритм производит много прямых линий; чтобы улучшить его, я обнаруживаю их и заменяю их «волнами», которые сохраняют одинаковое количество пикселей.
Параметры: 380 260 233 420 1300 3511 4772 5089 9507 22107 25117 26744
Параметры: 380 260 8 5 6 7 8 4 5 6 7 9 4 6 9 5 8 7 5
Темный век Камелота (213 307 1 1 1)
Код:
источник