Refined automatic map generation script

This commit is contained in:
Davide Passoni
2024-02-23 15:54:49 +01:00
parent acb55044d1
commit 9a571132c8
11 changed files with 274 additions and 155 deletions

View File

@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "main.py",
"console": "integratedTerminal",
"args": ["./configs/LasVegas/LasVegas.yml"]
}
]
}

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>Senza titolo</name>
<gx:CascadingStyle kml:id="__managed_style_2F9039BE1B2F0ACE3361">
<Style>
<IconStyle>
<scale>1.2</scale>
<Icon>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&amp;id=2000&amp;scale=4</href>
</Icon>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
</IconStyle>
<LabelStyle>
</LabelStyle>
<LineStyle>
<color>ff2dc0fb</color>
<width>6</width>
</LineStyle>
<PolyStyle>
<color>40ffffff</color>
</PolyStyle>
<BalloonStyle>
<displayMode>hide</displayMode>
</BalloonStyle>
</Style>
</gx:CascadingStyle>
<gx:CascadingStyle kml:id="__managed_style_1FB08D70372F0ACE3361">
<Style>
<IconStyle>
<Icon>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&amp;id=2000&amp;scale=4</href>
</Icon>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
</IconStyle>
<LabelStyle>
</LabelStyle>
<LineStyle>
<color>ff2dc0fb</color>
<width>4</width>
</LineStyle>
<PolyStyle>
<color>40ffffff</color>
</PolyStyle>
<BalloonStyle>
<displayMode>hide</displayMode>
</BalloonStyle>
</Style>
</gx:CascadingStyle>
<StyleMap id="__managed_style_0152B588AD2F0ACE3361">
<Pair>
<key>normal</key>
<styleUrl>#__managed_style_1FB08D70372F0ACE3361</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#__managed_style_2F9039BE1B2F0ACE3361</styleUrl>
</Pair>
</StyleMap>
<Placemark id="0389FD622C2F0ACE3361">
<name>Poligono senza titolo</name>
<LookAt>
<longitude>-115.0437621195802</longitude>
<latitude>36.2404454323581</latitude>
<altitude>568.1069300877758</altitude>
<heading>0</heading>
<tilt>0</tilt>
<gx:fovy>35</gx:fovy>
<range>21582.08160380367</range>
<altitudeMode>absolute</altitudeMode>
</LookAt>
<styleUrl>#__managed_style_0152B588AD2F0ACE3361</styleUrl>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<coordinates>
-115.0657741984423,36.20908708202413,0 -115.0064821223275,36.20969233542438,0 -115.0077003574054,36.25251471885595,0 -115.0604905801644,36.25236266770626,0 -115.0657741984423,36.20908708202413,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
<Placemark id="077CD022A52F0ACE74D0">
<name>Poligono senza titolo</name>
<LookAt>
<longitude>-115.144039076036</longitude>
<latitude>36.07599823986274</latitude>
<altitude>636.6854074835677</altitude>
<heading>0</heading>
<tilt>0</tilt>
<gx:fovy>35</gx:fovy>
<range>14114.14487087633</range>
<altitudeMode>absolute</altitudeMode>
</LookAt>
<styleUrl>#__managed_style_0152B588AD2F0ACE3361</styleUrl>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<coordinates>
-115.1866576903507,36.06787728722109,0 -115.1107872218999,36.06765965614163,0 -115.1145241760975,36.10294242539007,0 -115.1779590799479,36.10179027153036,0 -115.1866576903507,36.06787728722109,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
</Document>
</kml>

View File

@@ -0,0 +1,4 @@
{
'output_directory': '.\LasVegas', # Where to save the output files
'boundary_file': '.\configs\LasVegas\LasVegas.kml'
}

View File

@@ -0,0 +1,10 @@
{
'width': 1920, # The width of your screen, in pixels
'height': 1080, # The height of your screen, in pixels
'geo_resolution': 1.0 # The resolution of the map on the screen, in meters per pixel.
# To measure this value, first set the F10 map at the desired zoom level.
# Then, use F10's map measure tool, and measure the width of the screen in meters.
# Finally, divide that value by the width in pixels.
# A good value would be around 1 meter per pixel, meaning a 1920px wide map would measure about 1 nautical mile across on the F10 map
# Lower values will produce higher resolution maps, but beware of space usage!
}

View File

@@ -0,0 +1,67 @@
import sys
import yaml
from pyproj import Geod
from fastkml import kml
from shapely import wkt
from datetime import timedelta
import map_generator
if len(sys.argv) == 1:
print("Please provide a configuration file as first argument. You can also drop the configuration file on this script to run it.")
else:
config_file = sys.argv[1]
print(f"Using config file: {config_file}")
with open('configs/screen_properties.yml', 'r') as sp:
with open(config_file, 'r') as cp:
screen_config = yaml.safe_load(sp)
map_config = yaml.safe_load(cp)
print("#################################################################################################################################################")
print("# IMPORTANT NOTE: the screen properties must be configured according to your screen and desired zoom level. Make sure you set them accordingly. #")
print("#################################################################################################################################################")
print("Screen parameters:")
print(f"-> Screen width: {screen_config["width"]}px")
print(f"-> Screen height: {screen_config["height"]}px")
print(f"-> Geographic resolution: {screen_config["geo_resolution"]} meters/pixel")
print("Map parameters:")
print(f"-> Output directory: {map_config["output_directory"]}")
print(f"-> Boundary file: {map_config["boundary_file"]}")
with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp:
# Read the config file and compute the total area of the covered map
doc = bp.read()
k = kml.KML()
k.from_string(doc)
geod = Geod(ellps="WGS84")
features = []
area = 0
for feature in k.features():
for sub_feature in list(feature.features()):
geo = sub_feature.geometry
area += abs(geod.geometry_area_perimeter(wkt.loads(geo.wkt))[0])
features.append(sub_feature)
print(f"Found {len(features)} features in the provided kml file")
tile_size = 256 * screen_config["geo_resolution"] # meters
tiles_per_screenshot = int(screen_config["width"] / 256) * int(screen_config["height"] / 256)
tiles_num = int(area / (tile_size * tile_size))
screenshots_num = int(tiles_num / tiles_per_screenshot)
total_time = int(screenshots_num / 1.0)
print(f"Total area: {int(area / 1e6)} square kilometers")
print(f"Estimated number of tiles: {tiles_num}")
print(f"Estimated number of screenshots: {screenshots_num}")
print(f"Estimated time to complete: {timedelta(seconds=total_time)} (hh:mm:ss)")
print("The script is ready to go. After you press enter, it will wait for 5 seconds, then it will start.")
input("Press enter to continue...")
map_generator.run(map_config)

View File

@@ -0,0 +1,224 @@
import math
import requests
import pyautogui
import time
import os
import yaml
from fastkml import kml
from shapely import wkt, Point
from PIL import Image
from concurrent import futures
# global counters
fut_counter = 0
tot_futs = 0
# constants
C = 40075016.686 # meters, Earth equatorial circumference
def deg_to_num(lat_deg, lon_deg, zoom):
lat_rad = math.radians(lat_deg)
n = 1 << zoom
xtile = int((lon_deg + 180.0) / 360.0 * n)
ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
return xtile, ytile
def num_to_deg(xtile, ytile, zoom):
n = 1 << zoom
lon_deg = xtile / n * 360.0 - 180.0
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
lat_deg = math.degrees(lat_rad)
return lat_deg, lon_deg
def compute_mpps(lat, z):
return C * math.cos(math.radians(lat)) / math.pow(2, z + 8)
def printProgressBar(iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '', printEnd = "\r"):
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filledLength = int(length * iteration // total)
bar = fill * filledLength + '-' * (length - filledLength)
print(f'\r{prefix} |{bar}| {percent}% {suffix}', end = printEnd)
# Print New Line on Complete
if iteration == total:
print()
def done_callback(fut):
global fut_counter, tot_futs
fut_counter += 1
printProgressBar(fut_counter, tot_futs)
def extract_tiles(n, screenshots_coordinates, params):
f = params["f"]
zoom = params["zoom"]
output_directory = params["output_directory"]
n_width = params["n_width"]
n_height = params["n_height"]
screen_resolution = params["screen_resolution"]
mpps = params["mpps"]
coords = screenshots_coordinates[n]
if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))):
# Open the source screenshot
img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))
# Scale the image so that tiles are 256x256
scale = screen_resolution / mpps
w, h = img.size
img = img.resize((int(w * scale), int(h * scale)))
# Compute the Web Mercator Projection position of the top left corner of the most centered tile
lat = coords[0]
lng = coords[1]
X_center, Y_center = deg_to_num(lat, lng, zoom)
# Compute the position of the top left corner of the top left tile
start_x = w / 2 - n_width / 2 * 256
start_y = h / 2 - n_height / 2 * 256
# Iterate on the grid
for column in range(0, n_width):
for row in range(0, n_height):
# Crop the tile and compute its Web Mercator Projection position
box = (start_x + column * 256, start_y + row * 256, start_x + (column + 1) * 256, start_y + (row + 1) * 256)
X = X_center - math.floor(n_width / 2) + column
Y = Y_center - math.floor(n_height / 2) + row
# Save the tile
if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom), str(X))):
try:
os.mkdir(os.path.join(output_directory, "tiles", str(zoom), str(X)))
except FileExistsError:
continue
except Exception as e:
raise e
img.crop(box).save(os.path.join(output_directory, "tiles", str(zoom), str(X), f"{Y}.jpg"))
n += 1
else:
raise Exception(f"{os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")} missing")
def run(map_config):
with open('configs/screen_properties.yml', 'r') as sp:
screen_config = yaml.safe_load(sp)
# Create output folders
output_directory = map_config["output_directory"]
if not os.path.exists(output_directory):
os.mkdir(output_directory)
if not os.path.exists(os.path.join(output_directory, "screenshots")):
os.mkdir(os.path.join(output_directory, "screenshots"))
else:
skip_screenshots = (input("Raw screenshots already found for this config, do you want to skip directly to tiles extraction? Enter y to skip: ") == "y")
if not os.path.exists(os.path.join(output_directory, "tiles")):
os.mkdir(os.path.join(output_directory, "tiles"))
# Compute the optimal zoom level
usable_width = screen_config["width"] - 200 # Keep a margin around the center
usable_height = screen_config["height"] - 200 # Keep a margin around the center
with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp:
# Read the config file
doc = bp.read()
k = kml.KML()
k.from_string(doc)
# Extract the features
features = []
for feature in k.features():
for sub_feature in list(feature.features()):
features.append(sub_feature)
# Iterate over all the closed features in the kml file
f = 1
for feature in features:
geo = sub_feature.geometry
# Define the boundary rect around the area
start_lat = geo.bounds[3]
start_lng = geo.bounds[0]
end_lat = geo.bounds[1]
end_lng = geo.bounds[2]
# Find the zoom level that better approximates the provided resolution
screen_resolution = screen_config['geo_resolution']
mpps_delta = [abs(compute_mpps((start_lat + end_lat) / 2, z) - screen_resolution) for z in range(0, 21)]
zoom = mpps_delta.index(min(mpps_delta))
print(f"Feature {f} of {len(features)}, using zoom level {zoom}")
# Find the maximum dimension of the tiles at the given resolution
mpps = compute_mpps(end_lat, zoom)
d = 256 * mpps / screen_resolution
n_height = math.floor(usable_height / d)
n_width = math.floor(usable_width / d)
print(f"Feature {f} of {len(features)}, each screenshot will provide {n_height} tiles in height and {n_width} tiles in width")
# Find the starting and ending points
start_X, start_Y = deg_to_num(start_lat, start_lng, zoom)
end_X, end_Y = deg_to_num(end_lat, end_lng, zoom)
# Find all the X, Y coordinates inside of the provided area
screenshots_coordinates = []
for X in range(start_X, end_X, n_width):
for Y in range(start_Y, end_Y, n_height):
lat, lng = num_to_deg(X, Y, zoom)
p = Point(lng, lat)
if p.within(wkt.loads(geo.wkt)):
screenshots_coordinates.append((lat, lng))
print(f"Feature {f} of {len(features)}, {len(screenshots_coordinates)} screenshots will be taken")
# Start looping
if not skip_screenshots:
print(f"Feature {f} of {len(features)}, taking screenshots...")
n = 0
for coords in screenshots_coordinates:
# Making PUT request
#data = json.dumps({'lat': coords[0], 'lng': coords[1]})
#r = requests.put('http://localhost:8080', data = data)
time.sleep(0.1)
## Take and save screenshot
screenshot = pyautogui.screenshot()
screenshot.save(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))
printProgressBar(n + 1, len(screenshots_coordinates))
n += 1
# Extract the tiles
if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom))):
os.mkdir(os.path.join(output_directory, "tiles", str(zoom)))
params = {
"f": f,
"zoom": zoom,
"output_directory": output_directory,
"n_width": n_width,
"n_height": n_height,
"screen_resolution": screen_resolution,
"mpps": mpps
}
# Extract the tiles with parallel thread execution
with futures.ThreadPoolExecutor() as executor:
print(f"Feature {f} of {len(features)}, extracting tiles...")
global tot_futs, fut_counter
futs = [executor.submit(extract_tiles, n, screenshots_coordinates, params) for n in range(0, len(screenshots_coordinates))]
tot_futs = len(futs)
fut_counter = 0
[fut.add_done_callback(done_callback) for fut in futs]
[fut.result() for fut in futures.as_completed(futs)]
# Increase the feature counter
print(f"Feature {f} of {len(features)} completed!")
f += 1

View File

@@ -0,0 +1,23 @@
certifi==2024.2.2
charset-normalizer==3.3.2
fastkml==0.12
idna==3.6
MouseInfo==0.1.3
numpy==1.26.4
pillow==10.2.0
PyAutoGUI==0.9.54
pygeoif==0.7
PyGetWindow==0.0.9
PyMsgBox==1.0.9
pyperclip==1.8.2
pyproj==3.6.1
PyRect==0.2.0
PyScreeze==0.1.30
python-dateutil==2.8.2
pytweening==1.2.0
PyYAML==6.0.1
requests==2.31.0
setuptools==69.1.0
shapely==2.0.3
six==1.16.0
urllib3==2.2.1