Completed camera control server in lua and more work on merging algorithm

This commit is contained in:
Pax1601
2024-02-28 18:58:40 +01:00
parent 05e0cc393a
commit 832568aa00
14 changed files with 403 additions and 65 deletions

View File

@@ -582,7 +582,8 @@ export class Map extends L.Map {
setCameraControlMode(newCameraControlMode: string) { setCameraControlMode(newCameraControlMode: string) {
this.#cameraControlMode = newCameraControlMode; this.#cameraControlMode = newCameraControlMode;
this.#broadcastPosition(); if (this.#slaveDCSCamera)
this.#broadcastPosition();
} }
/* Event handlers */ /* Event handlers */
@@ -773,7 +774,7 @@ export class Map extends L.Map {
try { try {
groundElevation = parseFloat(response); groundElevation = parseFloat(response);
var xmlHttp = new XMLHttpRequest(); var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", `http://localhost:${this.#cameraControlPort}`); xmlHttp.open("PUT", `http://127.0.0.1:${this.#cameraControlPort}`);
xmlHttp.setRequestHeader("Content-Type", "application/json"); xmlHttp.setRequestHeader("Content-Type", "application/json");
const C = 40075016.686; const C = 40075016.686;
@@ -981,7 +982,7 @@ export class Map extends L.Map {
#checkCameraPort(){ #checkCameraPort(){
var xmlHttp = new XMLHttpRequest(); var xmlHttp = new XMLHttpRequest();
xmlHttp.open("OPTIONS", `http://localhost:${this.#cameraControlPort}`); xmlHttp.open("OPTIONS", `http://127.0.0.1:${this.#cameraControlPort}`);
xmlHttp.onload = (res: any) => { xmlHttp.onload = (res: any) => {
if (xmlHttp.status == 200) if (xmlHttp.status == 200)
this.#setSlaveDCSCameraAvailable(true); this.#setSlaveDCSCameraAvailable(true);

View File

@@ -0,0 +1,214 @@
local _prevLuaExportStart = LuaExportStart
local _prevLuaExportBeforeNextFrame = LuaExportBeforeNextFrame
local _prevLuaExportStop = LuaExportStop
local server = nil
local port = 3003
local headers = "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nVary: Accept-Encoding, Origin\r\nKeep-Alive: timeout=2, max=100\r\nConnection: Keep-Alive\r\n\r\n"
function startTCPServer()
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'Starting TCP Server')
package.path = package.path..";"..lfs.currentdir().."/LuaSocket/?.lua"
package.cpath = package.cpath..";"..lfs.currentdir().."/LuaSocket/?.dll"
socket = require("socket")
server = assert(socket.bind("127.0.0.1", port))
if server then
server:setoption("tcp-nodelay", true)
server:settimeout(0)
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'TCP Server listening on port ' .. port)
else
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'TCP Server did not start successfully')
end
end
function receiveTCP()
if server then
-- Accept a new connection without blocking
local client = server:accept()
if client then
-- Set the timeout of the connection to 5ms
client:settimeout(0)
client:setoption("tcp-nodelay", true)
local acc = ""
local data = ""
-- Start receiving data, accumulate it in acc
while data ~= nil do
-- Receive a new line
data, err, partial = client:receive('*l')
if data then
-- If we receive an empty string it means the header section of the message is over
if data == "" then
-- Is this an OPTIONS request?
if string.find(acc, "OPTIONS") ~= nil then
client:send("HTTP/1.1 200 OK\r\n" .. headers)
client:close()
-- Is this a PUT request?
elseif string.find(acc, "PUT") ~= nil then
-- Extract the length of the body
local contentLength = string.match(acc, "Content%-Length: (%d+)")
if contentLength ~= nil then
-- Receive the body
body, err, partial = client:receive(tonumber(contentLength))
if body ~= nil then
local lat = string.match(body, '"lat":%s*([%+%-]?[%d%.]+)%s*[},]')
local lng = string.match(body, '"lng":%s*([%+%-]?[%d%.]+)%s*[},]')
local alt = string.match(body, '"alt":%s*([%+%-]?[%d%.]+)%s*[},]')
local mode = string.match(body, '"mode":%s*"(%a+)"%s*[},]')
if lat ~= nil and lng ~= nil then
client:send("HTTP/1.1 200 OK\r\n" .. headers)
local position = {}
position["lat"] = tonumber(lat)
position["lng"] = tonumber(lng)
if alt ~= nil then
position["alt"] = tonumber(alt)
end
-- F11 view
if mode == "live" or mode == nil then
LoSetCommand(158)
-- F10 view
elseif mode == "map" then
LoSetCommand(15)
end
client:send(setCameraPosition(position))
client:close()
else
client:send("HTTP/1.1 500 ERROR\r\n" .. headers)
client:close()
end
else
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, err)
end
end
client:close()
break
end
else
-- Keep accumulating the incoming data
acc = acc .. " " .. data
end
end
end
end
end
end
function stopTCPServer()
if server then
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'Stopping TCP Server')
server:close()
end
server = nil
end
function setCameraPosition(position)
-- Get the old camera position
local oldPos = LoGetCameraPosition()
-- Extract the commanded position
local point = LoGeoCoordinatesToLoCoordinates(position.lng, position.lat)
local pointNorth = LoGeoCoordinatesToLoCoordinates(position.lng, position.lat + 0.1)
-- Compute the local map rotation and scale and send it back to the server
local rotation = math.atan2(pointNorth.z - point.z, pointNorth.x - point.x)
-- If no altitude is provided, preserve the current camera altitude
local altitude = nil
if position.alt == nil then
altitude = oldPos.p.y
else
altitude = position.alt
end
-- Set the camera position
local pos =
{
x = {x = 0, y = -1, z = 0},
y = {x = 1, y = 0, z = 0},
z = {x = 0, y = 0, z = 1},
p = {x = point.x, y = altitude, z = point.z}
}
LoSetCameraPosition(pos)
return '{"northRotation": ' .. rotation .. '}'
end
LuaExportStart = function()
package.path = package.path..";"..lfs.currentdir().."/LuaSocket/?.lua"
package.cpath = package.cpath..";"..lfs.currentdir().."/LuaSocket/?.dll"
startTCPServer()
-- call original
if _prevLuaExportStart then
_status, _result = pcall(_prevLuaExportStart)
if not _status then
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportStart from another script', _result)
end
end
end
LuaExportBeforeNextFrame = function()
receiveTCP()
-- call original
if _prevLuaExportBeforeNextFrame then
_status, _result = pcall(_prevLuaExportBeforeNextFrame)
if not _status then
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportBeforeNextFrame from another script', _result)
end
end
end
LuaExportStop = function()
stopTCPServer()
-- call original
if _prevLuaExportStop then
_status, _result = pcall(_prevLuaExportStop)
if not _status then
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportStop from another script', _result)
end
end
end
function serializeTable(val, name, skipnewlines, depth)
skipnewlines = skipnewlines or false
depth = depth or 0
local tmp = string.rep(" ", depth)
if name then
if type(name) == "number" then
tmp = tmp .. "[" .. name .. "]" .. " = "
else
tmp = tmp .. name .. " = "
end
end
if type(val) == "table" then
tmp = tmp .. "{" .. (not skipnewlines and "\n" or "")
for k, v in pairs(val) do
tmp = tmp .. serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "")
end
tmp = tmp .. string.rep(" ", depth) .. "}"
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
else
tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
end
return tmp
end

View File

@@ -10,7 +10,7 @@
"request": "launch", "request": "launch",
"program": "main.py", "program": "main.py",
"console": "integratedTerminal", "console": "integratedTerminal",
"args": ["./configs/NTTR/config.yml"] "args": ["./configs/Caucasus/HighResolution.yml"]
} }
] ]
} }

View File

@@ -0,0 +1,37 @@
import sys
from fastkml import kml
from pygeoif.geometry import Polygon
import json
import math
# constants
C = 40075016.686 # meters, Earth equatorial circumference
R = C / (2 * math.pi) # meters, Earth equatorial radius
W = 10000 # meters, size of the square around the airbase
if len(sys.argv) == 1:
print("Please provide a json file as first argument. You can also drop the json file on this script to run it.")
else:
input_file = sys.argv[1]
k = kml.KML()
ns = '{http://www.opengis.net/kml/2.2}'
d = kml.Document(ns, 'docid', 'doc name', 'doc description')
k.append(d)
with open(input_file) as jp:
j = json.load(jp)
for point in j['airbases'].values():
p = kml.Placemark(ns, 'id', 'name', 'description')
lat = point['latitude']
lng = point['longitude']
latDelta = math.degrees(W / R)
lngDelta = math.degrees(W / (R * math.cos(math.radians(lat))))
p.geometry = Polygon([(lng - lngDelta, lat - latDelta), (lng - lngDelta, lat + latDelta), (lng + lngDelta, lat + latDelta), (lng + lngDelta, lat - latDelta)])
d.append(p)
with open(input_file.removesuffix('.json')+'.kml', 'w') as kp:
kp.writelines(k.to_string(prettyprint=True))

View File

@@ -0,0 +1,5 @@
{
'output_directory': '.\Caucasus', # Where to save the output files
'boundary_file': '.\configs\Caucasus\airbases.kml', # Input kml file setting the boundary of the map to create
'zoom_factor': 0.1 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
}

View File

@@ -2,7 +2,7 @@
<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"> <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> <Document>
<name>Senza titolo</name> <name>Senza titolo</name>
<gx:CascadingStyle kml:id="__managed_style_2F9039BE1B2F0ACE3361"> <gx:CascadingStyle kml:id="__managed_style_280E5494AE2F24E92C22">
<Style> <Style>
<IconStyle> <IconStyle>
<scale>1.2</scale> <scale>1.2</scale>
@@ -25,7 +25,7 @@
</BalloonStyle> </BalloonStyle>
</Style> </Style>
</gx:CascadingStyle> </gx:CascadingStyle>
<gx:CascadingStyle kml:id="__managed_style_1FB08D70372F0ACE3361"> <gx:CascadingStyle kml:id="__managed_style_1EB9027B622F24E92C22">
<Style> <Style>
<IconStyle> <IconStyle>
<Icon> <Icon>
@@ -47,57 +47,34 @@
</BalloonStyle> </BalloonStyle>
</Style> </Style>
</gx:CascadingStyle> </gx:CascadingStyle>
<StyleMap id="__managed_style_0152B588AD2F0ACE3361"> <StyleMap id="__managed_style_0F57E9B9782F24E92C22">
<Pair> <Pair>
<key>normal</key> <key>normal</key>
<styleUrl>#__managed_style_1FB08D70372F0ACE3361</styleUrl> <styleUrl>#__managed_style_1EB9027B622F24E92C22</styleUrl>
</Pair> </Pair>
<Pair> <Pair>
<key>highlight</key> <key>highlight</key>
<styleUrl>#__managed_style_2F9039BE1B2F0ACE3361</styleUrl> <styleUrl>#__managed_style_280E5494AE2F24E92C22</styleUrl>
</Pair> </Pair>
</StyleMap> </StyleMap>
<Placemark id="0389FD622C2F0ACE3361"> <Placemark id="0975D432582F24E92C1E">
<name>Poligono senza titolo</name> <name>Poligono senza titolo</name>
<LookAt> <LookAt>
<longitude>-115.0437621195802</longitude> <longitude>37.25019544589698</longitude>
<latitude>36.2404454323581</latitude> <latitude>44.41771380726969</latitude>
<altitude>568.1069300877758</altitude> <altitude>-138.6844933247498</altitude>
<heading>0</heading> <heading>0</heading>
<tilt>0</tilt> <tilt>0</tilt>
<gx:fovy>35</gx:fovy> <gx:fovy>35</gx:fovy>
<range>21582.08160380367</range> <range>3831683.119853139</range>
<altitudeMode>absolute</altitudeMode> <altitudeMode>absolute</altitudeMode>
</LookAt> </LookAt>
<styleUrl>#__managed_style_0152B588AD2F0ACE3361</styleUrl> <styleUrl>#__managed_style_0F57E9B9782F24E92C22</styleUrl>
<Polygon> <Polygon>
<outerBoundaryIs> <outerBoundaryIs>
<LinearRing> <LinearRing>
<coordinates> <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 32.46459319237173,45.67416695848307,0 32.2740650283415,45.2221541106433,0 33.22174616520244,44.4837859435444,0 34.05427109764131,44.2149221586376,0 34.96485577272431,44.60230684639296,0 35.50552864748745,44.8069362633187,0 36.446105774871,44.84425518198143,0 36.76914203317659,44.70347050722764,0 38.22313992004164,44.3163345847565,0 39.43106567523965,43.72064977016311,0 40.23832274382622,43.06831352526857,0 41.01327578994438,42.67925159935859,0 41.34464189582403,42.34329512558789,0 41.16749495371268,41.74956946999534,0 40.80780496107725,41.39360013128164,0 39.98364177441992,41.27272565351572,0 39.42209428526464,41.27830763089842,0 38.82136897872954,41.2291415593637,0 38.78900701766597,39.59331113999448,0 46.4826445997655,39.11657164682355,0 46.83937081793388,45.04996086829865,0 46.88987497227086,47.59122144470205,0 32.29992865035658,47.73230965442627,0 32.46459319237173,45.67416695848307,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> </coordinates>
</LinearRing> </LinearRing>
</outerBoundaryIs> </outerBoundaryIs>

View File

@@ -0,0 +1,5 @@
{
'output_directory': '.\Caucasus', # Where to save the output files
'boundary_file': '.\configs\Caucasus\LowResolution.kml', # Input kml file setting the boundary of the map to create
'zoom_factor': 0.5 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
{
'output_directory': '.\Caucasus', # Where to save the output files
'boundary_file': '.\configs\Caucasus\MediumResolution.kml', # Input kml file setting the boundary of the map to create
'zoom_factor': 0.25 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
}

View File

@@ -0,0 +1 @@
{"airbases":{"1":{"callsign":"Anapa-Vityazevo","coalition":"neutral","latitude":45.013174733771677,"longitude":37.359783477555922},"10":{"callsign":"Gudauta","coalition":"neutral","latitude":43.124233340197144,"longitude":40.564175768400638},"11":{"callsign":"Batumi","coalition":"neutral","latitude":41.603279859649049,"longitude":41.609275483509791},"12":{"callsign":"Senaki-Kolkhi","coalition":"neutral","latitude":42.238728081573278,"longitude":42.061021312855914},"13":{"callsign":"Kobuleti","coalition":"neutral","latitude":41.93210535345338,"longitude":41.876483823101026},"14":{"callsign":"Kutaisi","coalition":"neutral","latitude":42.179153937689627,"longitude":42.495684077400142},"15":{"callsign":"Mineralnye Vody","coalition":"neutral","latitude":44.218646823806807,"longitude":43.100679733081456},"16":{"callsign":"Nalchik","coalition":"neutral","latitude":43.510071438529849,"longitude":43.625108736097914},"17":{"callsign":"Mozdok","coalition":"neutral","latitude":43.791303250938249,"longitude":44.620327262102009},"18":{"callsign":"Tbilisi-Lochini","coalition":"neutral","latitude":41.674720064437075,"longitude":44.946875226153338},"19":{"callsign":"Soganlug","coalition":"neutral","latitude":41.641163266786613,"longitude":44.947183065316693},"2":{"callsign":"Krasnodar-Center","coalition":"neutral","latitude":45.087429883845076,"longitude":38.925202300775062},"20":{"callsign":"Vaziani","coalition":"neutral","latitude":41.637735936261556,"longitude":45.019090938460067},"21":{"callsign":"Beslan","coalition":"neutral","latitude":43.208500987380937,"longitude":44.588922553542936},"3":{"callsign":"Novorossiysk","coalition":"neutral","latitude":44.673329604126899,"longitude":37.786226060479564},"4":{"callsign":"Krymsk","coalition":"neutral","latitude":44.961383022734175,"longitude":37.985886938697085},"5":{"callsign":"Maykop-Khanskaya","coalition":"neutral","latitude":44.67144025735508,"longitude":40.021427482235985},"6":{"callsign":"Gelendzhik","coalition":"neutral","latitude":44.56767458600406,"longitude":38.004146350528103},"7":{"callsign":"Sochi-Adler","coalition":"neutral","latitude":43.439378434050852,"longitude":39.924231880466095},"8":{"callsign":"Krasnodar-Pashkovsky","coalition":"neutral","latitude":45.0460996415433,"longitude":39.203066906324537},"9":{"callsign":"Sukhumi-Babushara","coalition":"neutral","latitude":42.852741071634995,"longitude":41.142447588488196}},"frameRate":60,"load":0,"sessionHash":"K2n7kpGE9yOaYE4G","time":"1709136685634"}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
{
'output_directory': '.\LasVegas', # Where to save the output files
'boundary_file': '.\configs\LasVegas\boundary.kml', # Input kml file setting the boundary of the map to create
'zoom_factor': 0.02, # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
'geo_width': 1.14
}

View File

@@ -2,7 +2,6 @@ import sys
import yaml import yaml
import json import json
import requests import requests
import time
from pyproj import Geod from pyproj import Geod
from fastkml import kml from fastkml import kml
@@ -11,6 +10,9 @@ from datetime import timedelta
import map_generator import map_generator
# Port on which the camera control module is listening
port = 3003
if len(sys.argv) == 1: 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.") print("Please provide a configuration file as first argument. You can also drop the configuration file on this script to run it.")
else: else:
@@ -53,9 +55,12 @@ else:
if 'geo_width' not in map_config: if 'geo_width' not in map_config:
# Let the user input the size of the screen to compute resolution # Let the user input the size of the screen to compute resolution
data = json.dumps({'lat': features[0].geometry.bounds[1], 'lng': features[0].geometry.bounds[0], 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350)}) data = json.dumps({'lat': features[0].geometry.bounds[1], 'lng': features[0].geometry.bounds[0], 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350), 'mode': 'map'})
r = requests.put('http://localhost:8080', data = data) try:
print("The F10 map in your DCS installation was setup. Please, use the measure tool and measure the width of the screen in Nautical Miles") r = requests.put(f'http://127.0.0.1:{port}', data = data)
print("The F10 map in your DCS installation was setup. Please, use the measure tool and measure the width of the screen in Nautical Miles")
except:
print("No running DCS instance detected. You can still run the algorithm if you already took the screenshots, otherwise you will not be able to produce a map.")
map_config['geo_width'] = input("Insert the width of the screen in Nautical Miles: ") map_config['geo_width'] = input("Insert the width of the screen in Nautical Miles: ")
map_config['mpps'] = float(map_config['geo_width']) * 1852 / screen_config['width'] map_config['mpps'] = float(map_config['geo_width']) * 1852 / screen_config['width']
@@ -72,7 +77,7 @@ else:
print(f"Estimated time to complete: {timedelta(seconds=total_time * 0.15)} (hh:mm:ss)") print(f"Estimated time to complete: {timedelta(seconds=total_time * 0.15)} (hh:mm:ss)")
input("Press enter to continue...") input("Press enter to continue...")
map_generator.run(map_config) map_generator.run(map_config, port)

View File

@@ -5,6 +5,7 @@ import time
import os import os
import yaml import yaml
import json import json
import numpy
from fastkml import kml from fastkml import kml
from shapely import wkt, Point from shapely import wkt, Point
@@ -19,7 +20,7 @@ tot_futs = 0
# constants # constants
C = 40075016.686 # meters, Earth equatorial circumference C = 40075016.686 # meters, Earth equatorial circumference
R = C / (2 * math.pi) R = C / (2 * math.pi) # meters, Earth equatorial radius
def deg_to_num(lat_deg, lon_deg, zoom): def deg_to_num(lat_deg, lon_deg, zoom):
lat_rad = math.radians(lat_deg) lat_rad = math.radians(lat_deg)
@@ -60,9 +61,9 @@ def extract_tiles(n, screenshots_XY, params):
n_height = params['n_height'] n_height = params['n_height']
XY = screenshots_XY[n] XY = screenshots_XY[n]
if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))): if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))):
# Open the source screenshot # Open the source screenshot
img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")) img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))
# Compute the Web Mercator Projection position of the top left corner of the most centered tile # Compute the Web Mercator Projection position of the top left corner of the most centered tile
X_center, Y_center = XY[0], XY[1] X_center, Y_center = XY[0], XY[1]
@@ -84,6 +85,7 @@ def extract_tiles(n, screenshots_XY, params):
try: try:
os.mkdir(os.path.join(output_directory, "tiles", str(zoom), str(X))) os.mkdir(os.path.join(output_directory, "tiles", str(zoom), str(X)))
except FileExistsError: except FileExistsError:
# Ignore this error, it means one other thread has already created the folder
continue continue
except Exception as e: except Exception as e:
raise e raise e
@@ -91,33 +93,53 @@ def extract_tiles(n, screenshots_XY, params):
n += 1 n += 1
else: else:
raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}.jpg')} missing") raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}_{zoom}.jpg')} missing")
def merge_tiles(base_path, zoom, tile): def merge_tiles(base_path, zoom, tile):
X = tile[0] X = tile[0]
Y = tile[1] Y = tile[1]
positions = [(0, 0), (0, 1), (1, 0), (1, 1)] # If the image already exists, open it so we can paste the higher quality data in it
if os.path.exists(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg")):
dst = Image.open(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"))
dst = make_background_transparent(dst)
else:
dst = Image.new('RGB', (256, 256), (221, 221, 221))
dst = Image.new('RGB', (256, 256), (0, 0, 0, 0)) # Loop on all the 4 subtiles in the tile
positions = [(0, 0), (0, 1), (1, 0), (1, 1)]
for i in range(0, 4): for i in range(0, 4):
# Open the subtile, if it exists, and resize it down to 128x128
if os.path.exists(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")): if os.path.exists(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")):
im = Image.open(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")).resize((128, 128)) im = Image.open(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")).resize((128, 128))
else: im = make_background_transparent(im)
im = Image.new('RGB', (128, 128), (0, 0, 0, 0)) dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128))
dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128))
# Create the output folder if it exists
if not os.path.exists(os.path.join(base_path, str(zoom - 1), str(X))): if not os.path.exists(os.path.join(base_path, str(zoom - 1), str(X))):
try: try:
os.mkdir(os.path.join(base_path, str(zoom - 1), str(X))) os.mkdir(os.path.join(base_path, str(zoom - 1), str(X)))
except FileExistsError: except FileExistsError:
# Ignore this error, it means one other thread has already created the folder
pass pass
except Exception as e: except Exception as e:
raise e raise e
dst.save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95)
def run(map_config): # Save the image
dst.convert('RGB').save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95)
def make_background_transparent(im):
im.putalpha(255)
data = numpy.array(im)
red, green, blue, alpha = data.T
# If present, remove any "background" areas
background_areas = (red == 221) & (blue == 221) & (green == 221)
data[..., :][background_areas.T] = (0, 0, 0, 0) # make transparent
return Image.fromarray(data)
def run(map_config, port):
global tot_futs, fut_counter global tot_futs, fut_counter
with open('configs/screen_properties.yml', 'r') as sp: with open('configs/screen_properties.yml', 'r') as sp:
@@ -203,8 +225,8 @@ def run(map_config):
# Making PUT request # Making PUT request
# If the number of rows or columns is odd, we need to take the picture at the CENTER of the tile! # If the number of rows or columns is odd, we need to take the picture at the CENTER of the tile!
lat, lng = num_to_deg(XY[0] + (n_width % 2) / 2, XY[1] + (n_height % 2) / 2, zoom) lat, lng = num_to_deg(XY[0] + (n_width % 2) / 2, XY[1] + (n_height % 2) / 2, zoom)
data = json.dumps({'lat': lat, 'lng': lng, 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350)}) data = json.dumps({'lat': lat, 'lng': lng, 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350), 'mode': 'map'})
r = requests.put('http://localhost:8080', data = data) r = requests.put(f'http://127.0.0.1:{port}', data = data)
geo_data = json.loads(r.text) geo_data = json.loads(r.text)
@@ -233,7 +255,7 @@ def run(map_config):
sy = s_height / m_height sy = s_height / m_height
# Resize, rotate and save the screenshot # Resize, rotate and save the screenshot
screenshot.resize((int(sx * screenshot.width), int(sy * screenshot.height))).rotate(math.degrees(geo_data['northRotation'])).save(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"), quality=95) screenshot.resize((int(sx * screenshot.width), int(sy * screenshot.height))).rotate(math.degrees(geo_data['northRotation'])).save(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"), quality=95)
printProgressBar(n + 1, len(screenshots_XY)) printProgressBar(n + 1, len(screenshots_XY))
n += 1 n += 1