mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 11:36:31 +00:00
rework to Azaion.Suite
This commit is contained in:
+19
-1
@@ -2,7 +2,13 @@
|
||||
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}"
|
||||
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
|
||||
Global
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
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: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"
|
||||
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"
|
||||
Title="Azaion Annotator" Height="450" Width="1100"
|
||||
>
|
||||
@@ -85,12 +87,6 @@
|
||||
<MenuItem x:Name="OpenFolderItem"
|
||||
Foreground="Black"
|
||||
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 Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0">
|
||||
<MenuItem x:Name="OpenHelpWindow"
|
||||
@@ -179,11 +175,11 @@
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
<controls:AnnotationClasses
|
||||
<controls1:AnnotationClasses
|
||||
x:Name="LvClasses"
|
||||
Grid.Column="0"
|
||||
Grid.Row="4">
|
||||
</controls:AnnotationClasses>
|
||||
</controls1:AnnotationClasses>
|
||||
|
||||
<GridSplitter
|
||||
Background="DarkGray"
|
||||
@@ -201,7 +197,7 @@
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="4"
|
||||
x:Name="VideoView">
|
||||
<controls:CanvasEditor x:Name="Editor"
|
||||
<controls1:CanvasEditor x:Name="Editor"
|
||||
Background="#01000000"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" />
|
||||
@@ -261,9 +257,9 @@
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<LinearGradientBrush StartPoint="0 0 " EndPoint="1 0">
|
||||
<GradientStop Offset="0.3" Color="{Binding Path=ClassColor1}" />
|
||||
<GradientStop Offset="0.5" Color="{Binding Path=ClassColor2}" />
|
||||
<GradientStop Offset="0.8" Color="{Binding Path=ClassColor3}" />
|
||||
<GradientStop Offset="0.3" Color="{Binding Path=ClassColor0}" />
|
||||
<GradientStop Offset="0.5" Color="{Binding Path=ClassColor1}" />
|
||||
<GradientStop Offset="0.8" Color="{Binding Path=ClassColor2}" />
|
||||
<GradientStop Offset="0.99" Color="{Binding Path=ClassColor3}" />
|
||||
</LinearGradientBrush>
|
||||
</Setter.Value>
|
||||
@@ -275,12 +271,12 @@
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
<controls:UpdatableProgressBar x:Name="VideoSlider"
|
||||
<controls2:UpdatableProgressBar x:Name="VideoSlider"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Background="#252525"
|
||||
Foreground="LightBlue">
|
||||
</controls:UpdatableProgressBar>
|
||||
</controls2:UpdatableProgressBar>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Grid
|
||||
@@ -469,14 +465,14 @@
|
||||
</Image>
|
||||
</Button>
|
||||
|
||||
<controls:UpdatableProgressBar
|
||||
<controls2:UpdatableProgressBar
|
||||
x:Name="Volume"
|
||||
Grid.Column="9"
|
||||
Width="70" Height="15"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="#252525" BorderBrush="#252525" Foreground="LightBlue"
|
||||
Maximum="100" Minimum="0">
|
||||
</controls:UpdatableProgressBar>
|
||||
</controls2:UpdatableProgressBar>
|
||||
|
||||
<Button
|
||||
x:Name="AIDetectBtn"
|
||||
@@ -7,9 +7,12 @@ using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.Extensions;
|
||||
using LibVLCSharp.Shared;
|
||||
using MediatR;
|
||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
@@ -17,122 +20,90 @@ using Newtonsoft.Json;
|
||||
using Size = System.Windows.Size;
|
||||
using IntervalTree;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
using ScottPlot.TickGenerators.TimeUnits;
|
||||
using Serilog;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public partial class MainWindow
|
||||
public partial class Annotator
|
||||
{
|
||||
private readonly AppConfig _appConfig;
|
||||
private readonly LibVLC _libVLC;
|
||||
private readonly MediaPlayer _mediaPlayer;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly FormState _formState;
|
||||
|
||||
private readonly IConfigRepository _configRepository;
|
||||
private readonly IConfigUpdater _configUpdater;
|
||||
private readonly HelpWindow _helpWindow;
|
||||
private readonly ILogger<MainWindow> _logger;
|
||||
private readonly IGalleryManager _galleryManager;
|
||||
private readonly ILogger<Annotator> _logger;
|
||||
private readonly VLCFrameExtractor _vlcFrameExtractor;
|
||||
private readonly IAIDetector _aiDetector;
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
|
||||
private bool _suspendLayout;
|
||||
|
||||
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
|
||||
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> FilteredMediaFiles { 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,
|
||||
FormState formState,
|
||||
IConfigRepository configRepository,
|
||||
HelpWindow helpWindow,
|
||||
DatasetExplorer datasetExplorer,
|
||||
ILogger<MainWindow> logger,
|
||||
IGalleryManager galleryManager,
|
||||
ILogger<Annotator> logger,
|
||||
VLCFrameExtractor vlcFrameExtractor,
|
||||
IAIDetector aiDetector)
|
||||
{
|
||||
InitializeComponent();
|
||||
_appConfig = appConfig.Value;
|
||||
_configUpdater = configUpdater;
|
||||
_libVLC = libVLC;
|
||||
_mediaPlayer = mediaPlayer;
|
||||
_mediator = mediator;
|
||||
_formState = formState;
|
||||
_configRepository = configRepository;
|
||||
_config = _configRepository.Get();
|
||||
_helpWindow = helpWindow;
|
||||
_datasetExplorer = datasetExplorer;
|
||||
_logger = logger;
|
||||
_galleryManager = galleryManager;
|
||||
_vlcFrameExtractor = vlcFrameExtractor;
|
||||
_aiDetector = aiDetector;
|
||||
|
||||
VideoView.Loaded += VideoView_Loaded;
|
||||
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);
|
||||
|
||||
Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.Main; };
|
||||
}
|
||||
|
||||
private void VideoView_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Core.Initialize();
|
||||
InitControls();
|
||||
_ = Task.Run(async () => await _galleryManager.RefreshThumbnails());
|
||||
|
||||
_suspendLayout = true;
|
||||
|
||||
Left = _config.MainWindowConfig.WindowLocation.X;
|
||||
Top = _config.MainWindowConfig.WindowLocation.Y;
|
||||
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;
|
||||
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.WindowConfig.LeftPanelWidth);
|
||||
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.WindowConfig.RightPanelWidth);
|
||||
|
||||
_suspendLayout = false;
|
||||
|
||||
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.SelectedIndex = 0;
|
||||
|
||||
if (LvFiles.Items.IsEmpty)
|
||||
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
|
||||
|
||||
if (_config.ShowHelpOnStart)
|
||||
if (_appConfig.WindowConfig.ShowHelpOnStart)
|
||||
_helpWindow.Show();
|
||||
}
|
||||
|
||||
@@ -194,7 +165,7 @@ public partial class MainWindow
|
||||
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
|
||||
|
||||
VideoSlider.KeyDown += (sender, args) =>
|
||||
_mediator.Publish(new KeyEvent(sender, args));
|
||||
_mediator.Publish(new KeyEvent(sender, args, WindowEnum.Annotator));
|
||||
|
||||
Volume.ValueChanged += (_, newValue) =>
|
||||
_mediator.Publish(new VolumeChangedEvent((int)newValue));
|
||||
@@ -226,9 +197,9 @@ public partial class MainWindow
|
||||
foreach (var annotationResult in res)
|
||||
{
|
||||
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(Path.Combine(_config.LabelsDirectory, $"{imgName}.txt"));
|
||||
File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt"));
|
||||
File.Delete(thumbnailPath);
|
||||
_formState.AnnotationResults.Remove(annotationResult);
|
||||
Annotations.Remove(Annotations.Query(annotationResult.Time));
|
||||
@@ -237,7 +208,6 @@ public partial class MainWindow
|
||||
}
|
||||
};
|
||||
|
||||
Editor.FormState = _formState;
|
||||
Editor.Mediator = _mediator;
|
||||
DgAnnotations.ItemsSource = _formState.AnnotationResults;
|
||||
}
|
||||
@@ -262,13 +232,12 @@ public partial class MainWindow
|
||||
if (_suspendLayout)
|
||||
return;
|
||||
|
||||
_config.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
||||
_config.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
||||
_appConfig.WindowConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
||||
_appConfig.WindowConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
||||
|
||||
_config.MainWindowConfig = this.GetConfig();
|
||||
await ThrottleExt.Throttle(() =>
|
||||
{
|
||||
_configRepository.Save(_config);
|
||||
_configUpdater.Save(_appConfig);
|
||||
return Task.CompletedTask;
|
||||
}, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
@@ -295,7 +264,7 @@ public partial class MainWindow
|
||||
if (showImage)
|
||||
{
|
||||
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))
|
||||
{
|
||||
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
|
||||
@@ -305,7 +274,7 @@ public partial class MainWindow
|
||||
}
|
||||
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);
|
||||
Editor.CreateAnnotation(annClass, time, canvasLabel);
|
||||
}
|
||||
@@ -319,7 +288,7 @@ public partial class MainWindow
|
||||
Annotations.Clear();
|
||||
Editor.RemoveAllAnns();
|
||||
|
||||
var labelDir = new DirectoryInfo(_config.LabelsDirectory);
|
||||
var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory);
|
||||
if (!labelDir.Exists)
|
||||
return;
|
||||
|
||||
@@ -327,7 +296,7 @@ public partial class MainWindow
|
||||
foreach (var file in labelFiles)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -335,12 +304,12 @@ public partial class MainWindow
|
||||
public async Task AddAnnotations(TimeSpan? time, List<YoloLabel> annotations, CancellationToken ct = default)
|
||||
=> 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 previousAnnotations = Annotations.Query(timeValue);
|
||||
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);
|
||||
if (existingResult != null)
|
||||
@@ -355,17 +324,51 @@ public partial class MainWindow
|
||||
.Select(x => x.Value + 1)
|
||||
.FirstOrDefault();
|
||||
|
||||
_formState.AnnotationResults.Insert(index, new AnnotationResult(timeValue, _formState.GetTimeName(time), annotations, _config));
|
||||
await File.WriteAllTextAsync($"{_config.ResultsDirectory}/{_formState.VideoName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults), ct);
|
||||
_formState.AnnotationResults.Insert(index, CreateAnnotationReult(timeValue, detections));
|
||||
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()
|
||||
{
|
||||
var dir = new DirectoryInfo(_config.VideosDirectory);
|
||||
var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory);
|
||||
if (!dir.Exists)
|
||||
return;
|
||||
|
||||
var labelNames = new DirectoryInfo(_config.LabelsDirectory).GetFiles()
|
||||
var labelNames = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory).GetFiles()
|
||||
.Select(x =>
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(x.Name);
|
||||
@@ -377,7 +380,7 @@ public partial class MainWindow
|
||||
.Select(gr => gr.Key)
|
||||
.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);
|
||||
media.Parse();
|
||||
@@ -392,7 +395,7 @@ public partial class MainWindow
|
||||
return fInfo;
|
||||
}).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,
|
||||
Path = x.FullName,
|
||||
@@ -402,7 +405,7 @@ public partial class MainWindow
|
||||
|
||||
AllMediaFiles = new ObservableCollection<MediaFileInfo>(videoFiles.Concat(imageFiles).ToList());
|
||||
LvFiles.ItemsSource = AllMediaFiles;
|
||||
TbFolder.Text = _config.VideosDirectory;
|
||||
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
|
||||
|
||||
BlinkHelp(AllMediaFiles.Count == 0
|
||||
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
|
||||
@@ -415,7 +418,7 @@ public partial class MainWindow
|
||||
_mediaPlayer.Stop();
|
||||
_mediaPlayer.Dispose();
|
||||
_libVLC.Dispose();
|
||||
_configRepository.Save(_config);
|
||||
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
@@ -460,7 +463,7 @@ public partial class MainWindow
|
||||
|
||||
if (!string.IsNullOrEmpty(dlg.FileName))
|
||||
{
|
||||
_config.VideosDirectory = dlg.FileName;
|
||||
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
|
||||
await SaveUserSettings();
|
||||
}
|
||||
|
||||
@@ -473,13 +476,6 @@ public partial class MainWindow
|
||||
LvFiles.ItemsSource = FilteredMediaFiles;
|
||||
}
|
||||
|
||||
private void OpenDataExplorerItemClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_datasetExplorer.Show();
|
||||
_datasetExplorer.Activate();
|
||||
}
|
||||
|
||||
|
||||
private void PlayClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_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 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)
|
||||
{
|
||||
var listItem = sender as ListViewItem;
|
||||
LvFilesContextMenu.DataContext = listItem.DataContext;
|
||||
LvFilesContextMenu.DataContext = listItem!.DataContext;
|
||||
}
|
||||
|
||||
private (TimeSpan Time, List<Detection> Detections)? _previousDetection;
|
||||
@@ -555,7 +541,7 @@ public partial class MainWindow
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using var detector = new YOLODetector(_config);
|
||||
using var detector = new YOLODetector(_appConfig.AIRecognitionConfig);
|
||||
Dispatcher.Invoke(() => _autoDetectDialog.Log("Ініціалізація AI..."));
|
||||
var prevSeekTime = 0.0;
|
||||
|
||||
@@ -601,7 +587,7 @@ public partial class MainWindow
|
||||
var prev = _previousDetection.Value;
|
||||
|
||||
// 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;
|
||||
|
||||
// Detection is earlier than previous + FrameRecognitionSeconds.
|
||||
@@ -624,11 +610,11 @@ public partial class MainWindow
|
||||
.First();
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -645,10 +631,10 @@ public partial class MainWindow
|
||||
var time = 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);
|
||||
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.RemoveAllAnns();
|
||||
@@ -656,16 +642,14 @@ public partial class MainWindow
|
||||
await AddAnnotations(timeframe.Time, detections, token);
|
||||
|
||||
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}), " +
|
||||
$"size=({det.Width:F2}, {det.Height:F2}), " +
|
||||
$"prob: {det.Probability:F1}%"));
|
||||
|
||||
Dispatcher.Invoke(() => _autoDetectDialog.Log(log));
|
||||
|
||||
var thumbnailDto = await _galleryManager.CreateThumbnail(imgPath, token);
|
||||
if (thumbnailDto != null)
|
||||
_datasetExplorer.AddThumbnail(thumbnailDto, detections.Select(x => x.ClassNumber));
|
||||
await _mediator.Publish(new ImageCreatedEvent(imgPath), token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,53 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ApplicationIcon>logo.ico</ApplicationIcon>
|
||||
<ApplicationIcon>..\logo.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="libc.translation" Version="7.1.1" />
|
||||
<PackageReference Include="LibVLCSharp" 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.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.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<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.Extensions.Hosting" 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.Sinks.Console" 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="VirtualizingWrapPanel" Version="2.0.10" />
|
||||
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
|
||||
<PackageReference Include="YoloV8.Gpu" Version="5.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="logo.ico" />
|
||||
<Resource Include="logo.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<None Update="config.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System.Windows.Media;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
|
||||
public class AnnotationResult
|
||||
{
|
||||
private readonly Config _config = null!;
|
||||
|
||||
[JsonProperty(PropertyName = "f")]
|
||||
public string Image { get; set; } = null!;
|
||||
|
||||
@@ -18,61 +19,26 @@ public class AnnotationResult
|
||||
public double Lon { get; set; }
|
||||
public List<Detection> Detections { get; set; } = new();
|
||||
|
||||
#region For Display in the grid
|
||||
#region For XAML Form
|
||||
|
||||
[JsonIgnore]
|
||||
//For XAML Form
|
||||
public string TimeStr => $"{Time:h\\:mm\\:ss}";
|
||||
|
||||
private List<int>? _detectionClasses = null!;
|
||||
|
||||
//For Form
|
||||
[JsonIgnore]
|
||||
public string ClassName
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public string ClassName { get; set; } = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Color ClassColor1 => GetAnnotationClass(0);
|
||||
public Color ClassColor0 { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Color ClassColor2 => GetAnnotationClass(1);
|
||||
public Color ClassColor1 { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Color ClassColor3 => GetAnnotationClass(2);
|
||||
public Color ClassColor2 { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Color ClassColor4 => GetAnnotationClass(3);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Color ClassColor3 { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public AnnotationResult() { }
|
||||
public AnnotationResult(TimeSpan time, string timeName, List<Detection> detections, Config config)
|
||||
{
|
||||
_config = config;
|
||||
Detections = detections;
|
||||
Time = time;
|
||||
Image = $"{timeName}.jpg";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -11,32 +11,13 @@ public class FormState
|
||||
? ""
|
||||
: Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", "");
|
||||
|
||||
public string CurrentMrl { get; set; }
|
||||
public string CurrentMrl { get; set; } = null!;
|
||||
public Size CurrentVideoSize { get; set; }
|
||||
public TimeSpan CurrentVideoLength { get; set; }
|
||||
|
||||
public TimeSpan? BackgroundTime { get; set; }
|
||||
public int CurrentVolume { get; set; } = 100;
|
||||
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
|
||||
public WindowsEnum ActiveWindow { get; set; }
|
||||
|
||||
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,14 +1,9 @@
|
||||
using System.Windows.Input;
|
||||
using Azaion.Common.DTO;
|
||||
using MediatR;
|
||||
|
||||
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 PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
|
||||
|
||||
@@ -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.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
|
||||
public static class DataGridExtensions
|
||||
{
|
||||
public static DataGridCell? GetCell(this DataGrid grid, int rowIndex, int columnIndex = 0)
|
||||
{
|
||||
var row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
|
||||
if (row == null)
|
||||
return null;
|
||||
|
||||
var presenter = FindVisualChild<DataGridCellsPresenter>(row);
|
||||
if (presenter == null)
|
||||
return null;
|
||||
|
||||
var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
||||
if (cell != null) return cell;
|
||||
|
||||
// now try to bring into view and retrieve the cell
|
||||
grid.ScrollIntoView(row, grid.Columns[columnIndex]);
|
||||
cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? dependencyObj) where T : DependencyObject
|
||||
{
|
||||
if (dependencyObj == null)
|
||||
yield break;
|
||||
|
||||
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObj); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(dependencyObj, i);
|
||||
if (child is T dependencyObject)
|
||||
{
|
||||
yield return dependencyObject;
|
||||
}
|
||||
|
||||
foreach (T childOfChild in FindVisualChildren<T>(child))
|
||||
{
|
||||
yield return childOfChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static TChildItem? FindVisualChild<TChildItem>(DependencyObject? obj) where TChildItem : DependencyObject =>
|
||||
FindVisualChildren<TChildItem>(obj).FirstOrDefault();
|
||||
}
|
||||
// using System.Windows;
|
||||
// using System.Windows.Controls;
|
||||
// using System.Windows.Controls.Primitives;
|
||||
// using System.Windows.Media;
|
||||
//
|
||||
// namespace Azaion.Annotator.Extensions;
|
||||
//
|
||||
// public static class DataGridExtensions
|
||||
// {
|
||||
// public static DataGridCell? GetCell(this DataGrid grid, int rowIndex, int columnIndex = 0)
|
||||
// {
|
||||
// var row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
|
||||
// if (row == null)
|
||||
// return null;
|
||||
//
|
||||
// var presenter = FindVisualChild<DataGridCellsPresenter>(row);
|
||||
// if (presenter == null)
|
||||
// return null;
|
||||
//
|
||||
// var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
||||
// if (cell != null) return cell;
|
||||
//
|
||||
// // now try to bring into view and retrieve the cell
|
||||
// grid.ScrollIntoView(row, grid.Columns[columnIndex]);
|
||||
// cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
||||
//
|
||||
// return cell;
|
||||
// }
|
||||
//
|
||||
// private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? dependencyObj) where T : DependencyObject
|
||||
// {
|
||||
// if (dependencyObj == null)
|
||||
// yield break;
|
||||
//
|
||||
// for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObj); i++)
|
||||
// {
|
||||
// var child = VisualTreeHelper.GetChild(dependencyObj, i);
|
||||
// if (child is T dependencyObject)
|
||||
// {
|
||||
// yield return dependencyObject;
|
||||
// }
|
||||
//
|
||||
// foreach (T childOfChild in FindVisualChildren<T>(child))
|
||||
// {
|
||||
// yield return childOfChild;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public static TChildItem? FindVisualChild<TChildItem>(DependencyObject? obj) where TChildItem : DependencyObject =>
|
||||
// FindVisualChildren<TChildItem>(obj).FirstOrDefault();
|
||||
// }
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Windows;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
using System.Windows;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public partial class HelpWindow : Window
|
||||
{
|
||||
private readonly Config _config;
|
||||
private readonly WindowConfig _windowConfig;
|
||||
|
||||
public HelpWindow(Config config)
|
||||
public HelpWindow(WindowConfig windowConfig)
|
||||
{
|
||||
_config = config;
|
||||
Loaded += (_, _) => CbShowHelp.IsChecked = _config.ShowHelpOnStart;
|
||||
_windowConfig = windowConfig;
|
||||
Loaded += (_, _) => CbShowHelp.IsChecked = windowConfig.ShowHelpOnStart;
|
||||
InitializeComponent();
|
||||
}
|
||||
private void Close(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _config.ShowHelpOnStart = true;
|
||||
private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _config.ShowHelpOnStart = false;
|
||||
private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _windowConfig.ShowHelpOnStart = true;
|
||||
private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _windowConfig.ShowHelpOnStart = false;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.IO;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common.DTO;
|
||||
using Compunet.YoloV8;
|
||||
using Compunet.YoloV8.Data;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Detection = Azaion.Annotator.DTO.Detection;
|
||||
using Detection = Azaion.Common.DTO.Detection;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
@@ -14,9 +15,9 @@ public interface IAIDetector
|
||||
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)
|
||||
{
|
||||
@@ -37,7 +38,7 @@ public class YOLODetector(Config config) : IAIDetector, IDisposable
|
||||
|
||||
private List<Detection> FilterOverlapping(List<Detection> detections)
|
||||
{
|
||||
var k = config.AIRecognitionConfig.TrackingIntersectionThreshold;
|
||||
var k = recognitionConfig.TrackingIntersectionThreshold;
|
||||
var filteredDetections = new List<Detection>();
|
||||
for (var i = 0; i < detections.Count; i++)
|
||||
{
|
||||
@@ -48,8 +49,9 @@ public class YOLODetector(Config config) : IAIDetector, IDisposable
|
||||
intersect.Intersect(detections[j].ToRectangle());
|
||||
|
||||
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)
|
||||
{
|
||||
filteredDetections.Add(detections[i]);
|
||||
@@ -63,7 +65,6 @@ public class YOLODetector(Config config) : IAIDetector, IDisposable
|
||||
detectionSelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!detectionSelected)
|
||||
filteredDetections.Add(detections[i]);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -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>
|
||||
@@ -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
-2
@@ -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: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.Annotator.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300"
|
||||
Background="Black"
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public partial class AnnotationClasses
|
||||
{
|
||||
public AnnotationClasses()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -3,10 +3,10 @@ using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Common.DTO;
|
||||
using Label = System.Windows.Controls.Label;
|
||||
|
||||
namespace Azaion.Annotator.Controls;
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public class AnnotationControl : Border
|
||||
{
|
||||
@@ -4,11 +4,12 @@ using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Common.DTO;
|
||||
using MediatR;
|
||||
using Color = System.Windows.Media.Color;
|
||||
using Rectangle = System.Windows.Shapes.Rectangle;
|
||||
|
||||
namespace Azaion.Annotator.Controls;
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public class CanvasEditor : Canvas
|
||||
{
|
||||
@@ -28,7 +29,6 @@ public class CanvasEditor : Canvas
|
||||
private const int MIN_SIZE = 20;
|
||||
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
|
||||
|
||||
public FormState FormState { get; set; } = null!;
|
||||
public IMediator Mediator { get; set; } = null!;
|
||||
|
||||
public static readonly DependencyProperty GetTimeFuncProp =
|
||||
@@ -1,15 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows.Media;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common.Extensions;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class AnnotationClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public string ShortName { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public string ShortName { get; set; } = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
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!;
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class ImageCreatedEvent(string imagePath) : INotification
|
||||
{
|
||||
public string ImagePath { get; } = imagePath;
|
||||
}
|
||||
@@ -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 Size = System.Windows.Size;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public abstract class Label
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class LabelInfo
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Azaion.Annotator.DTO;
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public enum PlaybackControlEnum
|
||||
{
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public enum WindowEnum
|
||||
{
|
||||
None = 0,
|
||||
Annotator = 10,
|
||||
DatasetExplorer = 20
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
namespace Azaion.Common.Extensions;
|
||||
|
||||
public static class ColorExtensions
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
namespace Azaion.Common.Extensions;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)]
|
||||
@@ -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>
|
||||
@@ -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: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:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
|
||||
xmlns:dto="clr-namespace:Azaion.Annotator.DTO"
|
||||
xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
|
||||
xmlns:ScottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
|
||||
xmlns:scottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
|
||||
xmlns:datasetExplorer="clr-namespace:Azaion.Dataset"
|
||||
xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
|
||||
xmlns:controls1="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common"
|
||||
mc:Ignorable="d"
|
||||
Title="Переглядач анотацій" Height="900" Width="1200">
|
||||
|
||||
<Window.Resources>
|
||||
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}">
|
||||
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type datasetExplorer:ThumbnailDto}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
@@ -79,7 +80,7 @@
|
||||
</controls:CanvasEditor>
|
||||
</TabItem>
|
||||
<TabItem Name="ClassDistributionTab" Header="Розподіл класів">
|
||||
<ScottPlot:WpfPlot x:Name="ClassDistribution" />
|
||||
<scottPlot:WpfPlot x:Name="ClassDistribution" />
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
<StatusBar
|
||||
@@ -3,27 +3,27 @@ using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScottPlot;
|
||||
using Color = ScottPlot.Color;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
using Orientation = ScottPlot.Orientation;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
namespace Azaion.Dataset;
|
||||
|
||||
public partial class DatasetExplorer
|
||||
{
|
||||
private readonly Config _config;
|
||||
private readonly ILogger<DatasetExplorer> _logger;
|
||||
private readonly AnnotationConfig _annotationConfig;
|
||||
private readonly DirectoriesConfig _directoriesConfig;
|
||||
|
||||
public ObservableCollection<ThumbnailDto> ThumbnailsDtos { get; set; } = new();
|
||||
private ObservableCollection<AnnotationClass> AllAnnotationClasses { get; set; } = new();
|
||||
|
||||
private int _tempSelectedClassIdx = 0;
|
||||
private readonly IConfigRepository _configRepository;
|
||||
private readonly FormState _formState;
|
||||
private readonly IGalleryManager _galleryManager;
|
||||
|
||||
public bool ThumbnailLoading { get; set; }
|
||||
@@ -31,31 +31,31 @@ public partial class DatasetExplorer
|
||||
public ThumbnailDto? CurrentThumbnail { get; set; }
|
||||
|
||||
public DatasetExplorer(
|
||||
Config config,
|
||||
IOptions<DirectoriesConfig> directoriesConfig,
|
||||
IOptions<AnnotationConfig> annotationConfig,
|
||||
ILogger<DatasetExplorer> logger,
|
||||
IConfigRepository configRepository,
|
||||
FormState formState,
|
||||
IGalleryManager galleryManager)
|
||||
{
|
||||
_config = config;
|
||||
_directoriesConfig = directoriesConfig.Value;
|
||||
_annotationConfig = annotationConfig.Value;
|
||||
_logger = logger;
|
||||
_configRepository = configRepository;
|
||||
_formState = formState;
|
||||
_galleryManager = galleryManager;
|
||||
|
||||
InitializeComponent();
|
||||
Loaded += async (_, _) =>
|
||||
{
|
||||
_ = Task.Run(async () => await _galleryManager.RefreshThumbnails());
|
||||
|
||||
AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
|
||||
new List<AnnotationClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
|
||||
.Concat(_config.AnnotationClasses));
|
||||
.Concat(_annotationConfig.AnnotationClasses));
|
||||
LvClasses.ItemsSource = AllAnnotationClasses;
|
||||
|
||||
LvClasses.MouseUp += async (_, _) =>
|
||||
{
|
||||
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
|
||||
ExplorerEditor.CurrentAnnClass = selectedClass;
|
||||
config.LastSelectedExplorerClass = selectedClass.Id;
|
||||
_annotationConfig.LastSelectedExplorerClass = selectedClass.Id;
|
||||
|
||||
if (Switcher.SelectedIndex == 0)
|
||||
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;
|
||||
await ReloadThumbnails();
|
||||
LoadClassDistribution();
|
||||
|
||||
SizeChanged += async (_, _) => await SaveUserSettings();
|
||||
LocationChanged += async (_, _) => await SaveUserSettings();
|
||||
StateChanged += async (_, _) => await SaveUserSettings();
|
||||
|
||||
RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage;
|
||||
DataContext = this;
|
||||
};
|
||||
@@ -118,9 +114,7 @@ public partial class DatasetExplorer
|
||||
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {ThumbnailsDtos.Count}";
|
||||
};
|
||||
|
||||
Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.DatasetExplorer; };
|
||||
|
||||
ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath);
|
||||
ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentThumbnail!.ImagePath);
|
||||
galleryManager.ThumbnailsUpdate += thumbnailsPercentage =>
|
||||
{
|
||||
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
|
||||
@@ -136,16 +130,15 @@ public partial class DatasetExplorer
|
||||
.Select(x => new
|
||||
{
|
||||
x.Key,
|
||||
_config.AnnotationClassesDict[x.Key].Name,
|
||||
_config.AnnotationClassesDict[x.Key].Color,
|
||||
_annotationConfig.AnnotationClassesDict[x.Key].Name,
|
||||
_annotationConfig.AnnotationClassesDict[x.Key].Color,
|
||||
ClassCount = x.Count()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
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,
|
||||
Position = -1.5 * x.Key + 1,
|
||||
@@ -154,22 +147,34 @@ public partial class DatasetExplorer
|
||||
Value = x.ClassCount,
|
||||
CenterLabel = true,
|
||||
LabelOffset = 10
|
||||
}));
|
||||
}).ToList();
|
||||
|
||||
ClassDistribution.Plot.Add.Bars(bars);
|
||||
|
||||
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.LabelFontSize = 18;
|
||||
}
|
||||
|
||||
plot.Axes.AutoScale();
|
||||
plot.HideAxesAndGrid();
|
||||
plot.FigureBackground.Color = new("#888888");
|
||||
ClassDistribution.Plot.Axes.AutoScale();
|
||||
ClassDistribution.Plot.HideAxesAndGrid();
|
||||
ClassDistribution.Plot.FigureBackground.Color = new("#888888");
|
||||
|
||||
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()
|
||||
{
|
||||
try
|
||||
@@ -187,11 +192,11 @@ public partial class DatasetExplorer
|
||||
};
|
||||
SwitchTab(toEditor: true);
|
||||
|
||||
var time = _formState.GetTime(dto.ImagePath);
|
||||
var time = Constants.GetTime(dto.ImagePath);
|
||||
ExplorerEditor.RemoveAllAnns();
|
||||
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);
|
||||
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
|
||||
}
|
||||
@@ -217,7 +222,7 @@ public partial class DatasetExplorer
|
||||
AnnotationsTab.Visibility = Visibility.Collapsed;
|
||||
EditorTab.Visibility = Visibility.Visible;
|
||||
_tempSelectedClassIdx = LvClasses.SelectedIndex;
|
||||
LvClasses.ItemsSource = _config.AnnotationClasses;
|
||||
LvClasses.ItemsSource = _annotationConfig.AnnotationClasses;
|
||||
|
||||
Switcher.SelectedIndex = 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()
|
||||
{
|
||||
var tempSelected = ThumbnailsView.SelectedIndex;
|
||||
@@ -266,10 +261,10 @@ public partial class DatasetExplorer
|
||||
LoadingAnnsCaption.Visibility = Visibility.Visible;
|
||||
LoadingAnnsBar.Visibility = Visibility.Visible;
|
||||
|
||||
if (!Directory.Exists(_config.ThumbnailsDirectory))
|
||||
if (!Directory.Exists(_directoriesConfig.ThumbnailsDirectory))
|
||||
return;
|
||||
|
||||
var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg");
|
||||
var thumbnails = Directory.GetFiles(_directoriesConfig.ThumbnailsDirectory, "*.jpg");
|
||||
var thumbnailDtos = new List<ThumbnailDto>();
|
||||
for (int i = 0; i < thumbnails.Length; i++)
|
||||
{
|
||||
@@ -293,11 +288,11 @@ public partial class DatasetExplorer
|
||||
{
|
||||
try
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.THUMBNAIL_PREFIX.Length];
|
||||
var imagePath = Path.Combine(_config.ImagesDirectory, name);
|
||||
var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt");
|
||||
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Constants.THUMBNAIL_PREFIX.Length];
|
||||
var imagePath = Path.Combine(_directoriesConfig.ImagesDirectory, name);
|
||||
var labelPath = Path.Combine(_directoriesConfig.LabelsDirectory, $"{name}.txt");
|
||||
|
||||
foreach (var f in _config.ImageFormats)
|
||||
foreach (var f in _annotationConfig.ImageFormats)
|
||||
{
|
||||
var curName = $"{imagePath}.{f}";
|
||||
if (File.Exists(curName))
|
||||
+14
-8
@@ -1,14 +1,15 @@
|
||||
using System.IO;
|
||||
using System.Windows.Input;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Common.DTO;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
namespace Azaion.Dataset;
|
||||
|
||||
public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer,
|
||||
Config config,
|
||||
IGalleryManager galleryManager,
|
||||
FormState formState) : INotificationHandler<KeyEvent>
|
||||
public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalleryManager galleryManager, IOptions<DirectoriesConfig> directoriesConfig)
|
||||
:
|
||||
INotificationHandler<KeyEvent>,
|
||||
INotificationHandler<ImageCreatedEvent>
|
||||
{
|
||||
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
|
||||
{
|
||||
@@ -20,7 +21,7 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer,
|
||||
|
||||
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
|
||||
{
|
||||
if (formState.ActiveWindow != WindowsEnum.DatasetExplorer)
|
||||
if (keyEvent.WindowEnum != WindowEnum.Annotator)
|
||||
return;
|
||||
|
||||
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))
|
||||
.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.SaveLabelsCache();
|
||||
datasetExplorer.CurrentThumbnail.UpdateImage();
|
||||
@@ -67,4 +68,9 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer,
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Task Handle(ImageCreatedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,37 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Color = System.Drawing.Color;
|
||||
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
|
||||
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 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;
|
||||
private readonly string _thumbnailsCacheFile;
|
||||
|
||||
|
||||
private readonly SemaphoreSlim _updateLock = new(1);
|
||||
|
||||
@@ -28,7 +39,6 @@ public class GalleryManager : IGalleryManager
|
||||
public ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; } = new();
|
||||
|
||||
private DirectoryInfo? _thumbnailsDirectory;
|
||||
private readonly Config _config;
|
||||
|
||||
private DirectoryInfo ThumbnailsDirectory
|
||||
{
|
||||
@@ -37,24 +47,17 @@ public class GalleryManager : IGalleryManager
|
||||
if (_thumbnailsDirectory != null)
|
||||
return _thumbnailsDirectory;
|
||||
|
||||
var dir = new DirectoryInfo(_config.ThumbnailsDirectory);
|
||||
var dir = new DirectoryInfo(_dirConfig.ThumbnailsDirectory);
|
||||
if (!dir.Exists)
|
||||
Directory.CreateDirectory(_config.ThumbnailsDirectory);
|
||||
_thumbnailsDirectory = new DirectoryInfo(_config.ThumbnailsDirectory);
|
||||
Directory.CreateDirectory(_dirConfig.ThumbnailsDirectory);
|
||||
_thumbnailsDirectory = new DirectoryInfo(_dirConfig.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()
|
||||
{
|
||||
foreach(var file in new DirectoryInfo(_config.ThumbnailsDirectory).GetFiles())
|
||||
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
|
||||
file.Delete();
|
||||
}
|
||||
|
||||
@@ -63,7 +66,7 @@ public class GalleryManager : IGalleryManager
|
||||
await _updateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var prefixLen = Config.THUMBNAIL_PREFIX.Length;
|
||||
var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
|
||||
|
||||
var thumbnails = ThumbnailsDirectory.GetFiles()
|
||||
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
|
||||
@@ -80,7 +83,7 @@ public class GalleryManager : IGalleryManager
|
||||
else
|
||||
LabelsCache = new ConcurrentDictionary<string, LabelInfo>();
|
||||
|
||||
var files = new DirectoryInfo(_config.ImagesDirectory).GetFiles();
|
||||
var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles();
|
||||
var imagesCount = files.Length;
|
||||
|
||||
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
|
||||
@@ -94,7 +97,7 @@ public class GalleryManager : IGalleryManager
|
||||
}
|
||||
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
|
||||
{
|
||||
@@ -126,11 +129,11 @@ public class GalleryManager : IGalleryManager
|
||||
{
|
||||
try
|
||||
{
|
||||
var width = (int)_config.ThumbnailConfig.Size.Width;
|
||||
var height = (int)_config.ThumbnailConfig.Size.Height;
|
||||
var width = (int)_thumbnailConfig.Size.Width;
|
||||
var height = (int)_thumbnailConfig.Size.Height;
|
||||
|
||||
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)));
|
||||
|
||||
@@ -145,7 +148,7 @@ public class GalleryManager : IGalleryManager
|
||||
if (!File.Exists(labelName))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -157,7 +160,7 @@ public class GalleryManager : IGalleryManager
|
||||
AddToCache(imgPath, classes);
|
||||
|
||||
var thumbWhRatio = width / (float)height;
|
||||
var border = _config.ThumbnailConfig.Border;
|
||||
var border = _thumbnailConfig.Border;
|
||||
|
||||
var frameX = 0.0;
|
||||
var frameY = 0.0;
|
||||
@@ -196,14 +199,14 @@ public class GalleryManager : IGalleryManager
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return new ThumbnailDto
|
||||
@@ -216,7 +219,7 @@ public class GalleryManager : IGalleryManager
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, e.Message);
|
||||
logger.LogError(e, e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,14 @@
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Azaion.Annotator.Extensions;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
namespace Azaion.Dataset;
|
||||
|
||||
public class ThumbnailDto : INotifyPropertyChanged
|
||||
{
|
||||
public string ThumbnailPath { get; set; }
|
||||
public string ImagePath { get; set; }
|
||||
public string LabelPath { get; set; }
|
||||
public string ThumbnailPath { get; set; } = null!;
|
||||
public string ImagePath { get; set; } = null!;
|
||||
public string LabelPath { get; set; } = null!;
|
||||
public DateTime ImageDate { get; set; }
|
||||
|
||||
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:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
@@ -1,18 +1,26 @@
|
||||
using System.Data;
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using Azaion.Annotator;
|
||||
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 MediatR;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
using Azaion.Annotator.Extensions;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
namespace Azaion.Suite;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
@@ -32,12 +40,21 @@ public partial class App : Application
|
||||
.CreateLogger();
|
||||
|
||||
_host = Host.CreateDefaultBuilder()
|
||||
.ConfigureAppConfiguration((context, config) => config
|
||||
.AddCommandLine(Environment.GetCommandLineArgs())
|
||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true))
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton<MainWindow>();
|
||||
services.AddSingleton<HelpWindow>();
|
||||
services.AddSingleton<Loader>();
|
||||
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<IGalleryManager, GalleryManager>();
|
||||
services.AddSingleton<HelpWindow>();
|
||||
services.AddSingleton<IAIDetector, YOLODetector>();
|
||||
services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
|
||||
services.AddSingleton<LibVLC>(_ => new LibVLC());
|
||||
@@ -47,12 +64,19 @@ public partial class App : Application
|
||||
var libVLC = sp.GetRequiredService<LibVLC>();
|
||||
return new MediaPlayer(libVLC);
|
||||
});
|
||||
services.AddSingleton<IConfigRepository, FileConfigRepository>();
|
||||
services.AddSingleton<Config>(sp => sp.GetRequiredService<IConfigRepository>().Get());
|
||||
services.AddSingleton<MainWindowEventHandler>();
|
||||
services.AddSingleton<AnnotatorEventHandler>();
|
||||
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();
|
||||
_mediator = _host.Services.GetRequiredService<IMediator>();
|
||||
_logger = _host.Services.GetRequiredService<ILogger<App>>();
|
||||
@@ -65,11 +89,11 @@ public partial class App : Application
|
||||
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));
|
||||
_host.Start();
|
||||
_host.Services.GetRequiredService<MainWindow>().Show();
|
||||
await _host.StartAsync();
|
||||
_host.Services.GetRequiredService<Loader>().Show();
|
||||
|
||||
base.OnStartup(e);
|
||||
}
|
||||
@@ -77,6 +101,14 @@ public partial class App : Application
|
||||
private void GlobalClick(object sender, RoutedEventArgs 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)]
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)));
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"ApiConfig": {
|
||||
"Url": "https://api.azaion.com",
|
||||
"TimeoutSeconds": 20,
|
||||
"RetryCount": 3
|
||||
},
|
||||
"LocalFilesConfig": {
|
||||
"DllPath": "AzaionSuite"
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<RootNamespace>Azaion.Annotator.Test</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Azaion.Annotator\Azaion.Annotator.csproj" />
|
||||
<ProjectReference Include="..\Azaion.Suite\Azaion.Suite.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -1,4 +1,5 @@
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Common.DTO;
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.Annotator.Test;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user