diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index ac5f528..f5d36f5 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -9,25 +9,26 @@ using System.Windows.Media; using System.Windows.Media.Imaging; using Azaion.Annotator.DTO; using Azaion.Annotator.Extensions; -using Azaion.Common; +using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; +using Azaion.Common.Events; using Azaion.Common.Extensions; using Azaion.Common.Services; using LibVLCSharp.Shared; using MediatR; using Microsoft.WindowsAPICodePack.Dialogs; -using Newtonsoft.Json; using Size = System.Windows.Size; using IntervalTree; +using LinqToDB; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MediaPlayer = LibVLCSharp.Shared.MediaPlayer; namespace Azaion.Annotator; -public partial class Annotator +public partial class Annotator : INotificationHandler { private readonly AppConfig _appConfig; private readonly LibVLC _libVLC; @@ -41,7 +42,8 @@ public partial class Annotator private readonly VLCFrameExtractor _vlcFrameExtractor; private readonly IAIDetector _aiDetector; private readonly AnnotationService _annotationService; - private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly IDbFactory _dbFactory; + private readonly CancellationTokenSource _ctSource = new(); private ObservableCollection AnnotationClasses { get; set; } = new(); private bool _suspendLayout; @@ -53,20 +55,22 @@ public partial class Annotator private ObservableCollection AllMediaFiles { get; set; } = new(); private ObservableCollection FilteredMediaFiles { get; set; } = new(); - public IntervalTree> Detections { get; set; } = new(); + public IntervalTree TimedAnnotations { get; set; } = new(); private AutodetectDialog _autoDetectDialog = new() { Topmost = true }; public Annotator( IConfigUpdater configUpdater, IOptions appConfig, - LibVLC libVLC, MediaPlayer mediaPlayer, + LibVLC libVLC, + MediaPlayer mediaPlayer, IMediator mediator, FormState formState, HelpWindow helpWindow, ILogger logger, VLCFrameExtractor vlcFrameExtractor, IAIDetector aiDetector, - AnnotationService annotationService) + AnnotationService annotationService, + IDbFactory dbFactory) { InitializeComponent(); _appConfig = appConfig.Value; @@ -80,10 +84,27 @@ public partial class Annotator _vlcFrameExtractor = vlcFrameExtractor; _aiDetector = aiDetector; _annotationService = annotationService; + _dbFactory = dbFactory; Loaded += OnLoaded; Closed += OnFormClosed; Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator; + TbFolder.TextChanged += async (sender, args) => + { + if (!Path.Exists(TbFolder.Text)) + return; + try + { + _appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text; + ReloadFiles(); + await SaveUserSettings(); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + } + }; + Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); } @@ -98,8 +119,7 @@ public partial class Annotator MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.RightPanelWidth); _suspendLayout = false; - - ReloadFiles(); + TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory; AnnotationClasses = new ObservableCollection(_appConfig.AnnotationConfig.AnnotationClasses); LvClasses.ItemsSource = AnnotationClasses; @@ -141,7 +161,7 @@ public partial class Annotator _formState.CurrentVideoSize = new Size(vw, vh); _formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length); - await Dispatcher.Invoke(async () => await ReloadAnnotations(_cancellationTokenSource.Token)); + await Dispatcher.Invoke(async () => await ReloadAnnotations()); if (_formState.CurrentMedia?.MediaType == MediaTypes.Image) { @@ -182,7 +202,7 @@ public partial class Annotator OpenAnnotationResult((AnnotationResult)dgRow!.Item); }; - DgAnnotations.KeyUp += (sender, args) => + DgAnnotations.KeyUp += async (sender, args) => { switch (args.Key) { @@ -196,17 +216,9 @@ public partial class Annotator return; var res = DgAnnotations.SelectedItems.Cast().ToList(); - foreach (var annotationResult in res) - { - var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image); - var thumbnailPath = Path.Combine(_appConfig.DirectoriesConfig.ThumbnailsDirectory, $"{imgName}{Constants.THUMBNAIL_PREFIX}.jpg"); - File.Delete(annotationResult.Image); + var annotations = res.Select(x => x.Annotation).ToList(); - File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt")); - File.Delete(thumbnailPath); - _formState.AnnotationResults.Remove(annotationResult); - Detections.Remove(Detections.Query(annotationResult.Time)); - } + await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); break; } }; @@ -219,16 +231,16 @@ public partial class Annotator { _mediaPlayer.SetPause(true); Editor.RemoveAllAnns(); - _mediaPlayer.Time = (long)res.Time.TotalMilliseconds; + _mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds; Dispatcher.Invoke(() => { VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; - Editor.ClearExpiredAnnotations(res.Time); + Editor.ClearExpiredAnnotations(res.Annotation.Time); }); - AddAnnotationsToCanvas(res.Time, res.Detections, showImage: true); + ShowAnnotations(res.Annotation, showImage: true); } private async Task SaveUserSettings() { @@ -254,114 +266,73 @@ public partial class Annotator Editor.ClearExpiredAnnotations(time); }); - var annotations = Detections.Query(time).SelectMany(x => x).Select(x => new Detection(_formState.GetTimeName(time), x)); - AddAnnotationsToCanvas(time, annotations); + ShowAnnotations(TimedAnnotations.Query(time).FirstOrDefault()); } - private void AddAnnotationsToCanvas(TimeSpan? time, IEnumerable labels, bool showImage = false) + private void ShowAnnotations(Annotation? annotation, bool showImage = false) { + if (annotation == null) + return; Dispatcher.Invoke(async () => { var canvasSize = Editor.RenderSize; var videoSize = _formState.CurrentVideoSize; if (showImage) { - var fName = _formState.GetTimeName(time); - var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg"); - if (File.Exists(imgPath)) + if (File.Exists(annotation.ImagePath)) { - Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; - _formState.BackgroundTime = time; + Editor.Background = new ImageBrush { ImageSource = await annotation.ImagePath.OpenImage() }; + _formState.BackgroundTime = annotation.Time; videoSize = Editor.RenderSize; } } - foreach (var label in labels) + foreach (var detection in annotation.Detections) { - var annClass = _appConfig.AnnotationConfig.AnnotationClasses[label.ClassNumber]; - var canvasLabel = new CanvasLabel(label, canvasSize, videoSize, label.Probability); - Editor.CreateAnnotation(annClass, time, canvasLabel); + var annClass = _appConfig.AnnotationConfig.AnnotationClasses[detection.ClassNumber]; + var canvasLabel = new CanvasLabel(detection, canvasSize, videoSize, detection.Probability); + Editor.CreateDetectionControl(annClass, annotation.Time, canvasLabel); } }); } - private async Task ReloadAnnotations(CancellationToken ct = default) + private async Task ReloadAnnotations() { _formState.AnnotationResults.Clear(); - Detections.Clear(); + TimedAnnotations.Clear(); Editor.RemoveAllAnns(); - var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory); - if (!labelDir.Exists) - return; + var annotations = await _dbFactory.Run(async db => + await db.Annotations.LoadWith(x => x.Detections) + .Where(x => x.Name.Contains(_formState.VideoName)) + .ToListAsync(token: _ctSource.Token)); - var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_??????.txt"); - foreach (var file in labelFiles) - await AddAnnotations(Path.GetFileNameWithoutExtension(file.Name), await YoloLabel.ReadFromFile(file.FullName, ct), ct); + foreach (var ann in annotations) + AddAnnotation(ann); } - //Load from yolo label file - public async Task AddAnnotations(string name, List annotations, CancellationToken ct = default) - => await AddAnnotations(name, annotations.Select(x => new Detection(name, x)).ToList(), ct); - //Add manually - public async Task AddAnnotations(string name, List detections, CancellationToken ct = default) + public void AddAnnotation(Annotation annotation) { - var time = Constants.GetTime(name); - var timeValue = time ?? TimeSpan.FromMinutes(0); - var previousAnnotations = Detections.Query(timeValue); - Detections.Remove(previousAnnotations); - Detections.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections); + var time = annotation.Time; + var previousAnnotations = TimedAnnotations.Query(time); + TimedAnnotations.Remove(previousAnnotations); + TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation); - var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time); + var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Annotation.Time == time); if (existingResult != null) _formState.AnnotationResults.Remove(existingResult); var dict = _formState.AnnotationResults - .Select((x, i) => new { x.Time, Index = i }) + .Select((x, i) => new { x.Annotation.Time, Index = i }) .ToDictionary(x => x.Time, x => x.Index); - var index = dict.Where(x => x.Key < timeValue) - .OrderBy(x => timeValue - x.Key) + var index = dict.Where(x => x.Key < time) + .OrderBy(x => time - x.Key) .Select(x => x.Value + 1) .FirstOrDefault(); - _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 detections) - { - var annotationResult = new AnnotationResult - { - Time = timeValue, - Image = $"{_formState.GetTimeName(timeValue)}.jpg", - Detections = detections, - }; - if (detections.Count <= 0) - return annotationResult; - - Color GetAnnotationClass(List detectionClasses, int colorNumber) - { - if (detections.Count == 0) - return (-1).ToColor(); - - return colorNumber >= detectionClasses.Count - ? _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.LastOrDefault()].Color - : _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses[colorNumber]].Color; - } - - var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList(); - - annotationResult.ClassName = detectionClasses.Count > 1 - ? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.DetectionClassesDict[x].ShortName)) - : _appConfig.AnnotationConfig.DetectionClassesDict[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; + _formState.AnnotationResults.Insert(index, new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, annotation)); } private void ReloadFiles() @@ -407,7 +378,6 @@ public partial class Annotator AllMediaFiles = new ObservableCollection(videoFiles.Concat(imageFiles).ToList()); LvFiles.ItemsSource = AllMediaFiles; - TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory; BlinkHelp(AllMediaFiles.Count == 0 ? HelpTexts.HelpTextsDict[HelpTextEnum.Initial] @@ -458,16 +428,15 @@ public partial class Annotator IsFolderPicker = true, InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) }; - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) + var dialogResult = dlg.ShowDialog(); + + if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName)) return; - if (!string.IsNullOrEmpty(dlg.FileName)) - { - _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName; - await SaveUserSettings(); - } - + _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName; + TbFolder.Text = dlg.FileName; ReloadFiles(); + await SaveUserSettings(); } private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e) @@ -546,7 +515,7 @@ public partial class Annotator while (mediaInfo != null) { _formState.CurrentMedia = mediaInfo; - await Dispatcher.Invoke(async () => await ReloadAnnotations(token)); + await Dispatcher.Invoke(async () => await ReloadAnnotations()); if (mediaInfo.MediaType == MediaTypes.Image) { @@ -632,7 +601,7 @@ public partial class Annotator continue; mediaInfo.HasAnnotations = true; - await ProcessDetection(timeframe, "jpg", detections, token); + await ProcessDetection(timeframe, ".jpg", detections, token); } catch (Exception ex) { @@ -697,17 +666,15 @@ public partial class Annotator try { var time = timeframe.Time; - var fName = _formState.GetTimeName(timeframe.Time); - var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.{imageExtension}"); - Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; + var annotation = await _annotationService.SaveAnnotation(fName, imageExtension, detections, SourceEnum.AI, timeframe.Stream, token); + + Editor.Background = new ImageBrush { ImageSource = await annotation.ImagePath.OpenImage() }; Editor.RemoveAllAnns(); - AddAnnotationsToCanvas(time, detections, true); - await AddAnnotations(fName, detections, token); + ShowAnnotations(annotation, true); + AddAnnotation(annotation); - await _annotationService.SaveAnnotation(fName, imageExtension, detections, SourceEnum.AI, timeframe.Stream, token); - var log = string.Join(Environment.NewLine, detections.Select(det => $"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " + $"xy=({det.CenterX:F2},{det.CenterY:F2}), " + @@ -724,4 +691,14 @@ public partial class Annotator } }); } + + public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) + { + var annResDict = _formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x); + foreach (var ann in notification.Annotations) + { + _formState.AnnotationResults.Remove(annResDict[ann.Name]); + TimedAnnotations.Remove(ann); + } + } } diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 76f5932..89c02ae 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -6,6 +6,7 @@ using Azaion.Common; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; +using Azaion.Common.Events; using Azaion.Common.Services; using LibVLCSharp.Shared; using MediatR; @@ -232,9 +233,7 @@ public class AnnotatorEventHandler( .Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))) .ToList(); - await mainWindow.AddAnnotations(fName, currentDetections, cancellationToken); - - formState.CurrentMedia.HasAnnotations = mainWindow.Detections.Count != 0; + formState.CurrentMedia.HasAnnotations = mainWindow.TimedAnnotations.Count != 0; mainWindow.LvFiles.Items.Refresh(); mainWindow.Editor.RemoveAllAnns(); @@ -267,6 +266,7 @@ public class AnnotatorEventHandler( File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true); NextMedia(); } - await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken); + var annotation = await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken); + mainWindow.AddAnnotation(annotation); } } diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index aa3cb22..d7161db 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -65,24 +65,6 @@ public class Constants #endregion - public static TimeSpan? GetTime(string imagePath) - { - var timeStr = imagePath.Split("_").LastOrDefault(); - if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6) - return null; - - //For some reason, TimeSpan.ParseExact doesn't work on every platform. - if (!int.TryParse(timeStr[0..1], out var hours)) - return null; - if (!int.TryParse(timeStr[1..3], out var minutes)) - return null; - if (!int.TryParse(timeStr[3..5], out var seconds)) - return null; - if (!int.TryParse(timeStr[5..6], out var milliseconds)) - return null; - return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100); - } - #region Queue public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; diff --git a/Azaion.Common/Controls/CanvasEditor.cs b/Azaion.Common/Controls/CanvasEditor.cs index d2d4430..379a03e 100644 --- a/Azaion.Common/Controls/CanvasEditor.cs +++ b/Azaion.Common/Controls/CanvasEditor.cs @@ -34,13 +34,13 @@ public class CanvasEditor : Canvas public static readonly DependencyProperty GetTimeFuncProp = DependencyProperty.Register( nameof(GetTimeFunc), - typeof(Func), + typeof(Func), typeof(CanvasEditor), new PropertyMetadata(null)); - public Func GetTimeFunc + public Func GetTimeFunc { - get => (Func)GetValue(GetTimeFuncProp); + get => (Func)GetValue(GetTimeFuncProp); set => SetValue(GetTimeFuncProp, value); } @@ -154,7 +154,7 @@ public class CanvasEditor : Canvas private void CanvasMouseUp(object sender, MouseButtonEventArgs e) { if (SelectionState == SelectionState.NewAnnCreating) - CreateAnnotation(e.GetPosition(this)); + CreateDetectionControl(e.GetPosition(this)); SelectionState = SelectionState.None; e.Handled = true; @@ -291,7 +291,7 @@ public class CanvasEditor : Canvas SetTop(_newAnnotationRect, currentPos.Y); } - private void CreateAnnotation(Point endPos) + private void CreateDetectionControl(Point endPos) { _newAnnotationRect.Width = 0; _newAnnotationRect.Height = 0; @@ -301,7 +301,7 @@ public class CanvasEditor : Canvas return; var time = GetTimeFunc(); - CreateAnnotation(CurrentAnnClass, time, new CanvasLabel + CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel { Width = width, Height = height, @@ -310,20 +310,20 @@ public class CanvasEditor : Canvas }); } - public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel) + public DetectionControl CreateDetectionControl(DetectionClass annClass, TimeSpan time, CanvasLabel canvasLabel) { - var annotationControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability) + var detectionControl = new DetectionControl(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); - CurrentDetections.Add(annotationControl); + detectionControl.MouseDown += AnnotationPositionStart; + SetLeft(detectionControl, canvasLabel.X ); + SetTop(detectionControl, canvasLabel.Y); + Children.Add(detectionControl); + CurrentDetections.Add(detectionControl); _newAnnotationRect.Fill = new SolidColorBrush(annClass.Color); - return annotationControl; + return detectionControl; } #endregion @@ -355,8 +355,7 @@ public class CanvasEditor : Canvas public void ClearExpiredAnnotations(TimeSpan time) { var expiredAnns = CurrentDetections.Where(x => - x.Time.HasValue && - Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds) + Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds) .ToList(); RemoveAnnotations(expiredAnns); } diff --git a/Azaion.Common/Controls/DetectionControl.cs b/Azaion.Common/Controls/DetectionControl.cs index dcec250..0fdc09e 100644 --- a/Azaion.Common/Controls/DetectionControl.cs +++ b/Azaion.Common/Controls/DetectionControl.cs @@ -16,7 +16,7 @@ public class DetectionControl : Border private readonly Grid _grid; private readonly TextBlock _classNameLabel; private readonly Label _probabilityLabel; - public TimeSpan? Time { get; set; } + public TimeSpan Time { get; set; } private DetectionClass _detectionClass = null!; public DetectionClass DetectionClass @@ -44,7 +44,7 @@ public class DetectionControl : Border } } - public DetectionControl(DetectionClass detectionClass, TimeSpan? time, Action resizeStart, double? probability = null) + public DetectionControl(DetectionClass detectionClass, TimeSpan time, Action resizeStart, double? probability = null) { Time = time; _resizeStart = resizeStart; diff --git a/Azaion.Common/DTO/AnnotationResult.cs b/Azaion.Common/DTO/AnnotationResult.cs index 94a89e4..bda5e88 100644 --- a/Azaion.Common/DTO/AnnotationResult.cs +++ b/Azaion.Common/DTO/AnnotationResult.cs @@ -1,40 +1,52 @@ using System.Windows.Media; +using Azaion.Common.Database; +using Azaion.Common.Extensions; using Newtonsoft.Json; namespace Azaion.Common.DTO; public class AnnotationResult { - [JsonProperty(PropertyName = "f")] - public string Image { get; set; } = null!; + public Annotation Annotation { get; set; } - [JsonProperty(PropertyName = "t")] - public TimeSpan Time { get; set; } + public string ImagePath { get; set; } + public string TimeStr { get; set; } - public double Lat { get; set; } - public double Lon { get; set; } - public List Detections { get; set; } = new(); + public string ClassName { get; set; } - #region For XAML Form + public Color ClassColor0 { get; set; } + public Color ClassColor1 { get; set; } + public Color ClassColor2 { get; set; } + public Color ClassColor3 { get; set; } - [JsonIgnore] - public string TimeStr => $"{Time:h\\:mm\\:ss}"; - [JsonIgnore] - public string ClassName { get; set; } = null!; + public AnnotationResult(Dictionary allDetectionClasses, Annotation annotation) + { + Annotation = annotation; + var detections = annotation.Detections.ToList(); - [JsonIgnore] - public Color ClassColor0 { get; set; } + Color GetAnnotationClass(List detectionClasses, int colorNumber) + { + if (detections.Count == 0) + return (-1).ToColor(); - [JsonIgnore] - public Color ClassColor1 { get; set; } + return colorNumber >= detectionClasses.Count + ? allDetectionClasses[detectionClasses.LastOrDefault()].Color + : allDetectionClasses[detectionClasses[colorNumber]].Color; + } - [JsonIgnore] - public Color ClassColor2 { get; set; } + TimeStr = $"{annotation.Time:h\\:mm\\:ss}"; + ImagePath = annotation.ImagePath; - [JsonIgnore] - public Color ClassColor3 { get; set; } + var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList(); - #endregion + ClassName = detectionClasses.Count > 1 + ? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].ShortName)) + : allDetectionClasses[detectionClasses.FirstOrDefault()].Name; + ClassColor0 = GetAnnotationClass(detectionClasses, 0); + ClassColor1 = GetAnnotationClass(detectionClasses, 1); + ClassColor2 = GetAnnotationClass(detectionClasses, 2); + ClassColor3 = GetAnnotationClass(detectionClasses, 3); + } } \ No newline at end of file diff --git a/Azaion.Common/DTO/AnnotationImageView.cs b/Azaion.Common/DTO/AnnotationThumbnail.cs similarity index 82% rename from Azaion.Common/DTO/AnnotationImageView.cs rename to Azaion.Common/DTO/AnnotationThumbnail.cs index 983b866..0fd7c8a 100644 --- a/Azaion.Common/DTO/AnnotationImageView.cs +++ b/Azaion.Common/DTO/AnnotationThumbnail.cs @@ -7,7 +7,7 @@ using Azaion.Common.Extensions; namespace Azaion.Common.DTO; -public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged +public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged { public Annotation Annotation { get; set; } = annotation; @@ -30,13 +30,6 @@ public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged public string ImageName => Path.GetFileName(Annotation.ImagePath); public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created; - public void Delete() - { - File.Delete(Annotation.ImagePath); - File.Delete(Annotation.LabelPath); - File.Delete(Annotation.ThumbPath); - } - public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { diff --git a/Azaion.Common/Database/Annotation.cs b/Azaion.Common/Database/Annotation.cs index 3a155f5..03351fd 100644 --- a/Azaion.Common/Database/Annotation.cs +++ b/Azaion.Common/Database/Annotation.cs @@ -36,6 +36,30 @@ public class Annotation public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}"); public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt"); public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg"); + + private TimeSpan? _time; + public TimeSpan Time + { + get + { + if (_time.HasValue) + return _time.Value; + + var timeStr = Name.Split("_").LastOrDefault(); + + //For some reason, TimeSpan.ParseExact doesn't work on every platform. + if (!string.IsNullOrEmpty(timeStr) && + timeStr.Length == 6 && + int.TryParse(timeStr[..1], out var hours) && + int.TryParse(timeStr[1..3], out var minutes) && + int.TryParse(timeStr[3..5], out var seconds) && + int.TryParse(timeStr[5..], out var milliseconds)) + return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100); + + _time = TimeSpan.FromSeconds(0); + return _time.Value; + } + } } diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs index 7614e4a..a9f27bc 100644 --- a/Azaion.Common/Database/DbFactory.cs +++ b/Azaion.Common/Database/DbFactory.cs @@ -17,6 +17,7 @@ public interface IDbFactory Task Run(Func> func); Task Run(Func func); void SaveToDisk(); + Task DeleteAnnotations(List annotations, CancellationToken cancellationToken = default); } public class DbFactory : IDbFactory @@ -41,7 +42,7 @@ public class DbFactory : IDbFactory .UseDataProvider(SQLiteTools.GetDataProvider()) .UseConnection(_memoryConnection) .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); - _ = _memoryDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText)); + //.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText)); _fileConnection = new SQLiteConnection(FileConnStr); @@ -96,6 +97,16 @@ public class DbFactory : IDbFactory { _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); } + + public async Task DeleteAnnotations(List annotations, CancellationToken cancellationToken = default) + { + var names = annotations.Select(x => x.Name).ToList(); + await Run(async db => + { + await db.Detections.DeleteAsync(x => names.Contains(x.AnnotationName), token: cancellationToken); + await db.Annotations.DeleteAsync(x => names.Contains(x.Name), token: cancellationToken); + }); + } } public static class AnnotationsDbSchemaHolder @@ -110,6 +121,7 @@ public static class AnnotationsDbSchemaHolder builder.Entity() .HasTableName(Constants.ANNOTATIONS_TABLENAME) .HasPrimaryKey(x => x.Name) + .Ignore(x => x.Time) .Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName); builder.Entity() diff --git a/Azaion.Common/DTO/AnnotationCreatedEvent.cs b/Azaion.Common/Events/AnnotationCreatedEvent.cs similarity index 85% rename from Azaion.Common/DTO/AnnotationCreatedEvent.cs rename to Azaion.Common/Events/AnnotationCreatedEvent.cs index 99539a3..5370352 100644 --- a/Azaion.Common/DTO/AnnotationCreatedEvent.cs +++ b/Azaion.Common/Events/AnnotationCreatedEvent.cs @@ -1,7 +1,7 @@ using Azaion.Common.Database; using MediatR; -namespace Azaion.Common.DTO; +namespace Azaion.Common.Events; public class AnnotationCreatedEvent(Annotation annotation) : INotification { diff --git a/Azaion.Common/Events/AnnotationsDeletedEvent.cs b/Azaion.Common/Events/AnnotationsDeletedEvent.cs new file mode 100644 index 0000000..1e815f2 --- /dev/null +++ b/Azaion.Common/Events/AnnotationsDeletedEvent.cs @@ -0,0 +1,9 @@ +using Azaion.Common.Database; +using MediatR; + +namespace Azaion.Common.Events; + +public class AnnotationsDeletedEvent(List annotations) : INotification +{ + public List Annotations { get; set; } = annotations; +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Annotator2ControlEvent.cs b/Azaion.Common/Events/AnnotatorControlEvent.cs similarity index 100% rename from Azaion.Common/DTO/Annotator2ControlEvent.cs rename to Azaion.Common/Events/AnnotatorControlEvent.cs diff --git a/Azaion.Common/DTO/KeyEvent.cs b/Azaion.Common/Events/KeyEvent.cs similarity index 84% rename from Azaion.Common/DTO/KeyEvent.cs rename to Azaion.Common/Events/KeyEvent.cs index 705d28b..560f440 100644 --- a/Azaion.Common/DTO/KeyEvent.cs +++ b/Azaion.Common/Events/KeyEvent.cs @@ -1,7 +1,8 @@ using System.Windows.Input; +using Azaion.Common.DTO; using MediatR; -namespace Azaion.Common.DTO; +namespace Azaion.Common.Events; public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification { diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index c05491f..61cb462 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -5,6 +5,7 @@ using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; +using Azaion.Common.Events; using Azaion.Common.Extensions; using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.Services; @@ -19,7 +20,7 @@ using RabbitMQ.Stream.Client.Reliable; namespace Azaion.Common.Services; -public class AnnotationService +public class AnnotationService : INotificationHandler { private readonly AzaionApiClient _apiClient; private readonly IDbFactory _dbFactory; @@ -83,12 +84,13 @@ public class AnnotationService } //AI / Manual - public async Task SaveAnnotation(string fName, string imageExtension, List detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) => - await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, token); + public async Task SaveAnnotation(string fName, string imageExtension, List detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) => + await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, generateThumbnail: true, token); //Manual public async Task ValidateAnnotation(Annotation annotation, CancellationToken token = default) => - await SaveAnnotationInner(DateTime.UtcNow, annotation.Name, annotation.ImageExtension, annotation.Detections.ToList(), SourceEnum.Manual, null, _apiClient.User.Role, _apiClient.User.Email, token); + await SaveAnnotationInner(DateTime.UtcNow, annotation.Name, annotation.ImageExtension, annotation.Detections.ToList(), SourceEnum.Manual, null, _apiClient.User.Role, _apiClient.User.Email, + generateThumbnail: false, token); //Queue (only from operators) public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default) @@ -105,12 +107,14 @@ public class AnnotationService new MemoryStream(message.Image), message.CreatedRole, message.CreatedEmail, + generateThumbnail: true, cancellationToken); } - private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List detections, SourceEnum source, Stream? stream, + private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List detections, SourceEnum source, Stream? stream, RoleEnum userRole, string createdEmail, + bool generateThumbnail = false, CancellationToken token = default) { //Flow for roles: @@ -129,11 +133,14 @@ public class AnnotationService await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); await db.BulkCopyAsync(detections, cancellationToken: token); if (ann != null) + { await db.Annotations .Where(x => x.Name == fName) .Set(x => x.Source, source) .Set(x => x.AnnotationStatus, status) .UpdateAsync(token: token); + ann.Detections = detections; + } else { ann = new Annotation @@ -158,7 +165,9 @@ public class AnnotationService img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue } await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); - await _galleryService.CreateThumbnail(annotation, token); + if (generateThumbnail) + await _galleryService.CreateThumbnail(annotation, token); + await _producer.SendToQueue(annotation, token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); @@ -167,5 +176,17 @@ public class AnnotationService _dbFactory.SaveToDisk(); return Task.CompletedTask; }, TimeSpan.FromSeconds(5), token); + return annotation; + } + + public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) + { + await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken); + foreach (var annotation in notification.Annotations) + { + File.Delete(annotation.ImagePath); + File.Delete(annotation.LabelPath); + File.Delete(annotation.ThumbPath); + } } } \ No newline at end of file diff --git a/Azaion.Dataset/DatasetExplorer.xaml b/Azaion.Dataset/DatasetExplorer.xaml index a4c0b10..b0fb5e7 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml +++ b/Azaion.Dataset/DatasetExplorer.xaml @@ -12,7 +12,7 @@ WindowState="Maximized"> - +