gallery manager WIP

This commit is contained in:
Alex Bezdieniezhnykh
2024-08-30 18:15:02 +03:00
parent 8d8a78c9f5
commit 9436e96d81
13 changed files with 402 additions and 157 deletions
+4 -1
View File
@@ -1,4 +1,5 @@
using System.Reflection; using System.Data;
using System.Reflection;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading; using System.Windows.Threading;
@@ -35,6 +36,8 @@ public partial class App : Application
{ {
services.AddSingleton<MainWindow>(); services.AddSingleton<MainWindow>();
services.AddSingleton<HelpWindow>(); services.AddSingleton<HelpWindow>();
services.AddSingleton<DatasetExplorer>();
services.AddSingleton<IGalleryManager, GalleryManager>();
services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddSingleton<LibVLC>(_ => new LibVLC()); services.AddSingleton<LibVLC>(_ => new LibVLC());
services.AddSingleton<FormState>(); services.AddSingleton<FormState>();
+20 -1
View File
@@ -10,10 +10,13 @@ namespace Azaion.Annotator.DTO;
public class Config public class Config
{ {
public const string ThumbnailPrefix = "_thumb";
public string VideosDirectory { get; set; } public string VideosDirectory { get; set; }
public string LabelsDirectory { get; set; } public string LabelsDirectory { get; set; }
public string ImagesDirectory { get; set; } public string ImagesDirectory { get; set; }
public string ResultsDirectory { get; set; } public string ResultsDirectory { get; set; }
public string ThumbnailsDirectory { get; set; }
public List<AnnotationClass> AnnotationClasses { get; set; } = []; public List<AnnotationClass> AnnotationClasses { get; set; } = [];
@@ -31,6 +34,14 @@ public class Config
public List<string> VideoFormats { get; set; } public List<string> VideoFormats { get; set; }
public List<string> ImageFormats { get; set; } public List<string> ImageFormats { get; set; }
public ThumbnailConfig ThumbnailConfig { get; set; }
}
public class ThumbnailConfig
{
public Size Size { get; set; }
public int Border { get; set; }
} }
public interface IConfigRepository public interface IConfigRepository
@@ -48,9 +59,11 @@ public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfi
private const string DEFAULT_LABELS_DIR = "labels"; private const string DEFAULT_LABELS_DIR = "labels";
private const string DEFAULT_IMAGES_DIR = "images"; private const string DEFAULT_IMAGES_DIR = "images";
private const string DEFAULT_RESULTS_DIR = "results"; private const string DEFAULT_RESULTS_DIR = "results";
private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
private static readonly Size DefaultWindowSize = new(1280, 720); private static readonly Size DefaultWindowSize = new(1280, 720);
private static readonly Point DefaultWindowLocation = new(100, 100); private static readonly Point DefaultWindowLocation = new(100, 100);
private static readonly Size DefaultThumbnailSize = new(240, 135);
private static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"]; private static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
private static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"]; private static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
@@ -68,13 +81,19 @@ public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfi
LabelsDirectory = Path.Combine(exePath, DEFAULT_LABELS_DIR), LabelsDirectory = Path.Combine(exePath, DEFAULT_LABELS_DIR),
ImagesDirectory = Path.Combine(exePath, DEFAULT_IMAGES_DIR), ImagesDirectory = Path.Combine(exePath, DEFAULT_IMAGES_DIR),
ResultsDirectory = Path.Combine(exePath, DEFAULT_RESULTS_DIR), ResultsDirectory = Path.Combine(exePath, DEFAULT_RESULTS_DIR),
ThumbnailsDirectory = Path.Combine(exePath, DEFAULT_THUMBNAILS_DIR),
WindowLocation = DefaultWindowLocation, WindowLocation = DefaultWindowLocation,
WindowSize = DefaultWindowSize, WindowSize = DefaultWindowSize,
ShowHelpOnStart = true, ShowHelpOnStart = true,
VideoFormats = DefaultVideoFormats, VideoFormats = DefaultVideoFormats,
ImageFormats = DefaultImageFormats ImageFormats = DefaultImageFormats,
ThumbnailConfig = new ThumbnailConfig
{
Size = DefaultThumbnailSize,
Border = 10
}
}; };
} }
var str = File.ReadAllText(CONFIG_PATH); var str = File.ReadAllText(CONFIG_PATH);
+4 -1
View File
@@ -9,11 +9,14 @@ public class FormState
public SelectionState SelectionState { get; set; } = SelectionState.None; public SelectionState SelectionState { get; set; } = SelectionState.None;
public MediaFileInfo? CurrentMedia { get; set; } public MediaFileInfo? CurrentMedia { get; set; }
public Size CurrentVideoSize { get; set; }
public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name) public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name)
? "" ? ""
: Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", ""); : Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", "");
public string CurrentMrl { get; set; }
public Size CurrentVideoSize { get; set; }
public TimeSpan CurrentVideoLength { get; set; } public TimeSpan CurrentVideoLength { get; set; }
public int CurrentVolume { get; set; } = 100; public int CurrentVolume { get; set; } = 100;
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = []; public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
+11
View File
@@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using System.IO;
using System.Windows; using System.Windows;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -147,6 +148,16 @@ public class YoloLabel : Label
} }
} }
public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken)
{
var str = await File.ReadAllTextAsync(filename, cancellationToken);
return str.Split(Environment.NewLine)
.Select(Parse)
.Where(ann => ann != null)
.ToList()!;
}
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.'); public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
} }
+26
View File
@@ -0,0 +1,26 @@
<Window x:Class="Azaion.Annotator.DatasetExplorer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Azaion.Annotator"
mc:Ignorable="d"
Title="Браузер анотацій" Height="450" Width="800">
<Grid
Name="MainGrid"
ShowGridLines="False"
Background="Black"
HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="32"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
</Grid>
</Window>
+30
View File
@@ -0,0 +1,30 @@
using System.Windows;
namespace Azaion.Annotator;
public partial class DatasetExplorer : Window
{
private CancellationTokenSource _cancellationTokenSource;
public DatasetExplorer(IGalleryManager galleryManager)
{
_cancellationTokenSource = new CancellationTokenSource();
InitializeComponent();
Loaded += (sender, args) =>
{
_ = Task.Run(async () =>
{
while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
await galleryManager.RefreshThumbnails(_cancellationTokenSource.Token);
await Task.Delay(30000, _cancellationTokenSource.Token);
}
});
};
Closing += (sender, args) => _cancellationTokenSource.Cancel();
}
}
@@ -10,7 +10,10 @@ public static class ThrottleExt
_throttleOn = true; _throttleOn = true;
await func(); await func();
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500)); _ = Task.Run(() =>
{
Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500));
_throttleOn = false; _throttleOn = false;
});
} }
} }
+122 -3
View File
@@ -1,10 +1,129 @@
namespace Azaion.Annotator; using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using Azaion.Annotator.DTO;
using Color = System.Drawing.Color;
using Size = System.Windows.Size;
public class GalleryManager namespace Azaion.Annotator;
public class GalleryManager : IGalleryManager
{ {
private readonly Config _config;
public void CreateThumbnails() public int ThumbnailsCount { get; set; }
public int ImagesCount { get; set; }
public GalleryManager(Config config)
{ {
_config = config;
}
public async Task RefreshThumbnails(CancellationToken cancellationToken)
{
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()
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
.GroupBy(x => x)
.Select(gr => gr.Key)
.ToHashSet();
ThumbnailsCount = thumbnails.Count;
var files = new DirectoryInfo(_config.ImagesDirectory).GetFiles();
ImagesCount = files.Length;
foreach (var img in files)
{
var imgName = Path.GetFileNameWithoutExtension(img.Name);
if (thumbnails.Contains(imgName))
continue;
var bitmap = await GenerateThumbnail(img, cancellationToken);
var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg");
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
ThumbnailsCount++;
}
}
private async Task<Bitmap> GenerateThumbnail(FileInfo img, CancellationToken cancellationToken)
{
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 originalImage = Image.FromFile(img.FullName);
var bitmap = new Bitmap(width, height);
using var g = Graphics.FromImage(bitmap);
g.CompositingQuality = CompositingQuality.HighSpeed;
g.SmoothingMode = SmoothingMode.HighSpeed;
g.InterpolationMode = InterpolationMode.Default;
var size = new Size(originalImage.Width, originalImage.Height);
var labels = (await YoloLabel.ReadFromFile(labelName, cancellationToken))
.Select(x => new CanvasLabel(x, size, size))
.ToList();
var thumbWhRatio = width / (float)height;
var border = _config.ThumbnailConfig.Border;
var labelsMinX = labels.Any() ? labels.Min(x => x.X);
var labelsMaxX = labels.Max(x => x.X + x.Width);
var labelsMinY = labels.Min(x => x.Y);
var labelsMaxY = labels.Max(x => x.Y + x.Height);
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
var frameHeight = 0.0;
var frameWidth = 0.0;
var frameX = 0.0;
var frameY = 0.0;
if (labelsWidth / labelsHeight > thumbWhRatio)
{
frameWidth = labelsWidth;
frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height);
frameX = Math.Max(0, labelsMinX - border);
frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border);
}
else
{
frameHeight = labelsHeight;
frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width);
frameY = Math.Max(0, labelsMinY - border);
frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border);
}
var scale = frameHeight / height;
g.DrawImage(originalImage, new Rectangle(0, 0, width, height), new RectangleF((float)frameX, (float)frameY, (float)frameWidth, (float)frameHeight), GraphicsUnit.Pixel);
foreach (var label in labels)
{
var color = _config.AnnotationClassesDict[label.ClassNumber].Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
g.FillRectangle(brush, rectangle);
}
return bitmap;
} }
} }
public interface IGalleryManager
{
int ThumbnailsCount { get; set; }
int ImagesCount { get; set; }
Task RefreshThumbnails(CancellationToken cancellationToken);
}
+38 -9
View File
@@ -53,6 +53,7 @@
Background="Black" Background="Black"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="28"></RowDefinition> <RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="28"></RowDefinition> <RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
@@ -75,6 +76,9 @@
<MenuItem x:Name="OpenFolderItem" <MenuItem x:Name="OpenFolderItem"
Foreground="Black" Foreground="Black"
IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/> IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/>
<MenuItem x:Name="OpenDataExplorerItem"
Foreground="Black"
IsEnabled="True" Header="Відкрити браузер анотацій..." Click="OpenDataExplorerItemClick"/>
</MenuItem> </MenuItem>
<MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0"> <MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0">
<MenuItem x:Name="OpenHelpWindow" <MenuItem x:Name="OpenHelpWindow"
@@ -105,7 +109,32 @@
</Button> </Button>
</Grid> </Grid>
<ListView Grid.Row="2" <Grid
HorizontalAlignment="Stretch"
Grid.Column="0"
Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label
Grid.Column="0"
Grid.Row="0"
HorizontalAlignment="Stretch"
Margin="1"
Foreground="LightGray"
Content="Фільтр: "/>
<TextBox
Grid.Column="1"
Grid.Row="0"
HorizontalAlignment="Stretch"
Margin="1"
x:Name="TbFilter"
TextChanged="TbFilter_OnTextChanged">
</TextBox>
</Grid>
<ListView Grid.Row="3"
Grid.Column="0" Grid.Column="0"
Name="LvFiles" Name="LvFiles"
Background="Black" Background="Black"
@@ -133,7 +162,7 @@
</ListView> </ListView>
<DataGrid x:Name="LvClasses" <DataGrid x:Name="LvClasses"
Grid.Column="0" Grid.Column="0"
Grid.Row="3" Grid.Row="4"
Background="Black" Background="Black"
RowBackground="#252525" RowBackground="#252525"
Foreground="White" Foreground="White"
@@ -181,7 +210,7 @@
ResizeDirection="Columns" ResizeDirection="Columns"
Grid.Column="1" Grid.Column="1"
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="3" Grid.RowSpan="4"
ResizeBehavior="PreviousAndNext" ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
@@ -190,7 +219,7 @@
<wpf:VideoView <wpf:VideoView
Grid.Row="1" Grid.Row="1"
Grid.Column="2" Grid.Column="2"
Grid.RowSpan="3" Grid.RowSpan="4"
x:Name="VideoView"> x:Name="VideoView">
<controls:CanvasEditor x:Name="Editor" <controls:CanvasEditor x:Name="Editor"
Background="#01000000" Background="#01000000"
@@ -202,7 +231,7 @@
ResizeDirection="Columns" ResizeDirection="Columns"
Grid.Column="3" Grid.Column="3"
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="3" Grid.RowSpan="4"
ResizeBehavior="PreviousAndNext" ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
@@ -211,7 +240,7 @@
<DataGrid x:Name="DgAnnotations" <DataGrid x:Name="DgAnnotations"
Grid.Column="4" Grid.Column="4"
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="3" Grid.RowSpan="6"
Background="Black" Background="Black"
RowBackground="#252525" RowBackground="#252525"
Foreground="White" Foreground="White"
@@ -265,14 +294,14 @@
<controls:UpdatableProgressBar x:Name="VideoSlider" <controls:UpdatableProgressBar x:Name="VideoSlider"
Grid.Column="0" Grid.Column="0"
Grid.Row="4" Grid.Row="5"
Grid.ColumnSpan="4" Grid.ColumnSpan="4"
Background="#252525" Background="#252525"
Foreground="LightBlue"> Foreground="LightBlue">
</controls:UpdatableProgressBar> </controls:UpdatableProgressBar>
<Grid <Grid
Grid.Row="5" Grid.Row="6"
Grid.Column="0" Grid.Column="0"
Background="Black" Background="Black"
> >
@@ -455,7 +484,7 @@
</Button> </Button>
</Grid> </Grid>
<StatusBar <StatusBar
Grid.Row="5" Grid.Row="6"
Grid.Column="2" Grid.Column="2"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Background="#252525" Background="#252525"
+35 -15
View File
@@ -27,6 +27,7 @@ public partial class MainWindow
private readonly IConfigRepository _configRepository; private readonly IConfigRepository _configRepository;
private readonly HelpWindow _helpWindow; private readonly HelpWindow _helpWindow;
private readonly ILogger<MainWindow> _logger; private readonly ILogger<MainWindow> _logger;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new(); public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
@@ -34,6 +35,10 @@ public partial class MainWindow
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100); private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(300); private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(300);
private readonly Config _config; private readonly Config _config;
private readonly DatasetExplorer _datasetExplorer;
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<YoloLabel>> Annotations { get; set; } = new();
@@ -42,6 +47,7 @@ public partial class MainWindow
FormState formState, FormState formState,
IConfigRepository configRepository, IConfigRepository configRepository,
HelpWindow helpWindow, HelpWindow helpWindow,
DatasetExplorer datasetExplorer,
ILogger<MainWindow> logger) ILogger<MainWindow> logger)
{ {
InitializeComponent(); InitializeComponent();
@@ -52,6 +58,7 @@ public partial class MainWindow
_configRepository = configRepository; _configRepository = configRepository;
_config = _configRepository.Get(); _config = _configRepository.Get();
_helpWindow = helpWindow; _helpWindow = helpWindow;
_datasetExplorer = datasetExplorer;
_logger = logger; _logger = logger;
VideoView.Loaded += VideoView_Loaded; VideoView.Loaded += VideoView_Loaded;
@@ -115,12 +122,16 @@ public partial class MainWindow
_mediaPlayer.Playing += async (sender, args) => _mediaPlayer.Playing += async (sender, args) =>
{ {
if (_formState.CurrentMrl == _mediaPlayer.Media?.Mrl)
return; //already loaded all the info
_formState.CurrentMrl = _mediaPlayer.Media?.Mrl ?? "";
uint vw = 0, vh = 0; uint vw = 0, vh = 0;
_mediaPlayer.Size(0, ref vw, ref vh); _mediaPlayer.Size(0, ref vw, ref vh);
_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()); await Dispatcher.Invoke(async () => await ReloadAnnotations(_cancellationTokenSource.Token));
if (_formState.CurrentMedia?.MediaType != MediaTypes.Image) if (_formState.CurrentMedia?.MediaType != MediaTypes.Image)
return; return;
@@ -195,7 +206,7 @@ public partial class MainWindow
} }
} }
public async Task ReloadAnnotations() public async Task ReloadAnnotations(CancellationToken cancellationToken)
{ {
_formState.AnnotationResults.Clear(); _formState.AnnotationResults.Clear();
Annotations.Clear(); Annotations.Clear();
@@ -211,12 +222,7 @@ public partial class MainWindow
var name = Path.GetFileNameWithoutExtension(file.Name); var name = Path.GetFileNameWithoutExtension(file.Name);
var time = _formState.GetTime(name)!.Value; var time = _formState.GetTime(name)!.Value;
var str = await File.ReadAllTextAsync(file.FullName); await AddAnnotation(time, await YoloLabel.ReadFromFile(file.FullName, cancellationToken));
var annotations = str.Split(Environment.NewLine).Select(YoloLabel.Parse)
.Where(ann => ann != null)
.ToList();
await AddAnnotation(time, annotations!);
} }
} }
@@ -271,11 +277,11 @@ public partial class MainWindow
HasAnnotations = labelNames.ContainsKey(Path.GetFileNameWithoutExtension(x.Name).Replace(" ", "")) HasAnnotations = labelNames.ContainsKey(Path.GetFileNameWithoutExtension(x.Name).Replace(" ", ""))
}); });
var mediaFiles = videoFiles.Concat(imageFiles).ToList(); AllMediaFiles = new ObservableCollection<MediaFileInfo>(videoFiles.Concat(imageFiles).ToList());
LvFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(mediaFiles); LvFiles.ItemsSource = AllMediaFiles;
TbFolder.Text = _config.VideosDirectory; TbFolder.Text = _config.VideosDirectory;
BlinkHelp(mediaFiles.Count == 0 BlinkHelp(AllMediaFiles.Count == 0
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial] ? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
: HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]); : HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]);
} }
@@ -296,9 +302,9 @@ public partial class MainWindow
// AnnotationClasses.Add(new AnnotationClass(AnnotationClasses.Count)); // AnnotationClasses.Add(new AnnotationClass(AnnotationClasses.Count));
// LvClasses.SelectedIndex = AnnotationClasses.Count - 1; // LvClasses.SelectedIndex = AnnotationClasses.Count - 1;
// } // }
private void OpenFolderItemClick(object sender, RoutedEventArgs e) => OpenFolder(); private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
private void OpenFolderButtonClick(object sender, RoutedEventArgs e) => OpenFolder(); private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
private void OpenFolder() private async Task OpenFolder()
{ {
var dlg = new CommonOpenFileDialog var dlg = new CommonOpenFileDialog
{ {
@@ -310,10 +316,25 @@ public partial class MainWindow
return; return;
if (!string.IsNullOrEmpty(dlg.FileName)) if (!string.IsNullOrEmpty(dlg.FileName))
{
_config.VideosDirectory = dlg.FileName; _config.VideosDirectory = dlg.FileName;
await SaveUserSettings();
}
ReloadFiles(); ReloadFiles();
} }
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
{
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
LvFiles.ItemsSource = FilteredMediaFiles;
}
private void OpenDataExplorerItemClick(object sender, RoutedEventArgs e)
{
_datasetExplorer.Show();
_datasetExplorer.Activate();
}
private void PlayClick(object sender, RoutedEventArgs e) private void PlayClick(object sender, RoutedEventArgs e)
{ {
@@ -354,4 +375,3 @@ public partial class MainWindow
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings(); private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings();
} }
+87 -70
View File
@@ -8,17 +8,19 @@ using Microsoft.Extensions.Logging;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public class PlayerControlHandler(LibVLC libVLC, public class PlayerControlHandler :
MediaPlayer mediaPlayer,
MainWindow mainWindow,
FormState formState,
Config config,
ILogger<PlayerControlHandler> logger) :
INotificationHandler<KeyEvent>, INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>, INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>, INotificationHandler<PlaybackControlEvent>,
INotificationHandler<VolumeChangedEvent> INotificationHandler<VolumeChangedEvent>
{ {
private readonly LibVLC _libVLC;
private readonly MediaPlayer _mediaPlayer;
private readonly MainWindow _mainWindow;
private readonly FormState _formState;
private readonly Config _config;
private readonly ILogger<PlayerControlHandler> _logger;
private const int STEP = 20; private const int STEP = 20;
private const int LARGE_STEP = 5000; private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280; private const int RESULT_WIDTH = 1280;
@@ -35,6 +37,21 @@ public class PlayerControlHandler(LibVLC libVLC,
{ Key.PageDown, PlaybackControlEnum.Next }, { Key.PageDown, PlaybackControlEnum.Next },
}; };
public PlayerControlHandler(LibVLC libVLC,
MediaPlayer mediaPlayer,
MainWindow mainWindow,
FormState formState,
Config config,
ILogger<PlayerControlHandler> logger)
{
_libVLC = libVLC;
_mediaPlayer = mediaPlayer;
_mainWindow = mainWindow;
_formState = formState;
_config = config;
_logger = logger;
}
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
{ {
SelectClass(notification.AnnotationClass); SelectClass(notification.AnnotationClass);
@@ -44,15 +61,15 @@ public class PlayerControlHandler(LibVLC libVLC,
private void SelectClass(AnnotationClass annClass) private void SelectClass(AnnotationClass annClass)
{ {
mainWindow.Editor.CurrentAnnClass = annClass; _mainWindow.Editor.CurrentAnnClass = annClass;
foreach (var ann in mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected)) foreach (var ann in _mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = annClass; ann.AnnotationClass = annClass;
mainWindow.LvClasses.SelectedIndex = annClass.Id; _mainWindow.LvClasses.SelectedIndex = annClass.Id;
} }
public async Task Handle(KeyEvent notification, CancellationToken cancellationToken) public async Task Handle(KeyEvent notification, CancellationToken cancellationToken)
{ {
logger.LogInformation($"Catch {notification.Args.Key} by {notification.Sender.GetType().Name}"); _logger.LogInformation($"Catch {notification.Args.Key} by {notification.Sender.GetType().Name}");
var key = notification.Args.Key; var key = notification.Args.Key;
var keyNumber = (int?)null; var keyNumber = (int?)null;
@@ -62,7 +79,7 @@ public class PlayerControlHandler(LibVLC libVLC,
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1; keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue) if (keyNumber.HasValue)
SelectClass(mainWindow.AnnotationClasses[keyNumber.Value]); SelectClass(_mainWindow.AnnotationClasses[keyNumber.Value]);
if (_keysControlEnumDict.TryGetValue(key, out var value)) if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value); await ControlPlayback(value);
@@ -74,7 +91,7 @@ public class PlayerControlHandler(LibVLC libVLC,
{ {
switch (key) switch (key)
{ {
case Key.VolumeMute when mediaPlayer.Volume == 0: case Key.VolumeMute when _mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume); await ControlPlayback(PlaybackControlEnum.TurnOnVolume);
break; break;
case Key.VolumeMute: case Key.VolumeMute:
@@ -82,15 +99,15 @@ public class PlayerControlHandler(LibVLC libVLC,
break; break;
case Key.Up: case Key.Up:
case Key.VolumeUp: case Key.VolumeUp:
var vUp = Math.Min(100, mediaPlayer.Volume + 5); var vUp = Math.Min(100, _mediaPlayer.Volume + 5);
ChangeVolume(vUp); ChangeVolume(vUp);
mainWindow.Volume.Value = vUp; _mainWindow.Volume.Value = vUp;
break; break;
case Key.Down: case Key.Down:
case Key.VolumeDown: case Key.VolumeDown:
var vDown = Math.Max(0, mediaPlayer.Volume - 5); var vDown = Math.Max(0, _mediaPlayer.Volume - 5);
ChangeVolume(vDown); ChangeVolume(vDown);
mainWindow.Volume.Value = vDown; _mainWindow.Volume.Value = vDown;
break; break;
} }
} }
@@ -98,7 +115,7 @@ public class PlayerControlHandler(LibVLC libVLC,
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken) public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken)
{ {
await ControlPlayback(notification.PlaybackControl); await ControlPlayback(notification.PlaybackControl);
mainWindow.VideoView.Focus(); _mainWindow.VideoView.Focus();
} }
private async Task ControlPlayback(PlaybackControlEnum controlEnum) private async Task ControlPlayback(PlaybackControlEnum controlEnum)
@@ -114,42 +131,42 @@ public class PlayerControlHandler(LibVLC libVLC,
await Play(); await Play();
break; break;
case PlaybackControlEnum.Pause: case PlaybackControlEnum.Pause:
mediaPlayer.Pause(); _mediaPlayer.Pause();
if (!mediaPlayer.IsPlaying) if (!_mediaPlayer.IsPlaying)
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]); _mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
break; break;
case PlaybackControlEnum.Stop: case PlaybackControlEnum.Stop:
mediaPlayer.Stop(); _mediaPlayer.Stop();
break; break;
case PlaybackControlEnum.PreviousFrame: case PlaybackControlEnum.PreviousFrame:
mediaPlayer.SetPause(true); _mediaPlayer.SetPause(true);
mediaPlayer.Time -= step; _mediaPlayer.Time -= step;
mainWindow.VideoSlider.Value = mediaPlayer.Position * 100; _mainWindow.VideoSlider.Value = _mediaPlayer.Position * 100;
break; break;
case PlaybackControlEnum.NextFrame: case PlaybackControlEnum.NextFrame:
mediaPlayer.SetPause(true); _mediaPlayer.SetPause(true);
mediaPlayer.Time += step; _mediaPlayer.Time += step;
mainWindow.VideoSlider.Value = mediaPlayer.Position * 100; _mainWindow.VideoSlider.Value = _mediaPlayer.Position * 100;
break; break;
case PlaybackControlEnum.SaveAnnotations: case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations(); await SaveAnnotations();
break; break;
case PlaybackControlEnum.RemoveSelectedAnns: case PlaybackControlEnum.RemoveSelectedAnns:
mainWindow.Editor.RemoveSelectedAnns(); _mainWindow.Editor.RemoveSelectedAnns();
break; break;
case PlaybackControlEnum.RemoveAllAnns: case PlaybackControlEnum.RemoveAllAnns:
mainWindow.Editor.RemoveAllAnns(); _mainWindow.Editor.RemoveAllAnns();
break; break;
case PlaybackControlEnum.TurnOnVolume: case PlaybackControlEnum.TurnOnVolume:
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed; _mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible; _mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
mediaPlayer.Volume = formState.CurrentVolume; _mediaPlayer.Volume = _formState.CurrentVolume;
break; break;
case PlaybackControlEnum.TurnOffVolume: case PlaybackControlEnum.TurnOffVolume:
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed; _mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible; _mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
formState.CurrentVolume = mediaPlayer.Volume; _formState.CurrentVolume = _mediaPlayer.Volume;
mediaPlayer.Volume = 0; _mediaPlayer.Volume = 0;
break; break;
case PlaybackControlEnum.Previous: case PlaybackControlEnum.Previous:
await NextMedia(isPrevious: true); await NextMedia(isPrevious: true);
@@ -173,11 +190,11 @@ public class PlayerControlHandler(LibVLC libVLC,
private async Task NextMedia(bool isPrevious = false) private async Task NextMedia(bool isPrevious = false)
{ {
var increment = isPrevious ? -1 : 1; var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count; var check = isPrevious ? -1 : _mainWindow.LvFiles.Items.Count;
if (mainWindow.LvFiles.SelectedIndex + increment == check) if (_mainWindow.LvFiles.SelectedIndex + increment == check)
return; return;
mainWindow.LvFiles.SelectedIndex += increment; _mainWindow.LvFiles.SelectedIndex += increment;
await Play(); await Play();
} }
@@ -189,63 +206,63 @@ public class PlayerControlHandler(LibVLC libVLC,
private void ChangeVolume(int volume) private void ChangeVolume(int volume)
{ {
formState.CurrentVolume = volume; _formState.CurrentVolume = volume;
mediaPlayer.Volume = volume; _mediaPlayer.Volume = volume;
} }
private async Task Play() private async Task Play()
{ {
if (mainWindow.LvFiles.SelectedItem == null) if (_mainWindow.LvFiles.SelectedItem == null)
return; return;
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem; var mediaInfo = (MediaFileInfo)_mainWindow.LvFiles.SelectedItem;
formState.CurrentMedia = mediaInfo; _formState.CurrentMedia = mediaInfo;
mediaPlayer.Stop(); _mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}"; _mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]); _mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path)); _mediaPlayer.Play(new Media(_libVLC, mediaInfo.Path));
} }
private async Task SaveAnnotations() private async Task SaveAnnotations()
{ {
if (formState.CurrentMedia == null) if (_formState.CurrentMedia == null)
return; return;
var time = TimeSpan.FromMilliseconds(mediaPlayer.Time); var time = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
var fName = formState.GetTimeName(time); var fName = _formState.GetTimeName(time);
var currentAnns = mainWindow.Editor.CurrentAnns var currentAnns = _mainWindow.Editor.CurrentAnns
.Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.CurrentVideoSize)) .Select(x => new YoloLabel(x.Info, _mainWindow.Editor.RenderSize, _formState.CurrentVideoSize))
.ToList(); .ToList();
var labels = string.Join(Environment.NewLine, currentAnns.Select(x => x.ToString())); var labels = string.Join(Environment.NewLine, currentAnns.Select(x => x.ToString()));
if (!Directory.Exists(config.LabelsDirectory)) if (!Directory.Exists(_config.LabelsDirectory))
Directory.CreateDirectory(config.LabelsDirectory); Directory.CreateDirectory(_config.LabelsDirectory);
if (!Directory.Exists(config.ImagesDirectory)) if (!Directory.Exists(_config.ImagesDirectory))
Directory.CreateDirectory(config.ImagesDirectory); Directory.CreateDirectory(_config.ImagesDirectory);
if (!Directory.Exists(config.ResultsDirectory)) if (!Directory.Exists(_config.ResultsDirectory))
Directory.CreateDirectory(config.ResultsDirectory); Directory.CreateDirectory(_config.ResultsDirectory);
await File.WriteAllTextAsync(Path.Combine(config.LabelsDirectory, $"{fName}.txt"), labels); await File.WriteAllTextAsync(Path.Combine(_config.LabelsDirectory, $"{fName}.txt"), labels);
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height); var resultHeight = (uint)Math.Round(RESULT_WIDTH / _formState.CurrentVideoSize.Width * _formState.CurrentVideoSize.Height);
await mainWindow.AddAnnotation(time, currentAnns); await _mainWindow.AddAnnotation(time, currentAnns);
formState.CurrentMedia.HasAnnotations = mainWindow.Annotations.Count != 0; _formState.CurrentMedia.HasAnnotations = _mainWindow.Annotations.Count != 0;
mainWindow.LvFiles.Items.Refresh(); _mainWindow.LvFiles.Items.Refresh();
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video; var isVideo = _formState.CurrentMedia.MediaType == MediaTypes.Video;
var destinationPath = Path.Combine(config.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}"); var destinationPath = Path.Combine(_config.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(_formState.CurrentMedia.Path))}");
mainWindow.Editor.RemoveAllAnns(); _mainWindow.Editor.RemoveAllAnns();
if (isVideo) if (isVideo)
{ {
mediaPlayer.TakeSnapshot(0, destinationPath, RESULT_WIDTH, resultHeight); _mediaPlayer.TakeSnapshot(0, destinationPath, RESULT_WIDTH, resultHeight);
mediaPlayer.Play(); _mediaPlayer.Play();
} }
else else
{ {
File.Copy(formState.CurrentMedia.Path, destinationPath, overwrite: true); File.Copy(_formState.CurrentMedia.Path, destinationPath, overwrite: true);
await NextMedia(); await NextMedia();
} }
} }
+15 -50
View File
@@ -3,60 +3,25 @@
"LabelsDirectory": "E:\\labels", "LabelsDirectory": "E:\\labels",
"ImagesDirectory": "E:\\images", "ImagesDirectory": "E:\\images",
"ResultsDirectory": "E:\\results", "ResultsDirectory": "E:\\results",
"ThumbnailsDirectory": "E:\\thumbnails",
"AnnotationClasses": [ "AnnotationClasses": [
{ { "Id": 0, "Name": "Броньована техніка", "Color": "#40FF0000" },
"Id": 0, { "Id": 1, "Name": "Вантажівка", "Color": "#4000FF00" },
"Name": "Броньована техніка", { "Id": 2, "Name": "Машина легкова", "Color": "#400000FF" },
"Color": "#40FF0000" { "Id": 3, "Name": "Артилерія", "Color": "#40FFFF00" },
}, { "Id": 4, "Name": "Тінь від техніки", "Color": "#40FF00FF" },
{ { "Id": 5, "Name": "Окопи", "Color": "#4000FFFF" },
"Id": 1, { "Id": 6, "Name": "Військовий", "Color": "#40000000" },
"Name": "Вантажівка", { "Id": 7, "Name": "Накати", "Color": "#40800000" },
"Color": "#4000FF00" { "Id": 8, "Name": "Танк з захистом", "Color": "#40008000" },
}, { "Id": 9, "Name": "Дим", "Color": "#40000080" }
{
"Id": 2,
"Name": "Машина легкова",
"Color": "#400000FF"
},
{
"Id": 3,
"Name": "Артилерія",
"Color": "#40FFFF00"
},
{
"Id": 4,
"Name": "Тінь від техніки",
"Color": "#40FF00FF"
},
{
"Id": 5,
"Name": "Окопи",
"Color": "#4000FFFF"
},
{
"Id": 6,
"Name": "Військовий",
"Color": "#40000000"
},
{
"Id": 7,
"Name": "Накати",
"Color": "#40800000"
},
{
"Id": 8,
"Name": "Танк з захистом",
"Color": "#40008000"
},
{
"Id": 9,
"Name": "Дим",
"Color": "#40000080"
}
], ],
"WindowSize": "1920,1080", "WindowSize": "1920,1080",
"WindowLocation": "200,121", "WindowLocation": "200,121",
"ThumbnailConfig": {
"Size": "480,270",
"Border": 10
},
"FullScreen": true, "FullScreen": true,
"LeftPanelWidth": 300, "LeftPanelWidth": 300,
"RightPanelWidth": 300, "RightPanelWidth": 300,