Skeleton highway

This commit is contained in:
NichiHachi
2024-06-13 00:36:19 +02:00
parent 2b2d5ba869
commit 03e1e441e5
16 changed files with 151 additions and 119 deletions

View File

@@ -63,15 +63,15 @@ class City:
"""
min_distance = point.distance_to(self.districts[index_district].center_expend)
index_district_chosen = index_district
for i in range(index_district + 1, len(self.districts)):
if point in self.districts[i].area_expend:
distance = point.distance_to(self.districts[i].center_expend)
for index in range(index_district + 1, len(self.districts)):
if point in self.districts[index].area_expend:
distance = point.distance_to(self.districts[index].center_expend)
if distance < min_distance:
min_distance = distance
self.districts[index_district_chosen].area_expend.remove(point)
index_district_chosen = i
index_district_chosen = index
else:
self.districts[i].area_expend.remove(point)
self.districts[index].area_expend.remove(point)
self.districts[index_district_chosen].area.append(point)
self.districts[index_district_chosen].area_expend_from_point.append(point)
self.districts[index_district_chosen].area_expend.remove(point)
@@ -106,8 +106,8 @@ class City:
"""
width, height = len(self.map_data[0]), len(self.map_data)
img = Image.new('RGB', (width, height))
colors = {i: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
for i in range(1, len(self.districts) + 1)}
colors = {id_district: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
for id_district in range(1, len(self.districts) + 1)}
for y in range(height):
for x in range(width):

View File

@@ -10,18 +10,18 @@ class District:
area_expend_from_point (list): The list of positions from which the district can expand.
area_expend (list): The list of positions to which the district will maybe expand.
"""
def __init__(self, tile_id: int, center: Position, type: str = ""):
def __init__(self, tile_id: int, center: Position, district_type: str = ""):
"""
The constructor for the District class.
:param tile_id: Unique id of the district (Must be greater than 0)
:param center: The center position from which the district expands.
:param type: The type of the district (Forest, City, Mountain, Villa)
:param district_type: The type of the district (Forest, City, Mountain, Villa)
"""
if tile_id <= 0:
raise ValueError("Tile id must be greater than 0")
self.tile_id = tile_id
self.type = type
self.type = district_type
self.center_expend = center
self.area = [center]
self.area_expend_from_point = [center]

View File

@@ -1,44 +1,40 @@
import numpy as np
import skan
#import skan
from skimage.morphology import skeletonize
from skan.csr import skeleton_to_csgraph
from collections import Counter
from PIL import Image
import random
from gdpc import Editor
class Skeleton:
def __init__(self):
def __init__(self, data: np.ndarray = None):
self.lines = []
self.intersections = []
self.centers = []
self.graph = []
self.coordinates = []
self.graph = None
if data is not None:
self.set_skeleton(data)
def setSkeleton(self, data):
binary_skeleton = skeletonize(data)
def set_skeleton(self, data: np.ndarray):
binary_skeleton = skeletonize(data, method="lee")
graph, coordinates = skeleton_to_csgraph(binary_skeleton)
self.graph = graph.tocoo()
# List of lists. Inverted coordinates.
coordinates = list(coordinates)
print(coordinates)
# print(coordinates)
for i in range(len(coordinates)):
coordinates[i] = list(coordinates[i])
print(coordinates)
coordinates_final = []
# print(coordinates)
for i in range(len(coordinates[0])):
print((coordinates[0][i], coordinates[1][i], coordinates[2][i]))
coordinates_final.append((coordinates[0][i], coordinates[1][i], coordinates[2][i]))
# print((coordinates[0][i], coordinates[1][i], coordinates[2][i]))
self.coordinates.append((coordinates[0][i], coordinates[1][i], coordinates[2][i]))
self.coordinates = coordinates_final
def findNextElements(self, key):
def find_next_elements(self, key: str) -> list:
"""Find the very nearest elements"""
line = []
@@ -51,51 +47,50 @@ class Skeleton:
line.append(self.graph.col[indices[i]])
return line
def findLine(self, key):
nextKeys = self.findNextElements(key)
def find_line(self, key: str):
next_keys = self.find_next_elements(key)
if len(nextKeys) >= 3: # Intersections.
return nextKeys
if len(next_keys) >= 3: # Intersections.
return next_keys
if len(nextKeys) == 2 or len(nextKeys) == 1: # In line or endpoints.
line = []
line.append(key)
line.insert(0, nextKeys[0])
if len(nextKeys) == 2:
line.insert(len(line), nextKeys[1])
if len(next_keys) == 2 or len(next_keys) == 1: # In line or endpoints.
line = [key]
line.insert(0, next_keys[0])
if len(next_keys) == 2:
line.insert(len(line), next_keys[1])
nextKeys = line[0], line[-1]
next_keys = line[0], line[-1]
while len(nextKeys) == 2 or len(nextKeys) == 1:
while len(next_keys) == 2 or len(next_keys) == 1:
extremity = []
for key in nextKeys:
nextKeys = self.findNextElements(key)
for key in next_keys:
next_keys = self.find_next_elements(key)
if len(nextKeys) <= 2:
if len(next_keys) <= 2:
# Add the neighbors that is not already in the line.
for element in nextKeys:
for element in next_keys:
if element not in line:
extremity.append(element)
line.append(element)
if len(nextKeys) >= 3:
if len(next_keys) >= 3:
# Add the intersection only.
extremity.append(key)
nextKeys = []
next_keys = []
for key in extremity:
ends = self.findNextElements(key)
ends = self.find_next_elements(key)
if len(ends) == 2:
nextKeys.append(key)
next_keys.append(key)
return line
def parseGraph(self):
def parse_graph(self, parse_orphan: bool = False):
for key, value in sorted(
Counter(self.graph.row).items(), key=lambda kv: kv[1], reverse=True
):
# Start from the biggest intersections.
if value != 2: # We don't want to be in the middle of a line.
line = self.findLine(key)
line = self.find_line(key)
# We have now all the connected points if it's an
# intersection. We need to find the line.
@@ -105,34 +100,44 @@ class Skeleton:
self.centers.append(key)
self.intersections.append(line)
for i in line:
line = self.findLine(i)
line = self.find_line(i)
if i in line:
# The key is inside the result : it's a line.
alreadyInside = False
already_inside = False
for l in self.lines:
# Verification if not already inside.
if Counter(l) == Counter(line):
alreadyInside = True
already_inside = True
# print(line, "inside", lines)
if alreadyInside == False:
if not already_inside:
self.lines.append(line)
else:
# The key is not inside the result, it's an
# intersection directly connected to the key.
line = [key, i]
alreadyInside = False
already_inside = False
for l in self.lines:
# Verification if not already inside.
if Counter(l) == Counter(line):
alreadyInside = True
already_inside = True
# print(line, "inside", lines)
if alreadyInside == False:
if not already_inside:
self.lines.append(line)
elif value == 2 and parse_orphan:
line = self.find_line(key)
already_inside = False
for l in self.lines:
# Verification if not already inside.
if Counter(l) == Counter(line):
already_inside = True
def map(self):
if not already_inside:
self.lines.append(line)
def map(self) -> Image:
"""
Generate an image to visualize 2D path of the skeleton.
@@ -140,17 +145,17 @@ class Skeleton:
Returns:
image: 2D path of the skeleton on top of the heightmap.
"""
editor = Editor()
# editor = Editor()
buildArea = editor.getBuildArea()
buildRect = buildArea.toRect()
xzStart = buildRect.begin
xzDistance = (max(buildRect.end[0], buildRect.begin[0]) - min(buildRect.end[0], buildRect.begin[0]),
max(buildRect.end[1], buildRect.begin[1]) - min(buildRect.end[1], buildRect.begin[1]))
# buildArea = editor.getBuildArea()
# buildRect = buildArea.toRect()
# xzStart = buildRect.begin
# xzDistance = (max(buildRect.end[0], buildRect.begin[0]) - min(buildRect.end[0], buildRect.begin[0]),
# max(buildRect.end[1], buildRect.begin[1]) - min(buildRect.end[1], buildRect.begin[1]))
heightmap = Image.open("data/heightmap.png").convert('RGB')
roadsArea = Image.new("L", xzDistance, 0)
width, height = heightmap.size
# roadsArea = Image.new("L", xzDistance, 0)
# width, height = heightmap.size
# Lines
for i in range(len(self.lines)):
@@ -158,7 +163,7 @@ class Skeleton:
for j in range(len(self.lines[i])):
z = self.coordinates[self.lines[i][j]][0]
y = self.coordinates[self.lines[i][j]][1]
# y = self.coordinates[self.lines[i][j]][1]
x = self.coordinates[self.lines[i][j]][2]
heightmap.putpixel(
@@ -169,26 +174,26 @@ class Skeleton:
(r + j, g + j, b + j),
)
roadsArea.putpixel(
(
int(z),
int(x),
),
(255),
)
# roadsArea.putpixel(
# (
# int(z),
# int(x),
# ),
# (255),
# )
# Centers
for i in range(len(self.centers)):
print(self.coordinates[self.centers[i]])
# print(self.coordinates[self.centers[i]])
heightmap.putpixel(
(int(self.coordinates[self.centers[i]][0]), int(self.coordinates[self.centers[i]][2])),
(255, 255, 0),
)
roadsArea.putpixel(
(int(self.coordinates[self.centers[i]][0]), int(self.coordinates[self.centers[i]][2])),
(255),
)
# roadsArea.putpixel(
# (int(self.coordinates[self.centers[i]][0]), int(self.coordinates[self.centers[i]][2])),
# (255),
# )
# # Intersections
# for i in range(len(self.intersections)):
@@ -202,4 +207,4 @@ class Skeleton:
# (255, 0, 255),
# )
return heightmap, roadsArea
return heightmap # , roadsArea

View File

@@ -145,7 +145,7 @@ class World:
number = 0
for i in range(-1, 2):
for j in range(-1, 2):
if (i != 0 or j != 0):
if i != 0 or j != 0:
if (0 <= x + i < xzDistance[0]) and (0 <= z + j < xzDistance[1]):
k = heightmapData[x + i][z + j] - 1
@@ -160,10 +160,10 @@ class World:
print(average, "average")
heightmap.putpixel((x, z), (average, average, average))
if ((biome in waterBiomes) or (block.id in waterBlocks)):
watermap.putpixel((x, z), (255))
if (biome in waterBiomes) or (block.id in waterBlocks):
watermap.putpixel((x, z), 255)
else:
watermap.putpixel((x, z), (0))
watermap.putpixel((x, z), 0)
self.addBlocks([Block((xzStart[0] + x, 100, xzStart[1] + z), block)]) # y set to 100 for 2D
@@ -190,7 +190,7 @@ class World:
for y in range(self.length_y):
binaryImage[x].append([])
for z in range(self.length_z):
if (self.volume[x][y][z] != None):
if self.volume[x][y][z] != None:
binaryImage[x][y].append(True)
else:
binaryImage[x][y].append(False)
@@ -222,7 +222,7 @@ class World:
for x in range(0, xzDistance[0]):
for z in range(0, xzDistance[1]):
y = heightmapData[x][z] - 1
if mask.getpixel((x, z)) == (255):
if mask.getpixel((x, z)) == 255:
self.removeBlock((x, 100, z)) # y set to 100 for 2D
def simplifyVolume(self):

0
world_maker/data/.gitignore vendored Normal file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,12 +1,10 @@
import World
from PIL import Image
from PIL import ImageFilter
from PIL import Image, ImageFilter
import numpy as np
import networkx as nx
from scipy import ndimage
from scipy.ndimage import gaussian_gradient_magnitude
from scipy.ndimage import label
from Skeleton import Skeleton
from typing import Union
def get_data(world: World):
heightmap, watermap, treemap = world.getData()
@@ -16,7 +14,13 @@ def get_data(world: World):
return heightmap, watermap, treemap
def filter_inverse(image: Image) -> Image:
def handle_import_image(image: Union[str, Image]) -> Image:
if isinstance(image, str):
return Image.open(image)
return image
def filter_negative(image: Image) -> Image:
"""
Invert the colors of an image.
@@ -26,7 +30,7 @@ def filter_inverse(image: Image) -> Image:
return Image.fromarray(np.invert(np.array(image)))
def filter_sobel(image) -> Image:
def filter_sobel(image: Union[str, Image]) -> Image:
"""
Edge detection algorithms from an image.
@@ -35,8 +39,7 @@ def filter_sobel(image) -> Image:
"""
# Open the image
if isinstance(image, str):
image = Image.open(image).convert('RGB')
image = handle_import_image(image).convert('RGB')
img = np.array(image).astype(np.uint8)
@@ -97,7 +100,7 @@ def filter_sobel(image) -> Image:
return image
def filter_smooth(image, radius: int = 3):
def filter_smooth(image: Union[str, Image], radius: int = 3):
"""
:param image: white and black image representing the derivative of the terrain (sobel), where black is flat and white is very steep.
:param radius: Radius of the Gaussian blur.
@@ -106,8 +109,7 @@ def filter_smooth(image, radius: int = 3):
image: black or white image, with black as flat areas to be skeletonized
"""
if isinstance(image, str):
image = Image.open(image)
image = handle_import_image(image)
# image = image.filter(ImageFilter.SMOOTH_MORE)
# image = image.filter(ImageFilter.SMOOTH_MORE)
@@ -128,7 +130,8 @@ def filter_smooth(image, radius: int = 3):
return Image.fromarray(bool_array)
def remove_water_from_map(image: Image) -> Image:
def remove_water_from_map(image: Union[str, Image]) -> Image:
image = handle_import_image(image)
watermap = Image.open('./data/watermap.png').convert('L')
array_heightmap = np.array(image)
@@ -141,42 +144,66 @@ def remove_water_from_map(image: Image) -> Image:
return result_image
def group_map(image1: Image, image2: Image) -> Image:
def group_map(image1: Union[str, Image], image2: Union[str, Image]) -> Image:
image1 = handle_import_image(image1)
image2 = handle_import_image(image2)
array1 = np.array(image1)
array2 = np.array(image2)
mask = array1 == 255
array2[mask] = 255
result_image = Image.fromarray(array2)
return result_image
return Image.fromarray(array2)
def filter_smooth_array(array: np.ndarray, radius: int = 3) -> np.ndarray:
image = Image.fromarray(array)
smooth_image = filter_smooth(image, radius)
array = np.array(smooth_image)
return array
def highway_map() -> Image:
smooth_sobel = filter_smooth("./data/sobelmap.png", 1)
inverse_sobel = filter_inverse(smooth_sobel)
inverse_sobel = filter_negative(smooth_sobel)
sobel_no_water = remove_water_from_map(inverse_sobel)
sobel_no_water.save("./data/test.png")
sobel_no_water.save("./data/negative_sobel_water_map.png")
array = np.array(sobel_no_water)
array = ndimage.binary_erosion(array, iterations=10)
array = ndimage.binary_dilation(array, iterations=5)
image = Image.fromarray(array)
smooth_image = filter_smooth(image, 5)
array = np.array(smooth_image)
array = filter_smooth_array(array, 5)
array = ndimage.binary_erosion(array, iterations=17)
image = Image.fromarray(array)
smooth_image = filter_smooth(image, 6)
array = np.array(smooth_image)
array = filter_smooth_array(array, 6)
array = ndimage.binary_dilation(array, iterations=3)
image = Image.fromarray(array)
image.save('./data/highwaymap.png')
return image
def skeletonnize_map(map: Image):
skeleton = Skeleton()
image_array = np.array(map)
skeleton.setSkeleton(image_array)
skeleton.parseGraph()
heightmap_skeleton, roadsArea = skeleton.map()
heightmap_skeleton.save('./data/skeleton.png')
roadsArea.save('./data/roads.png')
def create_volume(surface: np.ndarray, heightmap: np.ndarray, make_it_flat: bool = False) -> np.ndarray:
volume = np.full((len(surface), 255, len(surface[0])), False)
for z in range(len(surface)):
for x in range(len(surface[0])):
if not make_it_flat:
volume[x][heightmap[z][x]][z] = surface[z][x]
else:
volume[x][0][z] = surface[z][x]
return volume
def convert_2D_to_3D(image: Union[str, Image], make_it_flat: bool = False) -> np.ndarray:
image = handle_import_image(image)
heightmap = Image.open('./data/heightmap.png').convert('L')
heightmap = np.array(heightmap)
surface = np.array(image)
volume = create_volume(surface, heightmap, make_it_flat)
return volume
def skeleton_highway_map(map: Union[str, Image]):
image_array = convert_2D_to_3D(map, True)
skeleton = Skeleton(image_array)
skeleton.parse_graph(True)
heightmap_skeleton = skeleton.map()
heightmap_skeleton.save('./data/skeleton_highway.png')

View File

@@ -1,10 +1,10 @@
import World
from PIL import Image
from data_analysis import get_data, highway_map, filter_sobel, skeletonnize_map
from data_analysis import get_data, highway_map, filter_sobel, skeleton_highway_map
if __name__ == '__main__':
#world = World.World()
#heightmap, watermap, treemap = get_data(world)
#filter_sobel("./data/heightmap.png").save('./data/sobelmap.png')
filter_sobel("./data/heightmap.png").save('./data/sobelmap.png')
highway_map()
skeletonnize_map(Image.open('./data/highwaymap.png'))
skeleton_highway_map(Image.open('./data/highwaymap.png'))