From 6809a9cedfce5992cb0594e3e9e7131105424fc3 Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Wed, 15 May 2024 18:22:40 +0300 Subject: [PATCH] annotation previews add button controls --- .../Azaion.Annotator/Azaion.Annotator.csproj | 7 ++ .../Azaion.Annotator/Controls/CanvasEditor.cs | 16 +++ .../Azaion.Annotator/DTO/AnnotationInfo.cs | 4 +- .../Azaion.Annotator/DTO/FormState.cs | 4 +- .../DTO/{KeyEvent.cs => MediatrEvents.cs} | 5 + .../DTO/PlaybackControlEnum.cs | 14 +++ .../Azaion.Annotator/HelpTexts.cs | 24 ++++ .../Azaion.Annotator/MainWindow.xaml | 115 ++++++++++++++++-- .../Azaion.Annotator/MainWindow.xaml.cs | 26 +++- .../Azaion.Annotator/PlayerControlHandler.cs | 98 +++++++++++---- 10 files changed, 272 insertions(+), 41 deletions(-) rename Azaion.Annotator/Azaion.Annotator/DTO/{KeyEvent.cs => MediatrEvents.cs} (58%) create mode 100644 Azaion.Annotator/Azaion.Annotator/DTO/PlaybackControlEnum.cs create mode 100644 Azaion.Annotator/Azaion.Annotator/HelpTexts.cs diff --git a/Azaion.Annotator/Azaion.Annotator/Azaion.Annotator.csproj b/Azaion.Annotator/Azaion.Annotator/Azaion.Annotator.csproj index 858bf21..697ac2e 100644 --- a/Azaion.Annotator/Azaion.Annotator/Azaion.Annotator.csproj +++ b/Azaion.Annotator/Azaion.Annotator/Azaion.Annotator.csproj @@ -19,4 +19,11 @@ + + + + PreserveNewest + + + diff --git a/Azaion.Annotator/Azaion.Annotator/Controls/CanvasEditor.cs b/Azaion.Annotator/Azaion.Annotator/Controls/CanvasEditor.cs index 1ffe950..9dcd98d 100644 --- a/Azaion.Annotator/Azaion.Annotator/Controls/CanvasEditor.cs +++ b/Azaion.Annotator/Azaion.Annotator/Controls/CanvasEditor.cs @@ -1,5 +1,6 @@ using System.Windows; using System.Windows.Controls; +using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; @@ -19,6 +20,7 @@ public class CanvasEditor : Canvas private readonly Line _horizontalLine; private readonly Line _verticalLine; + private readonly TextBlock _classNameHint; private Rectangle _curRec; private AnnotationControl _curAnn; @@ -38,6 +40,9 @@ public class CanvasEditor : Canvas _verticalLine.Fill = value.ColorBrush; _horizontalLine.Stroke = value.ColorBrush; _horizontalLine.Fill = value.ColorBrush; + _classNameHint.Text = value.Name; + _classNameHint.Foreground = value.ColorBrush; + _newAnnotationRect.Stroke = value.ColorBrush; _newAnnotationRect.Fill = value.ColorBrush; _currentAnnClass = value; @@ -64,6 +69,14 @@ public class CanvasEditor : Canvas StrokeDashArray = [5], StrokeThickness = 2 }; + _classNameHint = new TextBlock + { + Text = CurrentAnnClass?.Name ?? "asd", + Foreground = new SolidColorBrush(Colors.Blue), + Cursor = Cursors.Arrow, + FontSize = 16, + FontWeight = FontWeights.Bold + }; _newAnnotationRect = new Rectangle { Name = "selector", @@ -94,6 +107,7 @@ public class CanvasEditor : Canvas Children.Add(_newAnnotationRect); Children.Add(_horizontalLine); Children.Add(_verticalLine); + Children.Add(_classNameHint); } private void CanvasMouseDown(object sender, MouseButtonEventArgs e) @@ -107,6 +121,8 @@ public class CanvasEditor : Canvas 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; diff --git a/Azaion.Annotator/Azaion.Annotator/DTO/AnnotationInfo.cs b/Azaion.Annotator/Azaion.Annotator/DTO/AnnotationInfo.cs index e253023..fa5b0e7 100644 --- a/Azaion.Annotator/Azaion.Annotator/DTO/AnnotationInfo.cs +++ b/Azaion.Annotator/Azaion.Annotator/DTO/AnnotationInfo.cs @@ -65,8 +65,8 @@ public class AnnotationInfo var annInfo = new AnnotationInfo { ClassNumber = this.ClassNumber }; - double left = annInfo.X - annInfo.Width * 2; - double top = annInfo.Y - annInfo.Height * 2; + double left = X - Width / 2; + double top = Y - Height / 2; if (videoAR > canvasAR) //100% width { diff --git a/Azaion.Annotator/Azaion.Annotator/DTO/FormState.cs b/Azaion.Annotator/Azaion.Annotator/DTO/FormState.cs index a754936..e8d5d23 100644 --- a/Azaion.Annotator/Azaion.Annotator/DTO/FormState.cs +++ b/Azaion.Annotator/Azaion.Annotator/DTO/FormState.cs @@ -6,9 +6,11 @@ namespace Azaion.Annotator.DTO; public class FormState { public SelectionState SelectionState { get; set; } = SelectionState.None; + public string CurrentFile { get; set; } = null!; public Size CurrentVideoSize { get; set; } + public string VideoName => Path.GetFileNameWithoutExtension(CurrentFile).Replace(" ", ""); + public TimeSpan CurrentVideoLength { get; set; } - public string VideoName => Path.GetFileNameWithoutExtension(CurrentFile).Replace(" ", ""); public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}"; } diff --git a/Azaion.Annotator/Azaion.Annotator/DTO/KeyEvent.cs b/Azaion.Annotator/Azaion.Annotator/DTO/MediatrEvents.cs similarity index 58% rename from Azaion.Annotator/Azaion.Annotator/DTO/KeyEvent.cs rename to Azaion.Annotator/Azaion.Annotator/DTO/MediatrEvents.cs index 54dfad1..1a818dd 100644 --- a/Azaion.Annotator/Azaion.Annotator/DTO/KeyEvent.cs +++ b/Azaion.Annotator/Azaion.Annotator/DTO/MediatrEvents.cs @@ -8,3 +8,8 @@ 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; +} diff --git a/Azaion.Annotator/Azaion.Annotator/DTO/PlaybackControlEnum.cs b/Azaion.Annotator/Azaion.Annotator/DTO/PlaybackControlEnum.cs new file mode 100644 index 0000000..c2389b5 --- /dev/null +++ b/Azaion.Annotator/Azaion.Annotator/DTO/PlaybackControlEnum.cs @@ -0,0 +1,14 @@ +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 +} \ No newline at end of file diff --git a/Azaion.Annotator/Azaion.Annotator/HelpTexts.cs b/Azaion.Annotator/Azaion.Annotator/HelpTexts.cs new file mode 100644 index 0000000..d936aa4 --- /dev/null +++ b/Azaion.Annotator/Azaion.Annotator/HelpTexts.cs @@ -0,0 +1,24 @@ +namespace Azaion.Annotator; + +public enum HelpTextEnum +{ + None = 0, + Initial = 1, + PlayVideo = 2, + PauseForAnnotations = 3, + AnnotationHelp = 4 +} + +public class HelpTexts +{ + public static Dictionary HelpTextsDict = new() + { + { HelpTextEnum.None, "" }, + { HelpTextEnum.Initial, "Натисніть Файл - Відкрити папку... та виберіть папку з вашими відео для анотації" }, + { HelpTextEnum.PlayVideo, "В списку відео виберіть потрібне та [подвійний клік] чи [Eнтер] на ньому - запустіть його на перегляд" }, + { HelpTextEnum.PauseForAnnotations, "В потрібному місці відео де є один з об'єктів для анотації зупиніть його [Пробіл] або кн. на панелі" }, + { HelpTextEnum.AnnotationHelp, "Клавішами [1] - [9] або мишкою оберіть потрібний клас та виділіть область з об'єктом. Виділяйте всі що є об'єкти. " + + "При потребі [Ctrl] виділяйте анотації та [Del] для видалення. [Eнтер] для збереження і перегляду далі" }, + + }; +} diff --git a/Azaion.Annotator/Azaion.Annotator/MainWindow.xaml b/Azaion.Annotator/Azaion.Annotator/MainWindow.xaml index 262564a..559b9b7 100644 --- a/Azaion.Annotator/Azaion.Annotator/MainWindow.xaml +++ b/Azaion.Annotator/Azaion.Annotator/MainWindow.xaml @@ -67,10 +67,10 @@ Grid.Column="0" Grid.ColumnSpan="3" Background="Black"> - + + IsEnabled="True" Header="Відкрити папку..." Click="MenuItem_OnClick"/> + + + + + - - - + + + + + - + - + diff --git a/Azaion.Annotator/Azaion.Annotator/MainWindow.xaml.cs b/Azaion.Annotator/Azaion.Annotator/MainWindow.xaml.cs index d96079c..89f9e0e 100644 --- a/Azaion.Annotator/Azaion.Annotator/MainWindow.xaml.cs +++ b/Azaion.Annotator/Azaion.Annotator/MainWindow.xaml.cs @@ -63,6 +63,7 @@ public partial class MainWindow AnnotationClasses = new ObservableCollection(_config.AnnotationClasses); LvClasses.ItemsSource = AnnotationClasses; LvClasses.SelectedIndex = 0; + CurrentHelp = HelpTexts.HelpTextsDict[HelpTextEnum.Initial]; } private void InitControls() @@ -74,6 +75,7 @@ public partial class MainWindow uint vw = 0, vh = 0; _mediaPlayer.Size(0, ref vw, ref vh); _formState.CurrentVideoSize = new Size(vw, vh); + _formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length); }; LvFiles.MouseDoubleClick += async (_, _) => @@ -91,6 +93,8 @@ public partial class MainWindow _mediaPlayer.PositionChanged += (o, args) => { Dispatcher.Invoke(() => videoSlider.Value = _mediaPlayer.Position * videoSlider.Maximum); + Dispatcher.Invoke(() => StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"); + var curTime = _formState.GetTimeName(TimeSpan.FromMilliseconds(_mediaPlayer.Time)); if (!Annotations.TryGetValue(curTime, out var annotationInfos)) return; @@ -99,14 +103,15 @@ public partial class MainWindow { var annClass = _config.AnnotationClasses[info.ClassNumber]; var annInfo = info.ToCanvasCoordinates(Editor.RenderSize, _formState.CurrentVideoSize); - return Editor.CreateAnnotation(annClass, annInfo); + var annotation = Dispatcher.Invoke(() => Editor.CreateAnnotation(annClass, annInfo)); + return annotation; }).ToList(); //remove annotations: either in 1 sec, either earlier if there is next annotation in a dictionary var strs = curTime.Split("_"); var timeStr = strs.LastOrDefault(); - var ts = TimeSpan.ParseExact(timeStr, "hmmssf", CultureInfo.InvariantCulture); - var timeSpanRemove = Enumerable.Range(0, (int)_annotationTime.TotalMilliseconds / 100) + var ts = TimeSpan.FromMilliseconds(int.Parse(timeStr)*100); + var timeSpanRemove = Enumerable.Range(1, ((int)_annotationTime.TotalMilliseconds / 100) - 1) .Select(x => { var time = TimeSpan.FromMilliseconds(x * 100); @@ -119,6 +124,7 @@ public partial class MainWindow await Task.Delay(timeSpanRemove); Dispatcher.Invoke(() => Editor.RemoveAnnotations(annotations)); }); + }; videoSlider.ValueChanged += (value, newValue) => @@ -177,6 +183,7 @@ public partial class MainWindow var files = dir.GetFiles("mp4", "mov").Select(x => { _mediaPlayer.Media = new Media(_libVLC, x.FullName); + return new VideoFileInfo { Name = x.Name, @@ -221,4 +228,15 @@ public partial class MainWindow ReloadFiles(); } -} + private void PlayClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Play)); + private void PauseClick(object sender, RoutedEventArgs e)=> _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Pause)); + private void StopClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Stop)); + + private void PreviousFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.PreviousFrame)); + private void NextFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.NextFrame)); + + private void SaveAnnotationsClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.SaveAnnotations)); + + private void RemoveSelectedClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.RemoveSelectedAnns)); + private void RemoveAllClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.RemoveAllAnns)); +} \ No newline at end of file diff --git a/Azaion.Annotator/Azaion.Annotator/PlayerControlHandler.cs b/Azaion.Annotator/Azaion.Annotator/PlayerControlHandler.cs index 14f6ef8..ab13a25 100644 --- a/Azaion.Annotator/Azaion.Annotator/PlayerControlHandler.cs +++ b/Azaion.Annotator/Azaion.Annotator/PlayerControlHandler.cs @@ -8,7 +8,8 @@ namespace Azaion.Annotator; public class PlayerControlHandler: INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler { private const int STEP = 20; private const int LARGE_STEP = 2000; @@ -26,7 +27,17 @@ public class PlayerControlHandler: _formState = formState; _config = config; } - + + private readonly Dictionary 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 } + }; + public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) => SelectClass(notification.AnnotationClass); @@ -40,13 +51,9 @@ public class PlayerControlHandler: public async Task Handle(KeyEvent notification, CancellationToken cancellationToken) { - //Console.WriteLine($"Time: {DateTime.UtcNow:hh:mm:ss.fff}. Sender {notification.Sender.GetType().Name} Key {notification.Args.Key}"); if (!CatchSenders.Contains(notification.Sender.GetType().Name)) return; - var isCtrlPressed = notification.Args.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || - notification.Args.KeyboardDevice.IsKeyDown(Key.RightCtrl); - var step = isCtrlPressed ? STEP : LARGE_STEP; var key = notification.Args.Key; var keyNumber = (int?)null; @@ -57,39 +64,76 @@ public class PlayerControlHandler: if (keyNumber.HasValue) SelectClass(_mainWindow.AnnotationClasses[keyNumber.Value]); - switch (key) + if (KeysControlEnumDict.TryGetValue(key, out var value)) + await ControlPlayback(value); + } + + public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken) + { + await ControlPlayback(notification.PlaybackControl); + } + + private async Task ControlPlayback(PlaybackControlEnum controlEnum) + { + var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); + var step = isCtrlPressed ? LARGE_STEP : STEP; + + switch (controlEnum) { - case Key.Space: + case PlaybackControlEnum.Play: + mediaPlayer.Play(); + break; + case PlaybackControlEnum.Pause: mediaPlayer.Pause(); break; - case Key.Left: + case PlaybackControlEnum.Stop: + mediaPlayer.Stop(); + break; + case PlaybackControlEnum.PreviousFrame: mediaPlayer.SetPause(true); mediaPlayer.Time -= step; break; - case Key.Right: + case PlaybackControlEnum.NextFrame: mediaPlayer.SetPause(true); mediaPlayer.Time += step; break; - case Key.Enter: - if (string.IsNullOrEmpty(_formState.CurrentFile)) - return; - - var fName = _formState.GetTimeName(TimeSpan.FromMilliseconds(mediaPlayer.Time)); - var currentAnns = _mainWindow.Editor.CurrentAnns - .Select(x => x.Info.ToLabelCoordinates(_mainWindow.Editor.RenderSize, _formState.CurrentVideoSize)) - .ToList(); - var labels = string.Join(Environment.NewLine, currentAnns.Select(x => x.ToString())); - - await File.WriteAllTextAsync($"{_config.LabelsDirectory}/{fName}.txt", labels, cancellationToken); - mediaPlayer.TakeSnapshot(0, $"{_config.ImagesDirectory}/{fName}.jpg", 0, 0); - - _mainWindow.Annotations[fName] = currentAnns; - _mainWindow.Editor.RemoveAllAnns(); + case PlaybackControlEnum.SaveAnnotations: + await SaveAnnotations(); break; - - case Key.Delete: + case PlaybackControlEnum.RemoveSelectedAnns: _mainWindow.Editor.RemoveSelectedAnns(); break; + case PlaybackControlEnum.RemoveAllAnns: + _mainWindow.Editor.RemoveAllAnns(); + break; + case PlaybackControlEnum.None: + break; + default: + throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null); } } + + private async Task SaveAnnotations() + { + if (string.IsNullOrEmpty(_formState.CurrentFile)) + return; + + var fName = _formState.GetTimeName(TimeSpan.FromMilliseconds(mediaPlayer.Time)); + var currentAnns = _mainWindow.Editor.CurrentAnns + .Select(x => x.Info.ToLabelCoordinates(_mainWindow.Editor.RenderSize, _formState.CurrentVideoSize)) + .ToList(); + var labels = string.Join(Environment.NewLine, currentAnns.Select(x => x.ToString())); + + if (!Directory.Exists(_config.LabelsDirectory)) + Directory.CreateDirectory(_config.LabelsDirectory); + if (!Directory.Exists(_config.ImagesDirectory)) + Directory.CreateDirectory(_config.ImagesDirectory); + + await File.WriteAllTextAsync($"{_config.LabelsDirectory}/{fName}.txt", labels); + mediaPlayer.TakeSnapshot(0, $"{_config.ImagesDirectory}/{fName}.jpg", 0, 0); + + _mainWindow.Annotations[fName] = currentAnns; + _mainWindow.Editor.RemoveAllAnns(); + mediaPlayer.Play(); + } } \ No newline at end of file