diff --git a/main.py b/main.py index 42fb88d..2938619 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import random +from math import exp, sqrt from gdpc import Editor, Block @@ -21,6 +22,9 @@ def main(): editor = Editor(buffering=True) buildArea = editor.getBuildArea() origin = ((buildArea.begin).x, (buildArea.begin).z) + center = (abs(buildArea.begin.x - buildArea.end.x) / 2, + abs(buildArea.begin.z - buildArea.end.z) / 2) + length_world = sqrt((center[0]*2) ** 2 + (center[1]*2) ** 2) remove_trees('./world_maker/data/heightmap.png', './world_maker/data/treemap.png', './world_maker/data/smooth_sobel_watermap.png') @@ -52,30 +56,42 @@ def main(): entranceDirection = ["N", "S", "E", "W"] for houses in rectangle_building: - start = (houses[0][0]+buildArea.begin[0], houses[0] - [1], houses[0][2]+buildArea.begin[2]) - end = (houses[1][0]+buildArea.begin[0], houses[1] - [1], houses[1][2]+buildArea.begin[2]) + height = get_height_building_from_center( + center, (houses[0][0], houses[0][2]), length_world) + start = (houses[0][0] + origin[0], houses[0] + [1], houses[0][2] + origin[1]) + end = (houses[1][0] + origin[0], houses[1] + [1] + height, houses[1][2] + origin[1]) house = House(editor, start, end, entranceDirection[random.randint(0, 3)], blocks) house.build() for houses in rectangle_house_mountain: - start = (houses[0][0]+buildArea.begin[0], houses[0] - [1], houses[0][2]+buildArea.begin[2]) - end = (houses[1][0]+buildArea.begin[0], houses[1] - [1], houses[1][2]+buildArea.begin[2]) + start = (houses[0][0] + origin[0], houses[0] + [1], houses[0][2] + origin[1]) + end = (houses[1][0] + origin[0], houses[1] + [1], houses[1][2] + origin[1]) house = House(editor, start, end, entranceDirection[random.randint(0, 3)], blocks) house.build() +def get_height_building_from_center(center, position, length_world): + length = abs( + sqrt(((center[0] - position[0]) ** 2 + (center[1] - position[1]) ** 2))) + print(length, length_world) + return int(exp(-(length / (length_world / 4)) ** 2) * 75 + 30) + + def set_roads_grids(road_grid: Road_grid, origin): for i in range(len(road_grid)): if road_grid[i].border: for j in range(len(road_grid)): # Same line - if (road_grid[i].position.x == road_grid[j].position.x and road_grid[i].position.y != road_grid[j].position.y) or (road_grid[i].position.x != road_grid[j].position.x and road_grid[i].position.y == road_grid[j].position.y): + if (road_grid[i].position.x == road_grid[j].position.x and road_grid[i].position.y != road_grid[ + j].position.y) or ( + road_grid[i].position.x != road_grid[j].position.x and road_grid[i].position.y == road_grid[ + j].position.y): point_1 = transpose_form_heightmap( './world_maker/data/heightmap.png', (road_grid[i].position.x, road_grid[i].position.y), origin) point_2 = transpose_form_heightmap( @@ -88,7 +104,7 @@ def set_roads(skeleton: Skeleton, origin): # Parsing print("[Roads] Start parsing...") for i in range(len(skeleton.lines)): - print(f"[Roads] Parsing skeleton {i+1}/{len(skeleton.lines)}.") + print(f"[Roads] Parsing skeleton {i + 1}/{len(skeleton.lines)}.") for j in range(len(skeleton.lines[i])): xyz = transpose_form_heightmap('./world_maker/data/heightmap.png', skeleton.coordinates[skeleton.lines[i][j]], origin) @@ -97,17 +113,17 @@ def set_roads(skeleton: Skeleton, origin): print("[Roads] Start simplification...") # Simplification for i in range(len(skeleton.lines)): - print(f"[Roads] Simplify skelton {i+1}/{len(skeleton.lines)}") - skeleton.lines[i] = simplify_coordinates(skeleton.lines[i], 40) + print(f"[Roads] Simplify skelton {i + 1}/{len(skeleton.lines)}") + skeleton.lines[i] = simplify_coordinates(skeleton.lines[i], 10) print("[Roads] Start generation...") for i in range(len(skeleton.lines)): - print(f"[Roads] Generating roads {i+1}/{len(skeleton.lines)}.") + print(f"[Roads] Generating roads {i + 1}/{len(skeleton.lines)}.") if len(skeleton.lines[i]) >= 4: Road(Point3D.from_arrays(skeleton.lines[i]), 9) else: print( - f"[Roads] Ignore roads {i+1} with {len(skeleton.lines[i])} coordinates between {skeleton.lines[i][1]} and {skeleton.lines[i][-1]}.") + f"[Roads] Ignore roads {i + 1} with {len(skeleton.lines[i])} coordinates between {skeleton.lines[i][1]} and {skeleton.lines[i][-1]}.") if __name__ == '__main__': diff --git a/world_maker/City.py b/world_maker/City.py index e7d0fb4..8148f62 100644 --- a/world_maker/City.py +++ b/world_maker/City.py @@ -170,14 +170,15 @@ class City: 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) + mountain = detect_mountain() + for mountain_coo in mountain: + self.add_district(mountain_coo, "mountain") + print("[City] Mountain district added.") + remove_circle_data(array, (mountain_coo.x, mountain_coo.y)) 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) + size_x, size_y = len(array[0]), len(array) + while area > size_x * size_y * 0.1: + x, y = randint(0, size_x - 1), randint(0, size_y - 1) if array[y][x]: self.add_district(Position(x, y)) remove_circle_data(array, (x, y)) diff --git a/world_maker/District.py b/world_maker/District.py index 6de31ac..61080c9 100644 --- a/world_maker/District.py +++ b/world_maker/District.py @@ -135,7 +135,6 @@ class District: self.roads = [self.roads_expend[0]] while len(self.roads_expend) > 0: road = self.roads_expend.pop(0) - print(road.position) for id_width in [-1, 1]: if road.id_width + id_width not in width: width[road.id_width + id_width] = width[road.id_width] + randint(random_range[0], diff --git a/world_maker/data_analysis.py b/world_maker/data_analysis.py index 317b081..477f085 100644 --- a/world_maker/data_analysis.py +++ b/world_maker/data_analysis.py @@ -3,8 +3,8 @@ from PIL import Image, ImageFilter import numpy as np from scipy import ndimage from world_maker.Skeleton import Skeleton -from typing import Union -from random import randint +from world_maker.Position import Position +from random import randint, choice import cv2 @@ -18,13 +18,13 @@ def get_data(world: World): return heightmap, watermap, treemap -def handle_import_image(image: Union[str, Image]) -> Image: +def handle_import_image(image: str | Image.Image) -> Image.Image: if isinstance(image, str): return Image.open(image) return image -def filter_negative(image: Union[str, Image]) -> Image: +def filter_negative(image: str | Image.Image) -> Image.Image: """ Invert the colors of an image. @@ -35,7 +35,7 @@ def filter_negative(image: Union[str, Image]) -> Image: return Image.fromarray(np.invert(np.array(image))) -def filter_sobel(image: Union[str, Image]) -> Image: +def filter_sobel(image: str | Image.Image) -> Image.Image: """ Edge detection algorithms from an image. @@ -68,29 +68,29 @@ def filter_sobel(image: Union[str, Image]) -> Image: for i in range(1, h - 1): for j in range(1, w - 1): horizontalGrad = ( - (horizontal[0, 0] * gray_img[i - 1, j - 1]) - + (horizontal[0, 1] * gray_img[i - 1, j]) - + (horizontal[0, 2] * gray_img[i - 1, j + 1]) - + (horizontal[1, 0] * gray_img[i, j - 1]) - + (horizontal[1, 1] * gray_img[i, j]) - + (horizontal[1, 2] * gray_img[i, j + 1]) - + (horizontal[2, 0] * gray_img[i + 1, j - 1]) - + (horizontal[2, 1] * gray_img[i + 1, j]) - + (horizontal[2, 2] * gray_img[i + 1, j + 1]) + (horizontal[0, 0] * gray_img[i - 1, j - 1]) + + (horizontal[0, 1] * gray_img[i - 1, j]) + + (horizontal[0, 2] * gray_img[i - 1, j + 1]) + + (horizontal[1, 0] * gray_img[i, j - 1]) + + (horizontal[1, 1] * gray_img[i, j]) + + (horizontal[1, 2] * gray_img[i, j + 1]) + + (horizontal[2, 0] * gray_img[i + 1, j - 1]) + + (horizontal[2, 1] * gray_img[i + 1, j]) + + (horizontal[2, 2] * gray_img[i + 1, j + 1]) ) newhorizontalImage[i - 1, j - 1] = abs(horizontalGrad) verticalGrad = ( - (vertical[0, 0] * gray_img[i - 1, j - 1]) - + (vertical[0, 1] * gray_img[i - 1, j]) - + (vertical[0, 2] * gray_img[i - 1, j + 1]) - + (vertical[1, 0] * gray_img[i, j - 1]) - + (vertical[1, 1] * gray_img[i, j]) - + (vertical[1, 2] * gray_img[i, j + 1]) - + (vertical[2, 0] * gray_img[i + 1, j - 1]) - + (vertical[2, 1] * gray_img[i + 1, j]) - + (vertical[2, 2] * gray_img[i + 1, j + 1]) + (vertical[0, 0] * gray_img[i - 1, j - 1]) + + (vertical[0, 1] * gray_img[i - 1, j]) + + (vertical[0, 2] * gray_img[i - 1, j + 1]) + + (vertical[1, 0] * gray_img[i, j - 1]) + + (vertical[1, 1] * gray_img[i, j]) + + (vertical[1, 2] * gray_img[i, j + 1]) + + (vertical[2, 0] * gray_img[i + 1, j - 1]) + + (vertical[2, 1] * gray_img[i + 1, j]) + + (vertical[2, 2] * gray_img[i + 1, j + 1]) ) newverticalImage[i - 1, j - 1] = abs(verticalGrad) @@ -105,7 +105,7 @@ def filter_sobel(image: Union[str, Image]) -> Image: return image -def filter_smooth_theshold(image: Union[str, Image], radius: int = 3): +def filter_smooth_theshold(image: str | Image.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. @@ -135,15 +135,14 @@ def filter_smooth_theshold(image: Union[str, Image], radius: int = 3): return Image.fromarray(bool_array) -def filter_smooth(image: Union[str, Image], radius: int = 3): - +def filter_smooth(image: str | Image.Image, radius: int = 3): image = handle_import_image(image) image = image.convert('L') image = image.filter(ImageFilter.GaussianBlur(radius)) return image -def subtract_map(image: Union[str, Image], substractImage: Union[str, Image]) -> Image: +def subtract_map(image: str | Image.Image, substractImage: str | Image.Image) -> Image.Image: image = handle_import_image(image) substractImage = handle_import_image(substractImage).convert('L') @@ -156,7 +155,7 @@ def subtract_map(image: Union[str, Image], substractImage: Union[str, Image]) -> return Image.fromarray(array_heightmap) -def overide_map(base: Image, top: Image) -> Image: +def overide_map(base: Image, top: Image) -> Image.Image: base = handle_import_image(base).convert('L') top = handle_import_image(top).convert('L') @@ -180,7 +179,7 @@ def overide_map(base: Image, top: Image) -> Image: return result_image -def group_map(image1: Union[str, Image], image2: Union[str, Image]) -> Image: +def group_map(image1: str | Image.Image, image2: str | Image.Image) -> Image.Image: image1 = handle_import_image(image1).convert('L') image2 = handle_import_image(image2).convert('L') @@ -200,7 +199,7 @@ def filter_smooth_array(array: np.ndarray, radius: int = 3) -> np.ndarray: return array -def filter_remove_details(image: Union[str, Image], n: int = 20) -> Image: +def filter_remove_details(image: str | Image.Image, n: int = 20) -> Image.Image: image = handle_import_image(image) array = np.array(image) for _ in range(n): @@ -212,7 +211,7 @@ def filter_remove_details(image: Union[str, Image], n: int = 20) -> Image: return image -def highway_map() -> Image: +def highway_map() -> Image.Image: print("[Data Analysis] Generating highway map...") smooth_sobel = filter_smooth_theshold("./world_maker/data/sobelmap.png", 1) negative_smooth_sobel = filter_negative(smooth_sobel) @@ -245,7 +244,7 @@ def create_volume(surface: np.ndarray, heightmap: np.ndarray, make_it_flat: bool return volume -def convert_2D_to_3D(image: Union[str, Image], make_it_flat: bool = False) -> np.ndarray: +def convert_2D_to_3D(image: str | Image.Image, make_it_flat: bool = False) -> np.ndarray: image = handle_import_image(image) heightmap = Image.open( './world_maker/data/heightmap_smooth.png').convert('L') @@ -255,7 +254,7 @@ def convert_2D_to_3D(image: Union[str, Image], make_it_flat: bool = False) -> np return volume -def skeleton_highway_map(image: Union[str, Image] = './world_maker/data/highwaymap.png') -> Skeleton: +def skeleton_highway_map(image: str | Image.Image = './world_maker/data/highwaymap.png') -> Skeleton: image_array = convert_2D_to_3D(image, True) skeleton = Skeleton(image_array) skeleton.parse_graph(True) @@ -265,7 +264,7 @@ def skeleton_highway_map(image: Union[str, Image] = './world_maker/data/highwaym return skeleton -def skeleton_mountain_map(image: Union[str, Image] = './world_maker/data/mountain_map.png') -> Skeleton: +def skeleton_mountain_map(image: str | Image.Image = './world_maker/data/mountain_map.png') -> Skeleton: image_array = convert_2D_to_3D(image, True) skeleton = Skeleton(image_array) skeleton.parse_graph() @@ -275,7 +274,7 @@ def skeleton_mountain_map(image: Union[str, Image] = './world_maker/data/mountai return skeleton -def smooth_sobel_water() -> Image: +def smooth_sobel_water() -> Image.Image: watermap = handle_import_image("./world_maker/data/watermap.png") watermap = filter_negative( filter_remove_details(filter_negative(watermap), 5)) @@ -287,32 +286,104 @@ def smooth_sobel_water() -> Image: 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) +def mountain_map_expend(mountain_map: list[list[int]], starting_point: tuple[int, int], value: int): + explore_points = [starting_point] + while len(explore_points) > 0: + x, y = explore_points.pop(0) + mountain_map[y][x] = value + for i in range(-1, 2): + for j in range(-1, 2): + if (0 <= x + i < len(mountain_map[0]) and 0 <= y + j < len(mountain_map) and + mountain_map[y + j][x + i] == 0): + if (x + i, y + j) not in explore_points: + explore_points.append((x + i, y + j)) - 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() +def set_values_of_building_mountain(mountain_map: list[list[int]], area_mountain: list[int], + building_map: str | Image.Image = "./world_maker/data/smooth_sobel_watermap.png"): + building_map = handle_import_image(building_map).convert('L') + for y in range(building_map.size[1]): + for x in range(building_map.size[0]): + if building_map.getpixel((x, y)) > 144: + if mountain_map[y][x] != -1: + area_mountain[mountain_map[y][x] - 1] += 1 - 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"]) +def get_index_of_biggest_area_mountain(area_mountain: list[int], exception: list[int]) -> int: + max_value = -1 + index = -1 + for i in range(len(area_mountain)): + if i not in exception and area_mountain[i] > max_value: + max_value = area_mountain[i] + index = i + return index - print(f"[Data Analysis] The center of the mountain is at ({cX}, {cY})") - return (cX, cY) +def get_random_point_in_area_mountain(mountain_map: list[list[int]], index: int) -> Position | None: + points = [] + for y in range(len(mountain_map)): + for x in range(len(mountain_map[0])): + if mountain_map[y][x] == index + 1: + points.append(Position(x, y)) + if not points: + return None + return choice(points) + +def get_center_of_area_mountain(mountain_map: list[list[int]], index: int) -> Position: + sum_x = 0 + sum_y = 0 + count = 0 + for y in range(len(mountain_map)): + for x in range(len(mountain_map[0])): + if mountain_map[y][x] == index + 1: + sum_x += x + sum_y += y + count += 1 + center = Position(sum_x // count, sum_y // count) + if mountain_map[center.y][center.x] != index + 1: + return get_random_point_in_area_mountain(mountain_map, index) + return center + + +def detect_mountain(number_of_mountain: int = 2, height_threshold: int = 10, + image_heightmap: str | Image.Image = './world_maker/data/heightmap.png') -> list[Position]: + print("[Data Analysis] Detecting mountains...") + image_heightmap = handle_import_image(image_heightmap).convert('L') + + avg_height = 0 + for y in range(image_heightmap.size[1]): + for x in range(image_heightmap.size[0]): + avg_height += image_heightmap.getpixel((x, y)) + avg_height = int(avg_height / (image_heightmap.size[0] * image_heightmap.size[1])) + print("[Data Analysis] Average height:", avg_height) + + mountain_map = [[-1 if image_heightmap.getpixel((x, y)) < (avg_height + height_threshold) else 0 for x in + range(image_heightmap.size[0])] for y in + range(image_heightmap.size[1])] + + area_mountain = [] + for y in range(image_heightmap.size[1]): + for x in range(image_heightmap.size[0]): + if mountain_map[y][x] == 0: + area_mountain.append(0) + mountain_map_expend(mountain_map, (x, y), len(area_mountain)) + + if not area_mountain: + print("[Data Analysis] No mountain detected.") + return [] + + set_values_of_building_mountain(mountain_map, area_mountain) + if number_of_mountain < len(area_mountain): + index_mountain = [] + for n in range(number_of_mountain): + index_mountain.append(get_index_of_biggest_area_mountain(area_mountain, index_mountain)) + else: + index_mountain = [i for i in range(len(area_mountain))] + + position_mountain = [] + for i in range(len(index_mountain)): + position_mountain.append(get_center_of_area_mountain(mountain_map, index_mountain[i])) + + return position_mountain def rectangle_2D_to_3D(rectangle: list[tuple[tuple[int, int], tuple[int, int]]], @@ -323,18 +394,21 @@ def rectangle_2D_to_3D(rectangle: list[tuple[tuple[int, int], tuple[int, int]]], new_rectangle = [] for rect in rectangle: start, end = rect - avg_height = 0 + height = {} 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]))) + 1 + if image.getpixel((x, y)) not in height: + height[image.getpixel((x, y))] = 1 + else: + height[image.getpixel((x, y))] += 1 + max_height = max(height, key=height.get) new_rectangle.append( - ((start[0], avg_height, start[1]), (end[0], avg_height + randint(height_min, height_max), end[1]))) + ((start[0], max_height, start[1]), (end[0], max_height + randint(height_min, height_max), end[1]))) return new_rectangle -def transpose_form_heightmap(heightmap: Union[str, Image], coordinates, origin: tuple[int, int]) -> tuple[int, int, int]: +def transpose_form_heightmap(heightmap: str | Image.Image, coordinates, origin: tuple[int, int]) -> tuple[ + int, int, int]: heightmap = handle_import_image(heightmap).convert('L') xMin, zMin = origin diff --git a/world_maker/pack_rectangle.py b/world_maker/pack_rectangle.py index 5a87dbd..2e04b10 100644 --- a/world_maker/pack_rectangle.py +++ b/world_maker/pack_rectangle.py @@ -69,13 +69,12 @@ def pack_rectangles(grid, min_width: int = 10, max_width: int = 25): rectangle = generate_rectangle(min_width, max_width) if not bin.place_rectangle(rectangle): break - print(len(bin.rectangles)) return bin.rectangles def draw_rectangles(rectangles, grid, heightmap): heightmap = handle_import_image(heightmap).convert('L') - image = Image.new('L', (len(grid[0]), len(grid)), (0)) + image = Image.new('L', (len(grid[0]), len(grid)), 0) for rectangle in rectangles: start, end = rectangle height = [] @@ -88,13 +87,29 @@ def draw_rectangles(rectangles, grid, heightmap): image.putpixel((x, y), round(height_average)) return image +def area_of_rectangles(rectangles): + area = 0 + for rectangle in rectangles: + start, end = rectangle + area += abs((end[0] - start[0]) * (end[1] - start[1])) + return area -def generate_building(image: Union[str, Image], heightmap: Union[str, Image], output: str = './world_maker/data/building.png', min_width: int = 10, max_width: int = 25): + + +def generate_building(image: str | Image.Image, heightmap: str | Image.Image, output: str = './world_maker/data/building.png', + number_of_try: int = 3, min_width: int = 10, max_width: int = 25): + print("[Building] Start generating building position...") image = handle_import_image(image).convert('L') - grid = np.array(image) - rectangles = pack_rectangles(grid, min_width, max_width) - draw_rectangles(rectangles, grid, heightmap).save( - output) - return rectangles + rectangles_output = [] + for n in range(number_of_try): + print("[Building] Try", n+1) + grid = np.array(image) + rectangles = pack_rectangles(grid, min_width, max_width) + print("[Building] Number of building:", len(rectangles)) + print("[Building] Area of building:", area_of_rectangles(rectangles)) + if area_of_rectangles(rectangles) > area_of_rectangles(rectangles_output): + rectangles_output = rectangles + draw_rectangles(rectangles_output, grid, heightmap).save(output) + return rectangles_output diff --git a/world_maker/world_maker.py b/world_maker/world_maker.py index f010f71..2e5a7ca 100644 --- a/world_maker/world_maker.py +++ b/world_maker/world_maker.py @@ -17,10 +17,12 @@ def world_maker(): smooth_sobel_water_map = smooth_sobel_water() skeleton_highway = skeleton_highway_map(highway_map()) + city = City() city.generate_district() city.loop_expend_district() city.district_draw_map() + road_grid = city.district_generate_road() image_mountain_map = city.get_district_mountain_map() road = city.draw_roads(4)