diff --git a/.gitignore b/.gitignore index 68bc17f..2dc53ca 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ diff --git a/metro/Metro_Line.py b/metro/Metro_Line.py new file mode 100644 index 0000000..6068d90 --- /dev/null +++ b/metro/Metro_Line.py @@ -0,0 +1,92 @@ +from math import sqrt, atan2 + + +class Position: + def __init__(self, x: int = 0, y: int = 0): + self.x = x + self.y = y + + def __add__(self, other: "Position") -> "Position": + return Position(self.x + other.x, self.y + other.y) + + def __sub__(self, other: "Position") -> "Position": + return Position(self.x - other.x, self.y - other.y) + + def __mul__(self, other: float) -> "Position": + return Position(int(self.x * other), int(self.y * other)) + + def __truediv__(self, other: float) -> "Position": + return Position(int(self.x / other), int(self.y / other)) + + def __str__(self): + return f"({self.x}, {self.y})" + + def __eq__(self, other: "Position"): + return self.x == other.x and self.y == other.y + + def distance_to(self, other: "Position") -> float: + return sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2) + + def norm(self) -> float: + return sqrt(self.x ** 2 + self.y ** 2) + + def angle_to(self, other: "Position") -> float: + return atan2(self.y - other.y, other.x - self.x) + + +class Station: + """ + This class represents the position and link of a metro station. + """ + + def __init__(self, pos: Position, orientation: float, name: str = "Station"): + """ + Constructor of Station. + + :param pos: Position x and y of the station + :param orientation: The orientation of the station in radian (The angle is where the station is facing next) + :param name: The name of the station + """ + self.name = name + self.orientation = orientation + self.pos = pos + self.last_station = None + self.next_station = None + + def distance_to(self, station: "Station") -> float: + """ + Calculate the distance between two stations. + + :param station: The station to calculate the distance to + :return: The distance between two stations + """ + return self.pos.distance_to(station.pos) + + +class Metro_Line: + """ + This class represents the metro line. + """ + + def __init__(self, name: str = "Metro line A"): + """ + Constructor of Metro_Line. + + :param name: The name of the metro line + """ + self.name = name + self.stations = [] + + def add_station(self, station: Station): + """ + Add a station to the metro map. + + :param station: The station to be added + """ + self.stations.append(station) + if len(self.stations) > 1: + self.stations[-2].next_station = station + station.last_station = self.stations[-2] + + +__all__ = ["Metro_Line", "Station", "Position"] diff --git a/metro/metro_line_map.py b/metro/metro_line_map.py new file mode 100644 index 0000000..42c3892 --- /dev/null +++ b/metro/metro_line_map.py @@ -0,0 +1,404 @@ +from Metro_Line import * +from math import pi, cos, sin, sqrt, atan2, inf +from pygame import Surface +import pygame + +from metro.Metro_Line import Position + + +def cubic_bezier(time: float, join1: Position, control_point1: Position, control_point2: Position, + join2: Position) -> Position: + """ + Calculate the position of a point on a cubic Bézier curve at a given time + + Formula used : B(t) = (1-t)^3 * P0 + 3(1-t)^2 * t * P1 + 3(1-t) * t^2 * P2 + t^3 * P3 + + :param time: The time at which to calculate the position + :param join1: The first join point of the curve + :param control_point1: The first control point of the curve + :param control_point2: The second control point of the curve + :param join2: The second join point of the curve + :return: The position of the point on the curve at the given time + """ + return (join1 * ((1 - time) ** 3) + + control_point1 * 3 * ((1 - time) ** 2) * time + + control_point2 * 3 * (1 - time) * (time ** 2) + + join2 * (time ** 3)) + + +def cubic_bezier_derivative(time: float, join1: Position, control_point1: Position, control_point2: Position, + join2: Position) -> Position: + """ + Calculate the first derivative of a cubic Bézier curve at a given time + + Formula used : B'(t) = 3(1-t)^2 * (P1 - P0) + 6(1-t) * t * (P2 - P1) + 3t^2 * (P3 - P2) + + :param time: The time at which to calculate the derivative + :param join1: The first join point of the curve + :param control_point1: The first control point of the curve + :param control_point2: The second control point of the curve + :param join2: The second join point of the curve + :return: The derivative of the curve at the given time + """ + return ((control_point1 - join1) * 3 * ((1 - time) ** 2) + + (control_point2 - control_point1) * 6 * (1 - time) * time + + (join2 - control_point2) * 3 * (time ** 2)) + + +def cubic_bezier_second_derivative(time: float, join1: Position, control_point1: Position, control_point2: Position, + join2: Position) -> Position: + """ + Calculate the second derivative of a cubic Bézier curve at a given time + + Formula used : B''(t) = 6(1-t) * (P2 - 2P1 + P0) + 6t * (P3 - 2P2 + P1) + + :param time: The time at which to calculate the second derivative + :param join1: The first join point of the curve + :param control_point1: The first control point of the curve + :param control_point2: The second control point of the curve + :param join2: The second join point of the curve + :return: The second derivative of the curve at the given time + """ + return ((control_point2 - control_point1 * 2 + join1) * 6 * (1 - time) + + (join2 - control_point2 * 2 + control_point1) * 6 * time) + + +def bezier_curve(control_points, num_points) -> tuple[list[Position], list[Position], list[Position]]: + """ + Generate a Bézier curve from a list of control points + + :param control_points: The control points of the curve + :param num_points: The number of points to generate + :return: A tuple containing the points of the curve, the derivative of the curve, + and the second derivative of the curve + """ + points = [] + derivative = [] + second_derivative = [] + for t in range(num_points + 1): + points.append(cubic_bezier(t / num_points, *control_points)) + derivative.append(cubic_bezier_derivative(t / num_points, *control_points)) + second_derivative.append(cubic_bezier_second_derivative(t / num_points, *control_points)) + return points, derivative, second_derivative + + +def osculating_circle(points: list[Position], derivative: list[Position], second_derivative: list[Position]) \ + -> list[tuple[int, Position]]: + """ + Calculate the osculating circle at each point of a curve + An osculating circle is the circle that best approximates the curve at a given point + + Source : https://en.wikipedia.org/wiki/Osculating_circle + + :param points: The points of the curve + :param derivative: The derivative of the curve + :param second_derivative: The second derivative of the curve + :return: A list of tuples, each containing the radius and center of each osculating circle + """ + circle = [] + for i in range(len(points)): + curvature = (abs(derivative[i].x * second_derivative[i].y - derivative[i].y * second_derivative[i].x) + / ((derivative[i].x ** 2 + derivative[i].y ** 2) ** 1.5)) + if curvature != 0: + radius = 1 / curvature + normal = derivative[i].norm() + cross_product = derivative[i].x * second_derivative[i].y - derivative[i].y * second_derivative[i].x + if cross_product > 0: + center = points[i] + Position(-derivative[i].y * radius / normal, derivative[i].x * radius / normal) + else: + center = points[i] + Position(derivative[i].y * radius / normal, -derivative[i].x * radius / normal) + circle.append((int(radius), center)) + return circle + + +def merge_similar_circles(circles: list[tuple[int, Position]], radius_threshold: float, center_threshold: float) \ + -> list[tuple[int, Position]]: + """ + Merge similar osculating circles + + :param circles: The osculating circles to merge + :param radius_threshold: The maximum difference in radius for two circles to be considered similar + :param center_threshold: The maximum distance between the centers of two circles to be considered similar + :return: The merged osculating circles + """ + merged_circles = [] + i = 0 + while i < len(circles) - 1: + radius1, center1 = circles[i] + radius2, center2 = circles[i + 1] + if abs(radius1 - radius2) <= radius_threshold and center1.distance_to(center2) <= center_threshold: + merged_radius = (radius1 + radius2) // 2 + merged_center = Position((center1.x + center2.x) // 2, (center1.y + center2.y) // 2) + merged_circles.append((merged_radius, merged_center)) + i += 2 + else: + merged_circles.append(circles[i]) + i += 1 + if i < len(circles): + merged_circles.append(circles[i]) + + if len(merged_circles) == len(circles): + return merged_circles + else: + return merge_similar_circles(merged_circles, radius_threshold, center_threshold) + + +def circle_intersection(circle1: tuple[int, Position], circle2: tuple[int, Position]) -> list[Position]: + distance = circle1[1].distance_to(circle2[1]) + + if (distance > circle1[0] + circle2[0] or distance < abs(circle1[0] - circle2[0]) + or (distance == 0 and circle1[0] == circle2[0])): + return [] + + distance_line_circle = (circle1[0] ** 2 - circle2[0] ** 2 + distance ** 2) / (2 * distance) + distance_line_intersec_point = sqrt(circle1[0] ** 2 - distance_line_circle ** 2) + p = circle1[1] + (circle2[1] - circle1[1]) * distance_line_circle / distance + + return [Position(int(p.x + distance_line_intersec_point * (circle2[1].y - circle1[1].y) / distance), + int(p.y - distance_line_intersec_point * (circle2[1].x - circle1[1].x) / distance)), + Position(int(p.x - distance_line_intersec_point * (circle2[1].y - circle1[1].y) / distance), + int(p.y + distance_line_intersec_point * (circle2[1].x - circle1[1].x) / distance))] + + +def closest_to_curve(points: list[Position], curve_points: list[Position]) -> Position: + closest_point = Position() + distance = inf + for point in points: + for curve_point in curve_points: + distance_point_curve = point.distance_to(curve_point) + if distance_point_curve < distance: + distance = distance_point_curve + closest_point = point + return closest_point + + +def midpoint_circle_segment(circle: tuple[int, Position], start_point: Position, end_point: Position, curve_points: list[Position]) -> list[ + Position]: + points = [] + + start_angle = circle[1].angle_to(start_point) + end_angle = circle[1].angle_to(end_point) + + if start_angle < 0: + start_angle += 2 * pi + if end_angle < 0: + end_angle += 2 * pi + if start_angle > end_angle: + start_angle, end_angle = end_angle, start_angle + + middle_angle = (start_angle+end_angle)/2 + middle_point = circle[1] + Position(int(circle[0]*cos(middle_angle)), -int(circle[0]*sin(middle_angle))) + is_outside_point = closest_to_curve([middle_point, circle[1]], curve_points) == middle_point + + x0, y0 = circle[1].x, circle[1].y + x = circle[0] + y = 0 + err = 0 + + while x >= y: + for (x1, y1) in [(x0 + x, y0 + y), (x0 + y, y0 + x), (x0 - y, y0 + x), (x0 - x, y0 + y), + (x0 - x, y0 - y), (x0 - y, y0 - x), (x0 + y, y0 - x), (x0 + x, y0 - y)]: + angle = atan2(y0 - y1, x1 - x0) + if angle < 0: + angle += 2*pi + if is_outside_point: + if start_angle <= angle <= end_angle: + points.append(Position(int(x1), int(y1))) + else: + if angle <= start_angle or end_angle <= angle: + points.append(Position(int(x1), int(y1))) + + if err <= 0: + y += 1 + err += 2 * y + 1 + if err > 0: + x -= 1 + err -= 2 * x + 1 + + return points + + +def calculate_control_points(station, next_station, curve_factor) -> tuple[Position, Position]: + """ + Calculate the control points for a Bézier curve between two stations + + :param station: The first station + :param next_station: The second station + :param curve_factor: The factor to multiply the distance between stations to create the control points + :return: A tuple containing the control points for the curve + """ + distance = station.distance_to(next_station) + + control_point_pos = station.pos + Position(cos(station.orientation) * distance * curve_factor, + -sin(station.orientation) * distance * curve_factor) + + control_point_next_pos = next_station.pos + Position( + cos(next_station.orientation + pi) * distance * curve_factor, + - sin(next_station.orientation + pi) * distance * curve_factor) + + return control_point_pos, control_point_next_pos + + +def metro_line_osculating_circles(metro: Metro_Line, curve_factor: float = 0.5, num_points_factor: float = 1 / 20) -> ( + tuple)[list[tuple[int, Position]], list[Position]]: + """ + Calculate the osculating circles of a metro line + + :param metro: The metro line to calculate the osculating circles of + :param curve_factor: How much the control points should be offset from the stations + :param num_points_factor: How many points to generate for each segment of the curve + :return: A tuple containing the osculating circles and the points of the metro line curve + """ + print(f"[METRO LINE] Calculating osculating circles of the metro line {metro.name}") + circles = [] + points_list = [] + for i in range(len(metro.stations) - 1): + print(f"[METRO LINE] Calculating between {metro.stations[i].name} and {metro.stations[i].next_station.name}") + station = metro.stations[i] + + distance = station.distance_to(station.next_station) + + control_point_pos, control_point_next_pos = calculate_control_points(station, station.next_station, + curve_factor) + + points, derivatives, second_derivatives = bezier_curve( + [station.pos, control_point_pos, control_point_next_pos, station.next_station.pos], + int(distance * num_points_factor)) + + osculating_circles = osculating_circle(points, derivatives, second_derivatives) + merged_circles = merge_similar_circles(osculating_circles, 50, 50) + print( + f"[METRO LINE] {len(osculating_circles) - len(merged_circles)} out of {len(osculating_circles)} circles deleted !") + circles.extend(merged_circles) + points_list.extend(points) + print(f"[METRO LINE] Osculating circles done") + return circles, points_list + + +# --- DRAW PART --- + +def draw_osculating_circle(circle: list[tuple[int, Position]], surface: Surface): + """ + :param circle: The osculating circles to draw + :param surface: The surface on which to draw the circles + """ + for radius, center in circle: + pygame.draw.circle(surface, (255, 0, 0), (center.x, center.y), int(radius), 1) + pygame.draw.circle(surface, (0, 0, 255), (center.x, center.y), 10) + + +def draw_station(station: Station, surface: Surface): + """ + :param station: The station to draw + :param surface: The surface on which to draw the station + """ + pygame.draw.circle(surface, (255, 255, 255), (station.pos.x, station.pos.y), 10) + + +def draw_points(points: list[Position], surface): + """ + :param points: The points to draw + :param surface: The surface on which to draw the points + """ + for point in points: + pygame.draw.circle(surface, (40, 255, 40), (point.x, point.y), 5) + + +def draw_point(point: Position, surface): + pygame.draw.circle(surface, (40, 255, 40), (point.x, point.y), 5) + + +def draw_pixels(points: list[Position], surface): + for point in points: + surface.set_at((point.x, point.y), (0, 255, 255)) + + +def draw_metro_line(metro: Metro_Line, surface: Surface, show_points: bool = True): + """ + :param metro: The metro line to draw + :param surface: The surface on which to draw the metro line + :param show_points: Whether to show the points of the curve + """ + for i in range(len(metro.stations) - 1): + station = metro.stations[i] + draw_station(station, surface) + draw_station(station.next_station, surface) + + circles, points = metro_line_osculating_circles(metro) + draw_osculating_circle(circles, surface) + for i in range(1, len(circles) - 1): + intersect_point_circle_before = closest_to_curve(circle_intersection(circles[i - 1], circles[i]), points) + intersect_point_circle_after = closest_to_curve(circle_intersection(circles[i], circles[i + 1]), points) + if intersect_point_circle_before == Position(): + continue + intersect_point_circle_before = circles[i - 1][1] + else: + draw_point(intersect_point_circle_before, surface) + + if intersect_point_circle_after == Position(): + continue + intersect_point_circle_after = circles[i + 1][1] + else: + draw_point(intersect_point_circle_after, surface) + + points_midpoint = midpoint_circle_segment(circles[i], intersect_point_circle_before, + intersect_point_circle_after, points) + draw_pixels(points_midpoint, surface) + + if len(points) != 0: + intersect_point_circle_before = points[0] + intersect_point_circle_after = closest_to_curve(circle_intersection(circles[0], circles[1]), points) + points_midpoint = midpoint_circle_segment(circles[0], intersect_point_circle_before, + intersect_point_circle_after, points) + draw_pixels(points_midpoint, surface) + + intersect_point_circle_before = points[-1] + intersect_point_circle_after = closest_to_curve(circle_intersection(circles[-1], circles[-2]), points) + points_midpoint = midpoint_circle_segment(circles[-1], intersect_point_circle_before, + intersect_point_circle_after, points) + draw_pixels(points_midpoint, surface) + + +def interface(): + """ + Interface for creating a metro line + + Control : + + - Up arrow ↑ Create a station facing north + + - Down arrow ↓ Create a station facing south + + - Left arrow ← Create a station facing west + + - Right arrow → Create a station facing east + """ + metro = Metro_Line('A') + surface = pygame.display.set_mode((1000, 1000)) + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + return + + if event.type == pygame.KEYDOWN: + angle = 0 + if event.key == pygame.K_UP: + angle = pi / 2 + elif event.key == pygame.K_DOWN: + angle = -pi / 2 + elif event.key == pygame.K_LEFT: + angle = pi + x, y = pygame.mouse.get_pos() + metro.add_station(Station(Position(x, y), angle, str(len(metro.stations)))) + draw_metro_line(metro, surface) + + pygame.display.flip() + + +def main(): + interface() + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index 346c05f..c763a55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,8 @@ gdpc==7.1.0 -matplotlib==3.8.2 numpy==1.26.4 -scipy==1.13.0 +Pillow==10.3.0 +pygame==2.5.2 +scipy==1.13.1 +skan==0.11.1 +skimage==0.0 pyyaml==6.0.1 diff --git a/world_maker/Block.py b/world_maker/Block.py new file mode 100644 index 0000000..33733ee --- /dev/null +++ b/world_maker/Block.py @@ -0,0 +1,34 @@ +from gdpc import Editor, Block, geometry +from gdpc.lookup import * +import numpy as np +import math + +SOLID_NATURAL_BLOCKS = SOILS | STONES | ORES | LIQUIDS + +class Block: + def __init__(self, coordinates:tuple, name:str): + self.coordinates = coordinates + self.name = name + self.neighbors = [] + self.surface = None + + + def addNeighbors(self, neighbors:list[Block]): + for neighbor in neighbors: + self.neighbors.append(neighbor) + + def isSurface(self): + if self.surface == None: + if str(self.name) in SOLID_NATURAL_BLOCKS: + for neighbor in self.neighbors: + if str(neighbor.name) not in SOLID_NATURAL_BLOCKS: + self.surface = True + return True + if len(self.neighbors) != 0: + self.surface = False + return False + else: + self.surface = False + return False + else: + return self.surface \ No newline at end of file diff --git a/world_maker/City.py b/world_maker/City.py new file mode 100644 index 0000000..113e214 --- /dev/null +++ b/world_maker/City.py @@ -0,0 +1,126 @@ +from District import District +from Position import Position +from PIL import Image +import random + + +class City: + """ + Attributes: + districts (list): The list of districts in the city. + map_data (list): The 2D list representing the map of the city. + height_map (list): The 2D list representing the height map of the city. + """ + + def __init__(self): + """ + The constructor for the City class. + """ + self.districts = [] + self.map_data = [] + self.height_map = [] + self.init_maps() + + def init_maps(self): + """ + 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') + 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)] + watermap.close() + heightmap.close() + + def add_district(self, center: Position, district_type: str = ""): + """ + Add a new district to the city. + + :param district_type: + :param center: The center position of the new district. + """ + self.districts.append(District(len(self.districts) + 1, center, district_type)) + self.map_data[center.y][center.x] = len(self.districts) + + def is_expend_finished(self): + """ + Check if the expansion of all districts in the city is finished. + + :return: True if the expansion is finished, False otherwise. + """ + for district in self.districts: + if len(district.area_expend_from_point) > 0: + return False + return True + + def choose_expend_point(self, point: Position, index_district: int): + """ + Choose a point to expand a district based on the distance between the center of the district and the point itself. + + :param point: The point to be expanded. + :param index_district: The index of the district to be expanded. + """ + min_distance = point.distance_to(self.districts[index_district].center_expend) + index_district_chosen = index_district + 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 = index + else: + 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) + self.map_data[point.y][point.x] = index_district_chosen + 1 + + def update_expend_district(self): + """ + Update the expansion points of all districts in the city. + """ + + for district in self.districts: + if len(district.area_expend_from_point) > 0: + district.update_expend_points(district.area_expend_from_point[0], self.map_data, self.height_map) + for district in self.districts: + for point in district.area_expend: + self.choose_expend_point(point, district.tile_id - 1) + + def loop_expend_district(self): + """ + Loop the expansion of all districts in the city until all districts are fully expanded. + """ + print("[City] Start expanding districts...") + while not self.is_expend_finished(): + self.update_expend_district() + print("[City] Finished expanding districts.") + + def district_draw_map(self): + """ + Draw the map of the city with different colors for each district. + """ + 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)) + for id_district in range(1, len(self.districts) + 1)} + + for y in range(height): + for x in range(width): + if self.map_data[y][x] <= 0: + img.putpixel((x, y), (0, 0, 0)) + else: + img.putpixel((x, y), colors[self.map_data[y][x]]) + + img.save('./data/district.png') + print("[City] District map created.") + + +if __name__ == '__main__': + city = City() + for i in range(10): + city.add_district(Position(random.randint(0, 800), random.randint(0, 800))) + city.loop_expend_district() + city.district_draw_map() diff --git a/world_maker/District.py b/world_maker/District.py new file mode 100644 index 0000000..e0e3f9a --- /dev/null +++ b/world_maker/District.py @@ -0,0 +1,58 @@ +from Position import Position + +class District: + """ + The CustomDistrict class represents a district that can be expanded. + + Attributes: + center_expend (Position): The center position from which the district expands. + area (list): The list of positions that are part of the 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, 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 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 = district_type + self.center_expend = center + self.area = [center] + self.area_expend_from_point = [center] + self.area_expend = [] + + def verify_point(self, point: Position, point_new: Position, map_data: list[list[int]], height_map: list[list[int]]): + """ + Verify if a new point can be added to a district extend area list. + + :param point: The current point. + :param point_new: The new point to be verified. + :param map_data: The 2D list representing the map. + :param height_map: The 2D list representing the height map. + :return: True if the new point can be added, False otherwise. + """ + 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 + abs(height_map[point_new.y][point_new.x] - height_map[point.y][point.x]) < 2)) + + def update_expend_points(self, point: Position, map_data: list[list[int]], height_map: list[list[int]]): + """ + Update the points to which the district can expand. + + :param point: The current point. + :param map_data: The 2D list representing the map. + :param height_map: The 2D list representing the height map. + """ + for pos in [Position(1, 0), Position(-1, 0), Position(0, 1), Position(0, -1)]: + if self.verify_point(point, point + pos, map_data, height_map): + if point + pos not in self.area_expend: + self.area_expend.append(point + pos) + self.area_expend_from_point.remove(point) diff --git a/world_maker/Position.py b/world_maker/Position.py new file mode 100644 index 0000000..a488037 --- /dev/null +++ b/world_maker/Position.py @@ -0,0 +1,37 @@ +from math import sqrt, atan2 + + +class Position: + def __init__(self, x: int = 0, y: int = 0): + self.x = x + self.y = y + + def __add__(self, other: "Position") -> "Position": + return Position(self.x + other.x, self.y + other.y) + + def __sub__(self, other: "Position") -> "Position": + return Position(self.x - other.x, self.y - other.y) + + def __mul__(self, other: float) -> "Position": + return Position(int(self.x * other), int(self.y * other)) + + def __truediv__(self, other: float) -> "Position": + return Position(int(self.x / other), int(self.y / other)) + + def __str__(self): + return f"({self.x}, {self.y})" + + def __eq__(self, other: "Position"): + return self.x == other.x and self.y == other.y + + def get_tuple(self) -> tuple[int, int]: + return self.x, self.y + + def distance_to(self, other: "Position") -> float: + return sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2) + + def norm(self) -> float: + return sqrt(self.x ** 2 + self.y ** 2) + + def angle_to(self, other: "Position") -> float: + return atan2(self.y - other.y, other.x - self.x) diff --git a/world_maker/Skeleton.py b/world_maker/Skeleton.py new file mode 100644 index 0000000..6b4d1ce --- /dev/null +++ b/world_maker/Skeleton.py @@ -0,0 +1,215 @@ +import numpy as np +#import skan +from skimage.morphology import skeletonize +from skan.csr import skeleton_to_csgraph +from collections import Counter +from PIL import Image +import random + + +class Skeleton: + def __init__(self, data: np.ndarray = None): + self.lines = [] + self.intersections = [] + self.centers = [] + self.coordinates = [] + self.graph = None + if data is not None: + self.set_skeleton(data) + + def set_skeleton(self, data: np.ndarray): + print("[Skeleton] Start skeletonization...") + 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) + for i in range(len(coordinates)): + coordinates[i] = list(coordinates[i]) + # print(coordinates) + + for i in range(len(coordinates[0])): + # print((coordinates[0][i], coordinates[1][i], coordinates[2][i])) + self.coordinates.append((coordinates[0][i], coordinates[1][i], coordinates[2][i])) + print("[Skeleton] Skeletonization completed.") + + def find_next_elements(self, key: str) -> list: + """Find the very nearest elements""" + + line = [] + + values = np.array(self.graph.row) + indices = np.where(values == key)[0] + + for i in range(len(indices)): + if self.graph.row[indices[i]] == key: + line.append(self.graph.col[indices[i]]) + return line + + def find_line(self, key: str): + next_keys = self.find_next_elements(key) + + if len(next_keys) >= 3: # Intersections. + return next_keys + + 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]) + + next_keys = line[0], line[-1] + + while len(next_keys) == 2 or len(next_keys) == 1: + extremity = [] + for key in next_keys: + next_keys = self.find_next_elements(key) + + if len(next_keys) <= 2: + # Add the neighbors that is not already in the line. + for element in next_keys: + if element not in line: + extremity.append(element) + line.append(element) + + if len(next_keys) >= 3: + # Add the intersection only. + extremity.append(key) + + next_keys = [] + for key in extremity: + ends = self.find_next_elements(key) + if len(ends) == 2: + next_keys.append(key) + return line + + def parse_graph(self, parse_orphan: bool = False): + print("[Skeleton] Start parsing the graph", ("with orphans" if parse_orphan else "") + "...") + 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.find_line(key) + + # We have now all the connected points if it's an + # intersection. We need to find the line. + + if value != 1: + # It's not an endpoint. + self.centers.append(key) + self.intersections.append(line) + for i in line: + line = self.find_line(i) + + if i in line: + # The key is inside the result : it's a line. + already_inside = False + for l in self.lines: + # Verification if not already inside. + if Counter(l) == Counter(line): + already_inside = True + # print(line, "inside", lines) + + 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] + already_inside = False + for l in self.lines: + # Verification if not already inside. + if Counter(l) == Counter(line): + already_inside = True + # print(line, "inside", lines) + + 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 + + if not already_inside: + self.lines.append(line) + print("[Skeleton] Graph parsing completed.") + + def map(self) -> Image: + """ + + Generate an image to visualize 2D path of the skeleton. + + Returns: + image: 2D path of the skeleton on top of the heightmap. + """ + print("[Skeleton] Start mapping the skeleton...") + # 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])) + + heightmap = Image.open("data/heightmap.png").convert('RGB') + # roadsArea = Image.new("L", xzDistance, 0) + # width, height = heightmap.size + + # Lines + for i in range(len(self.lines)): + r, g, b = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + + for j in range(len(self.lines[i])): + z = self.coordinates[self.lines[i][j]][0] + # y = self.coordinates[self.lines[i][j]][1] + x = self.coordinates[self.lines[i][j]][2] + + heightmap.putpixel( + ( + int(z), + int(x), + ), + (r + j, g + j, b + j), + ) + + # roadsArea.putpixel( + # ( + # int(z), + # int(x), + # ), + # (255), + # ) + + # Centers + for i in range(len(self.centers)): + # 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), + # ) + + # # Intersections + # for i in range(len(self.intersections)): + # intersection = [] + # for j in range(len(self.intersections[i])): + # intersection.append(self.coordinates[self.intersections[i][j]]) + + # for i in range(len(intersection)): + # heightmap.putpixel( + # (int(self.intersections[i][2]), int(self.intersections[i][0])), + # (255, 0, 255), + # ) + print("[Skeleton] Mapping completed.") + return heightmap # , roadsArea diff --git a/world_maker/World.py b/world_maker/World.py new file mode 100644 index 0000000..c771332 --- /dev/null +++ b/world_maker/World.py @@ -0,0 +1,233 @@ +from gdpc import Editor, geometry, lookup +import numpy as np +from PIL import Image +from Block import Block + +waterBiomes = [ + "minecraft:ocean", + "minecraft:deep_ocean", + "minecraft:warm_ocean", + "minecraft:lukewarm_ocean", + "minecraft:deep_lukewarm_ocean", + "minecraft:cold_ocean", + "minecraft:deep_cold_ocean", + "minecraft:frozen_ocean", + "minecraft:deep_frozen_ocean", + "minecraft:mushroom_fieds", + "minecraft:river", + "minecraft:frozen_river", +] + +waterBlocks = [ + "minecraft:water", +] + + +class World: + def __init__(self): + + editor = Editor(buffering=True) + buildArea = editor.getBuildArea() + + self.coordinates_min = [min(buildArea.begin[i], buildArea.last[i]) for i in range(3)] + self.coordinates_max = [max(buildArea.begin[i], buildArea.last[i]) for i in range(3)] + + self.length_x = self.coordinates_max[0] - self.coordinates_min[0] + 1 + self.length_y = self.coordinates_max[1] - self.coordinates_min[1] + 1 + self.length_z = self.coordinates_max[2] - self.coordinates_min[2] + 1 + + self.volume = [[[None for _ in range(self.length_z)] for _ in range(self.length_y)] for _ in + range(self.length_x)] + + def isInVolume(self, coordinates): + if (self.coordinates_min[0] <= coordinates[0] <= self.coordinates_max[0] and + self.coordinates_min[1] <= coordinates[1] <= self.coordinates_max[1] and + self.coordinates_min[2] <= coordinates[2] <= self.coordinates_max[2]): + return True + return False + + def addBlocks(self, blocks: list[Block]): + """ + Add block or list of block to the volume. + """ + + for block in blocks: + if self.isInVolume(block.coordinates): + self.volume[block.coordinates[0] - self.coordinates_min[0]][ + block.coordinates[1] - self.coordinates_min[1]][ + block.coordinates[2] - self.coordinates_min[2]] = block + + def removeBlock(self, volumeCoordinates): + """ + Add block or list of block to the volume. + """ + + self.volume[volumeCoordinates[0]][volumeCoordinates[1]][volumeCoordinates[2]] = None + + def getBlockFromCoordinates(self, coordinates): + """ + Use already created volume to get block data. + """ + + editor = Editor(buffering=True) + if self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] == None: + self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] = Block((coordinates[0], coordinates[1], coordinates[2]), + editor.getBlock((coordinates[0], coordinates[1], + coordinates[2])).id) + + return self.volume[coordinates[0] - self.coordinates_min[0]][coordinates[1] - self.coordinates_min[1]][ + coordinates[2] - self.coordinates_min[2]] + + def getNeighbors(self, Block): + for i in range(-1, 2): + for j in range(-1, 2): + for k in range(-1, 2): + if not (i == 0 and j == 0 and k == 0): + coordinates = (Block.coordinates[0] + i, Block.coordinates[1] + j, Block.coordinates[2] + k) + if self.isInVolume(coordinates): + Block.addNeighbors([self.getBlockFromCoordinates(coordinates)]) + + def setVolume(self): + """ + Scan the world with no optimization. Not tested on large areas. + """ + + editor = Editor(buffering=True) + + for x in range(self.coordinates_min[0], self.coordinates_max[0] + 1): + for y in range(self.coordinates_min[1], self.coordinates_max[1] + 1): + for z in range(self.coordinates_min[2], self.coordinates_max[2] + 1): + self.addBlocks([Block((x, y, z), editor.getBlock((x, y, z)).id)]) + + def getData(self): + """ + Generate all needed datas for the generator : heightmap, watermap, and preset the volume with data from the heightmap. + """ + + editor = Editor() + buildArea = editor.getBuildArea() + buildRect = buildArea.toRect() + + xzStart = buildRect.begin + print("[World]", '('+str(xzStart[0])+', '+str(xzStart[1])+')', "xzStart") + 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])) + watermap = Image.new("L", xzDistance, 0) + heightmap = Image.new("RGBA", xzDistance, 0) + treesmap = Image.new("RGBA", xzDistance, 0) + + slice = editor.loadWorldSlice(buildRect) + + heightmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING_NO_LEAVES"], dtype=np.uint8)) + treesmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING"], dtype=np.uint8)) + + for x in range(0, xzDistance[0]): + for z in range(0, xzDistance[1]): + y = heightmapData[x][z] - 1 + yTree = treesmapData[x][z] - 1 + + biome = slice.getBiome((x, y, z)) + block = slice.getBlock((x, y, z)) + maybeATree = slice.getBlock((x, yTree, z)) + + if maybeATree.id in lookup.TREES: + treesmap.putpixel((x, z), (yTree, yTree, yTree)) + + if block.id not in lookup.TREES: + heightmap.putpixel((x, z), (y, y, y)) + else: + height = 0 + number = 0 + for i in range(-1, 2): + for j in range(-1, 2): + 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 + + # print('getData for tree', xzStart[0] + x + i, k, xzStart[1] + z + j) + + blockNeighbor = slice.getBlock((x + i, k, z + j)) + if blockNeighbor.id not in lookup.TREES: + height += k + number += 1 + if number != 0: + average = round(height / number) + # print(average, "average") + heightmap.putpixel((x, z), (average, average, average)) + + if (biome in waterBiomes) or (block.id in waterBlocks): + watermap.putpixel((x, z), 255) + else: + watermap.putpixel((x, z), 0) + + self.addBlocks([Block((xzStart[0] + x, 100, xzStart[1] + z), block)]) # y set to 100 for 2D + + return heightmap, watermap, treesmap + + def propagate(self, coordinates, scanned=[]): + i = 0 + editor = Editor(buffering=True) + if self.isInVolume(coordinates): + Block = self.getBlockFromCoordinates(coordinates) + self.getNeighbors(Block) + for neighbor in Block.neighbors: + if neighbor not in scanned: + scanned.append(neighbor) + self.getNeighbors(neighbor) + if neighbor.isSurface(): + self.propagate(neighbor.coordinates, scanned) + + def volumeTo3DBinaryImage(self): + binaryImage = [] + for x in range(self.length_x): + binaryImage.append([]) + for y in range(self.length_y): + binaryImage[x].append([]) + for z in range(self.length_z): + if self.volume[x][y][z] != None: + binaryImage[x][y].append(True) + else: + binaryImage[x][y].append(False) + + return np.array(binaryImage) + + def maskVolume(self, mask): + """ + + Delete unusable area of the volume to not let it be use by the skeletonize, based on a filtered image that act as a mask. + + Args: + mask (image): white or black image : combined watermap smoothed and sobel smoothed. + """ + 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])) + + mask = Image.open(mask) + + slice = editor.loadWorldSlice(buildRect) + + heightmapData = list(np.array(slice.heightmaps["MOTION_BLOCKING_NO_LEAVES"], dtype=np.uint8)) + + 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: + self.removeBlock((x, 100, z)) # y set to 100 for 2D + + def simplifyVolume(self): + array = self.volumeTo3DBinaryImage() + # array = ndimage.binary_dilation(array, iterations=15) + + return array + + +if __name__ == "__main__": + w = World() + w.getData() \ No newline at end of file diff --git a/world_maker/data/.gitkeep b/world_maker/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/world_maker/data/heightmap.png b/world_maker/data/heightmap.png new file mode 100644 index 0000000..a743f15 Binary files /dev/null and b/world_maker/data/heightmap.png differ diff --git a/world_maker/data/highwaymap.png b/world_maker/data/highwaymap.png new file mode 100644 index 0000000..887f225 Binary files /dev/null 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..893f381 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..e6a99cc 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 new file mode 100644 index 0000000..b61cb41 Binary files /dev/null and b/world_maker/data/sobelmap.png differ diff --git a/world_maker/data/treemap.png b/world_maker/data/treemap.png new file mode 100644 index 0000000..ab753da Binary files /dev/null and b/world_maker/data/treemap.png differ diff --git a/world_maker/data/watermap.png b/world_maker/data/watermap.png new file mode 100644 index 0000000..0f1cdf9 Binary files /dev/null and b/world_maker/data/watermap.png differ diff --git a/world_maker/data_analysis.py b/world_maker/data_analysis.py new file mode 100644 index 0000000..fa685a2 --- /dev/null +++ b/world_maker/data_analysis.py @@ -0,0 +1,223 @@ +import World +from PIL import Image, ImageFilter +import numpy as np +from scipy import ndimage +from Skeleton import Skeleton +from typing import Union + + +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') + print("[Data Analysis] Data generated.") + return heightmap, watermap, treemap + + +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. + + Args: + image (image): image to filter + """ + return Image.fromarray(np.invert(np.array(image))) + + +def filter_sobel(image: Union[str, Image]) -> Image: + """ + Edge detection algorithms from an image. + + Args: + image (image): image to filter + """ + + # Open the image + image = handle_import_image(image).convert('RGB') + + img = np.array(image).astype(np.uint8) + + # Apply gray scale + gray_img = np.round( + 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2] + ).astype(np.uint8) + + # Sobel Operator + h, w = gray_img.shape + # define filters + horizontal = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]) # s2 + vertical = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]) # s1 + + # define images with 0s + newhorizontalImage = np.zeros((h, w)) + newverticalImage = np.zeros((h, w)) + newgradientImage = np.zeros((h, w)) + + # offset by 1 + 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]) + ) + + 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]) + ) + + newverticalImage[i - 1, j - 1] = abs(verticalGrad) + + # Edge Magnitude + mag = np.sqrt(pow(horizontalGrad, 2.0) + pow(verticalGrad, 2.0)) + newgradientImage[i - 1, j - 1] = mag + + image = Image.fromarray(newgradientImage) + image = image.convert("L") + + return image + + +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. + + Returns: + image: black or white image, with black as flat areas to be skeletonized + """ + + image = handle_import_image(image) + + # image = image.filter(ImageFilter.SMOOTH_MORE) + # image = image.filter(ImageFilter.SMOOTH_MORE) + # image = image.filter(ImageFilter.SMOOTH_MORE) + image = image.convert('L') + image = image.filter(ImageFilter.GaussianBlur(radius)) + array = np.array(image) + + bool_array = array > 7 + + # bool_array = ndimage.binary_opening(bool_array, structure=np.ones((3,3)), iterations=1) + # bool_array = ndimage.binary_closing(bool_array, structure=np.ones((3,3)), iterations=1) + # bool_array = ndimage.binary_opening(bool_array, structure=np.ones((5,5)), iterations=1) + # bool_array = ndimage.binary_closing(bool_array, structure=np.ones((5,5)), iterations=1) + # bool_array = ndimage.binary_opening(bool_array, structure=np.ones((7,7)), iterations=1) + # bool_array = ndimage.binary_closing(bool_array, structure=np.ones((7,7)), iterations=1) + + return Image.fromarray(bool_array) + + +def subtract_map(image: Union[str, Image], substractImage: Union[str, Image]) -> Image: + image = handle_import_image(image) + substractImage = handle_import_image(substractImage).convert('L') + + array_heightmap = np.array(image) + array_substractImage = np.array(substractImage) + + mask = array_substractImage == 255 + array_heightmap[mask] = 0 + + return Image.fromarray(array_heightmap) + + +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 + + 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 filter_remove_details(image: Union[str, Image], n: int = 20) -> Image: + image = handle_import_image(image) + array = np.array(image) + for _ in range(n): + array = ndimage.binary_dilation(array, iterations=4) + array = ndimage.binary_erosion(array, iterations=5) + array = filter_smooth_array(array, 2) + array = ndimage.binary_erosion(array, iterations=3) + image = Image.fromarray(array) + return image + + +def highway_map() -> Image: + print("[Data Analysis] Generating highway map...") + smooth_sobel = filter_smooth("./data/sobelmap.png", 1) + negative_smooth_sobel = filter_negative(smooth_sobel) + negative_smooth_sobel_water = subtract_map(negative_smooth_sobel, './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) + array_sobel_water = filter_smooth_array(array_sobel_water, 5) + array_sobel_water = ndimage.binary_erosion(array_sobel_water, iterations=20) + 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') + print("[Data Analysis] Highway map generated.") + return image_no_details + + +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(image: Union[str, Image] = './data/highwaymap.png'): + 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') diff --git a/world_maker/world_maker.py b/world_maker/world_maker.py new file mode 100644 index 0000000..c711b88 --- /dev/null +++ b/world_maker/world_maker.py @@ -0,0 +1,9 @@ +import World +from PIL import Image +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') + skeleton_highway_map(highway_map())