add gps matcher service

This commit is contained in:
Alex Bezdieniezhnykh
2025-04-14 09:50:34 +03:00
parent 36b3bf1712
commit ca1682a86e
26 changed files with 759 additions and 119 deletions
+5 -2
View File
@@ -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<MediaFileInfo> AllMediaFiles { get; set; } = new();
@@ -70,7 +71,8 @@ public partial class Annotator
ILogger<Annotator> 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)
+1 -1
View File
@@ -13,7 +13,7 @@
<PackageReference Include="LibVLCSharp" Version="3.9.1" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
+49 -4
View File
@@ -24,13 +24,18 @@
HorizontalAlignment="Stretch"
Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="30"/>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="32"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
@@ -45,9 +50,49 @@
Click="OpenGpsTilesFolderClick">
. . .
</Button>
<ListView Grid.Row="1"
</Grid>
<Grid
Grid.Row="1"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<!-- <TextBlock -->
<!-- Grid.Column="0" -->
<!-- Text="Lat" -->
<!-- Background="Gray"/> -->
<Button
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="1"
Click="TestGps">
Test
</Button>
<TextBox
Grid.Column="1"
HorizontalAlignment="Stretch"
x:Name="TbLat"
Text="48.2748909"></TextBox>
</Grid>
<Grid
Grid.Row="2"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Text="Lon"
Background="Gray"/>
<TextBox
Grid.Column="1"
HorizontalAlignment="Stretch"
x:Name="TbLon"
Text="37.3834877"></TextBox>
</Grid>
<ListView Grid.Row="3"
Name="GpsFiles"
Background="Black"
SelectedItem="{Binding Path=SelectedVideo}"
+43 -47
View File
@@ -11,6 +11,7 @@ using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Azaion.Common.Services;
using GMap.NET;
using GMap.NET.MapProviders;
using GMap.NET.WindowsPresentation;
@@ -22,17 +23,19 @@ public partial class MapMatcher : UserControl
{
private AppConfig _appConfig;
List<MediaFileInfo> _allMediaFiles;
private Dictionary<string, Annotation> _annotations;
private Dictionary<int, Annotation> _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<MediaFileInfo>(allFiles);
_allMediaFiles = mediaFiles;
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
var annotations = SetFromCsv(mediaFiles);
Cursor = Cursors.Wait;
await Task.Delay(TimeSpan.FromSeconds(10));
SetMarkers(annotations);
Cursor = Cursors.Arrow;
await OpenGpsLocation(0);
}
private Dictionary<string, Annotation> SetFromCsv(List<MediaFileInfo> 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<MediaFileInfo> 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<string, Annotation> 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));
}
}
+3
View File
@@ -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>
+2
View File
@@ -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
+3 -1
View File
@@ -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!;
}
+12
View File
@@ -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,8 +2,9 @@
using System.Collections.Generic;
using System.IO;
public class GpsCsvResult
public class GpsMatchResult
{
public int Index { get; set; }
public string Image { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
@@ -11,15 +12,15 @@ public class GpsCsvResult
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]),
+31
View File
@@ -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})]";
}
}
+39
View File
@@ -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();
}
}
+118
View File
@@ -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");
}
}
+4 -4
View File
@@ -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
};
}
}
@@ -15,5 +15,5 @@ public class InferenceClientConfig : ExternalClientConfig
public class GpsDeniedClientConfig : ExternalClientConfig
{
public int ZeroMqReceiverPort { get; set; }
public int ZeroMqSubscriberPort { get; set; }
}
-1
View File
@@ -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
}
+2 -1
View File
@@ -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;
@@ -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<User>();
inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials));
var user = inferenceClient.Get<User>();
if (user == null)
throw new Exception("Can't get user from Auth provider");
@@ -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<T>(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<InferenceClientConfig> 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<T>(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> inferenceClientConfig)
: BaseZeroMqExternalClient(inferenceClientConfig.Value)
{
protected override string ClientPath => SecurityConstants.EXTERNAL_INFERENCE_PATH;
}
public class GpsDeniedExternalClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
: BaseZeroMqExternalClient(gpsDeniedClientConfig.Value)
{
protected override string ClientPath => SecurityConstants.EXTERNAL_GPS_DENIED_PATH;
}
@@ -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}");
+1
View File
@@ -15,6 +15,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.8.0" />
</ItemGroup>
+32
View File
@@ -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<IHttpClientFactory>()!;
var satelliteDownloader = new SatelliteDownloader(Mock.Of<ILogger<SatelliteDownloader>>(), new OptionsWrapper<MapConfig>(new MapConfig
{
Service = "GoogleMaps",
ApiKey = "AIzaSyAXRBDBOskC5QOHG6VJWzmVJwYKcu6WH8k"
}), new OptionsWrapper<DirectoriesConfig>(new DirectoriesConfig
{
GpsSatDirectory = "satelliteMaps"
}), httpClientFactory);
await satelliteDownloader.GetTiles(48.2748909, 37.3834877, 600, 18);
}
}
@@ -12,7 +12,7 @@
<PackageReference Include="LibVLCSharp" Version="3.9.1" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
+5 -3
View File
@@ -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