mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 22:46:30 +00:00
remove fix, todo: test
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
-1
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"ApiConfig": {
|
"ApiConfig": {
|
||||||
"Url": "https://api.azaion.com/",
|
"Url": "https://api.azaion.com/",
|
||||||
"RetryCount": 3,
|
"RetryCount": 3,
|
||||||
|
|||||||
Reference in New Issue
Block a user