better view for class distribution

This commit is contained in:
Alex Bezdieniezhnykh
2025-05-27 13:26:27 +03:00
parent e957f1192a
commit 34ea821fb3
15 changed files with 311 additions and 217 deletions
+5 -44
View File
@@ -1,12 +1,9 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using Azaion.Common;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
@@ -14,7 +11,6 @@ using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using GMap.NET; using GMap.NET;
using GMap.NET.MapProviders; using GMap.NET.MapProviders;
using GMap.NET.WindowsPresentation;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
namespace Azaion.Annotator.Controls; namespace Azaion.Annotator.Controls;
@@ -23,7 +19,7 @@ public partial class MapMatcher : UserControl
{ {
private AppConfig _appConfig = null!; private AppConfig _appConfig = null!;
List<MediaFileInfo> _allMediaFiles = new(); List<MediaFileInfo> _allMediaFiles = new();
private Dictionary<int, Annotation> _annotations = new(); public Dictionary<int, Annotation> Annotations = new();
private string _currentDir = null!; private string _currentDir = null!;
private IGpsMatcherService _gpsMatcherService = null!; private IGpsMatcherService _gpsMatcherService = null!;
@@ -47,7 +43,7 @@ public partial class MapMatcher : UserControl
private async Task OpenGpsLocation(int gpsFilesIndex) private async Task OpenGpsLocation(int gpsFilesIndex)
{ {
//var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo; //var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
var ann = _annotations.GetValueOrDefault(gpsFilesIndex); var ann = Annotations.GetValueOrDefault(gpsFilesIndex);
if (ann == null) if (ann == null)
return; return;
@@ -101,7 +97,7 @@ public partial class MapMatcher : UserControl
_allMediaFiles = mediaFiles; _allMediaFiles = mediaFiles;
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles); GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
_annotations = mediaFiles.Select((x, i) => (i, new Annotation Annotations = mediaFiles.Select((x, i) => (i, new Annotation
{ {
Name = x.Name, Name = x.Name,
OriginalMediaName = x.Name OriginalMediaName = x.Name
@@ -110,42 +106,7 @@ public partial class MapMatcher : UserControl
var initialLat = double.Parse(TbLat.Text); var initialLat = double.Parse(TbLat.Text);
var initialLon = double.Parse(TbLon.Text); var initialLon = double.Parse(TbLon.Text);
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon, async res => await SetMarker(res)); await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon);
}
private Task SetMarker(GpsMatchResult result)
{
Dispatcher.Invoke(() =>
{
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);
});
return Task.CompletedTask;
}
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.Index);
foreach (var ann in _annotations)
{
var csvRes = csvDict.GetValueOrDefault(ann.Key);
if (csvRes == null)
continue;
await SetMarker(csvRes);
}
} }
private async void TestGps(object sender, RoutedEventArgs e) private async void TestGps(object sender, RoutedEventArgs e)
@@ -155,6 +116,6 @@ public partial class MapMatcher : UserControl
var initialLat = double.Parse(TbLat.Text); var initialLat = double.Parse(TbLat.Text);
var initialLon = double.Parse(TbLon.Text); var initialLon = double.Parse(TbLon.Text);
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon, async res => await SetMarker(res)); await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon);
} }
} }
@@ -0,0 +1,28 @@
using Azaion.Common.Services;
using GMap.NET;
using GMap.NET.WindowsPresentation;
using MediatR;
namespace Azaion.Annotator.Controls;
public class MapMatcherEventHandler(MapMatcher mapMatcher) : INotificationHandler<GPSMatcherResultEvent>
{
public Task Handle(GPSMatcherResultEvent result, CancellationToken cancellationToken)
{
mapMatcher.Dispatcher.Invoke(() =>
{
var marker = new GMapMarker(new PointLatLng(result.Latitude, result.Longitude));
var ann = mapMatcher.Annotations[result.Index];
marker.Shape = new CircleVisual(marker, System.Windows.Media.Brushes.Blue)
{
Text = result.Image
};
mapMatcher.SatelliteMap.Markers.Add(marker);
ann.Lat = result.Latitude;
ann.Lon = result.Longitude;
mapMatcher.SatelliteMap.Position = new PointLatLng(result.Latitude, result.Longitude);
mapMatcher.SatelliteMap.ZoomAndCenterMarkers(null);
});
return Task.CompletedTask;
}
}
+2 -1
View File
@@ -48,7 +48,8 @@ public class Constants
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() }, new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() },
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() }, new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() },
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() }, new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() },
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() } new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() },
new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor() },
]; ];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"]; public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
+11
View File
@@ -0,0 +1,11 @@
using System.Windows.Media;
namespace Azaion.Common.DTO;
public class ClusterDistribution
{
public string Label { get; set; } = "";
public Color Color { get; set; }
public int ClassCount { get; set; }
public double BarWidth { get; set; }
}
-51
View File
@@ -1,51 +0,0 @@
namespace Azaion.Common.DTO;
using System.Collections.Generic;
using System.IO;
public class GpsMatchResult
{
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 static List<GpsMatchResult> ReadFromCsv(string csvFilePath)
{
var imageDatas = new List<GpsMatchResult>();
using var reader = new StreamReader(csvFilePath);
//read header
reader.ReadLine();
if (reader.EndOfStream)
return new List<GpsMatchResult>();
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (string.IsNullOrWhiteSpace(line))
continue;
var values = line.Split(',');
if (values.Length == 6)
{
imageDatas.Add(new GpsMatchResult
{
Image = GetFilename(values[0]),
Latitude = double.Parse(values[1]),
Longitude = double.Parse(values[2]),
KeyPoints = int.Parse(values[3]),
Rotation = int.Parse(values[4]),
MatchType = values[5]
});
}
}
return imageDatas;
}
private static string GetFilename(string imagePath) =>
Path.GetFileNameWithoutExtension(imagePath)
.Replace("-small", "");
}
@@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
namespace Azaion.Common.Extensions; namespace Azaion.Common.Extensions;
@@ -22,4 +23,7 @@ public static class BitmapExtensions
image.Freeze(); image.Freeze();
return image; return image;
} }
public static Color CreateTransparent(this Color color, byte transparency) =>
Color.FromArgb(transparency, color.R, color.G, color.B);
} }
@@ -0,0 +1,18 @@
using MediatR;
namespace Azaion.Common.Services;
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 class GPSMatcherJobAcceptedEvent : INotification {}
public class GPSMatcherFinishedEvent : INotification {}
+68 -40
View File
@@ -1,74 +1,102 @@
using System.Diagnostics; using System.IO;
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity; using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
using MediatR;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public interface IGpsMatcherService public interface IGpsMatcherService
{ {
Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default); Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, CancellationToken detectToken = default);
void StopGpsMatching(); void StopGpsMatching();
} }
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig)
: IGpsMatcherService,
INotificationHandler<GPSMatcherResultEvent>,
INotificationHandler<GPSMatcherFinishedEvent>
{ {
private readonly IGpsMatcherClient _gpsMatcherClient = gpsMatcherClient;
private readonly DirectoriesConfig _dirConfig = dirConfig.Value;
private const int ZOOM_LEVEL = 18; private const int ZOOM_LEVEL = 18;
private const int POINTS_COUNT = 10; private const int POINTS_COUNT = 10;
private const int DISTANCE_BETWEEN_POINTS_M = 100; private const int DISTANCE_BETWEEN_POINTS_M = 100;
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1); 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) private string _routeDir = "";
{ private string _userRouteDir = "";
var currentLat = initialLatitude; private List<string> _allRouteImages = new();
var currentLon = initialLongitude; private Dictionary<string, int> _currentRouteImages = new();
private double _currentLat;
private double _currentLon;
private CancellationToken _detectToken;
private int _currentIndex;
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>(); public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, CancellationToken detectToken = default)
foreach (var file in Directory.GetFiles(userRouteDir))
{ {
routeFiles.Add(file); _routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, _dirConfig.GpsRouteDirectory);
File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file))); _userRouteDir = userRouteDir;
_allRouteImages = Directory.GetFiles(userRouteDir)
.OrderBy(x => x).ToList();
_currentLat = initialLatitude;
_currentLon = initialLongitude;
_detectToken = detectToken;
await StartMatchingRound(0);
} }
var indexOffset = 0; private async Task StartMatchingRound(int startIndex)
while (routeFiles.Any())
{ {
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken); //empty route dir
gpsMatcherClient.StartMatching(new StartMatchingEvent if (Directory.Exists(_routeDir))
Directory.Delete(_routeDir, true);
Directory.CreateDirectory(_routeDir);
_currentRouteImages = _allRouteImages
.Skip(startIndex)
.Take(POINTS_COUNT)
.Select((fullName, index) =>
{
var filename = Path.GetFileName(fullName);
File.Copy(Path.Combine(_userRouteDir, filename), Path.Combine(_routeDir, filename));
return new { Filename = filename, Index = startIndex + index };
})
.ToDictionary(x => x.Filename, x => x.Index);
await satelliteTileDownloader.GetTiles(_currentLat, _currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken);
_gpsMatcherClient.StartMatching(new StartMatchingEvent
{ {
ImagesCount = POINTS_COUNT, ImagesCount = POINTS_COUNT,
Latitude = initialLatitude, Latitude = _currentLat,
Longitude = initialLongitude, Longitude = _currentLon,
SatelliteImagesDir = dirConfig.Value.GpsSatDirectory, SatelliteImagesDir = _dirConfig.GpsSatDirectory,
RouteDir = dirConfig.Value.GpsRouteDirectory RouteDir = _dirConfig.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() public void StopGpsMatching()
{ {
gpsMatcherClient.Stop(); gpsMatcherClient.Stop();
} }
public Task Handle(GPSMatcherResultEvent result, CancellationToken cancellationToken)
{
_currentRouteImages.Remove(result.Image);
_currentLat = result.Latitude;
_currentLon = result.Longitude;
_currentIndex = _currentRouteImages[result.Image];
return Task.CompletedTask;
}
public async Task Handle(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken)
{
if (_currentRouteImages.Count == 0 && _currentIndex < _allRouteImages.Count)
await StartMatchingRound(_currentIndex);
}
} }
+49 -34
View File
@@ -1,18 +1,16 @@
using System.Diagnostics; using System.Diagnostics;
using Azaion.Common.DTO;
using Azaion.CommonSecurity; using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
using MediatR;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NetMQ; using NetMQ;
using NetMQ.Sockets; using NetMQ.Sockets;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public interface IGpsMatcherClient public interface IGpsMatcherClient : IDisposable
{ {
void StartMatching(StartMatchingEvent startEvent); void StartMatching(StartMatchingEvent startEvent);
GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop(); void Stop();
} }
@@ -33,17 +31,22 @@ public class StartMatchingEvent
public class GpsMatcherClient : IGpsMatcherClient public class GpsMatcherClient : IGpsMatcherClient
{ {
private readonly IMediator _mediator;
private readonly GpsDeniedClientConfig _gpsDeniedClientConfig; private readonly GpsDeniedClientConfig _gpsDeniedClientConfig;
private string _requestAddress;
private readonly RequestSocket _requestSocket = new(); private readonly RequestSocket _requestSocket = new();
private string _subscriberAddress;
private readonly SubscriberSocket _subscriberSocket = new(); private readonly SubscriberSocket _subscriberSocket = new();
public GpsMatcherClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
{ {
_mediator = mediator;
_gpsDeniedClientConfig = gpsDeniedClientConfig.Value; _gpsDeniedClientConfig = gpsDeniedClientConfig.Value;
Start(); Start();
} }
private void Start() private void Start(CancellationToken ct = default)
{ {
try try
{ {
@@ -60,58 +63,70 @@ public class GpsMatcherClient : IGpsMatcherClient
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
//process.Start(); process.Start();
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); Console.WriteLine(e);
//throw; //throw;
} }
_requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}");
_subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}"); _requestAddress = $"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}";
_requestSocket.Connect(_requestAddress);
_subscriberAddress = $"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}";
_subscriberSocket.Connect(_subscriberAddress);
_subscriberSocket.Subscribe(""); _subscriberSocket.Subscribe("");
_subscriberSocket.ReceiveReady += async (_, e) => await ProcessClientCommand(e.Socket, ct);
} }
public void StartMatching(StartMatchingEvent e) private async Task ProcessClientCommand(NetMQSocket socket, CancellationToken ct)
{ {
_requestSocket.SendFrame(e.ToString()); while (socket.TryReceiveFrameString(TimeSpan.Zero, out var str))
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; if (string.IsNullOrEmpty(str))
while (!ct.IsCancellationRequested && tryNum++ < retries)
{
if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update))
continue; continue;
if (update == "FINISHED")
return null;
var parts = update.Split(','); switch (str)
{
case "FINISHED":
await _mediator.Publish(new GPSMatcherFinishedEvent(), ct);
break;
case "OK":
await _mediator.Publish(new GPSMatcherJobAcceptedEvent(), ct);
break;
default:
var parts = str.Split(',');
if (parts.Length != 5) if (parts.Length != 5)
throw new Exception("Matching Result Failed"); throw new Exception("Matching Result Failed");
return new GpsMatchResult await _mediator.Publish(new GPSMatcherResultEvent
{ {
Index = int.Parse(parts[0]), Index = int.Parse(parts[0]),
Image = parts[1], Image = parts[1],
Latitude = double.Parse(parts[2]), Latitude = double.Parse(parts[2]),
Longitude = double.Parse(parts[3]), Longitude = double.Parse(parts[3]),
MatchType = parts[4] MatchType = parts[4]
}; }, ct);
break;
}
}
} }
if (!ct.IsCancellationRequested) public void StartMatching(StartMatchingEvent e)
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
return null;
}
public void Stop()
{ {
_requestSocket.SendFrame("STOP"); _requestSocket.SendFrame(e.ToString());
}
public void Stop() => _requestSocket.SendFrame("STOP");
public void Dispose()
{
_requestSocket.SendFrame("EXIT");
_requestSocket.Disconnect(_requestAddress);
_requestSocket.Dispose();
_subscriberSocket.Disconnect(_subscriberAddress);
_subscriberSocket.Dispose();
} }
} }
@@ -0,0 +1,43 @@
<UserControl x:Class="Azaion.Dataset.Controls.ClassDistribution"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:Azaion.Dataset.Controls"
xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400"
FontFamily="Segoe UI">
<UserControl.Resources>
<controls:ProportionToWidthConverter x:Key="ProportionToWidthConverter"/>
</UserControl.Resources>
<ListView ItemsSource="{Binding Items, RelativeSource={RelativeSource AncestorType=controls:ClassDistribution}}"
BorderThickness="0" Background="#FF333333" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Margin" Value="0,1"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type dto:ClusterDistribution}">
<Grid Height="18" x:Name="ItemGrid"> <!-- Give the Grid a name -->
<Border HorizontalAlignment="Left" VerticalAlignment="Stretch">
<Border.Width>
<MultiBinding Converter="{StaticResource ProportionToWidthConverter}">
<Binding Path="BarWidth"/>
<Binding Path="ActualWidth" ElementName="ItemGrid"/>
</MultiBinding>
</Border.Width>
<Border.Background>
<SolidColorBrush Color="{Binding Color}" Opacity="0.5"/>
</Border.Background>
</Border>
<TextBlock Text="{Binding Label}" VerticalAlignment="Center" Margin="5,0,0,0" Foreground="White" FontSize="12"/>
<TextBlock Text="{Binding ClassCount}" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,5,0" Foreground="White" FontSize="12"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</UserControl>
@@ -0,0 +1,22 @@
using System.Windows;
using System.Windows.Controls;
using Azaion.Common.DTO;
namespace Azaion.Dataset.Controls;
public partial class ClassDistribution : UserControl
{
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register(nameof(Items), typeof(IEnumerable<ClusterDistribution>), typeof(ClassDistribution), new PropertyMetadata(null));
public IEnumerable<ClusterDistribution> Items
{
get => (IEnumerable<ClusterDistribution>)GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
public ClassDistribution()
{
InitializeComponent();
}
}
@@ -0,0 +1,33 @@
using System.Globalization;
using System.Windows.Data;
namespace Azaion.Dataset.Controls
{
public class ProportionToWidthConverter : IMultiValueConverter
{
private const double MinPixelBarWidth = 2.0;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values == null || values.Length < 2 ||
!(values[0] is double proportion) ||
!(values[1] is double containerActualWidth))
return MinPixelBarWidth; // Default or fallback width
if (containerActualWidth <= 0 || !double.IsFinite(containerActualWidth) || double.IsNaN(containerActualWidth))
return MinPixelBarWidth; // Container not ready or invalid
double calculatedWidth = proportion * containerActualWidth;
if (proportion >= 0 && calculatedWidth < MinPixelBarWidth)
return MinPixelBarWidth;
return Math.Max(0, calculatedWidth); // Ensure width is not negative
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
+2 -2
View File
@@ -4,9 +4,9 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
xmlns:scottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common" xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common" xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common"
xmlns:controls1="clr-namespace:Azaion.Dataset.Controls"
mc:Ignorable="d" mc:Ignorable="d"
Title="Переглядач анотацій" Height="900" Width="1200" Title="Переглядач анотацій" Height="900" Width="1200"
WindowState="Maximized"> WindowState="Maximized">
@@ -104,7 +104,7 @@
</controls:CanvasEditor> </controls:CanvasEditor>
</TabItem> </TabItem>
<TabItem Name="ClassDistributionTab" Header="Розподіл класів"> <TabItem Name="ClassDistributionTab" Header="Розподіл класів">
<scottPlot:WpfPlot x:Name="ClassDistribution" /> <controls1:ClassDistribution x:Name="ClassDistributionPlot"/>
</TabItem> </TabItem>
</TabControl> </TabControl>
<StatusBar <StatusBar
+13 -32
View File
@@ -6,6 +6,7 @@ using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services; using Azaion.CommonSecurity.Services;
@@ -13,8 +14,6 @@ using LinqToDB;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using ScottPlot;
using Color = ScottPlot.Color;
namespace Azaion.Dataset; namespace Azaion.Dataset;
@@ -140,7 +139,7 @@ public partial class DatasetExplorer
ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First(); ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First();
await ReloadThumbnails(); await ReloadThumbnails();
await LoadClassDistribution(); LoadClassDistribution();
DataContext = this; DataContext = this;
} }
@@ -152,47 +151,29 @@ public partial class DatasetExplorer
_annotationsDict[-1][annotation.Name] = annotation; _annotationsDict[-1][annotation.Name] = annotation;
} }
private async Task LoadClassDistribution() private void LoadClassDistribution()
{ {
var data = _annotationsDict var data = _annotationsDict
.Where(x => x.Key != -1) .Where(x => x.Key != -1)
.Select(gr => new .OrderBy(x => x.Key)
.Select(gr => new ClusterDistribution
{ {
gr.Key, Label = $"{_annotationConfig.DetectionClassesDict[gr.Key].UIName}: {gr.Value.Count}",
_annotationConfig.DetectionClassesDict[gr.Key].ShortName, Color = _annotationConfig.DetectionClassesDict[gr.Key].Color,
_annotationConfig.DetectionClassesDict[gr.Key].Color,
ClassCount = gr.Value.Count ClassCount = gr.Value.Count
}) })
.Where(x => x.ClassCount > 0)
.ToList(); .ToList();
var foregroundColor = Color.FromColor(System.Drawing.Color.Black); var maxClassCount = Math.Max(1, data.Max(x => x.ClassCount));
var bars = data.Select(x => new Bar foreach (var cl in data)
{ {
Orientation = Orientation.Horizontal, cl.Color = cl.Color.CreateTransparent(150);
Position = -1.5 * x.Key + 1, cl.BarWidth = Math.Clamp(cl.ClassCount / (double)maxClassCount, 0, 1);
Label = x.ClassCount > 200 ? x.ClassCount.ToString() : "",
FillColor = new Color(x.Color.R, x.Color.G, x.Color.B, x.Color.A),
Value = x.ClassCount,
CenterLabel = true,
LabelOffset = 10
}).ToList();
ClassDistribution.Plot.Add.Bars(bars);
foreach (var x in data)
{
var label = ClassDistribution.Plot.Add.Text(x.ShortName, 50, -1.5 * x.Key + 1.1);
label.LabelFontColor = foregroundColor;
label.LabelFontSize = 18;
} }
ClassDistribution.Plot.Axes.AutoScale(); ClassDistributionPlot.Items = data;
ClassDistribution.Plot.HideAxesAndGrid();
ClassDistribution.Plot.FigureBackground.Color = new("#888888");
ClassDistribution.Refresh();
await Task.CompletedTask;
} }
private async void RefreshThumbnailsBtnClick(object sender, RoutedEventArgs e) private async void RefreshThumbnailsBtnClick(object sender, RoutedEventArgs e)
+1 -1
View File
@@ -144,7 +144,7 @@ public partial class MainSuite
window.Value.Close(); window.Value.Close();
_inferenceClient.Dispose(); _inferenceClient.Dispose();
_gpsMatcherClient.Stop(); _gpsMatcherClient.Dispose();
Application.Current.Shutdown(); Application.Current.Shutdown();
} }