rework to Azaion.Suite

This commit is contained in:
Alex Bezdieniezhnykh
2024-11-21 13:41:32 +02:00
parent 2cf69f4e4e
commit 5a592e9dbf
76 changed files with 1739 additions and 882 deletions
@@ -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)
{
+275
View File
@@ -0,0 +1,275 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Azaion.Annotator.DTO;
using Azaion.Common.DTO;
using LibVLCSharp.Shared;
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator;
public class AnnotatorEventHandler(
LibVLC libVLC,
MediaPlayer mediaPlayer,
Annotator mainWindow,
FormState formState,
IOptions<DirectoriesConfig> directoriesConfig,
IMediator mediator,
ILogger<AnnotatorEventHandler> logger)
:
INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>,
INotificationHandler<VolumeChangedEvent>
{
private readonly DirectoriesConfig _directoriesConfig = directoriesConfig.Value;
private const int STEP = 20;
private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280;
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Space, PlaybackControlEnum.Pause },
{ Key.Left, PlaybackControlEnum.PreviousFrame },
{ Key.Right, PlaybackControlEnum.NextFrame },
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.PageUp, PlaybackControlEnum.Previous },
{ Key.PageDown, PlaybackControlEnum.Next },
};
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
{
SelectClass(notification.AnnotationClass);
await Task.CompletedTask;
}
private void SelectClass(AnnotationClass annClass)
{
mainWindow.Editor.CurrentAnnClass = annClass;
foreach (var ann in mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = annClass;
mainWindow.LvClasses.SelectedIndex = annClass.Id;
}
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
{
if (keyEvent.WindowEnum != WindowEnum.Annotator)
return;
var key = keyEvent.Args.Key;
var keyNumber = (int?)null;
if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9)
keyNumber = key - Key.D1;
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue)
SelectClass((AnnotationClass)mainWindow.LvClasses.Items[keyNumber.Value]!);
if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value);
if (key == Key.A)
mainWindow.AutoDetect(null!, null!);
await VolumeControl(key);
}
private async Task VolumeControl(Key key)
{
switch (key)
{
case Key.VolumeMute when mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume);
break;
case Key.VolumeMute:
await ControlPlayback(PlaybackControlEnum.TurnOffVolume);
break;
case Key.Up:
case Key.VolumeUp:
var vUp = Math.Min(100, mediaPlayer.Volume + 5);
ChangeVolume(vUp);
mainWindow.Volume.Value = vUp;
break;
case Key.Down:
case Key.VolumeDown:
var vDown = Math.Max(0, mediaPlayer.Volume - 5);
ChangeVolume(vDown);
mainWindow.Volume.Value = vDown;
break;
}
}
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken)
{
await ControlPlayback(notification.PlaybackControl);
mainWindow.VideoView.Focus();
}
private async Task ControlPlayback(PlaybackControlEnum controlEnum)
{
try
{
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
var step = isCtrlPressed ? LARGE_STEP : STEP;
switch (controlEnum)
{
case PlaybackControlEnum.Play:
Play();
break;
case PlaybackControlEnum.Pause:
mediaPlayer.Pause();
if (!mediaPlayer.IsPlaying)
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
if (formState.BackgroundTime.HasValue)
{
mainWindow.Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
formState.BackgroundTime = null;
}
break;
case PlaybackControlEnum.Stop:
mediaPlayer.Stop();
break;
case PlaybackControlEnum.PreviousFrame:
mainWindow.SeekTo(mediaPlayer.Time - step);
break;
case PlaybackControlEnum.NextFrame:
mainWindow.SeekTo(mediaPlayer.Time + step);
break;
case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations();
break;
case PlaybackControlEnum.RemoveSelectedAnns:
mainWindow.Editor.RemoveSelectedAnns();
break;
case PlaybackControlEnum.RemoveAllAnns:
mainWindow.Editor.RemoveAllAnns();
break;
case PlaybackControlEnum.TurnOnVolume:
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
mediaPlayer.Volume = formState.CurrentVolume;
break;
case PlaybackControlEnum.TurnOffVolume:
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
formState.CurrentVolume = mediaPlayer.Volume;
mediaPlayer.Volume = 0;
break;
case PlaybackControlEnum.Previous:
NextMedia(isPrevious: true);
break;
case PlaybackControlEnum.Next:
NextMedia();
break;
case PlaybackControlEnum.None:
break;
default:
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null);
}
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
}
private void NextMedia(bool isPrevious = false)
{
var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count;
if (mainWindow.LvFiles.SelectedIndex + increment == check)
return;
mainWindow.LvFiles.SelectedIndex += increment;
Play();
}
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
{
ChangeVolume(notification.Volume);
await Task.CompletedTask;
}
private void ChangeVolume(int volume)
{
formState.CurrentVolume = volume;
mediaPlayer.Volume = volume;
}
private void Play()
{
if (mainWindow.LvFiles.SelectedItem == null)
return;
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
formState.CurrentMedia = mediaInfo;
mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
}
private async Task SaveAnnotations()
{
var annGridSelectedIndex = mainWindow.DgAnnotations.SelectedIndex;
if (formState.CurrentMedia == null)
return;
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var fName = formState.GetTimeName(time);
var currentAnns = mainWindow.Editor.CurrentAnns
.Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))
.ToList();
await YoloLabel.WriteToFile(currentAnns, Path.Combine(_directoriesConfig.LabelsDirectory, $"{fName}.txt"));
await mainWindow.AddAnnotations(time, currentAnns);
formState.CurrentMedia.HasAnnotations = mainWindow.Annotations.Count != 0;
mainWindow.LvFiles.Items.Refresh();
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
var destinationPath = Path.Combine(_directoriesConfig.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}");
mainWindow.Editor.RemoveAllAnns();
if (isVideo)
{
if (formState.BackgroundTime.HasValue)
{
//no need to save image, it's already there, just remove background
mainWindow.Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
formState.BackgroundTime = null;
//next item
var annGrid = mainWindow.DgAnnotations;
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGridSelectedIndex + 1);
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
}
else
{
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
mediaPlayer.TakeSnapshot(0, destinationPath, RESULT_WIDTH, resultHeight);
mediaPlayer.Play();
}
}
else
{
File.Copy(formState.CurrentMedia.Path, destinationPath, overwrite: true);
NextMedia();
}
await mediator.Publish(new ImageCreatedEvent(destinationPath));
}
}
-7
View File
@@ -1,7 +0,0 @@
<Application x:Class="Azaion.Annotator.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
</Application.Resources>
</Application>
-82
View File
@@ -1,82 +0,0 @@
using System.Data;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Azaion.Annotator.DTO;
using LibVLCSharp.Shared;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Azaion.Annotator.Extensions;
namespace Azaion.Annotator;
public partial class App : Application
{
private readonly IHost _host;
private readonly ILogger<App> _logger;
private readonly IMediator _mediator;
public App()
{
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File(
path: "Logs/log.txt",
rollingInterval: RollingInterval.Day)
.CreateLogger();
_host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddSingleton<MainWindow>();
services.AddSingleton<HelpWindow>();
services.AddSingleton<DatasetExplorer>();
services.AddSingleton<IGalleryManager, GalleryManager>();
services.AddSingleton<IAIDetector, YOLODetector>();
services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddSingleton<LibVLC>(_ => new LibVLC());
services.AddSingleton<FormState>();
services.AddSingleton<MediaPlayer>(sp =>
{
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<VLCFrameExtractor>();
})
.UseSerilog()
.Build();
_mediator = _host.Services.GetRequiredService<IMediator>();
_logger = _host.Services.GetRequiredService<ILogger<App>>();
DispatcherUnhandledException += OnDispatcherUnhandledException;
}
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
_logger.LogError(e.Exception, e.Exception.Message);
e.Handled = true;
}
protected override void OnStartup(StartupEventArgs e)
{
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.KeyDownEvent, new RoutedEventHandler(GlobalClick));
_host.Start();
_host.Services.GetRequiredService<MainWindow>().Show();
base.OnStartup(e);
}
private void GlobalClick(object sender, RoutedEventArgs e)
{
var args = (KeyEventArgs)e;
_ = ThrottleExt.Throttle(() => _mediator.Publish(new KeyEvent(sender, args)), TimeSpan.FromMilliseconds(50));
}
}
+17 -10
View File
@@ -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,50 +0,0 @@
<DataGrid x:Class="Azaion.Annotator.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"
RowBackground="#252525"
Foreground="White"
RowHeaderWidth="0"
Padding="2 0 0 0"
AutoGenerateColumns="False"
SelectionMode="Single"
CellStyle="{DynamicResource DataGridCellStyle1}"
IsReadOnly="True"
CanUserResizeRows="False"
CanUserResizeColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn
Width="50"
Header="Клавіша"
CanUserSort="False">
<DataGridTemplateColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter>
</Style>
</DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=ColorBrush}">
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
Header="Назва"
Binding="{Binding Path=Name}"
CanUserSort="False">
<DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter>
</Style>
</DataGridTextColumn.HeaderStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
@@ -1,11 +0,0 @@
using System.Windows.Controls;
namespace Azaion.Annotator.Controls;
public partial class AnnotationClasses : DataGrid
{
public AnnotationClasses()
{
InitializeComponent();
}
}
@@ -1,122 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Azaion.Annotator.DTO;
using Label = System.Windows.Controls.Label;
namespace Azaion.Annotator.Controls;
public class AnnotationControl : Border
{
private readonly Action<object, MouseButtonEventArgs> _resizeStart;
private const double RESIZE_RECT_SIZE = 9;
private readonly Grid _grid;
private readonly TextBlock _classNameLabel;
private readonly Label _probabilityLabel;
public TimeSpan? Time { get; set; }
private AnnotationClass _annotationClass = null!;
public AnnotationClass AnnotationClass
{
get => _annotationClass;
set
{
_grid.Background = value.ColorBrush;
_probabilityLabel.Background = value.ColorBrush;
_classNameLabel.Text = value.Name;
_annotationClass = value;
}
}
private readonly Rectangle _selectionFrame;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
_selectionFrame.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
_isSelected = value;
}
}
public AnnotationControl(AnnotationClass annotationClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null)
{
Time = time;
_resizeStart = resizeStart;
_classNameLabel = new TextBlock
{
Text = annotationClass.Name,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 15, 0, 0),
FontSize = 14,
Cursor = Cursors.SizeAll
};
_probabilityLabel = new Label
{
Content = probability.HasValue ? $"{probability.Value:F0}%" : string.Empty,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, -32, 0, 0),
FontSize = 16,
Visibility = Visibility.Visible
};
_selectionFrame = new Rectangle
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Stroke = new SolidColorBrush(Colors.Black),
StrokeThickness = 2,
Visibility = Visibility.Collapsed
};
_grid = new Grid
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Children =
{
_selectionFrame,
_classNameLabel,
AddRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
AddRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
AddRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
AddRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
AddRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
AddRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
AddRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
AddRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
}
};
if (probability.HasValue)
_grid.Children.Add(_probabilityLabel);
Child = _grid;
Cursor = Cursors.SizeAll;
AnnotationClass = annotationClass;
}
//small corners
private Rectangle AddRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs)
{
var rect = new Rectangle() // small rectangles at the corners and sides
{
HorizontalAlignment = ha,
VerticalAlignment = va,
Width = RESIZE_RECT_SIZE,
Height = RESIZE_RECT_SIZE,
Stroke = new SolidColorBrush(Color.FromArgb(230, 40, 40, 40)), // small rectangles color
Fill = new SolidColorBrush(Color.FromArgb(1, 255, 255, 255)),
Cursor = crs,
Name = name,
};
rect.MouseDown += (sender, args) => _resizeStart(sender, args);
return rect;
}
public CanvasLabel Info => new(AnnotationClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
}
-363
View File
@@ -1,363 +0,0 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Azaion.Annotator.DTO;
using MediatR;
using Color = System.Windows.Media.Color;
using Rectangle = System.Windows.Shapes.Rectangle;
namespace Azaion.Annotator.Controls;
public class CanvasEditor : Canvas
{
private Point _lastPos;
public SelectionState SelectionState { get; set; } = SelectionState.None;
private readonly Rectangle _newAnnotationRect;
private Point _newAnnotationStartPos;
private readonly Line _horizontalLine;
private readonly Line _verticalLine;
private readonly TextBlock _classNameHint;
private Rectangle _curRec = new();
private AnnotationControl _curAnn = null!;
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 =
DependencyProperty.Register(
nameof(GetTimeFunc),
typeof(Func<TimeSpan?>),
typeof(CanvasEditor),
new PropertyMetadata(null));
public Func<TimeSpan?> GetTimeFunc
{
get => (Func<TimeSpan?>)GetValue(GetTimeFuncProp);
set => SetValue(GetTimeFuncProp, value);
}
private AnnotationClass _currentAnnClass = null!;
public AnnotationClass CurrentAnnClass
{
get => _currentAnnClass;
set
{
_verticalLine.Stroke = value.ColorBrush;
_verticalLine.Fill = value.ColorBrush;
_horizontalLine.Stroke = value.ColorBrush;
_horizontalLine.Fill = value.ColorBrush;
_classNameHint.Text = value.Name;
_newAnnotationRect.Stroke = value.ColorBrush;
_newAnnotationRect.Fill = value.ColorBrush;
_currentAnnClass = value;
}
}
public readonly List<AnnotationControl> CurrentAnns = new();
public CanvasEditor()
{
_horizontalLine = new Line
{
HorizontalAlignment = HorizontalAlignment.Stretch,
Stroke = new SolidColorBrush(Colors.Blue),
Fill = new SolidColorBrush(Colors.Blue),
StrokeDashArray = [5],
StrokeThickness = 2
};
_verticalLine = new Line
{
VerticalAlignment = VerticalAlignment.Stretch,
Stroke = new SolidColorBrush(Colors.Blue),
Fill = new SolidColorBrush(Colors.Blue),
StrokeDashArray = [5],
StrokeThickness = 2
};
_classNameHint = new TextBlock
{
Text = CurrentAnnClass?.Name ?? "asd",
Foreground = new SolidColorBrush(Colors.Black),
Cursor = Cursors.Arrow,
FontSize = 16,
FontWeight = FontWeights.Bold
};
_newAnnotationRect = new Rectangle
{
Name = "selector",
Height = 0,
Width = 0,
Stroke = new SolidColorBrush(Colors.Gray),
Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)),
};
KeyDown += (_, args) =>
{
Console.WriteLine($"pressed {args.Key}");
};
MouseDown += CanvasMouseDown;
MouseMove += CanvasMouseMove;
MouseUp += CanvasMouseUp;
SizeChanged += CanvasResized;
Cursor = Cursors.Cross;
Children.Add(_newAnnotationRect);
Children.Add(_horizontalLine);
Children.Add(_verticalLine);
Children.Add(_classNameHint);
Loaded += Init;
}
private void Init(object sender, RoutedEventArgs e)
{
_horizontalLine.X1 = 0;
_horizontalLine.X2 = ActualWidth;
_verticalLine.Y1 = 0;
_verticalLine.Y2 = ActualHeight;
}
private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
{
ClearSelections();
NewAnnotationStart(sender, e);
}
private void CanvasMouseMove(object sender, MouseEventArgs e)
{
var pos = e.GetPosition(this);
_horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y;
_verticalLine.X1 = _verticalLine.X2 = pos.X;
SetLeft(_classNameHint, pos.X + 10);
SetTop(_classNameHint, pos.Y - 30);
if (e.LeftButton != MouseButtonState.Pressed)
return;
if (SelectionState == SelectionState.NewAnnCreating)
NewAnnotationCreatingMove(sender, e);
if (SelectionState == SelectionState.AnnResizing)
AnnotationResizeMove(sender, e);
if (SelectionState == SelectionState.AnnMoving)
AnnotationPositionMove(sender, e);
}
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{
if (SelectionState == SelectionState.NewAnnCreating)
CreateAnnotation(e.GetPosition(this));
SelectionState = SelectionState.None;
e.Handled = true;
}
private void CanvasResized(object sender, SizeChangedEventArgs e)
{
_horizontalLine.X2 = e.NewSize.Width;
_verticalLine.Y2 = e.NewSize.Height;
}
#region Annotation Resizing & Moving
private void AnnotationResizeStart(object sender, MouseEventArgs e)
{
SelectionState = SelectionState.AnnResizing;
_lastPos = e.GetPosition(this);
_curRec = (Rectangle)sender;
_curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent;
e.Handled = true;
}
private void AnnotationResizeMove(object sender, MouseEventArgs e)
{
if (SelectionState != SelectionState.AnnResizing)
return;
var currentPos = e.GetPosition(this);
var x = GetLeft(_curAnn);
var y = GetTop(_curAnn);
var offsetX = currentPos.X - _lastPos.X;
var offsetY = currentPos.Y - _lastPos.Y;
switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment)
{
case (HorizontalAlignment.Left, VerticalAlignment.Top):
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
SetLeft(_curAnn, x + offsetX);
SetTop(_curAnn, y + offsetY);
break;
case (HorizontalAlignment.Center, VerticalAlignment.Top):
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
SetTop(_curAnn, y + offsetY);
break;
case (HorizontalAlignment.Right, VerticalAlignment.Top):
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
SetTop(_curAnn, y + offsetY);
break;
case (HorizontalAlignment.Left, VerticalAlignment.Center):
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
SetLeft(_curAnn, x + offsetX);
break;
case (HorizontalAlignment.Right, VerticalAlignment.Center):
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
break;
case (HorizontalAlignment.Left, VerticalAlignment.Bottom):
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
SetLeft(_curAnn, x + offsetX);
break;
case (HorizontalAlignment.Center, VerticalAlignment.Bottom):
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
break;
case (HorizontalAlignment.Right, VerticalAlignment.Bottom):
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
break;
}
_lastPos = currentPos;
}
private void AnnotationPositionStart(object sender, MouseEventArgs e)
{
_lastPos = e.GetPosition(this);
_curAnn = (AnnotationControl)sender;
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
ClearSelections();
_curAnn.IsSelected = true;
SelectionState = SelectionState.AnnMoving;
e.Handled = true;
}
private void AnnotationPositionMove(object sender, MouseEventArgs e)
{
if (SelectionState != SelectionState.AnnMoving)
return;
var currentPos = e.GetPosition(this);
var offsetX = currentPos.X - _lastPos.X;
var offsetY = currentPos.Y - _lastPos.Y;
SetLeft(_curAnn, GetLeft(_curAnn) + offsetX);
SetTop(_curAnn, GetTop(_curAnn) + offsetY);
_lastPos = currentPos;
e.Handled = true;
}
#endregion
#region NewAnnotation
private void NewAnnotationStart(object sender, MouseButtonEventArgs e)
{
_newAnnotationStartPos = e.GetPosition(this);
SetLeft(_newAnnotationRect, _newAnnotationStartPos.X);
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
_newAnnotationRect.MouseMove += NewAnnotationCreatingMove;
SelectionState = SelectionState.NewAnnCreating;
}
private void NewAnnotationCreatingMove(object sender, MouseEventArgs e)
{
if (SelectionState != SelectionState.NewAnnCreating)
return;
var currentPos = e.GetPosition(this);
var diff = currentPos - _newAnnotationStartPos;
_newAnnotationRect.Height = Math.Abs(diff.Y);
_newAnnotationRect.Width = Math.Abs(diff.X);
if (diff.X < 0)
SetLeft(_newAnnotationRect, currentPos.X);
if (diff.Y < 0)
SetTop(_newAnnotationRect, currentPos.Y);
}
private void CreateAnnotation(Point endPos)
{
_newAnnotationRect.Width = 0;
_newAnnotationRect.Height = 0;
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
if (width < MIN_SIZE || height < MIN_SIZE)
return;
var time = GetTimeFunc();
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
{
Width = width,
Height = height,
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y)
});
}
public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
{
var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
{
Width = canvasLabel.Width,
Height = canvasLabel.Height
};
annotationControl.MouseDown += AnnotationPositionStart;
SetLeft(annotationControl, canvasLabel.X );
SetTop(annotationControl, canvasLabel.Y);
Children.Add(annotationControl);
CurrentAnns.Add(annotationControl);
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
return annotationControl;
}
#endregion
private void RemoveAnnotations(IEnumerable<AnnotationControl> listToRemove)
{
foreach (var ann in listToRemove)
{
Children.Remove(ann);
CurrentAnns.Remove(ann);
}
}
public void RemoveAllAnns()
{
foreach (var ann in CurrentAnns)
Children.Remove(ann);
CurrentAnns.Clear();
}
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList());
private void ClearSelections()
{
foreach (var ann in CurrentAnns)
ann.IsSelected = false;
}
public void ClearExpiredAnnotations(TimeSpan time)
{
var expiredAnns = CurrentAnns.Where(x =>
x.Time.HasValue &&
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
.ToList();
RemoveAnnotations(expiredAnns);
}
}
@@ -1,37 +0,0 @@
using System.Windows.Controls;
using System.Windows.Input;
namespace Azaion.Annotator.Controls
{
public class UpdatableProgressBar : ProgressBar
{
public delegate void ValueChange(double oldValue, double newValue);
public new event ValueChange? ValueChanged;
public UpdatableProgressBar() : base()
{
MouseDown += OnMouseDown;
MouseMove += OnMouseMove;
}
private double SetProgressBarValue(double mousePos)
{
Value = Minimum;
var pbValue = mousePos / ActualWidth * Maximum;
ValueChanged?.Invoke(Value, pbValue);
return pbValue;
}
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
Value = SetProgressBarValue(e.GetPosition(this).X);
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
Value = SetProgressBarValue(e.GetPosition(this).X);
}
}
}
@@ -1,4 +1,5 @@
using MediatR;
using Azaion.Common.DTO;
using MediatR;
namespace Azaion.Annotator.DTO;
-22
View File
@@ -1,22 +0,0 @@
using System.Text.Json.Serialization;
using System.Windows.Media;
using Azaion.Annotator.Extensions;
namespace Azaion.Annotator.DTO;
public class AnnotationClass
{
public int Id { get; set; }
public string Name { get; set; }
public string ShortName { get; set; }
[JsonIgnore]
public Color Color => Id.ToColor();
[JsonIgnore]
public int ClassNumber => Id + 1;
[JsonIgnore]
public SolidColorBrush ColorBrush => new(Color);
}
+13 -47
View File
@@ -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";
}
}
-151
View File
@@ -1,151 +0,0 @@
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Size = System.Windows.Size;
using Point = System.Windows.Point;
namespace Azaion.Annotator.DTO;
public class Config
{
public const string THUMBNAIL_PREFIX = "_thumb";
public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache";
public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!;
public string ImagesDirectory { get; set; } = null!;
public string ResultsDirectory { get; set; } = null!;
public string ThumbnailsDirectory { get; set; } = null!;
public string UnknownImages { get; set; } = null!;
public List<AnnotationClass> AnnotationClasses { get; set; } = [];
private Dictionary<int, AnnotationClass>? _annotationClassesDict;
[JsonIgnore]
public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
public WindowConfig MainWindowConfig { get; set; } = null!;
public WindowConfig DatasetExplorerConfig { get; set; } = null!;
public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; }
public bool ShowHelpOnStart { get; set; }
public List<string> VideoFormats { get; set; } = null!;
public List<string> ImageFormats { get; set; } = null!;
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
public int? LastSelectedExplorerClass { get; set; }
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
}
public class AIRecognitionConfig
{
public string AIModelPath { get; set; } = null!;
public double FrameRecognitionSeconds { get; set; }
public double TrackingDistanceConfidence { get; set; }
public double TrackingProbabilityIncrease { get; set; }
public double TrackingIntersectionThreshold { get; set; }
}
public class WindowConfig
{
public Size WindowSize { get; set; }
public Point WindowLocation { get; set; }
public bool FullScreen { get; set; }
}
public class ThumbnailConfig
{
public Size Size { get; set; }
public int Border { get; set; }
}
public interface IConfigRepository
{
public Config Get();
public void Save(Config config);
}
public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfigRepository
{
private const string CONFIG_PATH = "config.json";
private const string DEFAULT_VIDEO_DIR = "video";
private const string DEFAULT_LABELS_DIR = "labels";
private const string DEFAULT_IMAGES_DIR = "images";
private const string DEFAULT_RESULTS_DIR = "results";
private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
private const string DEFAULT_UNKNOWN_IMG_DIR = "unknown";
private const int DEFAULT_THUMBNAIL_BORDER = 10;
private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
private const double TRACKING_PROBABILITY_INCREASE = 15;
private const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
private static readonly Size DefaultWindowSize = new(1280, 720);
private static readonly Point DefaultWindowLocation = new(100, 100);
private static readonly Size DefaultThumbnailSize = new(240, 135);
private static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
private static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
public Config Get()
{
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
var configFilePath = Path.Combine(exePath, CONFIG_PATH);
if (!File.Exists(configFilePath))
{
return new Config
{
VideosDirectory = Path.Combine(exePath, DEFAULT_VIDEO_DIR),
LabelsDirectory = Path.Combine(exePath, DEFAULT_LABELS_DIR),
ImagesDirectory = Path.Combine(exePath, DEFAULT_IMAGES_DIR),
ResultsDirectory = Path.Combine(exePath, DEFAULT_RESULTS_DIR),
ThumbnailsDirectory = Path.Combine(exePath, DEFAULT_THUMBNAILS_DIR),
UnknownImages = Path.Combine(exePath, DEFAULT_UNKNOWN_IMG_DIR),
MainWindowConfig = new WindowConfig
{
WindowSize = DefaultWindowSize,
WindowLocation = DefaultWindowLocation
},
DatasetExplorerConfig = new WindowConfig
{
WindowSize = DefaultWindowSize,
WindowLocation = DefaultWindowLocation
},
ShowHelpOnStart = true,
VideoFormats = DefaultVideoFormats,
ImageFormats = DefaultImageFormats,
ThumbnailConfig = new ThumbnailConfig
{
Size = DefaultThumbnailSize,
Border = DEFAULT_THUMBNAIL_BORDER
},
AIRecognitionConfig = new AIRecognitionConfig
{
AIModelPath = "azaion.onnx",
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD
}
};
}
var str = File.ReadAllText(CONFIG_PATH);
return JsonConvert.DeserializeObject<Config>(str) ?? new Config();
}
public void Save(Config config)
{
File.WriteAllText(CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
}
}
+1 -20
View File
@@ -11,32 +11,13 @@ public class FormState
? ""
: Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", "");
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);
}
}
-189
View File
@@ -1,189 +0,0 @@
using System.Drawing;
using System.Globalization;
using System.IO;
using Newtonsoft.Json;
using Size = System.Windows.Size;
namespace Azaion.Annotator.DTO;
public abstract class Label
{
[JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; }
protected Label()
{
}
protected Label(int classNumber)
{
ClassNumber = classNumber;
}
}
public class CanvasLabel : Label
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double? Probability { get; }
public CanvasLabel()
{
}
public CanvasLabel(int classNumber, double x, double y, double width, double height, double? probability = null) : base(classNumber)
{
X = x;
Y = y;
Width = width;
Height = height;
Probability = probability;
}
public CanvasLabel(YoloLabel label, Size canvasSize, Size videoSize, double? probability = null)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAr = cw / ch;
var videoAr = videoSize.Width / videoSize.Height;
ClassNumber = label.ClassNumber;
var left = label.CenterX - label.Width / 2;
var top = label.CenterY - label.Height / 2;
if (videoAr > canvasAr) //100% width
{
var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
X = left * cw;
Y = top * realHeight + blackStripHeight;
Width = label.Width * cw;
Height = label.Height * realHeight;
}
else //100% height
{
var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
X = left * realWidth + blackStripWidth;
Y = top * ch;
Width = label.Width * realWidth;
Height = label.Height * ch;
}
Probability = probability;
}
}
public class YoloLabel : Label
{
[JsonProperty(PropertyName = "x")] public double CenterX { get; set; }
[JsonProperty(PropertyName = "y")] public double CenterY { get; set; }
[JsonProperty(PropertyName = "w")] public double Width { get; set; }
[JsonProperty(PropertyName = "h")] public double Height { get; set; }
public YoloLabel()
{
}
public YoloLabel(int classNumber, double centerX, double centerY, double width, double height) : base(classNumber)
{
CenterX = centerX;
CenterY = centerY;
Width = width;
Height = height;
}
public RectangleF ToRectangle() =>
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size videoSize)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAr = cw / ch;
var videoAr = videoSize.Width / videoSize.Height;
ClassNumber = canvasLabel.ClassNumber;
double left, top;
if (videoAr > canvasAr) //100% width
{
left = canvasLabel.X / cw;
Width = canvasLabel.Width / cw;
var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
top = (canvasLabel.Y - blackStripHeight) / realHeight;
Height = canvasLabel.Height / realHeight;
}
else //100% height
{
top = canvasLabel.Y / ch;
Height = canvasLabel.Height / ch;
var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
left = (canvasLabel.X - blackStripWidth) / realWidth;
Width = canvasLabel.Width / realWidth;
}
CenterX = left + Width / 2.0;
CenterY = top + Height / 2.0;
}
public static YoloLabel? Parse(string s)
{
if (string.IsNullOrEmpty(s))
return null;
var strings = s.Replace(',', '.').Split(' ');
if (strings.Length != 5)
throw new Exception("Wrong labels format!");
var res = new YoloLabel
{
ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture),
CenterX = double.Parse(strings[1], CultureInfo.InvariantCulture),
CenterY = double.Parse(strings[2], CultureInfo.InvariantCulture),
Width = double.Parse(strings[3], CultureInfo.InvariantCulture),
Height = double.Parse(strings[4], CultureInfo.InvariantCulture)
};
return res;
}
public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken = default)
{
var str = await File.ReadAllTextAsync(filename, cancellationToken);
return str.Split('\n')
.Select(Parse)
.Where(ann => ann != null)
.ToList()!;
}
public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename, CancellationToken cancellationToken = default)
{
var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
await File.WriteAllTextAsync(filename, labelsStr, cancellationToken);
}
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
}
public class Detection : YoloLabel
{
public Detection(YoloLabel label, double? probability = null)
{
ClassNumber = label.ClassNumber;
CenterX = label.CenterX;
CenterY = label.CenterY;
Height = label.Height;
Width = label.Width;
Probability = probability;
}
public double? Probability { get; set; }
}
-10
View File
@@ -1,10 +0,0 @@
using Newtonsoft.Json;
namespace Azaion.Annotator.DTO;
public class LabelInfo
{
[JsonProperty("c")] public List<int> Classes { get; set; } = null!;
[JsonProperty("d")] public DateTime ImageDateTime { get; set; }
}
+1 -6
View File
@@ -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,19 +0,0 @@
namespace Azaion.Annotator.DTO;
public enum PlaybackControlEnum
{
None = 0,
Play = 1,
Pause = 2,
Stop = 3,
PreviousFrame = 4,
NextFrame = 5,
SaveAnnotations = 6,
RemoveSelectedAnns = 7,
RemoveAllAnns = 8,
TurnOffVolume = 9,
TurnOnVolume = 10,
Previous = 11,
Next = 12,
Close = 13
}
-9
View File
@@ -1,9 +0,0 @@
namespace Azaion.Annotator.DTO;
public enum SelectionState
{
None = 0,
NewAnnCreating = 1,
AnnResizing = 2,
AnnMoving = 3
}
-40
View File
@@ -1,40 +0,0 @@
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Media.Imaging;
using Azaion.Annotator.Extensions;
namespace Azaion.Annotator.DTO;
public class ThumbnailDto : INotifyPropertyChanged
{
public string ThumbnailPath { get; set; }
public string ImagePath { get; set; }
public string LabelPath { get; set; }
public DateTime ImageDate { get; set; }
private BitmapImage? _image;
public BitmapImage? Image
{
get
{
if (_image == null)
Task.Run(async () => Image = await ThumbnailPath.OpenImage());
return _image;
}
set
{
_image = value;
OnPropertyChanged();
}
}
public string ImageName => Path.GetFileName(ImagePath);
public void UpdateImage() => _image = null;
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
-8
View File
@@ -1,8 +0,0 @@
namespace Azaion.Annotator.DTO;
public enum WindowsEnum
{
None = 0,
Main = 10,
DatasetExplorer = 20
}
-148
View File
@@ -1,148 +0,0 @@
<Window x:Class="Azaion.Annotator.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"
mc:Ignorable="d"
Title="Переглядач анотацій" Height="900" Width="1200">
<Window.Resources>
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="32"></RowDefinition>
</Grid.RowDefinitions>
<Image
Grid.Row="0"
Source="{Binding Image}"
Width="480"
Height="270"
Margin="2" />
<TextBlock
Grid.Row="1"
Foreground="LightGray"
Text="{Binding ImageName}" />
</Grid>
</DataTemplate>
</Window.Resources>
<Grid
Name="MainGrid"
ShowGridLines="False"
Background="Black"
HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="32"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:AnnotationClasses
x:Name="LvClasses"
Grid.Column="0"
Grid.Row="0">
</controls:AnnotationClasses>
<TabControl
Name="Switcher"
Grid.Column="2"
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black">
<TabItem Name="AnnotationsTab" Header="Анотації">
<vwp:GridView
Name="ThumbnailsView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black"
Margin="2,5,2,2"
ItemsSource="{Binding ThumbnailsDtos, Mode=OneWay}"
ItemTemplate="{StaticResource ThumbnailTemplate}"
>
</vwp:GridView>
</TabItem>
<TabItem Name="EditorTab"
Header="Редактор"
Visibility="Collapsed">
<controls:CanvasEditor x:Name="ExplorerEditor"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" >
</controls:CanvasEditor>
</TabItem>
<TabItem Name="ClassDistributionTab" Header="Розподіл класів">
<ScottPlot:WpfPlot x:Name="ClassDistribution" />
</TabItem>
</TabControl>
<StatusBar
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Background="#252525"
Foreground="White"
>
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="0" Background="Black">
<TextBlock Name="LoadingAnnsCaption">Завантаження:</TextBlock>
</StatusBarItem>
<StatusBarItem Grid.Column="1" Background="Black">
<ProgressBar x:Name="LoadingAnnsBar"
Width="150"
Height="15"
HorizontalAlignment="Stretch"
Background="#252525"
BorderBrush="#252525"
Foreground="LightBlue"
Maximum="100"
Minimum="0"
Value="0">
</ProgressBar>
</StatusBarItem>
<StatusBarItem Grid.Column="2" Background="Black">
<TextBlock Name="RefreshThumbCaption">База іконок:</TextBlock>
</StatusBarItem>
<StatusBarItem Grid.Column="3" Background="Black">
<ProgressBar x:Name="RefreshThumbBar"
Width="150"
Height="15"
HorizontalAlignment="Stretch"
Background="#252525"
BorderBrush="#252525"
Foreground="LightBlue"
Maximum="100"
Minimum="0"
Value="0">
</ProgressBar>
</StatusBarItem>
<Separator Grid.Column="4"/>
<StatusBarItem Grid.Column="5" Background="Black">
<TextBlock Name="StatusText" Text=""/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
-352
View File
@@ -1,352 +0,0 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using Microsoft.Extensions.Logging;
using ScottPlot;
using Color = ScottPlot.Color;
using MessageBox = System.Windows.MessageBox;
using Orientation = ScottPlot.Orientation;
namespace Azaion.Annotator;
public partial class DatasetExplorer
{
private readonly Config _config;
private readonly ILogger<DatasetExplorer> _logger;
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; }
public ThumbnailDto? CurrentThumbnail { get; set; }
public DatasetExplorer(
Config config,
ILogger<DatasetExplorer> logger,
IConfigRepository configRepository,
FormState formState,
IGalleryManager galleryManager)
{
_config = config;
_logger = logger;
_configRepository = configRepository;
_formState = formState;
_galleryManager = galleryManager;
InitializeComponent();
Loaded += async (_, _) =>
{
AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
new List<AnnotationClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_config.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.MouseUp += async (_, _) =>
{
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
ExplorerEditor.CurrentAnnClass = selectedClass;
config.LastSelectedExplorerClass = selectedClass.Id;
if (Switcher.SelectedIndex == 0)
await ReloadThumbnails();
else
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = selectedClass;
};
LvClasses.SelectionChanged += (_, _) =>
{
if (Switcher.SelectedIndex != 1)
return;
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
if (selectedClass == null)
return;
ExplorerEditor.CurrentAnnClass = selectedClass;
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = selectedClass;
};
LvClasses.SelectedIndex = config.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;
};
Closing += (sender, args) =>
{
args.Cancel = true;
Visibility = Visibility.Hidden;
};
ThumbnailsView.KeyDown += async (sender, args) =>
{
switch (args.Key)
{
case Key.Delete:
DeleteAnnotations();
break;
case Key.Enter:
await EditAnnotation();
break;
}
};
ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation();
ThumbnailsView.SelectionChanged += (_, _) =>
{
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {ThumbnailsDtos.Count}";
};
Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.DatasetExplorer; };
ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath);
galleryManager.ThumbnailsUpdate += thumbnailsPercentage =>
{
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
};
}
private void LoadClassDistribution()
{
var data = _galleryManager.LabelsCache
.SelectMany(x => x.Value.Classes)
.GroupBy(x => x)
.Select(x => new
{
x.Key,
_config.AnnotationClassesDict[x.Key].Name,
_config.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
{
Orientation = Orientation.Horizontal,
Position = -1.5 * x.Key + 1,
Label = x.ClassCount > 200 ? x.ClassCount.ToString() : "",
FillColor = new Color(x.Color.R, x.Color.G, x.Color.B, x.Color.A),
Value = x.ClassCount,
CenterLabel = true,
LabelOffset = 10
}));
foreach (var x in data)
{
var label = 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.Refresh();
}
private async Task EditAnnotation()
{
try
{
ThumbnailLoading = true;
if (ThumbnailsView.SelectedItem == null)
return;
var dto = (ThumbnailsView.SelectedItem as ThumbnailDto)!;
CurrentThumbnail = dto;
ExplorerEditor.Background = new ImageBrush
{
ImageSource = await dto.ImagePath.OpenImage()
};
SwitchTab(toEditor: true);
var time = _formState.GetTime(dto.ImagePath);
ExplorerEditor.RemoveAllAnns();
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{
var annClass = _config.AnnotationClassesDict[ann.ClassNumber];
var canvasLabel = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
}
ThumbnailLoading = false;
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
throw;
}
finally
{
ThumbnailLoading = false;
}
}
public void SwitchTab(bool toEditor)
{
if (toEditor)
{
AnnotationsTab.Visibility = Visibility.Collapsed;
EditorTab.Visibility = Visibility.Visible;
_tempSelectedClassIdx = LvClasses.SelectedIndex;
LvClasses.ItemsSource = _config.AnnotationClasses;
Switcher.SelectedIndex = 1;
LvClasses.SelectedIndex = Math.Max(0, _tempSelectedClassIdx - 1);
}
else
{
AnnotationsTab.Visibility = Visibility.Visible;
EditorTab.Visibility = Visibility.Collapsed;
LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.SelectedIndex = _tempSelectedClassIdx;
Switcher.SelectedIndex = 0;
}
}
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;
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
var selected = ThumbnailsView.SelectedItems.Count;
for (var i = 0; i < selected; i++)
{
var dto = (ThumbnailsView.SelectedItems[0] as ThumbnailDto)!;
File.Delete(dto.ImagePath);
File.Delete(dto.LabelPath);
File.Delete(dto.ThumbnailPath);
ThumbnailsDtos.Remove(dto);
}
ThumbnailsView.SelectedIndex = Math.Min(ThumbnailsDtos.Count, tempSelected);
}
private async Task ReloadThumbnails()
{
LoadingAnnsCaption.Visibility = Visibility.Visible;
LoadingAnnsBar.Visibility = Visibility.Visible;
if (!Directory.Exists(_config.ThumbnailsDirectory))
return;
var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg");
var thumbnailDtos = new List<ThumbnailDto>();
for (int i = 0; i < thumbnails.Length; i++)
{
var thumbnailDto = await GetThumbnail(thumbnails[i]);
if (thumbnailDto != null)
thumbnailDtos.Add(thumbnailDto);
if (i % 1000 == 0)
LoadingAnnsBar.Value = i * 100.0 / thumbnails.Length;
}
ThumbnailsDtos.Clear();
foreach (var th in thumbnailDtos.OrderByDescending(x => x.ImageDate))
ThumbnailsDtos.Add(th);
LoadingAnnsCaption.Visibility = Visibility.Collapsed;
LoadingAnnsBar.Visibility = Visibility.Collapsed;
}
private async Task<ThumbnailDto?> GetThumbnail(string thumbnail)
{
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");
foreach (var f in _config.ImageFormats)
{
var curName = $"{imagePath}.{f}";
if (File.Exists(curName))
{
imagePath = curName;
break;
}
}
if (!_galleryManager.LabelsCache.TryGetValue(Path.GetFileName(imagePath), out var info))
{
if (!File.Exists(imagePath) || !File.Exists(labelPath))
{
File.Delete(thumbnail);
_logger.LogError($"No label {labelPath} found ! Image {imagePath} not found, thumbnail {thumbnail} was removed");
return null;
}
var classes = (await YoloLabel.ReadFromFile(labelPath))
.Select(x => x.ClassNumber)
.Distinct()
.ToList();
info = _galleryManager.AddToCache(imagePath, classes);
}
if (!info.Classes.Contains(ExplorerEditor.CurrentAnnClass.Id) && ExplorerEditor.CurrentAnnClass.Id != -1)
return null;
return new ThumbnailDto
{
ThumbnailPath = thumbnail,
ImagePath = imagePath,
LabelPath = labelPath,
ImageDate = info.ImageDateTime
};
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return null;
}
}
public void AddThumbnail(ThumbnailDto thumbnailDto, IEnumerable<int> classes)
{
var selectedClass = ((AnnotationClass?)LvClasses.SelectedItem)?.Id;
if (selectedClass != null && (selectedClass == -1 || classes.Any(x => x == selectedClass)))
ThumbnailsDtos.Insert(0, thumbnailDto);
}
}
@@ -1,70 +0,0 @@
using System.IO;
using System.Windows.Input;
using Azaion.Annotator.DTO;
using MediatR;
namespace Azaion.Annotator;
public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer,
Config config,
IGalleryManager galleryManager,
FormState formState) : INotificationHandler<KeyEvent>
{
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.Escape, PlaybackControlEnum.Close }
};
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
{
if (formState.ActiveWindow != WindowsEnum.DatasetExplorer)
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)
datasetExplorer.LvClasses.SelectedIndex = keyNumber.Value;
else
{
if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value))
await HandleControl(value);
}
}
private async Task HandleControl(PlaybackControlEnum controlEnum)
{
switch (controlEnum)
{
case PlaybackControlEnum.SaveAnnotations:
if (datasetExplorer.ThumbnailLoading)
return;
var currentAnns = datasetExplorer.ExplorerEditor.CurrentAnns
.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 galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath);
await galleryManager.SaveLabelsCache();
datasetExplorer.CurrentThumbnail.UpdateImage();
datasetExplorer.SwitchTab(toEditor: false);
break;
case PlaybackControlEnum.RemoveSelectedAnns:
datasetExplorer.ExplorerEditor.RemoveSelectedAnns();
break;
case PlaybackControlEnum.RemoveAllAnns:
datasetExplorer.ExplorerEditor.RemoveAllAnns();
break;
case PlaybackControlEnum.Close:
datasetExplorer.SwitchTab(toEditor: false);
break;
}
}
}
@@ -1,28 +0,0 @@
using System.Windows.Media;
namespace Azaion.Annotator.Extensions;
public static class ColorExtensions
{
public static Color ToColor(this int id)
{
var index = id % ColorValues.Length;
var hex = index == -1
? "#40DDDDDD"
: $"#40{ColorValues[index]}";
var color =(Color)ColorConverter.ConvertFromString(hex);
return color;
}
private static readonly string[] ColorValues =
[
"FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", "000000",
"800000", "008000", "000080", "808000", "800080", "008080", "808080",
"C00000", "00C000", "0000C0", "C0C000", "C000C0", "00C0C0", "C0C0C0",
"400000", "004000", "000040", "404000", "400040", "004040", "404040",
"200000", "002000", "000020", "202000", "200020", "002020", "202020",
"600000", "006000", "000060", "606000", "600060", "006060", "606060",
"A00000", "00A000", "0000A0", "A0A000", "A000A0", "00A0A0", "A0A0A0",
"E00000", "00E000", "0000E0", "E0E000", "E000E0", "00E0E0", "E0E0E0"
];
}
@@ -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,12 +0,0 @@
using Newtonsoft.Json.Converters;
namespace Azaion.Annotator.Extensions;
public class DenseDateTimeConverter : IsoDateTimeConverter
{
public DenseDateTimeConverter()
{
DateTimeFormat = "yy-MM-dd HH:mm:ss";
}
}
@@ -1,10 +0,0 @@
using System.IO;
namespace Azaion.Annotator.Extensions;
public static class DirectoryInfoExtensions
{
public static IEnumerable<FileInfo> GetFiles(this DirectoryInfo dir, params string[] searchExtensions) =>
dir.GetFiles("*.*", SearchOption.AllDirectories)
.Where(f => searchExtensions.Any(s => f.Name.Contains(s, StringComparison.CurrentCultureIgnoreCase))).ToList();
}
@@ -1,67 +0,0 @@
namespace Azaion.Annotator.Extensions;
public class ParallelOptions
{
public int ProgressUpdateInterval { get; set; } = 100;
public Func<int, Task> ProgressFn { get; set; } = null!;
public double CpuUtilPercent { get; set; } = 100;
}
public class ParallelExt
{
public static async Task ForEachAsync<T>(ICollection<T> source,
Func<T, CancellationToken, ValueTask> processFn,
ParallelOptions? parallelOptions = null,
CancellationToken cancellationToken = default)
{
parallelOptions ??= new ParallelOptions
{
CpuUtilPercent = 100,
ProgressUpdateInterval = 100,
ProgressFn = i =>
{
Console.WriteLine($"Processed {i} item by Task {Task.CurrentId}%");
return Task.CompletedTask;
}
};
var threadsCount = (int)(Environment.ProcessorCount * parallelOptions.CpuUtilPercent / 100.0);
var processedCount = 0;
var chunkSize = Math.Max(1, (int)(source.Count / (decimal)threadsCount));
var chunks = source.Chunk(chunkSize).ToList();
if (chunks.Count > threadsCount)
{
chunks[^2] = chunks[^2].Concat(chunks.Last()).ToArray();
chunks.RemoveAt(chunks.Count - 1);
}
var progressUpdateLock = new SemaphoreSlim(1);
var tasks = new List<Task>();
foreach (var chunk in chunks)
{
tasks.Add(await Task.Factory.StartNew(async () =>
{
foreach (var item in chunk)
{
await processFn(item, cancellationToken);
Interlocked.Increment(ref processedCount);
if (processedCount % parallelOptions.ProgressUpdateInterval == 0 && parallelOptions.ProgressFn != null)
_ = Task.Run(async () =>
{
await progressUpdateLock.WaitAsync(cancellationToken);
try
{
await parallelOptions.ProgressFn(processedCount);
}
finally
{
progressUpdateLock.Release();
}
}, cancellationToken);
}
}, TaskCreationOptions.LongRunning));
}
await Task.WhenAll(tasks);
}
}
@@ -1,19 +0,0 @@
namespace Azaion.Annotator.Extensions;
public static class ThrottleExt
{
private static bool _throttleOn;
public static async Task Throttle(Func<Task> func, TimeSpan? throttleTime = null)
{
if (_throttleOn)
return;
_throttleOn = true;
await func();
_ = Task.Run(async () =>
{
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500));
_throttleOn = false;
});
}
}
@@ -1,5 +1,6 @@
using System.Windows;
using Azaion.Annotator.DTO;
using Azaion.Common.DTO.Config;
namespace Azaion.Annotator.Extensions;
-249
View File
@@ -1,249 +0,0 @@
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 Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Color = System.Drawing.Color;
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
using Size = System.Windows.Size;
namespace Azaion.Annotator;
public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
public class GalleryManager : IGalleryManager
{
private readonly ILogger<GalleryManager> _logger;
public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
private readonly string _thumbnailsCacheFile;
private readonly SemaphoreSlim _updateLock = new(1);
public double ThumbnailsPercentage { get; set; }
public ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; } = new();
private DirectoryInfo? _thumbnailsDirectory;
private readonly Config _config;
private DirectoryInfo ThumbnailsDirectory
{
get
{
if (_thumbnailsDirectory != null)
return _thumbnailsDirectory;
var dir = new DirectoryInfo(_config.ThumbnailsDirectory);
if (!dir.Exists)
Directory.CreateDirectory(_config.ThumbnailsDirectory);
_thumbnailsDirectory = new DirectoryInfo(_config.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())
file.Delete();
}
public async Task RefreshThumbnails()
{
await _updateLock.WaitAsync();
try
{
var prefixLen = Config.THUMBNAIL_PREFIX.Length;
var thumbnails = ThumbnailsDirectory.GetFiles()
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
.GroupBy(x => x)
.Select(gr => gr.Key)
.ToHashSet();
if (File.Exists(_thumbnailsCacheFile))
{
var cache = JsonConvert.DeserializeObject<ConcurrentDictionary<string, LabelInfo>>(
await File.ReadAllTextAsync(_thumbnailsCacheFile), new DenseDateTimeConverter());
LabelsCache = cache ?? new ConcurrentDictionary<string, LabelInfo>();
}
else
LabelsCache = new ConcurrentDictionary<string, LabelInfo>();
var files = new DirectoryInfo(_config.ImagesDirectory).GetFiles();
var imagesCount = files.Length;
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
{
var imgName = Path.GetFileNameWithoutExtension(file.Name);
if (thumbnails.Contains(imgName))
return;
try
{
await CreateThumbnail(file.FullName, cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}");
}
}, new ParallelOptions
{
ProgressFn = async num =>
{
Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
ThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
ThumbnailsUpdate?.Invoke(ThumbnailsPercentage);
await Task.CompletedTask;
},
CpuUtilPercent = 100,
ProgressUpdateInterval = 200
});
}
finally
{
await SaveLabelsCache();
_updateLock.Release();
}
}
public async Task SaveLabelsCache()
{
var labelsCacheStr = JsonConvert.SerializeObject(LabelsCache, new DenseDateTimeConverter());
await File.WriteAllTextAsync(_thumbnailsCacheFile, labelsCacheStr);
}
public async Task<ThumbnailDto?> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default)
{
try
{
var width = (int)_config.ThumbnailConfig.Size.Width;
var height = (int)_config.ThumbnailConfig.Size.Height;
var imgName = Path.GetFileName(imgPath);
var labelName = Path.Combine(_config.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt");
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken)));
var bitmap = new Bitmap(width, height);
using var g = Graphics.FromImage(bitmap);
g.CompositingQuality = CompositingQuality.HighSpeed;
g.SmoothingMode = SmoothingMode.HighSpeed;
g.InterpolationMode = InterpolationMode.Default;
var size = new Size(originalImage.Width, originalImage.Height);
if (!File.Exists(labelName))
{
File.Delete(imgPath);
_logger.LogInformation($"No labels found for image {imgName}! Image deleted!");
return null;
}
var labels = (await YoloLabel.ReadFromFile(labelName, cancellationToken))
.Select(x => new CanvasLabel(x, size, size))
.ToList();
var classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
AddToCache(imgPath, classes);
var thumbWhRatio = width / (float)height;
var border = _config.ThumbnailConfig.Border;
var frameX = 0.0;
var frameY = 0.0;
var frameHeight = size.Height;
var frameWidth = size.Width;
if (labels.Any())
{
var labelsMinX = labels.Min(x => x.X);
var labelsMaxX = labels.Max(x => x.X + x.Width);
var labelsMinY = labels.Min(x => x.Y);
var labelsMaxY = labels.Max(x => x.Y + x.Height);
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
if (labelsWidth / labelsHeight > thumbWhRatio)
{
frameWidth = labelsWidth;
frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height);
frameX = Math.Max(0, labelsMinX - border);
frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border);
}
else
{
frameHeight = labelsHeight;
frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width);
frameY = Math.Max(0, labelsMinY - border);
frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border);
}
}
var scale = frameHeight / height;
g.DrawImage(originalImage, new Rectangle(0, 0, width, height), new RectangleF((float)frameX, (float)frameY, (float)frameWidth, (float)frameHeight), GraphicsUnit.Pixel);
foreach (var label in labels)
{
var color = _config.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");
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
return new ThumbnailDto
{
ThumbnailPath = thumbnailName,
ImagePath = imgPath,
LabelPath = labelName,
ImageDate = File.GetCreationTimeUtc(imgPath)
};
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return null;
}
}
public LabelInfo AddToCache(string imgPath, List<int> classes)
{
var labelInfo = new LabelInfo
{
Classes = classes,
ImageDateTime = File.GetCreationTimeUtc(imgPath)
};
LabelsCache.TryAdd(Path.GetFileName(imgPath), labelInfo);
//Save to file only each 2 seconds
_ = ThrottleExt.Throttle(async () => await SaveLabelsCache(), TimeSpan.FromSeconds(2));
return labelInfo;
}
}
public interface IGalleryManager
{
event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
double ThumbnailsPercentage { get; set; }
Task SaveLabelsCache();
LabelInfo AddToCache(string imgPath, List<int> classes);
ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; }
Task<ThumbnailDto?> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default);
Task RefreshThumbnails();
void ClearThumbnails();
}
+7 -7
View File
@@ -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;
}
-298
View File
@@ -1,298 +0,0 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Azaion.Annotator.DTO;
using LibVLCSharp.Shared;
using MediatR;
using Microsoft.Extensions.Logging;
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator;
public class MainWindowEventHandler :
INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>,
INotificationHandler<VolumeChangedEvent>
{
private readonly LibVLC _libVLC;
private readonly MediaPlayer _mediaPlayer;
private readonly MainWindow _mainWindow;
private readonly FormState _formState;
private readonly Config _config;
private readonly IMediator _mediator;
private readonly IGalleryManager _galleryManager;
private readonly DatasetExplorer _datasetExplorer;
private readonly ILogger<MainWindowEventHandler> _logger;
private const int STEP = 20;
private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280;
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Space, PlaybackControlEnum.Pause },
{ Key.Left, PlaybackControlEnum.PreviousFrame },
{ Key.Right, PlaybackControlEnum.NextFrame },
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.PageUp, PlaybackControlEnum.Previous },
{ Key.PageDown, PlaybackControlEnum.Next },
};
public MainWindowEventHandler(LibVLC libVLC,
MediaPlayer mediaPlayer,
MainWindow mainWindow,
FormState formState,
Config config,
IMediator mediator,
IGalleryManager galleryManager,
DatasetExplorer datasetExplorer,
ILogger<MainWindowEventHandler> logger)
{
_libVLC = libVLC;
_mediaPlayer = mediaPlayer;
_mainWindow = mainWindow;
_formState = formState;
_config = config;
_mediator = mediator;
_galleryManager = galleryManager;
_datasetExplorer = datasetExplorer;
_logger = logger;
}
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
{
SelectClass(notification.AnnotationClass);
await Task.CompletedTask;
}
private void SelectClass(AnnotationClass annClass)
{
_mainWindow.Editor.CurrentAnnClass = annClass;
foreach (var ann in _mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = annClass;
_mainWindow.LvClasses.SelectedIndex = annClass.Id;
}
public async Task Handle(KeyEvent notification, CancellationToken cancellationToken)
{
if (_formState.ActiveWindow != WindowsEnum.Main)
return;
var key = notification.Args.Key;
var keyNumber = (int?)null;
if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9)
keyNumber = key - Key.D1;
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue)
SelectClass((AnnotationClass)_mainWindow.LvClasses.Items[keyNumber.Value]);
if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value);
if (key == Key.A)
_mainWindow.AutoDetect(null!, null!);
await VolumeControl(key);
}
private async Task VolumeControl(Key key)
{
switch (key)
{
case Key.VolumeMute when _mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume);
break;
case Key.VolumeMute:
await ControlPlayback(PlaybackControlEnum.TurnOffVolume);
break;
case Key.Up:
case Key.VolumeUp:
var vUp = Math.Min(100, _mediaPlayer.Volume + 5);
ChangeVolume(vUp);
_mainWindow.Volume.Value = vUp;
break;
case Key.Down:
case Key.VolumeDown:
var vDown = Math.Max(0, _mediaPlayer.Volume - 5);
ChangeVolume(vDown);
_mainWindow.Volume.Value = vDown;
break;
}
}
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken)
{
await ControlPlayback(notification.PlaybackControl);
_mainWindow.VideoView.Focus();
}
private async Task ControlPlayback(PlaybackControlEnum controlEnum)
{
try
{
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
var step = isCtrlPressed ? LARGE_STEP : STEP;
switch (controlEnum)
{
case PlaybackControlEnum.Play:
Play();
break;
case PlaybackControlEnum.Pause:
_mediaPlayer.Pause();
if (!_mediaPlayer.IsPlaying)
_mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
if (_formState.BackgroundTime.HasValue)
{
_mainWindow.Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
_formState.BackgroundTime = null;
}
break;
case PlaybackControlEnum.Stop:
_mediaPlayer.Stop();
break;
case PlaybackControlEnum.PreviousFrame:
_mainWindow.SeekTo(_mediaPlayer.Time - step);
break;
case PlaybackControlEnum.NextFrame:
_mainWindow.SeekTo(_mediaPlayer.Time + step);
break;
case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations();
break;
case PlaybackControlEnum.RemoveSelectedAnns:
_mainWindow.Editor.RemoveSelectedAnns();
break;
case PlaybackControlEnum.RemoveAllAnns:
_mainWindow.Editor.RemoveAllAnns();
break;
case PlaybackControlEnum.TurnOnVolume:
_mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
_mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
_mediaPlayer.Volume = _formState.CurrentVolume;
break;
case PlaybackControlEnum.TurnOffVolume:
_mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
_mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
_formState.CurrentVolume = _mediaPlayer.Volume;
_mediaPlayer.Volume = 0;
break;
case PlaybackControlEnum.Previous:
NextMedia(isPrevious: true);
break;
case PlaybackControlEnum.Next:
NextMedia();
break;
case PlaybackControlEnum.None:
break;
default:
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null);
}
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
throw;
}
}
private void NextMedia(bool isPrevious = false)
{
var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : _mainWindow.LvFiles.Items.Count;
if (_mainWindow.LvFiles.SelectedIndex + increment == check)
return;
_mainWindow.LvFiles.SelectedIndex += increment;
Play();
}
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
{
ChangeVolume(notification.Volume);
await Task.CompletedTask;
}
private void ChangeVolume(int volume)
{
_formState.CurrentVolume = volume;
_mediaPlayer.Volume = volume;
}
private void Play()
{
if (_mainWindow.LvFiles.SelectedItem == null)
return;
var mediaInfo = (MediaFileInfo)_mainWindow.LvFiles.SelectedItem;
_formState.CurrentMedia = mediaInfo;
_mediaPlayer.Stop();
_mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
_mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
_mediaPlayer.Play(new Media(_libVLC, mediaInfo.Path));
}
private async Task SaveAnnotations()
{
var annGridSelectedIndex = _mainWindow.DgAnnotations.SelectedIndex;
if (_formState.CurrentMedia == null)
return;
var time = _formState.BackgroundTime ?? TimeSpan.FromMilliseconds(_mediaPlayer.Time);
var fName = _formState.GetTimeName(time);
var currentAnns = _mainWindow.Editor.CurrentAnns
.Select(x => new YoloLabel(x.Info, _mainWindow.Editor.RenderSize, _formState.BackgroundTime.HasValue ? _mainWindow.Editor.RenderSize : _formState.CurrentVideoSize))
.ToList();
await YoloLabel.WriteToFile(currentAnns, Path.Combine(_config.LabelsDirectory, $"{fName}.txt"));
await _mainWindow.AddAnnotations(time, currentAnns);
_formState.CurrentMedia.HasAnnotations = _mainWindow.Annotations.Count != 0;
_mainWindow.LvFiles.Items.Refresh();
var isVideo = _formState.CurrentMedia.MediaType == MediaTypes.Video;
var destinationPath = Path.Combine(_config.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(_formState.CurrentMedia.Path))}");
_mainWindow.Editor.RemoveAllAnns();
if (isVideo)
{
if (_formState.BackgroundTime.HasValue)
{
//no need to save image, it's already there, just remove background
_mainWindow.Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
_formState.BackgroundTime = null;
//next item
var annGrid = _mainWindow.DgAnnotations;
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGridSelectedIndex + 1);
_mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
}
else
{
var resultHeight = (uint)Math.Round(RESULT_WIDTH / _formState.CurrentVideoSize.Width * _formState.CurrentVideoSize.Height);
_mediaPlayer.TakeSnapshot(0, destinationPath, RESULT_WIDTH, resultHeight);
_mediaPlayer.Play();
}
}
else
{
File.Copy(_formState.CurrentMedia.Path, destinationPath, overwrite: true);
NextMedia();
}
var thumbnailDto = await _galleryManager.CreateThumbnail(destinationPath);
if (thumbnailDto != null)
_datasetExplorer.AddThumbnail(thumbnailDto, currentAnns.Select(x => x.ClassNumber));
}
}
+19 -18
View File
@@ -1,11 +1,12 @@
using System.IO;
using 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,21 +49,21 @@ 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)
{
if (detections[i].Probability > detections[j].Probability)
{
filteredDetections.Add(detections[i]);
detections.RemoveAt(j);
}
else
{
filteredDetections.Add(detections[j]);
detections.RemoveAt(i);
}
detectionSelected = true;
break;
filteredDetections.Add(detections[i]);
detections.RemoveAt(j);
}
else
{
filteredDetections.Add(detections[j]);
detections.RemoveAt(i);
}
detectionSelected = true;
break;
}
if (!detectionSelected)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB