Files
GDMC-2024/world_maker/Skeleton.py

293 lines
11 KiB
Python

import random
from collections import Counter
from typing import List, Union
import numpy as np
from gdpc import Editor
from PIL.Image import Image
from PIL import Image as img, ImageDraw
from skan.csr import skeleton_to_csgraph
from skimage.morphology import skeletonize
from networks.geometry.Point3D import Point3D
def handle_import_image(image: Union[str, Image]) -> Image:
if isinstance(image, str):
return img.open(image)
return image
def simplify_coordinates(coordinates, epsilon):
if len(coordinates) < 3:
return coordinates
# Find the point with the maximum distance
max_distance = 0
max_index = 0
end_index = len(coordinates) - 1
for i in range(1, end_index):
distance = Point3D(coordinates[i][0], coordinates[i][1], coordinates[i][2]).distance(
Point3D(coordinates[0][0], coordinates[0][1], coordinates[0][2]))
if distance > max_distance:
max_distance = distance
max_index = i
simplified_coordinates = []
# If the maximum distance is greater than epsilon, recursively simplify
if max_distance > epsilon:
rec_results1 = simplify_coordinates(coordinates[:max_index+1], epsilon)
rec_results2 = simplify_coordinates(coordinates[max_index:], epsilon)
# Combine the simplified sub-results
simplified_coordinates.extend(rec_results1[:-1])
simplified_coordinates.extend(rec_results2)
else:
# The maximum distance is less than epsilon, retain the endpoints
simplified_coordinates.append(coordinates[0])
simplified_coordinates.append(coordinates[end_index])
return simplified_coordinates
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 = img.open(
"./world_maker/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
def road_area(self, name: str, radius: int = 10) -> Image:
print("[Skeleton] Start mapping the road area...")
heightmap = img.open("./world_maker/data/heightmap.png")
width, height = heightmap.size
road_area_map = img.new("L", (width, height), 0)
road_area_map_draw = ImageDraw.Draw(road_area_map)
# Lines
for i in range(len(self.lines)):
for j in range(len(self.lines[i])):
z = self.coordinates[self.lines[i][j]][0]
x = self.coordinates[self.lines[i][j]][2]
circle_coords = (z - radius, x - radius,
z + radius, x + radius)
road_area_map_draw.ellipse(circle_coords, fill=255)
# Centers
for i in range(len(self.centers)):
z = self.coordinates[self.centers[i]][0]
x = self.coordinates[self.centers[i]][2]
circle_coords = (z - radius, x - radius, z + radius, x + radius)
road_area_map_draw.ellipse(circle_coords, fill=255)
road_area_map.save("./world_maker/data/"+name)
print("[Skeleton] Road area mapping completed.")
return road_area_map