rework to Azaion.Suite

This commit is contained in:
Alex Bezdieniezhnykh
2024-11-21 13:41:32 +02:00
parent 2cf69f4e4e
commit 5a592e9dbf
76 changed files with 1739 additions and 882 deletions
+19 -1
View File
@@ -2,7 +2,13 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator", "Azaion.Annotator\Azaion.Annotator.csproj", "{8E0809AF-2920-4267-B14D-84BAB334A46F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator", "Azaion.Annotator\Azaion.Annotator.csproj", "{8E0809AF-2920-4267-B14D-84BAB334A46F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator.Test", "Azaion.Annotator.Test\Azaion.Annotator.Test.csproj", "{85359558-FB59-4542-A597-FD9E1B04C8E7}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Test", "Azaion.Test\Azaion.Test.csproj", "{85359558-FB59-4542-A597-FD9E1B04C8E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Suite", "Azaion.Suite\Azaion.Suite.csproj", "{BA77500E-8B66-4F31-81B0-E831FC12EDFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Common", "Azaion.Common\Azaion.Common.csproj", "{1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Dataset", "Azaion.Dataset\Azaion.Dataset.csproj", "{01A5CA37-A62E-4EF3-8678-D72CD9525677}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -18,5 +24,17 @@ Global
{85359558-FB59-4542-A597-FD9E1B04C8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {85359558-FB59-4542-A597-FD9E1B04C8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85359558-FB59-4542-A597-FD9E1B04C8E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {85359558-FB59-4542-A597-FD9E1B04C8E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85359558-FB59-4542-A597-FD9E1B04C8E7}.Release|Any CPU.Build.0 = Release|Any CPU {85359558-FB59-4542-A597-FD9E1B04C8E7}.Release|Any CPU.Build.0 = Release|Any CPU
{BA77500E-8B66-4F31-81B0-E831FC12EDFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA77500E-8B66-4F31-81B0-E831FC12EDFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA77500E-8B66-4F31-81B0-E831FC12EDFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA77500E-8B66-4F31-81B0-E831FC12EDFB}.Release|Any CPU.Build.0 = Release|Any CPU
{1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D8E6F44-C64E-4DBE-8665-2101EC5BE36E}.Release|Any CPU.Build.0 = Release|Any CPU
{01A5CA37-A62E-4EF3-8678-D72CD9525677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01A5CA37-A62E-4EF3-8678-D72CD9525677}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
@@ -1,9 +1,11 @@
<Window x:Class="Azaion.Annotator.MainWindow" <Window x:Class="Azaion.Annotator.Annotator"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF" xmlns:controls="clr-namespace:Azaion.Annotator.Controls" xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF" xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
xmlns:controls1="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
xmlns:controls2="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common"
mc:Ignorable="d" mc:Ignorable="d"
Title="Azaion Annotator" Height="450" Width="1100" Title="Azaion Annotator" Height="450" Width="1100"
> >
@@ -85,12 +87,6 @@
<MenuItem x:Name="OpenFolderItem" <MenuItem x:Name="OpenFolderItem"
Foreground="Black" Foreground="Black"
IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/> IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/>
<MenuItem x:Name="OpenDataExplorerItem"
Foreground="Black"
IsEnabled="True" Header="Відкрити переглядач анотацій..." Click="OpenDataExplorerItemClick"/>
<MenuItem x:Name="ReloadThumbnailsItem"
Foreground="Black"
IsEnabled="True" Header="Оновити базу іконок" Click="ReloadThumbnailsItemClick"/>
</MenuItem> </MenuItem>
<MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0"> <MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0">
<MenuItem x:Name="OpenHelpWindow" <MenuItem x:Name="OpenHelpWindow"
@@ -179,11 +175,11 @@
</GridView> </GridView>
</ListView.View> </ListView.View>
</ListView> </ListView>
<controls:AnnotationClasses <controls1:AnnotationClasses
x:Name="LvClasses" x:Name="LvClasses"
Grid.Column="0" Grid.Column="0"
Grid.Row="4"> Grid.Row="4">
</controls:AnnotationClasses> </controls1:AnnotationClasses>
<GridSplitter <GridSplitter
Background="DarkGray" Background="DarkGray"
@@ -201,7 +197,7 @@
Grid.Column="2" Grid.Column="2"
Grid.RowSpan="4" Grid.RowSpan="4"
x:Name="VideoView"> x:Name="VideoView">
<controls:CanvasEditor x:Name="Editor" <controls1:CanvasEditor x:Name="Editor"
Background="#01000000" Background="#01000000"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" /> HorizontalAlignment="Stretch" />
@@ -261,9 +257,9 @@
<Setter Property="Background"> <Setter Property="Background">
<Setter.Value> <Setter.Value>
<LinearGradientBrush StartPoint="0 0 " EndPoint="1 0"> <LinearGradientBrush StartPoint="0 0 " EndPoint="1 0">
<GradientStop Offset="0.3" Color="{Binding Path=ClassColor1}" /> <GradientStop Offset="0.3" Color="{Binding Path=ClassColor0}" />
<GradientStop Offset="0.5" Color="{Binding Path=ClassColor2}" /> <GradientStop Offset="0.5" Color="{Binding Path=ClassColor1}" />
<GradientStop Offset="0.8" Color="{Binding Path=ClassColor3}" /> <GradientStop Offset="0.8" Color="{Binding Path=ClassColor2}" />
<GradientStop Offset="0.99" Color="{Binding Path=ClassColor3}" /> <GradientStop Offset="0.99" Color="{Binding Path=ClassColor3}" />
</LinearGradientBrush> </LinearGradientBrush>
</Setter.Value> </Setter.Value>
@@ -275,12 +271,12 @@
</DataGrid> </DataGrid>
</Grid> </Grid>
<controls:UpdatableProgressBar x:Name="VideoSlider" <controls2:UpdatableProgressBar x:Name="VideoSlider"
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="1"
Background="#252525" Background="#252525"
Foreground="LightBlue"> Foreground="LightBlue">
</controls:UpdatableProgressBar> </controls2:UpdatableProgressBar>
<!-- Buttons --> <!-- Buttons -->
<Grid <Grid
@@ -469,14 +465,14 @@
</Image> </Image>
</Button> </Button>
<controls:UpdatableProgressBar <controls2:UpdatableProgressBar
x:Name="Volume" x:Name="Volume"
Grid.Column="9" Grid.Column="9"
Width="70" Height="15" Width="70" Height="15"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="#252525" BorderBrush="#252525" Foreground="LightBlue" Background="#252525" BorderBrush="#252525" Foreground="LightBlue"
Maximum="100" Minimum="0"> Maximum="100" Minimum="0">
</controls:UpdatableProgressBar> </controls2:UpdatableProgressBar>
<Button <Button
x:Name="AIDetectBtn" x:Name="AIDetectBtn"
@@ -7,9 +7,12 @@ using System.Windows.Controls;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
@@ -17,122 +20,90 @@ using Newtonsoft.Json;
using Size = System.Windows.Size; using Size = System.Windows.Size;
using IntervalTree; using IntervalTree;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OpenTK.Graphics.OpenGL; using Microsoft.Extensions.Options;
using ScottPlot.TickGenerators.TimeUnits;
using Serilog;
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer; using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public partial class MainWindow public partial class Annotator
{ {
private readonly AppConfig _appConfig;
private readonly LibVLC _libVLC; private readonly LibVLC _libVLC;
private readonly MediaPlayer _mediaPlayer; private readonly MediaPlayer _mediaPlayer;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly FormState _formState; private readonly FormState _formState;
private readonly IConfigRepository _configRepository; private readonly IConfigUpdater _configUpdater;
private readonly HelpWindow _helpWindow; private readonly HelpWindow _helpWindow;
private readonly ILogger<MainWindow> _logger; private readonly ILogger<Annotator> _logger;
private readonly IGalleryManager _galleryManager;
private readonly VLCFrameExtractor _vlcFrameExtractor; private readonly VLCFrameExtractor _vlcFrameExtractor;
private readonly IAIDetector _aiDetector; private readonly IAIDetector _aiDetector;
private CancellationTokenSource _cancellationTokenSource = new(); private readonly CancellationTokenSource _cancellationTokenSource = new();
private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new(); private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100); private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(300); private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(300);
private readonly Config _config;
private readonly DatasetExplorer _datasetExplorer;
private ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new(); private ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new(); private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
public IntervalTree<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new(); public IntervalTree<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new();
private AutodetectDialog _autoDetectDialog; private AutodetectDialog _autoDetectDialog = new() { Topmost = true };
public MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer, public Annotator(
IConfigUpdater configUpdater,
IOptions<AppConfig> appConfig,
LibVLC libVLC, MediaPlayer mediaPlayer,
IMediator mediator, IMediator mediator,
FormState formState, FormState formState,
IConfigRepository configRepository,
HelpWindow helpWindow, HelpWindow helpWindow,
DatasetExplorer datasetExplorer, ILogger<Annotator> logger,
ILogger<MainWindow> logger,
IGalleryManager galleryManager,
VLCFrameExtractor vlcFrameExtractor, VLCFrameExtractor vlcFrameExtractor,
IAIDetector aiDetector) IAIDetector aiDetector)
{ {
InitializeComponent(); InitializeComponent();
_appConfig = appConfig.Value;
_configUpdater = configUpdater;
_libVLC = libVLC; _libVLC = libVLC;
_mediaPlayer = mediaPlayer; _mediaPlayer = mediaPlayer;
_mediator = mediator; _mediator = mediator;
_formState = formState; _formState = formState;
_configRepository = configRepository;
_config = _configRepository.Get();
_helpWindow = helpWindow; _helpWindow = helpWindow;
_datasetExplorer = datasetExplorer;
_logger = logger; _logger = logger;
_galleryManager = galleryManager;
_vlcFrameExtractor = vlcFrameExtractor; _vlcFrameExtractor = vlcFrameExtractor;
_aiDetector = aiDetector; _aiDetector = aiDetector;
VideoView.Loaded += VideoView_Loaded; VideoView.Loaded += VideoView_Loaded;
Closed += OnFormClosed; Closed += OnFormClosed;
if (!Directory.Exists(_config.LabelsDirectory))
Directory.CreateDirectory(_config.LabelsDirectory);
if (!Directory.Exists(_config.ImagesDirectory))
Directory.CreateDirectory(_config.ImagesDirectory);
if (!Directory.Exists(_config.ResultsDirectory))
Directory.CreateDirectory(_config.ResultsDirectory);
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.Main; };
} }
private void VideoView_Loaded(object sender, RoutedEventArgs e) private void VideoView_Loaded(object sender, RoutedEventArgs e)
{ {
Core.Initialize(); Core.Initialize();
InitControls(); InitControls();
_ = Task.Run(async () => await _galleryManager.RefreshThumbnails());
_suspendLayout = true; _suspendLayout = true;
Left = _config.MainWindowConfig.WindowLocation.X; MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.WindowConfig.LeftPanelWidth);
Top = _config.MainWindowConfig.WindowLocation.Y; MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.WindowConfig.RightPanelWidth);
Width = _config.MainWindowConfig.WindowSize.Width;
Height = _config.MainWindowConfig.WindowSize.Height;
_datasetExplorer.Left = _config.MainWindowConfig.WindowLocation.X;
_datasetExplorer.Top = _config.DatasetExplorerConfig.WindowLocation.Y;
_datasetExplorer.Width = _config.DatasetExplorerConfig.WindowSize.Width;
_datasetExplorer.Height = _config.DatasetExplorerConfig.WindowSize.Height;
if (_config.DatasetExplorerConfig.FullScreen)
_datasetExplorer.WindowState = WindowState.Maximized;
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_config.LeftPanelWidth);
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_config.RightPanelWidth);
if (_config.MainWindowConfig.FullScreen)
WindowState = WindowState.Maximized;
_suspendLayout = false; _suspendLayout = false;
ReloadFiles(); ReloadFiles();
if (_config.AnnotationClasses.Count == 0)
_config.AnnotationClasses.Add(new AnnotationClass{Id = 0});
AnnotationClasses = new ObservableCollection<AnnotationClass>(_config.AnnotationClasses); AnnotationClasses = new ObservableCollection<AnnotationClass>(_appConfig.AnnotationConfig.AnnotationClasses);
LvClasses.ItemsSource = AnnotationClasses; LvClasses.ItemsSource = AnnotationClasses;
LvClasses.SelectedIndex = 0; LvClasses.SelectedIndex = 0;
if (LvFiles.Items.IsEmpty) if (LvFiles.Items.IsEmpty)
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]); BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
if (_config.ShowHelpOnStart) if (_appConfig.WindowConfig.ShowHelpOnStart)
_helpWindow.Show(); _helpWindow.Show();
} }
@@ -194,7 +165,7 @@ public partial class MainWindow
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum); _mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
VideoSlider.KeyDown += (sender, args) => VideoSlider.KeyDown += (sender, args) =>
_mediator.Publish(new KeyEvent(sender, args)); _mediator.Publish(new KeyEvent(sender, args, WindowEnum.Annotator));
Volume.ValueChanged += (_, newValue) => Volume.ValueChanged += (_, newValue) =>
_mediator.Publish(new VolumeChangedEvent((int)newValue)); _mediator.Publish(new VolumeChangedEvent((int)newValue));
@@ -226,9 +197,9 @@ public partial class MainWindow
foreach (var annotationResult in res) foreach (var annotationResult in res)
{ {
var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image); var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image);
var thumbnailPath = Path.Combine(_config.ThumbnailsDirectory, $"{imgName}{Config.THUMBNAIL_PREFIX}.jpg"); var thumbnailPath = Path.Combine(_appConfig.DirectoriesConfig.ThumbnailsDirectory, $"{imgName}{Constants.THUMBNAIL_PREFIX}.jpg");
File.Delete(annotationResult.Image); File.Delete(annotationResult.Image);
File.Delete(Path.Combine(_config.LabelsDirectory, $"{imgName}.txt")); File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt"));
File.Delete(thumbnailPath); File.Delete(thumbnailPath);
_formState.AnnotationResults.Remove(annotationResult); _formState.AnnotationResults.Remove(annotationResult);
Annotations.Remove(Annotations.Query(annotationResult.Time)); Annotations.Remove(Annotations.Query(annotationResult.Time));
@@ -237,7 +208,6 @@ public partial class MainWindow
} }
}; };
Editor.FormState = _formState;
Editor.Mediator = _mediator; Editor.Mediator = _mediator;
DgAnnotations.ItemsSource = _formState.AnnotationResults; DgAnnotations.ItemsSource = _formState.AnnotationResults;
} }
@@ -262,13 +232,12 @@ public partial class MainWindow
if (_suspendLayout) if (_suspendLayout)
return; return;
_config.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.WindowConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
_config.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; _appConfig.WindowConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
_config.MainWindowConfig = this.GetConfig();
await ThrottleExt.Throttle(() => await ThrottleExt.Throttle(() =>
{ {
_configRepository.Save(_config); _configUpdater.Save(_appConfig);
return Task.CompletedTask; return Task.CompletedTask;
}, TimeSpan.FromSeconds(5)); }, TimeSpan.FromSeconds(5));
} }
@@ -295,7 +264,7 @@ public partial class MainWindow
if (showImage) if (showImage)
{ {
var fName = _formState.GetTimeName(time); var fName = _formState.GetTimeName(time);
var imgPath = Path.Combine(_config.ImagesDirectory, $"{fName}.jpg"); var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg");
if (File.Exists(imgPath)) if (File.Exists(imgPath))
{ {
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
@@ -305,7 +274,7 @@ public partial class MainWindow
} }
foreach (var label in labels) foreach (var label in labels)
{ {
var annClass = _config.AnnotationClasses[label.ClassNumber]; var annClass = _appConfig.AnnotationConfig.AnnotationClasses[label.ClassNumber];
var canvasLabel = new CanvasLabel(label, canvasSize, videoSize, label.Probability); var canvasLabel = new CanvasLabel(label, canvasSize, videoSize, label.Probability);
Editor.CreateAnnotation(annClass, time, canvasLabel); Editor.CreateAnnotation(annClass, time, canvasLabel);
} }
@@ -319,7 +288,7 @@ public partial class MainWindow
Annotations.Clear(); Annotations.Clear();
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
var labelDir = new DirectoryInfo(_config.LabelsDirectory); var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory);
if (!labelDir.Exists) if (!labelDir.Exists)
return; return;
@@ -327,7 +296,7 @@ public partial class MainWindow
foreach (var file in labelFiles) foreach (var file in labelFiles)
{ {
var name = Path.GetFileNameWithoutExtension(file.Name); var name = Path.GetFileNameWithoutExtension(file.Name);
var time = _formState.GetTime(name); var time = Constants.GetTime(name);
await AddAnnotations(time, await YoloLabel.ReadFromFile(file.FullName, ct), ct); await AddAnnotations(time, await YoloLabel.ReadFromFile(file.FullName, ct), ct);
} }
} }
@@ -335,12 +304,12 @@ public partial class MainWindow
public async Task AddAnnotations(TimeSpan? time, List<YoloLabel> annotations, CancellationToken ct = default) public async Task AddAnnotations(TimeSpan? time, List<YoloLabel> annotations, CancellationToken ct = default)
=> await AddAnnotations(time, annotations.Select(x => new Detection(x)).ToList(), ct); => await AddAnnotations(time, annotations.Select(x => new Detection(x)).ToList(), ct);
public async Task AddAnnotations(TimeSpan? time, List<Detection> annotations, CancellationToken ct = default) public async Task AddAnnotations(TimeSpan? time, List<Detection> detections, CancellationToken ct = default)
{ {
var timeValue = time ?? TimeSpan.FromMinutes(0); var timeValue = time ?? TimeSpan.FromMinutes(0);
var previousAnnotations = Annotations.Query(timeValue); var previousAnnotations = Annotations.Query(timeValue);
Annotations.Remove(previousAnnotations); Annotations.Remove(previousAnnotations);
Annotations.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), annotations.Cast<YoloLabel>().ToList()); Annotations.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections.Cast<YoloLabel>().ToList());
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time); var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
if (existingResult != null) if (existingResult != null)
@@ -355,17 +324,51 @@ public partial class MainWindow
.Select(x => x.Value + 1) .Select(x => x.Value + 1)
.FirstOrDefault(); .FirstOrDefault();
_formState.AnnotationResults.Insert(index, new AnnotationResult(timeValue, _formState.GetTimeName(time), annotations, _config)); _formState.AnnotationResults.Insert(index, CreateAnnotationReult(timeValue, detections));
await File.WriteAllTextAsync($"{_config.ResultsDirectory}/{_formState.VideoName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults), ct); await File.WriteAllTextAsync($"{_appConfig.DirectoriesConfig.ResultsDirectory}/{_formState.VideoName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults), ct);
}
private AnnotationResult CreateAnnotationReult(TimeSpan timeValue, List<Detection> detections)
{
var annotationResult = new AnnotationResult
{
Time = timeValue,
Image = $"{_formState.GetTimeName(timeValue)}.jpg",
Detections = detections,
};
if (detections.Count <= 0)
return annotationResult;
Color GetAnnotationClass(List<int> detectionClasses, int colorNumber)
{
if (detections.Count == 0)
return (-1).ToColor();
return colorNumber >= detectionClasses.Count
? _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses.LastOrDefault()].Color
: _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses[colorNumber]].Color;
}
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
annotationResult.ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.AnnotationClassesDict[x].ShortName))
: _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses.FirstOrDefault()].Name;
annotationResult.ClassColor0 = GetAnnotationClass(detectionClasses, 0);
annotationResult.ClassColor1 = GetAnnotationClass(detectionClasses, 1);
annotationResult.ClassColor2 = GetAnnotationClass(detectionClasses, 2);
annotationResult.ClassColor3 = GetAnnotationClass(detectionClasses, 3);
return annotationResult;
} }
private void ReloadFiles() private void ReloadFiles()
{ {
var dir = new DirectoryInfo(_config.VideosDirectory); var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory);
if (!dir.Exists) if (!dir.Exists)
return; return;
var labelNames = new DirectoryInfo(_config.LabelsDirectory).GetFiles() var labelNames = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory).GetFiles()
.Select(x => .Select(x =>
{ {
var name = Path.GetFileNameWithoutExtension(x.Name); var name = Path.GetFileNameWithoutExtension(x.Name);
@@ -377,7 +380,7 @@ public partial class MainWindow
.Select(gr => gr.Key) .Select(gr => gr.Key)
.ToDictionary(x => x); .ToDictionary(x => x);
var videoFiles = dir.GetFiles(_config.VideoFormats.ToArray()).Select(x => var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x =>
{ {
using var media = new Media(_libVLC, x.FullName); using var media = new Media(_libVLC, x.FullName);
media.Parse(); media.Parse();
@@ -392,7 +395,7 @@ public partial class MainWindow
return fInfo; return fInfo;
}).ToList(); }).ToList();
var imageFiles = dir.GetFiles(_config.ImageFormats.ToArray()).Select(x => new MediaFileInfo var imageFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray()).Select(x => new MediaFileInfo
{ {
Name = x.Name, Name = x.Name,
Path = x.FullName, Path = x.FullName,
@@ -402,7 +405,7 @@ public partial class MainWindow
AllMediaFiles = new ObservableCollection<MediaFileInfo>(videoFiles.Concat(imageFiles).ToList()); AllMediaFiles = new ObservableCollection<MediaFileInfo>(videoFiles.Concat(imageFiles).ToList());
LvFiles.ItemsSource = AllMediaFiles; LvFiles.ItemsSource = AllMediaFiles;
TbFolder.Text = _config.VideosDirectory; TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
BlinkHelp(AllMediaFiles.Count == 0 BlinkHelp(AllMediaFiles.Count == 0
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial] ? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
@@ -415,7 +418,7 @@ public partial class MainWindow
_mediaPlayer.Stop(); _mediaPlayer.Stop();
_mediaPlayer.Dispose(); _mediaPlayer.Dispose();
_libVLC.Dispose(); _libVLC.Dispose();
_configRepository.Save(_config);
Application.Current.Shutdown(); Application.Current.Shutdown();
} }
@@ -460,7 +463,7 @@ public partial class MainWindow
if (!string.IsNullOrEmpty(dlg.FileName)) if (!string.IsNullOrEmpty(dlg.FileName))
{ {
_config.VideosDirectory = dlg.FileName; _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
await SaveUserSettings(); await SaveUserSettings();
} }
@@ -473,13 +476,6 @@ public partial class MainWindow
LvFiles.ItemsSource = FilteredMediaFiles; LvFiles.ItemsSource = FilteredMediaFiles;
} }
private void OpenDataExplorerItemClick(object sender, RoutedEventArgs e)
{
_datasetExplorer.Show();
_datasetExplorer.Activate();
}
private void PlayClick(object sender, RoutedEventArgs e) private void PlayClick(object sender, RoutedEventArgs e)
{ {
_mediator.Publish(new PlaybackControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play)); _mediator.Publish(new PlaybackControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
@@ -506,20 +502,10 @@ public partial class MainWindow
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings(); private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings();
private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e)
{
var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_config.ThumbnailsDirectory}?",
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
_galleryManager.ClearThumbnails();
_galleryManager.RefreshThumbnails();
}
private void LvFilesContextOpening(object sender, ContextMenuEventArgs e) private void LvFilesContextOpening(object sender, ContextMenuEventArgs e)
{ {
var listItem = sender as ListViewItem; var listItem = sender as ListViewItem;
LvFilesContextMenu.DataContext = listItem.DataContext; LvFilesContextMenu.DataContext = listItem!.DataContext;
} }
private (TimeSpan Time, List<Detection> Detections)? _previousDetection; private (TimeSpan Time, List<Detection> Detections)? _previousDetection;
@@ -555,7 +541,7 @@ public partial class MainWindow
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
using var detector = new YOLODetector(_config); using var detector = new YOLODetector(_appConfig.AIRecognitionConfig);
Dispatcher.Invoke(() => _autoDetectDialog.Log("Ініціалізація AI...")); Dispatcher.Invoke(() => _autoDetectDialog.Log("Ініціалізація AI..."));
var prevSeekTime = 0.0; var prevSeekTime = 0.0;
@@ -601,7 +587,7 @@ public partial class MainWindow
var prev = _previousDetection.Value; var prev = _previousDetection.Value;
// Time between detections is >= than Frame Recognition Seconds, allow // Time between detections is >= than Frame Recognition Seconds, allow
if (time >= prev.Time.Add(TimeSpan.FromSeconds(_config.AIRecognitionConfig.FrameRecognitionSeconds))) if (time >= prev.Time.Add(TimeSpan.FromSeconds(_appConfig.AIRecognitionConfig.FrameRecognitionSeconds)))
return true; return true;
// Detection is earlier than previous + FrameRecognitionSeconds. // Detection is earlier than previous + FrameRecognitionSeconds.
@@ -624,11 +610,11 @@ public partial class MainWindow
.First(); .First();
// Closest object is farther than Tracking distance confidence, hence it's a different object, allow // Closest object is farther than Tracking distance confidence, hence it's a different object, allow
if (closestObject.Distance > _config.AIRecognitionConfig.TrackingDistanceConfidence) if (closestObject.Distance > _appConfig.AIRecognitionConfig.TrackingDistanceConfidence)
return true; return true;
// Since closest object within distance confidence, then it is tracking of the same object. Then if recognition probability for the object > increase from previous // Since closest object within distance confidence, then it is tracking of the same object. Then if recognition probability for the object > increase from previous
if (det.Probability >= closestObject.Point.Probability + _config.AIRecognitionConfig.TrackingProbabilityIncrease) if (det.Probability >= closestObject.Point.Probability + _appConfig.AIRecognitionConfig.TrackingProbabilityIncrease)
return true; return true;
} }
@@ -645,10 +631,10 @@ public partial class MainWindow
var time = timeframe.Time; var time = timeframe.Time;
var fName = _formState.GetTimeName(timeframe.Time); var fName = _formState.GetTimeName(timeframe.Time);
var imgPath = Path.Combine(_config.ImagesDirectory, $"{fName}.jpg"); var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg");
var img = System.Drawing.Image.FromStream(timeframe.Stream); var img = System.Drawing.Image.FromStream(timeframe.Stream);
img.Save(imgPath, ImageFormat.Jpeg); img.Save(imgPath, ImageFormat.Jpeg);
await YoloLabel.WriteToFile(detections, Path.Combine(_config.LabelsDirectory, $"{fName}.txt"), token); await YoloLabel.WriteToFile(detections, Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{fName}.txt"), token);
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
@@ -656,16 +642,14 @@ public partial class MainWindow
await AddAnnotations(timeframe.Time, detections, token); await AddAnnotations(timeframe.Time, detections, token);
var log = string.Join(Environment.NewLine, detections.Select(det => var log = string.Join(Environment.NewLine, detections.Select(det =>
$"{_config.AnnotationClassesDict[det.ClassNumber].Name}: " + $"{_appConfig.AnnotationConfig.AnnotationClassesDict[det.ClassNumber].Name}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " + $"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"size=({det.Width:F2}, {det.Height:F2}), " + $"size=({det.Width:F2}, {det.Height:F2}), " +
$"prob: {det.Probability:F1}%")); $"prob: {det.Probability:F1}%"));
Dispatcher.Invoke(() => _autoDetectDialog.Log(log)); Dispatcher.Invoke(() => _autoDetectDialog.Log(log));
var thumbnailDto = await _galleryManager.CreateThumbnail(imgPath, token); await _mediator.Publish(new ImageCreatedEvent(imgPath), token);
if (thumbnailDto != null)
_datasetExplorer.AddThumbnail(thumbnailDto, detections.Select(x => x.ClassNumber));
} }
catch (Exception e) catch (Exception e)
{ {
+275
View File
@@ -0,0 +1,275 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Azaion.Annotator.DTO;
using Azaion.Common.DTO;
using LibVLCSharp.Shared;
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator;
public class AnnotatorEventHandler(
LibVLC libVLC,
MediaPlayer mediaPlayer,
Annotator mainWindow,
FormState formState,
IOptions<DirectoriesConfig> directoriesConfig,
IMediator mediator,
ILogger<AnnotatorEventHandler> logger)
:
INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>,
INotificationHandler<VolumeChangedEvent>
{
private readonly DirectoriesConfig _directoriesConfig = directoriesConfig.Value;
private const int STEP = 20;
private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280;
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Space, PlaybackControlEnum.Pause },
{ Key.Left, PlaybackControlEnum.PreviousFrame },
{ Key.Right, PlaybackControlEnum.NextFrame },
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.PageUp, PlaybackControlEnum.Previous },
{ Key.PageDown, PlaybackControlEnum.Next },
};
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
{
SelectClass(notification.AnnotationClass);
await Task.CompletedTask;
}
private void SelectClass(AnnotationClass annClass)
{
mainWindow.Editor.CurrentAnnClass = annClass;
foreach (var ann in mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = annClass;
mainWindow.LvClasses.SelectedIndex = annClass.Id;
}
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
{
if (keyEvent.WindowEnum != WindowEnum.Annotator)
return;
var key = keyEvent.Args.Key;
var keyNumber = (int?)null;
if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9)
keyNumber = key - Key.D1;
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue)
SelectClass((AnnotationClass)mainWindow.LvClasses.Items[keyNumber.Value]!);
if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value);
if (key == Key.A)
mainWindow.AutoDetect(null!, null!);
await VolumeControl(key);
}
private async Task VolumeControl(Key key)
{
switch (key)
{
case Key.VolumeMute when mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume);
break;
case Key.VolumeMute:
await ControlPlayback(PlaybackControlEnum.TurnOffVolume);
break;
case Key.Up:
case Key.VolumeUp:
var vUp = Math.Min(100, mediaPlayer.Volume + 5);
ChangeVolume(vUp);
mainWindow.Volume.Value = vUp;
break;
case Key.Down:
case Key.VolumeDown:
var vDown = Math.Max(0, mediaPlayer.Volume - 5);
ChangeVolume(vDown);
mainWindow.Volume.Value = vDown;
break;
}
}
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken)
{
await ControlPlayback(notification.PlaybackControl);
mainWindow.VideoView.Focus();
}
private async Task ControlPlayback(PlaybackControlEnum controlEnum)
{
try
{
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
var step = isCtrlPressed ? LARGE_STEP : STEP;
switch (controlEnum)
{
case PlaybackControlEnum.Play:
Play();
break;
case PlaybackControlEnum.Pause:
mediaPlayer.Pause();
if (!mediaPlayer.IsPlaying)
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
if (formState.BackgroundTime.HasValue)
{
mainWindow.Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
formState.BackgroundTime = null;
}
break;
case PlaybackControlEnum.Stop:
mediaPlayer.Stop();
break;
case PlaybackControlEnum.PreviousFrame:
mainWindow.SeekTo(mediaPlayer.Time - step);
break;
case PlaybackControlEnum.NextFrame:
mainWindow.SeekTo(mediaPlayer.Time + step);
break;
case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations();
break;
case PlaybackControlEnum.RemoveSelectedAnns:
mainWindow.Editor.RemoveSelectedAnns();
break;
case PlaybackControlEnum.RemoveAllAnns:
mainWindow.Editor.RemoveAllAnns();
break;
case PlaybackControlEnum.TurnOnVolume:
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
mediaPlayer.Volume = formState.CurrentVolume;
break;
case PlaybackControlEnum.TurnOffVolume:
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
formState.CurrentVolume = mediaPlayer.Volume;
mediaPlayer.Volume = 0;
break;
case PlaybackControlEnum.Previous:
NextMedia(isPrevious: true);
break;
case PlaybackControlEnum.Next:
NextMedia();
break;
case PlaybackControlEnum.None:
break;
default:
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null);
}
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
}
private void NextMedia(bool isPrevious = false)
{
var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count;
if (mainWindow.LvFiles.SelectedIndex + increment == check)
return;
mainWindow.LvFiles.SelectedIndex += increment;
Play();
}
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
{
ChangeVolume(notification.Volume);
await Task.CompletedTask;
}
private void ChangeVolume(int volume)
{
formState.CurrentVolume = volume;
mediaPlayer.Volume = volume;
}
private void Play()
{
if (mainWindow.LvFiles.SelectedItem == null)
return;
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
formState.CurrentMedia = mediaInfo;
mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
}
private async Task SaveAnnotations()
{
var annGridSelectedIndex = mainWindow.DgAnnotations.SelectedIndex;
if (formState.CurrentMedia == null)
return;
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var fName = formState.GetTimeName(time);
var currentAnns = mainWindow.Editor.CurrentAnns
.Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))
.ToList();
await YoloLabel.WriteToFile(currentAnns, Path.Combine(_directoriesConfig.LabelsDirectory, $"{fName}.txt"));
await mainWindow.AddAnnotations(time, currentAnns);
formState.CurrentMedia.HasAnnotations = mainWindow.Annotations.Count != 0;
mainWindow.LvFiles.Items.Refresh();
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
var destinationPath = Path.Combine(_directoriesConfig.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}");
mainWindow.Editor.RemoveAllAnns();
if (isVideo)
{
if (formState.BackgroundTime.HasValue)
{
//no need to save image, it's already there, just remove background
mainWindow.Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
formState.BackgroundTime = null;
//next item
var annGrid = mainWindow.DgAnnotations;
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGridSelectedIndex + 1);
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
}
else
{
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
mediaPlayer.TakeSnapshot(0, destinationPath, RESULT_WIDTH, resultHeight);
mediaPlayer.Play();
}
}
else
{
File.Copy(formState.CurrentMedia.Path, destinationPath, overwrite: true);
NextMedia();
}
await mediator.Publish(new ImageCreatedEvent(destinationPath));
}
}
+17 -10
View File
@@ -1,46 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<ApplicationIcon>logo.ico</ApplicationIcon> <ApplicationIcon>..\logo.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="libc.translation" Version="7.1.1" /> <PackageReference Include="libc.translation" Version="7.1.1" />
<PackageReference Include="LibVLCSharp" Version="3.8.2" /> <PackageReference Include="LibVLCSharp" Version="3.8.2" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.8.2" /> <PackageReference Include="LibVLCSharp.WPF" Version="3.8.2" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RangeTree" Version="3.0.1" /> <PackageReference Include="RangeTree" Version="3.0.1" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.39" />
<PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" /> <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.20" /> <PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.20" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.0.10" />
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" /> <PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
<PackageReference Include="YoloV8.Gpu" Version="5.0.4" /> <PackageReference Include="YoloV8.Gpu" Version="5.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="logo.ico" /> <None Remove="logo.ico" />
<Resource Include="logo.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<None Update="config.json"> <None Update="config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="AutodetectDialog.xaml">
<Generator>MSBuild:Compile</Generator>
<XamlRuntime>Wpf</XamlRuntime>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project> </Project>
@@ -1,11 +0,0 @@
using System.Windows.Controls;
namespace Azaion.Annotator.Controls;
public partial class AnnotationClasses : DataGrid
{
public AnnotationClasses()
{
InitializeComponent();
}
}
@@ -1,4 +1,5 @@
using MediatR; using Azaion.Common.DTO;
using MediatR;
namespace Azaion.Annotator.DTO; namespace Azaion.Annotator.DTO;
+13 -47
View File
@@ -1,13 +1,14 @@
using System.Windows.Media; using System.Windows.Media;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Annotator.DTO; namespace Azaion.Annotator.DTO;
public class AnnotationResult public class AnnotationResult
{ {
private readonly Config _config = null!;
[JsonProperty(PropertyName = "f")] [JsonProperty(PropertyName = "f")]
public string Image { get; set; } = null!; public string Image { get; set; } = null!;
@@ -18,61 +19,26 @@ public class AnnotationResult
public double Lon { get; set; } public double Lon { get; set; }
public List<Detection> Detections { get; set; } = new(); public List<Detection> Detections { get; set; } = new();
#region For Display in the grid #region For XAML Form
[JsonIgnore] [JsonIgnore]
//For XAML Form
public string TimeStr => $"{Time:h\\:mm\\:ss}"; public string TimeStr => $"{Time:h\\:mm\\:ss}";
private List<int>? _detectionClasses = null!;
//For Form
[JsonIgnore] [JsonIgnore]
public string ClassName public string ClassName { get; set; } = null!;
{
get
{
if (Detections.Count == 0)
return "";
_detectionClasses ??= Detections.Select(x => x.ClassNumber).Distinct().ToList();
return _detectionClasses.Count > 1
? string.Join(", ", _detectionClasses.Select(x => _config.AnnotationClassesDict[x].ShortName))
: _config.AnnotationClassesDict[_detectionClasses.FirstOrDefault()].Name;
}
}
[JsonIgnore] [JsonIgnore]
public Color ClassColor1 => GetAnnotationClass(0); public Color ClassColor0 { get; set; }
[JsonIgnore] [JsonIgnore]
public Color ClassColor2 => GetAnnotationClass(1); public Color ClassColor1 { get; set; }
[JsonIgnore] [JsonIgnore]
public Color ClassColor3 => GetAnnotationClass(2); public Color ClassColor2 { get; set; }
[JsonIgnore] [JsonIgnore]
public Color ClassColor4 => GetAnnotationClass(3); public Color ClassColor3 { get; set; }
private Color GetAnnotationClass(int colorNumber)
{
if (Detections.Count == 0)
return (-1).ToColor();
_detectionClasses ??= Detections.Select(x => x.ClassNumber).Distinct().ToList();
return colorNumber >= _detectionClasses.Count
? _config.AnnotationClassesDict[_detectionClasses.LastOrDefault()].Color
: _config.AnnotationClassesDict[_detectionClasses[colorNumber]].Color;
}
#endregion #endregion
public AnnotationResult() { }
public AnnotationResult(TimeSpan time, string timeName, List<Detection> detections, Config config)
{
_config = config;
Detections = detections;
Time = time;
Image = $"{timeName}.jpg";
}
} }
-151
View File
@@ -1,151 +0,0 @@
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Size = System.Windows.Size;
using Point = System.Windows.Point;
namespace Azaion.Annotator.DTO;
public class Config
{
public const string THUMBNAIL_PREFIX = "_thumb";
public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache";
public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!;
public string ImagesDirectory { get; set; } = null!;
public string ResultsDirectory { get; set; } = null!;
public string ThumbnailsDirectory { get; set; } = null!;
public string UnknownImages { get; set; } = null!;
public List<AnnotationClass> AnnotationClasses { get; set; } = [];
private Dictionary<int, AnnotationClass>? _annotationClassesDict;
[JsonIgnore]
public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
public WindowConfig MainWindowConfig { get; set; } = null!;
public WindowConfig DatasetExplorerConfig { get; set; } = null!;
public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; }
public bool ShowHelpOnStart { get; set; }
public List<string> VideoFormats { get; set; } = null!;
public List<string> ImageFormats { get; set; } = null!;
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
public int? LastSelectedExplorerClass { get; set; }
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
}
public class AIRecognitionConfig
{
public string AIModelPath { get; set; } = null!;
public double FrameRecognitionSeconds { get; set; }
public double TrackingDistanceConfidence { get; set; }
public double TrackingProbabilityIncrease { get; set; }
public double TrackingIntersectionThreshold { get; set; }
}
public class WindowConfig
{
public Size WindowSize { get; set; }
public Point WindowLocation { get; set; }
public bool FullScreen { get; set; }
}
public class ThumbnailConfig
{
public Size Size { get; set; }
public int Border { get; set; }
}
public interface IConfigRepository
{
public Config Get();
public void Save(Config config);
}
public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfigRepository
{
private const string CONFIG_PATH = "config.json";
private const string DEFAULT_VIDEO_DIR = "video";
private const string DEFAULT_LABELS_DIR = "labels";
private const string DEFAULT_IMAGES_DIR = "images";
private const string DEFAULT_RESULTS_DIR = "results";
private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
private const string DEFAULT_UNKNOWN_IMG_DIR = "unknown";
private const int DEFAULT_THUMBNAIL_BORDER = 10;
private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
private const double TRACKING_PROBABILITY_INCREASE = 15;
private const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
private static readonly Size DefaultWindowSize = new(1280, 720);
private static readonly Point DefaultWindowLocation = new(100, 100);
private static readonly Size DefaultThumbnailSize = new(240, 135);
private static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
private static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
public Config Get()
{
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
var configFilePath = Path.Combine(exePath, CONFIG_PATH);
if (!File.Exists(configFilePath))
{
return new Config
{
VideosDirectory = Path.Combine(exePath, DEFAULT_VIDEO_DIR),
LabelsDirectory = Path.Combine(exePath, DEFAULT_LABELS_DIR),
ImagesDirectory = Path.Combine(exePath, DEFAULT_IMAGES_DIR),
ResultsDirectory = Path.Combine(exePath, DEFAULT_RESULTS_DIR),
ThumbnailsDirectory = Path.Combine(exePath, DEFAULT_THUMBNAILS_DIR),
UnknownImages = Path.Combine(exePath, DEFAULT_UNKNOWN_IMG_DIR),
MainWindowConfig = new WindowConfig
{
WindowSize = DefaultWindowSize,
WindowLocation = DefaultWindowLocation
},
DatasetExplorerConfig = new WindowConfig
{
WindowSize = DefaultWindowSize,
WindowLocation = DefaultWindowLocation
},
ShowHelpOnStart = true,
VideoFormats = DefaultVideoFormats,
ImageFormats = DefaultImageFormats,
ThumbnailConfig = new ThumbnailConfig
{
Size = DefaultThumbnailSize,
Border = DEFAULT_THUMBNAIL_BORDER
},
AIRecognitionConfig = new AIRecognitionConfig
{
AIModelPath = "azaion.onnx",
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD
}
};
}
var str = File.ReadAllText(CONFIG_PATH);
return JsonConvert.DeserializeObject<Config>(str) ?? new Config();
}
public void Save(Config config)
{
File.WriteAllText(CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
}
}
+1 -20
View File
@@ -11,32 +11,13 @@ public class FormState
? "" ? ""
: Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", ""); : Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", "");
public string CurrentMrl { get; set; } public string CurrentMrl { get; set; } = null!;
public Size CurrentVideoSize { get; set; } public Size CurrentVideoSize { get; set; }
public TimeSpan CurrentVideoLength { get; set; } public TimeSpan CurrentVideoLength { get; set; }
public TimeSpan? BackgroundTime { get; set; } public TimeSpan? BackgroundTime { get; set; }
public int CurrentVolume { get; set; } = 100; public int CurrentVolume { get; set; } = 100;
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = []; public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
public WindowsEnum ActiveWindow { get; set; }
public string GetTimeName(TimeSpan? ts) => $"{VideoName}_{ts:hmmssf}"; public string GetTimeName(TimeSpan? ts) => $"{VideoName}_{ts:hmmssf}";
public TimeSpan? GetTime(string name)
{
var timeStr = name.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
return null;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!int.TryParse(timeStr[0..1], out var hours))
return null;
if (!int.TryParse(timeStr[1..3], out var minutes))
return null;
if (!int.TryParse(timeStr[3..5], out var seconds))
return null;
if (!int.TryParse(timeStr[5..6], out var milliseconds))
return null;
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
}
} }
+1 -6
View File
@@ -1,14 +1,9 @@
using System.Windows.Input; using System.Windows.Input;
using Azaion.Common.DTO;
using MediatR; using MediatR;
namespace Azaion.Annotator.DTO; namespace Azaion.Annotator.DTO;
public class KeyEvent(object sender, KeyEventArgs args) : INotification
{
public object Sender { get; set; } = sender;
public KeyEventArgs Args { get; set; } = args;
}
public class PlaybackControlEvent(PlaybackControlEnum playbackControlEnum) : INotification public class PlaybackControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
{ {
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum; public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
-8
View File
@@ -1,8 +0,0 @@
namespace Azaion.Annotator.DTO;
public enum WindowsEnum
{
None = 0,
Main = 10,
DatasetExplorer = 20
}
@@ -1,52 +1,52 @@
using System.Windows; // using System.Windows;
using System.Windows.Controls; // using System.Windows.Controls;
using System.Windows.Controls.Primitives; // using System.Windows.Controls.Primitives;
using System.Windows.Media; // using System.Windows.Media;
//
namespace Azaion.Annotator.Extensions; // namespace Azaion.Annotator.Extensions;
//
public static class DataGridExtensions // public static class DataGridExtensions
{ // {
public static DataGridCell? GetCell(this DataGrid grid, int rowIndex, int columnIndex = 0) // public static DataGridCell? GetCell(this DataGrid grid, int rowIndex, int columnIndex = 0)
{ // {
var row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(rowIndex); // var row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
if (row == null) // if (row == null)
return null; // return null;
//
var presenter = FindVisualChild<DataGridCellsPresenter>(row); // var presenter = FindVisualChild<DataGridCellsPresenter>(row);
if (presenter == null) // if (presenter == null)
return null; // return null;
//
var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex); // var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
if (cell != null) return cell; // if (cell != null) return cell;
//
// now try to bring into view and retrieve the cell // // now try to bring into view and retrieve the cell
grid.ScrollIntoView(row, grid.Columns[columnIndex]); // grid.ScrollIntoView(row, grid.Columns[columnIndex]);
cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex); // cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
//
return cell; // return cell;
} // }
//
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? dependencyObj) where T : DependencyObject // private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? dependencyObj) where T : DependencyObject
{ // {
if (dependencyObj == null) // if (dependencyObj == null)
yield break; // yield break;
//
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObj); i++) // for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObj); i++)
{ // {
var child = VisualTreeHelper.GetChild(dependencyObj, i); // var child = VisualTreeHelper.GetChild(dependencyObj, i);
if (child is T dependencyObject) // if (child is T dependencyObject)
{ // {
yield return dependencyObject; // yield return dependencyObject;
} // }
//
foreach (T childOfChild in FindVisualChildren<T>(child)) // foreach (T childOfChild in FindVisualChildren<T>(child))
{ // {
yield return childOfChild; // yield return childOfChild;
} // }
} // }
} // }
//
public static TChildItem? FindVisualChild<TChildItem>(DependencyObject? obj) where TChildItem : DependencyObject => // public static TChildItem? FindVisualChild<TChildItem>(DependencyObject? obj) where TChildItem : DependencyObject =>
FindVisualChildren<TChildItem>(obj).FirstOrDefault(); // FindVisualChildren<TChildItem>(obj).FirstOrDefault();
} // }
@@ -1,5 +1,6 @@
using System.Windows; using System.Windows;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Common.DTO.Config;
namespace Azaion.Annotator.Extensions; namespace Azaion.Annotator.Extensions;
+7 -7
View File
@@ -1,20 +1,20 @@
using System.Windows; using System.Windows;
using Azaion.Annotator.DTO; using Azaion.Common.DTO.Config;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public partial class HelpWindow : Window public partial class HelpWindow : Window
{ {
private readonly Config _config; private readonly WindowConfig _windowConfig;
public HelpWindow(Config config) public HelpWindow(WindowConfig windowConfig)
{ {
_config = config; _windowConfig = windowConfig;
Loaded += (_, _) => CbShowHelp.IsChecked = _config.ShowHelpOnStart; Loaded += (_, _) => CbShowHelp.IsChecked = windowConfig.ShowHelpOnStart;
InitializeComponent(); InitializeComponent();
} }
private void Close(object sender, RoutedEventArgs e) => Close(); private void Close(object sender, RoutedEventArgs e) => Close();
private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _config.ShowHelpOnStart = true; private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _windowConfig.ShowHelpOnStart = true;
private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _config.ShowHelpOnStart = false; private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _windowConfig.ShowHelpOnStart = false;
} }
-298
View File
@@ -1,298 +0,0 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Azaion.Annotator.DTO;
using LibVLCSharp.Shared;
using MediatR;
using Microsoft.Extensions.Logging;
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator;
public class MainWindowEventHandler :
INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>,
INotificationHandler<VolumeChangedEvent>
{
private readonly LibVLC _libVLC;
private readonly MediaPlayer _mediaPlayer;
private readonly MainWindow _mainWindow;
private readonly FormState _formState;
private readonly Config _config;
private readonly IMediator _mediator;
private readonly IGalleryManager _galleryManager;
private readonly DatasetExplorer _datasetExplorer;
private readonly ILogger<MainWindowEventHandler> _logger;
private const int STEP = 20;
private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280;
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Space, PlaybackControlEnum.Pause },
{ Key.Left, PlaybackControlEnum.PreviousFrame },
{ Key.Right, PlaybackControlEnum.NextFrame },
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.PageUp, PlaybackControlEnum.Previous },
{ Key.PageDown, PlaybackControlEnum.Next },
};
public MainWindowEventHandler(LibVLC libVLC,
MediaPlayer mediaPlayer,
MainWindow mainWindow,
FormState formState,
Config config,
IMediator mediator,
IGalleryManager galleryManager,
DatasetExplorer datasetExplorer,
ILogger<MainWindowEventHandler> logger)
{
_libVLC = libVLC;
_mediaPlayer = mediaPlayer;
_mainWindow = mainWindow;
_formState = formState;
_config = config;
_mediator = mediator;
_galleryManager = galleryManager;
_datasetExplorer = datasetExplorer;
_logger = logger;
}
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
{
SelectClass(notification.AnnotationClass);
await Task.CompletedTask;
}
private void SelectClass(AnnotationClass annClass)
{
_mainWindow.Editor.CurrentAnnClass = annClass;
foreach (var ann in _mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = annClass;
_mainWindow.LvClasses.SelectedIndex = annClass.Id;
}
public async Task Handle(KeyEvent notification, CancellationToken cancellationToken)
{
if (_formState.ActiveWindow != WindowsEnum.Main)
return;
var key = notification.Args.Key;
var keyNumber = (int?)null;
if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9)
keyNumber = key - Key.D1;
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue)
SelectClass((AnnotationClass)_mainWindow.LvClasses.Items[keyNumber.Value]);
if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value);
if (key == Key.A)
_mainWindow.AutoDetect(null!, null!);
await VolumeControl(key);
}
private async Task VolumeControl(Key key)
{
switch (key)
{
case Key.VolumeMute when _mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume);
break;
case Key.VolumeMute:
await ControlPlayback(PlaybackControlEnum.TurnOffVolume);
break;
case Key.Up:
case Key.VolumeUp:
var vUp = Math.Min(100, _mediaPlayer.Volume + 5);
ChangeVolume(vUp);
_mainWindow.Volume.Value = vUp;
break;
case Key.Down:
case Key.VolumeDown:
var vDown = Math.Max(0, _mediaPlayer.Volume - 5);
ChangeVolume(vDown);
_mainWindow.Volume.Value = vDown;
break;
}
}
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken)
{
await ControlPlayback(notification.PlaybackControl);
_mainWindow.VideoView.Focus();
}
private async Task ControlPlayback(PlaybackControlEnum controlEnum)
{
try
{
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
var step = isCtrlPressed ? LARGE_STEP : STEP;
switch (controlEnum)
{
case PlaybackControlEnum.Play:
Play();
break;
case PlaybackControlEnum.Pause:
_mediaPlayer.Pause();
if (!_mediaPlayer.IsPlaying)
_mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
if (_formState.BackgroundTime.HasValue)
{
_mainWindow.Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
_formState.BackgroundTime = null;
}
break;
case PlaybackControlEnum.Stop:
_mediaPlayer.Stop();
break;
case PlaybackControlEnum.PreviousFrame:
_mainWindow.SeekTo(_mediaPlayer.Time - step);
break;
case PlaybackControlEnum.NextFrame:
_mainWindow.SeekTo(_mediaPlayer.Time + step);
break;
case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations();
break;
case PlaybackControlEnum.RemoveSelectedAnns:
_mainWindow.Editor.RemoveSelectedAnns();
break;
case PlaybackControlEnum.RemoveAllAnns:
_mainWindow.Editor.RemoveAllAnns();
break;
case PlaybackControlEnum.TurnOnVolume:
_mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
_mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
_mediaPlayer.Volume = _formState.CurrentVolume;
break;
case PlaybackControlEnum.TurnOffVolume:
_mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
_mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
_formState.CurrentVolume = _mediaPlayer.Volume;
_mediaPlayer.Volume = 0;
break;
case PlaybackControlEnum.Previous:
NextMedia(isPrevious: true);
break;
case PlaybackControlEnum.Next:
NextMedia();
break;
case PlaybackControlEnum.None:
break;
default:
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null);
}
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
throw;
}
}
private void NextMedia(bool isPrevious = false)
{
var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : _mainWindow.LvFiles.Items.Count;
if (_mainWindow.LvFiles.SelectedIndex + increment == check)
return;
_mainWindow.LvFiles.SelectedIndex += increment;
Play();
}
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
{
ChangeVolume(notification.Volume);
await Task.CompletedTask;
}
private void ChangeVolume(int volume)
{
_formState.CurrentVolume = volume;
_mediaPlayer.Volume = volume;
}
private void Play()
{
if (_mainWindow.LvFiles.SelectedItem == null)
return;
var mediaInfo = (MediaFileInfo)_mainWindow.LvFiles.SelectedItem;
_formState.CurrentMedia = mediaInfo;
_mediaPlayer.Stop();
_mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
_mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
_mediaPlayer.Play(new Media(_libVLC, mediaInfo.Path));
}
private async Task SaveAnnotations()
{
var annGridSelectedIndex = _mainWindow.DgAnnotations.SelectedIndex;
if (_formState.CurrentMedia == null)
return;
var time = _formState.BackgroundTime ?? TimeSpan.FromMilliseconds(_mediaPlayer.Time);
var fName = _formState.GetTimeName(time);
var currentAnns = _mainWindow.Editor.CurrentAnns
.Select(x => new YoloLabel(x.Info, _mainWindow.Editor.RenderSize, _formState.BackgroundTime.HasValue ? _mainWindow.Editor.RenderSize : _formState.CurrentVideoSize))
.ToList();
await YoloLabel.WriteToFile(currentAnns, Path.Combine(_config.LabelsDirectory, $"{fName}.txt"));
await _mainWindow.AddAnnotations(time, currentAnns);
_formState.CurrentMedia.HasAnnotations = _mainWindow.Annotations.Count != 0;
_mainWindow.LvFiles.Items.Refresh();
var isVideo = _formState.CurrentMedia.MediaType == MediaTypes.Video;
var destinationPath = Path.Combine(_config.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(_formState.CurrentMedia.Path))}");
_mainWindow.Editor.RemoveAllAnns();
if (isVideo)
{
if (_formState.BackgroundTime.HasValue)
{
//no need to save image, it's already there, just remove background
_mainWindow.Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
_formState.BackgroundTime = null;
//next item
var annGrid = _mainWindow.DgAnnotations;
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGridSelectedIndex + 1);
_mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
}
else
{
var resultHeight = (uint)Math.Round(RESULT_WIDTH / _formState.CurrentVideoSize.Width * _formState.CurrentVideoSize.Height);
_mediaPlayer.TakeSnapshot(0, destinationPath, RESULT_WIDTH, resultHeight);
_mediaPlayer.Play();
}
}
else
{
File.Copy(_formState.CurrentMedia.Path, destinationPath, overwrite: true);
NextMedia();
}
var thumbnailDto = await _galleryManager.CreateThumbnail(destinationPath);
if (thumbnailDto != null)
_datasetExplorer.AddThumbnail(thumbnailDto, currentAnns.Select(x => x.ClassNumber));
}
}
+19 -18
View File
@@ -1,11 +1,12 @@
using System.IO; using System.IO;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common.DTO;
using Compunet.YoloV8; using Compunet.YoloV8;
using Compunet.YoloV8.Data; using Microsoft.Extensions.Options;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using Detection = Azaion.Annotator.DTO.Detection; using Detection = Azaion.Common.DTO.Detection;
namespace Azaion.Annotator; namespace Azaion.Annotator;
@@ -14,9 +15,9 @@ public interface IAIDetector
List<Detection> Detect(Stream stream); List<Detection> Detect(Stream stream);
} }
public class YOLODetector(Config config) : IAIDetector, IDisposable public class YOLODetector(AIRecognitionConfig recognitionConfig) : IAIDetector, IDisposable
{ {
private readonly YoloPredictor _predictor = new(config.AIRecognitionConfig.AIModelPath); private readonly YoloPredictor _predictor = new(recognitionConfig.AIModelPath);
public List<Detection> Detect(Stream stream) public List<Detection> Detect(Stream stream)
{ {
@@ -37,7 +38,7 @@ public class YOLODetector(Config config) : IAIDetector, IDisposable
private List<Detection> FilterOverlapping(List<Detection> detections) private List<Detection> FilterOverlapping(List<Detection> detections)
{ {
var k = config.AIRecognitionConfig.TrackingIntersectionThreshold; var k = recognitionConfig.TrackingIntersectionThreshold;
var filteredDetections = new List<Detection>(); var filteredDetections = new List<Detection>();
for (var i = 0; i < detections.Count; i++) for (var i = 0; i < detections.Count; i++)
{ {
@@ -48,21 +49,21 @@ public class YOLODetector(Config config) : IAIDetector, IDisposable
intersect.Intersect(detections[j].ToRectangle()); intersect.Intersect(detections[j].ToRectangle());
var maxArea = Math.Max(detections[i].ToRectangle().Area(), detections[j].ToRectangle().Area()); var maxArea = Math.Max(detections[i].ToRectangle().Area(), detections[j].ToRectangle().Area());
if (intersect.Area() > k * maxArea) if (!(intersect.Area() > k * maxArea))
continue;
if (detections[i].Probability > detections[j].Probability)
{ {
if (detections[i].Probability > detections[j].Probability) filteredDetections.Add(detections[i]);
{ detections.RemoveAt(j);
filteredDetections.Add(detections[i]);
detections.RemoveAt(j);
}
else
{
filteredDetections.Add(detections[j]);
detections.RemoveAt(i);
}
detectionSelected = true;
break;
} }
else
{
filteredDetections.Add(detections[j]);
detections.RemoveAt(i);
}
detectionSelected = true;
break;
} }
if (!detectionSelected) if (!detectionSelected)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

+16
View File
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
+73
View File
@@ -0,0 +1,73 @@
using System.Windows;
using Azaion.Common.DTO;
namespace Azaion.Common;
public class Constants
{
#region DefaultConfig
public const string CONFIG_PATH = "config.json";
public const string DEFAULT_VIDEO_DIR = "video";
public const string DEFAULT_LABELS_DIR = "labels";
public const string DEFAULT_IMAGES_DIR = "images";
public const string DEFAULT_RESULTS_DIR = "results";
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
public const int DEFAULT_THUMBNAIL_BORDER = 10;
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
public const double TRACKING_PROBABILITY_INCREASE = 15;
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
public static readonly Size DefaultWindowSize = new(1280, 720);
public static readonly Point DefaultWindowLocation = new(100, 100);
public static readonly Size DefaultThumbnailSize = new(240, 135);
#endregion
#region Thumbnails
public const string THUMBNAIL_PREFIX = "_thumb";
public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache";
#endregion
public static readonly List<AnnotationClass> DefaultAnnotationClasses =
[
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" },
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" },
new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" },
new() { Id = 3, Name = "Артилерія", ShortName = "Арта" },
new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" },
new() { Id = 5, Name = "Окопи", ShortName = "Окопи" },
new() { Id = 6, Name = "Військовий", ShortName = "Військов" },
new() { Id = 7, Name = "Накати", ShortName = "Накати" },
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" },
new() { Id = 9, Name = "Дим", ShortName = "Дим" },
new() { Id = 10, Name = "Літак", ShortName = "Літак" }
];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
public static TimeSpan? GetTime(string imagePath)
{
var timeStr = imagePath.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
return null;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!int.TryParse(timeStr[0..1], out var hours))
return null;
if (!int.TryParse(timeStr[1..3], out var minutes))
return null;
if (!int.TryParse(timeStr[3..5], out var seconds))
return null;
if (!int.TryParse(timeStr[5..6], out var milliseconds))
return null;
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
}
}
@@ -1,9 +1,8 @@
<DataGrid x:Class="Azaion.Annotator.Controls.AnnotationClasses" <DataGrid x:Class="Azaion.Common.Controls.AnnotationClasses"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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:local="clr-namespace:Azaion.Annotator.Controls"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" d:DesignHeight="300" d:DesignWidth="300"
Background="Black" Background="Black"
@@ -0,0 +1,9 @@
namespace Azaion.Common.Controls;
public partial class AnnotationClasses
{
public AnnotationClasses()
{
InitializeComponent();
}
}
@@ -3,10 +3,10 @@ using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Shapes; using System.Windows.Shapes;
using Azaion.Annotator.DTO; using Azaion.Common.DTO;
using Label = System.Windows.Controls.Label; using Label = System.Windows.Controls.Label;
namespace Azaion.Annotator.Controls; namespace Azaion.Common.Controls;
public class AnnotationControl : Border public class AnnotationControl : Border
{ {
@@ -4,11 +4,12 @@ using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Shapes; using System.Windows.Shapes;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Common.DTO;
using MediatR; using MediatR;
using Color = System.Windows.Media.Color; using Color = System.Windows.Media.Color;
using Rectangle = System.Windows.Shapes.Rectangle; using Rectangle = System.Windows.Shapes.Rectangle;
namespace Azaion.Annotator.Controls; namespace Azaion.Common.Controls;
public class CanvasEditor : Canvas public class CanvasEditor : Canvas
{ {
@@ -28,7 +29,6 @@ public class CanvasEditor : Canvas
private const int MIN_SIZE = 20; private const int MIN_SIZE = 20;
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400); private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
public FormState FormState { get; set; } = null!;
public IMediator Mediator { get; set; } = null!; public IMediator Mediator { get; set; } = null!;
public static readonly DependencyProperty GetTimeFuncProp = public static readonly DependencyProperty GetTimeFuncProp =
@@ -1,15 +1,15 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Windows.Media; using System.Windows.Media;
using Azaion.Annotator.Extensions; using Azaion.Common.Extensions;
namespace Azaion.Annotator.DTO; namespace Azaion.Common.DTO;
public class AnnotationClass public class AnnotationClass
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; } = null!;
public string ShortName { get; set; } public string ShortName { get; set; } = null!;
[JsonIgnore] [JsonIgnore]
public Color Color => Id.ToColor(); public Color Color => Id.ToColor();
@@ -0,0 +1,10 @@
namespace Azaion.Annotator.DTO;
public class AIRecognitionConfig
{
public string AIModelPath { get; set; } = null!;
public double FrameRecognitionSeconds { get; set; }
public double TrackingDistanceConfidence { get; set; }
public double TrackingProbabilityIncrease { get; set; }
public double TrackingIntersectionThreshold { get; set; }
}
@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config;
public class AnnotationConfig
{
public List<AnnotationClass> AnnotationClasses { get; set; } = null!;
[JsonIgnore]
private Dictionary<int, AnnotationClass>? _annotationClassesDict;
[JsonIgnore]
public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
public int? LastSelectedExplorerClass { get; set; }
public List<string> VideoFormats { get; set; } = null!;
public List<string> ImageFormats { get; set; } = null!;
}
+13
View File
@@ -0,0 +1,13 @@
namespace Azaion.Suite.Services.DTO;
public class ApiConfig
{
public string Url { get; set; } = null!;
public int RetryCount {get;set;}
public double TimeoutSeconds { get; set; }
}
public class LocalFilesConfig
{
public string DllPath { get; set; } = null!;
}
+91
View File
@@ -0,0 +1,91 @@
using System.IO;
using System.Text;
using Azaion.Annotator.DTO;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config;
public class AppConfig
{
public ApiConfig ApiConfig { get; set; } = null!;
public AnnotationConfig AnnotationConfig { get; set; } = null!;
public WindowConfig WindowConfig { get; set; } = null!;
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
}
public interface IConfigUpdater
{
void CheckConfig();
void Save(AppConfig config);
}
public class ConfigUpdater : IConfigUpdater
{
public void CheckConfig()
{
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH);
if (File.Exists(configFilePath))
return;
var appConfig = new AppConfig
{
AnnotationConfig = new AnnotationConfig
{
AnnotationClasses = Constants.DefaultAnnotationClasses,
VideoFormats = Constants.DefaultVideoFormats,
ImageFormats = Constants.DefaultImageFormats,
},
WindowConfig = new WindowConfig
{
WindowSize = Constants.DefaultWindowSize,
WindowLocation = Constants.DefaultWindowLocation,
ShowHelpOnStart = true,
FullScreen = true,
LeftPanelWidth = 250,
RightPanelWidth = 250,
},
DirectoriesConfig = new DirectoriesConfig
{
VideosDirectory = Constants.DEFAULT_VIDEO_DIR,
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR
},
ThumbnailConfig = new ThumbnailConfig
{
Size = Constants.DefaultThumbnailSize,
Border = Constants.DEFAULT_THUMBNAIL_BORDER
},
AIRecognitionConfig = new AIRecognitionConfig
{
AIModelPath = "azaion.onnx",
FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD
}
};
Save(appConfig);
}
public void Save(AppConfig config)
{
File.WriteAllText(Constants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
}
}
@@ -0,0 +1,10 @@
namespace Azaion.Common.DTO;
public class DirectoriesConfig
{
public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!;
public string ImagesDirectory { get; set; } = null!;
public string ResultsDirectory { get; set; } = null!;
public string ThumbnailsDirectory { get; set; } = null!;
}
@@ -0,0 +1,9 @@
using System.Windows;
namespace Azaion.Common.DTO.Config;
public class ThumbnailConfig
{
public Size Size { get; set; }
public int Border { get; set; }
}
+14
View File
@@ -0,0 +1,14 @@
using System.Windows;
namespace Azaion.Common.DTO.Config;
public class WindowConfig
{
public Size WindowSize { get; set; }
public Point WindowLocation { get; set; }
public bool FullScreen { get; set; }
public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; }
public bool ShowHelpOnStart { get; set; }
}
+8
View File
@@ -0,0 +1,8 @@
using MediatR;
namespace Azaion.Common.DTO;
public class ImageCreatedEvent(string imagePath) : INotification
{
public string ImagePath { get; } = imagePath;
}
+11
View File
@@ -0,0 +1,11 @@
using System.Windows.Input;
using MediatR;
namespace Azaion.Common.DTO;
public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification
{
public object Sender { get; set; } = sender;
public KeyEventArgs Args { get; set; } = args;
public WindowEnum WindowEnum { get; } = windowEnum;
}
@@ -4,7 +4,7 @@ using System.IO;
using Newtonsoft.Json; using Newtonsoft.Json;
using Size = System.Windows.Size; using Size = System.Windows.Size;
namespace Azaion.Annotator.DTO; namespace Azaion.Common.DTO;
public abstract class Label public abstract class Label
{ {
@@ -1,6 +1,6 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Annotator.DTO; namespace Azaion.Common.DTO;
public class LabelInfo public class LabelInfo
{ {
@@ -1,4 +1,4 @@
namespace Azaion.Annotator.DTO; namespace Azaion.Common.DTO;
public enum PlaybackControlEnum public enum PlaybackControlEnum
{ {
+8
View File
@@ -0,0 +1,8 @@
namespace Azaion.Common.DTO;
public enum WindowEnum
{
None = 0,
Annotator = 10,
DatasetExplorer = 20
}
@@ -1,6 +1,6 @@
using System.Windows.Media; using System.Windows.Media;
namespace Azaion.Annotator.Extensions; namespace Azaion.Common.Extensions;
public static class ColorExtensions public static class ColorExtensions
{ {
@@ -1,6 +1,6 @@
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
namespace Azaion.Annotator.Extensions; namespace Azaion.Common.Extensions;
public class DenseDateTimeConverter : IsoDateTimeConverter public class DenseDateTimeConverter : IsoDateTimeConverter
{ {
@@ -0,0 +1,20 @@
using System.Windows;
using Azaion.Common.DTO;
namespace Azaion.Common.Extensions;
public static class WindowExtensions
{
public static WindowEnum GetParentWindow(this FrameworkElement? element)
{
while (element != null && element is not Window)
{
element = element.Parent as FrameworkElement;
}
if (element is not Window)
return WindowEnum.None;
return WindowEnum.Annotator;
}
}
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+34
View File
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<Page Update="DatasetExplorer.xaml">
<Generator>MSBuild:Compile</Generator>
<XamlRuntime>Wpf</XamlRuntime>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.46" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Drawing.Common">
<HintPath>C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\8.0.8\System.Drawing.Common.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
+19
View File
@@ -0,0 +1,19 @@
using System.IO;
using System.Windows.Media.Imaging;
namespace Azaion.Dataset;
public static class BitmapExtensions
{
public static async Task<BitmapImage> OpenImage(this string imagePath)
{
var image = new BitmapImage();
await using var stream = File.OpenRead(imagePath);
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
return image;
}
}
@@ -1,17 +1,18 @@
<Window x:Class="Azaion.Annotator.DatasetExplorer" <Window x:Class="Azaion.Dataset.DatasetExplorer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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:dto="clr-namespace:Azaion.Annotator.DTO" xmlns:scottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
xmlns:controls="clr-namespace:Azaion.Annotator.Controls" xmlns:datasetExplorer="clr-namespace:Azaion.Dataset"
xmlns:ScottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF" xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
xmlns:controls1="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common"
mc:Ignorable="d" mc:Ignorable="d"
Title="Переглядач анотацій" Height="900" Width="1200"> Title="Переглядач анотацій" Height="900" Width="1200">
<Window.Resources> <Window.Resources>
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}"> <DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type datasetExplorer:ThumbnailDto}">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
@@ -79,7 +80,7 @@
</controls:CanvasEditor> </controls:CanvasEditor>
</TabItem> </TabItem>
<TabItem Name="ClassDistributionTab" Header="Розподіл класів"> <TabItem Name="ClassDistributionTab" Header="Розподіл класів">
<ScottPlot:WpfPlot x:Name="ClassDistribution" /> <scottPlot:WpfPlot x:Name="ClassDistribution" />
</TabItem> </TabItem>
</TabControl> </TabControl>
<StatusBar <StatusBar
@@ -3,27 +3,27 @@ using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ScottPlot; using ScottPlot;
using Color = ScottPlot.Color; using Color = ScottPlot.Color;
using MessageBox = System.Windows.MessageBox;
using Orientation = ScottPlot.Orientation;
namespace Azaion.Annotator; namespace Azaion.Dataset;
public partial class DatasetExplorer public partial class DatasetExplorer
{ {
private readonly Config _config;
private readonly ILogger<DatasetExplorer> _logger; private readonly ILogger<DatasetExplorer> _logger;
private readonly AnnotationConfig _annotationConfig;
private readonly DirectoriesConfig _directoriesConfig;
public ObservableCollection<ThumbnailDto> ThumbnailsDtos { get; set; } = new(); public ObservableCollection<ThumbnailDto> ThumbnailsDtos { get; set; } = new();
private ObservableCollection<AnnotationClass> AllAnnotationClasses { get; set; } = new(); private ObservableCollection<AnnotationClass> AllAnnotationClasses { get; set; } = new();
private int _tempSelectedClassIdx = 0; private int _tempSelectedClassIdx = 0;
private readonly IConfigRepository _configRepository;
private readonly FormState _formState;
private readonly IGalleryManager _galleryManager; private readonly IGalleryManager _galleryManager;
public bool ThumbnailLoading { get; set; } public bool ThumbnailLoading { get; set; }
@@ -31,31 +31,31 @@ public partial class DatasetExplorer
public ThumbnailDto? CurrentThumbnail { get; set; } public ThumbnailDto? CurrentThumbnail { get; set; }
public DatasetExplorer( public DatasetExplorer(
Config config, IOptions<DirectoriesConfig> directoriesConfig,
IOptions<AnnotationConfig> annotationConfig,
ILogger<DatasetExplorer> logger, ILogger<DatasetExplorer> logger,
IConfigRepository configRepository,
FormState formState,
IGalleryManager galleryManager) IGalleryManager galleryManager)
{ {
_config = config; _directoriesConfig = directoriesConfig.Value;
_annotationConfig = annotationConfig.Value;
_logger = logger; _logger = logger;
_configRepository = configRepository;
_formState = formState;
_galleryManager = galleryManager; _galleryManager = galleryManager;
InitializeComponent(); InitializeComponent();
Loaded += async (_, _) => Loaded += async (_, _) =>
{ {
_ = Task.Run(async () => await _galleryManager.RefreshThumbnails());
AllAnnotationClasses = new ObservableCollection<AnnotationClass>( AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
new List<AnnotationClass> { new() {Id = -1, Name = "All", ShortName = "All"}} new List<AnnotationClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_config.AnnotationClasses)); .Concat(_annotationConfig.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.MouseUp += async (_, _) => LvClasses.MouseUp += async (_, _) =>
{ {
var selectedClass = (AnnotationClass)LvClasses.SelectedItem; var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
ExplorerEditor.CurrentAnnClass = selectedClass; ExplorerEditor.CurrentAnnClass = selectedClass;
config.LastSelectedExplorerClass = selectedClass.Id; _annotationConfig.LastSelectedExplorerClass = selectedClass.Id;
if (Switcher.SelectedIndex == 0) if (Switcher.SelectedIndex == 0)
await ReloadThumbnails(); await ReloadThumbnails();
@@ -80,15 +80,11 @@ public partial class DatasetExplorer
}; };
LvClasses.SelectedIndex = config.LastSelectedExplorerClass ?? 0; LvClasses.SelectedIndex = _annotationConfig.LastSelectedExplorerClass ?? 0;
ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem; ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem;
await ReloadThumbnails(); await ReloadThumbnails();
LoadClassDistribution(); LoadClassDistribution();
SizeChanged += async (_, _) => await SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings();
StateChanged += async (_, _) => await SaveUserSettings();
RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage; RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage;
DataContext = this; DataContext = this;
}; };
@@ -118,9 +114,7 @@ public partial class DatasetExplorer
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {ThumbnailsDtos.Count}"; StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {ThumbnailsDtos.Count}";
}; };
Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.DatasetExplorer; }; ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentThumbnail!.ImagePath);
ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath);
galleryManager.ThumbnailsUpdate += thumbnailsPercentage => galleryManager.ThumbnailsUpdate += thumbnailsPercentage =>
{ {
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage); Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
@@ -136,16 +130,15 @@ public partial class DatasetExplorer
.Select(x => new .Select(x => new
{ {
x.Key, x.Key,
_config.AnnotationClassesDict[x.Key].Name, _annotationConfig.AnnotationClassesDict[x.Key].Name,
_config.AnnotationClassesDict[x.Key].Color, _annotationConfig.AnnotationClassesDict[x.Key].Color,
ClassCount = x.Count() ClassCount = x.Count()
}) })
.ToList(); .ToList();
var foregroundColor = Color.FromColor(System.Drawing.Color.Black); var foregroundColor = Color.FromColor(System.Drawing.Color.Black);
var plot = ClassDistribution.Plot;
plot.Add.Bars(data.Select(x => new Bar var bars = data.Select(x => new Bar
{ {
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
Position = -1.5 * x.Key + 1, Position = -1.5 * x.Key + 1,
@@ -154,22 +147,34 @@ public partial class DatasetExplorer
Value = x.ClassCount, Value = x.ClassCount,
CenterLabel = true, CenterLabel = true,
LabelOffset = 10 LabelOffset = 10
})); }).ToList();
ClassDistribution.Plot.Add.Bars(bars);
foreach (var x in data) foreach (var x in data)
{ {
var label = plot.Add.Text(x.Name, 50, -1.5 * x.Key + 1.1); var label = ClassDistribution.Plot.Add.Text(x.Name, 50, -1.5 * x.Key + 1.1);
label.LabelFontColor = foregroundColor; label.LabelFontColor = foregroundColor;
label.LabelFontSize = 18; label.LabelFontSize = 18;
} }
plot.Axes.AutoScale(); ClassDistribution.Plot.Axes.AutoScale();
plot.HideAxesAndGrid(); ClassDistribution.Plot.HideAxesAndGrid();
plot.FigureBackground.Color = new("#888888"); ClassDistribution.Plot.FigureBackground.Color = new("#888888");
ClassDistribution.Refresh(); ClassDistribution.Refresh();
} }
private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e)
{
var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?",
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
_galleryManager.ClearThumbnails();
_galleryManager.RefreshThumbnails();
}
private async Task EditAnnotation() private async Task EditAnnotation()
{ {
try try
@@ -187,11 +192,11 @@ public partial class DatasetExplorer
}; };
SwitchTab(toEditor: true); SwitchTab(toEditor: true);
var time = _formState.GetTime(dto.ImagePath); var time = Constants.GetTime(dto.ImagePath);
ExplorerEditor.RemoveAllAnns(); ExplorerEditor.RemoveAllAnns();
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{ {
var annClass = _config.AnnotationClassesDict[ann.ClassNumber]; var annClass = _annotationConfig.AnnotationClassesDict[ann.ClassNumber];
var canvasLabel = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize); var canvasLabel = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel); ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
} }
@@ -217,7 +222,7 @@ public partial class DatasetExplorer
AnnotationsTab.Visibility = Visibility.Collapsed; AnnotationsTab.Visibility = Visibility.Collapsed;
EditorTab.Visibility = Visibility.Visible; EditorTab.Visibility = Visibility.Visible;
_tempSelectedClassIdx = LvClasses.SelectedIndex; _tempSelectedClassIdx = LvClasses.SelectedIndex;
LvClasses.ItemsSource = _config.AnnotationClasses; LvClasses.ItemsSource = _annotationConfig.AnnotationClasses;
Switcher.SelectedIndex = 1; Switcher.SelectedIndex = 1;
LvClasses.SelectedIndex = Math.Max(0, _tempSelectedClassIdx - 1); LvClasses.SelectedIndex = Math.Max(0, _tempSelectedClassIdx - 1);
@@ -232,16 +237,6 @@ public partial class DatasetExplorer
} }
} }
private async Task SaveUserSettings()
{
_config.DatasetExplorerConfig = this.GetConfig();
await ThrottleExt.Throttle(() =>
{
_configRepository.Save(_config);
return Task.CompletedTask;
}, TimeSpan.FromSeconds(5));
}
private void DeleteAnnotations() private void DeleteAnnotations()
{ {
var tempSelected = ThumbnailsView.SelectedIndex; var tempSelected = ThumbnailsView.SelectedIndex;
@@ -266,10 +261,10 @@ public partial class DatasetExplorer
LoadingAnnsCaption.Visibility = Visibility.Visible; LoadingAnnsCaption.Visibility = Visibility.Visible;
LoadingAnnsBar.Visibility = Visibility.Visible; LoadingAnnsBar.Visibility = Visibility.Visible;
if (!Directory.Exists(_config.ThumbnailsDirectory)) if (!Directory.Exists(_directoriesConfig.ThumbnailsDirectory))
return; return;
var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg"); var thumbnails = Directory.GetFiles(_directoriesConfig.ThumbnailsDirectory, "*.jpg");
var thumbnailDtos = new List<ThumbnailDto>(); var thumbnailDtos = new List<ThumbnailDto>();
for (int i = 0; i < thumbnails.Length; i++) for (int i = 0; i < thumbnails.Length; i++)
{ {
@@ -293,11 +288,11 @@ public partial class DatasetExplorer
{ {
try try
{ {
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.THUMBNAIL_PREFIX.Length]; var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Constants.THUMBNAIL_PREFIX.Length];
var imagePath = Path.Combine(_config.ImagesDirectory, name); var imagePath = Path.Combine(_directoriesConfig.ImagesDirectory, name);
var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"); var labelPath = Path.Combine(_directoriesConfig.LabelsDirectory, $"{name}.txt");
foreach (var f in _config.ImageFormats) foreach (var f in _annotationConfig.ImageFormats)
{ {
var curName = $"{imagePath}.{f}"; var curName = $"{imagePath}.{f}";
if (File.Exists(curName)) if (File.Exists(curName))
@@ -1,14 +1,15 @@
using System.IO; using System.IO;
using System.Windows.Input; using System.Windows.Input;
using Azaion.Annotator.DTO; using Azaion.Common.DTO;
using MediatR; using MediatR;
using Microsoft.Extensions.Options;
namespace Azaion.Annotator; namespace Azaion.Dataset;
public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalleryManager galleryManager, IOptions<DirectoriesConfig> directoriesConfig)
Config config, :
IGalleryManager galleryManager, INotificationHandler<KeyEvent>,
FormState formState) : INotificationHandler<KeyEvent> INotificationHandler<ImageCreatedEvent>
{ {
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new() private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{ {
@@ -20,7 +21,7 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer,
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken) public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
{ {
if (formState.ActiveWindow != WindowsEnum.DatasetExplorer) if (keyEvent.WindowEnum != WindowEnum.Annotator)
return; return;
var key = keyEvent.Args.Key; var key = keyEvent.Args.Key;
@@ -50,7 +51,7 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer,
.Select(x => new YoloLabel(x.Info, datasetExplorer.ExplorerEditor.RenderSize, datasetExplorer.ExplorerEditor.RenderSize)) .Select(x => new YoloLabel(x.Info, datasetExplorer.ExplorerEditor.RenderSize, datasetExplorer.ExplorerEditor.RenderSize))
.ToList(); .ToList();
await YoloLabel.WriteToFile(currentAnns, Path.Combine(config.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath)); await YoloLabel.WriteToFile(currentAnns, Path.Combine(directoriesConfig.Value.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath));
await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath); await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath);
await galleryManager.SaveLabelsCache(); await galleryManager.SaveLabelsCache();
datasetExplorer.CurrentThumbnail.UpdateImage(); datasetExplorer.CurrentThumbnail.UpdateImage();
@@ -67,4 +68,9 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer,
break; break;
} }
} }
public Task Handle(ImageCreatedEvent notification, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
} }
@@ -1,26 +1,37 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Drawing; using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common;
using Azaion.Common.DTO;
using Azaion.Common.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using Color = System.Drawing.Color; using Color = System.Drawing.Color;
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions; using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
using Size = System.Windows.Size; using Size = System.Windows.Size;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using Azaion.Common.DTO.Config;
namespace Azaion.Annotator; namespace Azaion.Dataset;
public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage); public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
public class GalleryManager : IGalleryManager public class GalleryManager(
IOptions<DirectoriesConfig> directoriesConfig,
IOptions<ThumbnailConfig> thumbnailConfig,
IOptions<AnnotationConfig> annotationConfig,
ILogger<GalleryManager> logger) : IGalleryManager
{ {
private readonly ILogger<GalleryManager> _logger; private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value;
private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value;
private readonly AnnotationConfig _annotationConfig = annotationConfig.Value;
private readonly string _thumbnailsCacheFile = Path.Combine(directoriesConfig.Value.ThumbnailsDirectory, Constants.THUMBNAILS_CACHE_FILE);
public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate; public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
private readonly string _thumbnailsCacheFile;
private readonly SemaphoreSlim _updateLock = new(1); private readonly SemaphoreSlim _updateLock = new(1);
@@ -28,7 +39,6 @@ public class GalleryManager : IGalleryManager
public ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; } = new(); public ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; } = new();
private DirectoryInfo? _thumbnailsDirectory; private DirectoryInfo? _thumbnailsDirectory;
private readonly Config _config;
private DirectoryInfo ThumbnailsDirectory private DirectoryInfo ThumbnailsDirectory
{ {
@@ -37,24 +47,17 @@ public class GalleryManager : IGalleryManager
if (_thumbnailsDirectory != null) if (_thumbnailsDirectory != null)
return _thumbnailsDirectory; return _thumbnailsDirectory;
var dir = new DirectoryInfo(_config.ThumbnailsDirectory); var dir = new DirectoryInfo(_dirConfig.ThumbnailsDirectory);
if (!dir.Exists) if (!dir.Exists)
Directory.CreateDirectory(_config.ThumbnailsDirectory); Directory.CreateDirectory(_dirConfig.ThumbnailsDirectory);
_thumbnailsDirectory = new DirectoryInfo(_config.ThumbnailsDirectory); _thumbnailsDirectory = new DirectoryInfo(_dirConfig.ThumbnailsDirectory);
return _thumbnailsDirectory; return _thumbnailsDirectory;
} }
} }
public GalleryManager(Config config, ILogger<GalleryManager> logger)
{
_config = config;
_logger = logger;
_thumbnailsCacheFile = Path.Combine(config.ThumbnailsDirectory, Config.THUMBNAILS_CACHE_FILE);
}
public void ClearThumbnails() public void ClearThumbnails()
{ {
foreach(var file in new DirectoryInfo(_config.ThumbnailsDirectory).GetFiles()) foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
file.Delete(); file.Delete();
} }
@@ -63,7 +66,7 @@ public class GalleryManager : IGalleryManager
await _updateLock.WaitAsync(); await _updateLock.WaitAsync();
try try
{ {
var prefixLen = Config.THUMBNAIL_PREFIX.Length; var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
var thumbnails = ThumbnailsDirectory.GetFiles() var thumbnails = ThumbnailsDirectory.GetFiles()
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen]) .Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
@@ -80,7 +83,7 @@ public class GalleryManager : IGalleryManager
else else
LabelsCache = new ConcurrentDictionary<string, LabelInfo>(); LabelsCache = new ConcurrentDictionary<string, LabelInfo>();
var files = new DirectoryInfo(_config.ImagesDirectory).GetFiles(); var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles();
var imagesCount = files.Length; var imagesCount = files.Length;
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) => await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
@@ -94,7 +97,7 @@ public class GalleryManager : IGalleryManager
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}"); logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}");
} }
}, new ParallelOptions }, new ParallelOptions
{ {
@@ -126,11 +129,11 @@ public class GalleryManager : IGalleryManager
{ {
try try
{ {
var width = (int)_config.ThumbnailConfig.Size.Width; var width = (int)_thumbnailConfig.Size.Width;
var height = (int)_config.ThumbnailConfig.Size.Height; var height = (int)_thumbnailConfig.Size.Height;
var imgName = Path.GetFileName(imgPath); var imgName = Path.GetFileName(imgPath);
var labelName = Path.Combine(_config.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt"); var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt");
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken))); var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken)));
@@ -145,7 +148,7 @@ public class GalleryManager : IGalleryManager
if (!File.Exists(labelName)) if (!File.Exists(labelName))
{ {
File.Delete(imgPath); File.Delete(imgPath);
_logger.LogInformation($"No labels found for image {imgName}! Image deleted!"); logger.LogInformation($"No labels found for image {imgName}! Image deleted!");
return null; return null;
} }
@@ -157,7 +160,7 @@ public class GalleryManager : IGalleryManager
AddToCache(imgPath, classes); AddToCache(imgPath, classes);
var thumbWhRatio = width / (float)height; var thumbWhRatio = width / (float)height;
var border = _config.ThumbnailConfig.Border; var border = _thumbnailConfig.Border;
var frameX = 0.0; var frameX = 0.0;
var frameY = 0.0; var frameY = 0.0;
@@ -196,14 +199,14 @@ public class GalleryManager : IGalleryManager
foreach (var label in labels) foreach (var label in labels)
{ {
var color = _config.AnnotationClassesDict[label.ClassNumber].Color; var color = _annotationConfig.AnnotationClassesDict[label.ClassNumber].Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B)); var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale)); var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
g.FillRectangle(brush, rectangle); g.FillRectangle(brush, rectangle);
} }
var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Config.THUMBNAIL_PREFIX}.jpg"); var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Constants.THUMBNAIL_PREFIX}.jpg");
bitmap.Save(thumbnailName, ImageFormat.Jpeg); bitmap.Save(thumbnailName, ImageFormat.Jpeg);
return new ThumbnailDto return new ThumbnailDto
@@ -216,7 +219,7 @@ public class GalleryManager : IGalleryManager
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, e.Message); logger.LogError(e, e.Message);
return null; return null;
} }
} }
@@ -2,15 +2,14 @@
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Azaion.Annotator.Extensions;
namespace Azaion.Annotator.DTO; namespace Azaion.Dataset;
public class ThumbnailDto : INotifyPropertyChanged public class ThumbnailDto : INotifyPropertyChanged
{ {
public string ThumbnailPath { get; set; } public string ThumbnailPath { get; set; } = null!;
public string ImagePath { get; set; } public string ImagePath { get; set; } = null!;
public string LabelPath { get; set; } public string LabelPath { get; set; } = null!;
public DateTime ImageDate { get; set; } public DateTime ImageDate { get; set; }
private BitmapImage? _image; private BitmapImage? _image;
@@ -1,4 +1,4 @@
<Application x:Class="Azaion.Annotator.App" <Application x:Class="Azaion.Suite.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources> <Application.Resources>
@@ -1,18 +1,26 @@
using System.Data; using System.Reflection;
using System.Reflection;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading; using System.Windows.Threading;
using Azaion.Annotator;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Azaion.Suite.Services;
using Azaion.Suite.Services.DTO;
using Azaion.Dataset;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog; using Serilog;
using Azaion.Annotator.Extensions;
namespace Azaion.Annotator; namespace Azaion.Suite;
public partial class App : Application public partial class App : Application
{ {
@@ -32,12 +40,21 @@ public partial class App : Application
.CreateLogger(); .CreateLogger();
_host = Host.CreateDefaultBuilder() _host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) => config
.AddCommandLine(Environment.GetCommandLineArgs())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true))
.ConfigureServices((context, services) => .ConfigureServices((context, services) =>
{ {
services.AddSingleton<MainWindow>(); services.AddSingleton<Loader>();
services.AddSingleton<HelpWindow>(); services.AddSingleton<IHardwareService, HardwareService>();
services.AddSingleton<IResourceLoader, ResourceLoader>();
services.Configure<ApiConfig>(context.Configuration.GetSection(nameof(ApiConfig)));
services.AddSingleton<IConfigUpdater, ConfigUpdater>();
services.AddSingleton<Annotator.Annotator>();
services.AddSingleton<DatasetExplorer>(); services.AddSingleton<DatasetExplorer>();
services.AddSingleton<IGalleryManager, GalleryManager>(); services.AddSingleton<HelpWindow>();
services.AddSingleton<IAIDetector, YOLODetector>(); services.AddSingleton<IAIDetector, YOLODetector>();
services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddSingleton<LibVLC>(_ => new LibVLC()); services.AddSingleton<LibVLC>(_ => new LibVLC());
@@ -47,29 +64,36 @@ public partial class App : Application
var libVLC = sp.GetRequiredService<LibVLC>(); var libVLC = sp.GetRequiredService<LibVLC>();
return new MediaPlayer(libVLC); return new MediaPlayer(libVLC);
}); });
services.AddSingleton<IConfigRepository, FileConfigRepository>(); services.AddSingleton<AnnotatorEventHandler>();
services.AddSingleton<Config>(sp => sp.GetRequiredService<IConfigRepository>().Get());
services.AddSingleton<MainWindowEventHandler>();
services.AddSingleton<VLCFrameExtractor>(); services.AddSingleton<VLCFrameExtractor>();
services.AddHttpClient<AzaionApiClient>((sp, client) =>
{
var apiConfig = sp.GetRequiredService<IOptions<ApiConfig>>().Value;
client.BaseAddress = new Uri(apiConfig.Url);
client.Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds);
});
services.AddSingleton<DatasetExplorer>();
services.AddSingleton<IGalleryManager, GalleryManager>();
}) })
.UseSerilog()
.Build(); .Build();
_mediator = _host.Services.GetRequiredService<IMediator>(); _mediator = _host.Services.GetRequiredService<IMediator>();
_logger = _host.Services.GetRequiredService<ILogger<App>>(); _logger = _host.Services.GetRequiredService<ILogger<App>>();
DispatcherUnhandledException += OnDispatcherUnhandledException; DispatcherUnhandledException += OnDispatcherUnhandledException;
} }
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{ {
_logger.LogError(e.Exception, e.Exception.Message); _logger.LogError(e.Exception, e.Exception.Message);
e.Handled = true; e.Handled = true;
} }
protected override void OnStartup(StartupEventArgs e) protected override async void OnStartup(StartupEventArgs e)
{ {
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.KeyDownEvent, new RoutedEventHandler(GlobalClick)); EventManager.RegisterClassHandler(typeof(UIElement), UIElement.KeyDownEvent, new RoutedEventHandler(GlobalClick));
_host.Start(); await _host.StartAsync();
_host.Services.GetRequiredService<MainWindow>().Show(); _host.Services.GetRequiredService<Loader>().Show();
base.OnStartup(e); base.OnStartup(e);
} }
@@ -77,6 +101,14 @@ public partial class App : Application
private void GlobalClick(object sender, RoutedEventArgs e) private void GlobalClick(object sender, RoutedEventArgs e)
{ {
var args = (KeyEventArgs)e; var args = (KeyEventArgs)e;
_ = ThrottleExt.Throttle(() => _mediator.Publish(new KeyEvent(sender, args)), TimeSpan.FromMilliseconds(50)); var keyEvent = new KeyEvent(sender, args, (sender as FrameworkElement).GetParentWindow());
_ = ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), TimeSpan.FromMilliseconds(50));
}
protected override async void OnExit(ExitEventArgs e)
{
await _host.StopAsync();
_host.Dispose();
base.OnExit(e);
} }
} }
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+37
View File
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<ApplicationIcon>..\logo.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Azaion.Annotator\Azaion.Annotator.csproj" />
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
<ProjectReference Include="..\Azaion.Dataset\Azaion.Dataset.csproj" />
</ItemGroup>
</Project>
+29
View File
@@ -0,0 +1,29 @@
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Options;
namespace Azaion.Suite;
public class DynamicAssemblyLoader(IOptions<LocalFilesConfig> localFilesConfig) : AssemblyLoadContext
{
private static readonly Dictionary<string?, Assembly> LoadedAssemblies = new();
static DynamicAssemblyLoader()
{
LoadedAssemblies = Default.Assemblies.ToDictionary(a => a.GetName().Name, a => a);
}
protected override Assembly Load(AssemblyName assemblyName)
{
var assembly = LoadedAssemblies.GetValueOrDefault(assemblyName.Name);
if (assembly != null)
return assembly;
var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var asm = Assembly.LoadFile(Path.Combine(currentLocation, localFilesConfig.Value.DllPath, $"{assemblyName.Name!}.dll"));
return asm;
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace Azaion.Suite;
public class HardwareInfo
{
public string CPU { get; set; } = null!;
public string GPU { get; set; } = null!;
public string Memory { get; set; } = null!;
public string Hash { get; set; } = null!;
}
+126
View File
@@ -0,0 +1,126 @@
<Window x:Class="Azaion.Suite.Loader"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
WindowStartupLocation="CenterScreen"
Title="Azaion Annotator Security"
Height="280" Width="350">
<Border Width="350"
Height="280"
Background="DarkGray"
CornerRadius="15"
MouseMove="MainMouseMove">
<Border.Effect>
<DropShadowEffect BlurRadius="15"
Direction ="-90"
RenderingBias ="Quality"
ShadowDepth ="2"
Color ="Gray" />
</Border.Effect>
<StackPanel Orientation="Vertical"
Margin="20">
<Canvas>
<Button Padding="5" ToolTip="Закрити" Background="DarkGray" BorderBrush="DarkGray" Canvas.Left="290" Cursor="Hand"
Name="CloseBtn"
Click="CloseClick">
<Path Stretch="Fill" Fill="LightGray" Data="M5.29289 5.29289C5.68342 4.90237 6.31658 4.90237 6.70711 5.29289L12 10.5858L17.2929 5.29289C17.6834 4.90237 18.3166
4.90237 18.7071 5.29289C19.0976 5.68342 19.0976 6.31658 18.7071 6.70711L13.4142 12L18.7071 17.2929C19.0976 17.6834 19.0976 18.3166 18.7071 18.7071C18.3166
19.0976 17.6834 19.0976 17.2929 18.7071L12 13.4142L6.70711 18.7071C6.31658 19.0976 5.68342 19.0976 5.29289 18.7071C4.90237 18.3166 4.90237 17.6834 5.29289
17.2929L10.5858 12L5.29289 6.70711C4.90237 6.31658 4.90237 5.68342 5.29289 5.29289Z" />
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
</Canvas>
<TextBlock Text="Вхід"
FontSize="25"
HorizontalAlignment="Center"
VerticalAlignment="Top"
FontWeight="Bold"
/>
<Grid VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="Email"
Grid.Row="0"
Margin="0, 20, 0, 5"
HorizontalAlignment="Left"/>
<TextBox
Name="TbEmail"
Grid.Row="1"
Padding="0,5"
Width="300"
FontSize="16"
Background="DarkGray"
BorderBrush="DimGray"
BorderThickness="0,0,0,1"
HorizontalAlignment="Left"
Text="admin@azaion.com"/>
<TextBlock Text="Пароль"
Grid.Row="2"
Margin="0, 20, 0, 5"
HorizontalAlignment="Left"/>
<PasswordBox Grid.Row="3"
Name="TbPassword"
Password="Az@1on1000Odm$n"
FontSize="16"
Background="DarkGray"
BorderBrush="DimGray"
Padding="0,5"
Width="300"
BorderThickness="0,0,0,1"
HorizontalAlignment="Left"/>
</Grid>
<Button x:Name="LoginBtn"
Content="Вхід"
Foreground="White"
Background="DimGray"
Margin="0,25"
Height="35"
Width="280"
Cursor="Hand"
Click="RunClick">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="LoginBorder" Background="{TemplateBinding Background}"
CornerRadius="16">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightGray" TargetName="LoginBorder" />
<Setter Property="TextBlock.Foreground" Value="Black" TargetName="LoginBorder" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
</StackPanel>
</Border>
</Window>
+58
View File
@@ -0,0 +1,58 @@
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Options;
namespace Azaion.Suite;
public partial class Loader : Window
{
private readonly IResourceLoader _resourceLoader;
private readonly IOptions<LocalFilesConfig> _localFilesConfig;
public Loader(IResourceLoader resourceLoader, IOptions<LocalFilesConfig> localFilesConfig)
{
_resourceLoader = resourceLoader;
_localFilesConfig = localFilesConfig;
InitializeComponent();
}
private async void RunClick(object sender, RoutedEventArgs e)
{
var stream = new MemoryStream();
await _resourceLoader.LoadAnnotator(TbEmail.Text, TbPassword.Password, stream);
stream.Seek(0, SeekOrigin.Begin);
var loader = new AssemblyLoadContext("DynamicContext", isCollectible: true);
var annotatorAssembly = loader.LoadFromStream(stream);
var appType = annotatorAssembly.GetType("Azaion.Annotator.App");
var appInstance = Activator.CreateInstance(appType);
var runMethod = appType.GetMethod("Run", BindingFlags.Public | BindingFlags.Instance);
if (runMethod != null)
{
runMethod.Invoke(appInstance, null);
}
// var entryPoint = annotatorAssembly.EntryPoint;
// if (entryPoint == null)
// return;
//
// var o = annotatorAssembly.CreateInstance(entryPoint.Name);
// entryPoint.Invoke(o, null);
}
private void CloseClick(object sender, RoutedEventArgs e) => Close();
private void MainMouseMove(object sender, MouseEventArgs e)
{
if (e.OriginalSource is Button || e.OriginalSource is TextBox)
return;
if (e.LeftButton == MouseButtonState.Pressed)
DragMove();
}
}
+12
View File
@@ -0,0 +1,12 @@
<Window x:Class="Azaion.Suite.MainSuite"
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:local="clr-namespace:Azaion.Suite"
mc:Ignorable="d"
Title="MainSuite" Height="450" Width="800">
<Grid>
</Grid>
</Window>
+59
View File
@@ -0,0 +1,59 @@
using System.IO;
using System.Windows;
using Azaion.Annotator.Extensions;
using Azaion.Common.DTO.Config;
using Microsoft.Extensions.Options;
namespace Azaion.Suite;
public partial class MainSuite : Window
{
private readonly AppConfig _appConfig;
private readonly IConfigUpdater _configUpdater;
public MainSuite(IOptions<AppConfig> appConfig, IConfigUpdater configUpdater)
{
_configUpdater = configUpdater;
_appConfig = appConfig.Value;
InitializeComponent();
Loaded += OnLoaded;
Closed += OnFormClosed;
SizeChanged += async (_, _) => await SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings();
StateChanged += async (_, _) => await SaveUserSettings();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (!Directory.Exists(_appConfig.DirectoriesConfig.LabelsDirectory))
Directory.CreateDirectory(_appConfig.DirectoriesConfig.LabelsDirectory);
if (!Directory.Exists(_appConfig.DirectoriesConfig.ImagesDirectory))
Directory.CreateDirectory(_appConfig.DirectoriesConfig.ImagesDirectory);
if (!Directory.Exists(_appConfig.DirectoriesConfig.ResultsDirectory))
Directory.CreateDirectory(_appConfig.DirectoriesConfig.ResultsDirectory);
Left = _appConfig.WindowConfig.WindowLocation.X;
Top = _appConfig.WindowConfig.WindowLocation.Y;
Width = _appConfig.WindowConfig.WindowSize.Width;
Height = _appConfig.WindowConfig.WindowSize.Height;
if (_appConfig.WindowConfig.FullScreen)
WindowState = WindowState.Maximized;
}
private async Task SaveUserSettings()
{
await ThrottleExt.Throttle(() =>
{
_configUpdater.Save(_appConfig);
return Task.CompletedTask;
}, TimeSpan.FromSeconds(5));
}
private void OnFormClosed(object? sender, EventArgs e)
{
_configUpdater.Save(_appConfig);
}
}
+38
View File
@@ -0,0 +1,38 @@
using System.IO;
using System.Reflection;
using Azaion.Suite.Services;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Options;
namespace Azaion.Suite;
public interface IResourceLoader
{
Task LoadAnnotator(string email, string password, Stream outStream, CancellationToken cancellationToken = default);
Assembly LoadAssembly(string name, CancellationToken cancellationToken = default);
}
public class ResourceLoader(AzaionApiClient azaionApi, IHardwareService hardwareService, IOptions<LocalFilesConfig> localFilesConfig) : IResourceLoader
{
public async Task LoadAnnotator(string email, string password, Stream outStream, CancellationToken cancellationToken = default)
{
var hardwareInfo = await hardwareService.GetHardware();
azaionApi.Login(email, password);
var key = Security.MakeEncryptionKey(email, password, hardwareInfo.Hash);
var encryptedStream = await azaionApi.GetResource(password, hardwareInfo, ResourceEnum.AnnotatorDll);
await encryptedStream.DecryptTo(outStream, key, cancellationToken);
//return Assembly.Load(stream.ToArray());
}
public Assembly LoadAssembly(string name, CancellationToken cancellationToken = default)
{
var dllValues = name.Split(",");
var dllName = $"{dllValues[0]}.dll";
var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var asm = Assembly.LoadFile(Path.Combine(currentLocation, localFilesConfig.Value.DllPath, dllName));
return asm;
}
}
+85
View File
@@ -0,0 +1,85 @@
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security;
using System.Text;
using Azaion.Suite.Services.DTO;
using Newtonsoft.Json;
namespace Azaion.Suite.Services;
public class AzaionApiClient(HttpClient httpClient)
{
const string JSON_MEDIA = "application/json";
private string Email { get; set; } = null!;
private SecureString Password { get; set; } = new();
private string JwtToken { get; set; } = null!;
public void Login(string email, string password)
{
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password))
throw new Exception("Email or password is empty!");
Email = email;
Password = password.ToSecureString();
}
public async Task<Stream> GetResource(string password, HardwareInfo hardware, ResourceEnum resourceEnum)
{
var response = await Send(httpClient, new HttpRequestMessage(HttpMethod.Post, "/resources/get")
{
Content = new StringContent(JsonConvert.SerializeObject(new { password, hardware, resourceEnum }), Encoding.UTF8, JSON_MEDIA)
});
return await response.Content.ReadAsStreamAsync();
}
private async Task<string> Authorize()
{
if (string.IsNullOrEmpty(Email) || Password.Length == 0)
throw new Exception("Email or password is empty! Please do Login first!");
var payload = new
{
email = Email,
password = Password.ToRealString()
};
var response = await httpClient.PostAsync(
"login",
new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, JSON_MEDIA));
if (!response.IsSuccessStatusCode)
throw new Exception($"Login failed: {response.StatusCode}");
var responseData = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<LoginResponse>(responseData);
if (string.IsNullOrEmpty(result?.Token))
throw new Exception("JWT Token not found in response");
return result.Token;
}
private async Task<HttpResponseMessage> Send(HttpClient client, HttpRequestMessage request)
{
if (string.IsNullOrEmpty(JwtToken))
JwtToken = await Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken);
var response = await client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
JwtToken = await Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken);
response = await client.SendAsync(request);
}
if (response.IsSuccessStatusCode)
return response;
var result = await response.Content.ReadAsStringAsync();
throw new Exception($"Failed: {response.StatusCode}! Result: {result}");
}
}
@@ -0,0 +1,9 @@
namespace Azaion.Suite.Services.DTO;
public enum ResourceEnum
{
None = 0,
AnnotatorDll = 10,
AIModelRKNN = 20,
AIModelONNX = 30,
}
+107
View File
@@ -0,0 +1,107 @@
using System.Diagnostics;
using System.Net.NetworkInformation;
using System.Security.Cryptography;
using System.Text;
namespace Azaion.Suite.Services;
public interface IHardwareService
{
Task<HardwareInfo> GetHardware();
}
public class HardwareService : IHardwareService
{
private const string WIN32_GET_HARDWARE_COMMAND =
"wmic OS get TotalVisibleMemorySize /Value && " +
"wmic CPU get Name /Value && " +
"wmic path Win32_VideoController get Name /Value";
private const string UNIX_GET_HARDWARE_COMMAND =
"/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " +
"lscpu | grep 'Model name:' | cut -d':' -f2 && " +
"lspci | grep VGA | cut -d':' -f3\"";
public async Task<HardwareInfo> GetHardware()
{
try
{
var output = await RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT
? WIN32_GET_HARDWARE_COMMAND
: UNIX_GET_HARDWARE_COMMAND);
var lines = output
.Replace("TotalVisibleMemorySize=", "")
.Replace("Name=", "")
.Replace(" ", " ")
.Trim()
.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
var memoryStr = "Unknown RAM";
if (lines.Length > 0)
{
memoryStr = lines[0];
if (int.TryParse(memoryStr, out var memKb))
memoryStr = $"{Math.Round(memKb / 1024.0 / 1024.0)} Gb";
}
var hardwareInfo = new HardwareInfo
{
Memory = memoryStr,
CPU = lines.Length > 1 && string.IsNullOrEmpty(lines[1])
? "Unknown RAM"
: lines[1],
GPU = lines.Length > 2 && string.IsNullOrEmpty(lines[2])
? "Unknown GPU"
: lines[2]
};
hardwareInfo.Hash = ToHash($"Azaion_{MacAddress()}_{hardwareInfo.CPU}_{hardwareInfo.GPU}");
return hardwareInfo;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
}
private string MacAddress()
{
var macAddress = NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == OperationalStatus.Up)
.Select(nic => nic.GetPhysicalAddress().ToString())
.FirstOrDefault();
return macAddress ?? string.Empty;
}
private async Task<string> RunCommand(string command)
{
try
{
using var process = new Process();
process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe";
process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix
? $"-c \"{command}\""
: $"/c {command}";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.Start();
var result = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return result.Trim();
}
catch
{
return string.Empty;
}
}
private static string ToHash(string str) =>
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
}
+83
View File
@@ -0,0 +1,83 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
using System.Text;
namespace Azaion.Suite.Services;
public static class Security
{
private const int BUFFER_SIZE = 524288; // 512 KB buffer size
public static string ToHash(this string str) =>
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
public static string MakeEncryptionKey(string email, string password, string? hardwareHash) =>
$"{email}-{password}-{hardwareHash}-#%@AzaionKey@%#---".ToHash();
public static SecureString ToSecureString(this string str)
{
var secureString = new SecureString();
foreach (var c in str.ToCharArray())
secureString.AppendChar(c);
return secureString;
}
public static string? ToRealString(this SecureString value)
{
var valuePtr = IntPtr.Zero;
try
{
valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
return Marshal.PtrToStringUni(valuePtr);
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
}
}
public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default)
{
if (stream is { CanRead: false }) throw new ArgumentNullException(nameof(stream));
if (key is not { Length: > 0 }) throw new ArgumentNullException(nameof(key));
using var aes = Aes.Create();
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
await using var cs = new CryptoStream(toStream, encryptor, CryptoStreamMode.Write, leaveOpen: true);
// Prepend IV to the encrypted data
await toStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken);
var buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken)) > 0)
await cs.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
}
public static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken cancellationToken = default)
{
using var aes = Aes.Create();
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
// Read the IV from the start of the input stream
var iv = new byte[aes.BlockSize / 8];
_ = await encryptedStream.ReadAsync(iv, cancellationToken);
aes.IV = iv;
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read, leaveOpen: true);
// Read and write in chunks
var buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = await cryptoStream.ReadAsync(buffer, cancellationToken)) > 0)
await toStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"ApiConfig": {
"Url": "https://api.azaion.com",
"TimeoutSeconds": 20,
"RetryCount": 3
},
"LocalFilesConfig": {
"DllPath": "AzaionSuite"
}
}
@@ -4,10 +4,12 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>Azaion.Annotator.Test</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Azaion.Annotator\Azaion.Annotator.csproj" /> <ProjectReference Include="..\Azaion.Annotator\Azaion.Annotator.csproj" />
<ProjectReference Include="..\Azaion.Suite\Azaion.Suite.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -1,4 +1,5 @@
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Common.DTO;
using Xunit; using Xunit;
namespace Azaion.Annotator.Test; namespace Azaion.Annotator.Test;
+16
View File
@@ -0,0 +1,16 @@
using Azaion.Suite;
using Azaion.Suite.Services;
using Xunit;
namespace Azaion.Annotator.Test;
public class HardwareServiceTest
{
[Fact]
public async Task GetHardware_Test()
{
var hardwareService = new HardwareService();
var hw = await hardwareService.GetHardware();
Console.WriteLine(hw);
}
}