mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 11:26:31 +00:00
add db WIP 2, 80%
refactor, renames
This commit is contained in:
@@ -176,11 +176,11 @@
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
<controls1:AnnotationClasses
|
||||
<controls1:DetectionClasses
|
||||
x:Name="LvClasses"
|
||||
Grid.Column="0"
|
||||
Grid.Row="4">
|
||||
</controls1:AnnotationClasses>
|
||||
</controls1:DetectionClasses>
|
||||
|
||||
<GridSplitter
|
||||
Background="DarkGray"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -8,13 +7,14 @@ using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.DTO.Queue;
|
||||
using Azaion.Common.Extensions;
|
||||
using Azaion.Common.Services;
|
||||
using LibVLCSharp.Shared;
|
||||
using MediatR;
|
||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
@@ -40,9 +40,10 @@ public partial class Annotator
|
||||
private readonly ILogger<Annotator> _logger;
|
||||
private readonly VLCFrameExtractor _vlcFrameExtractor;
|
||||
private readonly IAIDetector _aiDetector;
|
||||
private readonly AnnotationService _annotationService;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
|
||||
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
|
||||
private bool _suspendLayout;
|
||||
|
||||
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
|
||||
@@ -52,7 +53,7 @@ public partial class Annotator
|
||||
private ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
|
||||
private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
|
||||
|
||||
public IntervalTree<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new();
|
||||
public IntervalTree<TimeSpan, List<Detection>> Detections { get; set; } = new();
|
||||
private AutodetectDialog _autoDetectDialog = new() { Topmost = true };
|
||||
|
||||
public Annotator(
|
||||
@@ -64,7 +65,8 @@ public partial class Annotator
|
||||
HelpWindow helpWindow,
|
||||
ILogger<Annotator> logger,
|
||||
VLCFrameExtractor vlcFrameExtractor,
|
||||
IAIDetector aiDetector)
|
||||
IAIDetector aiDetector,
|
||||
AnnotationService annotationService)
|
||||
{
|
||||
InitializeComponent();
|
||||
_appConfig = appConfig.Value;
|
||||
@@ -77,6 +79,7 @@ public partial class Annotator
|
||||
_logger = logger;
|
||||
_vlcFrameExtractor = vlcFrameExtractor;
|
||||
_aiDetector = aiDetector;
|
||||
_annotationService = annotationService;
|
||||
|
||||
Loaded += OnLoaded;
|
||||
Closed += OnFormClosed;
|
||||
@@ -98,7 +101,7 @@ public partial class Annotator
|
||||
|
||||
ReloadFiles();
|
||||
|
||||
AnnotationClasses = new ObservableCollection<AnnotationClass>(_appConfig.AnnotationConfig.AnnotationClasses);
|
||||
AnnotationClasses = new ObservableCollection<DetectionClass>(_appConfig.AnnotationConfig.AnnotationClasses);
|
||||
LvClasses.ItemsSource = AnnotationClasses;
|
||||
LvClasses.SelectedIndex = 0;
|
||||
|
||||
@@ -152,7 +155,7 @@ public partial class Annotator
|
||||
|
||||
LvClasses.SelectionChanged += (_, _) =>
|
||||
{
|
||||
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
|
||||
var selectedClass = (DetectionClass)LvClasses.SelectedItem;
|
||||
Editor.CurrentAnnClass = selectedClass;
|
||||
_mediator.Publish(new AnnClassSelectedEvent(selectedClass));
|
||||
};
|
||||
@@ -188,7 +191,7 @@ public partial class Annotator
|
||||
OpenAnnotationResult((AnnotationResult)DgAnnotations.SelectedItem);
|
||||
break;
|
||||
case Key.Delete:
|
||||
var result = MessageBox.Show(Application.Current.MainWindow, "Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.OK)
|
||||
return;
|
||||
|
||||
@@ -202,7 +205,7 @@ public partial class Annotator
|
||||
File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt"));
|
||||
File.Delete(thumbnailPath);
|
||||
_formState.AnnotationResults.Remove(annotationResult);
|
||||
Annotations.Remove(Annotations.Query(annotationResult.Time));
|
||||
Detections.Remove(Detections.Query(annotationResult.Time));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -251,7 +254,7 @@ public partial class Annotator
|
||||
Editor.ClearExpiredAnnotations(time);
|
||||
});
|
||||
|
||||
var annotations = Annotations.Query(time).SelectMany(x => x).Select(x => new Detection(x));
|
||||
var annotations = Detections.Query(time).SelectMany(x => x).Select(x => new Detection(_formState.GetTimeName(time), x));
|
||||
AddAnnotationsToCanvas(time, annotations);
|
||||
}
|
||||
|
||||
@@ -285,7 +288,7 @@ public partial class Annotator
|
||||
private async Task ReloadAnnotations(CancellationToken ct = default)
|
||||
{
|
||||
_formState.AnnotationResults.Clear();
|
||||
Annotations.Clear();
|
||||
Detections.Clear();
|
||||
Editor.RemoveAllAnns();
|
||||
|
||||
var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory);
|
||||
@@ -294,22 +297,21 @@ public partial class Annotator
|
||||
|
||||
var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_??????.txt");
|
||||
foreach (var file in labelFiles)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(file.Name);
|
||||
var time = Constants.GetTime(name);
|
||||
await AddAnnotations(time, await YoloLabel.ReadFromFile(file.FullName, ct), ct);
|
||||
}
|
||||
await AddAnnotations(Path.GetFileNameWithoutExtension(file.Name), await YoloLabel.ReadFromFile(file.FullName, ct), ct);
|
||||
}
|
||||
|
||||
public async Task AddAnnotations(TimeSpan? time, List<YoloLabel> annotations, CancellationToken ct = default)
|
||||
=> await AddAnnotations(time, annotations.Select(x => new Detection(x)).ToList(), ct);
|
||||
//Load from yolo label file
|
||||
public async Task AddAnnotations(string name, List<YoloLabel> annotations, CancellationToken ct = default)
|
||||
=> await AddAnnotations(name, annotations.Select(x => new Detection(name, x)).ToList(), ct);
|
||||
|
||||
public async Task AddAnnotations(TimeSpan? time, List<Detection> detections, CancellationToken ct = default)
|
||||
//Add manually
|
||||
public async Task AddAnnotations(string name, List<Detection> detections, CancellationToken ct = default)
|
||||
{
|
||||
var time = Constants.GetTime(name);
|
||||
var timeValue = time ?? TimeSpan.FromMinutes(0);
|
||||
var previousAnnotations = Annotations.Query(timeValue);
|
||||
Annotations.Remove(previousAnnotations);
|
||||
Annotations.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections.Cast<YoloLabel>().ToList());
|
||||
var previousAnnotations = Detections.Query(timeValue);
|
||||
Detections.Remove(previousAnnotations);
|
||||
Detections.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections);
|
||||
|
||||
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
|
||||
if (existingResult != null)
|
||||
@@ -345,15 +347,15 @@ public partial class Annotator
|
||||
return (-1).ToColor();
|
||||
|
||||
return colorNumber >= detectionClasses.Count
|
||||
? _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses.LastOrDefault()].Color
|
||||
: _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses[colorNumber]].Color;
|
||||
? _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.LastOrDefault()].Color
|
||||
: _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses[colorNumber]].Color;
|
||||
}
|
||||
|
||||
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
|
||||
|
||||
annotationResult.ClassName = detectionClasses.Count > 1
|
||||
? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.AnnotationClassesDict[x].ShortName))
|
||||
: _appConfig.AnnotationConfig.AnnotationClassesDict[detectionClasses.FirstOrDefault()].Name;
|
||||
? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.DetectionClassesDict[x].ShortName))
|
||||
: _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.FirstOrDefault()].Name;
|
||||
|
||||
annotationResult.ClassColor0 = GetAnnotationClass(detectionClasses, 0);
|
||||
annotationResult.ClassColor1 = GetAnnotationClass(detectionClasses, 1);
|
||||
@@ -442,8 +444,8 @@ public partial class Annotator
|
||||
// private void AddClassBtnClick(object sender, RoutedEventArgs e)
|
||||
// {
|
||||
// LvClasses.IsReadOnly = false;
|
||||
// AnnotationClasses.Add(new AnnotationClass(AnnotationClasses.Count));
|
||||
// LvClasses.SelectedIndex = AnnotationClasses.Count - 1;
|
||||
// DetectionClasses.Add(new DetectionClass(DetectionClasses.Count));
|
||||
// LvClasses.SelectedIndex = DetectionClasses.Count - 1;
|
||||
// }
|
||||
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
|
||||
private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
|
||||
@@ -578,9 +580,10 @@ public partial class Annotator
|
||||
{
|
||||
try
|
||||
{
|
||||
var fName = Path.GetFileNameWithoutExtension(mediaInfo.Path);
|
||||
var stream = new FileStream(mediaInfo.Path, FileMode.Open);
|
||||
var detections = await _aiDetector.Detect(stream, token);
|
||||
await ProcessDetection((TimeSpan.FromMilliseconds(0), stream), detections, token);
|
||||
var detections = await _aiDetector.Detect(fName, stream, token);
|
||||
await ProcessDetection((TimeSpan.FromMilliseconds(0), stream), Path.GetExtension(mediaInfo.Path), detections, token);
|
||||
if (detections.Count != 0)
|
||||
mediaInfo.HasAnnotations = true;
|
||||
}
|
||||
@@ -599,7 +602,8 @@ public partial class Annotator
|
||||
Console.WriteLine($"Detect time: {timeframe.Time}");
|
||||
try
|
||||
{
|
||||
var detections = await _aiDetector.Detect(timeframe.Stream, token);
|
||||
var fName = _formState.GetTimeName(timeframe.Time);
|
||||
var detections = await _aiDetector.Detect(fName, timeframe.Stream, token);
|
||||
var isValid = IsValidDetection(timeframe.Time, detections);
|
||||
|
||||
if (timeframe.Time.TotalSeconds > prevSeekTime + 1)
|
||||
@@ -628,7 +632,7 @@ public partial class Annotator
|
||||
continue;
|
||||
|
||||
mediaInfo.HasAnnotations = true;
|
||||
await ProcessDetection(timeframe, detections, token);
|
||||
await ProcessDetection(timeframe, "jpg", detections, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -685,7 +689,7 @@ public partial class Annotator
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, List<Detection> detections, CancellationToken token = default)
|
||||
private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, string imageExtension, List<Detection> detections, CancellationToken token = default)
|
||||
{
|
||||
_previousDetection = (timeframe.Time, detections);
|
||||
await Dispatcher.Invoke(async () =>
|
||||
@@ -695,22 +699,24 @@ public partial class Annotator
|
||||
var time = timeframe.Time;
|
||||
|
||||
var fName = _formState.GetTimeName(timeframe.Time);
|
||||
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg");
|
||||
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.{imageExtension}");
|
||||
|
||||
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
|
||||
Editor.RemoveAllAnns();
|
||||
AddAnnotationsToCanvas(time, detections, true);
|
||||
await AddAnnotations(timeframe.Time, detections, token);
|
||||
await AddAnnotations(fName, detections, token);
|
||||
|
||||
await _annotationService.SaveAnnotation(fName, imageExtension, detections, SourceEnum.AI, timeframe.Stream, token);
|
||||
|
||||
var log = string.Join(Environment.NewLine, detections.Select(det =>
|
||||
$"{_appConfig.AnnotationConfig.AnnotationClassesDict[det.ClassNumber].Name}: " +
|
||||
$"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
|
||||
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
|
||||
$"size=({det.Width:F2}, {det.Height:F2}), " +
|
||||
$"prob: {det.Probability:F1}%"));
|
||||
|
||||
Dispatcher.Invoke(() => _autoDetectDialog.Log(log));
|
||||
|
||||
await _mediator.Publish(new ImageCreatedEvent(imgPath), token);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -21,7 +21,6 @@ public class AnnotatorEventHandler(
|
||||
Annotator mainWindow,
|
||||
FormState formState,
|
||||
AnnotationService annotationService,
|
||||
IMediator mediator,
|
||||
ILogger<AnnotatorEventHandler> logger,
|
||||
IOptions<DirectoriesConfig> dirConfig)
|
||||
:
|
||||
@@ -48,15 +47,15 @@ public class AnnotatorEventHandler(
|
||||
|
||||
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
SelectClass(notification.AnnotationClass);
|
||||
SelectClass(notification.DetectionClass);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void SelectClass(AnnotationClass annClass)
|
||||
private void SelectClass(DetectionClass annClass)
|
||||
{
|
||||
mainWindow.Editor.CurrentAnnClass = annClass;
|
||||
foreach (var ann in mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected))
|
||||
ann.AnnotationClass = annClass;
|
||||
foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected))
|
||||
ann.DetectionClass = annClass;
|
||||
mainWindow.LvClasses.SelectedIndex = annClass.Id;
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ public class AnnotatorEventHandler(
|
||||
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
|
||||
keyNumber = key - Key.NumPad1;
|
||||
if (keyNumber.HasValue)
|
||||
SelectClass((AnnotationClass)mainWindow.LvClasses.Items[keyNumber.Value]!);
|
||||
SelectClass((DetectionClass)mainWindow.LvClasses.Items[keyNumber.Value]!);
|
||||
|
||||
if (_keysControlEnumDict.TryGetValue(key, out var value))
|
||||
await ControlPlayback(value, cancellationToken);
|
||||
@@ -144,7 +143,7 @@ public class AnnotatorEventHandler(
|
||||
mainWindow.SeekTo(mediaPlayer.Time + step);
|
||||
break;
|
||||
case PlaybackControlEnum.SaveAnnotations:
|
||||
await SaveAnnotations();
|
||||
await SaveAnnotations(cancellationToken);
|
||||
break;
|
||||
case PlaybackControlEnum.RemoveSelectedAnns:
|
||||
|
||||
@@ -229,18 +228,19 @@ public class AnnotatorEventHandler(
|
||||
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||
var fName = formState.GetTimeName(time);
|
||||
|
||||
var currentAnns = mainWindow.Editor.CurrentAnns
|
||||
.Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))
|
||||
var currentDetections = mainWindow.Editor.CurrentDetections
|
||||
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
|
||||
.ToList();
|
||||
|
||||
await mainWindow.AddAnnotations(time, currentAnns, cancellationToken);
|
||||
await mainWindow.AddAnnotations(fName, currentDetections, cancellationToken);
|
||||
|
||||
formState.CurrentMedia.HasAnnotations = mainWindow.Annotations.Count != 0;
|
||||
formState.CurrentMedia.HasAnnotations = mainWindow.Detections.Count != 0;
|
||||
mainWindow.LvFiles.Items.Refresh();
|
||||
mainWindow.Editor.RemoveAllAnns();
|
||||
|
||||
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
|
||||
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}");
|
||||
var imageExtension = isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path);
|
||||
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{imageExtension}");
|
||||
|
||||
if (isVideo)
|
||||
{
|
||||
@@ -267,8 +267,6 @@ public class AnnotatorEventHandler(
|
||||
File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true);
|
||||
NextMedia();
|
||||
}
|
||||
await annotationService.SaveAnnotation(fName, currentAnns, SourceEnum.Manual, token: cancellationToken);
|
||||
|
||||
await mediator.Publish(new ImageCreatedEvent(imgPath), cancellationToken);
|
||||
await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using MediatR;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
|
||||
public class AnnClassSelectedEvent(AnnotationClass annotationClass) : INotification
|
||||
public class AnnClassSelectedEvent(DetectionClass detectionClass) : INotification
|
||||
{
|
||||
public AnnotationClass AnnotationClass { get; } = annotationClass;
|
||||
public DetectionClass DetectionClass { get; } = detectionClass;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
|
||||
public static class BitmapExtensions
|
||||
{
|
||||
public static async Task<BitmapImage> OpenImage(this string imagePath)
|
||||
{
|
||||
var image = new BitmapImage();
|
||||
await using var stream = File.OpenRead(imagePath);
|
||||
image.BeginInit();
|
||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||
image.StreamSource = stream;
|
||||
image.EndInit();
|
||||
image.Freeze();
|
||||
return image;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Azaion.Annotator;
|
||||
|
||||
public interface IAIDetector
|
||||
{
|
||||
Task<List<Detection>> Detect(Stream imageStream, CancellationToken cancellationToken = default);
|
||||
Task<List<Detection>> Detect(string fName, Stream imageStream, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IResourceLoader resourceLoader) : IAIDetector, IDisposable
|
||||
@@ -25,7 +25,7 @@ public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IReso
|
||||
private const string YOLO_MODEL = "azaion.onnx";
|
||||
|
||||
|
||||
public async Task<List<Detection>> Detect(Stream imageStream, CancellationToken cancellationToken)
|
||||
public async Task<List<Detection>> Detect(string fName, Stream imageStream, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_predictor == null)
|
||||
{
|
||||
@@ -42,7 +42,7 @@ public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IReso
|
||||
var detections = result.Select(d =>
|
||||
{
|
||||
var label = new YoloLabel(new CanvasLabel(d.Name.Id, d.Bounds.X, d.Bounds.Y, d.Bounds.Width, d.Bounds.Height), imageSize, imageSize);
|
||||
return new Detection(label, (double?)d.Confidence * 100);
|
||||
return new Detection(fName, label, (double?)d.Confidence * 100);
|
||||
}).ToList();
|
||||
|
||||
return FilterOverlapping(detections);
|
||||
|
||||
Reference in New Issue
Block a user