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<Config>(sp => sp.GetRequiredService<IConfigRepository>().Get());
services.AddSingleton<PlayerControlHandler>();
services.AddSingleton<MainWindowEventHandler>();
})
.UseSerilog()
.Build();
+1 -1
View File
@@ -37,7 +37,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<None Update="config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
+1
View File
@@ -36,6 +36,7 @@ public class Config
public List<string> ImageFormats { get; set; }
public ThumbnailConfig ThumbnailConfig { get; set; }
public int? LastSelectedExplorerClass { get; set; }
}
public class WindowConfig
+2 -1
View File
@@ -17,6 +17,7 @@ public class FormState
public int CurrentVolume { get; set; } = 100;
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
public WindowsEnum ActiveWindow { get; set; }
public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}";
@@ -38,4 +39,4 @@ public class FormState
}
catch (Exception e) { return null; }
}
}
}
+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.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
@@ -24,16 +25,23 @@ public partial class DatasetExplorer
private readonly string _thumbnailsCacheFile;
private IConfigRepository _configRepository;
private readonly FormState _formState;
private readonly IGalleryManager _galleryManager;
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;
_logger = logger;
_configRepository = configRepository;
_formState = formState;
_galleryManager = galleryManager;
_thumbnailsCacheFile = Path.Combine(config.ThumbnailsDirectory, Config.ThumbnailsCacheFile);
if (File.Exists(_thumbnailsCacheFile))
{
@@ -43,19 +51,45 @@ public partial class DatasetExplorer
InitializeComponent();
DataContext = this;
Loaded += (_, _) =>
Loaded += async (_, _) =>
{
AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
new List<AnnotationClass> { new(-1, "All") }
.Concat(_config.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.SelectionChanged += async (_, _) =>
LvClasses.MouseUp += async (_, _) =>
{
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();
LocationChanged += async (_, _) => await SaveUserSettings();
@@ -68,7 +102,7 @@ public partial class DatasetExplorer
Visibility = Visibility.Hidden;
};
ThumbnailsView.KeyDown += (sender, args) =>
ThumbnailsView.KeyDown += async (sender, args) =>
{
switch (args.Key)
{
@@ -76,29 +110,15 @@ public partial class DatasetExplorer
DeleteAnnotations();
break;
case Key.Enter:
EditAnnotation();
await EditAnnotation();
break;
}
};
ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation();
ExplorerEditor.KeyDown += (_, args) =>
{
var key = args.Key;
var keyNumber = (int?)null;
Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.DatasetExplorer; };
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)
return;
LvClasses.SelectedIndex = keyNumber.Value;
};
ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentImage!)!.Value;
ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath)!.Value;
}
private async Task EditAnnotation()
@@ -111,11 +131,12 @@ public partial class DatasetExplorer
{
ImageSource = new BitmapImage(new Uri(dto.ImagePath))
};
CurrentImage = dto.ImagePath;
CurrentThumbnail = dto;
Switcher.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))
{
var annClass = _config.AnnotationClasses[ann.ClassNumber];
@@ -123,33 +144,23 @@ public partial class DatasetExplorer
Dispatcher.Invoke(() => ExplorerEditor.CreateAnnotation(annClass, time, annInfo));
}
Switcher.SelectionChanged += (_, args) =>
Switcher.SelectionChanged += (sender, args) =>
{
//From Explorer to Editor
if (Switcher.SelectedIndex == 1)
{
//Editor
_tempSelectedClassIdx = LvClasses.SelectedIndex;
LvClasses.ItemsSource = _config.AnnotationClasses;
}
else
{
//Explorer
LvClasses.ItemsSource = AllAnnotationClasses;
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()
{
_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;
await func();
_ = Task.Run(() =>
_ = Task.Run(async () =>
{
Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500));
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500));
_throttleOn = false;
});
}
+35 -19
View File
@@ -14,16 +14,27 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
public int ThumbnailsCount { get; set; }
public int ImagesCount { get; set; }
private DirectoryInfo? _thumbnailsDirectory;
private DirectoryInfo ThumbnailsDirectory
{
get
{
if (_thumbnailsDirectory != null)
return _thumbnailsDirectory;
var dir = new DirectoryInfo(config.ThumbnailsDirectory);
if (!dir.Exists)
Directory.CreateDirectory(config.ThumbnailsDirectory);
_thumbnailsDirectory = new DirectoryInfo(config.ThumbnailsDirectory);
return _thumbnailsDirectory;
}
}
public async Task RefreshThumbnails()
{
var dir = new DirectoryInfo(config.ThumbnailsDirectory);
if (!dir.Exists)
Directory.CreateDirectory(config.ThumbnailsDirectory);
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])
.GroupBy(x => x)
.Select(gr => gr.Key)
@@ -38,15 +49,9 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
var imgName = Path.GetFileNameWithoutExtension(img.Name);
if (thumbnails.Contains(imgName))
continue;
try
{
var bitmap = await GenerateThumbnail(img);
if (bitmap != null)
{
var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg");
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
}
await CreateThumbnail(img.FullName);
}
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 height = (int)config.ThumbnailConfig.Size.Height;
var imgName = Path.GetFileNameWithoutExtension(img.Name);
var labelName = Path.Combine(config.LabelsDirectory, $"{imgName}.txt");
var imgName = Path.GetFileName(imgPath);
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);
@@ -77,8 +92,8 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
var size = new Size(originalImage.Width, originalImage.Height);
if (!File.Exists(labelName))
{
File.Move(img.FullName, Path.Combine(config.UnknownImages, Path.GetFileName(img.Name)));
logger.LogInformation($"No labels found for image {img.Name}! Moved image to the {config.UnknownImages} folder.");
File.Move(imgPath, Path.Combine(config.UnknownImages, imgName));
logger.LogInformation($"No labels found for image {imgName}! Moved image to the {config.UnknownImages} folder.");
return null;
}
var labels = (await YoloLabel.ReadFromFile(labelName))
@@ -139,5 +154,6 @@ public interface IGalleryManager
{
int ThumbnailsCount { get; set; }
int ImagesCount { get; set; }
Task CreateThumbnail(string imgPath);
Task RefreshThumbnails();
}
+2
View File
@@ -75,6 +75,8 @@ public partial class MainWindow
Directory.CreateDirectory(_config.ResultsDirectory);
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
Activated += (_, _) => { _formState.ActiveWindow = WindowsEnum.Main; };
}
private void VideoView_Loaded(object sender, RoutedEventArgs e)
@@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging;
namespace Azaion.Annotator;
public class PlayerControlHandler :
public class MainWindowEventHandler :
INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>,
@@ -19,7 +19,7 @@ public class PlayerControlHandler :
private readonly MainWindow _mainWindow;
private readonly FormState _formState;
private readonly Config _config;
private readonly ILogger<PlayerControlHandler> _logger;
private readonly ILogger<MainWindowEventHandler> _logger;
private const int STEP = 20;
private const int LARGE_STEP = 5000;
@@ -37,12 +37,12 @@ public class PlayerControlHandler :
{ Key.PageDown, PlaybackControlEnum.Next },
};
public PlayerControlHandler(LibVLC libVLC,
public MainWindowEventHandler(LibVLC libVLC,
MediaPlayer mediaPlayer,
MainWindow mainWindow,
FormState formState,
Config config,
ILogger<PlayerControlHandler> logger)
ILogger<MainWindowEventHandler> logger)
{
_libVLC = libVLC;
_mediaPlayer = mediaPlayer;
@@ -69,6 +69,9 @@ public class PlayerControlHandler :
public async Task Handle(KeyEvent notification, CancellationToken cancellationToken)
{
if (_formState.ActiveWindow != WindowsEnum.Main)
return;
var key = notification.Args.Key;
var keyNumber = (int?)null;