mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 10:46:30 +00:00
add db WIP 2, 80%
refactor, renames
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
-1
@@ -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"
|
||||||
+2
-2
@@ -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();
|
||||||
}
|
}
|
||||||
+13
-9
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
|
||||||
|
|
||||||
public class ImageCreatedEvent(string imagePath) : INotification
|
|
||||||
{
|
|
||||||
public string ImagePath { get; } = imagePath;
|
|
||||||
}
|
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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>();
|
||||||
}
|
}
|
||||||
@@ -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
-1
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user