fix editing annotation

This commit is contained in:
Alex Bezdieniezhnykh
2024-09-13 17:39:53 +03:00
parent 52371ace3a
commit 42fdee599e
11 changed files with 171 additions and 67 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ public partial class App : Application
}); });
services.AddSingleton<IConfigRepository, FileConfigRepository>(); services.AddSingleton<IConfigRepository, FileConfigRepository>();
services.AddSingleton<Config>(sp => sp.GetRequiredService<IConfigRepository>().Get()); services.AddSingleton<Config>(sp => sp.GetRequiredService<IConfigRepository>().Get());
services.AddSingleton<PlayerControlHandler>(); services.AddSingleton<MainWindowEventHandler>();
}) })
.UseSerilog() .UseSerilog()
.Build(); .Build();
+1 -1
View File
@@ -37,7 +37,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource> </Resource>
<None Update="config.json"> <None Update="config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
+1
View File
@@ -36,6 +36,7 @@ public class Config
public List<string> ImageFormats { get; set; } public List<string> ImageFormats { get; set; }
public ThumbnailConfig ThumbnailConfig { get; set; } public ThumbnailConfig ThumbnailConfig { get; set; }
public int? LastSelectedExplorerClass { get; set; }
} }
public class WindowConfig public class WindowConfig
+1
View File
@@ -17,6 +17,7 @@ public class FormState
public int CurrentVolume { get; set; } = 100; public int CurrentVolume { get; set; } = 100;
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = []; public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
public WindowsEnum ActiveWindow { get; set; }
public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}"; public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}";
+8
View File
@@ -0,0 +1,8 @@
namespace Azaion.Annotator.DTO;
public enum WindowsEnum
{
None = 0,
Main = 10,
DatasetExplorer = 20
}
+50 -39
View File
@@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
@@ -24,16 +25,23 @@ public partial class DatasetExplorer
private readonly string _thumbnailsCacheFile; private readonly string _thumbnailsCacheFile;
private IConfigRepository _configRepository; private IConfigRepository _configRepository;
private readonly FormState _formState; private readonly FormState _formState;
private readonly IGalleryManager _galleryManager;
private static Dictionary<string, List<int>> LabelsCache { get; set; } = new(); private static Dictionary<string, List<int>> LabelsCache { get; set; } = new();
public string CurrentImage { get; set; } public ThumbnailDto? CurrentThumbnail { get; set; }
public DatasetExplorer(Config config, ILogger<DatasetExplorer> logger, IConfigRepository configRepository, FormState formState) public DatasetExplorer(
Config config,
ILogger<DatasetExplorer> logger,
IConfigRepository configRepository,
FormState formState,
IGalleryManager galleryManager)
{ {
_config = config; _config = config;
_logger = logger; _logger = logger;
_configRepository = configRepository; _configRepository = configRepository;
_formState = formState; _formState = formState;
_galleryManager = galleryManager;
_thumbnailsCacheFile = Path.Combine(config.ThumbnailsDirectory, Config.ThumbnailsCacheFile); _thumbnailsCacheFile = Path.Combine(config.ThumbnailsDirectory, Config.ThumbnailsCacheFile);
if (File.Exists(_thumbnailsCacheFile)) if (File.Exists(_thumbnailsCacheFile))
{ {
@@ -43,19 +51,45 @@ public partial class DatasetExplorer
InitializeComponent(); InitializeComponent();
DataContext = this; DataContext = this;
Loaded += (_, _) => Loaded += async (_, _) =>
{ {
AllAnnotationClasses = new ObservableCollection<AnnotationClass>( AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
new List<AnnotationClass> { new(-1, "All") } new List<AnnotationClass> { new(-1, "All") }
.Concat(_config.AnnotationClasses)); .Concat(_config.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.SelectionChanged += async (_, _) => LvClasses.MouseUp += async (_, _) =>
{ {
var selectedClass = (AnnotationClass)LvClasses.SelectedItem; var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
await SelectClass(selectedClass); ExplorerEditor.CurrentAnnClass = selectedClass;
config.LastSelectedExplorerClass = selectedClass.Id;
if (Switcher.SelectedIndex == 0)
await ReloadThumbnails();
else
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = selectedClass;
}; };
LvClasses.SelectedIndex = 0;
LvClasses.SelectionChanged += (_, _) =>
{
if (Switcher.SelectedIndex != 1)
return;
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
if (selectedClass == null)
return;
ExplorerEditor.CurrentAnnClass = selectedClass;
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = selectedClass;
};
LvClasses.SelectedIndex = config.LastSelectedExplorerClass ?? 0;
ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem;
await ReloadThumbnails();
SizeChanged += async (_, _) => await SaveUserSettings(); SizeChanged += async (_, _) => await SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings(); LocationChanged += async (_, _) => await SaveUserSettings();
@@ -68,7 +102,7 @@ public partial class DatasetExplorer
Visibility = Visibility.Hidden; Visibility = Visibility.Hidden;
}; };
ThumbnailsView.KeyDown += (sender, args) => ThumbnailsView.KeyDown += async (sender, args) =>
{ {
switch (args.Key) switch (args.Key)
{ {
@@ -76,29 +110,15 @@ public partial class DatasetExplorer
DeleteAnnotations(); DeleteAnnotations();
break; break;
case Key.Enter: case Key.Enter:
EditAnnotation(); await EditAnnotation();
break; break;
} }
}; };
ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation(); ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation();
ExplorerEditor.KeyDown += (_, args) => Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.DatasetExplorer; };
{
var key = args.Key;
var keyNumber = (int?)null;
if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9) ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath)!.Value;
keyNumber = key - Key.D1;
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1;
if (!keyNumber.HasValue)
return;
LvClasses.SelectedIndex = keyNumber.Value;
};
ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentImage!)!.Value;
} }
private async Task EditAnnotation() private async Task EditAnnotation()
@@ -111,11 +131,12 @@ public partial class DatasetExplorer
{ {
ImageSource = new BitmapImage(new Uri(dto.ImagePath)) ImageSource = new BitmapImage(new Uri(dto.ImagePath))
}; };
CurrentImage = dto.ImagePath; CurrentThumbnail = dto;
Switcher.SelectedIndex = 1; Switcher.SelectedIndex = 1;
LvClasses.SelectedIndex = 1; LvClasses.SelectedIndex = 1;
var time = _formState.GetTime(CurrentImage)!.Value; var time = _formState.GetTime(dto.ImagePath)!.Value;
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{ {
var annClass = _config.AnnotationClasses[ann.ClassNumber]; var annClass = _config.AnnotationClasses[ann.ClassNumber];
@@ -123,33 +144,23 @@ public partial class DatasetExplorer
Dispatcher.Invoke(() => ExplorerEditor.CreateAnnotation(annClass, time, annInfo)); Dispatcher.Invoke(() => ExplorerEditor.CreateAnnotation(annClass, time, annInfo));
} }
Switcher.SelectionChanged += (_, args) => Switcher.SelectionChanged += (sender, args) =>
{ {
//From Explorer to Editor
if (Switcher.SelectedIndex == 1) if (Switcher.SelectedIndex == 1)
{ {
//Editor
_tempSelectedClassIdx = LvClasses.SelectedIndex; _tempSelectedClassIdx = LvClasses.SelectedIndex;
LvClasses.ItemsSource = _config.AnnotationClasses; LvClasses.ItemsSource = _config.AnnotationClasses;
} }
else else
{ {
//Explorer
LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.SelectedIndex = _tempSelectedClassIdx; LvClasses.SelectedIndex = _tempSelectedClassIdx;
} }
}; };
} }
private async Task SelectClass(AnnotationClass annClass)
{
ExplorerEditor.CurrentAnnClass = annClass;
if (Switcher.SelectedIndex == 0)
await ReloadThumbnails();
else
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = annClass;
}
private async Task SaveUserSettings() private async Task SaveUserSettings()
{ {
_config.DatasetExplorerConfig = this.GetConfig(); _config.DatasetExplorerConfig = this.GetConfig();
@@ -0,0 +1,62 @@
using System.IO;
using System.Windows.Input;
using Azaion.Annotator.DTO;
using MediatR;
namespace Azaion.Annotator;
public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer,
Config config,
IGalleryManager galleryManager,
FormState formState) : INotificationHandler<KeyEvent>
{
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns }
};
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
{
if (formState.ActiveWindow != WindowsEnum.DatasetExplorer)
return;
var key = keyEvent.Args.Key;
var keyNumber = (int?)null;
if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9) keyNumber = key - Key.D1;
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue)
datasetExplorer.LvClasses.SelectedIndex = keyNumber.Value;
else
{
if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value))
await HandleControl(value);
}
}
private async Task HandleControl(PlaybackControlEnum controlEnum)
{
switch (controlEnum)
{
case PlaybackControlEnum.SaveAnnotations:
var currentAnns = datasetExplorer.ExplorerEditor.CurrentAnns
.Select(x => new YoloLabel(x.Info, datasetExplorer.ExplorerEditor.RenderSize, datasetExplorer.ExplorerEditor.RenderSize))
.ToList();
await YoloLabel.WriteToFile(currentAnns, Path.Combine(config.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath));
await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath);
datasetExplorer.Switcher.SelectedIndex = 0;
break;
case PlaybackControlEnum.RemoveSelectedAnns:
datasetExplorer.ExplorerEditor.RemoveSelectedAnns();
break;
case PlaybackControlEnum.RemoveAllAnns:
datasetExplorer.ExplorerEditor.RemoveAllAnns();
break;
}
}
}
@@ -10,9 +10,9 @@ public static class ThrottleExt
_throttleOn = true; _throttleOn = true;
await func(); await func();
_ = Task.Run(() => _ = Task.Run(async () =>
{ {
Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500)); await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500));
_throttleOn = false; _throttleOn = false;
}); });
} }
+32 -16
View File
@@ -14,16 +14,27 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
public int ThumbnailsCount { get; set; } public int ThumbnailsCount { get; set; }
public int ImagesCount { get; set; } public int ImagesCount { get; set; }
public async Task RefreshThumbnails() private DirectoryInfo? _thumbnailsDirectory;
private DirectoryInfo ThumbnailsDirectory
{ {
get
{
if (_thumbnailsDirectory != null)
return _thumbnailsDirectory;
var dir = new DirectoryInfo(config.ThumbnailsDirectory); var dir = new DirectoryInfo(config.ThumbnailsDirectory);
if (!dir.Exists) if (!dir.Exists)
Directory.CreateDirectory(config.ThumbnailsDirectory); Directory.CreateDirectory(config.ThumbnailsDirectory);
_thumbnailsDirectory = new DirectoryInfo(config.ThumbnailsDirectory);
return _thumbnailsDirectory;
}
}
public async Task RefreshThumbnails()
{
var prefixLen = Config.ThumbnailPrefix.Length; var prefixLen = Config.ThumbnailPrefix.Length;
var thumbnailsDir = new DirectoryInfo(config.ThumbnailsDirectory);
var thumbnails = thumbnailsDir.GetFiles() var thumbnails = ThumbnailsDirectory.GetFiles()
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen]) .Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
.GroupBy(x => x) .GroupBy(x => x)
.Select(gr => gr.Key) .Select(gr => gr.Key)
@@ -38,15 +49,9 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
var imgName = Path.GetFileNameWithoutExtension(img.Name); var imgName = Path.GetFileNameWithoutExtension(img.Name);
if (thumbnails.Contains(imgName)) if (thumbnails.Contains(imgName))
continue; continue;
try try
{ {
var bitmap = await GenerateThumbnail(img); await CreateThumbnail(img.FullName);
if (bitmap != null)
{
var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg");
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -57,15 +62,25 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
} }
} }
private async Task<Bitmap?> GenerateThumbnail(FileInfo img) public async Task CreateThumbnail(string imgPath)
{
var bitmap = await GenerateThumbnail(imgPath);
if (bitmap != null)
{
var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Config.ThumbnailPrefix}.jpg");
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
}
}
private async Task<Bitmap?> GenerateThumbnail(string imgPath)
{ {
var width = (int)config.ThumbnailConfig.Size.Width; var width = (int)config.ThumbnailConfig.Size.Width;
var height = (int)config.ThumbnailConfig.Size.Height; var height = (int)config.ThumbnailConfig.Size.Height;
var imgName = Path.GetFileNameWithoutExtension(img.Name); var imgName = Path.GetFileName(imgPath);
var labelName = Path.Combine(config.LabelsDirectory, $"{imgName}.txt"); var labelName = Path.Combine(config.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt");
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(img.FullName))); var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath)));
var bitmap = new Bitmap(width, height); var bitmap = new Bitmap(width, height);
@@ -77,8 +92,8 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
var size = new Size(originalImage.Width, originalImage.Height); var size = new Size(originalImage.Width, originalImage.Height);
if (!File.Exists(labelName)) if (!File.Exists(labelName))
{ {
File.Move(img.FullName, Path.Combine(config.UnknownImages, Path.GetFileName(img.Name))); File.Move(imgPath, Path.Combine(config.UnknownImages, imgName));
logger.LogInformation($"No labels found for image {img.Name}! Moved image to the {config.UnknownImages} folder."); logger.LogInformation($"No labels found for image {imgName}! Moved image to the {config.UnknownImages} folder.");
return null; return null;
} }
var labels = (await YoloLabel.ReadFromFile(labelName)) var labels = (await YoloLabel.ReadFromFile(labelName))
@@ -139,5 +154,6 @@ public interface IGalleryManager
{ {
int ThumbnailsCount { get; set; } int ThumbnailsCount { get; set; }
int ImagesCount { get; set; } int ImagesCount { get; set; }
Task CreateThumbnail(string imgPath);
Task RefreshThumbnails(); Task RefreshThumbnails();
} }
+2
View File
@@ -75,6 +75,8 @@ public partial class MainWindow
Directory.CreateDirectory(_config.ResultsDirectory); Directory.CreateDirectory(_config.ResultsDirectory);
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.Main; };
} }
private void VideoView_Loaded(object sender, RoutedEventArgs e) private void VideoView_Loaded(object sender, RoutedEventArgs e)
@@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public class PlayerControlHandler : public class MainWindowEventHandler :
INotificationHandler<KeyEvent>, INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>, INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>, INotificationHandler<PlaybackControlEvent>,
@@ -19,7 +19,7 @@ public class PlayerControlHandler :
private readonly MainWindow _mainWindow; private readonly MainWindow _mainWindow;
private readonly FormState _formState; private readonly FormState _formState;
private readonly Config _config; private readonly Config _config;
private readonly ILogger<PlayerControlHandler> _logger; private readonly ILogger<MainWindowEventHandler> _logger;
private const int STEP = 20; private const int STEP = 20;
private const int LARGE_STEP = 5000; private const int LARGE_STEP = 5000;
@@ -37,12 +37,12 @@ public class PlayerControlHandler :
{ Key.PageDown, PlaybackControlEnum.Next }, { Key.PageDown, PlaybackControlEnum.Next },
}; };
public PlayerControlHandler(LibVLC libVLC, public MainWindowEventHandler(LibVLC libVLC,
MediaPlayer mediaPlayer, MediaPlayer mediaPlayer,
MainWindow mainWindow, MainWindow mainWindow,
FormState formState, FormState formState,
Config config, Config config,
ILogger<PlayerControlHandler> logger) ILogger<MainWindowEventHandler> logger)
{ {
_libVLC = libVLC; _libVLC = libVLC;
_mediaPlayer = mediaPlayer; _mediaPlayer = mediaPlayer;
@@ -69,6 +69,9 @@ public class PlayerControlHandler :
public async Task Handle(KeyEvent notification, CancellationToken cancellationToken) public async Task Handle(KeyEvent notification, CancellationToken cancellationToken)
{ {
if (_formState.ActiveWindow != WindowsEnum.Main)
return;
var key = notification.Args.Key; var key = notification.Args.Key;
var keyNumber = (int?)null; var keyNumber = (int?)null;