From ca1682a86ef60d669330e03b97bc96a0d69219a3 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Mon, 14 Apr 2025 09:50:34 +0300 Subject: [PATCH] add gps matcher service --- Azaion.Annotator/Annotator.xaml.cs | 7 +- Azaion.Annotator/Azaion.Annotator.csproj | 2 +- Azaion.Annotator/Controls/MapMatcher.xaml | 83 +++-- Azaion.Annotator/Controls/MapMatcher.xaml.cs | 92 +++--- Azaion.Common/Azaion.Common.csproj | 3 + Azaion.Common/Constants.cs | 2 + Azaion.Common/DTO/Config/AppConfig.cs | 4 +- Azaion.Common/DTO/Config/DirectoriesConfig.cs | 3 + Azaion.Common/DTO/DownloadTilesResult.cs | 12 + .../{GpsCsvResult.cs => GpsMatchResult.cs} | 15 +- Azaion.Common/DTO/SatTile.cs | 31 ++ Azaion.Common/Extensions/GeoUtils.cs | 39 +++ Azaion.Common/Services/GPSMatcherService.cs | 73 +++++ Azaion.Common/Services/GpsMatcherClient.cs | 118 +++++++ Azaion.Common/Services/InferenceService.cs | 8 +- Azaion.Common/Services/SatelliteDownloader.cs | 290 ++++++++++++++++++ .../DTO/ExternalClientsConfig.cs | 2 +- Azaion.CommonSecurity/DTO/RoleEnum.cs | 1 - Azaion.CommonSecurity/SecurityConstants.cs | 3 +- .../Services/AuthProvider.cs | 6 +- .../{ExternalClient.cs => InferenceClient.cs} | 35 +-- .../Services/ResourceLoader.cs | 6 +- Azaion.Test/Azaion.Test.csproj | 1 + Azaion.Test/GetTilesTest.cs | 32 ++ .../Azaion.Annotator/Azaion.Annotator.csproj | 2 +- build/publish.cmd | 8 +- 26 files changed, 759 insertions(+), 119 deletions(-) create mode 100644 Azaion.Common/DTO/DownloadTilesResult.cs rename Azaion.Common/DTO/{GpsCsvResult.cs => GpsMatchResult.cs} (76%) create mode 100644 Azaion.Common/DTO/SatTile.cs create mode 100644 Azaion.Common/Extensions/GeoUtils.cs create mode 100644 Azaion.Common/Services/GPSMatcherService.cs create mode 100644 Azaion.Common/Services/GpsMatcherClient.cs create mode 100644 Azaion.Common/Services/SatelliteDownloader.cs rename Azaion.CommonSecurity/Services/{ExternalClient.cs => InferenceClient.cs} (74%) create mode 100644 Azaion.Test/GetTilesTest.cs diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 8b9a332..9ba1775 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -52,6 +52,7 @@ public partial class Annotator private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50); private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150); + private readonly IGpsMatcherService _gpsMatcherService; private static readonly Guid SaveConfigTaskId = Guid.NewGuid(); public ObservableCollection AllMediaFiles { get; set; } = new(); @@ -70,7 +71,8 @@ public partial class Annotator ILogger logger, AnnotationService annotationService, IDbFactory dbFactory, - IInferenceService inferenceService) + IInferenceService inferenceService, + IGpsMatcherService gpsMatcherService) { InitializeComponent(); @@ -85,6 +87,7 @@ public partial class Annotator _annotationService = annotationService; _dbFactory = dbFactory; _inferenceService = inferenceService; + _gpsMatcherService = gpsMatcherService; Loaded += OnLoaded; Closed += OnFormClosed; @@ -106,7 +109,7 @@ public partial class Annotator }; Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); - MapMatcherComponent.Init(_appConfig); + MapMatcherComponent.Init(_appConfig, _gpsMatcherService); } private void OnLoaded(object sender, RoutedEventArgs e) diff --git a/Azaion.Annotator/Azaion.Annotator.csproj b/Azaion.Annotator/Azaion.Annotator.csproj index 66dcac9..4ccc008 100644 --- a/Azaion.Annotator/Azaion.Annotator.csproj +++ b/Azaion.Annotator/Azaion.Annotator.csproj @@ -13,7 +13,7 @@ - + diff --git a/Azaion.Annotator/Controls/MapMatcher.xaml b/Azaion.Annotator/Controls/MapMatcher.xaml index 798552d..22c9e35 100644 --- a/Azaion.Annotator/Controls/MapMatcher.xaml +++ b/Azaion.Annotator/Controls/MapMatcher.xaml @@ -24,30 +24,75 @@ HorizontalAlignment="Stretch" Grid.Column="0"> + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _allMediaFiles; - private Dictionary _annotations; + private Dictionary _annotations; private string _currentDir; + private IGpsMatcherService _gpsMatcherService; public MapMatcher() { InitializeComponent(); } - public void Init(AppConfig appConfig) + public void Init(AppConfig appConfig, IGpsMatcherService gpsMatcherService) { _appConfig = appConfig; + _gpsMatcherService = gpsMatcherService; GoogleMapProvider.Instance.ApiKey = appConfig.MapConfig.ApiKey; SatelliteMap.MapProvider = GMapProviders.GoogleSatelliteMap; SatelliteMap.Position = new PointLatLng(48.295985271707664, 37.14477539062501); @@ -44,7 +47,7 @@ public partial class MapMatcher : UserControl private async Task OpenGpsLocation(int gpsFilesIndex) { var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo; - var ann = _annotations.GetValueOrDefault(Path.GetFileNameWithoutExtension(media.Name)); + var ann = _annotations.GetValueOrDefault(gpsFilesIndex); GpsImageEditor.Background = new ImageBrush { ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage() @@ -91,70 +94,63 @@ public partial class MapMatcher : UserControl Path = x.FullName, MediaType = MediaTypes.Image }).ToList(); - // var allFiles = videoFiles.Concat(imageFiles).ToList(); - // - - // var labelsDict = await _dbFactory.Run(async db => await db.Annotations - // .GroupBy(x => x.Name.Substring(0, x.Name.Length - 7)) - // .Where(x => allFileNames.Contains(x.Key)) - // .ToDictionaryAsync(x => x.Key, x => x.Key)); - // - // foreach (var mediaFile in allFiles) - // mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName); - // - // AllMediaFiles = new ObservableCollection(allFiles); _allMediaFiles = mediaFiles; GpsFiles.ItemsSource = new ObservableCollection(_allMediaFiles); - var annotations = SetFromCsv(mediaFiles); - Cursor = Cursors.Wait; - await Task.Delay(TimeSpan.FromSeconds(10)); - SetMarkers(annotations); - Cursor = Cursors.Arrow; - await OpenGpsLocation(0); - } - private Dictionary SetFromCsv(List mediaFiles) - { - _annotations = mediaFiles.Select(x => new Annotation + _annotations = mediaFiles.Select((x, i) => (i, new Annotation { Name = x.Name, OriginalMediaName = x.Name - }).ToDictionary(x => Path.GetFileNameWithoutExtension(x.OriginalMediaName)); + })).ToDictionary(x => x.i, x => x.Item2); - var csvResults = GpsCsvResult.ReadFromCsv(Constants.CSV_PATH); + var initialLat = double.Parse(TbLat.Text); + var initialLon = double.Parse(TbLon.Text); + + await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon, async res => await SetMarker(res)); + } + + private async Task SetMarker(GpsMatchResult result) + { + await Dispatcher.Invoke(async () => + { + var marker = new GMapMarker(new PointLatLng(result.Latitude, result.Longitude)); + var ann = _annotations[result.Index]; + marker.Shape = new CircleVisual(marker, System.Windows.Media.Brushes.Blue) + { + Text = ann.Name + }; + SatelliteMap.Markers.Add(marker); + ann.Lat = result.Latitude; + ann.Lon = result.Longitude; + SatelliteMap.Position = new PointLatLng(result.Latitude, result.Longitude); + SatelliteMap.ZoomAndCenterMarkers(null); + }); + } + + private async Task SetFromCsv(List mediaFiles) + { + + var csvResults = GpsMatchResult.ReadFromCsv(Constants.CSV_PATH); var csvDict = csvResults .Where(x => x.MatchType == "stitched") - .ToDictionary(x => x.Image); + .ToDictionary(x => x.Index); foreach (var ann in _annotations) { var csvRes = csvDict.GetValueOrDefault(ann.Key); if (csvRes == null) continue; - - ann.Value.Lat = csvRes.Latitude; - ann.Value.Lon = csvRes.Longitude; + await SetMarker(csvRes); } - return _annotations; } - private void SetMarkers(Dictionary annotations) + private async void TestGps(object sender, RoutedEventArgs e) { - if (!annotations.Any()) + if (string.IsNullOrEmpty(TbGpsMapFolder.Text)) return; - var firstAnnotation = annotations.FirstOrDefault(); - SatelliteMap.Position = new PointLatLng(firstAnnotation.Value.Lat, firstAnnotation.Value.Lon); - foreach (var ann in annotations.Where(x => x.Value.Lat != 0 && x.Value.Lon != 0)) - { - var marker = new GMapMarker(new PointLatLng(ann.Value.Lat, ann.Value.Lon)); - var circle = new CircleVisual(marker, System.Windows.Media.Brushes.Blue) - { - Text = " " - }; - marker.Shape = circle; - SatelliteMap.Markers.Add(marker); - } - SatelliteMap.ZoomAndCenterMarkers(null); + var initialLat = double.Parse(TbLat.Text); + var initialLon = double.Parse(TbLon.Text); + await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon, async res => await SetMarker(res)); } -} \ No newline at end of file +} diff --git a/Azaion.Common/Azaion.Common.csproj b/Azaion.Common/Azaion.Common.csproj index 9cc3e5a..373e4b8 100644 --- a/Azaion.Common/Azaion.Common.csproj +++ b/Azaion.Common/Azaion.Common.csproj @@ -12,10 +12,13 @@ + + + diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index 4f8287e..1237b9a 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -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 diff --git a/Azaion.Common/DTO/Config/AppConfig.cs b/Azaion.Common/DTO/Config/AppConfig.cs index 662a14a..701170e 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -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 diff --git a/Azaion.Common/DTO/Config/DirectoriesConfig.cs b/Azaion.Common/DTO/Config/DirectoriesConfig.cs index 7813875..b36a57b 100644 --- a/Azaion.Common/DTO/Config/DirectoriesConfig.cs +++ b/Azaion.Common/DTO/Config/DirectoriesConfig.cs @@ -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!; } \ No newline at end of file diff --git a/Azaion.Common/DTO/DownloadTilesResult.cs b/Azaion.Common/DTO/DownloadTilesResult.cs new file mode 100644 index 0000000..1a2d5a1 --- /dev/null +++ b/Azaion.Common/DTO/DownloadTilesResult.cs @@ -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; } +} \ No newline at end of file diff --git a/Azaion.Common/DTO/GpsCsvResult.cs b/Azaion.Common/DTO/GpsMatchResult.cs similarity index 76% rename from Azaion.Common/DTO/GpsCsvResult.cs rename to Azaion.Common/DTO/GpsMatchResult.cs index 0b57466..c5601fb 100644 --- a/Azaion.Common/DTO/GpsCsvResult.cs +++ b/Azaion.Common/DTO/GpsMatchResult.cs @@ -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 ReadFromCsv(string csvFilePath) + public static List ReadFromCsv(string csvFilePath) { - var imageDatas = new List(); + var imageDatas = new List(); using var reader = new StreamReader(csvFilePath); //read header reader.ReadLine(); if (reader.EndOfStream) - return new List(); + return new List(); 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]), diff --git a/Azaion.Common/DTO/SatTile.cs b/Azaion.Common/DTO/SatTile.cs new file mode 100644 index 0000000..42dfb6a --- /dev/null +++ b/Azaion.Common/DTO/SatTile.cs @@ -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})]"; + } +} \ No newline at end of file diff --git a/Azaion.Common/Extensions/GeoUtils.cs b/Azaion.Common/Extensions/GeoUtils.cs new file mode 100644 index 0000000..d06257e --- /dev/null +++ b/Azaion.Common/Extensions/GeoUtils.cs @@ -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); + } +} \ No newline at end of file diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs new file mode 100644 index 0000000..cd803d6 --- /dev/null +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -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 processResult, CancellationToken detectToken = default); + void StopGpsMatching(); +} + +public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions 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 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(); + 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(); + } +} + diff --git a/Azaion.Common/Services/GpsMatcherClient.cs b/Azaion.Common/Services/GpsMatcherClient.cs new file mode 100644 index 0000000..cb8e225 --- /dev/null +++ b/Azaion.Common/Services/GpsMatcherClient.cs @@ -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.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"); + } +} \ No newline at end of file diff --git a/Azaion.Common/Services/InferenceService.cs b/Azaion.Common/Services/InferenceService.cs index 414ecf8..c66baff 100644 --- a/Azaion.Common/Services/InferenceService.cs +++ b/Azaion.Common/Services/InferenceService.cs @@ -17,20 +17,20 @@ public interface IInferenceService void StopInference(); } -public class InferenceService(ILogger logger, [FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IExternalClient externalClient, IOptions aiConfigOptions) : IInferenceService +public class InferenceService(ILogger logger, IInferenceClient client, IOptions aiConfigOptions) : IInferenceService { public async Task RunInference(List mediaPaths, Func 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 logger, [FromKeyedServic public void StopInference() { - externalClient.Send(RemoteCommand.Create(CommandType.StopInference)); + client.Send(RemoteCommand.Create(CommandType.StopInference)); } } \ No newline at end of file diff --git a/Azaion.Common/Services/SatelliteDownloader.cs b/Azaion.Common/Services/SatelliteDownloader.cs new file mode 100644 index 0000000..4660820 --- /dev/null +++ b/Azaion.Common/Services/SatelliteDownloader.cs @@ -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 logger, + IOptions mapConfig, + IOptions 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 image, DownloadTilesResult bounds, CancellationToken token = default) + { + // Calculate all crop parameters beforehand + var cropTasks = new List(); + 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 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?> 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(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 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(); + return sessionResponse?.Session; + } + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; + } + } + + private async Task 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(); + 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(); + 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 + }; + } +} \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs b/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs index d88fabd..aaf7d64 100644 --- a/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs +++ b/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs @@ -15,5 +15,5 @@ public class InferenceClientConfig : ExternalClientConfig public class GpsDeniedClientConfig : ExternalClientConfig { - public int ZeroMqReceiverPort { get; set; } + public int ZeroMqSubscriberPort { get; set; } } \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/RoleEnum.cs b/Azaion.CommonSecurity/DTO/RoleEnum.cs index ea8a062..548d5e0 100644 --- a/Azaion.CommonSecurity/DTO/RoleEnum.cs +++ b/Azaion.CommonSecurity/DTO/RoleEnum.cs @@ -9,7 +9,6 @@ public enum RoleEnum Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue. CompanionPC = 30, Admin = 40, // - ResourceUploader = 50, //Uploading dll and ai models ApiAdmin = 1000 //everything } diff --git a/Azaion.CommonSecurity/SecurityConstants.cs b/Azaion.CommonSecurity/SecurityConstants.cs index 75d06cc..da0bffc 100644 --- a/Azaion.CommonSecurity/SecurityConstants.cs +++ b/Azaion.CommonSecurity/SecurityConstants.cs @@ -10,7 +10,8 @@ public class SecurityConstants #region ExternalClientsConfig public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe"; - public const string EXTERNAL_GPS_DENIED_PATH = "image-matcher.exe"; + public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied"; + public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe"); public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1"; public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227; diff --git a/Azaion.CommonSecurity/Services/AuthProvider.cs b/Azaion.CommonSecurity/Services/AuthProvider.cs index 60dcb72..ebc5d0d 100644 --- a/Azaion.CommonSecurity/Services/AuthProvider.cs +++ b/Azaion.CommonSecurity/Services/AuthProvider.cs @@ -10,14 +10,14 @@ public interface IAuthProvider User CurrentUser { get; } } -public class AuthProvider([FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IExternalClient externalClient) : IAuthProvider +public class AuthProvider(IInferenceClient inferenceClient) : IAuthProvider { public User CurrentUser { get; private set; } = null!; public void Login(ApiCredentials credentials) { - externalClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); - var user = externalClient.Get(); + inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); + var user = inferenceClient.Get(); if (user == null) throw new Exception("Can't get user from Auth provider"); diff --git a/Azaion.CommonSecurity/Services/ExternalClient.cs b/Azaion.CommonSecurity/Services/InferenceClient.cs similarity index 74% rename from Azaion.CommonSecurity/Services/ExternalClient.cs rename to Azaion.CommonSecurity/Services/InferenceClient.cs index cd4e0d7..401243a 100644 --- a/Azaion.CommonSecurity/Services/ExternalClient.cs +++ b/Azaion.CommonSecurity/Services/InferenceClient.cs @@ -9,27 +9,23 @@ using NetMQ.Sockets; namespace Azaion.CommonSecurity.Services; -public interface IExternalClient +public interface IInferenceClient { - void Stop(); - void Send(RemoteCommand create); T? Get(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class; byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default); + void Stop(); } -public abstract class BaseZeroMqExternalClient : IExternalClient +public class InferenceClient : IInferenceClient { private readonly DealerSocket _dealer = new(); private readonly Guid _clientId = Guid.NewGuid(); + private readonly InferenceClientConfig _inferenceClientConfig; - private readonly ExternalClientConfig _externalClientConfig; - - protected abstract string ClientPath { get; } - - protected BaseZeroMqExternalClient(ExternalClientConfig config) + public InferenceClient(IOptions config) { - _externalClientConfig = config; + _inferenceClientConfig = config.Value; Start(); } @@ -40,7 +36,7 @@ public abstract class BaseZeroMqExternalClient : IExternalClient using var process = new Process(); process.StartInfo = new ProcessStartInfo { - FileName = ClientPath, + FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH, //Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}", //RedirectStandardOutput = true, //RedirectStandardError = true, @@ -58,7 +54,7 @@ public abstract class BaseZeroMqExternalClient : IExternalClient } _dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N")); - _dealer.Connect($"tcp://{_externalClientConfig.ZeroMqHost}:{_externalClientConfig.ZeroMqPort}"); + _dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}"); } public void Stop() @@ -75,6 +71,9 @@ public abstract class BaseZeroMqExternalClient : IExternalClient _dealer.SendFrame(MessagePackSerializer.Serialize(command)); } + public void SendString(string text) => + Send(new RemoteCommand(CommandType.Load, MessagePackSerializer.Serialize(text))); + public T? Get(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class { var bytes = GetBytes(retries, tryTimeoutSeconds, ct); @@ -98,15 +97,3 @@ public abstract class BaseZeroMqExternalClient : IExternalClient return null; } } - -public class InferenceExternalClient(IOptions inferenceClientConfig) - : BaseZeroMqExternalClient(inferenceClientConfig.Value) -{ - protected override string ClientPath => SecurityConstants.EXTERNAL_INFERENCE_PATH; -} - -public class GpsDeniedExternalClient(IOptions gpsDeniedClientConfig) - : BaseZeroMqExternalClient(gpsDeniedClientConfig.Value) -{ - protected override string ClientPath => SecurityConstants.EXTERNAL_GPS_DENIED_PATH; -} diff --git a/Azaion.CommonSecurity/Services/ResourceLoader.cs b/Azaion.CommonSecurity/Services/ResourceLoader.cs index 860bced..7929ad1 100644 --- a/Azaion.CommonSecurity/Services/ResourceLoader.cs +++ b/Azaion.CommonSecurity/Services/ResourceLoader.cs @@ -8,12 +8,12 @@ public interface IResourceLoader MemoryStream LoadFile(string fileName, string? folder = null); } -public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IExternalClient externalClient) : IResourceLoader +public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IInferenceClient inferenceClient) : IResourceLoader { public MemoryStream LoadFile(string fileName, string? folder = null) { - externalClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder))); - var bytes = externalClient.GetBytes(); + inferenceClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder))); + var bytes = inferenceClient.GetBytes(); if (bytes == null) throw new Exception($"Unable to receive {fileName}"); diff --git a/Azaion.Test/Azaion.Test.csproj b/Azaion.Test/Azaion.Test.csproj index 6d937bd..e0cc5b7 100644 --- a/Azaion.Test/Azaion.Test.csproj +++ b/Azaion.Test/Azaion.Test.csproj @@ -15,6 +15,7 @@ + diff --git a/Azaion.Test/GetTilesTest.cs b/Azaion.Test/GetTilesTest.cs new file mode 100644 index 0000000..d27e8d3 --- /dev/null +++ b/Azaion.Test/GetTilesTest.cs @@ -0,0 +1,32 @@ +using Azaion.Common.DTO.Config; +using Azaion.Common.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Azaion.Annotator.Test; + +public class GetTilesTestClass +{ + [Fact] + public async Task GetTilesTest() + { + var services = new ServiceCollection(); + services.AddHttpClient(); + var provider = services.BuildServiceProvider(); + var httpClientFactory = provider.GetService()!; + + var satelliteDownloader = new SatelliteDownloader(Mock.Of>(), new OptionsWrapper(new MapConfig + { + Service = "GoogleMaps", + ApiKey = "AIzaSyAXRBDBOskC5QOHG6VJWzmVJwYKcu6WH8k" + }), new OptionsWrapper(new DirectoriesConfig + { + GpsSatDirectory = "satelliteMaps" + }), httpClientFactory); + + await satelliteDownloader.GetTiles(48.2748909, 37.3834877, 600, 18); + } +} \ No newline at end of file diff --git a/Dummy/Azaion.Annotator/Azaion.Annotator.csproj b/Dummy/Azaion.Annotator/Azaion.Annotator.csproj index 0ce98e0..0005c54 100644 --- a/Dummy/Azaion.Annotator/Azaion.Annotator.csproj +++ b/Dummy/Azaion.Annotator/Azaion.Annotator.csproj @@ -12,7 +12,7 @@ - + diff --git a/build/publish.cmd b/build/publish.cmd index b9af0bb..1a6b131 100644 --- a/build/publish.cmd +++ b/build/publish.cmd @@ -76,9 +76,11 @@ copy logo.ico dist\ echo Copying cudnn files set cudnn-folder="C:\Program Files\NVIDIA\CUDNN\v9.4\bin\12.6" copy %cudnn-folder%\* dist\* -@REM dont need +@REM don't need del dist\cudnn_adv64_9.dll +cd Azaion.Suite +call gps-denied-deploy ..\dist\gps-denied -echo building installer... -iscc build\installer.iss +@REM echo building installer... +@REM iscc build\installer.iss