mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 11:16:30 +00:00
add gps matcher service
This commit is contained in:
@@ -12,10 +12,13 @@
|
||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Polly" Version="8.5.2" />
|
||||
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ public class Constants
|
||||
public const string DEFAULT_IMAGES_DIR = "images";
|
||||
public const string DEFAULT_RESULTS_DIR = "results";
|
||||
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
|
||||
public const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
|
||||
public const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ public class ConfigUpdater : IConfigUpdater
|
||||
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
|
||||
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
|
||||
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
|
||||
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR
|
||||
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR,
|
||||
GpsSatDirectory = Constants.DEFAULT_GPS_SAT_DIRECTORY,
|
||||
GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY
|
||||
},
|
||||
|
||||
ThumbnailConfig = new ThumbnailConfig
|
||||
|
||||
@@ -7,4 +7,7 @@ public class DirectoriesConfig
|
||||
public string ImagesDirectory { get; set; } = null!;
|
||||
public string ResultsDirectory { get; set; } = null!;
|
||||
public string ThumbnailsDirectory { get; set; } = null!;
|
||||
|
||||
public string GpsSatDirectory { get; set; } = null!;
|
||||
public string GpsRouteDirectory { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class DownloadTilesResult
|
||||
{
|
||||
public ConcurrentDictionary<(int x, int y), byte[]> Tiles { get; set; } = null!;
|
||||
public double LatMin { get; set; }
|
||||
public double LatMax { get; set; }
|
||||
public double LonMin { get; set; }
|
||||
public double LonMax { get; set; }
|
||||
}
|
||||
@@ -2,24 +2,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
public class GpsCsvResult
|
||||
public class GpsMatchResult
|
||||
{
|
||||
public string Image { get; set; }
|
||||
public double Latitude { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string Image { get; set; }
|
||||
public double Latitude { get; set; }
|
||||
public double Longitude { get; set; }
|
||||
public int Keypoints { get; set; }
|
||||
public int Rotation { get; set; }
|
||||
public string MatchType { get; set; }
|
||||
|
||||
public static List<GpsCsvResult> ReadFromCsv(string csvFilePath)
|
||||
public static List<GpsMatchResult> ReadFromCsv(string csvFilePath)
|
||||
{
|
||||
var imageDatas = new List<GpsCsvResult>();
|
||||
var imageDatas = new List<GpsMatchResult>();
|
||||
|
||||
using var reader = new StreamReader(csvFilePath);
|
||||
//read header
|
||||
reader.ReadLine();
|
||||
if (reader.EndOfStream)
|
||||
return new List<GpsCsvResult>();
|
||||
return new List<GpsMatchResult>();
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
@@ -29,7 +30,7 @@ public class GpsCsvResult
|
||||
var values = line.Split(',');
|
||||
if (values.Length == 6)
|
||||
{
|
||||
imageDatas.Add(new GpsCsvResult
|
||||
imageDatas.Add(new GpsMatchResult
|
||||
{
|
||||
Image = GetFilename(values[0]),
|
||||
Latitude = double.Parse(values[1]),
|
||||
@@ -0,0 +1,31 @@
|
||||
using Azaion.Common.Extensions;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class SatTile
|
||||
{
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public double LeftTopLat { get; }
|
||||
public double LeftTopLon { get; }
|
||||
|
||||
public double BottomRightLat { get; }
|
||||
public double BottomRightLon { get; }
|
||||
public string Url { get; set; }
|
||||
|
||||
|
||||
public SatTile(int x, int y, int zoom, string url)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Url = url;
|
||||
|
||||
(LeftTopLat, LeftTopLon) = GeoUtils.TileToWorldPos(x, y, zoom);
|
||||
(BottomRightLat, BottomRightLon) = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Tile[X={X}, Y={Y}, TL=({LeftTopLat:F6}, {LeftTopLon:F6}), BR=({BottomRightLat:F6}, {BottomRightLon:F6})]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace Azaion.Common.Extensions;
|
||||
|
||||
public static class GeoUtils
|
||||
{
|
||||
private const double EARTH_RADIUS = 6378137;
|
||||
|
||||
public static (int x, int y) WorldToTilePos(double lat, double lon, int zoom)
|
||||
{
|
||||
var latRad = lat * Math.PI / 180.0;
|
||||
var n = Math.Pow(2.0, zoom);
|
||||
var xTile = (int)Math.Floor((lon + 180.0) / 360.0 * n);
|
||||
var yTile = (int)Math.Floor((1.0 - Math.Log(Math.Tan(latRad) + 1.0 / Math.Cos(latRad)) / Math.PI) / 2.0 * n);
|
||||
return (xTile, yTile);
|
||||
}
|
||||
|
||||
public static (double lat, double lon) TileToWorldPos(int x, int y, int zoom)
|
||||
{
|
||||
var n = Math.Pow(2.0, zoom);
|
||||
var lonDeg = x / n * 360.0 - 180.0;
|
||||
var latRad = Math.Atan(Math.Sinh(Math.PI * (1.0 - 2.0 * y / n)));
|
||||
var latDeg = latRad * 180.0 / Math.PI;
|
||||
return (latDeg, lonDeg);
|
||||
}
|
||||
|
||||
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(double centerLat, double centerLon, double radiusM)
|
||||
{
|
||||
var latRad = centerLat * Math.PI / 180.0;
|
||||
|
||||
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
|
||||
var minLat = Math.Max(centerLat - latDiff, -90.0);
|
||||
var maxLat = Math.Min(centerLat + latDiff, 90.0);
|
||||
|
||||
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
|
||||
var minLon = Math.Max(centerLon - lonDiff, -180.0);
|
||||
var maxLon = Math.Min(centerLon + lonDiff, 180.0);
|
||||
|
||||
return (minLat, maxLat, minLon, maxLon);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.CommonSecurity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public interface IGpsMatcherService
|
||||
{
|
||||
Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default);
|
||||
void StopGpsMatching();
|
||||
}
|
||||
|
||||
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService
|
||||
{
|
||||
private const int ZOOM_LEVEL = 18;
|
||||
private const int POINTS_COUNT = 10;
|
||||
private const int DISTANCE_BETWEEN_POINTS_M = 100;
|
||||
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
|
||||
|
||||
public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default)
|
||||
{
|
||||
var currentLat = initialLatitude;
|
||||
var currentLon = initialLongitude;
|
||||
|
||||
var routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, dirConfig.Value.GpsRouteDirectory);
|
||||
if (Directory.Exists(routeDir))
|
||||
Directory.Delete(routeDir, true);
|
||||
Directory.CreateDirectory(routeDir);
|
||||
|
||||
var routeFiles = new List<string>();
|
||||
foreach (var file in Directory.GetFiles(userRouteDir))
|
||||
{
|
||||
routeFiles.Add(file);
|
||||
File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file)));
|
||||
}
|
||||
|
||||
var indexOffset = 0;
|
||||
while (routeFiles.Any())
|
||||
{
|
||||
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken);
|
||||
gpsMatcherClient.StartMatching(new StartMatchingEvent
|
||||
{
|
||||
ImagesCount = POINTS_COUNT,
|
||||
Latitude = initialLatitude,
|
||||
Longitude = initialLongitude,
|
||||
SatelliteImagesDir = dirConfig.Value.GpsSatDirectory,
|
||||
RouteDir = dirConfig.Value.GpsRouteDirectory
|
||||
});
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = gpsMatcherClient.GetResult();
|
||||
if (result == null)
|
||||
break;
|
||||
result.Index += indexOffset;
|
||||
await processResult(result);
|
||||
currentLat = result.Latitude;
|
||||
currentLon = result.Longitude;
|
||||
routeFiles.RemoveAt(0);
|
||||
}
|
||||
indexOffset += POINTS_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopGpsMatching()
|
||||
{
|
||||
gpsMatcherClient.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Diagnostics;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.CommonSecurity;
|
||||
using Azaion.CommonSecurity.DTO;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NetMQ;
|
||||
using NetMQ.Sockets;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public interface IGpsMatcherClient
|
||||
{
|
||||
|
||||
void StartMatching(StartMatchingEvent startEvent);
|
||||
GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default);
|
||||
void Stop();
|
||||
}
|
||||
|
||||
public class StartMatchingEvent
|
||||
{
|
||||
public string RouteDir { get; set; } = null!;
|
||||
public string SatelliteImagesDir { get; set; } = null!;
|
||||
public int ImagesCount { get; set; }
|
||||
public double Latitude { get; set; }
|
||||
public double Longitude { get; set; }
|
||||
public string ProcessingType { get; set; } = "cuda";
|
||||
public int Altitude { get; set; } = 400;
|
||||
public double CameraSensorWidth { get; set; } = 23.5;
|
||||
public double CameraFocalLength { get; set; } = 24;
|
||||
|
||||
public override string ToString() =>
|
||||
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
|
||||
}
|
||||
|
||||
public class GpsMatcherClient : IGpsMatcherClient
|
||||
{
|
||||
private readonly GpsDeniedClientConfig _gpsDeniedClientConfig;
|
||||
private readonly RequestSocket _requestSocket = new();
|
||||
private readonly SubscriberSocket _subscriberSocket = new();
|
||||
|
||||
public GpsMatcherClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
|
||||
{
|
||||
_gpsDeniedClientConfig = gpsDeniedClientConfig.Value;
|
||||
Start();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = SecurityConstants.ExternalGpsDeniedPath,
|
||||
WorkingDirectory = SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER
|
||||
//Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}",
|
||||
//RedirectStandardOutput = true,
|
||||
//RedirectStandardError = true,
|
||||
//CreateNoWindow = true
|
||||
};
|
||||
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
||||
//process.Start();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
//throw;
|
||||
}
|
||||
_requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}");
|
||||
_subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}");
|
||||
_subscriberSocket.Subscribe("");
|
||||
}
|
||||
|
||||
public void StartMatching(StartMatchingEvent e)
|
||||
{
|
||||
_requestSocket.SendFrame(e.ToString());
|
||||
var response = _requestSocket.ReceiveFrameString();
|
||||
if (response != "OK")
|
||||
throw new Exception("Start Matching Failed");
|
||||
}
|
||||
|
||||
public GpsMatchResult? GetResult(int retries = 15, int tryTimeoutSeconds = 5, CancellationToken ct = default)
|
||||
{
|
||||
var tryNum = 0;
|
||||
while (!ct.IsCancellationRequested && tryNum++ < retries)
|
||||
{
|
||||
if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update))
|
||||
continue;
|
||||
if (update == "FINISHED")
|
||||
return null;
|
||||
|
||||
var parts = update.Split(',');
|
||||
if (parts.Length != 5)
|
||||
throw new Exception("Matching Result Failed");
|
||||
|
||||
return new GpsMatchResult
|
||||
{
|
||||
Index = int.Parse(parts[0]),
|
||||
Image = parts[1],
|
||||
Latitude = double.Parse(parts[2]),
|
||||
Longitude = double.Parse(parts[3]),
|
||||
MatchType = parts[4]
|
||||
};
|
||||
}
|
||||
|
||||
if (!ct.IsCancellationRequested)
|
||||
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_requestSocket.SendFrame("STOP");
|
||||
}
|
||||
}
|
||||
@@ -17,20 +17,20 @@ public interface IInferenceService
|
||||
void StopInference();
|
||||
}
|
||||
|
||||
public class InferenceService(ILogger<InferenceService> logger, [FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IExternalClient externalClient, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService
|
||||
public class InferenceService(ILogger<InferenceService> logger, IInferenceClient client, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService
|
||||
{
|
||||
public async Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default)
|
||||
{
|
||||
var aiConfig = aiConfigOptions.Value;
|
||||
|
||||
aiConfig.Paths = mediaPaths;
|
||||
externalClient.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
|
||||
client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
|
||||
|
||||
while (!detectToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = externalClient.GetBytes(ct: detectToken);
|
||||
var bytes = client.GetBytes(ct: detectToken);
|
||||
if (bytes == null)
|
||||
throw new Exception("Can't get bytes from inference client");
|
||||
|
||||
@@ -51,6 +51,6 @@ public class InferenceService(ILogger<InferenceService> logger, [FromKeyedServic
|
||||
|
||||
public void StopInference()
|
||||
{
|
||||
externalClient.Send(RemoteCommand.Create(CommandType.StopInference));
|
||||
client.Send(RemoteCommand.Create(CommandType.StopInference));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.Extensions;
|
||||
using Azaion.CommonSecurity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public interface ISatelliteDownloader
|
||||
{
|
||||
Task GetTiles(double latitude, double longitude, double radiusM, int zoomLevel, CancellationToken token = default);
|
||||
}
|
||||
|
||||
public class SatelliteDownloader(
|
||||
ILogger<SatelliteDownloader> logger,
|
||||
IOptions<MapConfig> mapConfig,
|
||||
IOptions<DirectoriesConfig> directoriesConfig,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
: ISatelliteDownloader
|
||||
{
|
||||
private const int INPUT_TILE_SIZE = 256;
|
||||
|
||||
private const string TILE_URL_TEMPLATE = "https://mt{0}.google.com/vt/lyrs=s&x={1}&y={2}&z={3}&token={4}";
|
||||
private const int NUM_SERVERS = 4;
|
||||
|
||||
private const int CROP_WIDTH = 1024;
|
||||
private const int CROP_HEIGHT = 1024;
|
||||
private const int STEP_X = 300;
|
||||
private const int STEP_Y = 300;
|
||||
private const int OUTPUT_TILE_SIZE = 512;
|
||||
|
||||
private readonly string _apiKey = mapConfig.Value.ApiKey;
|
||||
private readonly string _satDirectory = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, directoriesConfig.Value.GpsSatDirectory);
|
||||
|
||||
public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
|
||||
{
|
||||
//empty Satellite directory
|
||||
if (Directory.Exists(_satDirectory))
|
||||
Directory.Delete(_satDirectory, true);
|
||||
Directory.CreateDirectory(_satDirectory);
|
||||
|
||||
var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token);
|
||||
var image = await ComposeTiles(downloadTilesResult.Tiles, token);
|
||||
if (image != null)
|
||||
await SplitToTiles(image, downloadTilesResult, token);
|
||||
}
|
||||
|
||||
private async Task SplitToTiles(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
|
||||
{
|
||||
// Calculate all crop parameters beforehand
|
||||
var cropTasks = new List<Action>();
|
||||
var latRange = bounds.LatMax - bounds.LatMin; // [cite: 13]
|
||||
var lonRange = bounds.LonMax - bounds.LonMin; // [cite: 13]
|
||||
var degreesPerPixelLat = latRange / image.Height; // [cite: 13]
|
||||
var degreesPerPixelLon = lonRange / image.Width; // [cite: 14]
|
||||
int tempRowIndex = 0;
|
||||
|
||||
|
||||
for (int top = 0; top <= image.Height - CROP_HEIGHT; top += STEP_Y) // [cite: 15]
|
||||
{
|
||||
int tempColIndex = 0;
|
||||
for (int left = 0; left <= image.Width - CROP_WIDTH; left += STEP_X) // [cite: 16]
|
||||
{
|
||||
// Capture loop variables for the closure
|
||||
int currentTop = top;
|
||||
int currentLeft = left;
|
||||
int rowIndex = tempRowIndex;
|
||||
int colIndex = tempColIndex;
|
||||
|
||||
cropTasks.Add(() =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var cropBox = new Rectangle(currentLeft, currentTop, CROP_WIDTH, CROP_HEIGHT);
|
||||
|
||||
using var croppedImage = image.Clone(ctx => ctx.Crop(cropBox));
|
||||
var cropTlLat = bounds.LatMax - (currentTop * degreesPerPixelLat);
|
||||
var cropTlLon = bounds.LonMin + (currentLeft * degreesPerPixelLon);
|
||||
var cropBrLat = cropTlLat - (CROP_HEIGHT * degreesPerPixelLat);
|
||||
var cropBrLon = cropTlLon + (CROP_WIDTH * degreesPerPixelLon);
|
||||
|
||||
var outputFilename = Path.Combine(_satDirectory,
|
||||
$"map_{rowIndex:D4}_{colIndex:D4}_tl_{cropTlLat:F6}_{cropTlLon:F6}_br_{cropBrLat:F6}_{cropBrLon:F6}.tif"
|
||||
);
|
||||
|
||||
using var resizedImage = croppedImage.Clone(ctx => ctx.Resize(OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE, KnownResamplers.Lanczos3));
|
||||
resizedImage.SaveAsTiffAsync(outputFilename, token).GetAwaiter().GetResult(); // Use synchronous saving or manage async Tasks properly in parallel context
|
||||
});
|
||||
tempColIndex++;
|
||||
}
|
||||
tempRowIndex++;
|
||||
}
|
||||
// Execute tasks in parallel
|
||||
await Task.Run(() => Parallel.ForEach(cropTasks, action => action()), token);
|
||||
}
|
||||
|
||||
private async Task SplitToTiles_OLD(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(image);
|
||||
ArgumentNullException.ThrowIfNull(bounds);
|
||||
|
||||
if (bounds.LatMax <= bounds.LatMin || bounds.LonMax <= bounds.LonMin || image.Width <= 0 || image.Height <= 0)
|
||||
throw new ArgumentException("Invalid coordinate bounds (LatMax <= LatMin or LonMax <= LonMin) or image dimensions (Width/Height <= 0).");
|
||||
|
||||
var latRange = bounds.LatMax - bounds.LatMin;
|
||||
var lonRange = bounds.LonMax - bounds.LonMin;
|
||||
var degreesPerPixelLat = latRange / image.Height;
|
||||
var degreesPerPixelLon = lonRange / image.Width;
|
||||
|
||||
var rowIndex = 0;
|
||||
for (int top = 0; top <= image.Height - CROP_HEIGHT; top += STEP_Y)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
int colIndex = 0;
|
||||
for (int left = 0; left <= image.Width - CROP_WIDTH; left += STEP_X)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var cropBox = new Rectangle(left, top, CROP_WIDTH, CROP_HEIGHT);
|
||||
|
||||
using (var croppedImage = image.Clone(ctx => ctx.Crop(cropBox)))
|
||||
{
|
||||
var cropTlLat = bounds.LatMax - (top * degreesPerPixelLat);
|
||||
var cropTlLon = bounds.LonMin + (left * degreesPerPixelLon);
|
||||
var cropBrLat = cropTlLat - (CROP_HEIGHT * degreesPerPixelLat);
|
||||
var cropBrLon = cropTlLon + (CROP_WIDTH * degreesPerPixelLon);
|
||||
|
||||
var outputFilename = Path.Combine(_satDirectory,
|
||||
$"map_{rowIndex:D4}_{colIndex:D4}_tl_{cropTlLat:F6}_{cropTlLon:F6}_br_{cropBrLat:F6}_{cropBrLon:F6}.tif"
|
||||
);
|
||||
|
||||
using (var resizedImage = croppedImage.Clone(ctx => ctx.Resize(OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE, KnownResamplers.Lanczos3)))
|
||||
await resizedImage.SaveAsTiffAsync(outputFilename, token);
|
||||
}
|
||||
colIndex++;
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<Image<Rgba32>?> ComposeTiles(ConcurrentDictionary<(int x, int y), byte[]> downloadedTiles, CancellationToken token = default)
|
||||
{
|
||||
if (downloadedTiles.IsEmpty)
|
||||
return null;
|
||||
|
||||
var xMin = downloadedTiles.Min(t => t.Key.x);
|
||||
var xMax = downloadedTiles.Max(t => t.Key.x);
|
||||
var yMin = downloadedTiles.Min(t => t.Key.y);
|
||||
var yMax = downloadedTiles.Max(t => t.Key.y);
|
||||
|
||||
var totalWidth = (xMax - xMin + 1) * INPUT_TILE_SIZE;
|
||||
var totalHeight = (yMax - yMin + 1) * INPUT_TILE_SIZE;
|
||||
|
||||
if (totalWidth <= 0 || totalHeight <= 0)
|
||||
return null;
|
||||
|
||||
var largeImage = new Image<Rgba32>(totalWidth, totalHeight);
|
||||
|
||||
largeImage.Mutate(ctx =>
|
||||
{
|
||||
for (var y = yMin; y <= yMax; y++)
|
||||
{
|
||||
for (var x = xMin; x <= xMax; x++)
|
||||
{
|
||||
if (!downloadedTiles.TryGetValue((x, y), out var tileData))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
using var tileImage = Image.Load(tileData);
|
||||
var offsetX = (x - xMin) * INPUT_TILE_SIZE;
|
||||
var offsetY = (y - yMin) * INPUT_TILE_SIZE;
|
||||
ctx.DrawImage(tileImage, new Point(offsetX, offsetY), 1f);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine($"Error while loading tile: {tileData}");
|
||||
}
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// await largeImage.SaveAsync(Path.Combine(_satDirectory, "full_map.tif"),
|
||||
// new TiffEncoder { Compression = TiffCompression.Deflate }, token);
|
||||
return largeImage;
|
||||
}
|
||||
|
||||
private record SessionResponse(string Session);
|
||||
|
||||
private async Task<string?> GetSessionToken()
|
||||
{
|
||||
var url = $"https://tile.googleapis.com/v1/createSession?key={_apiKey}";
|
||||
using var httpClient = httpClientFactory.CreateClient();
|
||||
try
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(new { mapType = "satellite" });
|
||||
var response = await httpClient.PostAsync(url, new StringContent(str));
|
||||
response.EnsureSuccessStatusCode();
|
||||
var sessionResponse = await response.Content.ReadFromJsonAsync<SessionResponse>();
|
||||
return sessionResponse?.Session;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DownloadTilesResult> DownloadTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
|
||||
{
|
||||
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerLat, centerLon, radiusM);
|
||||
|
||||
var (xMin, yMin) = GeoUtils.WorldToTilePos(latMax, lonMin, zoomLevel); // Top-left corner
|
||||
var (xMax, yMax) = GeoUtils.WorldToTilePos(latMin, lonMax, zoomLevel); // Bottom-right corner
|
||||
|
||||
var tilesToDownload = new ConcurrentQueue<SatTile>();
|
||||
var downloadedTiles = new ConcurrentDictionary<(int x, int y), byte[]>();
|
||||
var server = 0;
|
||||
var sessionToken = await GetSessionToken();
|
||||
|
||||
for (var y = yMin; y <= yMax + 1; y++)
|
||||
for (var x = xMin; x <= xMax + 1; x++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var url = string.Format(TILE_URL_TEMPLATE, server, x, y, zoomLevel, sessionToken);
|
||||
|
||||
tilesToDownload.Enqueue(new SatTile(x, y, zoomLevel, url));
|
||||
server = (server + 1) % NUM_SERVERS;
|
||||
}
|
||||
|
||||
var downloadTasks = new List<Task>();
|
||||
int downloadedCount = 0;
|
||||
|
||||
|
||||
for (int i = 0; i < NUM_SERVERS; i++)
|
||||
{
|
||||
downloadTasks.Add(Task.Run(async () =>
|
||||
{
|
||||
using var httpClient = httpClientFactory.CreateClient();
|
||||
|
||||
while (tilesToDownload.TryDequeue(out var tileInfo))
|
||||
{
|
||||
if (token.IsCancellationRequested) break;
|
||||
try
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36");
|
||||
var response = await httpClient.GetAsync(tileInfo.Url, token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var tileData = await response.Content.ReadAsByteArrayAsync(token);
|
||||
if (tileData?.Length > 0)
|
||||
{
|
||||
downloadedTiles.TryAdd((tileInfo.X, tileInfo.Y), tileData);
|
||||
Interlocked.Increment(ref downloadedCount);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
logger.LogError(requestException, $"Fail to download tile! Url: {tileInfo.Url}. {requestException.Message}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, $"Fail to download tile! {e.Message}");
|
||||
}
|
||||
}
|
||||
}, token));
|
||||
}
|
||||
|
||||
await Task.WhenAll(downloadTasks);
|
||||
return new DownloadTilesResult
|
||||
{
|
||||
Tiles = downloadedTiles,
|
||||
LatMin = latMin,
|
||||
LatMax = latMax,
|
||||
LonMin = lonMin,
|
||||
LonMax = lonMax
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user