Merge pull request #4 from NichiHachi/main

Highway map and skeleton
This commit is contained in:
Xeon0X
2024-06-13 16:42:43 +02:00
committed by GitHub
16 changed files with 499 additions and 102 deletions

View File

@@ -1,4 +1,7 @@
gdpc==7.1.0
numpy==1.26.4
Pillow==10.3.0
pygame==2.5.2
scipy==1.13.0
scipy==1.13.1
skan==0.11.1
skimage==0.0

View File

@@ -1,4 +1,4 @@
from District import District, CustomDistrict, VoronoiDistrict
from District import District
from Position import Position
from PIL import Image
import random
@@ -33,13 +33,14 @@ class City:
watermap.close()
heightmap.close()
def add_district(self, center: Position):
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(CustomDistrict(len(self.districts) + 1, center))
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):
@@ -62,15 +63,15 @@ class City:
"""
min_distance = point.distance_to(self.districts[index_district].center_expend)
index_district_chosen = index_district
for i in range(index_district + 1, len(self.districts)):
if point in self.districts[i].area_expend:
distance = point.distance_to(self.districts[i].center_expend)
for index in range(index_district + 1, len(self.districts)):
if point in self.districts[index].area_expend:
distance = point.distance_to(self.districts[index].center_expend)
if distance < min_distance:
min_distance = distance
self.districts[index_district_chosen].area_expend.remove(point)
index_district_chosen = i
index_district_chosen = index
else:
self.districts[i].area_expend.remove(point)
self.districts[index].area_expend.remove(point)
self.districts[index_district_chosen].area.append(point)
self.districts[index_district_chosen].area_expend_from_point.append(point)
self.districts[index_district_chosen].area_expend.remove(point)
@@ -92,21 +93,19 @@ class City:
"""
Loop the expansion of all districts in the city until all districts are fully expanded.
"""
loop_count = 0
print("[City] Start expanding districts...")
while not self.is_expend_finished():
self.update_expend_district()
loop_count += 1
if loop_count % 100 == 0:
print("[City] Loop count: ", loop_count)
print("[City] Finished expanding districts.")
def custom_district_draw_map(self):
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 = {i: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
for i in range(1, len(self.districts) + 1)}
colors = {id_district: (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
for id_district in range(1, len(self.districts) + 1)}
for y in range(height):
for x in range(width):
@@ -115,12 +114,13 @@ class City:
else:
img.putpixel((x, y), colors[self.map_data[y][x]])
img.save('./data/custom_district.png')
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, 600), random.randint(0, 600)))
city.add_district(Position(random.randint(0, 800), random.randint(0, 800)))
city.loop_expend_district()
city.custom_district_draw_map()
city.district_draw_map()

View File

@@ -1,45 +1,6 @@
from Position import Position
class District:
"""
The District class represents a district in the world.
A district can be characterized by its type and its unique id.
Attributes:
tile_id (int): The unique id of the district.
type (str): The type of the district. Can be "Forest", "City", "Mountain" or "Villa".
"""
def __init__(self, tile_id: int):
"""
The constructor for the District class.
:param tile_id: Unique id of the district (Must be greater than 0)
"""
if tile_id <= 0:
raise ValueError("Tile id must be greater than 0")
self.tile_id = tile_id
self.type = "" #Forest, City, Montain, Villa
def verify_point(point: Position, point_new: Position, map_data: list[list[int]], height_map: list[list[int]]):
"""
Function to 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
abs(height_map[point_new.y][point_new.x] - height_map[point.y][point.x]) < 2)
class CustomDistrict(District):
"""
The CustomDistrict class represents a district that can be expanded.
@@ -49,19 +10,39 @@ class CustomDistrict(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):
def __init__(self, tile_id: int, center: Position, district_type: str = ""):
"""
The constructor for the CustomDistrict class.
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)
"""
super().__init__(tile_id)
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.
@@ -71,20 +52,7 @@ class CustomDistrict(District):
: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 verify_point(point, point + pos, map_data, height_map):
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)
class Edge: #I'm Edging rn
def __init__(self, point1, point2):
self.point1 = point1
self.point2 = point2
class VoronoiDistrict(District):
def __init__(self, tile_id: int, center: Position):
super().__init__(tile_id)
self.center = center
self.edges = []

215
world_maker/Skeleton.py Normal file
View File

@@ -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

View File

@@ -111,7 +111,7 @@ class World:
buildRect = buildArea.toRect()
xzStart = buildRect.begin
print(xzStart, "xzStart")
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)
@@ -125,12 +125,9 @@ class World:
for x in range(0, xzDistance[0]):
for z in range(0, xzDistance[1]):
y = heightmapData[x][z] - 1
yTree = treesmapData[x][z] - 1
print('getData', xzStart[0] + x, y, xzStart[1] + z)
biome = slice.getBiome((x, y, z))
block = slice.getBlock((x, y, z))
maybeATree = slice.getBlock((x, yTree, z))
@@ -145,11 +142,11 @@ class World:
number = 0
for i in range(-1, 2):
for j in range(-1, 2):
if (i != 0 or j != 0):
if i != 0 or j != 0:
if (0 <= x + i < xzDistance[0]) and (0 <= z + j < xzDistance[1]):
k = heightmapData[x + i][z + j] - 1
print('getData for tree', xzStart[0] + x + i, k, xzStart[1] + z + j)
# 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:
@@ -157,20 +154,19 @@ class World:
number += 1
if number != 0:
average = round(height / number)
print(average, "average")
# print(average, "average")
heightmap.putpixel((x, z), (average, average, average))
if ((biome in waterBiomes) or (block.id in waterBlocks)):
watermap.putpixel((x, z), (255))
if (biome in waterBiomes) or (block.id in waterBlocks):
watermap.putpixel((x, z), 255)
else:
watermap.putpixel((x, z), (0))
watermap.putpixel((x, z), 0)
self.addBlocks([Block((xzStart[0] + x, 100, xzStart[1] + z), block)]) # y set to 100 for 2D
return heightmap, watermap, treesmap
def propagate(self, coordinates, scanned=[]):
print('propagate', coordinates)
i = 0
editor = Editor(buffering=True)
if self.isInVolume(coordinates):
@@ -190,7 +186,7 @@ class World:
for y in range(self.length_y):
binaryImage[x].append([])
for z in range(self.length_z):
if (self.volume[x][y][z] != None):
if self.volume[x][y][z] != None:
binaryImage[x][y].append(True)
else:
binaryImage[x][y].append(False)
@@ -222,7 +218,7 @@ class World:
for x in range(0, xzDistance[0]):
for z in range(0, xzDistance[1]):
y = heightmapData[x][z] - 1
if mask.getpixel((x, z)) == (255):
if mask.getpixel((x, z)) == 255:
self.removeBlock((x, 100, z)) # y set to 100 for 2D
def simplifyVolume(self):

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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')

View File

@@ -1,17 +1,9 @@
import World
def get_data(world: World):
heightmap, watermap, treemap = world.getData()
heightmap.save('./data/heightmap.png')
watermap.save('./data/watermap.png')
treemap.save('./data/treemap.png')
def main():
world = World.World()
get_data(world)
from PIL import Image
from data_analysis import get_data, highway_map, filter_sobel, skeleton_highway_map
if __name__ == '__main__':
main()
world = World.World()
heightmap, watermap, treemap = get_data(world)
filter_sobel("./data/heightmap.png").save('./data/sobelmap.png')
skeleton_highway_map(highway_map())