add db WIP 2, 80%

refactor, renames
This commit is contained in:
Alex Bezdieniezhnykh
2024-12-24 06:07:13 +02:00
parent 5fa18aa514
commit 48c9ccbfda
32 changed files with 499 additions and 459 deletions
+2 -2
View File
@@ -176,11 +176,11 @@
</GridView> </GridView>
</ListView.View> </ListView.View>
</ListView> </ListView>
<controls1:AnnotationClasses <controls1:DetectionClasses
x:Name="LvClasses" x:Name="LvClasses"
Grid.Column="0" Grid.Column="0"
Grid.Row="4"> Grid.Row="4">
</controls1:AnnotationClasses> </controls1:DetectionClasses>
<GridSplitter <GridSplitter
Background="DarkGray" Background="DarkGray"
+43 -37
View File
@@ -1,6 +1,5 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
@@ -8,13 +7,14 @@ using System.Windows.Controls.Primitives;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common; using Azaion.Common;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
@@ -40,9 +40,10 @@ public partial class Annotator
private readonly ILogger<Annotator> _logger; private readonly ILogger<Annotator> _logger;
private readonly VLCFrameExtractor _vlcFrameExtractor; private readonly VLCFrameExtractor _vlcFrameExtractor;
private readonly IAIDetector _aiDetector; private readonly IAIDetector _aiDetector;
private readonly AnnotationService _annotationService;
private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly CancellationTokenSource _cancellationTokenSource = new();
private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new(); private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100); private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
@@ -52,7 +53,7 @@ public partial class Annotator
private ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new(); private ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new(); private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
public IntervalTree<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new(); public IntervalTree<TimeSpan, List<Detection>> Detections { get; set; } = new();
private AutodetectDialog _autoDetectDialog = new() { Topmost = true }; private AutodetectDialog _autoDetectDialog = new() { Topmost = true };
public Annotator( public Annotator(
@@ -64,7 +65,8 @@ public partial class Annotator
HelpWindow helpWindow, HelpWindow helpWindow,
ILogger<Annotator> logger, ILogger<Annotator> logger,
VLCFrameExtractor vlcFrameExtractor, VLCFrameExtractor vlcFrameExtractor,
IAIDetector aiDetector) IAIDetector aiDetector,
AnnotationService annotationService)
{ {
InitializeComponent(); InitializeComponent();
_appConfig = appConfig.Value; _appConfig = appConfig.Value;
@@ -77,6 +79,7 @@ public partial class Annotator
_logger = logger; _logger = logger;
_vlcFrameExtractor = vlcFrameExtractor; _vlcFrameExtractor = vlcFrameExtractor;
_aiDetector = aiDetector; _aiDetector = aiDetector;
_annotationService = annotationService;
Loaded += OnLoaded; Loaded += OnLoaded;
Closed += OnFormClosed; Closed += OnFormClosed;
@@ -98,7 +101,7 @@ public partial class Annotator
ReloadFiles(); ReloadFiles();
AnnotationClasses = new ObservableCollection<AnnotationClass>(_appConfig.AnnotationConfig.AnnotationClasses); AnnotationClasses = new ObservableCollection<DetectionClass>(_appConfig.AnnotationConfig.AnnotationClasses);
LvClasses.ItemsSource = AnnotationClasses; LvClasses.ItemsSource = AnnotationClasses;
LvClasses.SelectedIndex = 0; LvClasses.SelectedIndex = 0;
@@ -152,7 +155,7 @@ public partial class Annotator
LvClasses.SelectionChanged += (_, _) => LvClasses.SelectionChanged += (_, _) =>
{ {
var selectedClass = (AnnotationClass)LvClasses.SelectedItem; var selectedClass = (DetectionClass)LvClasses.SelectedItem;
Editor.CurrentAnnClass = selectedClass; Editor.CurrentAnnClass = selectedClass;
_mediator.Publish(new AnnClassSelectedEvent(selectedClass)); _mediator.Publish(new AnnClassSelectedEvent(selectedClass));
}; };
@@ -188,7 +191,7 @@ public partial class Annotator
OpenAnnotationResult((AnnotationResult)DgAnnotations.SelectedItem); OpenAnnotationResult((AnnotationResult)DgAnnotations.SelectedItem);
break; break;
case Key.Delete: 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) if (result != MessageBoxResult.OK)
return; return;
@@ -202,7 +205,7 @@ public partial class Annotator
File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt")); File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt"));
File.Delete(thumbnailPath); File.Delete(thumbnailPath);
_formState.AnnotationResults.Remove(annotationResult); _formState.AnnotationResults.Remove(annotationResult);
Annotations.Remove(Annotations.Query(annotationResult.Time)); Detections.Remove(Detections.Query(annotationResult.Time));
} }
break; break;
} }
@@ -251,7 +254,7 @@ public partial class Annotator
Editor.ClearExpiredAnnotations(time); 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); AddAnnotationsToCanvas(time, annotations);
} }
@@ -285,7 +288,7 @@ public partial class Annotator
private async Task ReloadAnnotations(CancellationToken ct = default) private async Task ReloadAnnotations(CancellationToken ct = default)
{ {
_formState.AnnotationResults.Clear(); _formState.AnnotationResults.Clear();
Annotations.Clear(); Detections.Clear();
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory); var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory);
@@ -294,22 +297,21 @@ public partial class Annotator
var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_??????.txt"); var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_??????.txt");
foreach (var file in labelFiles) foreach (var file in labelFiles)
{ await AddAnnotations(Path.GetFileNameWithoutExtension(file.Name), await YoloLabel.ReadFromFile(file.FullName, ct), ct);
var name = Path.GetFileNameWithoutExtension(file.Name);
var time = Constants.GetTime(name);
await AddAnnotations(time, await YoloLabel.ReadFromFile(file.FullName, ct), ct);
}
} }
public async Task AddAnnotations(TimeSpan? time, List<YoloLabel> annotations, CancellationToken ct = default) //Load from yolo label file
=> await AddAnnotations(time, annotations.Select(x => new Detection(x)).ToList(), ct); public async Task AddAnnotations(string name, List<YoloLabel> annotations, CancellationToken ct = default)
=> await AddAnnotations(name, annotations.Select(x => new Detection(name, x)).ToList(), ct);
public async Task AddAnnotations(TimeSpan? time, List<Detection> detections, CancellationToken ct = default) //Add manually
public async Task AddAnnotations(string name, List<Detection> detections, CancellationToken ct = default)
{ {
var time = Constants.GetTime(name);
var timeValue = time ?? TimeSpan.FromMinutes(0); var timeValue = time ?? TimeSpan.FromMinutes(0);
var previousAnnotations = Annotations.Query(timeValue); var previousAnnotations = Detections.Query(timeValue);
Annotations.Remove(previousAnnotations); Detections.Remove(previousAnnotations);
Annotations.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections.Cast<YoloLabel>().ToList()); Detections.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections);
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time); var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
if (existingResult != null) if (existingResult != null)
@@ -345,15 +347,15 @@ public partial class Annotator
return (-1).ToColor(); return (-1).ToColor();
return colorNumber >= detectionClasses.Count return colorNumber >= detectionClasses.Count
? _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses.LastOrDefault()].Color ? _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.LastOrDefault()].Color
: _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses[colorNumber]].Color; : _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses[colorNumber]].Color;
} }
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList(); var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
annotationResult.ClassName = detectionClasses.Count > 1 annotationResult.ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.AnnotationClassesDict[x].ShortName)) ? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.DetectionClassesDict[x].ShortName))
: _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses.FirstOrDefault()].Name; : _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.FirstOrDefault()].Name;
annotationResult.ClassColor0 = GetAnnotationClass(detectionClasses, 0); annotationResult.ClassColor0 = GetAnnotationClass(detectionClasses, 0);
annotationResult.ClassColor1 = GetAnnotationClass(detectionClasses, 1); annotationResult.ClassColor1 = GetAnnotationClass(detectionClasses, 1);
@@ -442,8 +444,8 @@ public partial class Annotator
// private void AddClassBtnClick(object sender, RoutedEventArgs e) // private void AddClassBtnClick(object sender, RoutedEventArgs e)
// { // {
// LvClasses.IsReadOnly = false; // LvClasses.IsReadOnly = false;
// AnnotationClasses.Add(new AnnotationClass(AnnotationClasses.Count)); // DetectionClasses.Add(new DetectionClass(DetectionClasses.Count));
// LvClasses.SelectedIndex = AnnotationClasses.Count - 1; // LvClasses.SelectedIndex = DetectionClasses.Count - 1;
// } // }
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder(); private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
private async void OpenFolderButtonClick(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 try
{ {
var fName = Path.GetFileNameWithoutExtension(mediaInfo.Path);
var stream = new FileStream(mediaInfo.Path, FileMode.Open); var stream = new FileStream(mediaInfo.Path, FileMode.Open);
var detections = await _aiDetector.Detect(stream, token); var detections = await _aiDetector.Detect(fName, stream, token);
await ProcessDetection((TimeSpan.FromMilliseconds(0), stream), detections, token); await ProcessDetection((TimeSpan.FromMilliseconds(0), stream), Path.GetExtension(mediaInfo.Path), detections, token);
if (detections.Count != 0) if (detections.Count != 0)
mediaInfo.HasAnnotations = true; mediaInfo.HasAnnotations = true;
} }
@@ -599,7 +602,8 @@ public partial class Annotator
Console.WriteLine($"Detect time: {timeframe.Time}"); Console.WriteLine($"Detect time: {timeframe.Time}");
try 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); var isValid = IsValidDetection(timeframe.Time, detections);
if (timeframe.Time.TotalSeconds > prevSeekTime + 1) if (timeframe.Time.TotalSeconds > prevSeekTime + 1)
@@ -628,7 +632,7 @@ public partial class Annotator
continue; continue;
mediaInfo.HasAnnotations = true; mediaInfo.HasAnnotations = true;
await ProcessDetection(timeframe, detections, token); await ProcessDetection(timeframe, "jpg", detections, token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -685,7 +689,7 @@ public partial class Annotator
return false; return false;
} }
private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, List<Detection> detections, CancellationToken token = default) private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, string imageExtension, List<Detection> detections, CancellationToken token = default)
{ {
_previousDetection = (timeframe.Time, detections); _previousDetection = (timeframe.Time, detections);
await Dispatcher.Invoke(async () => await Dispatcher.Invoke(async () =>
@@ -695,22 +699,24 @@ public partial class Annotator
var time = timeframe.Time; var time = timeframe.Time;
var fName = _formState.GetTimeName(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.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
AddAnnotationsToCanvas(time, detections, true); 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 => 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}), " + $"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"size=({det.Width:F2}, {det.Height:F2}), " + $"size=({det.Width:F2}, {det.Height:F2}), " +
$"prob: {det.Probability:F1}%")); $"prob: {det.Probability:F1}%"));
Dispatcher.Invoke(() => _autoDetectDialog.Log(log)); Dispatcher.Invoke(() => _autoDetectDialog.Log(log));
await _mediator.Publish(new ImageCreatedEvent(imgPath), token);
} }
catch (Exception e) catch (Exception e)
{ {
+13 -15
View File
@@ -21,7 +21,6 @@ public class AnnotatorEventHandler(
Annotator mainWindow, Annotator mainWindow,
FormState formState, FormState formState,
AnnotationService annotationService, AnnotationService annotationService,
IMediator mediator,
ILogger<AnnotatorEventHandler> logger, ILogger<AnnotatorEventHandler> logger,
IOptions<DirectoriesConfig> dirConfig) IOptions<DirectoriesConfig> dirConfig)
: :
@@ -48,15 +47,15 @@ public class AnnotatorEventHandler(
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
{ {
SelectClass(notification.AnnotationClass); SelectClass(notification.DetectionClass);
await Task.CompletedTask; await Task.CompletedTask;
} }
private void SelectClass(AnnotationClass annClass) private void SelectClass(DetectionClass annClass)
{ {
mainWindow.Editor.CurrentAnnClass = annClass; mainWindow.Editor.CurrentAnnClass = annClass;
foreach (var ann in mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected)) foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected))
ann.AnnotationClass = annClass; ann.DetectionClass = annClass;
mainWindow.LvClasses.SelectedIndex = annClass.Id; mainWindow.LvClasses.SelectedIndex = annClass.Id;
} }
@@ -73,7 +72,7 @@ public class AnnotatorEventHandler(
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1; keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue) if (keyNumber.HasValue)
SelectClass((AnnotationClass)mainWindow.LvClasses.Items[keyNumber.Value]!); SelectClass((DetectionClass)mainWindow.LvClasses.Items[keyNumber.Value]!);
if (_keysControlEnumDict.TryGetValue(key, out var value)) if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value, cancellationToken); await ControlPlayback(value, cancellationToken);
@@ -144,7 +143,7 @@ public class AnnotatorEventHandler(
mainWindow.SeekTo(mediaPlayer.Time + step); mainWindow.SeekTo(mediaPlayer.Time + step);
break; break;
case PlaybackControlEnum.SaveAnnotations: case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations(); await SaveAnnotations(cancellationToken);
break; break;
case PlaybackControlEnum.RemoveSelectedAnns: case PlaybackControlEnum.RemoveSelectedAnns:
@@ -229,18 +228,19 @@ public class AnnotatorEventHandler(
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time); var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var fName = formState.GetTimeName(time); var fName = formState.GetTimeName(time);
var currentAnns = mainWindow.Editor.CurrentAnns var currentDetections = mainWindow.Editor.CurrentDetections
.Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)) .Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
.ToList(); .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.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns(); mainWindow.Editor.RemoveAllAnns();
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video; 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) if (isVideo)
{ {
@@ -267,8 +267,6 @@ public class AnnotatorEventHandler(
File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true); File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true);
NextMedia(); NextMedia();
} }
await annotationService.SaveAnnotation(fName, currentAnns, SourceEnum.Manual, token: cancellationToken); await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken);
await mediator.Publish(new ImageCreatedEvent(imgPath), cancellationToken);
} }
} }
@@ -3,7 +3,7 @@ using MediatR;
namespace Azaion.Annotator.DTO; 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;
} }
+3 -3
View File
@@ -15,7 +15,7 @@ namespace Azaion.Annotator;
public interface IAIDetector public interface IAIDetector
{ {
Task<List<Detection>> Detect(Stream imageStream, CancellationToken cancellationToken = default); Task<List<Detection>> Detect(string fName, Stream imageStream, CancellationToken cancellationToken = default);
} }
public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IResourceLoader resourceLoader) : IAIDetector, IDisposable public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IResourceLoader resourceLoader) : IAIDetector, IDisposable
@@ -25,7 +25,7 @@ public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IReso
private const string YOLO_MODEL = "azaion.onnx"; private const string YOLO_MODEL = "azaion.onnx";
public async Task<List<Detection>> Detect(Stream imageStream, CancellationToken cancellationToken) public async Task<List<Detection>> Detect(string fName, Stream imageStream, CancellationToken cancellationToken)
{ {
if (_predictor == null) if (_predictor == null)
{ {
@@ -42,7 +42,7 @@ public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IReso
var detections = result.Select(d => 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); 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(); }).ToList();
return FilterOverlapping(detections); return FilterOverlapping(detections);
+9 -2
View File
@@ -19,7 +19,7 @@ public class Constants
#region AnnotatorConfig #region AnnotatorConfig
public static readonly List<AnnotationClass> DefaultAnnotationClasses = public static readonly List<DetectionClass> DefaultAnnotationClasses =
[ [
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" }, new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" },
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" }, new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" },
@@ -62,7 +62,6 @@ public class Constants
public const int DEFAULT_THUMBNAIL_BORDER = 10; public const int DEFAULT_THUMBNAIL_BORDER = 10;
public const string THUMBNAIL_PREFIX = "_thumb"; public const string THUMBNAIL_PREFIX = "_thumb";
public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache";
#endregion #endregion
@@ -92,6 +91,14 @@ public class Constants
public const string ANNOTATION_PRODUCER = "AnnotationsProducer"; public const string ANNOTATION_PRODUCER = "AnnotationsProducer";
public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer"; 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 #endregion
+16 -16
View File
@@ -24,7 +24,7 @@ public class CanvasEditor : Canvas
private readonly TextBlock _classNameHint; private readonly TextBlock _classNameHint;
private Rectangle _curRec = new(); private Rectangle _curRec = new();
private AnnotationControl _curAnn = null!; private DetectionControl _curAnn = null!;
private const int MIN_SIZE = 20; private const int MIN_SIZE = 20;
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400); private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
@@ -44,8 +44,8 @@ public class CanvasEditor : Canvas
set => SetValue(GetTimeFuncProp, value); set => SetValue(GetTimeFuncProp, value);
} }
private AnnotationClass _currentAnnClass = null!; private DetectionClass _currentAnnClass = null!;
public AnnotationClass CurrentAnnClass public DetectionClass CurrentAnnClass
{ {
get => _currentAnnClass; get => _currentAnnClass;
set set
@@ -62,7 +62,7 @@ public class CanvasEditor : Canvas
} }
} }
public readonly List<AnnotationControl> CurrentAnns = new(); public readonly List<DetectionControl> CurrentDetections = new();
public CanvasEditor() public CanvasEditor()
{ {
@@ -173,7 +173,7 @@ public class CanvasEditor : Canvas
SelectionState = SelectionState.AnnResizing; SelectionState = SelectionState.AnnResizing;
_lastPos = e.GetPosition(this); _lastPos = e.GetPosition(this);
_curRec = (Rectangle)sender; _curRec = (Rectangle)sender;
_curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent; _curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent;
e.Handled = true; e.Handled = true;
} }
@@ -233,7 +233,7 @@ public class CanvasEditor : Canvas
private void AnnotationPositionStart(object sender, MouseEventArgs e) private void AnnotationPositionStart(object sender, MouseEventArgs e)
{ {
_lastPos = e.GetPosition(this); _lastPos = e.GetPosition(this);
_curAnn = (AnnotationControl)sender; _curAnn = (DetectionControl)sender;
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl)) if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
ClearSelections(); 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, Width = canvasLabel.Width,
Height = canvasLabel.Height Height = canvasLabel.Height
@@ -321,40 +321,40 @@ public class CanvasEditor : Canvas
SetLeft(annotationControl, canvasLabel.X ); SetLeft(annotationControl, canvasLabel.X );
SetTop(annotationControl, canvasLabel.Y); SetTop(annotationControl, canvasLabel.Y);
Children.Add(annotationControl); Children.Add(annotationControl);
CurrentAnns.Add(annotationControl); CurrentDetections.Add(annotationControl);
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color); _newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
return annotationControl; return annotationControl;
} }
#endregion #endregion
private void RemoveAnnotations(IEnumerable<AnnotationControl> listToRemove) private void RemoveAnnotations(IEnumerable<DetectionControl> listToRemove)
{ {
foreach (var ann in listToRemove) foreach (var ann in listToRemove)
{ {
Children.Remove(ann); Children.Remove(ann);
CurrentAnns.Remove(ann); CurrentDetections.Remove(ann);
} }
} }
public void RemoveAllAnns() public void RemoveAllAnns()
{ {
foreach (var ann in CurrentAnns) foreach (var ann in CurrentDetections)
Children.Remove(ann); 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() private void ClearSelections()
{ {
foreach (var ann in CurrentAnns) foreach (var ann in CurrentDetections)
ann.IsSelected = false; ann.IsSelected = false;
} }
public void ClearExpiredAnnotations(TimeSpan time) public void ClearExpiredAnnotations(TimeSpan time)
{ {
var expiredAnns = CurrentAnns.Where(x => var expiredAnns = CurrentDetections.Where(x =>
x.Time.HasValue && x.Time.HasValue &&
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds) Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
.ToList(); .ToList();
@@ -1,4 +1,4 @@
<DataGrid x:Class="Azaion.Common.Controls.AnnotationClasses" <DataGrid x:Class="Azaion.Common.Controls.DetectionClasses"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -1,8 +1,8 @@
namespace Azaion.Common.Controls; namespace Azaion.Common.Controls;
public partial class AnnotationClasses public partial class DetectionClasses
{ {
public AnnotationClasses() public DetectionClasses()
{ {
InitializeComponent(); InitializeComponent();
} }
@@ -8,7 +8,7 @@ using Label = System.Windows.Controls.Label;
namespace Azaion.Common.Controls; namespace Azaion.Common.Controls;
public class AnnotationControl : Border public class DetectionControl : Border
{ {
private readonly Action<object, MouseButtonEventArgs> _resizeStart; private readonly Action<object, MouseButtonEventArgs> _resizeStart;
private const double RESIZE_RECT_SIZE = 9; private const double RESIZE_RECT_SIZE = 9;
@@ -18,16 +18,16 @@ public class AnnotationControl : Border
private readonly Label _probabilityLabel; private readonly Label _probabilityLabel;
public TimeSpan? Time { get; set; } public TimeSpan? Time { get; set; }
private AnnotationClass _annotationClass = null!; private DetectionClass _detectionClass = null!;
public AnnotationClass AnnotationClass public DetectionClass DetectionClass
{ {
get => _annotationClass; get => _detectionClass;
set set
{ {
_grid.Background = value.ColorBrush; _grid.Background = value.ColorBrush;
_probabilityLabel.Background = value.ColorBrush; _probabilityLabel.Background = value.ColorBrush;
_classNameLabel.Text = value.Name; _classNameLabel.Text = value.Name;
_annotationClass = value; _detectionClass = value;
} }
} }
@@ -44,13 +44,13 @@ public class AnnotationControl : Border
} }
} }
public AnnotationControl(AnnotationClass annotationClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null) public DetectionControl(DetectionClass detectionClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null)
{ {
Time = time; Time = time;
_resizeStart = resizeStart; _resizeStart = resizeStart;
_classNameLabel = new TextBlock _classNameLabel = new TextBlock
{ {
Text = annotationClass.Name, Text = detectionClass.Name,
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top, VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 15, 0, 0), Margin = new Thickness(0, 15, 0, 0),
@@ -97,7 +97,7 @@ public class AnnotationControl : Border
_grid.Children.Add(_probabilityLabel); _grid.Children.Add(_probabilityLabel);
Child = _grid; Child = _grid;
Cursor = Cursors.SizeAll; Cursor = Cursors.SizeAll;
AnnotationClass = annotationClass; DetectionClass = detectionClass;
} }
//small corners //small corners
@@ -118,5 +118,9 @@ public class AnnotationControl : Border
return rect; 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);
}
} }
+15 -3
View File
@@ -1,6 +1,8 @@
using System.IO; using System.IO;
using System.Windows.Media.Imaging;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
@@ -9,26 +11,36 @@ public class Annotation
{ {
private static string _labelsDir = null!; private static string _labelsDir = null!;
private static string _imagesDir = null!; private static string _imagesDir = null!;
private static string _thumbDir = null!;
public static void InitializeDirs(DirectoriesConfig config) public static void InitializeDirs(DirectoriesConfig config)
{ {
_labelsDir = config.LabelsDirectory; _labelsDir = config.LabelsDirectory;
_imagesDir = config.ImagesDirectory; _imagesDir = config.ImagesDirectory;
_thumbDir = config.ThumbnailsDirectory;
} }
public string Name { get; set; } = null!; public string Name { get; set; } = null!;
public string ImageExtension { get; set; } = null!;
public DateTime CreatedDate { get; set; } public DateTime CreatedDate { get; set; }
public List<int> Classes { get; set; } = null!;
public string CreatedEmail { get; set; } = null!; public string CreatedEmail { get; set; } = null!;
public RoleEnum CreatedRole { get; set; } public RoleEnum CreatedRole { get; set; }
public SourceEnum Source { get; set; } public SourceEnum Source { get; set; }
public AnnotationStatus AnnotationStatus { get; set; } public AnnotationStatus AnnotationStatus { get; set; }
public string ImagePath => Path.Combine(_imagesDir, $"{Name}.jpg"); public IEnumerable<Detection> Detections { get; set; } = null!;
public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
public double Lat { get; set; }
public double Lon { get; set; }
public List<int> 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 public enum AnnotationStatus
{ {
None = 0, None = 0,
@@ -0,0 +1,8 @@
using MediatR;
namespace Azaion.Common.DTO;
public class AnnotationCreatedEvent(Annotation annotation) : INotification
{
public Annotation Annotation { get; } = annotation;
}
+38
View File
@@ -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));
}
}
+3 -3
View File
@@ -4,12 +4,12 @@ namespace Azaion.Common.DTO.Config;
public class AnnotationConfig public class AnnotationConfig
{ {
public List<AnnotationClass> AnnotationClasses { get; set; } = null!; public List<DetectionClass> AnnotationClasses { get; set; } = null!;
[JsonIgnore] [JsonIgnore]
private Dictionary<int, AnnotationClass>? _annotationClassesDict; private Dictionary<int, DetectionClass>? _detectionClassesDict;
[JsonIgnore] [JsonIgnore]
public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id); public Dictionary<int, DetectionClass> DetectionClassesDict => _detectionClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
public int? LastSelectedExplorerClass { get; set; } public int? LastSelectedExplorerClass { get; set; }
@@ -4,7 +4,7 @@ using Newtonsoft.Json;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationClass public class DetectionClass
{ {
public int Id { get; set; } public int Id { get; set; }
-8
View File
@@ -1,8 +0,0 @@
using MediatR;
namespace Azaion.Common.DTO;
public class ImageCreatedEvent(string imagePath) : INotification
{
public string ImagePath { get; } = imagePath;
}
+16 -6
View File
@@ -41,12 +41,14 @@ public class CanvasLabel : Label
Probability = probability; 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 cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
var canvasAr = cw / ch; var canvasAr = cw / ch;
var videoAr = videoSize.Width / videoSize.Height; var videoAr = videoSize.HasValue
? videoSize.Value.Width / videoSize.Value.Height
: canvasAr;
ClassNumber = label.ClassNumber; ClassNumber = label.ClassNumber;
@@ -102,12 +104,14 @@ public class YoloLabel : Label
public RectangleF ToRectangle() => public RectangleF ToRectangle() =>
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height); 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 cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
var canvasAr = cw / ch; var canvasAr = cw / ch;
var videoAr = videoSize.Width / videoSize.Height; var videoAr = videoSize.HasValue
? videoSize.Value.Width / videoSize.Value.Height
: canvasAr;
ClassNumber = canvasLabel.ClassNumber; ClassNumber = canvasLabel.ClassNumber;
@@ -182,8 +186,15 @@ public class YoloLabel : Label
public class Detection : YoloLabel 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; ClassNumber = label.ClassNumber;
CenterX = label.CenterX; CenterX = label.CenterX;
CenterY = label.CenterY; CenterY = label.CenterY;
@@ -191,5 +202,4 @@ public class Detection : YoloLabel
Width = label.Width; Width = label.Width;
Probability = probability; Probability = probability;
} }
public double? Probability { get; set; }
} }
@@ -6,14 +6,15 @@ using MessagePack;
[MessagePackObject] [MessagePackObject]
public class AnnotationCreatedMessage public class AnnotationCreatedMessage
{ {
[Key(0)] public DateTime CreatedDate { get; set; } [Key(0)] public DateTime CreatedDate { get; set; }
[Key(1)] public string Name { get; set; } = null!; [Key(1)] public string Name { get; set; } = null!;
[Key(2)] public string Label { get; set; } = null!; [Key(2)] public string ImageExtension { get; set; } = null!;
[Key(3)] public byte[] Image { get; set; } = null!; [Key(3)] public string Detections { get; set; } = null!;
[Key(4)] public RoleEnum CreatedRole { get; set; } [Key(4)] public byte[] Image { get; set; } = null!;
[Key(5)] public string CreatedEmail { get; set; } = null!; [Key(5)] public RoleEnum CreatedRole { get; set; }
[Key(6)] public SourceEnum Source { get; set; } [Key(6)] public string CreatedEmail { get; set; } = null!;
[Key(7)] public AnnotationStatus Status { get; set; } [Key(7)] public SourceEnum Source { get; set; }
[Key(8)] public AnnotationStatus Status { get; set; }
} }
[MessagePackObject] [MessagePackObject]
+1
View File
@@ -8,4 +8,5 @@ public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions
{ {
public ITable<Annotation> Annotations => this.GetTable<Annotation>(); public ITable<Annotation> Annotations => this.GetTable<Annotation>();
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>(); public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>();
public ITable<Detection> Detections => this.GetTable<Detection>();
} }
+60 -17
View File
@@ -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;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using LinqToDB; using LinqToDB;
using LinqToDB.DataProvider.SQLite;
using LinqToDB.Mapping; using LinqToDB.Mapping;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Azaion.Common.Database; namespace Azaion.Common.Database;
@@ -11,42 +15,72 @@ public interface IDbFactory
{ {
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func); Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
Task Run(Func<AnnotationsDb, Task> func); Task Run(Func<AnnotationsDb, Task> func);
void SaveToDisk();
} }
public class DbFactory : IDbFactory public class DbFactory : IDbFactory
{ {
private readonly DataOptions _dataOptions; private readonly AnnotationConfig _annConfig;
public DbFactory(IOptions<AnnotationConfig> 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<AnnotationConfig> annConfig, ILogger<DbFactory> logger)
{ {
_dataOptions = LoadOptions(annConfig.Value.AnnotationsDbFile); _annConfig = annConfig.Value;
}
private DataOptions LoadOptions(string dbFile) _memoryConnection = new SQLiteConnection(MemoryConnStr);
{ _memoryConnection.Open();
if (string.IsNullOrEmpty(dbFile)) _memoryDataOptions = new DataOptions()
throw new ArgumentException($"Empty AnnotationsDbFile in config!"); .UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_memoryConnection)
var dataOptions = new DataOptions()
.UseSQLiteOfficial($"Data Source={dbFile}")
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); .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<Annotation>();
db.CreateTable<AnnotationName>();
db.CreateTable<Detection>();
}
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func) public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
{ {
await using var db = new AnnotationsDb(_dataOptions); await using var db = new AnnotationsDb(_memoryDataOptions);
return await func(db); return await func(db);
} }
public async Task Run(Func<AnnotationsDb, Task> func) public async Task Run(Func<AnnotationsDb, Task> func)
{ {
await using var db = new AnnotationsDb(_dataOptions); await using var db = new AnnotationsDb(_memoryDataOptions);
await func(db); await func(db);
} }
public void SaveToDisk()
{
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
}
} }
public static class AnnotationsDbSchemaHolder public static class AnnotationsDbSchemaHolder
@@ -58,7 +92,16 @@ public static class AnnotationsDbSchemaHolder
MappingSchema = new MappingSchema(); MappingSchema = new MappingSchema();
var builder = new FluentMappingBuilder(MappingSchema); var builder = new FluentMappingBuilder(MappingSchema);
builder.Entity<AnnotationName>().HasTableName("annotations_queue"); builder.Entity<Annotation>()
.HasTableName(Constants.ANNOTATIONS_TABLENAME)
.HasPrimaryKey(x => x.Name)
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName);
builder.Entity<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME);
builder.Entity<AnnotationName>()
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME);
builder.Build(); builder.Build();
} }
@@ -1,7 +1,7 @@
using System.IO; using System.IO;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
namespace Azaion.Annotator.Extensions; namespace Azaion.Common.Extensions;
public static class BitmapExtensions public static class BitmapExtensions
{ {
+22 -11
View File
@@ -8,8 +8,10 @@ using Azaion.Common.DTO.Queue;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services; using Azaion.CommonSecurity.Services;
using LinqToDB; using LinqToDB;
using MediatR;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client;
using RabbitMQ.Stream.Client.Reliable; using RabbitMQ.Stream.Client.Reliable;
@@ -20,17 +22,23 @@ public class AnnotationService
private readonly AzaionApiClient _apiClient; private readonly AzaionApiClient _apiClient;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly FailsafeAnnotationsProducer _producer; private readonly FailsafeAnnotationsProducer _producer;
private readonly IGalleryService _galleryService;
private readonly IMediator _mediator;
private readonly QueueConfig _queueConfig; private readonly QueueConfig _queueConfig;
private Consumer _consumer = null!; private Consumer _consumer = null!;
public AnnotationService(AzaionApiClient apiClient, public AnnotationService(AzaionApiClient apiClient,
IDbFactory dbFactory, IDbFactory dbFactory,
FailsafeAnnotationsProducer producer, FailsafeAnnotationsProducer producer,
IOptions<QueueConfig> queueConfig) IOptions<QueueConfig> queueConfig,
IGalleryService galleryService,
IMediator mediator)
{ {
_apiClient = apiClient; _apiClient = apiClient;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_producer = producer; _producer = producer;
_galleryService = galleryService;
_mediator = mediator;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
Task.Run(async () => await Init()).Wait(); Task.Run(async () => await Init()).Wait();
@@ -53,8 +61,8 @@ public class AnnotationService
} }
//AI / Manual //AI / Manual
public async Task SaveAnnotation(string fName, List<YoloLabel>? labels, SourceEnum source, MemoryStream? stream = null, CancellationToken token = default) => public async Task SaveAnnotation(string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, fName, labels, source, stream, _apiClient.User.Role, _apiClient.User.Email, token); await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, token);
//Queue (only from operators) //Queue (only from operators)
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default) public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
@@ -65,7 +73,8 @@ public class AnnotationService
await SaveAnnotationInner( await SaveAnnotationInner(
message.CreatedDate, message.CreatedDate,
message.Name, message.Name,
YoloLabel.Deserialize(message.Label), message.ImageExtension,
JsonConvert.DeserializeObject<List<Detection>>(message.Detections) ?? [],
message.Source, message.Source,
new MemoryStream(message.Image), new MemoryStream(message.Image),
message.CreatedRole, message.CreatedRole,
@@ -73,7 +82,7 @@ public class AnnotationService
cancellationToken); cancellationToken);
} }
private async Task SaveAnnotationInner(DateTime createdDate, string fName, List<YoloLabel>? labels, SourceEnum source, MemoryStream? stream, private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum createdRole, RoleEnum createdRole,
string createdEmail, string createdEmail,
CancellationToken token = default) CancellationToken token = default)
@@ -85,7 +94,7 @@ public class AnnotationService
// sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage> // sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage>
// sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json // 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; AnnotationStatus status;
var annotation = await _dbFactory.Run(async db => var annotation = await _dbFactory.Run(async db =>
@@ -108,11 +117,12 @@ public class AnnotationService
{ {
CreatedDate = createdDate, CreatedDate = createdDate,
Name = fName, Name = fName,
Classes = classes, ImageExtension = imageExtension,
CreatedEmail = createdEmail, CreatedEmail = createdEmail,
CreatedRole = createdRole, CreatedRole = createdRole,
AnnotationStatus = status, AnnotationStatus = status,
Source = source Source = source,
Detections = detections
}; };
await db.InsertAsync(ann, token: token); await db.InsertAsync(ann, token: token);
} }
@@ -124,9 +134,10 @@ public class AnnotationService
var img = System.Drawing.Image.FromStream(stream); var img = System.Drawing.Image.FromStream(stream);
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
} }
if (labels != null) await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
await YoloLabel.WriteToFile(labels, annotation.LabelPath, token); await _galleryService.CreateThumbnail(annotation, token);
await _producer.SendToQueue(annotation, token); await _producer.SendToQueue(annotation, token);
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
} }
} }
+4 -5
View File
@@ -8,6 +8,7 @@ using LinqToDB;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client;
using RabbitMQ.Stream.Client.Reliable; using RabbitMQ.Stream.Client.Reliable;
@@ -28,14 +29,13 @@ public class FailsafeAnnotationsProducer
_logger = logger; _logger = logger;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
Task.Run(async () => await ProcessQueue()).Wait(); Task.Run(async () => await ProcessQueue());
} }
private async Task<StreamSystem> GetProducerQueueConfig() private async Task<StreamSystem> GetProducerQueueConfig()
{ {
return await StreamSystem.Create(new StreamSystemConfig return await StreamSystem.Create(new StreamSystemConfig
{ {
Endpoints = new List<EndPoint> { new IPEndPoint(IPAddress.Parse(_queueConfig.Host), _queueConfig.Port) }, Endpoints = new List<EndPoint> { new IPEndPoint(IPAddress.Parse(_queueConfig.Host), _queueConfig.Port) },
UserName = _queueConfig.ProducerUsername, UserName = _queueConfig.ProducerUsername,
Password = _queueConfig.ProducerPassword Password = _queueConfig.ProducerPassword
@@ -45,7 +45,7 @@ public class FailsafeAnnotationsProducer
private async Task Init(CancellationToken cancellationToken = default) private async Task Init(CancellationToken cancellationToken = default)
{ {
_annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE)); _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) private async Task ProcessQueue(CancellationToken cancellationToken = default)
@@ -98,7 +98,6 @@ public class FailsafeAnnotationsProducer
foreach (var annotation in annotations) foreach (var annotation in annotations)
{ {
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken); var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
var label = await File.ReadAllTextAsync(annotation.LabelPath, cancellationToken);
var annCreateMessage = new AnnotationCreatedMessage var annCreateMessage = new AnnotationCreatedMessage
{ {
Name = annotation.Name, Name = annotation.Name,
@@ -108,7 +107,7 @@ public class FailsafeAnnotationsProducer
CreatedDate = annotation.CreatedDate, CreatedDate = annotation.CreatedDate,
Image = image, Image = image,
Label = label, Detections = JsonConvert.SerializeObject(annotation.Detections),
Source = annotation.Source Source = annotation.Source
}; };
messages.Add(annCreateMessage); messages.Add(annCreateMessage);
@@ -1,42 +1,43 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Drawing; using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common; using Azaion.Common.Database;
using Azaion.Common.DTO; 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.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Color = System.Drawing.Color; using Color = System.Drawing.Color;
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions; using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
using Size = System.Windows.Size; 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 delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
public class GalleryManager( public class GalleryService(
IOptions<DirectoriesConfig> directoriesConfig, IOptions<DirectoriesConfig> directoriesConfig,
IOptions<ThumbnailConfig> thumbnailConfig, IOptions<ThumbnailConfig> thumbnailConfig,
IOptions<AnnotationConfig> annotationConfig, IOptions<AnnotationConfig> annotationConfig,
ILogger<GalleryManager> logger) : IGalleryManager ILogger<GalleryService> logger,
IDbFactory dbFactory) : IGalleryService
{ {
private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value; private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value;
private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value; private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value;
private readonly AnnotationConfig _annotationConfig = annotationConfig.Value; private readonly AnnotationConfig _annotationConfig = annotationConfig.Value;
private readonly string _thumbnailsCacheFile = Path.Combine(directoriesConfig.Value.ThumbnailsDirectory, Constants.THUMBNAILS_CACHE_FILE);
public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate; public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
private readonly SemaphoreSlim _updateLock = new(1); private readonly SemaphoreSlim _updateLock = new(1);
public double ThumbnailsPercentage { get; set; } public double ProcessedThumbnailsPercentage { get; set; }
public ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; } = new();
private DirectoryInfo? _thumbnailsDirectory; 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()) foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
file.Delete(); 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() public async Task RefreshThumbnails()
{ {
await _updateLock.WaitAsync(); await _updateLock.WaitAsync();
var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
await db.Annotations.ToDictionaryAsync(x => x.Name)));
var missedAnnotations = new ConcurrentBag<Annotation>();
try try
{ {
var prefixLen = Constants.THUMBNAIL_PREFIX.Length; var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
var thumbnails = ThumbnailsDirectory.GetFiles() var thumbnails = ThumbnailsDirectory.GetFiles()
@@ -74,68 +84,93 @@ public class GalleryManager(
.Select(gr => gr.Key) .Select(gr => gr.Key)
.ToHashSet(); .ToHashSet();
if (File.Exists(_thumbnailsCacheFile))
{
var cache = JsonConvert.DeserializeObject<ConcurrentDictionary<string, LabelInfo>>(
await File.ReadAllTextAsync(_thumbnailsCacheFile), new DenseDateTimeConverter());
LabelsCache = cache ?? new ConcurrentDictionary<string, LabelInfo>();
}
else
LabelsCache = new ConcurrentDictionary<string, LabelInfo>();
var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles(); var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles();
var imagesCount = files.Length; var imagesCount = files.Length;
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) => await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
{ {
var imgName = Path.GetFileNameWithoutExtension(file.Name); var fName = Path.GetFileNameWithoutExtension(file.Name);
if (thumbnails.Contains(imgName))
return;
try 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) catch (Exception e)
{ {
logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}"); logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}");
} }
}, new ParallelOptions },
{ new ParallelOptions
ProgressFn = async num =>
{ {
Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}"); ProgressFn = async num =>
ThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount); {
ThumbnailsUpdate?.Invoke(ThumbnailsPercentage); Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
await Task.CompletedTask; ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
}, ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
CpuUtilPercent = 100, await Task.CompletedTask;
ProgressUpdateInterval = 200 },
}); CpuUtilPercent = 100,
ProgressUpdateInterval = 200
});
} }
finally 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(); _updateLock.Release();
} }
} }
public async Task SaveLabelsCache() public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default)
{
var labelsCacheStr = JsonConvert.SerializeObject(LabelsCache, new DenseDateTimeConverter());
await File.WriteAllTextAsync(_thumbnailsCacheFile, labelsCacheStr);
}
public async Task<(ThumbnailDto? thumbnailDto, List<int>? classes)> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default)
{ {
try try
{ {
var width = (int)_thumbnailConfig.Size.Width; var width = (int)_thumbnailConfig.Size.Width;
var height = (int)_thumbnailConfig.Size.Height; var height = (int)_thumbnailConfig.Size.Height;
var imgName = Path.GetFileName(imgPath); var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken)));
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt");
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken)));
var bitmap = new Bitmap(width, height); var bitmap = new Bitmap(width, height);
@@ -145,19 +180,6 @@ public class GalleryManager(
g.InterpolationMode = InterpolationMode.Default; g.InterpolationMode = InterpolationMode.Default;
var size = new Size(originalImage.Width, originalImage.Height); 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 thumbWhRatio = width / (float)height;
var border = _thumbnailConfig.Border; var border = _thumbnailConfig.Border;
@@ -167,7 +189,10 @@ public class GalleryManager(
var frameHeight = size.Height; var frameHeight = size.Height;
var frameWidth = size.Width; 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 labelsMinX = labels.Min(x => x.X);
var labelsMaxX = labels.Max(x => x.X + x.Width); var labelsMaxX = labels.Max(x => x.X + x.Width);
@@ -199,55 +224,28 @@ public class GalleryManager(
foreach (var label in labels) 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 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)); 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); g.FillRectangle(brush, rectangle);
} }
var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Constants.THUMBNAIL_PREFIX}.jpg"); bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
var thumbnailDto = new ThumbnailDto
{
ThumbnailPath = thumbnailName,
ImagePath = imgPath,
LabelPath = labelName,
ImageDate = File.GetCreationTimeUtc(imgPath)
};
return (thumbnailDto, classes);
} }
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, e.Message); logger.LogError(e, e.Message);
return (null, null);
} }
} }
public LabelInfo AddToCache(string imgPath, List<int> 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; event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
double ThumbnailsPercentage { get; set; } double ProcessedThumbnailsPercentage { get; set; }
Task SaveLabelsCache(); Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default);
LabelInfo AddToCache(string imgPath, List<int> classes);
ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; }
Task<(ThumbnailDto? thumbnailDto, List<int>? classes)> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default);
Task RefreshThumbnails(); Task RefreshThumbnails();
void ClearThumbnails(); Task ClearThumbnails(CancellationToken cancellationToken = default);
} }
+1
View File
@@ -17,6 +17,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.46" /> <PackageReference Include="ScottPlot.WPF" Version="5.0.46" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" /> <PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" />
</ItemGroup> </ItemGroup>
+6 -23
View File
@@ -5,15 +5,14 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
xmlns:scottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF" 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: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" mc:Ignorable="d"
Title="Переглядач анотацій" Height="900" Width="1200" Title="Переглядач анотацій" Height="900" Width="1200"
WindowState="Maximized"> WindowState="Maximized">
<Window.Resources> <Window.Resources>
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type datasetExplorer:ThumbnailDto}"> <DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:AnnotationImageView}">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
@@ -21,7 +20,7 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Image <Image
Grid.Row="0" Grid.Row="0"
Source="{Binding Image}" Source="{Binding Thumbnail}"
Width="480" Width="480"
Height="270" Height="270"
Margin="2" /> Margin="2" />
@@ -47,11 +46,11 @@
<ColumnDefinition Width="4"/> <ColumnDefinition Width="4"/>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<controls:AnnotationClasses <controls:DetectionClasses
x:Name="LvClasses" x:Name="LvClasses"
Grid.Column="0" Grid.Column="0"
Grid.Row="0"> Grid.Row="0">
</controls:AnnotationClasses> </controls:DetectionClasses>
<TabControl <TabControl
Name="Switcher" Name="Switcher"
@@ -67,7 +66,7 @@
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Background="Black" Background="Black"
Margin="2,5,2,2" Margin="2,5,2,2"
ItemsSource="{Binding ThumbnailsDtos, Mode=OneWay}" ItemsSource="{Binding SelectedAnnotations, Mode=OneWay}"
ItemTemplate="{StaticResource ThumbnailTemplate}" ItemTemplate="{StaticResource ThumbnailTemplate}"
> >
</vwp:GridView> </vwp:GridView>
@@ -109,22 +108,6 @@
</Grid> </Grid>
</ItemsPanelTemplate> </ItemsPanelTemplate>
</StatusBar.ItemsPanel> </StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="0" Background="Black">
<TextBlock Name="LoadingAnnsCaption">Завантаження:</TextBlock>
</StatusBarItem>
<StatusBarItem Grid.Column="1" Background="Black">
<ProgressBar x:Name="LoadingAnnsBar"
Width="150"
Height="15"
HorizontalAlignment="Stretch"
Background="#252525"
BorderBrush="#252525"
Foreground="LightBlue"
Maximum="100"
Minimum="0"
Value="0">
</ProgressBar>
</StatusBarItem>
<StatusBarItem Grid.Column="2" Background="Black"> <StatusBarItem Grid.Column="2" Background="Black">
<TextBlock Name="RefreshThumbCaption">База іконок:</TextBlock> <TextBlock Name="RefreshThumbCaption">База іконок:</TextBlock>
</StatusBarItem> </StatusBarItem>
+84 -125
View File
@@ -4,8 +4,12 @@ using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using Azaion.Common; using Azaion.Common;
using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Services;
using LinqToDB;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using ScottPlot; using ScottPlot;
@@ -13,33 +17,40 @@ using Color = ScottPlot.Color;
namespace Azaion.Dataset; namespace Azaion.Dataset;
public partial class DatasetExplorer public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEvent>
{ {
private readonly ILogger<DatasetExplorer> _logger; private readonly ILogger<DatasetExplorer> _logger;
private readonly AnnotationConfig _annotationConfig; private readonly AnnotationConfig _annotationConfig;
private readonly DirectoriesConfig _directoriesConfig; private readonly DirectoriesConfig _directoriesConfig;
public ObservableCollection<ThumbnailDto> ThumbnailsDtos { get; set; } = new(); private Dictionary<int, List<Annotation>> _annotationsDict;
private ObservableCollection<AnnotationClass> AllAnnotationClasses { get; set; } = new();
public ObservableCollection<AnnotationImageView> SelectedAnnotations { get; set; } = new();
private ObservableCollection<DetectionClass> AllAnnotationClasses { get; set; } = new();
public Dictionary<string, LabelInfo> LabelsCache { get; set; } = new();
private int _tempSelectedClassIdx = 0; private int _tempSelectedClassIdx = 0;
private readonly IGalleryManager _galleryManager; private readonly IGalleryService _galleryService;
private readonly IDbFactory _dbFactory;
public bool ThumbnailLoading { get; set; } public bool ThumbnailLoading { get; set; }
public ThumbnailDto? CurrentThumbnail { get; set; } public AnnotationImageView? CurrentAnnotation { get; set; }
public DatasetExplorer( public DatasetExplorer(
IOptions<DirectoriesConfig> directoriesConfig, IOptions<DirectoriesConfig> directoriesConfig,
IOptions<AnnotationConfig> annotationConfig, IOptions<AnnotationConfig> annotationConfig,
ILogger<DatasetExplorer> logger, ILogger<DatasetExplorer> logger,
IGalleryManager galleryManager, IGalleryService galleryService,
FormState formState) FormState formState,
IDbFactory dbFactory)
{ {
_directoriesConfig = directoriesConfig.Value; _directoriesConfig = directoriesConfig.Value;
_annotationConfig = annotationConfig.Value; _annotationConfig = annotationConfig.Value;
_logger = logger; _logger = logger;
_galleryManager = galleryManager; _galleryService = galleryService;
_dbFactory = dbFactory;
InitializeComponent(); InitializeComponent();
Loaded += OnLoaded; Loaded += OnLoaded;
@@ -61,11 +72,11 @@ public partial class DatasetExplorer
ThumbnailsView.SelectionChanged += (_, _) => 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); ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentAnnotation!.Annotation.ImagePath);
galleryManager.ThumbnailsUpdate += thumbnailsPercentage => galleryService.ThumbnailsUpdate += thumbnailsPercentage =>
{ {
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage); Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
}; };
@@ -74,24 +85,22 @@ public partial class DatasetExplorer
private async void OnLoaded(object sender, RoutedEventArgs e) private async void OnLoaded(object sender, RoutedEventArgs e)
{ {
_ = Task.Run(async () => await _galleryManager.RefreshThumbnails()); AllAnnotationClasses = new ObservableCollection<DetectionClass>(
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
new List<AnnotationClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_annotationConfig.AnnotationClasses)); .Concat(_annotationConfig.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.MouseUp += async (_, _) => LvClasses.MouseUp += async (_, _) =>
{ {
var selectedClass = (AnnotationClass)LvClasses.SelectedItem; var selectedClass = (DetectionClass)LvClasses.SelectedItem;
ExplorerEditor.CurrentAnnClass = selectedClass; ExplorerEditor.CurrentAnnClass = selectedClass;
_annotationConfig.LastSelectedExplorerClass = selectedClass.Id; _annotationConfig.LastSelectedExplorerClass = selectedClass.Id;
if (Switcher.SelectedIndex == 0) if (Switcher.SelectedIndex == 0)
await ReloadThumbnails(); await ReloadThumbnails();
else else
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected)) foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
ann.AnnotationClass = selectedClass; ann.DetectionClass = selectedClass;
}; };
LvClasses.SelectionChanged += (_, _) => LvClasses.SelectionChanged += (_, _) =>
@@ -99,35 +108,54 @@ public partial class DatasetExplorer
if (Switcher.SelectedIndex != 1) if (Switcher.SelectedIndex != 1)
return; return;
var selectedClass = (AnnotationClass)LvClasses.SelectedItem; var selectedClass = (DetectionClass)LvClasses.SelectedItem;
if (selectedClass == null) if (selectedClass == null)
return; return;
ExplorerEditor.CurrentAnnClass = selectedClass; ExplorerEditor.CurrentAnnClass = selectedClass;
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected)) foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
ann.AnnotationClass = selectedClass; ann.DetectionClass = selectedClass;
}; };
LvClasses.SelectedIndex = _annotationConfig.LastSelectedExplorerClass ?? 0; LvClasses.SelectedIndex = _annotationConfig.LastSelectedExplorerClass ?? 0;
ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem; ExplorerEditor.CurrentAnnClass = (DetectionClass)LvClasses.SelectedItem;
await ReloadThumbnails();
LoadClassDistribution();
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<Annotation>());
foreach (var annotation in allAnnotations)
AddAnnotationToDict(annotation);
});
await ReloadThumbnails();
await LoadClassDistribution();
RefreshThumbBar.Value = _galleryService.ProcessedThumbnailsPercentage;
DataContext = this; 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) .SelectMany(x => x.Value.Classes)
.GroupBy(x => x) .GroupBy(x => x)
.Select(x => new .Select(x => new
{ {
x.Key, x.Key,
_annotationConfig.AnnotationClassesDict[x.Key].Name, _annotationConfig.DetectionClassesDict[x.Key].Name,
_annotationConfig.AnnotationClassesDict[x.Key].Color, _annotationConfig.DetectionClassesDict[x.Key].Color,
ClassCount = x.Count() ClassCount = x.Count()
}) })
.ToList(); .ToList();
@@ -167,8 +195,8 @@ public partial class DatasetExplorer
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question); "Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
return; return;
_galleryManager.ClearThumbnails(); _galleryService.ClearThumbnails();
_galleryManager.RefreshThumbnails(); _galleryService.RefreshThumbnails();
} }
private async Task EditAnnotation() private async Task EditAnnotation()
@@ -180,20 +208,20 @@ public partial class DatasetExplorer
if (ThumbnailsView.SelectedItem == null) if (ThumbnailsView.SelectedItem == null)
return; return;
var dto = (ThumbnailsView.SelectedItem as ThumbnailDto)!; CurrentAnnotation = (ThumbnailsView.SelectedItem as AnnotationImageView)!;
CurrentThumbnail = dto; var ann = CurrentAnnotation.Annotation;
ExplorerEditor.Background = new ImageBrush ExplorerEditor.Background = new ImageBrush
{ {
ImageSource = await dto.ImagePath.OpenImage() ImageSource = await ann.ImagePath.OpenImage()
}; };
SwitchTab(toEditor: true); SwitchTab(toEditor: true);
var time = Constants.GetTime(dto.ImagePath); var time = Constants.GetTime(ann.ImagePath);
ExplorerEditor.RemoveAllAnns(); ExplorerEditor.RemoveAllAnns();
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) foreach (var deetection in ann.Detections)
{ {
var annClass = _annotationConfig.AnnotationClassesDict[ann.ClassNumber]; var annClass = _annotationConfig.DetectionClassesDict[deetection.ClassNumber];
var canvasLabel = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize); var canvasLabel = new CanvasLabel(deetection, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel); ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
} }
@@ -243,101 +271,32 @@ public partial class DatasetExplorer
var selected = ThumbnailsView.SelectedItems.Count; var selected = ThumbnailsView.SelectedItems.Count;
for (var i = 0; i < selected; i++) for (var i = 0; i < selected; i++)
{ {
var dto = (ThumbnailsView.SelectedItems[0] as ThumbnailDto)!; var dto = (ThumbnailsView.SelectedItems[0] as AnnotationImageView)!;
File.Delete(dto.ImagePath); dto.Delete();
File.Delete(dto.LabelPath); SelectedAnnotations.Remove(dto);
File.Delete(dto.ThumbnailPath);
ThumbnailsDtos.Remove(dto);
} }
ThumbnailsView.SelectedIndex = Math.Min(ThumbnailsDtos.Count, tempSelected); ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected);
} }
private async Task ReloadThumbnails() private async Task ReloadThumbnails()
{ {
LoadingAnnsCaption.Visibility = Visibility.Visible; SelectedAnnotations.Clear();
LoadingAnnsBar.Visibility = Visibility.Visible; 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; return;
var thumbnails = Directory.GetFiles(_directoriesConfig.ThumbnailsDirectory, "*.jpg"); AddAnnotationToDict(annotation);
var thumbnailDtos = new List<ThumbnailDto>(); if (annotation.Classes.Contains(selectedClass.Value))
for (int i = 0; i < thumbnails.Length; i++) SelectedAnnotations.Add(new AnnotationImageView(annotation));
{
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;
} }
private async Task<ThumbnailDto?> GetThumbnail(string thumbnail) public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) =>
{ AddThumbnail(notification.Annotation);
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<int> classes)
{
var selectedClass = ((AnnotationClass?)LvClasses.SelectedItem)?.Id;
if (selectedClass != null && (selectedClass == -1 || classes.Any(x => x == selectedClass)))
ThumbnailsDtos.Insert(0, thumbnailDto);
}
} }
+11 -19
View File
@@ -2,15 +2,16 @@
using System.Windows.Input; using System.Windows.Input;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Services;
using MediatR; using MediatR;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Azaion.Dataset; namespace Azaion.Dataset;
public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalleryManager galleryManager, IOptions<DirectoriesConfig> directoriesConfig) public class DatasetExplorerEventHandler(
: DatasetExplorer datasetExplorer,
INotificationHandler<KeyEvent>, AnnotationService annotationService) : INotificationHandler<KeyEvent>
INotificationHandler<ImageCreatedEvent>
{ {
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new() private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{ {
@@ -48,14 +49,13 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalle
if (datasetExplorer.ThumbnailLoading) if (datasetExplorer.ThumbnailLoading)
return; return;
var currentAnns = datasetExplorer.ExplorerEditor.CurrentAnns var fName = Path.GetFileNameWithoutExtension(datasetExplorer.CurrentAnnotation!.Annotation.ImagePath);
.Select(x => new YoloLabel(x.Info, datasetExplorer.ExplorerEditor.RenderSize, datasetExplorer.ExplorerEditor.RenderSize)) var extension = Path.GetExtension(fName);
.ToList();
await YoloLabel.WriteToFile(currentAnns, Path.Combine(directoriesConfig.Value.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath)); var detections = datasetExplorer.ExplorerEditor.CurrentDetections
await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath); .Select(x => new Detection(fName, x.GetLabel(datasetExplorer.ExplorerEditor.RenderSize)))
await galleryManager.SaveLabelsCache(); .ToList();
datasetExplorer.CurrentThumbnail.UpdateImage(); await annotationService.SaveAnnotation(fName, extension, detections, SourceEnum.Manual);
datasetExplorer.SwitchTab(toEditor: false); datasetExplorer.SwitchTab(toEditor: false);
break; break;
case PlaybackControlEnum.RemoveSelectedAnns: case PlaybackControlEnum.RemoveSelectedAnns:
@@ -69,12 +69,4 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalle
break; 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();
}
} }
-39
View File
@@ -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));
}
}
+1 -1
View File
@@ -130,7 +130,7 @@ public partial class App
services.AddSingleton<AnnotationService>(); services.AddSingleton<AnnotationService>();
services.AddSingleton<DatasetExplorer>(); services.AddSingleton<DatasetExplorer>();
services.AddSingleton<IGalleryManager, GalleryManager>(); services.AddSingleton<IGalleryService, GalleryService>();
services.AddSingleton<IAzaionModule, AnnotatorModule>(); services.AddSingleton<IAzaionModule, AnnotatorModule>();
services.AddSingleton<IAzaionModule, DatasetExplorerModule>(); services.AddSingleton<IAzaionModule, DatasetExplorerModule>();
+2
View File
@@ -7,6 +7,8 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<ApplicationIcon>..\logo.ico</ApplicationIcon> <ApplicationIcon>..\logo.ico</ApplicationIcon>
<RunPostBuildEvent>Always</RunPostBuildEvent>
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+17 -3
View File
@@ -3,10 +3,12 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using Azaion.Annotator.Extensions; using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services;
using Azaion.Dataset;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using SharpVectors.Converters; using SharpVectors.Converters;
@@ -19,13 +21,23 @@ public partial class MainSuite
private readonly IConfigUpdater _configUpdater; private readonly IConfigUpdater _configUpdater;
private readonly IEnumerable<IAzaionModule> _modules; private readonly IEnumerable<IAzaionModule> _modules;
private readonly IServiceProvider _sp; private readonly IServiceProvider _sp;
private readonly IGalleryService _galleryService;
private readonly IDbFactory _dbFactory;
private readonly Dictionary<WindowEnum, Window> _openedWindows = new(); private readonly Dictionary<WindowEnum, Window> _openedWindows = new();
public MainSuite(IOptions<AppConfig> appConfig, IConfigUpdater configUpdater, IEnumerable<IAzaionModule> modules, IServiceProvider sp ) public MainSuite(IOptions<AppConfig> appConfig,
IConfigUpdater configUpdater,
IEnumerable<IAzaionModule> modules,
IServiceProvider sp,
IGalleryService galleryService,
IDbFactory dbFactory
)
{ {
_configUpdater = configUpdater; _configUpdater = configUpdater;
_modules = modules; _modules = modules;
_sp = sp; _sp = sp;
_galleryService = galleryService;
_dbFactory = dbFactory;
_appConfig = appConfig.Value; _appConfig = appConfig.Value;
InitializeComponent(); InitializeComponent();
Loaded += OnLoaded; Loaded += OnLoaded;
@@ -76,7 +88,8 @@ public partial class MainSuite
ListView.Items.Add(lvItem); ListView.Items.Add(lvItem);
} }
_ = Task.Run(async () => await _galleryService.RefreshThumbnails());
//by default show first //by default show first
ListView.SelectedIndex = 0; ListView.SelectedIndex = 0;
OpenWindow((ListView.Items[0] as ListViewItem)!); OpenWindow((ListView.Items[0] as ListViewItem)!);
@@ -115,6 +128,7 @@ public partial class MainSuite
private void OnFormClosed(object? sender, EventArgs e) private void OnFormClosed(object? sender, EventArgs e)
{ {
_configUpdater.Save(_appConfig); _configUpdater.Save(_appConfig);
_dbFactory.SaveToDisk();
foreach (var window in _openedWindows) foreach (var window in _openedWindows)
window.Value.Close(); window.Value.Close();
Application.Current.Shutdown(); Application.Current.Shutdown();