Files
annotations/Azaion.Annotator/AnnotatorEventHandler.cs
T
Alex Bezdieniezhnykh d842466594 gps matcher async
put cryptography lib to fixed version
fix race condition bug in queue handler
add lock to db writing and backup to file db on each write
2025-05-29 00:35:35 +03:00

367 lines
14 KiB
C#

using System.IO;
using System.Windows;
using System.Windows.Input;
using Azaion.Annotator.DTO;
using Azaion.Common;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.Common.Services;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services;
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,
IAnnotationService annotationService,
ILogger<AnnotatorEventHandler> logger,
IOptions<DirectoriesConfig> dirConfig,
IOptions<AnnotationConfig> annotationConfig,
IInferenceService inferenceService,
IDbFactory dbFactory,
IAzaionApi api,
FailsafeAnnotationsProducer producer)
:
INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<AnnotatorControlEvent>,
INotificationHandler<VolumeChangedEvent>,
INotificationHandler<AnnotationsDeletedEvent>,
INotificationHandler<AnnotationAddedEvent>
{
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 ct)
{
SelectClass(notification.DetectionClass);
await Task.CompletedTask;
}
private void SelectClass(DetectionClass detClass)
{
mainWindow.Editor.CurrentAnnClass = detClass;
foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected))
ann.DetectionClass = detClass;
mainWindow.LvClasses.SelectNum(detClass.Id);
}
public async Task Handle(KeyEvent keyEvent, CancellationToken ct = default)
{
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((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!);
if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value, ct);
if (key == Key.R)
await mainWindow.AutoDetect();
#region Volume
switch (key)
{
case Key.VolumeMute when mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, ct);
break;
case Key.VolumeMute:
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, ct);
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;
}
#endregion
}
public async Task Handle(AnnotatorControlEvent notification, CancellationToken ct = default)
{
await ControlPlayback(notification.PlaybackControl, ct);
mainWindow.VideoView.Focus();
}
private async Task ControlPlayback(PlaybackControlEnum controlEnum, CancellationToken cancellationToken = default)
{
try
{
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
var step = isCtrlPressed ? LARGE_STEP : STEP;
switch (controlEnum)
{
case PlaybackControlEnum.Play:
await Play(cancellationToken);
break;
case PlaybackControlEnum.Pause:
mediaPlayer.Pause();
if (formState.BackgroundTime.HasValue)
{
mainWindow.Editor.ResetBackground();
formState.BackgroundTime = null;
}
break;
case PlaybackControlEnum.Stop:
inferenceService.StopInference();
await mainWindow.DetectionCancellationSource.CancelAsync();
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(cancellationToken);
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:
await NextMedia(isPrevious: true, ct: cancellationToken);
break;
case PlaybackControlEnum.Next:
await NextMedia(ct: cancellationToken);
break;
case PlaybackControlEnum.None:
break;
default:
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null);
}
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
}
private async Task NextMedia(bool isPrevious = false, CancellationToken ct = default)
{
var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count;
if (mainWindow.LvFiles.SelectedIndex + increment == check)
return;
mainWindow.LvFiles.SelectedIndex += increment;
await Play(ct);
}
public async Task Handle(VolumeChangedEvent notification, CancellationToken ct)
{
ChangeVolume(notification.Volume);
await Task.CompletedTask;
}
private void ChangeVolume(int volume)
{
formState.CurrentVolume = volume;
mediaPlayer.Volume = volume;
}
private async Task Play(CancellationToken ct = default)
{
if (mainWindow.LvFiles.SelectedItem == null)
return;
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
mainWindow.Editor.ResetBackground();
formState.CurrentMedia = mediaInfo;
//need to wait a bit for correct VLC playback event handling
await Task.Delay(100, ct);
mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
if (formState.CurrentMedia.MediaType == MediaTypes.Image)
mediaPlayer.SetPause(true);
}
//SAVE: MANUAL
private async Task SaveAnnotations(CancellationToken cancellationToken = default)
{
if (formState.CurrentMedia == null)
return;
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var originalMediaName = formState.VideoName;
var fName = originalMediaName.ToTimeName(time);
var currentDetections = mainWindow.Editor.CurrentDetections
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
.ToList();
formState.CurrentMedia.HasAnnotations = currentDetections.Count != 0;
mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns();
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{Constants.JPG_EXT}");
if (formState.BackgroundTime.HasValue)
{
//no need to save image, it's already there, just remove background
mainWindow.Editor.ResetBackground();
formState.BackgroundTime = null;
//next item
var annGrid = mainWindow.DgAnnotations;
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
}
else
{
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
if (isVideo)
mediaPlayer.Play();
else
await NextMedia(ct: cancellationToken);
}
var annotation = await annotationService.SaveAnnotation(originalMediaName, time, currentDetections, token: cancellationToken);
if (isVideo)
mainWindow.AddAnnotation(annotation);
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct)
{
try
{
mainWindow.Dispatcher.Invoke(() =>
{
var namesSet = notification.AnnotationNames.ToHashSet();
var remainAnnotations = formState.AnnotationResults
.Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList();
formState.AnnotationResults.Clear();
foreach (var ann in remainAnnotations)
formState.AnnotationResults.Add(ann);
var timedAnnsToRemove = mainWindow.TimedAnnotations
.Where(x => namesSet.Contains(x.Value.Name))
.Select(x => x.Value).ToList();
mainWindow.TimedAnnotations.Remove(timedAnnsToRemove);
if (formState.AnnotationResults.Count == 0)
{
var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name);
if (media != null)
{
media.HasAnnotations = false;
mainWindow.LvFiles.Items.Refresh();
}
}
});
await dbFactory.DeleteAnnotations(notification.AnnotationNames, ct);
try
{
foreach (var name in notification.AnnotationNames)
{
File.Delete(Path.Combine(dirConfig.Value.ImagesDirectory, $"{name}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.LabelsDirectory, $"{name}{Constants.TXT_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
}
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
//Only validators can send Delete to the queue
if (!notification.FromQueue && api.CurrentUser.Role.IsValidator())
await producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
}
public Task Handle(AnnotationAddedEvent e, CancellationToken ct)
{
mainWindow.Dispatcher.Invoke(() =>
{
mainWindow.AddAnnotation(e.Annotation);
var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det =>
$"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"розмір=({det.Width:F2}, {det.Height:F2}), " +
$"conf: {det.Confidence*100:F0}%"));
mainWindow.LvFiles.Items.Refresh();
var media = mainWindow.MediaFilesDict.GetValueOrDefault(e.Annotation.OriginalMediaName);
if (media != null)
media.HasAnnotations = true;
mainWindow.LvFiles.Items.Refresh();
mainWindow.StatusHelp.Text = log;
});
return Task.CompletedTask;
}
}