From f58dd3d04f2db14566845f4f273bf1b1d9ee4d3a Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Mon, 23 Jun 2025 20:47:28 +0300 Subject: [PATCH] switcher dataset explorer lat lon -> geopoint correct location for gps if small keypoints number --- Azaion.Annotator/Annotator.xaml.cs | 13 +--- Azaion.Annotator/AnnotatorEventHandler.cs | 31 +++++---- Azaion.Annotator/Controls/MapMatcher.xaml.cs | 11 ++-- Azaion.Common/Constants.cs | 9 +++ Azaion.Common/Controls/CanvasEditor.cs | 52 ++++++++++++--- Azaion.Common/Controls/DetectionControl.cs | 22 +++++-- Azaion.Common/DTO/Config/AppConfig.cs | 29 ++++++--- Azaion.Common/DTO/Config/GpsDeniedConfig.cs | 6 ++ Azaion.Common/DTO/Config/UIConfig.cs | 1 + Azaion.Common/DTO/Coordinates.cs | 32 +++++++++ Azaion.Common/DTO/Direction.cs | 17 +++++ Azaion.Common/DTO/SatTile.cs | 13 ++-- Azaion.Common/Extensions/GeoUtils.cs | 65 ++++++++++++++++--- .../Extensions/QueryableExtensions.cs | 47 ++++++++++++++ Azaion.Common/Services/AnnotationService.cs | 10 +-- Azaion.Common/Services/GPSMatcherEvents.cs | 41 ++++++++++-- Azaion.Common/Services/GPSMatcherService.cs | 46 ++++++++----- Azaion.Common/Services/GpsMatcherClient.cs | 26 ++++---- Azaion.Common/Services/InferenceClient.cs | 1 - Azaion.Common/Services/SatelliteDownloader.cs | 23 ++++--- Azaion.Dataset/DatasetExplorer.xaml | 24 ++++++- Azaion.Dataset/DatasetExplorer.xaml.cs | 59 +++++++++-------- Azaion.Dataset/DatasetExplorerEventHandler.cs | 38 ++++++----- Azaion.Loader/hardware_service.pyx | 2 + Azaion.Suite/App.xaml.cs | 2 + Azaion.Suite/MainSuite.xaml.cs | 16 ----- Azaion.Suite/config.json | 7 +- Azaion.Suite/config.system.json | 5 +- Azaion.Test/GetTilesTest.cs | 2 +- build/build_dotnet.cmd | 9 ++- build/publish.cmd | 2 +- 31 files changed, 469 insertions(+), 192 deletions(-) create mode 100644 Azaion.Common/DTO/Config/GpsDeniedConfig.cs create mode 100644 Azaion.Common/DTO/Coordinates.cs create mode 100644 Azaion.Common/DTO/Direction.cs create mode 100644 Azaion.Common/Extensions/QueryableExtensions.cs diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 22970ac..cd939a6 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -51,7 +51,6 @@ public partial class Annotator private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50); private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150); - private static readonly Guid SaveConfigTaskId = Guid.NewGuid(); public ObservableCollection AllMediaFiles { get; set; } = new(); public ObservableCollection FilteredMediaFiles { get; set; } = new(); @@ -223,10 +222,6 @@ public partial class Annotator Volume.ValueChanged += (_, newValue) => _mediator.Publish(new VolumeChangedEvent((int)newValue)); - SizeChanged += (_, _) => SaveUserSettings(); - LocationChanged += (_, _) => SaveUserSettings(); - StateChanged += (_, _) => SaveUserSettings(); - DgAnnotations.MouseDoubleClick += (sender, args) => { var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow; @@ -283,11 +278,7 @@ public partial class Annotator _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; - ThrottleExt.Throttle(() => - { - _configUpdater.Save(_appConfig); - return Task.CompletedTask; - }, SaveConfigTaskId, TimeSpan.FromSeconds(5)); + _configUpdater.Save(_appConfig); } private void ShowTimeAnnotations(TimeSpan time) @@ -589,7 +580,7 @@ public partial class Annotator private void SoundDetections(object sender, RoutedEventArgs e) { - throw new NotImplementedException(); + _logger.LogInformation("To be implemented"); } } diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index b8bcbfa..3b244d4 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -42,7 +42,7 @@ public class AnnotatorEventHandler( INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler { private const int STEP = 20; private const int LARGE_STEP = 5000; @@ -378,20 +378,29 @@ public class AnnotatorEventHandler( return Task.CompletedTask; } - public Task Handle(GPSMatcherResultEvent e, CancellationToken cancellationToken) + public Task Handle(GPSMatcherResultProcessedEvent e, CancellationToken cancellationToken) { mainWindow.Dispatcher.Invoke(() => { - var mapMatcher = mainWindow.MapMatcherComponent; - var marker = new GMapMarker(new PointLatLng(e.Latitude, e.Longitude)); - var ann = mapMatcher.Annotations[e.Index]; - marker.Shape = new CircleVisual(marker, size: 14, text: e.Image, background: Brushes.Blue); - mapMatcher.SatelliteMap.Markers.Add(marker); - ann.Lat = e.Latitude; - ann.Lon = e.Longitude; - mapMatcher.SatelliteMap.Position = new PointLatLng(e.Latitude, e.Longitude); - mapMatcher.SatelliteMap.ZoomAndCenterMarkers(null); + var ann = mainWindow.MapMatcherComponent.Annotations[e.Index]; + AddMarker(e.GeoPoint, e.Image, Brushes.Blue); + if (e.ProcessedGeoPoint != e.GeoPoint) + AddMarker(e.ProcessedGeoPoint, $"{e.Image}: corrected", Brushes.DarkViolet); + ann.Lat = e.GeoPoint.Lat; + ann.Lon = e.GeoPoint.Lon; + }); return Task.CompletedTask; } + + private void AddMarker(GeoPoint point, string text, SolidColorBrush color) + { + var map = mainWindow.MapMatcherComponent; + var pointLatLon = new PointLatLng(point.Lat, point.Lon); + var marker = new GMapMarker(pointLatLon); + marker.Shape = new CircleVisual(marker, size: 14, text: text, background: color); + map.SatelliteMap.Markers.Add(marker); + map.SatelliteMap.Position = pointLatLon; + map.SatelliteMap.ZoomAndCenterMarkers(null); + } } diff --git a/Azaion.Annotator/Controls/MapMatcher.xaml.cs b/Azaion.Annotator/Controls/MapMatcher.xaml.cs index 16d1e3b..db95036 100644 --- a/Azaion.Annotator/Controls/MapMatcher.xaml.cs +++ b/Azaion.Annotator/Controls/MapMatcher.xaml.cs @@ -103,10 +103,8 @@ public partial class MapMatcher : UserControl OriginalMediaName = x.Name })).ToDictionary(x => x.i, x => x.Item2); - var initialLat = double.Parse(TbLat.Text); - var initialLon = double.Parse(TbLon.Text); - - await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon); + var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text)); + await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLatLon); } private async void TestGps(object sender, RoutedEventArgs e) @@ -114,8 +112,7 @@ public partial class MapMatcher : UserControl if (string.IsNullOrEmpty(TbGpsMapFolder.Text)) return; - var initialLat = double.Parse(TbLat.Text); - var initialLon = double.Parse(TbLon.Text); - await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon); + var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text)); + await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLatLon); } } diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index 874d418..8acb0ee 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -81,6 +81,15 @@ public class Constants # endregion AIRecognitionConfig + # region GpsDeniedConfig + + public static readonly GpsDeniedConfig DefaultGpsDeniedConfig = new() + { + MinKeyPoints = 15 + }; + + # endregion + #region Thumbnails public static readonly ThumbnailConfig DefaultThumbnailConfig = new() diff --git a/Azaion.Common/Controls/CanvasEditor.cs b/Azaion.Common/Controls/CanvasEditor.cs index 0f1c8a1..86ec997 100644 --- a/Azaion.Common/Controls/CanvasEditor.cs +++ b/Azaion.Common/Controls/CanvasEditor.cs @@ -1,4 +1,5 @@ -using System.Windows; +using System.Drawing; +using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; @@ -7,7 +8,9 @@ using Azaion.Annotator.DTO; using Azaion.Common.DTO; using MediatR; using Color = System.Windows.Media.Color; +using Point = System.Windows.Point; using Rectangle = System.Windows.Shapes.Rectangle; +using Size = System.Windows.Size; namespace Azaion.Common.Controls; @@ -160,7 +163,7 @@ public class CanvasEditor : Canvas return; var time = GetTimeFunc(); - CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel + var control = CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel { Width = width, Height = height, @@ -168,12 +171,44 @@ public class CanvasEditor : Canvas Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y), Confidence = 1 }); + control.UpdateLayout(); + CheckLabelBoundaries(control); + SelectionState = SelectionState.None; + e.Handled = true; + } + else + { + CheckLabelBoundaries(_curAnn); + SelectionState = SelectionState.None; + e.Handled = true; } - - SelectionState = SelectionState.None; - e.Handled = true; } - + + private void CheckLabelBoundaries(DetectionControl detectionControl) + { + var lb = detectionControl.DetectionLabelContainer; + var origin = lb.TranslatePoint(new Point(0, 0), this); + lb.Children[0].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + var size = lb.Children[0].DesiredSize; + var lbRect = new RectangleF((float)origin.X, (float)origin.Y, (float)size.Width, (float)size.Height); + + foreach (var c in CurrentDetections) + { + if (c == detectionControl) + continue; + var detRect = new RectangleF((float)GetLeft(c), (float)GetTop(c), (float)c.Width, (float)c.Height); + detRect.Intersect(lbRect); + + + // var intersect = detections[i].ToRectangle(); + // intersect.Intersect(detections[j].ToRectangle()); + + // detectionControl. + // var otherControls = allControls.Where(c => c != control); + // control.UpdateLabelPosition(otherControls); + } + } + private void CanvasResized(object sender, SizeChangedEventArgs e) { _horizontalLine.X2 = e.NewSize.Width; @@ -253,7 +288,7 @@ public class CanvasEditor : Canvas ClearSelections(); _curAnn.IsSelected = true; - + SelectionState = SelectionState.AnnMoving; e.Handled = true; } @@ -315,7 +350,7 @@ public class CanvasEditor : Canvas } } - private void CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel) + private DetectionControl CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel) { var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel); detectionControl.MouseDown += AnnotationPositionStart; @@ -324,6 +359,7 @@ public class CanvasEditor : Canvas Children.Add(detectionControl); CurrentDetections.Add(detectionControl); _newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color); + return detectionControl; } #endregion diff --git a/Azaion.Common/Controls/DetectionControl.cs b/Azaion.Common/Controls/DetectionControl.cs index 80d9edc..ed69144 100644 --- a/Azaion.Common/Controls/DetectionControl.cs +++ b/Azaion.Common/Controls/DetectionControl.cs @@ -16,6 +16,8 @@ public class DetectionControl : Border private readonly Grid _grid; private readonly Label _detectionLabel; + public readonly Canvas DetectionLabelContainer; + public TimeSpan Time { get; set; } private readonly double _confidence; private List _resizedRectangles = new(); @@ -51,6 +53,16 @@ public class DetectionControl : Border } } + public (HorizontalAlignment Horizontal, VerticalAlignment Vertical) DetectionLabelPosition + { + get => (DetectionLabelContainer.HorizontalAlignment, DetectionLabelContainer.VerticalAlignment); + set + { + DetectionLabelContainer.HorizontalAlignment = value.Horizontal; + DetectionLabelContainer.VerticalAlignment = value.Vertical; + } + } + private string _detectionLabelText(string detectionClassName) => _confidence >= 0.995 ? detectionClassName : $"{detectionClassName}: {_confidence * 100:F0}%"; //double @@ -62,23 +74,19 @@ public class DetectionControl : Border _resizeStart = resizeStart; _confidence = canvasLabel.Confidence; - var labelContainer = new Canvas + DetectionLabelContainer = new Canvas { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Top, ClipToBounds = false, - Margin = new Thickness(0, 0, 32, 0) }; _detectionLabel = new Label { Content = _detectionLabelText(detectionClass.Name), - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Top, - Margin = new Thickness(0, -32, 0, 0), FontSize = 16, Visibility = Visibility.Visible }; - labelContainer.Children.Add(_detectionLabel); + DetectionLabelContainer.Children.Add(_detectionLabel); _selectionFrame = new Rectangle { @@ -108,7 +116,7 @@ public class DetectionControl : Border }; foreach (var rect in _resizedRectangles) _grid.Children.Add(rect); - _grid.Children.Add(labelContainer); + _grid.Children.Add(DetectionLabelContainer); Child = _grid; Cursor = Cursors.SizeAll; diff --git a/Azaion.Common/DTO/Config/AppConfig.cs b/Azaion.Common/DTO/Config/AppConfig.cs index b4e2275..cd1d43d 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -1,5 +1,6 @@ using System.IO; using System.Text; +using Azaion.Common.Extensions; using Azaion.CommonSecurity; using Newtonsoft.Json; @@ -26,6 +27,8 @@ public class AppConfig public ThumbnailConfig ThumbnailConfig { get; set; } = null!; public MapConfig MapConfig{ get; set; } = null!; + + public GpsDeniedConfig GpsDeniedConfig { get; set; } = null!; } public interface IConfigUpdater @@ -36,6 +39,8 @@ public interface IConfigUpdater public class ConfigUpdater : IConfigUpdater { + private static readonly Guid SaveConfigTaskId = Guid.NewGuid(); + public void CheckConfig() { var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!; @@ -67,23 +72,27 @@ public class ConfigUpdater : IConfigUpdater }, ThumbnailConfig = Constants.DefaultThumbnailConfig, - AIRecognitionConfig = Constants.DefaultAIRecognitionConfig + AIRecognitionConfig = Constants.DefaultAIRecognitionConfig, + GpsDeniedConfig = Constants.DefaultGpsDeniedConfig, }; Save(appConfig); } public void Save(AppConfig config) { - //Save only user's config - var publicConfig = new + ThrottleExt.Throttle(async () => { - config.LoaderClientConfig, - config.InferenceClientConfig, - config.GpsDeniedClientConfig, - config.DirectoriesConfig, - config.UIConfig - }; + var publicConfig = new + { + config.LoaderClientConfig, + config.InferenceClientConfig, + config.GpsDeniedClientConfig, + config.DirectoriesConfig, + config.UIConfig + }; + + await File.WriteAllTextAsync(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8); + }, SaveConfigTaskId, TimeSpan.FromSeconds(5)); - File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8); } } diff --git a/Azaion.Common/DTO/Config/GpsDeniedConfig.cs b/Azaion.Common/DTO/Config/GpsDeniedConfig.cs new file mode 100644 index 0000000..5c274c4 --- /dev/null +++ b/Azaion.Common/DTO/Config/GpsDeniedConfig.cs @@ -0,0 +1,6 @@ +namespace Azaion.Common.DTO.Config; + +public class GpsDeniedConfig +{ + public int MinKeyPoints { get; set; } +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/UIConfig.cs b/Azaion.Common/DTO/Config/UIConfig.cs index d1f75e7..d0b639d 100644 --- a/Azaion.Common/DTO/Config/UIConfig.cs +++ b/Azaion.Common/DTO/Config/UIConfig.cs @@ -6,4 +6,5 @@ public class UIConfig public double RightPanelWidth { get; set; } public bool GenerateAnnotatedImage { get; set; } public bool SilentDetection { get; set; } + public bool ShowDatasetWithDetectionsOnly { get; set; } } diff --git a/Azaion.Common/DTO/Coordinates.cs b/Azaion.Common/DTO/Coordinates.cs new file mode 100644 index 0000000..c044103 --- /dev/null +++ b/Azaion.Common/DTO/Coordinates.cs @@ -0,0 +1,32 @@ +namespace Azaion.Common.DTO; + +public class GeoPoint +{ + const double PRECISION_TOLERANCE = 0.00005; + public double Lat { get; } + public double Lon { get; } + + public GeoPoint() { } + + public GeoPoint(double lat, double lon) + { + Lat = lat; + Lon = lon; + } + + public override string ToString() => $"{Lat:F4}, {Lon:F4}"; + + public override bool Equals(object? obj) + { + if (obj is not GeoPoint point) return false; + return ReferenceEquals(this, obj) || Equals(point); + } + + private bool Equals(GeoPoint point) => + Math.Abs(Lat - point.Lat) < PRECISION_TOLERANCE && Math.Abs(Lon - point.Lon) < PRECISION_TOLERANCE; + + public override int GetHashCode() => HashCode.Combine(Lat, Lon); + + public static bool operator ==(GeoPoint left, GeoPoint right) => Equals(left, right); + public static bool operator !=(GeoPoint left, GeoPoint right) => !Equals(left, right); +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Direction.cs b/Azaion.Common/DTO/Direction.cs new file mode 100644 index 0000000..f2cce89 --- /dev/null +++ b/Azaion.Common/DTO/Direction.cs @@ -0,0 +1,17 @@ +namespace Azaion.Common.DTO; + +public class Direction +{ + public double Distance { get; set; } + public double Azimuth { get; set; } + + public Direction() { } + + public Direction(double distance, double azimuth) + { + Distance = distance; + Azimuth = azimuth; + } + + public override string ToString() => $"{Distance:F2}, {Azimuth:F1} deg"; +} diff --git a/Azaion.Common/DTO/SatTile.cs b/Azaion.Common/DTO/SatTile.cs index 42dfb6a..fb58250 100644 --- a/Azaion.Common/DTO/SatTile.cs +++ b/Azaion.Common/DTO/SatTile.cs @@ -6,11 +6,8 @@ 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 GeoPoint LeftTop { get; } + public GeoPoint BottomRight { get; } public string Url { get; set; } @@ -20,12 +17,12 @@ public class SatTile Y = y; Url = url; - (LeftTopLat, LeftTopLon) = GeoUtils.TileToWorldPos(x, y, zoom); - (BottomRightLat, BottomRightLon) = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom); + LeftTop = GeoUtils.TileToWorldPos(x, y, zoom); + BottomRight = 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})]"; + return $"Tile[X={X}, Y={Y}, TL=({LeftTop.Lat:F6}, {LeftTop.Lon:F6}), BR=({BottomRight.Lat:F6}, {BottomRight.Lon:F6})]"; } } \ No newline at end of file diff --git a/Azaion.Common/Extensions/GeoUtils.cs b/Azaion.Common/Extensions/GeoUtils.cs index d06257e..461c782 100644 --- a/Azaion.Common/Extensions/GeoUtils.cs +++ b/Azaion.Common/Extensions/GeoUtils.cs @@ -1,4 +1,6 @@ -namespace Azaion.Common.Extensions; +using Azaion.Common.DTO; + +namespace Azaion.Common.Extensions; public static class GeoUtils { @@ -13,26 +15,71 @@ public static class GeoUtils return (xTile, yTile); } - public static (double lat, double lon) TileToWorldPos(int x, int y, int zoom) + public static double ToRadians(double degrees) => degrees * Math.PI / 180.0; + public static double ToDegrees(double radians) => radians * 180.0 / Math.PI; + + public static Direction DirectionTo(this GeoPoint p1, GeoPoint p2) + { + var lat1Rad = ToRadians(p1.Lat); + var lat2Rad = ToRadians(p2.Lat); + var dLon = ToRadians(p2.Lon - p1.Lon); + var dLat = ToRadians(p2.Lat - p1.Lat); + + var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(lat1Rad) * Math.Cos(lat2Rad) * + Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + var c = 2 * Math.Asin(Math.Sqrt(a)); + var distance = EARTH_RADIUS * c; + + var y = Math.Sin(dLon) * Math.Cos(lat2Rad); + var x = Math.Cos(lat1Rad) * Math.Sin(lat2Rad) - + Math.Sin(lat1Rad) * Math.Cos(lat2Rad) * Math.Cos(dLon); + var azimuthRadians = Math.Atan2(y, x); + var azimuth = (ToDegrees(azimuthRadians) + 360) % 360; + + return new Direction + { + Distance = distance, + Azimuth = azimuth + }; + } + + public static GeoPoint GoDirection(this GeoPoint startPoint, Direction direction) + { + var angularDistance = direction.Distance / EARTH_RADIUS; + var azimuthRadians = ToRadians(direction.Azimuth); + var startLatRad = ToRadians(startPoint.Lat); + var startLonRad = ToRadians(startPoint.Lon); + + var destLatRad = Math.Asin(Math.Sin(startLatRad) * Math.Cos(angularDistance) + + Math.Cos(startLatRad) * Math.Sin(angularDistance) * Math.Cos(azimuthRadians)); + + var destLonRad = startLonRad + Math.Atan2(Math.Sin(azimuthRadians) * Math.Sin(angularDistance) * Math.Cos(startLatRad), + Math.Cos(angularDistance) - Math.Sin(startLatRad) * Math.Sin(destLatRad)); + + return new GeoPoint(ToDegrees(destLatRad), ToDegrees(destLonRad)); + } + + public static GeoPoint 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); + return new GeoPoint(latDeg, lonDeg); } - public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(double centerLat, double centerLon, double radiusM) + public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(GeoPoint centerGeoPoint, double radiusM) { - var latRad = centerLat * Math.PI / 180.0; + var latRad = centerGeoPoint.Lat * 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 minLat = Math.Max(centerGeoPoint.Lat - latDiff, -90.0); + var maxLat = Math.Min(centerGeoPoint.Lat + 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); + var minLon = Math.Max(centerGeoPoint.Lon - lonDiff, -180.0); + var maxLon = Math.Min(centerGeoPoint.Lon + lonDiff, 180.0); return (minLat, maxLat, minLon, maxLon); } diff --git a/Azaion.Common/Extensions/QueryableExtensions.cs b/Azaion.Common/Extensions/QueryableExtensions.cs new file mode 100644 index 0000000..ae46a3f --- /dev/null +++ b/Azaion.Common/Extensions/QueryableExtensions.cs @@ -0,0 +1,47 @@ +using System.Linq.Expressions; + +namespace Azaion.Common.Extensions; + +public static class QueryableExtensions +{ + /// + /// Adds Where true predicate only if result of condition is true. + /// If false predicate provided, uses it in case of false result + /// Useful for filters, when filters should be applied only when it was set (not NULL) + /// + public static IQueryable WhereIf(this IQueryable query, bool? condition, + Expression> truePredicate, + Expression>? falsePredicate = null) + { + if (!condition.HasValue) + return query; + + if (condition.Value) + return query.Where(truePredicate); + + return falsePredicate != null + ? query.Where(falsePredicate) + : query; + } + + /// + /// Adds Where true predicate only if result of condition is true. + /// If false predicate provided, uses it in case of false result + /// Useful for filters, when filters should be applied only when it was set (not NULL) + /// + public static IEnumerable WhereIf(this IEnumerable query, bool? condition, + Func truePredicate, + Func? falsePredicate = null) + { + if (!condition.HasValue) + return query; + + if (condition.Value) + return query.Where(truePredicate); + + return falsePredicate != null + ? query.Where(falsePredicate) + : query; + } + +} \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 9143691..e34edb6 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -59,7 +59,7 @@ public class AnnotationService : IAnnotationService Task.Run(async () => await InitQueueConsumer()).Wait(); } - private async Task InitQueueConsumer(CancellationToken cancellationToken = default) + private async Task InitQueueConsumer(CancellationToken token = default) { if (!_api.CurrentUser.Role.IsValidator()) return; @@ -79,7 +79,7 @@ public class AnnotationService : IAnnotationService OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset), MessageHandler = async (_, _, context, message) => { - await _messageProcessingSemaphore.WaitAsync(cancellationToken); + await _messageProcessingSemaphore.WaitAsync(token); try { var email = (string)message.ApplicationProperties[nameof(User.Email)]!; @@ -101,15 +101,15 @@ public class AnnotationService : IAnnotationService msg.Role, msg.Email, context.Offset, - token: cancellationToken); + token: token); } else { var msg = MessagePackSerializer.Deserialize(message.Data.Contents); if (annotationStatus == AnnotationStatus.Validated) - await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); + await ValidateAnnotations(msg.AnnotationNames.ToList(), true, token); if (annotationStatus == AnnotationStatus.Deleted) - await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); + await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), token); } } diff --git a/Azaion.Common/Services/GPSMatcherEvents.cs b/Azaion.Common/Services/GPSMatcherEvents.cs index ec1cb95..b03139d 100644 --- a/Azaion.Common/Services/GPSMatcherEvents.cs +++ b/Azaion.Common/Services/GPSMatcherEvents.cs @@ -1,18 +1,45 @@ +using Azaion.Common.DTO; using MediatR; namespace Azaion.Common.Services; +public enum MatchTypeEnum +{ + None = -1, + MatchTypeSingle = 0, + MatchTypeStitched = 1, + MatchTypeOpticalFlow = 2, + MatchTypeInterpolated = 3, + MatchTypeFailure = 4 +} + public class GPSMatcherResultEvent : INotification { - public int Index { get; set; } - public string Image { get; set; } = null!; - 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; } = null!; + public int Index { get; set; } + public string Image { get; set; } = null!; + public GeoPoint GeoPoint { get; set; } = null!; + public int KeyPoints { get; set; } + public MatchTypeEnum MatchType { get; set; } } +public class GPSMatcherResultProcessedEvent : GPSMatcherResultEvent +{ + public GeoPoint ProcessedGeoPoint { get; set; } = null!; + + public GPSMatcherResultProcessedEvent() { } + + public GPSMatcherResultProcessedEvent(GPSMatcherResultEvent gpsMatcherResultEvent, GeoPoint processedGeoPoint) + { + Index = gpsMatcherResultEvent.Index; + Image = gpsMatcherResultEvent.Image; + GeoPoint = gpsMatcherResultEvent.GeoPoint; + KeyPoints = gpsMatcherResultEvent.KeyPoints; + MatchType = gpsMatcherResultEvent.MatchType; + ProcessedGeoPoint = processedGeoPoint; + } +} + + public class GPSMatcherJobAcceptedEvent : INotification {} public class GPSMatcherFinishedEvent : INotification {} \ No newline at end of file diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs index 79aae9d..c091ef9 100644 --- a/Azaion.Common/Services/GPSMatcherService.cs +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -1,46 +1,51 @@ using System.IO; using Azaion.Common.DTO; +using Azaion.Common.DTO.Config; +using Azaion.Common.Extensions; +using MediatR; using Microsoft.Extensions.Options; namespace Azaion.Common.Services; public interface IGpsMatcherService { - Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, CancellationToken detectToken = default); + Task RunGpsMatching(string userRouteDir, GeoPoint geoPoint, CancellationToken detectToken = default); void StopGpsMatching(); Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default); Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken); } -public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions dirConfig) : IGpsMatcherService +public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, + ISatelliteDownloader satelliteTileDownloader, + IOptions dirConfig, + IOptions gpsDeniedConfig, + IMediator mediator) : IGpsMatcherService { private readonly DirectoriesConfig _dirConfig = dirConfig.Value; 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); + private const int MAX_AVG_POINTS = 2; private string _routeDir = ""; private string _userRouteDir = ""; private List _allRouteImages = new(); private Dictionary _currentRouteImages = new(); - private double _currentLat; - private double _currentLon; + private GeoPoint _lastGeoPoint = new(); private CancellationToken _detectToken; private int _currentIndex; + private readonly Queue _directions = new(); - public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, CancellationToken detectToken = default) + public async Task RunGpsMatching(string userRouteDir, GeoPoint initGeoPoint, CancellationToken detectToken = default) { _routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, _dirConfig.GpsRouteDirectory); _userRouteDir = userRouteDir; _allRouteImages = Directory.GetFiles(userRouteDir) .OrderBy(x => x).ToList(); - - _currentLat = initialLatitude; - _currentLon = initialLongitude; - + _lastGeoPoint = initGeoPoint; _detectToken = detectToken; await StartMatchingRound(0); } @@ -63,12 +68,11 @@ public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDow }) .ToDictionary(x => x.Filename, x => x.Index); - await satelliteTileDownloader.GetTiles(_currentLat, _currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken); + await satelliteTileDownloader.GetTiles(_lastGeoPoint, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken); await gpsMatcherClient.StartMatching(new StartMatchingEvent { ImagesCount = POINTS_COUNT, - Latitude = _currentLat, - Longitude = _currentLon, + GeoPoint = _lastGeoPoint, SatelliteImagesDir = _dirConfig.GpsSatDirectory, RouteDir = _dirConfig.GpsRouteDirectory }); @@ -83,9 +87,21 @@ public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDow { _currentIndex = _currentRouteImages[result.Image]; _currentRouteImages.Remove(result.Image); - _currentLat = result.Latitude; - _currentLon = result.Longitude; - await Task.CompletedTask; + + if (result.KeyPoints > gpsDeniedConfig.Value.MinKeyPoints) + { + var direction = _lastGeoPoint.DirectionTo(result.GeoPoint); + _directions.Enqueue(direction); + if (_directions.Count > MAX_AVG_POINTS) + _directions.Dequeue(); + _lastGeoPoint = result.GeoPoint; + } + else + { + var direction = new Direction(_directions.Average(x => x.Distance), _directions.Average(x => x.Azimuth)); + _lastGeoPoint = _lastGeoPoint.GoDirection(direction); + } + await mediator.Publish(new GPSMatcherResultProcessedEvent(result, _lastGeoPoint), detectToken); } public async Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken) diff --git a/Azaion.Common/Services/GpsMatcherClient.cs b/Azaion.Common/Services/GpsMatcherClient.cs index d432596..2d4e1f1 100644 --- a/Azaion.Common/Services/GpsMatcherClient.cs +++ b/Azaion.Common/Services/GpsMatcherClient.cs @@ -18,17 +18,16 @@ public interface IGpsMatcherClient : IDisposable 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 int Altitude { get; set; } = 400; - public double CameraSensorWidth { get; set; } = 23.5; - public double CameraFocalLength { get; set; } = 24; + public string RouteDir { get; set; } = null!; + public string SatelliteImagesDir { get; set; } = null!; + public int ImagesCount { get; set; } + public GeoPoint GeoPoint { get; set; } = null!; + 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},{Altitude},{CameraSensorWidth},{CameraFocalLength}"; + $"{RouteDir},{SatelliteImagesDir},{ImagesCount},{GeoPoint.Lat},{GeoPoint.Lon},{Altitude},{CameraSensorWidth},{CameraFocalLength}"; } public class GpsMatcherClient : IGpsMatcherClient @@ -59,7 +58,6 @@ public class GpsMatcherClient : IGpsMatcherClient catch (Exception e) { _logger.LogError(e, e.ToString()); - throw; } _requestAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqPort}"; @@ -93,7 +91,7 @@ public class GpsMatcherClient : IGpsMatcherClient break; default: var parts = str.Split(','); - if (parts.Length != 5) + if (parts.Length != 6) throw new Exception("Matching Result Failed"); var filename = Path.GetFileNameWithoutExtension(parts[1]); @@ -101,9 +99,9 @@ public class GpsMatcherClient : IGpsMatcherClient { Index = int.Parse(parts[0]), Image = filename, - Latitude = double.Parse(parts[2]), - Longitude = double.Parse(parts[3]), - MatchType = parts[4] + GeoPoint = new GeoPoint(double.Parse(parts[2]), double.Parse(parts[3])), + KeyPoints = int.Parse(parts[4]), + MatchType = Enum.TryParse(parts[5], out var type) ? type : MatchTypeEnum.None }); break; } diff --git a/Azaion.Common/Services/InferenceClient.cs b/Azaion.Common/Services/InferenceClient.cs index 9bf5b2d..63483ed 100644 --- a/Azaion.Common/Services/InferenceClient.cs +++ b/Azaion.Common/Services/InferenceClient.cs @@ -54,7 +54,6 @@ public class InferenceClient : IInferenceClient catch (Exception e) { _logger.LogError(e, e.Message); - throw; } _dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N")); diff --git a/Azaion.Common/Services/SatelliteDownloader.cs b/Azaion.Common/Services/SatelliteDownloader.cs index 38dac60..7012ff0 100644 --- a/Azaion.Common/Services/SatelliteDownloader.cs +++ b/Azaion.Common/Services/SatelliteDownloader.cs @@ -20,7 +20,7 @@ namespace Azaion.Common.Services; public interface ISatelliteDownloader { - Task GetTiles(double latitude, double longitude, double radiusM, int zoomLevel, CancellationToken token = default); + Task GetTiles(GeoPoint geoPoint, double radiusM, int zoomLevel, CancellationToken token = default); } public class SatelliteDownloader( @@ -45,21 +45,28 @@ public class SatelliteDownloader( 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) + public async Task GetTiles(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default) { - await mediator.Publish(new SetStatusTextEvent($"Завантажується супутникові зображення по координатах: центр: lat: {centerLat:F3} lon: {centerLon:F3} квадрат {radiusM}м * {radiusM}м, zoom: {zoomLevel}..."), token); + await mediator.Publish(new SetStatusTextEvent( + $"Завантажуються супутникові зображення по координатах: центр: " + + $"lat: {centerGeoPoint.Lat:F3} lon: {centerGeoPoint.Lon:F3} квадрат {radiusM}м * {radiusM}м, zoom: {zoomLevel}..."), token); //empty Satellite directory if (Directory.Exists(_satDirectory)) Directory.Delete(_satDirectory, true); Directory.CreateDirectory(_satDirectory); - var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token); + var dtRes = await DownloadTiles(centerGeoPoint, radiusM, zoomLevel, token); await mediator.Publish(new SetStatusTextEvent("Завершено! Склеюється в 1 зображення..."), token); - var image = ComposeTiles(downloadTilesResult.Tiles, token); + var image = ComposeTiles(dtRes.Tiles, token); if (image == null) return; + // Save big map. Uncomment when MapHandler with custom pick images would be ready + // var outputFilename = Path.Combine(_satDirectory, + // $"map_tl_{dtRes.LatMax:F6}_{dtRes.LonMin:F6}_br_{dtRes.LatMin:F6}_{dtRes.LonMax:F6}.tif" + // ); + // await image.SaveAsTiffAsync(outputFilename, token); await mediator.Publish(new SetStatusTextEvent("Розбиття на малі зображення для опрацювання..."), token); - await SplitToTiles(image, downloadTilesResult, token); + await SplitToTiles(image, dtRes, token); } private async Task SplitToTiles(Image image, DownloadTilesResult bounds, CancellationToken token = default) @@ -178,9 +185,9 @@ public class SatelliteDownloader( } } - private async Task DownloadTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default) + private async Task DownloadTiles(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default) { - var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerLat, centerLon, radiusM); + var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerGeoPoint, radiusM); var (xMin, yMin) = GeoUtils.WorldToTilePos(latMax, lonMin, zoomLevel); // Top-left corner var (xMax, yMax) = GeoUtils.WorldToTilePos(latMin, lonMax, zoomLevel); // Bottom-right corner diff --git a/Azaion.Dataset/DatasetExplorer.xaml b/Azaion.Dataset/DatasetExplorer.xaml index 16debd8..17f05b7 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml +++ b/Azaion.Dataset/DatasetExplorer.xaml @@ -73,9 +73,27 @@ - + + + + + + + + + Показувати лише анотації з об'єктами + + _logger; - private readonly AnnotationConfig _annotationConfig; - private readonly DirectoriesConfig _directoriesConfig; + private readonly AppConfig _appConfig; private readonly Dictionary> _annotationsDict; private readonly CancellationTokenSource _cts = new(); - public List AllDetectionClasses { get; set; } - public ObservableCollection SelectedAnnotations { get; set; } = new(); + private List AllDetectionClasses { get; } + public ObservableCollection SelectedAnnotations { get; } = new(); public readonly Dictionary SelectedAnnotationDict = new(); - private int _tempSelectedClassIdx = 0; + private int _tempSelectedClassIdx; private readonly IGalleryService _galleryService; private readonly IDbFactory _dbFactory; private readonly IMediator _mediator; - public readonly List AnnotationsClasses; - private IAzaionApi _azaionApi; - + private readonly IAzaionApi _azaionApi; + private readonly IConfigUpdater _configUpdater; public bool ThumbnailLoading { get; set; } public AnnotationThumbnail? CurrentAnnotation { get; set; } public DatasetExplorer( - IOptions directoriesConfig, - IOptions annotationConfig, + IOptions appConfig, ILogger logger, IGalleryService galleryService, FormState formState, IDbFactory dbFactory, IMediator mediator, - IAzaionApi azaionApi) + IAzaionApi azaionApi, + IConfigUpdater configUpdater) { InitializeComponent(); - - _directoriesConfig = directoriesConfig.Value; - _annotationConfig = annotationConfig.Value; + _appConfig = appConfig.Value; _logger = logger; _galleryService = galleryService; _dbFactory = dbFactory; _mediator = mediator; _azaionApi = azaionApi; + _configUpdater = configUpdater; + ShowWithObjectsOnlyChBox.IsChecked = _appConfig.UIConfig.ShowDatasetWithDetectionsOnly; var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast().ToList(); - _annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id)) + _annotationsDict = _appConfig.AnnotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id)) .ToDictionary(x => x, _ => new Dictionary()); _annotationsDict.Add(-1, []); - AnnotationsClasses = annotationConfig.Value.DetectionClasses; - Loaded += OnLoaded; Activated += (_, _) => formState.ActiveWindow = WindowEnum.DatasetExplorer; - ThumbnailsView.KeyDown += async (sender, args) => + ThumbnailsView.KeyDown += async (_, args) => { switch (args.Key) { @@ -102,7 +99,7 @@ public partial class DatasetExplorer AllDetectionClasses = new List( new List { new() {Id = -1, Name = "All", ShortName = "All"}} - .Concat(_annotationConfig.DetectionClasses)); + .Concat(_appConfig.AnnotationConfig.DetectionClasses)); LvClasses.Init(AllDetectionClasses); } @@ -119,7 +116,7 @@ public partial class DatasetExplorer ann.DetectionClass = args.DetectionClass; }; - ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First(); + ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _appConfig.AnnotationConfig.DetectionClasses.First(); var allAnnotations = await _dbFactory.Run(async db => await db.Annotations.LoadWith(x => x.Detections) @@ -150,8 +147,8 @@ public partial class DatasetExplorer .OrderBy(x => x.Key) .Select(gr => new ClusterDistribution { - Label = $"{_annotationConfig.DetectionClassesDict[gr.Key].UIName}: {gr.Value.Count}", - Color = _annotationConfig.DetectionClassesDict[gr.Key].Color, + Label = $"{_appConfig.AnnotationConfig.DetectionClassesDict[gr.Key].UIName}: {gr.Value.Count}", + Color = _appConfig.AnnotationConfig.DetectionClassesDict[gr.Key].Color, ClassCount = gr.Value.Count }) .Where(x => x.ClassCount > 0) @@ -173,7 +170,7 @@ public partial class DatasetExplorer RefreshThumbnailsButtonItem.Visibility = Visibility.Hidden; RefreshProgressBarItem.Visibility = Visibility.Visible; - var result = MessageBox.Show($"Видалити всі іконки та згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?", + var result = MessageBox.Show($"Видалити всі іконки та згенерувати нову базу іконок в {_appConfig.DirectoriesConfig.ThumbnailsDirectory}?", "Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; @@ -204,7 +201,7 @@ public partial class DatasetExplorer var time = ann.Time; ExplorerEditor.RemoveAllAnns(); - ExplorerEditor.CreateDetections(time, ann.Detections, _annotationConfig.DetectionClasses, ExplorerEditor.RenderSize); + ExplorerEditor.CreateDetections(time, ann.Detections, _appConfig.AnnotationConfig.DetectionClasses, ExplorerEditor.RenderSize); } catch (Exception e) { @@ -229,7 +226,7 @@ public partial class DatasetExplorer AnnotationsTab.Visibility = Visibility.Collapsed; EditorTab.Visibility = Visibility.Visible; _tempSelectedClassIdx = LvClasses.CurrentClassNumber; - LvClasses.DetectionDataGrid.ItemsSource = _annotationConfig.DetectionClasses; + LvClasses.DetectionDataGrid.ItemsSource = _appConfig.AnnotationConfig.DetectionClasses; Switcher.SelectedIndex = 1; LvClasses.SelectNum(Math.Max(0, _tempSelectedClassIdx - 1)); @@ -259,16 +256,15 @@ public partial class DatasetExplorer private async Task ReloadThumbnails() { + var withDetectionsOnly = ShowWithObjectsOnlyChBox.IsChecked; SelectedAnnotations.Clear(); SelectedAnnotationDict.Clear(); var annThumbnails = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId] + .WhereIf(withDetectionsOnly, x => x.Value.Detections.Any()) .Select(x => new AnnotationThumbnail(x.Value, _azaionApi.CurrentUser.Role.IsValidator())) .OrderBy(x => !x.IsSeed) .ThenByDescending(x =>x.Annotation.CreatedDate); - //var dict = annThumbnails.Take(20).ToDictionary(x => x.Annotation.Name, x => x.IsSeed); - - foreach (var thumb in annThumbnails) { SelectedAnnotations.Add(thumb); @@ -292,4 +288,11 @@ public partial class DatasetExplorer _logger.LogError(ex, ex.Message); } } + + private async void ShowWithObjectsOnly_OnClick(object sender, RoutedEventArgs e) + { + _appConfig.UIConfig.ShowDatasetWithDetectionsOnly = (sender as CheckBox)?.IsChecked ?? false; + _configUpdater.Save(_appConfig); + await ReloadThumbnails(); + } } diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index e074623..2a39822 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -31,12 +31,12 @@ public class DatasetExplorerEventHandler( { Key.V, PlaybackControlEnum.ValidateAnnotations}, }; - public async Task Handle(DatasetExplorerControlEvent notification, CancellationToken cancellationToken) + public async Task Handle(DatasetExplorerControlEvent notification, CancellationToken token) { - await HandleControl(notification.PlaybackControl, cancellationToken); + await HandleControl(notification.PlaybackControl, token); } - public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken) + public async Task Handle(KeyEvent keyEvent, CancellationToken token) { if (keyEvent.WindowEnum != WindowEnum.DatasetExplorer) return; @@ -52,11 +52,11 @@ public class DatasetExplorerEventHandler( else { if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value)) - await HandleControl(value, cancellationToken); + await HandleControl(value, token); } } - private async Task HandleControl(PlaybackControlEnum controlEnum, CancellationToken cancellationToken = default) + private async Task HandleControl(PlaybackControlEnum controlEnum, CancellationToken token = default) { switch (controlEnum) { @@ -70,7 +70,8 @@ public class DatasetExplorerEventHandler( .Select(x => new Detection(a.Name, x.GetLabel(datasetExplorer.ExplorerEditor.RenderSize))) .ToList(); var index = datasetExplorer.ThumbnailsView.SelectedIndex; - await annotationService.SaveAnnotation(a.OriginalMediaName, a.Time, detections, token: cancellationToken); + var annotation = await annotationService.SaveAnnotation(a.OriginalMediaName, a.Time, detections, token: token); + await ValidateAnnotations([annotation], token); await datasetExplorer.EditAnnotation(index + 1); break; case PlaybackControlEnum.RemoveSelectedAnns: @@ -98,19 +99,24 @@ public class DatasetExplorerEventHandler( var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast() .Select(x => x.Annotation) .ToList(); - await annotationService.ValidateAnnotations(annotations.Select(x => x.Name).ToList(), token: cancellationToken); - foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation))) - { - ann.Annotation.AnnotationStatus = AnnotationStatus.Validated; - if (datasetExplorer.SelectedAnnotationDict.TryGetValue(ann.Annotation.Name, out var value)) - value.Annotation.AnnotationStatus = AnnotationStatus.Validated; - ann.UpdateUI(); - } + await ValidateAnnotations(annotations, token); break; } } - public Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) + private async Task ValidateAnnotations(List annotations, CancellationToken token = default) + { + await annotationService.ValidateAnnotations(annotations.Select(x => x.Name).ToList(), token: token); + foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation))) + { + ann.Annotation.AnnotationStatus = AnnotationStatus.Validated; + if (datasetExplorer.SelectedAnnotationDict.TryGetValue(ann.Annotation.Name, out var value)) + value.Annotation.AnnotationStatus = AnnotationStatus.Validated; + ann.UpdateUI(); + } + } + + public Task Handle(AnnotationCreatedEvent notification, CancellationToken token) { datasetExplorer.Dispatcher.Invoke(() => { @@ -140,7 +146,7 @@ public class DatasetExplorerEventHandler( return Task.CompletedTask; } - public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) + public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken token) { try { diff --git a/Azaion.Loader/hardware_service.pyx b/Azaion.Loader/hardware_service.pyx index 5907ed1..997ef1b 100644 --- a/Azaion.Loader/hardware_service.pyx +++ b/Azaion.Loader/hardware_service.pyx @@ -1,5 +1,6 @@ import os import subprocess +cimport constants cdef class HardwareService: @staticmethod @@ -33,4 +34,5 @@ cdef class HardwareService: cdef str drive_serial = lines[len_lines-1] cdef str res = f'CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {drive_serial}' + constants.log(f'Gathered hardware: {res}') return res diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index 12b71d1..1ca0e1e 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -83,6 +83,7 @@ public partial class App { AnnotationConfig = Constants.DefaultAnnotationConfig, AIRecognitionConfig = Constants.DefaultAIRecognitionConfig, + GpsDeniedConfig = Constants.DefaultGpsDeniedConfig, ThumbnailConfig = Constants.DefaultThumbnailConfig, }))); } @@ -132,6 +133,7 @@ public partial class App services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); + services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); diff --git a/Azaion.Suite/MainSuite.xaml.cs b/Azaion.Suite/MainSuite.xaml.cs index 80c0be2..912c66d 100644 --- a/Azaion.Suite/MainSuite.xaml.cs +++ b/Azaion.Suite/MainSuite.xaml.cs @@ -25,7 +25,6 @@ public partial class MainSuite private readonly Dictionary _openedWindows = new(); private readonly IInferenceClient _inferenceClient; private readonly IGpsMatcherClient _gpsMatcherClient; - private static readonly Guid SaveConfigTaskId = Guid.NewGuid(); public MainSuite(IOptions appConfig, IConfigUpdater configUpdater, @@ -48,10 +47,6 @@ public partial class MainSuite InitializeComponent(); Loaded += OnLoaded; Closed += OnFormClosed; - - SizeChanged += (_, _) => SaveUserSettings(); - LocationChanged += (_, _) => SaveUserSettings(); - StateChanged += (_, _) => SaveUserSettings(); Left = (SystemParameters.WorkArea.Width - Width) / 2; } @@ -125,19 +120,8 @@ public partial class MainSuite } } - - private void SaveUserSettings() - { - ThrottleExt.Throttle(() => - { - _configUpdater.Save(_appConfig); - return Task.CompletedTask; - }, SaveConfigTaskId, TimeSpan.FromSeconds(2)); - } - private void OnFormClosed(object? sender, EventArgs e) { - _configUpdater.Save(_appConfig); foreach (var window in _openedWindows) window.Value.Close(); diff --git a/Azaion.Suite/config.json b/Azaion.Suite/config.json index 422921b..16000cc 100644 --- a/Azaion.Suite/config.json +++ b/Azaion.Suite/config.json @@ -1,12 +1,12 @@ { "LoaderClientConfig": { "ZeroMqHost": "127.0.0.1", - "ZeroMqPort": 5025, + "ZeroMqPort": 5024, "ApiUrl": "https://api.azaion.com" }, "InferenceClientConfig": { "ZeroMqHost": "127.0.0.1", - "ZeroMqPort": 5127, + "ZeroMqPort": 5126, "ApiUrl": "https://api.azaion.com" }, "GpsDeniedClientConfig": { @@ -28,6 +28,7 @@ "LeftPanelWidth": 220.0, "RightPanelWidth": 230.0, "GenerateAnnotatedImage": true, - "SilentDetection": false + "SilentDetection": false, + "ShowDatasetWithDetectionsOnly": false } } \ No newline at end of file diff --git a/Azaion.Suite/config.system.json b/Azaion.Suite/config.system.json index e3a280c..0bb9b2d 100644 --- a/Azaion.Suite/config.system.json +++ b/Azaion.Suite/config.system.json @@ -19,7 +19,7 @@ { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" }, { "Id": 16, "Name": "Caponier", "ShortName": "Капонір", "Color": "#ffa500" } ], - "VideoFormats": [ ".mp4", ".mov", ".avi" ], + "VideoFormats": [ ".mp4", ".mov", ".avi", ".ts", ".mkv" ], "ImageFormats": [ ".jpg", ".jpeg", ".png", ".bmp" ], "AnnotationsDbFile": "annotations.db" }, @@ -34,5 +34,8 @@ "ModelBatchSize": 4 }, + "GpsDeniedConfig": { + "MinKeyPoints": 15 + }, "ThumbnailConfig": { "Size": "240,135", "Border": 10 } } \ No newline at end of file diff --git a/Azaion.Test/GetTilesTest.cs b/Azaion.Test/GetTilesTest.cs index b33a7fc..c136d3e 100644 --- a/Azaion.Test/GetTilesTest.cs +++ b/Azaion.Test/GetTilesTest.cs @@ -36,6 +36,6 @@ public class GetTilesTestClass GpsSatDirectory = "satelliteMaps" }), httpClientFactory, Mock.Of()); - await satelliteDownloader.GetTiles(48.2748909, 37.3834877, 600, 18); + await satelliteDownloader.GetTiles(new GeoPoint(48.2748909, 37.3834877), 600, 18); } } \ No newline at end of file diff --git a/build/build_dotnet.cmd b/build/build_dotnet.cmd index f83c3f2..2843fc3 100644 --- a/build/build_dotnet.cmd +++ b/build/build_dotnet.cmd @@ -10,13 +10,18 @@ cd .. xcopy Azaion.Suite\bin\Release\net8.0-windows\win-x64\publish dist\ /s /e /q del dist\config.json -move dist\config.production.json dist\config.json robocopy "dist" "dist-azaion" "Azaion.Annotator.dll" "Azaion.Dataset.dll" "Azaion.Common.dll" "Azaion.CommonSecurity.dll" /MOV -robocopy "dist" "dist-azaion" "Azaion.Suite.dll" "Azaion.Suite.exe" "Azaion.Suite.runtimeconfig.json" "Azaion.Suite.deps.json" "config.json" "logo.png" /MOV +robocopy "dist" "dist-azaion" "Azaion.Suite.dll" "Azaion.Suite.exe" "Azaion.Suite.runtimeconfig.json" "Azaion.Suite.deps.json" "logo.png" /MOV robocopy "Azaion.LoaderUI\bin\Release\net8.0-windows\win-x64\publish" "dist-dlls" "Azaion.LoaderUI.dll" "Azaion.LoaderUI.exe" "Azaion.LoaderUI.runtimeconfig.json" ^ "Azaion.LoaderUI.deps.json" "loaderconfig.json" +move dist\config.production.json dist\config.json +rem if config updates: + rem robocopy "dist" "dist-azaion" "config.json" /MOV +rem else: + del dist\config.json + if exist dist\libvlc\win-x86 rmdir dist\libvlc\win-x86 /s /q robocopy "dist" "dist-dlls" /E /MOVE diff --git a/build/publish.cmd b/build/publish.cmd index b672b92..ce16cb0 100644 --- a/build/publish.cmd +++ b/build/publish.cmd @@ -17,7 +17,7 @@ call build\download_models echo building and upload iterative installer... iscc build\installer.iterative.iss -call build\upload.cmd "suite" +call build\upload.cmd "suite-dev" echo building full installer iscc build\installer.full.iss