Files
annotations/Azaion.Annotator/AnnotatorEventHandler.cs
T
Alex Bezdieniezhnykh f58dd3d04f switcher dataset explorer
lat lon -> geopoint
correct location for gps if small keypoints number
2025-06-24 02:13:30 +03:00

407 lines
16 KiB
C#

using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Azaion.Annotator.Controls;
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 GMap.NET;
using GMap.NET.WindowsPresentation;
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>,
INotificationHandler<SetStatusTextEvent>,
INotificationHandler<GPSMatcherResultProcessedEvent>
{
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;
}
public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
mainWindow.StatusHelp.Text = e.Text;
mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White;
});
return Task.CompletedTask;
}
public Task Handle(GPSMatcherResultProcessedEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
var ann = mainWindow.MapMatcherComponent.Annotations[e.Index];
AddMarker(e.GeoPoint, e.Image, Brushes.Blue);
if (e.ProcessedGeoPoint != e.GeoPoint)
AddMarker(e.ProcessedGeoPoint, $"{e.Image}: corrected", Brushes.DarkViolet);
ann.Lat = e.GeoPoint.Lat;
ann.Lon = e.GeoPoint.Lon;
});
return Task.CompletedTask;
}
private void AddMarker(GeoPoint point, string text, SolidColorBrush color)
{
var map = mainWindow.MapMatcherComponent;
var pointLatLon = new PointLatLng(point.Lat, point.Lon);
var marker = new GMapMarker(pointLatLon);
marker.Shape = new CircleVisual(marker, size: 14, text: text, background: color);
map.SatelliteMap.Markers.Add(marker);
map.SatelliteMap.Position = pointLatLon;
map.SatelliteMap.ZoomAndCenterMarkers(null);
}
}