diff --git a/world_maker/City.py b/world_maker/City.py index 8611be7..6993016 100644 --- a/world_maker/City.py +++ b/world_maker/City.py @@ -1,9 +1,10 @@ from District import District, Road from Position import Position from PIL import Image -import random -from data_analysis import handle_import_image +from random import randint +from data_analysis import handle_import_image, detect_mountain from typing import Union +import numpy as np class City: @@ -27,8 +28,8 @@ class City: """ Initialize the maps of the city. It reads the heightmap and watermap images and converts them into 2D lists. """ - heightmap = Image.open('./data/heightmap.png').convert('L') - watermap = Image.open('./data/watermap.png').convert('L') + heightmap = Image.open('./world_maker/data/heightmap.png').convert('L') + watermap = Image.open('./world_maker/data/watermap.png').convert('L') width, height = heightmap.size self.map_data = [[-1 if watermap.getpixel((x, y)) > 0 else 0 for x in range(width)] for y in range(height)] self.height_map = [[heightmap.getpixel((x, y)) for x in range(width)] for y in range(height)] @@ -104,7 +105,7 @@ class City: """ width, height = len(self.map_data[0]), len(self.map_data) img = Image.new('RGB', (width, height)) - colors = {id_district: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + colors = {id_district: (randint(0, 255), randint(0, 255), randint(0, 255)) for id_district in range(1, len(self.districts) + 1)} for y in range(height): @@ -114,19 +115,18 @@ class City: else: img.putpixel((x, y), colors[self.map_data[y][x]]) - img.save('./data/district.png') + img.save('./world_maker/data/district.png') print("[City] District map created.") - def draw_roads(self, image: Union[str, Image], size: int = 1) -> Image: + def draw_roads(self, size_road: int = 1) -> Image: """ Draw the roads of the city on the image. :param size: - :param image: The image to draw the roads on. """ - image = handle_import_image(image) + image = Image.new('RGB', Image.open('./world_maker/data/heightmap.png').size) for district in self.districts: - district.draw_roads(image, size) + district.draw_roads(image, size_road) return image def district_generate_road(self) -> list[Road]: @@ -137,17 +137,71 @@ class City: """ roads = [] for district in self.districts: - district.generate_roads(self.map_data) - roads.extend(district.roads) + if district.type != "mountain": + district.generate_roads(self.map_data) + roads.extend(district.roads) return roads + def point_in_which_district(self, point: Union[Position, tuple[int, int]]) -> int: + """ + Get the index of the district in which the point is located. + + :param point: The point to check. + :return: The index of the district in which the point is located. + """ + if isinstance(point, Position): + point = (point.x, point.y) + return self.map_data[point[1]][point[0]] + + def get_district_mountain_map(self) -> Image: + """ + Get the map of a district. + + :param district_id: The id of the district. + :return: The map of the district. + """ + district_id = [district.tile_id for district in self.districts if district.type == "mountain"] + array = np.array([[True if self.map_data[y][x] in district_id else False for x in range(len(self.map_data[0]))] + for y in range(len(self.map_data))]) + image = Image.fromarray(array) + image.save('./world_maker/data/mountain_map.png') + return image + + def generate_district(self): + image = handle_import_image('./world_maker/data/smooth_sobel_watermap.png').convert('L') + array = np.array(image) + mountain_coo = detect_mountain() + self.add_district(Position(mountain_coo[0], mountain_coo[1]), "mountain") + print("[City] District added.") + remove_circle_data(array, mountain_coo) + area = get_area_array(array) + sizeX, sizeY = len(array[0]), len(array) + while area > sizeX * sizeY * 0.1: + x, y = randint(0, sizeX - 1), randint(0, sizeY - 1) + if array[y][x]: + self.add_district(Position(x, y)) + remove_circle_data(array, (x, y)) + area = get_area_array(array) + print("[City] District added.") + + +def remove_circle_data(array, center, radius=100): + y_indices, x_indices = np.indices(array.shape) + dist_sq = (y_indices - center[1]) ** 2 + (x_indices - center[0]) ** 2 + mask = dist_sq <= radius ** 2 + array[mask] = False + + +def get_area_array(array) -> int: + return np.sum(array) + if __name__ == '__main__': city = City() for i in range(10): - city.add_district(Position(random.randint(0, 400), random.randint(0, 400))) + city.add_district(Position(randint(0, 400), randint(0, 400))) city.loop_expend_district() city.district_draw_map() city.district_generate_road() - image = city.draw_roads(Image.new('RGB', (401, 401)),4) - image.save('./data/roadmap.png') + image = city.draw_roads(Image.new('RGB', (401, 401)), 4) + image.save('./world_maker/data/roadmap.png') diff --git a/world_maker/District.py b/world_maker/District.py index 4a2f887..e0cd15a 100644 --- a/world_maker/District.py +++ b/world_maker/District.py @@ -3,6 +3,7 @@ from typing import Union from random import randint from PIL import Image + class Road: def __init__(self, position: Position, id_height: int, id_width: int, border: bool = False): self.position: Position = position @@ -58,7 +59,7 @@ class District: return (0 <= point_new.x < len(map_data[0]) and 0 <= point_new.y < len(map_data) and map_data[point_new.y][point_new.x] == 0 and - (self.type == "Mountain" or + (self.type == "mountain" or abs(height_map[point_new.y][point_new.x] - height_map[point.y][point.x]) < 2)) def is_point_inside(self, point: Position, map_data) -> bool: diff --git a/world_maker/Skeleton.py b/world_maker/Skeleton.py index 6b4d1ce..63a92f9 100644 --- a/world_maker/Skeleton.py +++ b/world_maker/Skeleton.py @@ -3,7 +3,7 @@ import numpy as np from skimage.morphology import skeletonize from skan.csr import skeleton_to_csgraph from collections import Counter -from PIL import Image +from PIL import Image, ImageDraw import random @@ -158,7 +158,7 @@ class Skeleton: # 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') + heightmap = Image.open("./world_maker/data/heightmap.png").convert('RGB') # roadsArea = Image.new("L", xzDistance, 0) # width, height = heightmap.size @@ -213,3 +213,30 @@ class Skeleton: # ) print("[Skeleton] Mapping completed.") return heightmap # , roadsArea + + def road_area(self, name: str, radius: int = 10) -> Image: + print("[Skeleton] Start mapping the road area...") + heightmap = Image.open("./world_maker/data/heightmap.png") + width, height = heightmap.size + road_area_map = Image.new("L", (width, height), 0) + road_area_map_draw = ImageDraw.Draw(road_area_map) + + # Lines + for i in range(len(self.lines)): + for j in range(len(self.lines[i])): + z = self.coordinates[self.lines[i][j]][0] + x = self.coordinates[self.lines[i][j]][2] + circle_coords = (z - radius, x - radius, z + radius, x + radius) + road_area_map_draw.ellipse(circle_coords, fill=255) + + # Centers + for i in range(len(self.centers)): + z = self.coordinates[self.centers[i]][0] + x = self.coordinates[self.centers[i]][2] + circle_coords = (z - radius, x - radius, z + radius, x + radius) + road_area_map_draw.ellipse(circle_coords, fill=255) + + road_area_map.save("./world_maker/data/"+name) + + print("[Skeleton] Road area mapping completed.") + return road_area_map diff --git a/world_maker/data/building.png b/world_maker/data/building.png index 48e51dc..9dc311c 100644 Binary files a/world_maker/data/building.png and b/world_maker/data/building.png differ diff --git a/world_maker/data/city_map.png b/world_maker/data/city_map.png new file mode 100644 index 0000000..1a5949c Binary files /dev/null and b/world_maker/data/city_map.png differ diff --git a/world_maker/data/district.png b/world_maker/data/district.png index fc00abf..96b3d4f 100644 Binary files a/world_maker/data/district.png and b/world_maker/data/district.png differ diff --git a/world_maker/data/heightmap.png b/world_maker/data/heightmap.png index 4ec6160..fc75ff3 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 0a5673e..7c4d178 100644 Binary files a/world_maker/data/highwaymap.png and b/world_maker/data/highwaymap.png differ diff --git a/world_maker/data/mountain_map.png b/world_maker/data/mountain_map.png new file mode 100644 index 0000000..082c138 Binary files /dev/null and b/world_maker/data/mountain_map.png differ diff --git a/world_maker/data/roadmap.png b/world_maker/data/roadmap.png index 841ca64..3c976c0 100644 Binary files a/world_maker/data/roadmap.png and b/world_maker/data/roadmap.png differ diff --git a/world_maker/data/roadmap2.png b/world_maker/data/roadmap2.png deleted file mode 100644 index c084d6e..0000000 Binary files a/world_maker/data/roadmap2.png and /dev/null differ diff --git a/world_maker/data/skeleton_highway.png b/world_maker/data/skeleton_highway.png index 2ecb893..221a5d9 100644 Binary files a/world_maker/data/skeleton_highway.png and b/world_maker/data/skeleton_highway.png differ diff --git a/world_maker/data/skeleton_highway_area.png b/world_maker/data/skeleton_highway_area.png new file mode 100644 index 0000000..4bffcc6 Binary files /dev/null and b/world_maker/data/skeleton_highway_area.png differ diff --git a/world_maker/data/skeleton_mountain.png b/world_maker/data/skeleton_mountain.png new file mode 100644 index 0000000..050fff0 Binary files /dev/null and b/world_maker/data/skeleton_mountain.png differ diff --git a/world_maker/data/skeleton_mountain_area.png b/world_maker/data/skeleton_mountain_area.png new file mode 100644 index 0000000..af563e2 Binary files /dev/null and b/world_maker/data/skeleton_mountain_area.png differ diff --git a/world_maker/data/smooth_sobel_watermap.png b/world_maker/data/smooth_sobel_watermap.png index 20ff75c..a2c350d 100644 Binary files a/world_maker/data/smooth_sobel_watermap.png and b/world_maker/data/smooth_sobel_watermap.png differ diff --git a/world_maker/data/sobelmap.png b/world_maker/data/sobelmap.png index c608899..e587909 100644 Binary files a/world_maker/data/sobelmap.png and b/world_maker/data/sobelmap.png differ diff --git a/world_maker/data/treemap.png b/world_maker/data/treemap.png index bf7f01f..83a187a 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 c900dbb..36cfd74 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 56e061f..6386d74 100644 --- a/world_maker/data_analysis.py +++ b/world_maker/data_analysis.py @@ -4,14 +4,16 @@ import numpy as np from scipy import ndimage from Skeleton import Skeleton from typing import Union +from random import randint +import cv2 def get_data(world: World): print("[Data Analysis] Generating data...") heightmap, watermap, treemap = world.getData() - heightmap.save('./data/heightmap.png') - watermap.save('./data/watermap.png') - treemap.save('./data/treemap.png') + heightmap.save('./world_maker/data/heightmap.png') + watermap.save('./world_maker/data/watermap.png') + treemap.save('./world_maker/data/treemap.png') print("[Data Analysis] Data generated.") return heightmap, watermap, treemap @@ -22,13 +24,14 @@ def handle_import_image(image: Union[str, Image]) -> Image: return image -def filter_negative(image: Image) -> Image: +def filter_negative(image: Union[str, Image]) -> Image: """ Invert the colors of an image. Args: image (image): image to filter """ + image = handle_import_image(image) return Image.fromarray(np.invert(np.array(image))) @@ -179,9 +182,9 @@ def filter_remove_details(image: Union[str, Image], n: int = 20) -> Image: def highway_map() -> Image: print("[Data Analysis] Generating highway map...") - smooth_sobel = filter_smooth("./data/sobelmap.png", 1) + smooth_sobel = filter_smooth("./world_maker/data/sobelmap.png", 1) negative_smooth_sobel = filter_negative(smooth_sobel) - negative_smooth_sobel_water = subtract_map(negative_smooth_sobel, './data/watermap.png') + negative_smooth_sobel_water = subtract_map(negative_smooth_sobel, './world_maker/data/watermap.png') array_sobel_water = np.array(negative_smooth_sobel_water) array_sobel_water = ndimage.binary_erosion(array_sobel_water, iterations=12) array_sobel_water = ndimage.binary_dilation(array_sobel_water, iterations=5) @@ -190,7 +193,7 @@ def highway_map() -> Image: array_sobel_water = filter_smooth_array(array_sobel_water, 6) image = Image.fromarray(array_sobel_water) image_no_details = filter_remove_details(image, 15) - image_no_details.save('./data/highwaymap.png') + image_no_details.save('./world_maker/data/highwaymap.png') print("[Data Analysis] Highway map generated.") return image_no_details @@ -208,27 +211,82 @@ def create_volume(surface: np.ndarray, heightmap: np.ndarray, make_it_flat: bool 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 = Image.open('./world_maker/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(image: Union[str, Image] = './data/highwaymap.png'): +def skeleton_highway_map(image: Union[str, Image] = './world_maker/data/highwaymap.png') -> Skeleton: image_array = convert_2D_to_3D(image, True) skeleton = Skeleton(image_array) skeleton.parse_graph(True) heightmap_skeleton = skeleton.map() - heightmap_skeleton.save('./data/skeleton_highway.png') + heightmap_skeleton.save('./world_maker/data/skeleton_highway.png') + skeleton.road_area('skeleton_highway_area.png', 10) + return skeleton + + +def skeleton_mountain_map(image: Union[str, Image] = './world_maker/data/mountain_map.png') -> Skeleton: + image_array = convert_2D_to_3D(image, True) + skeleton = Skeleton(image_array) + skeleton.parse_graph() + heightmap_skeleton = skeleton.map() + heightmap_skeleton.save('./world_maker/data/skeleton_mountain.png') + skeleton.road_area('skeleton_mountain_area.png', 3) + return skeleton def smooth_sobel_water() -> Image: - watermap = handle_import_image("./data/watermap.png") + watermap = handle_import_image("./world_maker/data/watermap.png") watermap = filter_negative(filter_remove_details(filter_negative(watermap), 5)) - sobel = handle_import_image("./data/sobelmap.png") + sobel = handle_import_image("./world_maker/data/sobelmap.png") sobel = filter_remove_details(filter_smooth(sobel, 1), 2) group = group_map(watermap, sobel) group = filter_negative(group) - group.save('./data/smooth_sobel_watermap.png') + group.save('./world_maker/data/smooth_sobel_watermap.png') return group + + +def detect_mountain(image: Union[str, Image] = './world_maker/data/sobelmap.png') -> Image: + image = handle_import_image(image) + sobel = np.array(image) + pixels = sobel.reshape((-1, 1)) + pixels = np.float32(pixels) + + criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2) + k = 3 + _, labels, centers = cv2.kmeans(pixels, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS) + + centers = np.uint8(centers) + segmented_image = centers[labels.flatten()] + segmented_image = segmented_image.reshape(sobel.shape) + mountain = segmented_image == segmented_image.max() + + contours, _ = cv2.findContours(mountain.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + max_contour = max(contours, key=cv2.contourArea) + M = cv2.moments(max_contour) + cX = int(M["m10"] / M["m00"]) + cY = int(M["m01"] / M["m00"]) + + print(f"[Data Analysis] The center of the mountain is at ({cX}, {cY})") + return (cX, cY) + + +def rectangle_2D_to_3D(rectangle: list[tuple[tuple[int, int], tuple[int, int]]], + height_min: int = 6, height_max: int = 10) \ + -> list[tuple[tuple[int, int, int], tuple[int, int, int]]]: + image = handle_import_image('./world_maker/data/heightmap.png').convert('L') + new_rectangle = [] + for rect in rectangle: + start, end = rect + avg_height = 0 + for x in range(start[0], end[0]): + for y in range(start[1], end[1]): + avg_height += image.getpixel((x, y)) + avg_height = int(avg_height / ((end[0] - start[0]) * (end[1] - start[1]))) + new_rectangle.append( + ((start[0], avg_height, start[1]), (end[0], avg_height + randint(height_min, height_max), end[1]))) + return new_rectangle diff --git a/world_maker/pack_rectangle.py b/world_maker/pack_rectangle.py index 879ae8c..3611fc1 100644 --- a/world_maker/pack_rectangle.py +++ b/world_maker/pack_rectangle.py @@ -2,6 +2,8 @@ from PIL import Image import numpy as np from typing import Union from data_analysis import handle_import_image +from random import randint + class Rectangle: def __init__(self, width, height): @@ -18,7 +20,7 @@ class Bin: best_spot = None best_spot_empty_area = float('inf') - for i in range(len(self.grid[0]) - rectangle.width + 1): # Swap usage of x and y + for i in range(len(self.grid[0]) - rectangle.width + 1): for j in range(len(self.grid) - rectangle.height + 1): if self.can_place(rectangle, i, j): empty_area = self.calculate_empty_area(rectangle, i, j) @@ -27,7 +29,7 @@ class Bin: best_spot_empty_area = empty_area if best_spot is not None: - self.rectangles.append((best_spot, (best_spot[0]+rectangle.width, best_spot[1]+rectangle.height))) + self.rectangles.append((best_spot, (best_spot[0] + rectangle.width, best_spot[1] + rectangle.height))) self.update_grid(rectangle, *best_spot) return True @@ -37,21 +39,21 @@ class Bin: empty_area = 0 for rect_x in range(x, x + rectangle.width): for rect_y in range(y, y + rectangle.height): - if self.grid[rect_y][rect_x]: # Swap usage of x and y + if self.grid[rect_y][rect_x]: empty_area += 1 return empty_area def can_place(self, rectangle, x, y): for rect_x in range(x, x + rectangle.width): for rect_y in range(y, y + rectangle.height): - if not self.grid[rect_y][rect_x]: # Swap usage of x and y + if not self.grid[rect_y][rect_x]: return False return True def update_grid(self, rectangle, x, y): for rect_x in range(x, x + rectangle.width): for rect_y in range(y, y + rectangle.height): - self.grid[rect_y][rect_x] = False # Swap usage of x and y + self.grid[rect_y][rect_x] = False def pack_rectangles(rectangles, grid): @@ -62,36 +64,30 @@ def pack_rectangles(rectangles, grid): for bin in bins: if bin.place_rectangle(rectangle): break - else: # No break, meaning rectangle couldn't be placed in any bin + else: new_bin = Bin(grid) if new_bin.place_rectangle(rectangle): bins.append(new_bin) else: - return False # If a rectangle can't be placed even in a new bin, return False + return False - return True # If all rectangles can be placed, return True + return True -import random - - -def generate_rectangle(max_width, max_height): - width = random.randint(6, 20) - height = random.randint(6, 20) +def generate_rectangle(min_width: int = 10, max_width: int = 25): + width = randint(min_width, max_width) + height = randint(min_width, max_width) return Rectangle(width, height) -def pack_rectangles(grid): - max_width = len(grid[0]) - max_height = len(grid) +def pack_rectangles(grid, min_width: int = 10, max_width: int = 25): bin = Bin(grid) - while True: - rectangle = generate_rectangle(max_width // 2, max_height // 2) + rectangle = generate_rectangle(min_width, max_width) if not bin.place_rectangle(rectangle): - break # Stop when a rectangle can't be placed + break print(len(bin.rectangles)) - return bin.rectangles # Return the list of rectangles that were placed + return bin.rectangles def draw_rectangles(rectangles, grid): @@ -104,12 +100,13 @@ def draw_rectangles(rectangles, grid): return image -def generate_building(image: Union[str, Image] = './data/roadmap2.png'): +def generate_building(image: Union[str, Image], min_width: int = 10, max_width: int = 25): image = handle_import_image(image).convert('L') grid = np.array(image) - rectangles = pack_rectangles(grid) - draw_rectangles(rectangles, grid).save('./data/building.png') + rectangles = pack_rectangles(grid, min_width, max_width) + draw_rectangles(rectangles, grid).save('./world_maker/data/building.png') return rectangles + if __name__ == '__main__': generate_building() diff --git a/world_maker/world_maker.py b/world_maker/world_maker.py index c86e021..8ea3510 100644 --- a/world_maker/world_maker.py +++ b/world_maker/world_maker.py @@ -1,22 +1,33 @@ import World from PIL import Image -from data_analysis import get_data, highway_map, filter_sobel, skeleton_highway_map, smooth_sobel_water, subtract_map +from data_analysis import get_data,filter_negative, rectangle_2D_to_3D, skeleton_mountain_map, highway_map, filter_sobel, skeleton_highway_map, \ + smooth_sobel_water, subtract_map, detect_mountain from City import City from Position import Position from random import randint +from pack_rectangle import generate_building if __name__ == '__main__': #world = World.World() #heightmap, watermap, treemap = get_data(world) - #filter_sobel("./data/heightmap.png").save('./data/sobelmap.png') + #filter_sobel("./world_maker/data/heightmap.png").save('./world_maker/data/sobelmap.png') smooth_sobel_water = smooth_sobel_water() - #skeleton_highway_map(highway_map()) + skeleton_highway_map(highway_map()) city = City() - for i in range(10): - city.add_district(Position(randint(0, 400), randint(0, 400))) + city.generate_district() city.loop_expend_district() city.district_draw_map() city.district_generate_road() - road = city.draw_roads(Image.new('RGB', (401, 401)), 4) - road.save('./data/roadmap.png') - subtract_map(smooth_sobel_water, road).save('./data/roadmap2.png') + image_mountain_map = city.get_district_mountain_map() + road = city.draw_roads(4) + road.save('./world_maker/data/roadmap.png') + subtract_map(smooth_sobel_water, road).save('./world_maker/data/city_map.png') + subtract_map('./world_maker/data/city_map.png', './world_maker/data/skeleton_highway_area.png').save('./world_maker/data/city_map.png') + subtract_map('./world_maker/data/city_map.png', './world_maker/data/mountain_map.png').save('./world_maker/data/city_map.png') + rectangle_building = generate_building('./world_maker/data/city_map.png') + rectangle_building = rectangle_2D_to_3D(rectangle_building) + + skeleton_mountain_map(image_mountain_map) + subtract_map('./world_maker/data/mountain_map.png', './world_maker/data/skeleton_mountain_area.png').save('./world_maker/data/mountain_map.png') + subtract_map(smooth_sobel_water, filter_negative('./world_maker/data/mountain_map.png')).save('./world_maker/data/mountain_map.png') + rectangle_mountain = generate_building('./world_maker/data/mountain_map.png')