remove fix, todo: test

This commit is contained in:
Alex Bezdieniezhnykh
2025-01-03 18:32:56 +02:00
parent 9aebfd787b
commit ae2c62350a
19 changed files with 353 additions and 245 deletions
+87 -110
View File
@@ -9,25 +9,26 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
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.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
using Newtonsoft.Json;
using Size = System.Windows.Size; using Size = System.Windows.Size;
using IntervalTree; using IntervalTree;
using LinqToDB;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer; using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public partial class Annotator public partial class Annotator : INotificationHandler<AnnotationsDeletedEvent>
{ {
private readonly AppConfig _appConfig; private readonly AppConfig _appConfig;
private readonly LibVLC _libVLC; private readonly LibVLC _libVLC;
@@ -41,7 +42,8 @@ public partial class Annotator
private readonly VLCFrameExtractor _vlcFrameExtractor; private readonly VLCFrameExtractor _vlcFrameExtractor;
private readonly IAIDetector _aiDetector; private readonly IAIDetector _aiDetector;
private readonly AnnotationService _annotationService; private readonly AnnotationService _annotationService;
private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly IDbFactory _dbFactory;
private readonly CancellationTokenSource _ctSource = new();
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new(); private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
@@ -53,20 +55,22 @@ 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<Detection>> Detections { get; set; } = new(); public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
private AutodetectDialog _autoDetectDialog = new() { Topmost = true }; private AutodetectDialog _autoDetectDialog = new() { Topmost = true };
public Annotator( public Annotator(
IConfigUpdater configUpdater, IConfigUpdater configUpdater,
IOptions<AppConfig> appConfig, IOptions<AppConfig> appConfig,
LibVLC libVLC, MediaPlayer mediaPlayer, LibVLC libVLC,
MediaPlayer mediaPlayer,
IMediator mediator, IMediator mediator,
FormState formState, FormState formState,
HelpWindow helpWindow, HelpWindow helpWindow,
ILogger<Annotator> logger, ILogger<Annotator> logger,
VLCFrameExtractor vlcFrameExtractor, VLCFrameExtractor vlcFrameExtractor,
IAIDetector aiDetector, IAIDetector aiDetector,
AnnotationService annotationService) AnnotationService annotationService,
IDbFactory dbFactory)
{ {
InitializeComponent(); InitializeComponent();
_appConfig = appConfig.Value; _appConfig = appConfig.Value;
@@ -80,10 +84,27 @@ public partial class Annotator
_vlcFrameExtractor = vlcFrameExtractor; _vlcFrameExtractor = vlcFrameExtractor;
_aiDetector = aiDetector; _aiDetector = aiDetector;
_annotationService = annotationService; _annotationService = annotationService;
_dbFactory = dbFactory;
Loaded += OnLoaded; Loaded += OnLoaded;
Closed += OnFormClosed; Closed += OnFormClosed;
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator; Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
TbFolder.TextChanged += async (sender, args) =>
{
if (!Path.Exists(TbFolder.Text))
return;
try
{
_appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text;
ReloadFiles();
await SaveUserSettings();
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
};
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
} }
@@ -98,8 +119,7 @@ public partial class Annotator
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.RightPanelWidth); MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.RightPanelWidth);
_suspendLayout = false; _suspendLayout = false;
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
ReloadFiles();
AnnotationClasses = new ObservableCollection<DetectionClass>(_appConfig.AnnotationConfig.AnnotationClasses); AnnotationClasses = new ObservableCollection<DetectionClass>(_appConfig.AnnotationConfig.AnnotationClasses);
LvClasses.ItemsSource = AnnotationClasses; LvClasses.ItemsSource = AnnotationClasses;
@@ -141,7 +161,7 @@ public partial class Annotator
_formState.CurrentVideoSize = new Size(vw, vh); _formState.CurrentVideoSize = new Size(vw, vh);
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length); _formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
await Dispatcher.Invoke(async () => await ReloadAnnotations(_cancellationTokenSource.Token)); await Dispatcher.Invoke(async () => await ReloadAnnotations());
if (_formState.CurrentMedia?.MediaType == MediaTypes.Image) if (_formState.CurrentMedia?.MediaType == MediaTypes.Image)
{ {
@@ -182,7 +202,7 @@ public partial class Annotator
OpenAnnotationResult((AnnotationResult)dgRow!.Item); OpenAnnotationResult((AnnotationResult)dgRow!.Item);
}; };
DgAnnotations.KeyUp += (sender, args) => DgAnnotations.KeyUp += async (sender, args) =>
{ {
switch (args.Key) switch (args.Key)
{ {
@@ -196,17 +216,9 @@ public partial class Annotator
return; return;
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList(); var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
foreach (var annotationResult in res) var annotations = res.Select(x => x.Annotation).ToList();
{
var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image);
var thumbnailPath = Path.Combine(_appConfig.DirectoriesConfig.ThumbnailsDirectory, $"{imgName}{Constants.THUMBNAIL_PREFIX}.jpg");
File.Delete(annotationResult.Image);
File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt")); await _mediator.Publish(new AnnotationsDeletedEvent(annotations));
File.Delete(thumbnailPath);
_formState.AnnotationResults.Remove(annotationResult);
Detections.Remove(Detections.Query(annotationResult.Time));
}
break; break;
} }
}; };
@@ -219,16 +231,16 @@ public partial class Annotator
{ {
_mediaPlayer.SetPause(true); _mediaPlayer.SetPause(true);
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
_mediaPlayer.Time = (long)res.Time.TotalMilliseconds; _mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds;
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum; VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
Editor.ClearExpiredAnnotations(res.Time); Editor.ClearExpiredAnnotations(res.Annotation.Time);
}); });
AddAnnotationsToCanvas(res.Time, res.Detections, showImage: true); ShowAnnotations(res.Annotation, showImage: true);
} }
private async Task SaveUserSettings() private async Task SaveUserSettings()
{ {
@@ -254,114 +266,73 @@ public partial class Annotator
Editor.ClearExpiredAnnotations(time); Editor.ClearExpiredAnnotations(time);
}); });
var annotations = Detections.Query(time).SelectMany(x => x).Select(x => new Detection(_formState.GetTimeName(time), x)); ShowAnnotations(TimedAnnotations.Query(time).FirstOrDefault());
AddAnnotationsToCanvas(time, annotations);
} }
private void AddAnnotationsToCanvas(TimeSpan? time, IEnumerable<Detection> labels, bool showImage = false) private void ShowAnnotations(Annotation? annotation, bool showImage = false)
{ {
if (annotation == null)
return;
Dispatcher.Invoke(async () => Dispatcher.Invoke(async () =>
{ {
var canvasSize = Editor.RenderSize; var canvasSize = Editor.RenderSize;
var videoSize = _formState.CurrentVideoSize; var videoSize = _formState.CurrentVideoSize;
if (showImage) if (showImage)
{ {
var fName = _formState.GetTimeName(time); if (File.Exists(annotation.ImagePath))
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg");
if (File.Exists(imgPath))
{ {
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; Editor.Background = new ImageBrush { ImageSource = await annotation.ImagePath.OpenImage() };
_formState.BackgroundTime = time; _formState.BackgroundTime = annotation.Time;
videoSize = Editor.RenderSize; videoSize = Editor.RenderSize;
} }
} }
foreach (var label in labels) foreach (var detection in annotation.Detections)
{ {
var annClass = _appConfig.AnnotationConfig.AnnotationClasses[label.ClassNumber]; var annClass = _appConfig.AnnotationConfig.AnnotationClasses[detection.ClassNumber];
var canvasLabel = new CanvasLabel(label, canvasSize, videoSize, label.Probability); var canvasLabel = new CanvasLabel(detection, canvasSize, videoSize, detection.Probability);
Editor.CreateAnnotation(annClass, time, canvasLabel); Editor.CreateDetectionControl(annClass, annotation.Time, canvasLabel);
} }
}); });
} }
private async Task ReloadAnnotations(CancellationToken ct = default) private async Task ReloadAnnotations()
{ {
_formState.AnnotationResults.Clear(); _formState.AnnotationResults.Clear();
Detections.Clear(); TimedAnnotations.Clear();
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory); var annotations = await _dbFactory.Run(async db =>
if (!labelDir.Exists) await db.Annotations.LoadWith(x => x.Detections)
return; .Where(x => x.Name.Contains(_formState.VideoName))
.ToListAsync(token: _ctSource.Token));
var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_??????.txt"); foreach (var ann in annotations)
foreach (var file in labelFiles) AddAnnotation(ann);
await AddAnnotations(Path.GetFileNameWithoutExtension(file.Name), await YoloLabel.ReadFromFile(file.FullName, ct), ct);
} }
//Load from yolo label file
public async Task AddAnnotations(string name, List<YoloLabel> annotations, CancellationToken ct = default)
=> await AddAnnotations(name, annotations.Select(x => new Detection(name, x)).ToList(), ct);
//Add manually //Add manually
public async Task AddAnnotations(string name, List<Detection> detections, CancellationToken ct = default) public void AddAnnotation(Annotation annotation)
{ {
var time = Constants.GetTime(name); var time = annotation.Time;
var timeValue = time ?? TimeSpan.FromMinutes(0); var previousAnnotations = TimedAnnotations.Query(time);
var previousAnnotations = Detections.Query(timeValue); TimedAnnotations.Remove(previousAnnotations);
Detections.Remove(previousAnnotations); TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation);
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.Annotation.Time == time);
if (existingResult != null) if (existingResult != null)
_formState.AnnotationResults.Remove(existingResult); _formState.AnnotationResults.Remove(existingResult);
var dict = _formState.AnnotationResults var dict = _formState.AnnotationResults
.Select((x, i) => new { x.Time, Index = i }) .Select((x, i) => new { x.Annotation.Time, Index = i })
.ToDictionary(x => x.Time, x => x.Index); .ToDictionary(x => x.Time, x => x.Index);
var index = dict.Where(x => x.Key < timeValue) var index = dict.Where(x => x.Key < time)
.OrderBy(x => timeValue - x.Key) .OrderBy(x => time - x.Key)
.Select(x => x.Value + 1) .Select(x => x.Value + 1)
.FirstOrDefault(); .FirstOrDefault();
_formState.AnnotationResults.Insert(index, CreateAnnotationReult(timeValue, detections)); _formState.AnnotationResults.Insert(index, new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, annotation));
await File.WriteAllTextAsync($"{_appConfig.DirectoriesConfig.ResultsDirectory}/{_formState.VideoName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults), ct);
}
private AnnotationResult CreateAnnotationReult(TimeSpan timeValue, List<Detection> detections)
{
var annotationResult = new AnnotationResult
{
Time = timeValue,
Image = $"{_formState.GetTimeName(timeValue)}.jpg",
Detections = detections,
};
if (detections.Count <= 0)
return annotationResult;
Color GetAnnotationClass(List<int> detectionClasses, int colorNumber)
{
if (detections.Count == 0)
return (-1).ToColor();
return colorNumber >= detectionClasses.Count
? _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.LastOrDefault()].Color
: _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses[colorNumber]].Color;
}
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
annotationResult.ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.DetectionClassesDict[x].ShortName))
: _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.FirstOrDefault()].Name;
annotationResult.ClassColor0 = GetAnnotationClass(detectionClasses, 0);
annotationResult.ClassColor1 = GetAnnotationClass(detectionClasses, 1);
annotationResult.ClassColor2 = GetAnnotationClass(detectionClasses, 2);
annotationResult.ClassColor3 = GetAnnotationClass(detectionClasses, 3);
return annotationResult;
} }
private void ReloadFiles() private void ReloadFiles()
@@ -407,7 +378,6 @@ public partial class Annotator
AllMediaFiles = new ObservableCollection<MediaFileInfo>(videoFiles.Concat(imageFiles).ToList()); AllMediaFiles = new ObservableCollection<MediaFileInfo>(videoFiles.Concat(imageFiles).ToList());
LvFiles.ItemsSource = AllMediaFiles; LvFiles.ItemsSource = AllMediaFiles;
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
BlinkHelp(AllMediaFiles.Count == 0 BlinkHelp(AllMediaFiles.Count == 0
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial] ? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
@@ -458,16 +428,15 @@ public partial class Annotator
IsFolderPicker = true, IsFolderPicker = true,
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
}; };
if (dlg.ShowDialog() != CommonFileDialogResult.Ok) var dialogResult = dlg.ShowDialog();
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
return; return;
if (!string.IsNullOrEmpty(dlg.FileName))
{
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName; _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
await SaveUserSettings(); TbFolder.Text = dlg.FileName;
}
ReloadFiles(); ReloadFiles();
await SaveUserSettings();
} }
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e) private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
@@ -546,7 +515,7 @@ public partial class Annotator
while (mediaInfo != null) while (mediaInfo != null)
{ {
_formState.CurrentMedia = mediaInfo; _formState.CurrentMedia = mediaInfo;
await Dispatcher.Invoke(async () => await ReloadAnnotations(token)); await Dispatcher.Invoke(async () => await ReloadAnnotations());
if (mediaInfo.MediaType == MediaTypes.Image) if (mediaInfo.MediaType == MediaTypes.Image)
{ {
@@ -632,7 +601,7 @@ public partial class Annotator
continue; continue;
mediaInfo.HasAnnotations = true; mediaInfo.HasAnnotations = true;
await ProcessDetection(timeframe, "jpg", detections, token); await ProcessDetection(timeframe, ".jpg", detections, token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -697,16 +666,14 @@ public partial class Annotator
try try
{ {
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}.{imageExtension}");
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; var annotation = await _annotationService.SaveAnnotation(fName, imageExtension, detections, SourceEnum.AI, timeframe.Stream, token);
Editor.Background = new ImageBrush { ImageSource = await annotation.ImagePath.OpenImage() };
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
AddAnnotationsToCanvas(time, detections, true); ShowAnnotations(annotation, true);
await AddAnnotations(fName, detections, token); AddAnnotation(annotation);
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.DetectionClassesDict[det.ClassNumber].Name}: " + $"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
@@ -724,4 +691,14 @@ public partial class Annotator
} }
}); });
} }
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{
var annResDict = _formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x);
foreach (var ann in notification.Annotations)
{
_formState.AnnotationResults.Remove(annResDict[ann.Name]);
TimedAnnotations.Remove(ann);
}
}
} }
+4 -4
View File
@@ -6,6 +6,7 @@ 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.DTO.Queue;
using Azaion.Common.Events;
using Azaion.Common.Services; using Azaion.Common.Services;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
@@ -232,9 +233,7 @@ public class AnnotatorEventHandler(
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))) .Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
.ToList(); .ToList();
await mainWindow.AddAnnotations(fName, currentDetections, cancellationToken); formState.CurrentMedia.HasAnnotations = mainWindow.TimedAnnotations.Count != 0;
formState.CurrentMedia.HasAnnotations = mainWindow.Detections.Count != 0;
mainWindow.LvFiles.Items.Refresh(); mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns(); mainWindow.Editor.RemoveAllAnns();
@@ -267,6 +266,7 @@ 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, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken); var annotation = await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken);
mainWindow.AddAnnotation(annotation);
} }
} }
-18
View File
@@ -65,24 +65,6 @@ public class Constants
#endregion #endregion
public static TimeSpan? GetTime(string imagePath)
{
var timeStr = imagePath.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
return null;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!int.TryParse(timeStr[0..1], out var hours))
return null;
if (!int.TryParse(timeStr[1..3], out var minutes))
return null;
if (!int.TryParse(timeStr[3..5], out var seconds))
return null;
if (!int.TryParse(timeStr[5..6], out var milliseconds))
return null;
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
}
#region Queue #region Queue
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
+15 -16
View File
@@ -34,13 +34,13 @@ public class CanvasEditor : Canvas
public static readonly DependencyProperty GetTimeFuncProp = public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register( DependencyProperty.Register(
nameof(GetTimeFunc), nameof(GetTimeFunc),
typeof(Func<TimeSpan?>), typeof(Func<TimeSpan>),
typeof(CanvasEditor), typeof(CanvasEditor),
new PropertyMetadata(null)); new PropertyMetadata(null));
public Func<TimeSpan?> GetTimeFunc public Func<TimeSpan> GetTimeFunc
{ {
get => (Func<TimeSpan?>)GetValue(GetTimeFuncProp); get => (Func<TimeSpan>)GetValue(GetTimeFuncProp);
set => SetValue(GetTimeFuncProp, value); set => SetValue(GetTimeFuncProp, value);
} }
@@ -154,7 +154,7 @@ public class CanvasEditor : Canvas
private void CanvasMouseUp(object sender, MouseButtonEventArgs e) private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{ {
if (SelectionState == SelectionState.NewAnnCreating) if (SelectionState == SelectionState.NewAnnCreating)
CreateAnnotation(e.GetPosition(this)); CreateDetectionControl(e.GetPosition(this));
SelectionState = SelectionState.None; SelectionState = SelectionState.None;
e.Handled = true; e.Handled = true;
@@ -291,7 +291,7 @@ public class CanvasEditor : Canvas
SetTop(_newAnnotationRect, currentPos.Y); SetTop(_newAnnotationRect, currentPos.Y);
} }
private void CreateAnnotation(Point endPos) private void CreateDetectionControl(Point endPos)
{ {
_newAnnotationRect.Width = 0; _newAnnotationRect.Width = 0;
_newAnnotationRect.Height = 0; _newAnnotationRect.Height = 0;
@@ -301,7 +301,7 @@ public class CanvasEditor : Canvas
return; return;
var time = GetTimeFunc(); var time = GetTimeFunc();
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
{ {
Width = width, Width = width,
Height = height, Height = height,
@@ -310,20 +310,20 @@ public class CanvasEditor : Canvas
}); });
} }
public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel) public DetectionControl CreateDetectionControl(DetectionClass annClass, TimeSpan time, CanvasLabel canvasLabel)
{ {
var annotationControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability) var detectionControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
{ {
Width = canvasLabel.Width, Width = canvasLabel.Width,
Height = canvasLabel.Height Height = canvasLabel.Height
}; };
annotationControl.MouseDown += AnnotationPositionStart; detectionControl.MouseDown += AnnotationPositionStart;
SetLeft(annotationControl, canvasLabel.X ); SetLeft(detectionControl, canvasLabel.X );
SetTop(annotationControl, canvasLabel.Y); SetTop(detectionControl, canvasLabel.Y);
Children.Add(annotationControl); Children.Add(detectionControl);
CurrentDetections.Add(annotationControl); CurrentDetections.Add(detectionControl);
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color); _newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
return annotationControl; return detectionControl;
} }
#endregion #endregion
@@ -355,8 +355,7 @@ public class CanvasEditor : Canvas
public void ClearExpiredAnnotations(TimeSpan time) public void ClearExpiredAnnotations(TimeSpan time)
{ {
var expiredAnns = CurrentDetections.Where(x => var expiredAnns = CurrentDetections.Where(x =>
x.Time.HasValue && Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
.ToList(); .ToList();
RemoveAnnotations(expiredAnns); RemoveAnnotations(expiredAnns);
} }
+2 -2
View File
@@ -16,7 +16,7 @@ public class DetectionControl : Border
private readonly Grid _grid; private readonly Grid _grid;
private readonly TextBlock _classNameLabel; private readonly TextBlock _classNameLabel;
private readonly Label _probabilityLabel; private readonly Label _probabilityLabel;
public TimeSpan? Time { get; set; } public TimeSpan Time { get; set; }
private DetectionClass _detectionClass = null!; private DetectionClass _detectionClass = null!;
public DetectionClass DetectionClass public DetectionClass DetectionClass
@@ -44,7 +44,7 @@ public class DetectionControl : Border
} }
} }
public DetectionControl(DetectionClass detectionClass, 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;
+35 -23
View File
@@ -1,40 +1,52 @@
using System.Windows.Media; using System.Windows.Media;
using Azaion.Common.Database;
using Azaion.Common.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationResult public class AnnotationResult
{ {
[JsonProperty(PropertyName = "f")] public Annotation Annotation { get; set; }
public string Image { get; set; } = null!;
[JsonProperty(PropertyName = "t")] public string ImagePath { get; set; }
public TimeSpan Time { get; set; } public string TimeStr { get; set; }
public double Lat { get; set; } public string ClassName { get; set; }
public double Lon { get; set; }
public List<Detection> Detections { get; set; } = new();
#region For XAML Form
[JsonIgnore]
public string TimeStr => $"{Time:h\\:mm\\:ss}";
[JsonIgnore]
public string ClassName { get; set; } = null!;
[JsonIgnore]
public Color ClassColor0 { get; set; } public Color ClassColor0 { get; set; }
[JsonIgnore]
public Color ClassColor1 { get; set; } public Color ClassColor1 { get; set; }
[JsonIgnore]
public Color ClassColor2 { get; set; } public Color ClassColor2 { get; set; }
[JsonIgnore]
public Color ClassColor3 { get; set; } public Color ClassColor3 { get; set; }
#endregion
public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
{
Annotation = annotation;
var detections = annotation.Detections.ToList();
Color GetAnnotationClass(List<int> detectionClasses, int colorNumber)
{
if (detections.Count == 0)
return (-1).ToColor();
return colorNumber >= detectionClasses.Count
? allDetectionClasses[detectionClasses.LastOrDefault()].Color
: allDetectionClasses[detectionClasses[colorNumber]].Color;
}
TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
ImagePath = annotation.ImagePath;
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].ShortName))
: allDetectionClasses[detectionClasses.FirstOrDefault()].Name;
ClassColor0 = GetAnnotationClass(detectionClasses, 0);
ClassColor1 = GetAnnotationClass(detectionClasses, 1);
ClassColor2 = GetAnnotationClass(detectionClasses, 2);
ClassColor3 = GetAnnotationClass(detectionClasses, 3);
}
} }
@@ -7,7 +7,7 @@ using Azaion.Common.Extensions;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
{ {
public Annotation Annotation { get; set; } = annotation; public Annotation Annotation { get; set; } = annotation;
@@ -30,13 +30,6 @@ public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged
public string ImageName => Path.GetFileName(Annotation.ImagePath); public string ImageName => Path.GetFileName(Annotation.ImagePath);
public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created; public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created;
public void Delete()
{
File.Delete(Annotation.ImagePath);
File.Delete(Annotation.LabelPath);
File.Delete(Annotation.ThumbPath);
}
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{ {
+24
View File
@@ -36,6 +36,30 @@ public class Annotation
public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}"); public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt"); public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg"); public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
private TimeSpan? _time;
public TimeSpan Time
{
get
{
if (_time.HasValue)
return _time.Value;
var timeStr = Name.Split("_").LastOrDefault();
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!string.IsNullOrEmpty(timeStr) &&
timeStr.Length == 6 &&
int.TryParse(timeStr[..1], out var hours) &&
int.TryParse(timeStr[1..3], out var minutes) &&
int.TryParse(timeStr[3..5], out var seconds) &&
int.TryParse(timeStr[5..], out var milliseconds))
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
_time = TimeSpan.FromSeconds(0);
return _time.Value;
}
}
} }
+13 -1
View File
@@ -17,6 +17,7 @@ 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(); void SaveToDisk();
Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default);
} }
public class DbFactory : IDbFactory public class DbFactory : IDbFactory
@@ -41,7 +42,7 @@ public class DbFactory : IDbFactory
.UseDataProvider(SQLiteTools.GetDataProvider()) .UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_memoryConnection) .UseConnection(_memoryConnection)
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
_ = _memoryDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText)); //.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
_fileConnection = new SQLiteConnection(FileConnStr); _fileConnection = new SQLiteConnection(FileConnStr);
@@ -96,6 +97,16 @@ public class DbFactory : IDbFactory
{ {
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
} }
public async Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default)
{
var names = annotations.Select(x => x.Name).ToList();
await Run(async db =>
{
await db.Detections.DeleteAsync(x => names.Contains(x.AnnotationName), token: cancellationToken);
await db.Annotations.DeleteAsync(x => names.Contains(x.Name), token: cancellationToken);
});
}
} }
public static class AnnotationsDbSchemaHolder public static class AnnotationsDbSchemaHolder
@@ -110,6 +121,7 @@ public static class AnnotationsDbSchemaHolder
builder.Entity<Annotation>() builder.Entity<Annotation>()
.HasTableName(Constants.ANNOTATIONS_TABLENAME) .HasTableName(Constants.ANNOTATIONS_TABLENAME)
.HasPrimaryKey(x => x.Name) .HasPrimaryKey(x => x.Name)
.Ignore(x => x.Time)
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName); .Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName);
builder.Entity<Detection>() builder.Entity<Detection>()
@@ -1,7 +1,7 @@
using Azaion.Common.Database; using Azaion.Common.Database;
using MediatR; using MediatR;
namespace Azaion.Common.DTO; namespace Azaion.Common.Events;
public class AnnotationCreatedEvent(Annotation annotation) : INotification public class AnnotationCreatedEvent(Annotation annotation) : INotification
{ {
@@ -0,0 +1,9 @@
using Azaion.Common.Database;
using MediatR;
namespace Azaion.Common.Events;
public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotification
{
public List<Annotation> Annotations { get; set; } = annotations;
}
@@ -1,7 +1,8 @@
using System.Windows.Input; using System.Windows.Input;
using Azaion.Common.DTO;
using MediatR; using MediatR;
namespace Azaion.Common.DTO; namespace Azaion.Common.Events;
public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification
{ {
+26 -5
View File
@@ -5,6 +5,7 @@ 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.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services; using Azaion.CommonSecurity.Services;
@@ -19,7 +20,7 @@ using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public class AnnotationService public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
{ {
private readonly AzaionApiClient _apiClient; private readonly AzaionApiClient _apiClient;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
@@ -83,12 +84,13 @@ public class AnnotationService
} }
//AI / Manual //AI / Manual
public async Task SaveAnnotation(string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) => public async Task<Annotation> SaveAnnotation(string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, token); await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, generateThumbnail: true, token);
//Manual //Manual
public async Task ValidateAnnotation(Annotation annotation, CancellationToken token = default) => public async Task ValidateAnnotation(Annotation annotation, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, annotation.Name, annotation.ImageExtension, annotation.Detections.ToList(), SourceEnum.Manual, null, _apiClient.User.Role, _apiClient.User.Email, token); await SaveAnnotationInner(DateTime.UtcNow, annotation.Name, annotation.ImageExtension, annotation.Detections.ToList(), SourceEnum.Manual, null, _apiClient.User.Role, _apiClient.User.Email,
generateThumbnail: false, token);
//Queue (only from operators) //Queue (only from operators)
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default) public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
@@ -105,12 +107,14 @@ public class AnnotationService
new MemoryStream(message.Image), new MemoryStream(message.Image),
message.CreatedRole, message.CreatedRole,
message.CreatedEmail, message.CreatedEmail,
generateThumbnail: true,
cancellationToken); cancellationToken);
} }
private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream, private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum userRole, RoleEnum userRole,
string createdEmail, string createdEmail,
bool generateThumbnail = false,
CancellationToken token = default) CancellationToken token = default)
{ {
//Flow for roles: //Flow for roles:
@@ -129,11 +133,14 @@ public class AnnotationService
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token);
await db.BulkCopyAsync(detections, cancellationToken: token); await db.BulkCopyAsync(detections, cancellationToken: token);
if (ann != null) if (ann != null)
{
await db.Annotations await db.Annotations
.Where(x => x.Name == fName) .Where(x => x.Name == fName)
.Set(x => x.Source, source) .Set(x => x.Source, source)
.Set(x => x.AnnotationStatus, status) .Set(x => x.AnnotationStatus, status)
.UpdateAsync(token: token); .UpdateAsync(token: token);
ann.Detections = detections;
}
else else
{ {
ann = new Annotation ann = new Annotation
@@ -158,7 +165,9 @@ public class AnnotationService
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
} }
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
if (generateThumbnail)
await _galleryService.CreateThumbnail(annotation, token); await _galleryService.CreateThumbnail(annotation, token);
await _producer.SendToQueue(annotation, token); await _producer.SendToQueue(annotation, token);
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
@@ -167,5 +176,17 @@ public class AnnotationService
_dbFactory.SaveToDisk(); _dbFactory.SaveToDisk();
return Task.CompletedTask; return Task.CompletedTask;
}, TimeSpan.FromSeconds(5), token); }, TimeSpan.FromSeconds(5), token);
return annotation;
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{
await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken);
foreach (var annotation in notification.Annotations)
{
File.Delete(annotation.ImagePath);
File.Delete(annotation.LabelPath);
File.Delete(annotation.ThumbPath);
}
} }
} }
+53 -6
View File
@@ -12,7 +12,7 @@
WindowState="Maximized"> WindowState="Maximized">
<Window.Resources> <Window.Resources>
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:AnnotationImageView}"> <DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:AnnotationThumbnail}">
<Border BorderBrush="IndianRed" Padding="5"> <Border BorderBrush="IndianRed" Padding="5">
<Border.Style> <Border.Style>
<Style TargetType="Border"> <Style TargetType="Border">
@@ -123,11 +123,12 @@
</Grid> </Grid>
</ItemsPanelTemplate> </ItemsPanelTemplate>
</StatusBar.ItemsPanel> </StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="2" Background="Black"> <StatusBarItem Grid.Column="0" Background="Black">
<Button Name="ValidateBtn" <Button Name="ValidateBtn"
Padding="2" Padding="2"
ToolTip="Підтвердити валідність. Клавіша: [A]" ToolTip="Підтвердити валідність. Клавіша: [V]"
Background="Black" BorderBrush="Black" Cursor="Hand" Background="Black" BorderBrush="Black"
Cursor="Hand"
Click="ValidateAnnotationsClick"> Click="ValidateAnnotationsClick">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Image> <Image>
@@ -148,8 +149,54 @@
</StackPanel> </StackPanel>
</Button> </Button>
</StatusBarItem> </StatusBarItem>
<Separator Grid.Column="4"/>
<StatusBarItem Grid.Column="5" Background="Black"> <Separator Grid.Column="1" />
<StatusBarItem x:Name="RefreshThumbnailsButtonItem" Grid.Column="2" Background="Black">
<Button
Padding="2"
Height="25"
ToolTip="Оновити базу іконок" Background="Black"
BorderBrush="Black"
Cursor="Hand"
Click="RefreshThumbnailsBtnClick">
<StackPanel Orientation="Horizontal">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V1200 H1200 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="M889.68 166.32c-93.608-102.216-228.154-166.32-377.68-166.32-282.77 0-512
229.23-512 512h96c0-229.75 186.25-416 416-416 123.020 0 233.542 53.418 309.696 138.306l-149.696 149.694h352v-352l-134.32 134.32z" />
<GeometryDrawing Brush="LightGray" Geometry="M928 512c0 229.75-186.25 416-416 416-123.020
0-233.542-53.418-309.694-138.306l149.694-149.694h-352v352l134.32-134.32c93.608 102.216 228.154 166.32 377.68 166.32 282.77 0 512-229.23 512-512h-96z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Foreground="White" Padding="8 0 0 0">Оновити базу іконок</TextBlock>
</StackPanel>
</Button>
</StatusBarItem>
<StatusBarItem Grid.Column="2" x:Name="RefreshProgressBarItem" Visibility="Hidden">
<StackPanel>
<TextBlock Name="RefreshThumbCaption" Padding="0 0 5 0">База іконок:</TextBlock>
<ProgressBar x:Name="RefreshThumbBar"
Width="150"
Height="15"
HorizontalAlignment="Stretch"
Background="#252525"
BorderBrush="#252525"
Foreground="LightBlue"
Maximum="100"
Minimum="0"
Value="0">
</ProgressBar>
</StackPanel>
</StatusBarItem>
<StatusBarItem Grid.Column="3" Background="Black">
<TextBlock Name="StatusText" Text=""/> <TextBlock Name="StatusText" Text=""/>
</StatusBarItem> </StatusBarItem>
</StatusBar> </StatusBar>
+74 -44
View File
@@ -2,12 +2,11 @@
using System.Windows; 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.Database; 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.Events;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.CommonSecurity.Services;
using LinqToDB; using LinqToDB;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -17,28 +16,28 @@ using Color = ScottPlot.Color;
namespace Azaion.Dataset; namespace Azaion.Dataset;
public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEvent> public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEvent>, INotificationHandler<AnnotationsDeletedEvent>
{ {
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;
private Dictionary<int, List<Annotation>> _annotationsDict; private Dictionary<int, List<Annotation>> _annotationsDict;
private readonly CancellationTokenSource _cts = new();
public ObservableCollection<AnnotationImageView> SelectedAnnotations { get; set; } = new(); public ObservableCollection<DetectionClass> AllDetectionClasses { get; set; } = new();
public ObservableCollection<DetectionClass> AllAnnotationClasses { get; set; } = new(); public ObservableCollection<AnnotationThumbnail> SelectedAnnotations { get; set; } = new();
private Dictionary<string, LabelInfo> LabelsCache { get; set; } = new();
private int _tempSelectedClassIdx = 0; private int _tempSelectedClassIdx = 0;
private readonly IGalleryService _galleryService; private readonly IGalleryService _galleryService;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly AzaionApiClient _apiClient; private readonly Dictionary<string, AnnotationThumbnail> _selectedAnnotationDict = new();
public bool ThumbnailLoading { get; set; } public bool ThumbnailLoading { get; set; }
public AnnotationImageView? CurrentAnnotation { get; set; } public AnnotationThumbnail? CurrentAnnotation { get; set; }
public DatasetExplorer( public DatasetExplorer(
IOptions<DirectoriesConfig> directoriesConfig, IOptions<DirectoriesConfig> directoriesConfig,
@@ -47,8 +46,7 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
IGalleryService galleryService, IGalleryService galleryService,
FormState formState, FormState formState,
IDbFactory dbFactory, IDbFactory dbFactory,
IMediator mediator, IMediator mediator)
AzaionApiClient apiClient)
{ {
_directoriesConfig = directoriesConfig.Value; _directoriesConfig = directoriesConfig.Value;
_annotationConfig = annotationConfig.Value; _annotationConfig = annotationConfig.Value;
@@ -56,7 +54,6 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
_galleryService = galleryService; _galleryService = galleryService;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_mediator = mediator; _mediator = mediator;
_apiClient = apiClient;
InitializeComponent(); InitializeComponent();
Loaded += OnLoaded; Loaded += OnLoaded;
@@ -67,7 +64,7 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
switch (args.Key) switch (args.Key)
{ {
case Key.Delete: case Key.Delete:
DeleteAnnotations(); await DeleteAnnotations();
break; break;
case Key.Enter: case Key.Enter:
await EditAnnotation(); await EditAnnotation();
@@ -79,19 +76,24 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
ThumbnailsView.SelectionChanged += (_, _) => ThumbnailsView.SelectionChanged += (_, _) =>
{ {
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}"; StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}";
ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast<AnnotationImageView>().Any(x => x.IsSeed) ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Any(x => x.IsSeed)
? Visibility.Visible ? Visibility.Visible
: Visibility.Hidden; : Visibility.Hidden;
}; };
ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentAnnotation!.Annotation.ImagePath); ExplorerEditor.GetTimeFunc = () => CurrentAnnotation!.Annotation.Time;
_galleryService.ThumbnailsUpdate += thumbnailsPercentage =>
{
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
};
Closing += (_, _) => _cts.Cancel();
} }
private async void OnLoaded(object sender, RoutedEventArgs e) private async void OnLoaded(object sender, RoutedEventArgs e)
{ {
AllAnnotationClasses = new ObservableCollection<DetectionClass>( AllDetectionClasses = new ObservableCollection<DetectionClass>(
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}} new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_annotationConfig.AnnotationClasses)); .Concat(_annotationConfig.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.ItemsSource = AllDetectionClasses;
LvClasses.MouseUp += async (_, _) => LvClasses.MouseUp += async (_, _) =>
{ {
@@ -128,9 +130,10 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
{ {
var allAnnotations = await db.Annotations var allAnnotations = await db.Annotations
.LoadWith(x => x.Detections) .LoadWith(x => x.Detections)
.OrderByDescending(x => x.CreatedDate) .OrderBy(x => x.AnnotationStatus)
.ThenByDescending(x => x.CreatedDate)
.ToListAsync(); .ToListAsync();
_annotationsDict = AllAnnotationClasses.ToDictionary(x => x.Id, _ => new List<Annotation>()); _annotationsDict = AllDetectionClasses.ToDictionary(x => x.Id, _ => new List<Annotation>());
foreach (var annotation in allAnnotations) foreach (var annotation in allAnnotations)
AddAnnotationToDict(annotation); AddAnnotationToDict(annotation);
@@ -150,15 +153,14 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
private async Task LoadClassDistribution() private async Task LoadClassDistribution()
{ {
var data = LabelsCache var data = _annotationsDict
.SelectMany(x => x.Value.Classes) .Where(x => x.Key != -1)
.GroupBy(x => x) .Select(gr => new
.Select(x => new
{ {
x.Key, gr.Key,
_annotationConfig.DetectionClassesDict[x.Key].Name, _annotationConfig.DetectionClassesDict[gr.Key].Name,
_annotationConfig.DetectionClassesDict[x.Key].Color, _annotationConfig.DetectionClassesDict[gr.Key].Color,
ClassCount = x.Count() ClassCount = gr.Value.Count
}) })
.ToList(); .ToList();
@@ -191,14 +193,20 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
ClassDistribution.Refresh(); ClassDistribution.Refresh();
} }
private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e) private async void RefreshThumbnailsBtnClick(object sender, RoutedEventArgs e)
{ {
RefreshThumbnailsButtonItem.Visibility = Visibility.Hidden;
RefreshProgressBarItem.Visibility = Visibility.Visible;
var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?", var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?",
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question); "Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
return; return;
_galleryService.ClearThumbnails(); await _galleryService.ClearThumbnails();
_galleryService.RefreshThumbnails(); await _galleryService.RefreshThumbnails();
RefreshProgressBarItem.Visibility = Visibility.Hidden;
RefreshThumbnailsButtonItem.Visibility = Visibility.Visible;
} }
private async Task EditAnnotation() private async Task EditAnnotation()
@@ -210,7 +218,7 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
if (ThumbnailsView.SelectedItem == null) if (ThumbnailsView.SelectedItem == null)
return; return;
CurrentAnnotation = (ThumbnailsView.SelectedItem as AnnotationImageView)!; CurrentAnnotation = (ThumbnailsView.SelectedItem as AnnotationThumbnail)!;
var ann = CurrentAnnotation.Annotation; var ann = CurrentAnnotation.Annotation;
ExplorerEditor.Background = new ImageBrush ExplorerEditor.Background = new ImageBrush
{ {
@@ -218,13 +226,13 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
}; };
SwitchTab(toEditor: true); SwitchTab(toEditor: true);
var time = Constants.GetTime(ann.ImagePath); var time = ann.Time;
ExplorerEditor.RemoveAllAnns(); ExplorerEditor.RemoveAllAnns();
foreach (var deetection in ann.Detections) foreach (var deetection in ann.Detections)
{ {
var annClass = _annotationConfig.DetectionClassesDict[deetection.ClassNumber]; var annClass = _annotationConfig.DetectionClassesDict[deetection.ClassNumber];
var canvasLabel = new CanvasLabel(deetection, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize); var canvasLabel = new CanvasLabel(deetection, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel); ExplorerEditor.CreateDetectionControl(annClass, time, canvasLabel);
} }
ThumbnailLoading = false; ThumbnailLoading = false;
@@ -257,26 +265,23 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
{ {
AnnotationsTab.Visibility = Visibility.Visible; AnnotationsTab.Visibility = Visibility.Visible;
EditorTab.Visibility = Visibility.Collapsed; EditorTab.Visibility = Visibility.Collapsed;
LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.ItemsSource = AllDetectionClasses;
LvClasses.SelectedIndex = _tempSelectedClassIdx; LvClasses.SelectedIndex = _tempSelectedClassIdx;
Switcher.SelectedIndex = 0; Switcher.SelectedIndex = 0;
} }
} }
private void DeleteAnnotations() private async Task DeleteAnnotations()
{ {
var tempSelected = ThumbnailsView.SelectedIndex; var tempSelected = ThumbnailsView.SelectedIndex;
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question); var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
return; return;
var selected = ThumbnailsView.SelectedItems.Count; var annotations = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Select(x => x.Annotation)
for (var i = 0; i < selected; i++) .ToList();
{
var dto = (ThumbnailsView.SelectedItems[0] as AnnotationImageView)!; await _mediator.Publish(new AnnotationsDeletedEvent(annotations));
dto.Delete();
SelectedAnnotations.Remove(dto);
}
ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected); ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected);
} }
@@ -284,7 +289,11 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
{ {
SelectedAnnotations.Clear(); SelectedAnnotations.Clear();
foreach (var ann in _annotationsDict[ExplorerEditor.CurrentAnnClass.Id]) foreach (var ann in _annotationsDict[ExplorerEditor.CurrentAnnClass.Id])
SelectedAnnotations.Add(new AnnotationImageView(ann)); {
var annThumb = new AnnotationThumbnail(ann);
SelectedAnnotations.Add(annThumb);
_selectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
}
} }
public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken)
@@ -298,15 +307,36 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
AddAnnotationToDict(annotation); AddAnnotationToDict(annotation);
if (annotation.Classes.Contains(selectedClass.Value)) if (annotation.Classes.Contains(selectedClass.Value))
{ {
SelectedAnnotations.Add(new AnnotationImageView(annotation)); var annThumb = new AnnotationThumbnail(annotation);
SelectedAnnotations.Add(annThumb);
_selectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
}
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{
var names = notification.Annotations.Select(x => x.Name).ToList();
var annThumbs = _selectedAnnotationDict
.Where(x => names.Contains(x.Key))
.Select(x => x.Value)
.ToList();
foreach (var annThumb in annThumbs)
{
SelectedAnnotations.Remove(annThumb);
_selectedAnnotationDict.Remove(annThumb.Annotation.Name);
} }
} }
private async void ValidateAnnotationsClick(object sender, RoutedEventArgs e) private async void ValidateAnnotationsClick(object sender, RoutedEventArgs e)
{ {
var result = MessageBox.Show("Підтверджуєте валідність обраних аннотацій?","Підтвердження валідності", MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (result != MessageBoxResult.OK)
return;
try try
{ {
await _mediator.Publish(new DatasetExplorerControlEvent(PlaybackControlEnum.ValidateAnnotations)); await _mediator.Publish(new DatasetExplorerControlEvent(PlaybackControlEnum.ValidateAnnotations), _cts.Token);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -2,6 +2,7 @@
using System.Windows.Input; using System.Windows.Input;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Events;
using Azaion.Common.Services; using Azaion.Common.Services;
using MediatR; using MediatR;
@@ -19,7 +20,7 @@ public class DatasetExplorerEventHandler(
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns }, { Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns }, { Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.Escape, PlaybackControlEnum.Close }, { Key.Escape, PlaybackControlEnum.Close },
{ Key.A, PlaybackControlEnum.ValidateAnnotations} { Key.V, PlaybackControlEnum.ValidateAnnotations}
}; };
public async Task Handle(DatasetExplorerControlEvent notification, CancellationToken cancellationToken) public async Task Handle(DatasetExplorerControlEvent notification, CancellationToken cancellationToken)
@@ -74,13 +75,11 @@ public class DatasetExplorerEventHandler(
datasetExplorer.SwitchTab(toEditor: false); datasetExplorer.SwitchTab(toEditor: false);
break; break;
case PlaybackControlEnum.ValidateAnnotations: case PlaybackControlEnum.ValidateAnnotations:
var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast<AnnotationImageView>() var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>()
.Select(x => x.Annotation) .Select(x => x.Annotation)
.ToList(); .ToList();
foreach (var annotation in annotations) foreach (var annotation in annotations)
{
await annotationService.ValidateAnnotation(annotation, cancellationToken); await annotationService.ValidateAnnotation(annotation, cancellationToken);
}
break; break;
} }
} }
+2
View File
@@ -6,6 +6,7 @@ using Azaion.Annotator.Extensions;
using Azaion.Common.Database; 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.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.CommonSecurity; using Azaion.CommonSecurity;
@@ -83,6 +84,7 @@ public partial class App
.AddCommandLine(Environment.GetCommandLineArgs()) .AddCommandLine(Environment.GetCommandLineArgs())
.AddJsonFile(SecurityConstants.CONFIG_PATH, optional: true, reloadOnChange: true) .AddJsonFile(SecurityConstants.CONFIG_PATH, optional: true, reloadOnChange: true)
.AddJsonStream(_securedConfig)) .AddJsonStream(_securedConfig))
.UseSerilog()
.ConfigureServices((context, services) => .ConfigureServices((context, services) =>
{ {
services.AddSingleton<MainSuite>(); services.AddSingleton<MainSuite>();
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"ApiConfig": { "ApiConfig": {
"Url": "https://api.azaion.com/", "Url": "https://api.azaion.com/",
"RetryCount": 3, "RetryCount": 3,