diff --git a/world_maker/City.py b/world_maker/City.py index fa0e581..95ca05f 100644 --- a/world_maker/City.py +++ b/world_maker/City.py @@ -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): diff --git a/world_maker/District.py b/world_maker/District.py index 5ee0888..e0e3f9a 100644 --- a/world_maker/District.py +++ b/world_maker/District.py @@ -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] diff --git a/world_maker/Skeleton.py b/world_maker/Skeleton.py index 574d856..ec1c0f1 100644 --- a/world_maker/Skeleton.py +++ b/world_maker/Skeleton.py @@ -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 \ No newline at end of file + return heightmap # , roadsArea diff --git a/world_maker/World.py b/world_maker/World.py index f914df4..c6499fd 100644 --- a/world_maker/World.py +++ b/world_maker/World.py @@ -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): diff --git a/world_maker/data/.gitignore b/world_maker/data/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/world_maker/data/custom_district.png b/world_maker/data/custom_district.png deleted file mode 100644 index 54bdf7f..0000000 Binary files a/world_maker/data/custom_district.png and /dev/null differ diff --git a/world_maker/data/heightmap.png b/world_maker/data/heightmap.png index 0f41f54..91a6780 100644 Binary files a/world_maker/data/heightmap.png and b/world_maker/data/heightmap.png differ diff --git a/world_maker/data/highwaymap.png b/world_maker/data/highwaymap.png index 84ba4bb..d675fe6 100644 Binary files a/world_maker/data/highwaymap.png and b/world_maker/data/highwaymap.png differ diff --git a/world_maker/data/negative_sobel_water_map.png b/world_maker/data/negative_sobel_water_map.png new file mode 100644 index 0000000..923b6a2 Binary files /dev/null and b/world_maker/data/negative_sobel_water_map.png differ diff --git a/world_maker/data/skeleton_highway.png b/world_maker/data/skeleton_highway.png new file mode 100644 index 0000000..47db64e Binary files /dev/null and b/world_maker/data/skeleton_highway.png differ diff --git a/world_maker/data/sobelmap.png b/world_maker/data/sobelmap.png index bab6916..0139d07 100644 Binary files a/world_maker/data/sobelmap.png and b/world_maker/data/sobelmap.png differ diff --git a/world_maker/data/test.png b/world_maker/data/test.png deleted file mode 100644 index 750776d..0000000 Binary files a/world_maker/data/test.png and /dev/null differ diff --git a/world_maker/data/treemap.png b/world_maker/data/treemap.png index acf6614..5d5e761 100644 Binary files a/world_maker/data/treemap.png and b/world_maker/data/treemap.png differ diff --git a/world_maker/data/watermap.png b/world_maker/data/watermap.png index 4f74618..2c3cdd5 100644 Binary files a/world_maker/data/watermap.png and b/world_maker/data/watermap.png differ diff --git a/world_maker/data_analysis.py b/world_maker/data_analysis.py index 4c55ff3..b9441e4 100644 --- a/world_maker/data_analysis.py +++ b/world_maker/data_analysis.py @@ -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') \ No newline at end of file + +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') diff --git a/world_maker/world_maker.py b/world_maker/world_maker.py index ac8b3db..6561f65 100644 --- a/world_maker/world_maker.py +++ b/world_maker/world_maker.py @@ -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'))