diff --git a/Azaion.Annotator/Annotator.xaml b/Azaion.Annotator/Annotator.xaml index 24161c8..471b81d 100644 --- a/Azaion.Annotator/Annotator.xaml +++ b/Azaion.Annotator/Annotator.xaml @@ -176,11 +176,11 @@ - - + _logger; private readonly VLCFrameExtractor _vlcFrameExtractor; private readonly IAIDetector _aiDetector; + private readonly AnnotationService _annotationService; private readonly CancellationTokenSource _cancellationTokenSource = new(); - private ObservableCollection AnnotationClasses { get; set; } = new(); + private ObservableCollection AnnotationClasses { get; set; } = new(); private bool _suspendLayout; private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100); @@ -52,7 +53,7 @@ public partial class Annotator private ObservableCollection AllMediaFiles { get; set; } = new(); private ObservableCollection FilteredMediaFiles { get; set; } = new(); - public IntervalTree> Annotations { get; set; } = new(); + public IntervalTree> Detections { get; set; } = new(); private AutodetectDialog _autoDetectDialog = new() { Topmost = true }; public Annotator( @@ -64,7 +65,8 @@ public partial class Annotator HelpWindow helpWindow, ILogger logger, VLCFrameExtractor vlcFrameExtractor, - IAIDetector aiDetector) + IAIDetector aiDetector, + AnnotationService annotationService) { InitializeComponent(); _appConfig = appConfig.Value; @@ -77,6 +79,7 @@ public partial class Annotator _logger = logger; _vlcFrameExtractor = vlcFrameExtractor; _aiDetector = aiDetector; + _annotationService = annotationService; Loaded += OnLoaded; Closed += OnFormClosed; @@ -98,7 +101,7 @@ public partial class Annotator ReloadFiles(); - AnnotationClasses = new ObservableCollection(_appConfig.AnnotationConfig.AnnotationClasses); + AnnotationClasses = new ObservableCollection(_appConfig.AnnotationConfig.AnnotationClasses); LvClasses.ItemsSource = AnnotationClasses; LvClasses.SelectedIndex = 0; @@ -152,7 +155,7 @@ public partial class Annotator LvClasses.SelectionChanged += (_, _) => { - var selectedClass = (AnnotationClass)LvClasses.SelectedItem; + var selectedClass = (DetectionClass)LvClasses.SelectedItem; Editor.CurrentAnnClass = selectedClass; _mediator.Publish(new AnnClassSelectedEvent(selectedClass)); }; @@ -188,7 +191,7 @@ public partial class Annotator OpenAnnotationResult((AnnotationResult)DgAnnotations.SelectedItem); break; case Key.Delete: - var result = MessageBox.Show(Application.Current.MainWindow, "Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question); + var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question); if (result != MessageBoxResult.OK) return; @@ -202,7 +205,7 @@ public partial class Annotator File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt")); File.Delete(thumbnailPath); _formState.AnnotationResults.Remove(annotationResult); - Annotations.Remove(Annotations.Query(annotationResult.Time)); + Detections.Remove(Detections.Query(annotationResult.Time)); } break; } @@ -251,7 +254,7 @@ public partial class Annotator Editor.ClearExpiredAnnotations(time); }); - var annotations = Annotations.Query(time).SelectMany(x => x).Select(x => new Detection(x)); + var annotations = Detections.Query(time).SelectMany(x => x).Select(x => new Detection(_formState.GetTimeName(time), x)); AddAnnotationsToCanvas(time, annotations); } @@ -285,7 +288,7 @@ public partial class Annotator private async Task ReloadAnnotations(CancellationToken ct = default) { _formState.AnnotationResults.Clear(); - Annotations.Clear(); + Detections.Clear(); Editor.RemoveAllAnns(); var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory); @@ -294,22 +297,21 @@ public partial class Annotator var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_??????.txt"); foreach (var file in labelFiles) - { - var name = Path.GetFileNameWithoutExtension(file.Name); - var time = Constants.GetTime(name); - await AddAnnotations(time, await YoloLabel.ReadFromFile(file.FullName, ct), ct); - } + await AddAnnotations(Path.GetFileNameWithoutExtension(file.Name), await YoloLabel.ReadFromFile(file.FullName, ct), ct); } - public async Task AddAnnotations(TimeSpan? time, List annotations, CancellationToken ct = default) - => await AddAnnotations(time, annotations.Select(x => new Detection(x)).ToList(), ct); + //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); - public async Task AddAnnotations(TimeSpan? time, List detections, CancellationToken ct = default) + //Add manually + public async Task AddAnnotations(string name, List detections, CancellationToken ct = default) { + var time = Constants.GetTime(name); var timeValue = time ?? TimeSpan.FromMinutes(0); - var previousAnnotations = Annotations.Query(timeValue); - Annotations.Remove(previousAnnotations); - Annotations.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections.Cast().ToList()); + var previousAnnotations = Detections.Query(timeValue); + Detections.Remove(previousAnnotations); + Detections.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections); var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time); if (existingResult != null) @@ -345,15 +347,15 @@ public partial class Annotator return (-1).ToColor(); return colorNumber >= detectionClasses.Count - ? _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses.LastOrDefault()].Color - : _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses[colorNumber]].Color; + ? _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.AnnotationClassesDict[x].ShortName)) - : _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses.FirstOrDefault()].Name; + ? 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); @@ -442,8 +444,8 @@ public partial class Annotator // private void AddClassBtnClick(object sender, RoutedEventArgs e) // { // LvClasses.IsReadOnly = false; - // AnnotationClasses.Add(new AnnotationClass(AnnotationClasses.Count)); - // LvClasses.SelectedIndex = AnnotationClasses.Count - 1; + // DetectionClasses.Add(new DetectionClass(DetectionClasses.Count)); + // LvClasses.SelectedIndex = DetectionClasses.Count - 1; // } private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder(); private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder(); @@ -578,9 +580,10 @@ public partial class Annotator { try { + var fName = Path.GetFileNameWithoutExtension(mediaInfo.Path); var stream = new FileStream(mediaInfo.Path, FileMode.Open); - var detections = await _aiDetector.Detect(stream, token); - await ProcessDetection((TimeSpan.FromMilliseconds(0), stream), detections, token); + var detections = await _aiDetector.Detect(fName, stream, token); + await ProcessDetection((TimeSpan.FromMilliseconds(0), stream), Path.GetExtension(mediaInfo.Path), detections, token); if (detections.Count != 0) mediaInfo.HasAnnotations = true; } @@ -599,7 +602,8 @@ public partial class Annotator Console.WriteLine($"Detect time: {timeframe.Time}"); try { - var detections = await _aiDetector.Detect(timeframe.Stream, token); + var fName = _formState.GetTimeName(timeframe.Time); + var detections = await _aiDetector.Detect(fName, timeframe.Stream, token); var isValid = IsValidDetection(timeframe.Time, detections); if (timeframe.Time.TotalSeconds > prevSeekTime + 1) @@ -628,7 +632,7 @@ public partial class Annotator continue; mediaInfo.HasAnnotations = true; - await ProcessDetection(timeframe, detections, token); + await ProcessDetection(timeframe, "jpg", detections, token); } catch (Exception ex) { @@ -685,7 +689,7 @@ public partial class Annotator return false; } - private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, List detections, CancellationToken token = default) + private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, string imageExtension, List detections, CancellationToken token = default) { _previousDetection = (timeframe.Time, detections); await Dispatcher.Invoke(async () => @@ -695,22 +699,24 @@ public partial class Annotator var time = timeframe.Time; var fName = _formState.GetTimeName(timeframe.Time); - var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg"); + var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.{imageExtension}"); Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; Editor.RemoveAllAnns(); AddAnnotationsToCanvas(time, detections, true); - await AddAnnotations(timeframe.Time, detections, token); + await AddAnnotations(fName, detections, token); + await _annotationService.SaveAnnotation(fName, imageExtension, detections, SourceEnum.AI, timeframe.Stream, token); + var log = string.Join(Environment.NewLine, detections.Select(det => - $"{_appConfig.AnnotationConfig.AnnotationClassesDict[det.ClassNumber].Name}: " + + $"{_appConfig.AnnotationConfig.DetectionClassesDict[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)); - await _mediator.Publish(new ImageCreatedEvent(imgPath), token); + } catch (Exception e) { diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index f68ab58..f366394 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -21,7 +21,6 @@ public class AnnotatorEventHandler( Annotator mainWindow, FormState formState, AnnotationService annotationService, - IMediator mediator, ILogger logger, IOptions dirConfig) : @@ -48,15 +47,15 @@ public class AnnotatorEventHandler( public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) { - SelectClass(notification.AnnotationClass); + SelectClass(notification.DetectionClass); await Task.CompletedTask; } - private void SelectClass(AnnotationClass annClass) + private void SelectClass(DetectionClass annClass) { mainWindow.Editor.CurrentAnnClass = annClass; - foreach (var ann in mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected)) - ann.AnnotationClass = annClass; + foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected)) + ann.DetectionClass = annClass; mainWindow.LvClasses.SelectedIndex = annClass.Id; } @@ -73,7 +72,7 @@ public class AnnotatorEventHandler( 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]!); + SelectClass((DetectionClass)mainWindow.LvClasses.Items[keyNumber.Value]!); if (_keysControlEnumDict.TryGetValue(key, out var value)) await ControlPlayback(value, cancellationToken); @@ -144,7 +143,7 @@ public class AnnotatorEventHandler( mainWindow.SeekTo(mediaPlayer.Time + step); break; case PlaybackControlEnum.SaveAnnotations: - await SaveAnnotations(); + await SaveAnnotations(cancellationToken); break; case PlaybackControlEnum.RemoveSelectedAnns: @@ -229,18 +228,19 @@ public class AnnotatorEventHandler( 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)) + var currentDetections = mainWindow.Editor.CurrentDetections + .Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))) .ToList(); - await mainWindow.AddAnnotations(time, currentAnns, cancellationToken); + await mainWindow.AddAnnotations(fName, currentDetections, cancellationToken); - formState.CurrentMedia.HasAnnotations = mainWindow.Annotations.Count != 0; + formState.CurrentMedia.HasAnnotations = mainWindow.Detections.Count != 0; mainWindow.LvFiles.Items.Refresh(); mainWindow.Editor.RemoveAllAnns(); var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video; - var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}"); + var imageExtension = isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path); + var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{imageExtension}"); if (isVideo) { @@ -267,8 +267,6 @@ public class AnnotatorEventHandler( File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true); NextMedia(); } - await annotationService.SaveAnnotation(fName, currentAnns, SourceEnum.Manual, token: cancellationToken); - - await mediator.Publish(new ImageCreatedEvent(imgPath), cancellationToken); + await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken); } } diff --git a/Azaion.Annotator/DTO/AnnClassSelectedEvent.cs b/Azaion.Annotator/DTO/AnnClassSelectedEvent.cs index 2e0079b..5417378 100644 --- a/Azaion.Annotator/DTO/AnnClassSelectedEvent.cs +++ b/Azaion.Annotator/DTO/AnnClassSelectedEvent.cs @@ -3,7 +3,7 @@ using MediatR; namespace Azaion.Annotator.DTO; -public class AnnClassSelectedEvent(AnnotationClass annotationClass) : INotification +public class AnnClassSelectedEvent(DetectionClass detectionClass) : INotification { - public AnnotationClass AnnotationClass { get; } = annotationClass; + public DetectionClass DetectionClass { get; } = detectionClass; } \ No newline at end of file diff --git a/Azaion.Annotator/YOLODetector.cs b/Azaion.Annotator/YOLODetector.cs index fafe424..883cf6c 100644 --- a/Azaion.Annotator/YOLODetector.cs +++ b/Azaion.Annotator/YOLODetector.cs @@ -15,7 +15,7 @@ namespace Azaion.Annotator; public interface IAIDetector { - Task> Detect(Stream imageStream, CancellationToken cancellationToken = default); + Task> Detect(string fName, Stream imageStream, CancellationToken cancellationToken = default); } public class YOLODetector(IOptions recognitionConfig, IResourceLoader resourceLoader) : IAIDetector, IDisposable @@ -25,7 +25,7 @@ public class YOLODetector(IOptions recognitionConfig, IReso private const string YOLO_MODEL = "azaion.onnx"; - public async Task> Detect(Stream imageStream, CancellationToken cancellationToken) + public async Task> Detect(string fName, Stream imageStream, CancellationToken cancellationToken) { if (_predictor == null) { @@ -42,7 +42,7 @@ public class YOLODetector(IOptions recognitionConfig, IReso var detections = result.Select(d => { var label = new YoloLabel(new CanvasLabel(d.Name.Id, d.Bounds.X, d.Bounds.Y, d.Bounds.Width, d.Bounds.Height), imageSize, imageSize); - return new Detection(label, (double?)d.Confidence * 100); + return new Detection(fName, label, (double?)d.Confidence * 100); }).ToList(); return FilterOverlapping(detections); diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index 6f42d35..b8a2572 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -19,7 +19,7 @@ public class Constants #region AnnotatorConfig - public static readonly List DefaultAnnotationClasses = + public static readonly List DefaultAnnotationClasses = [ new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" }, new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" }, @@ -62,7 +62,6 @@ public class Constants public const int DEFAULT_THUMBNAIL_BORDER = 10; public const string THUMBNAIL_PREFIX = "_thumb"; - public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache"; #endregion @@ -92,6 +91,14 @@ public class Constants public const string ANNOTATION_PRODUCER = "AnnotationsProducer"; public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer"; + #endregion + + #region Database + + public const string ANNOTATIONS_TABLENAME = "annotations"; + public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue"; + public const string ADMIN_EMAIL = "admin@azaion.com"; + public const string DETECTIONS_TABLENAME = "detections"; #endregion diff --git a/Azaion.Common/Controls/CanvasEditor.cs b/Azaion.Common/Controls/CanvasEditor.cs index 1ef090f..d2d4430 100644 --- a/Azaion.Common/Controls/CanvasEditor.cs +++ b/Azaion.Common/Controls/CanvasEditor.cs @@ -24,7 +24,7 @@ public class CanvasEditor : Canvas private readonly TextBlock _classNameHint; private Rectangle _curRec = new(); - private AnnotationControl _curAnn = null!; + private DetectionControl _curAnn = null!; private const int MIN_SIZE = 20; private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400); @@ -44,8 +44,8 @@ public class CanvasEditor : Canvas set => SetValue(GetTimeFuncProp, value); } - private AnnotationClass _currentAnnClass = null!; - public AnnotationClass CurrentAnnClass + private DetectionClass _currentAnnClass = null!; + public DetectionClass CurrentAnnClass { get => _currentAnnClass; set @@ -62,7 +62,7 @@ public class CanvasEditor : Canvas } } - public readonly List CurrentAnns = new(); + public readonly List CurrentDetections = new(); public CanvasEditor() { @@ -173,7 +173,7 @@ public class CanvasEditor : Canvas SelectionState = SelectionState.AnnResizing; _lastPos = e.GetPosition(this); _curRec = (Rectangle)sender; - _curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent; + _curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent; e.Handled = true; } @@ -233,7 +233,7 @@ public class CanvasEditor : Canvas private void AnnotationPositionStart(object sender, MouseEventArgs e) { _lastPos = e.GetPosition(this); - _curAnn = (AnnotationControl)sender; + _curAnn = (DetectionControl)sender; if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl)) ClearSelections(); @@ -310,9 +310,9 @@ public class CanvasEditor : Canvas }); } - public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel) + public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel) { - var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability) + var annotationControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability) { Width = canvasLabel.Width, Height = canvasLabel.Height @@ -321,40 +321,40 @@ public class CanvasEditor : Canvas SetLeft(annotationControl, canvasLabel.X ); SetTop(annotationControl, canvasLabel.Y); Children.Add(annotationControl); - CurrentAnns.Add(annotationControl); + CurrentDetections.Add(annotationControl); _newAnnotationRect.Fill = new SolidColorBrush(annClass.Color); return annotationControl; } #endregion - private void RemoveAnnotations(IEnumerable listToRemove) + private void RemoveAnnotations(IEnumerable listToRemove) { foreach (var ann in listToRemove) { Children.Remove(ann); - CurrentAnns.Remove(ann); + CurrentDetections.Remove(ann); } } public void RemoveAllAnns() { - foreach (var ann in CurrentAnns) + foreach (var ann in CurrentDetections) Children.Remove(ann); - CurrentAnns.Clear(); + CurrentDetections.Clear(); } - public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList()); + public void RemoveSelectedAnns() => RemoveAnnotations(CurrentDetections.Where(x => x.IsSelected).ToList()); private void ClearSelections() { - foreach (var ann in CurrentAnns) + foreach (var ann in CurrentDetections) ann.IsSelected = false; } public void ClearExpiredAnnotations(TimeSpan time) { - var expiredAnns = CurrentAnns.Where(x => + var expiredAnns = CurrentDetections.Where(x => x.Time.HasValue && Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds) .ToList(); diff --git a/Azaion.Common/Controls/AnnotationClasses.xaml b/Azaion.Common/Controls/DetectionClasses.xaml similarity index 96% rename from Azaion.Common/Controls/AnnotationClasses.xaml rename to Azaion.Common/Controls/DetectionClasses.xaml index 7ffb7d8..c823502 100644 --- a/Azaion.Common/Controls/AnnotationClasses.xaml +++ b/Azaion.Common/Controls/DetectionClasses.xaml @@ -1,4 +1,4 @@ - _resizeStart; private const double RESIZE_RECT_SIZE = 9; @@ -18,16 +18,16 @@ public class AnnotationControl : Border private readonly Label _probabilityLabel; public TimeSpan? Time { get; set; } - private AnnotationClass _annotationClass = null!; - public AnnotationClass AnnotationClass + private DetectionClass _detectionClass = null!; + public DetectionClass DetectionClass { - get => _annotationClass; + get => _detectionClass; set { _grid.Background = value.ColorBrush; _probabilityLabel.Background = value.ColorBrush; _classNameLabel.Text = value.Name; - _annotationClass = value; + _detectionClass = value; } } @@ -44,13 +44,13 @@ public class AnnotationControl : Border } } - public AnnotationControl(AnnotationClass annotationClass, TimeSpan? time, Action resizeStart, double? probability = null) + public DetectionControl(DetectionClass detectionClass, TimeSpan? time, Action resizeStart, double? probability = null) { Time = time; _resizeStart = resizeStart; _classNameLabel = new TextBlock { - Text = annotationClass.Name, + Text = detectionClass.Name, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 15, 0, 0), @@ -97,7 +97,7 @@ public class AnnotationControl : Border _grid.Children.Add(_probabilityLabel); Child = _grid; Cursor = Cursors.SizeAll; - AnnotationClass = annotationClass; + DetectionClass = detectionClass; } //small corners @@ -118,5 +118,9 @@ public class AnnotationControl : Border return rect; } - public CanvasLabel Info => new(AnnotationClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height); + public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null) + { + var label = new CanvasLabel(DetectionClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height); + return new YoloLabel(label, canvasSize, videoSize); + } } diff --git a/Azaion.Common/DTO/Annotation.cs b/Azaion.Common/DTO/Annotation.cs index fdb0f0c..9fbfd68 100644 --- a/Azaion.Common/DTO/Annotation.cs +++ b/Azaion.Common/DTO/Annotation.cs @@ -1,6 +1,8 @@ using System.IO; +using System.Windows.Media.Imaging; using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; +using Azaion.Common.Extensions; using Azaion.CommonSecurity.DTO; namespace Azaion.Common.DTO; @@ -9,26 +11,36 @@ public class Annotation { private static string _labelsDir = null!; private static string _imagesDir = null!; + private static string _thumbDir = null!; public static void InitializeDirs(DirectoriesConfig config) { _labelsDir = config.LabelsDirectory; _imagesDir = config.ImagesDirectory; + _thumbDir = config.ThumbnailsDirectory; } public string Name { get; set; } = null!; + public string ImageExtension { get; set; } = null!; public DateTime CreatedDate { get; set; } - public List Classes { get; set; } = null!; public string CreatedEmail { get; set; } = null!; public RoleEnum CreatedRole { get; set; } public SourceEnum Source { get; set; } public AnnotationStatus AnnotationStatus { get; set; } - public string ImagePath => Path.Combine(_imagesDir, $"{Name}.jpg"); - public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt"); + public IEnumerable Detections { get; set; } = null!; + public double Lat { get; set; } + public double Lon { get; set; } + + public List Classes => Detections.Select(x => x.ClassNumber).ToList(); + 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"); } + + public enum AnnotationStatus { None = 0, diff --git a/Azaion.Common/DTO/AnnotationCreatedEvent.cs b/Azaion.Common/DTO/AnnotationCreatedEvent.cs new file mode 100644 index 0000000..c4e755f --- /dev/null +++ b/Azaion.Common/DTO/AnnotationCreatedEvent.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Azaion.Common.DTO; + +public class AnnotationCreatedEvent(Annotation annotation) : INotification +{ + public Annotation Annotation { get; } = annotation; +} \ No newline at end of file diff --git a/Azaion.Common/DTO/AnnotationImageView.cs b/Azaion.Common/DTO/AnnotationImageView.cs new file mode 100644 index 0000000..181acf9 --- /dev/null +++ b/Azaion.Common/DTO/AnnotationImageView.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using System.IO; +using System.Runtime.CompilerServices; +using System.Windows.Media.Imaging; +using Azaion.Common.Extensions; + +namespace Azaion.Common.DTO; + +public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged +{ + public Annotation Annotation { get; set; } = annotation; + + private BitmapImage? _thumbnail; + public BitmapImage? Thumbnail + { + get + { + if (_thumbnail == null) + Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage()); + return _thumbnail; + } + private set => _thumbnail = value; + } + public string ImageName => Path.GetFileName(Annotation.ImagePath); + + 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) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/AnnotationConfig.cs b/Azaion.Common/DTO/Config/AnnotationConfig.cs index 71d0e8e..c46979a 100644 --- a/Azaion.Common/DTO/Config/AnnotationConfig.cs +++ b/Azaion.Common/DTO/Config/AnnotationConfig.cs @@ -4,12 +4,12 @@ namespace Azaion.Common.DTO.Config; public class AnnotationConfig { - public List AnnotationClasses { get; set; } = null!; + public List AnnotationClasses { get; set; } = null!; [JsonIgnore] - private Dictionary? _annotationClassesDict; + private Dictionary? _detectionClassesDict; [JsonIgnore] - public Dictionary AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id); + public Dictionary DetectionClassesDict => _detectionClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id); public int? LastSelectedExplorerClass { get; set; } diff --git a/Azaion.Common/DTO/AnnotationClass.cs b/Azaion.Common/DTO/DetectionClass.cs similarity index 93% rename from Azaion.Common/DTO/AnnotationClass.cs rename to Azaion.Common/DTO/DetectionClass.cs index 3411eab..be07669 100644 --- a/Azaion.Common/DTO/AnnotationClass.cs +++ b/Azaion.Common/DTO/DetectionClass.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Azaion.Common.DTO; -public class AnnotationClass +public class DetectionClass { public int Id { get; set; } diff --git a/Azaion.Common/DTO/ImageCreatedEvent.cs b/Azaion.Common/DTO/ImageCreatedEvent.cs deleted file mode 100644 index 4f4069e..0000000 --- a/Azaion.Common/DTO/ImageCreatedEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediatR; - -namespace Azaion.Common.DTO; - -public class ImageCreatedEvent(string imagePath) : INotification -{ - public string ImagePath { get; } = imagePath; -} \ No newline at end of file diff --git a/Azaion.Common/DTO/Label.cs b/Azaion.Common/DTO/Label.cs index e044674..cf27ebe 100644 --- a/Azaion.Common/DTO/Label.cs +++ b/Azaion.Common/DTO/Label.cs @@ -41,12 +41,14 @@ public class CanvasLabel : Label Probability = probability; } - public CanvasLabel(YoloLabel label, Size canvasSize, Size videoSize, double? probability = null) + public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double? probability = null) { var cw = canvasSize.Width; var ch = canvasSize.Height; var canvasAr = cw / ch; - var videoAr = videoSize.Width / videoSize.Height; + var videoAr = videoSize.HasValue + ? videoSize.Value.Width / videoSize.Value.Height + : canvasAr; ClassNumber = label.ClassNumber; @@ -102,12 +104,14 @@ public class YoloLabel : Label 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) + public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? videoSize = null) { var cw = canvasSize.Width; var ch = canvasSize.Height; var canvasAr = cw / ch; - var videoAr = videoSize.Width / videoSize.Height; + var videoAr = videoSize.HasValue + ? videoSize.Value.Width / videoSize.Value.Height + : canvasAr; ClassNumber = canvasLabel.ClassNumber; @@ -182,8 +186,15 @@ public class YoloLabel : Label public class Detection : YoloLabel { - public Detection(YoloLabel label, double? probability = null) + public string AnnotationName { get; set; } + public double? Probability { get; set; } + + //For db + public Detection(){} + + public Detection(string annotationName, YoloLabel label, double? probability = null) { + AnnotationName = annotationName; ClassNumber = label.ClassNumber; CenterX = label.CenterX; CenterY = label.CenterY; @@ -191,5 +202,4 @@ public class Detection : YoloLabel Width = label.Width; Probability = probability; } - public double? Probability { get; set; } } \ No newline at end of file diff --git a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs index 1b0f07c..57cf87f 100644 --- a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs +++ b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs @@ -6,14 +6,15 @@ using MessagePack; [MessagePackObject] public class AnnotationCreatedMessage { - [Key(0)] public DateTime CreatedDate { get; set; } - [Key(1)] public string Name { get; set; } = null!; - [Key(2)] public string Label { get; set; } = null!; - [Key(3)] public byte[] Image { get; set; } = null!; - [Key(4)] public RoleEnum CreatedRole { get; set; } - [Key(5)] public string CreatedEmail { get; set; } = null!; - [Key(6)] public SourceEnum Source { get; set; } - [Key(7)] public AnnotationStatus Status { get; set; } + [Key(0)] public DateTime CreatedDate { get; set; } + [Key(1)] public string Name { get; set; } = null!; + [Key(2)] public string ImageExtension { get; set; } = null!; + [Key(3)] public string Detections { get; set; } = null!; + [Key(4)] public byte[] Image { get; set; } = null!; + [Key(5)] public RoleEnum CreatedRole { get; set; } + [Key(6)] public string CreatedEmail { get; set; } = null!; + [Key(7)] public SourceEnum Source { get; set; } + [Key(8)] public AnnotationStatus Status { get; set; } } [MessagePackObject] diff --git a/Azaion.Common/Database/AnnotationsDb.cs b/Azaion.Common/Database/AnnotationsDb.cs index 0cb064e..92df84c 100644 --- a/Azaion.Common/Database/AnnotationsDb.cs +++ b/Azaion.Common/Database/AnnotationsDb.cs @@ -8,4 +8,5 @@ public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions { public ITable Annotations => this.GetTable(); public ITable AnnotationsQueue => this.GetTable(); + public ITable Detections => this.GetTable(); } \ No newline at end of file diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs index b0e861a..ce72dd4 100644 --- a/Azaion.Common/Database/DbFactory.cs +++ b/Azaion.Common/Database/DbFactory.cs @@ -1,8 +1,12 @@ -using System.Diagnostics; +using System.Data.SQLite; +using System.Diagnostics; +using System.IO; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using LinqToDB; +using LinqToDB.DataProvider.SQLite; using LinqToDB.Mapping; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Azaion.Common.Database; @@ -11,42 +15,72 @@ public interface IDbFactory { Task Run(Func> func); Task Run(Func func); + void SaveToDisk(); } public class DbFactory : IDbFactory { - private readonly DataOptions _dataOptions; + private readonly AnnotationConfig _annConfig; - public DbFactory(IOptions annConfig) + private string MemoryConnStr => "Data Source=:memory:"; + private readonly SQLiteConnection _memoryConnection; + private readonly DataOptions _memoryDataOptions; + + private string FileConnStr => $"Data Source={_annConfig.AnnotationsDbFile}"; + private readonly SQLiteConnection _fileConnection; + private readonly DataOptions _fileDataOptions; + + public DbFactory(IOptions annConfig, ILogger logger) { - _dataOptions = LoadOptions(annConfig.Value.AnnotationsDbFile); - } + _annConfig = annConfig.Value; - private DataOptions LoadOptions(string dbFile) - { - if (string.IsNullOrEmpty(dbFile)) - throw new ArgumentException($"Empty AnnotationsDbFile in config!"); - - var dataOptions = new DataOptions() - .UseSQLiteOfficial($"Data Source={dbFile}") + _memoryConnection = new SQLiteConnection(MemoryConnStr); + _memoryConnection.Open(); + _memoryDataOptions = new DataOptions() + .UseDataProvider(SQLiteTools.GetDataProvider()) + .UseConnection(_memoryConnection) .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); + _ = _memoryDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText)); - _ = dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText)); - return dataOptions; + + _fileConnection = new SQLiteConnection(FileConnStr); + _fileDataOptions = new DataOptions() + .UseDataProvider(SQLiteTools.GetDataProvider()) + .UseConnection(_fileConnection) + .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); + _ = _fileDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText)); + + if (!File.Exists(_annConfig.AnnotationsDbFile)) + CreateDb(); + _fileConnection.Open(); + _fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1); } + private void CreateDb() + { + SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile); + using var db = new AnnotationsDb(_fileDataOptions); + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + } public async Task Run(Func> func) { - await using var db = new AnnotationsDb(_dataOptions); + await using var db = new AnnotationsDb(_memoryDataOptions); return await func(db); } public async Task Run(Func func) { - await using var db = new AnnotationsDb(_dataOptions); + await using var db = new AnnotationsDb(_memoryDataOptions); await func(db); } + + public void SaveToDisk() + { + _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); + } } public static class AnnotationsDbSchemaHolder @@ -58,7 +92,16 @@ public static class AnnotationsDbSchemaHolder MappingSchema = new MappingSchema(); var builder = new FluentMappingBuilder(MappingSchema); - builder.Entity().HasTableName("annotations_queue"); + builder.Entity() + .HasTableName(Constants.ANNOTATIONS_TABLENAME) + .HasPrimaryKey(x => x.Name) + .Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName); + + builder.Entity() + .HasTableName(Constants.DETECTIONS_TABLENAME); + + builder.Entity() + .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME); builder.Build(); } diff --git a/Azaion.Annotator/Extensions/BitmapExtensions.cs b/Azaion.Common/Extensions/BitmapExtensions.cs similarity index 92% rename from Azaion.Annotator/Extensions/BitmapExtensions.cs rename to Azaion.Common/Extensions/BitmapExtensions.cs index 5216731..d749feb 100644 --- a/Azaion.Annotator/Extensions/BitmapExtensions.cs +++ b/Azaion.Common/Extensions/BitmapExtensions.cs @@ -1,7 +1,7 @@ using System.IO; using System.Windows.Media.Imaging; -namespace Azaion.Annotator.Extensions; +namespace Azaion.Common.Extensions; public static class BitmapExtensions { diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 47aefb5..cd3e298 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -8,8 +8,10 @@ using Azaion.Common.DTO.Queue; using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.Services; using LinqToDB; +using MediatR; using MessagePack; using Microsoft.Extensions.Options; +using Newtonsoft.Json; using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client.Reliable; @@ -20,17 +22,23 @@ public class AnnotationService private readonly AzaionApiClient _apiClient; private readonly IDbFactory _dbFactory; private readonly FailsafeAnnotationsProducer _producer; + private readonly IGalleryService _galleryService; + private readonly IMediator _mediator; private readonly QueueConfig _queueConfig; private Consumer _consumer = null!; public AnnotationService(AzaionApiClient apiClient, IDbFactory dbFactory, FailsafeAnnotationsProducer producer, - IOptions queueConfig) + IOptions queueConfig, + IGalleryService galleryService, + IMediator mediator) { _apiClient = apiClient; _dbFactory = dbFactory; _producer = producer; + _galleryService = galleryService; + _mediator = mediator; _queueConfig = queueConfig.Value; Task.Run(async () => await Init()).Wait(); @@ -53,8 +61,8 @@ public class AnnotationService } //AI / Manual - public async Task SaveAnnotation(string fName, List? labels, SourceEnum source, MemoryStream? stream = null, CancellationToken token = default) => - await SaveAnnotationInner(DateTime.UtcNow, fName, labels, 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, token); //Queue (only from operators) public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default) @@ -65,7 +73,8 @@ public class AnnotationService await SaveAnnotationInner( message.CreatedDate, message.Name, - YoloLabel.Deserialize(message.Label), + message.ImageExtension, + JsonConvert.DeserializeObject>(message.Detections) ?? [], message.Source, new MemoryStream(message.Image), message.CreatedRole, @@ -73,7 +82,7 @@ public class AnnotationService cancellationToken); } - private async Task SaveAnnotationInner(DateTime createdDate, string fName, List? labels, SourceEnum source, MemoryStream? stream, + private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List detections, SourceEnum source, Stream? stream, RoleEnum createdRole, string createdEmail, CancellationToken token = default) @@ -85,7 +94,7 @@ public class AnnotationService // sourceEnum: (manual) if was in received.json then else // sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json - var classes = labels?.Select(x => x.ClassNumber).Distinct().ToList() ?? []; + var classes = detections.Select(x => x.ClassNumber).Distinct().ToList() ?? []; AnnotationStatus status; var annotation = await _dbFactory.Run(async db => @@ -108,11 +117,12 @@ public class AnnotationService { CreatedDate = createdDate, Name = fName, - Classes = classes, + ImageExtension = imageExtension, CreatedEmail = createdEmail, CreatedRole = createdRole, AnnotationStatus = status, - Source = source + Source = source, + Detections = detections }; await db.InsertAsync(ann, token: token); } @@ -124,9 +134,10 @@ public class AnnotationService var img = System.Drawing.Image.FromStream(stream); img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue } - if (labels != null) - await YoloLabel.WriteToFile(labels, annotation.LabelPath, token); - + await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); + await _galleryService.CreateThumbnail(annotation, token); await _producer.SendToQueue(annotation, token); + + await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); } } \ No newline at end of file diff --git a/Azaion.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs index 32fcf11..54ddee8 100644 --- a/Azaion.Common/Services/FailsafeProducer.cs +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -8,6 +8,7 @@ using LinqToDB; using MessagePack; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json; using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client.Reliable; @@ -28,14 +29,13 @@ public class FailsafeAnnotationsProducer _logger = logger; _dbFactory = dbFactory; _queueConfig = queueConfig.Value; - Task.Run(async () => await ProcessQueue()).Wait(); + Task.Run(async () => await ProcessQueue()); } private async Task GetProducerQueueConfig() { return await StreamSystem.Create(new StreamSystemConfig { - Endpoints = new List { new IPEndPoint(IPAddress.Parse(_queueConfig.Host), _queueConfig.Port) }, UserName = _queueConfig.ProducerUsername, Password = _queueConfig.ProducerPassword @@ -45,7 +45,7 @@ public class FailsafeAnnotationsProducer private async Task Init(CancellationToken cancellationToken = default) { _annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE)); - //_annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE)); + _annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE)); } private async Task ProcessQueue(CancellationToken cancellationToken = default) @@ -98,7 +98,6 @@ public class FailsafeAnnotationsProducer foreach (var annotation in annotations) { var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken); - var label = await File.ReadAllTextAsync(annotation.LabelPath, cancellationToken); var annCreateMessage = new AnnotationCreatedMessage { Name = annotation.Name, @@ -108,7 +107,7 @@ public class FailsafeAnnotationsProducer CreatedDate = annotation.CreatedDate, Image = image, - Label = label, + Detections = JsonConvert.SerializeObject(annotation.Detections), Source = annotation.Source }; messages.Add(annCreateMessage); diff --git a/Azaion.Dataset/GalleryManager.cs b/Azaion.Common/Services/GalleryService.cs similarity index 55% rename from Azaion.Dataset/GalleryManager.cs rename to Azaion.Common/Services/GalleryService.cs index 1f5c960..cc97cc5 100644 --- a/Azaion.Dataset/GalleryManager.cs +++ b/Azaion.Common/Services/GalleryService.cs @@ -1,42 +1,43 @@ using System.Collections.Concurrent; using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using System.IO; using Azaion.Annotator.Extensions; -using Azaion.Common; +using Azaion.Common.Database; using Azaion.Common.DTO; -using Azaion.Common.Extensions; +using Azaion.Common.DTO.Config; +using Azaion.Common.DTO.Queue; +using Azaion.CommonSecurity.DTO; +using LinqToDB; +using LinqToDB.Data; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Color = System.Drawing.Color; using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions; using Size = System.Windows.Size; -using System.Drawing.Imaging; -using System.Drawing.Drawing2D; -using Azaion.Common.DTO.Config; -namespace Azaion.Dataset; +namespace Azaion.Common.Services; public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage); -public class GalleryManager( +public class GalleryService( IOptions directoriesConfig, IOptions thumbnailConfig, IOptions annotationConfig, - ILogger logger) : IGalleryManager + ILogger logger, + IDbFactory dbFactory) : IGalleryService { private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value; private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value; private readonly AnnotationConfig _annotationConfig = annotationConfig.Value; - private readonly string _thumbnailsCacheFile = Path.Combine(directoriesConfig.Value.ThumbnailsDirectory, Constants.THUMBNAILS_CACHE_FILE); public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate; private readonly SemaphoreSlim _updateLock = new(1); - public double ThumbnailsPercentage { get; set; } - public ConcurrentDictionary LabelsCache { get; set; } = new(); + public double ProcessedThumbnailsPercentage { get; set; } private DirectoryInfo? _thumbnailsDirectory; @@ -55,17 +56,26 @@ public class GalleryManager( } } - public void ClearThumbnails() + public async Task ClearThumbnails(CancellationToken cancellationToken = default) { foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles()) file.Delete(); + await dbFactory.Run(async db => + { + await db.Detections.DeleteAsync(x => true, token: cancellationToken); + await db.Annotations.DeleteAsync(x => true, token: cancellationToken); + }); } public async Task RefreshThumbnails() { await _updateLock.WaitAsync(); + var existingAnnotations = new ConcurrentDictionary(await dbFactory.Run(async db => + await db.Annotations.ToDictionaryAsync(x => x.Name))); + var missedAnnotations = new ConcurrentBag(); try { + var prefixLen = Constants.THUMBNAIL_PREFIX.Length; var thumbnails = ThumbnailsDirectory.GetFiles() @@ -74,68 +84,93 @@ public class GalleryManager( .Select(gr => gr.Key) .ToHashSet(); - if (File.Exists(_thumbnailsCacheFile)) - { - var cache = JsonConvert.DeserializeObject>( - await File.ReadAllTextAsync(_thumbnailsCacheFile), new DenseDateTimeConverter()); - LabelsCache = cache ?? new ConcurrentDictionary(); - } - else - LabelsCache = new ConcurrentDictionary(); - var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles(); var imagesCount = files.Length; await ParallelExt.ForEachAsync(files, async (file, cancellationToken) => { - var imgName = Path.GetFileNameWithoutExtension(file.Name); - if (thumbnails.Contains(imgName)) - return; + var fName = Path.GetFileNameWithoutExtension(file.Name); try { - await CreateThumbnail(file.FullName, cancellationToken); + var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt"); + if (!File.Exists(labelName)) + { + File.Delete(file.FullName); + logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!"); + return; + } + + //Read labels file only when it needed + if (existingAnnotations.ContainsKey(fName) && thumbnails.Contains(fName)) + return; + + var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList(); + var annotation = new Annotation + { + Name = fName, + ImageExtension = Path.GetExtension(file.Name), + Detections = detections, + CreatedDate = File.GetCreationTimeUtc(file.FullName), + Source = SourceEnum.Manual, + CreatedRole = RoleEnum.Validator, + CreatedEmail = Constants.ADMIN_EMAIL, + AnnotationStatus = AnnotationStatus.Validated + }; + + if (!existingAnnotations.ContainsKey(fName)) + missedAnnotations.Add(annotation); + + if (!thumbnails.Contains(fName)) + await CreateThumbnail(annotation, cancellationToken); + } catch (Exception e) { logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}"); } - }, new ParallelOptions - { - ProgressFn = async num => + }, + new ParallelOptions { - 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 - }); + ProgressFn = async num => + { + Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}"); + ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount); + ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage); + await Task.CompletedTask; + }, + CpuUtilPercent = 100, + ProgressUpdateInterval = 200 + }); } finally { - await SaveLabelsCache(); + var copyOptions = new BulkCopyOptions + { + MaxBatchSize = 50 + }; + await dbFactory.Run(async db => + { + var xx = missedAnnotations.GroupBy(x => x.Name) + .Where(gr => gr.Count() > 1) + .ToList(); + foreach (var gr in xx) + Console.WriteLine(gr.Key); + await db.BulkCopyAsync(copyOptions, missedAnnotations); + await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections)); + }); + dbFactory.SaveToDisk(); _updateLock.Release(); } } - public async Task SaveLabelsCache() - { - var labelsCacheStr = JsonConvert.SerializeObject(LabelsCache, new DenseDateTimeConverter()); - await File.WriteAllTextAsync(_thumbnailsCacheFile, labelsCacheStr); - } - - public async Task<(ThumbnailDto? thumbnailDto, List? classes)> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default) + public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default) { try { var width = (int)_thumbnailConfig.Size.Width; var height = (int)_thumbnailConfig.Size.Height; - var imgName = Path.GetFileName(imgPath); - var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt"); - - var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken))); + var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken))); var bitmap = new Bitmap(width, height); @@ -145,19 +180,6 @@ public class GalleryManager( 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, 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 = _thumbnailConfig.Border; @@ -167,7 +189,10 @@ public class GalleryManager( var frameHeight = size.Height; var frameWidth = size.Width; - if (labels.Any()) + var labels = annotation.Detections + .Select(x => new CanvasLabel(x, size)) + .ToList(); + if (annotation.Detections.Any()) { var labelsMinX = labels.Min(x => x.X); var labelsMaxX = labels.Max(x => x.X + x.Width); @@ -199,55 +224,28 @@ public class GalleryManager( foreach (var label in labels) { - var color = _annotationConfig.AnnotationClassesDict[label.ClassNumber].Color; + var color = _annotationConfig.DetectionClassesDict[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)}{Constants.THUMBNAIL_PREFIX}.jpg"); - bitmap.Save(thumbnailName, ImageFormat.Jpeg); - - var thumbnailDto = new ThumbnailDto - { - ThumbnailPath = thumbnailName, - ImagePath = imgPath, - LabelPath = labelName, - ImageDate = File.GetCreationTimeUtc(imgPath) - }; - return (thumbnailDto, classes); + bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg); } catch (Exception e) { logger.LogError(e, e.Message); - return (null, null); } } - - public LabelInfo AddToCache(string imgPath, List 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 +public interface IGalleryService { event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate; - double ThumbnailsPercentage { get; set; } - Task SaveLabelsCache(); - LabelInfo AddToCache(string imgPath, List classes); - ConcurrentDictionary LabelsCache { get; set; } - Task<(ThumbnailDto? thumbnailDto, List? classes)> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default); + double ProcessedThumbnailsPercentage { get; set; } + Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default); Task RefreshThumbnails(); - void ClearThumbnails(); + Task ClearThumbnails(CancellationToken cancellationToken = default); + } \ No newline at end of file diff --git a/Azaion.Dataset/Azaion.Dataset.csproj b/Azaion.Dataset/Azaion.Dataset.csproj index 3c9880d..f2fdf82 100644 --- a/Azaion.Dataset/Azaion.Dataset.csproj +++ b/Azaion.Dataset/Azaion.Dataset.csproj @@ -17,6 +17,7 @@ + diff --git a/Azaion.Dataset/DatasetExplorer.xaml b/Azaion.Dataset/DatasetExplorer.xaml index 87aeff0..9e3257e 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml +++ b/Azaion.Dataset/DatasetExplorer.xaml @@ -5,15 +5,14 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" xmlns:scottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF" - xmlns:datasetExplorer="clr-namespace:Azaion.Dataset" xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common" - xmlns:controls1="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common" + xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common" mc:Ignorable="d" Title="Переглядач анотацій" Height="900" Width="1200" WindowState="Maximized"> - + @@ -21,7 +20,7 @@ @@ -47,11 +46,11 @@ - - + @@ -109,22 +108,6 @@ - - Завантаження: - - - - - База іконок: diff --git a/Azaion.Dataset/DatasetExplorer.xaml.cs b/Azaion.Dataset/DatasetExplorer.xaml.cs index 1b6aa32..89cc93b 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml.cs +++ b/Azaion.Dataset/DatasetExplorer.xaml.cs @@ -4,8 +4,12 @@ using System.Windows; using System.Windows.Input; using System.Windows.Media; using Azaion.Common; +using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; +using Azaion.Common.Services; +using LinqToDB; +using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ScottPlot; @@ -13,33 +17,40 @@ using Color = ScottPlot.Color; namespace Azaion.Dataset; -public partial class DatasetExplorer +public partial class DatasetExplorer : INotificationHandler { private readonly ILogger _logger; private readonly AnnotationConfig _annotationConfig; private readonly DirectoriesConfig _directoriesConfig; - public ObservableCollection ThumbnailsDtos { get; set; } = new(); - private ObservableCollection AllAnnotationClasses { get; set; } = new(); + private Dictionary> _annotationsDict; + + public ObservableCollection SelectedAnnotations { get; set; } = new(); + private ObservableCollection AllAnnotationClasses { get; set; } = new(); + + public Dictionary LabelsCache { get; set; } = new(); private int _tempSelectedClassIdx = 0; - private readonly IGalleryManager _galleryManager; + private readonly IGalleryService _galleryService; + private readonly IDbFactory _dbFactory; public bool ThumbnailLoading { get; set; } - public ThumbnailDto? CurrentThumbnail { get; set; } + public AnnotationImageView? CurrentAnnotation { get; set; } public DatasetExplorer( IOptions directoriesConfig, IOptions annotationConfig, ILogger logger, - IGalleryManager galleryManager, - FormState formState) + IGalleryService galleryService, + FormState formState, + IDbFactory dbFactory) { _directoriesConfig = directoriesConfig.Value; _annotationConfig = annotationConfig.Value; _logger = logger; - _galleryManager = galleryManager; + _galleryService = galleryService; + _dbFactory = dbFactory; InitializeComponent(); Loaded += OnLoaded; @@ -61,11 +72,11 @@ public partial class DatasetExplorer ThumbnailsView.SelectionChanged += (_, _) => { - StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {ThumbnailsDtos.Count}"; + StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}"; }; - ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentThumbnail!.ImagePath); - galleryManager.ThumbnailsUpdate += thumbnailsPercentage => + ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentAnnotation!.Annotation.ImagePath); + galleryService.ThumbnailsUpdate += thumbnailsPercentage => { Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage); }; @@ -74,24 +85,22 @@ public partial class DatasetExplorer private async void OnLoaded(object sender, RoutedEventArgs e) { - _ = Task.Run(async () => await _galleryManager.RefreshThumbnails()); - - AllAnnotationClasses = new ObservableCollection( - new List { new() {Id = -1, Name = "All", ShortName = "All"}} + AllAnnotationClasses = new ObservableCollection( + new List { new() {Id = -1, Name = "All", ShortName = "All"}} .Concat(_annotationConfig.AnnotationClasses)); LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.MouseUp += async (_, _) => { - var selectedClass = (AnnotationClass)LvClasses.SelectedItem; + var selectedClass = (DetectionClass)LvClasses.SelectedItem; ExplorerEditor.CurrentAnnClass = selectedClass; _annotationConfig.LastSelectedExplorerClass = selectedClass.Id; if (Switcher.SelectedIndex == 0) await ReloadThumbnails(); else - foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected)) - ann.AnnotationClass = selectedClass; + foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected)) + ann.DetectionClass = selectedClass; }; LvClasses.SelectionChanged += (_, _) => @@ -99,35 +108,54 @@ public partial class DatasetExplorer if (Switcher.SelectedIndex != 1) return; - var selectedClass = (AnnotationClass)LvClasses.SelectedItem; + var selectedClass = (DetectionClass)LvClasses.SelectedItem; if (selectedClass == null) return; ExplorerEditor.CurrentAnnClass = selectedClass; - foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected)) - ann.AnnotationClass = selectedClass; + foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected)) + ann.DetectionClass = selectedClass; }; LvClasses.SelectedIndex = _annotationConfig.LastSelectedExplorerClass ?? 0; - ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem; - await ReloadThumbnails(); - LoadClassDistribution(); + ExplorerEditor.CurrentAnnClass = (DetectionClass)LvClasses.SelectedItem; - RefreshThumbBar.Value = _galleryManager.ThumbnailsPercentage; + await _dbFactory.Run(async db => + { + var allAnnotations = await db.Annotations + .LoadWith(x => x.Detections) + .OrderByDescending(x => x.CreatedDate) + .ToListAsync(); + _annotationsDict = AllAnnotationClasses.ToDictionary(x => x.Id, _ => new List()); + + foreach (var annotation in allAnnotations) + AddAnnotationToDict(annotation); + }); + await ReloadThumbnails(); + await LoadClassDistribution(); + + RefreshThumbBar.Value = _galleryService.ProcessedThumbnailsPercentage; DataContext = this; } - private void LoadClassDistribution() + private void AddAnnotationToDict(Annotation annotation) { - var data = _galleryManager.LabelsCache + foreach (var c in annotation.Classes) + _annotationsDict[c].Add(annotation); + _annotationsDict[-1].Add(annotation); + } + + private async Task LoadClassDistribution() + { + var data = LabelsCache .SelectMany(x => x.Value.Classes) .GroupBy(x => x) .Select(x => new { x.Key, - _annotationConfig.AnnotationClassesDict[x.Key].Name, - _annotationConfig.AnnotationClassesDict[x.Key].Color, + _annotationConfig.DetectionClassesDict[x.Key].Name, + _annotationConfig.DetectionClassesDict[x.Key].Color, ClassCount = x.Count() }) .ToList(); @@ -167,8 +195,8 @@ public partial class DatasetExplorer "Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; - _galleryManager.ClearThumbnails(); - _galleryManager.RefreshThumbnails(); + _galleryService.ClearThumbnails(); + _galleryService.RefreshThumbnails(); } private async Task EditAnnotation() @@ -180,20 +208,20 @@ public partial class DatasetExplorer if (ThumbnailsView.SelectedItem == null) return; - var dto = (ThumbnailsView.SelectedItem as ThumbnailDto)!; - CurrentThumbnail = dto; + CurrentAnnotation = (ThumbnailsView.SelectedItem as AnnotationImageView)!; + var ann = CurrentAnnotation.Annotation; ExplorerEditor.Background = new ImageBrush { - ImageSource = await dto.ImagePath.OpenImage() + ImageSource = await ann.ImagePath.OpenImage() }; SwitchTab(toEditor: true); - var time = Constants.GetTime(dto.ImagePath); + var time = Constants.GetTime(ann.ImagePath); ExplorerEditor.RemoveAllAnns(); - foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) + foreach (var deetection in ann.Detections) { - var annClass = _annotationConfig.AnnotationClassesDict[ann.ClassNumber]; - var canvasLabel = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize); + var annClass = _annotationConfig.DetectionClassesDict[deetection.ClassNumber]; + var canvasLabel = new CanvasLabel(deetection, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize); ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel); } @@ -243,101 +271,32 @@ public partial class DatasetExplorer 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); + var dto = (ThumbnailsView.SelectedItems[0] as AnnotationImageView)!; + dto.Delete(); + SelectedAnnotations.Remove(dto); } - ThumbnailsView.SelectedIndex = Math.Min(ThumbnailsDtos.Count, tempSelected); + ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected); } private async Task ReloadThumbnails() { - LoadingAnnsCaption.Visibility = Visibility.Visible; - LoadingAnnsBar.Visibility = Visibility.Visible; + SelectedAnnotations.Clear(); + foreach (var ann in _annotationsDict[ExplorerEditor.CurrentAnnClass.Id]) + SelectedAnnotations.Add(new AnnotationImageView(ann)); + } - if (!Directory.Exists(_directoriesConfig.ThumbnailsDirectory)) + + private void AddThumbnail(Annotation annotation) + { + var selectedClass = ((DetectionClass?)LvClasses.SelectedItem)?.Id; + if (selectedClass == null) return; - var thumbnails = Directory.GetFiles(_directoriesConfig.ThumbnailsDirectory, "*.jpg"); - var thumbnailDtos = new List(); - 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; + AddAnnotationToDict(annotation); + if (annotation.Classes.Contains(selectedClass.Value)) + SelectedAnnotations.Add(new AnnotationImageView(annotation)); } - private async Task GetThumbnail(string thumbnail) - { - try - { - var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Constants.THUMBNAIL_PREFIX.Length]; - var imagePath = Path.Combine(_directoriesConfig.ImagesDirectory, name); - var labelPath = Path.Combine(_directoriesConfig.LabelsDirectory, $"{name}.txt"); - - foreach (var f in _annotationConfig.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 classes) - { - var selectedClass = ((AnnotationClass?)LvClasses.SelectedItem)?.Id; - - if (selectedClass != null && (selectedClass == -1 || classes.Any(x => x == selectedClass))) - ThumbnailsDtos.Insert(0, thumbnailDto); - } + public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) => + AddThumbnail(notification.Annotation); } \ No newline at end of file diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index 2b370cb..d837673 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -2,15 +2,16 @@ using System.Windows.Input; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; +using Azaion.Common.DTO.Queue; +using Azaion.Common.Services; using MediatR; using Microsoft.Extensions.Options; namespace Azaion.Dataset; -public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalleryManager galleryManager, IOptions directoriesConfig) - : - INotificationHandler, - INotificationHandler +public class DatasetExplorerEventHandler( + DatasetExplorer datasetExplorer, + AnnotationService annotationService) : INotificationHandler { private readonly Dictionary _keysControlEnumDict = new() { @@ -48,14 +49,13 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalle if (datasetExplorer.ThumbnailLoading) return; - var currentAnns = datasetExplorer.ExplorerEditor.CurrentAnns - .Select(x => new YoloLabel(x.Info, datasetExplorer.ExplorerEditor.RenderSize, datasetExplorer.ExplorerEditor.RenderSize)) - .ToList(); + var fName = Path.GetFileNameWithoutExtension(datasetExplorer.CurrentAnnotation!.Annotation.ImagePath); + var extension = Path.GetExtension(fName); - await YoloLabel.WriteToFile(currentAnns, Path.Combine(directoriesConfig.Value.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath)); - await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath); - await galleryManager.SaveLabelsCache(); - datasetExplorer.CurrentThumbnail.UpdateImage(); + var detections = datasetExplorer.ExplorerEditor.CurrentDetections + .Select(x => new Detection(fName, x.GetLabel(datasetExplorer.ExplorerEditor.RenderSize))) + .ToList(); + await annotationService.SaveAnnotation(fName, extension, detections, SourceEnum.Manual); datasetExplorer.SwitchTab(toEditor: false); break; case PlaybackControlEnum.RemoveSelectedAnns: @@ -69,12 +69,4 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalle break; } } - - public async Task Handle(ImageCreatedEvent imageCreatedEvent, CancellationToken cancellationToken) - { - var (thumbnailDto, detections) = await galleryManager.CreateThumbnail(imageCreatedEvent.ImagePath, cancellationToken); - if (thumbnailDto != null && detections != null) - datasetExplorer.AddThumbnail(thumbnailDto, detections); - await galleryManager.SaveLabelsCache(); - } } diff --git a/Azaion.Dataset/ThumbnailDto.cs b/Azaion.Dataset/ThumbnailDto.cs deleted file mode 100644 index 03dc387..0000000 --- a/Azaion.Dataset/ThumbnailDto.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.ComponentModel; -using System.IO; -using System.Runtime.CompilerServices; -using System.Windows.Media.Imaging; - -namespace Azaion.Dataset; - -public class ThumbnailDto : INotifyPropertyChanged -{ - public string ThumbnailPath { get; set; } = null!; - public string ImagePath { get; set; } = null!; - public string LabelPath { get; set; } = null!; - 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)); - } -} \ No newline at end of file diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index 92fb502..35136dd 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -130,7 +130,7 @@ public partial class App services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Azaion.Suite/Azaion.Suite.csproj b/Azaion.Suite/Azaion.Suite.csproj index 55dba53..2789476 100644 --- a/Azaion.Suite/Azaion.Suite.csproj +++ b/Azaion.Suite/Azaion.Suite.csproj @@ -7,6 +7,8 @@ enable true ..\logo.ico + Always + true diff --git a/Azaion.Suite/MainSuite.xaml.cs b/Azaion.Suite/MainSuite.xaml.cs index bfc5d2f..9552c29 100644 --- a/Azaion.Suite/MainSuite.xaml.cs +++ b/Azaion.Suite/MainSuite.xaml.cs @@ -3,10 +3,12 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; -using Azaion.Annotator.Extensions; +using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Extensions; +using Azaion.Common.Services; +using Azaion.Dataset; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using SharpVectors.Converters; @@ -19,13 +21,23 @@ public partial class MainSuite private readonly IConfigUpdater _configUpdater; private readonly IEnumerable _modules; private readonly IServiceProvider _sp; + private readonly IGalleryService _galleryService; + private readonly IDbFactory _dbFactory; private readonly Dictionary _openedWindows = new(); - public MainSuite(IOptions appConfig, IConfigUpdater configUpdater, IEnumerable modules, IServiceProvider sp ) + public MainSuite(IOptions appConfig, + IConfigUpdater configUpdater, + IEnumerable modules, + IServiceProvider sp, + IGalleryService galleryService, + IDbFactory dbFactory + ) { _configUpdater = configUpdater; _modules = modules; _sp = sp; + _galleryService = galleryService; + _dbFactory = dbFactory; _appConfig = appConfig.Value; InitializeComponent(); Loaded += OnLoaded; @@ -76,7 +88,8 @@ public partial class MainSuite ListView.Items.Add(lvItem); } - + _ = Task.Run(async () => await _galleryService.RefreshThumbnails()); + //by default show first ListView.SelectedIndex = 0; OpenWindow((ListView.Items[0] as ListViewItem)!); @@ -115,6 +128,7 @@ public partial class MainSuite private void OnFormClosed(object? sender, EventArgs e) { _configUpdater.Save(_appConfig); + _dbFactory.SaveToDisk(); foreach (var window in _openedWindows) window.Value.Close(); Application.Current.Shutdown();