add editor, fix some bugs

WIP
This commit is contained in:
Alex Bezdieniezhnykh
2024-09-10 17:10:54 +03:00
parent b4bedb7520
commit 52371ace3a
16 changed files with 498 additions and 148 deletions
+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>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
@@ -0,0 +1,50 @@
<DataGrid x:Class="Azaion.Annotator.Controls.AnnotationClasses"
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.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Background="Black"
RowBackground="#252525"
Foreground="White"
RowHeaderWidth="0"
Padding="2 0 0 0"
AutoGenerateColumns="False"
SelectionMode="Single"
CellStyle="{DynamicResource DataGridCellStyle1}"
IsReadOnly="True"
CanUserResizeRows="False"
CanUserResizeColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn
Width="50"
Header="Клавіша"
CanUserSort="False">
<DataGridTemplateColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter>
</Style>
</DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=ColorBrush}">
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
Header="Назва"
Binding="{Binding Path=Name}"
CanUserSort="False">
<DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter>
</Style>
</DataGridTextColumn.HeaderStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace Azaion.Annotator.Controls;
public partial class AnnotationClasses : DataGrid
{
public AnnotationClasses()
{
InitializeComponent();
}
}
+36 -21
View File
@@ -13,6 +13,7 @@ namespace Azaion.Annotator.Controls;
public class CanvasEditor : Canvas public class CanvasEditor : Canvas
{ {
private Point _lastPos; private Point _lastPos;
public SelectionState SelectionState { get; set; } = SelectionState.None;
private readonly Rectangle _newAnnotationRect; private readonly Rectangle _newAnnotationRect;
private Point _newAnnotationStartPos; private Point _newAnnotationStartPos;
@@ -30,6 +31,19 @@ public class CanvasEditor : Canvas
public FormState FormState { get; set; } = null!; public FormState FormState { get; set; } = null!;
public IMediator Mediator { get; set; } = null!; public IMediator Mediator { get; set; } = null!;
public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register(
nameof(GetTimeFunc),
typeof(Func<TimeSpan>),
typeof(CanvasEditor),
new PropertyMetadata(null));
public Func<TimeSpan> GetTimeFunc
{
get => (Func<TimeSpan>)GetValue(GetTimeFuncProp);
set => SetValue(GetTimeFuncProp, value);
}
private AnnotationClass _currentAnnClass = null!; private AnnotationClass _currentAnnClass = null!;
public AnnotationClass CurrentAnnClass public AnnotationClass CurrentAnnClass
{ {
@@ -84,11 +98,7 @@ public class CanvasEditor : Canvas
Stroke = new SolidColorBrush(Colors.Gray), Stroke = new SolidColorBrush(Colors.Gray),
Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)), Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)),
}; };
Loaded += Init;
}
private void Init(object sender, RoutedEventArgs e)
{
KeyDown += (_, args) => KeyDown += (_, args) =>
{ {
Console.WriteLine($"pressed {args.Key}"); Console.WriteLine($"pressed {args.Key}");
@@ -98,15 +108,21 @@ public class CanvasEditor : Canvas
MouseUp += CanvasMouseUp; MouseUp += CanvasMouseUp;
SizeChanged += CanvasResized; SizeChanged += CanvasResized;
Cursor = Cursors.Cross; Cursor = Cursors.Cross;
_horizontalLine.X1 = 0;
_horizontalLine.X2 = ActualWidth;
_verticalLine.Y1 = 0;
_verticalLine.Y2 = ActualHeight;
Children.Add(_newAnnotationRect); Children.Add(_newAnnotationRect);
Children.Add(_horizontalLine); Children.Add(_horizontalLine);
Children.Add(_verticalLine); Children.Add(_verticalLine);
Children.Add(_classNameHint); Children.Add(_classNameHint);
Loaded += Init;
}
private void Init(object sender, RoutedEventArgs e)
{
_horizontalLine.X1 = 0;
_horizontalLine.X2 = ActualWidth;
_verticalLine.Y1 = 0;
_verticalLine.Y2 = ActualHeight;
} }
private void CanvasMouseDown(object sender, MouseButtonEventArgs e) private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
@@ -125,22 +141,22 @@ public class CanvasEditor : Canvas
if (e.LeftButton != MouseButtonState.Pressed) if (e.LeftButton != MouseButtonState.Pressed)
return; return;
if (FormState.SelectionState == SelectionState.NewAnnCreating) if (SelectionState == SelectionState.NewAnnCreating)
NewAnnotationCreatingMove(sender, e); NewAnnotationCreatingMove(sender, e);
if (FormState.SelectionState == SelectionState.AnnResizing) if (SelectionState == SelectionState.AnnResizing)
AnnotationResizeMove(sender, e); AnnotationResizeMove(sender, e);
if (FormState.SelectionState == SelectionState.AnnMoving) if (SelectionState == SelectionState.AnnMoving)
AnnotationPositionMove(sender, e); AnnotationPositionMove(sender, e);
} }
private void CanvasMouseUp(object sender, MouseButtonEventArgs e) private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{ {
if (FormState.SelectionState == SelectionState.NewAnnCreating) if (SelectionState == SelectionState.NewAnnCreating)
CreateAnnotation(e.GetPosition(this)); CreateAnnotation(e.GetPosition(this));
FormState.SelectionState = SelectionState.None; SelectionState = SelectionState.None;
e.Handled = true; e.Handled = true;
} }
@@ -154,7 +170,7 @@ public class CanvasEditor : Canvas
private void AnnotationResizeStart(object sender, MouseEventArgs e) private void AnnotationResizeStart(object sender, MouseEventArgs e)
{ {
FormState.SelectionState = SelectionState.AnnResizing; SelectionState = SelectionState.AnnResizing;
_lastPos = e.GetPosition(this); _lastPos = e.GetPosition(this);
_curRec = (Rectangle)sender; _curRec = (Rectangle)sender;
_curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent; _curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent;
@@ -163,7 +179,7 @@ public class CanvasEditor : Canvas
private void AnnotationResizeMove(object sender, MouseEventArgs e) private void AnnotationResizeMove(object sender, MouseEventArgs e)
{ {
if (FormState.SelectionState != SelectionState.AnnResizing) if (SelectionState != SelectionState.AnnResizing)
return; return;
var currentPos = e.GetPosition(this); var currentPos = e.GetPosition(this);
@@ -224,13 +240,13 @@ public class CanvasEditor : Canvas
_curAnn.IsSelected = true; _curAnn.IsSelected = true;
FormState.SelectionState = SelectionState.AnnMoving; SelectionState = SelectionState.AnnMoving;
e.Handled = true; e.Handled = true;
} }
private void AnnotationPositionMove(object sender, MouseEventArgs e) private void AnnotationPositionMove(object sender, MouseEventArgs e)
{ {
if (FormState.SelectionState != SelectionState.AnnMoving) if (SelectionState != SelectionState.AnnMoving)
return; return;
var currentPos = e.GetPosition(this); var currentPos = e.GetPosition(this);
@@ -255,12 +271,12 @@ public class CanvasEditor : Canvas
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y); SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
_newAnnotationRect.MouseMove += NewAnnotationCreatingMove; _newAnnotationRect.MouseMove += NewAnnotationCreatingMove;
FormState.SelectionState = SelectionState.NewAnnCreating; SelectionState = SelectionState.NewAnnCreating;
} }
private void NewAnnotationCreatingMove(object sender, MouseEventArgs e) private void NewAnnotationCreatingMove(object sender, MouseEventArgs e)
{ {
if (FormState.SelectionState != SelectionState.NewAnnCreating) if (SelectionState != SelectionState.NewAnnCreating)
return; return;
var currentPos = e.GetPosition(this); var currentPos = e.GetPosition(this);
@@ -284,8 +300,7 @@ public class CanvasEditor : Canvas
if (width < MIN_SIZE || height < MIN_SIZE) if (width < MIN_SIZE || height < MIN_SIZE)
return; return;
var mainWindow = (MainWindow)((Window)((Grid)Parent).Parent).Owner; var time = GetTimeFunc();
var time = TimeSpan.FromMilliseconds(mainWindow.VideoView.MediaPlayer.Time);
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
{ {
Width = width, Width = width,
+22 -5
View File
@@ -1,6 +1,5 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Windows.Media;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Size = System.Windows.Size; using Size = System.Windows.Size;
@@ -11,21 +10,22 @@ namespace Azaion.Annotator.DTO;
public class Config public class Config
{ {
public const string ThumbnailPrefix = "_thumb"; public const string ThumbnailPrefix = "_thumb";
public const string ThumbnailsCacheFile = "thumbnails.cache";
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 string ThumbnailsDirectory { get; set; }
public string UnknownImages { get; set; }
public List<AnnotationClass> AnnotationClasses { get; set; } = []; public List<AnnotationClass> AnnotationClasses { get; set; } = [];
private Dictionary<int, AnnotationClass>? _annotationClassesDict; private Dictionary<int, AnnotationClass>? _annotationClassesDict;
public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id); public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
public Size WindowSize { get; set; } public WindowConfig MainWindowConfig { get; set; }
public Point WindowLocation { get; set; } public WindowConfig DatasetExplorerConfig { get; set; }
public bool FullScreen { get; set; }
public double LeftPanelWidth { get; set; } public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; } public double RightPanelWidth { get; set; }
@@ -38,6 +38,13 @@ public class Config
public ThumbnailConfig ThumbnailConfig { get; set; } public ThumbnailConfig ThumbnailConfig { get; set; }
} }
public class WindowConfig
{
public Size WindowSize { get; set; }
public Point WindowLocation { get; set; }
public bool FullScreen { get; set; }
}
public class ThumbnailConfig public class ThumbnailConfig
{ {
public Size Size { get; set; } public Size Size { get; set; }
@@ -60,6 +67,7 @@ public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfi
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 const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
private const string DEFAULT_UNKNOWN_IMG_DIR = "unknown";
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);
@@ -82,9 +90,18 @@ public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfi
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), ThumbnailsDirectory = Path.Combine(exePath, DEFAULT_THUMBNAILS_DIR),
UnknownImages = Path.Combine(exePath, DEFAULT_UNKNOWN_IMG_DIR),
WindowLocation = DefaultWindowLocation, MainWindowConfig = new WindowConfig
{
WindowSize = DefaultWindowSize, WindowSize = DefaultWindowSize,
WindowLocation = DefaultWindowLocation
},
DatasetExplorerConfig = new WindowConfig
{
WindowSize = DefaultWindowSize,
WindowLocation = DefaultWindowLocation
},
ShowHelpOnStart = true, ShowHelpOnStart = true,
VideoFormats = DefaultVideoFormats, VideoFormats = DefaultVideoFormats,
-2
View File
@@ -6,8 +6,6 @@ namespace Azaion.Annotator.DTO;
public class FormState public class FormState
{ {
public SelectionState SelectionState { get; set; } = SelectionState.None;
public MediaFileInfo? CurrentMedia { get; set; } public MediaFileInfo? CurrentMedia { get; set; }
public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name) public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name)
? "" ? ""
+28 -16
View File
@@ -7,11 +7,16 @@ namespace Azaion.Annotator.DTO;
public abstract class Label public abstract class Label
{ {
[JsonProperty(PropertyName = "cl")] [JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; }
public int ClassNumber { get; set; }
public Label(){} public Label()
public Label(int classNumber) { ClassNumber = classNumber; } {
}
public Label(int classNumber)
{
ClassNumber = classNumber;
}
} }
public class CanvasLabel : Label public class CanvasLabel : Label
@@ -21,7 +26,10 @@ public class CanvasLabel : Label
public double Width { get; set; } public double Width { get; set; }
public double Height { get; set; } public double Height { get; set; }
public CanvasLabel() { } public CanvasLabel()
{
}
public CanvasLabel(int classNumber, double x, double y, double width, double height) : base(classNumber) public CanvasLabel(int classNumber, double x, double y, double width, double height) : base(classNumber)
{ {
X = x; X = x;
@@ -67,19 +75,18 @@ public class CanvasLabel : Label
public class YoloLabel : Label public class YoloLabel : Label
{ {
[JsonProperty(PropertyName = "x")] [JsonProperty(PropertyName = "x")] public double CenterX { get; set; }
public double CenterX { get; set; }
[JsonProperty(PropertyName = "y")] [JsonProperty(PropertyName = "y")] public double CenterY { get; set; }
public double CenterY { get; set; }
[JsonProperty(PropertyName = "w")] [JsonProperty(PropertyName = "w")] public double Width { get; set; }
public double Width { get; set; }
[JsonProperty(PropertyName = "h")] [JsonProperty(PropertyName = "h")] public double Height { get; set; }
public double Height { get; set; }
public YoloLabel()
{
}
public YoloLabel() { }
public YoloLabel(int classNumber, double centerX, double centerY, double width, double height) : base(classNumber) public YoloLabel(int classNumber, double centerX, double centerY, double width, double height) : base(classNumber)
{ {
CenterX = centerX; CenterX = centerX;
@@ -158,6 +165,11 @@ public class YoloLabel : Label
.ToList()!; .ToList()!;
} }
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.'); public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename)
{
var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
await File.WriteAllTextAsync(filename, labelsStr);
}
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
} }
+72 -5
View File
@@ -6,6 +6,7 @@
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
xmlns:local="clr-namespace:Azaion.Annotator" xmlns:local="clr-namespace:Azaion.Annotator"
xmlns:dto="clr-namespace:Azaion.Annotator.DTO" xmlns:dto="clr-namespace:Azaion.Annotator.DTO"
xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
mc:Ignorable="d" mc:Ignorable="d"
Title="Браузер анотацій" Height="900" Width="1200"> Title="Браузер анотацій" Height="900" Width="1200">
@@ -28,16 +29,82 @@
<ColumnDefinition Width="150" /> <ColumnDefinition Width="150" />
<ColumnDefinition Width="4"/> <ColumnDefinition Width="4"/>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<vwp:GridView <controls:AnnotationClasses
x:Name="LvClasses"
Grid.Column="0"
Grid.Row="0">
</controls:AnnotationClasses>
<TabControl
Name="Switcher"
Grid.Column="2" Grid.Column="2"
Grid.Row="0" Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black">
<TabItem Header="Браузер">
<vwp:GridView
Name="ThumbnailsView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black"
Margin="2,5,2,2" Margin="2,5,2,2"
ItemsSource="{Binding ThumbnailsDtos, Mode=OneWay}" ItemsSource="{Binding ThumbnailsDtos, Mode=OneWay}"
ItemTemplate="{StaticResource ThumbnailTemplate}"> ItemTemplate="{StaticResource ThumbnailTemplate}"
>
</vwp:GridView> </vwp:GridView>
</TabItem>
<TabItem Header="Перегляд">
<controls:CanvasEditor x:Name="ExplorerEditor"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" >
</controls:CanvasEditor>
</TabItem>
</TabControl>
<StatusBar
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Background="#252525"
Foreground="White"
>
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="0" Background="Black">
<TextBlock>Loading:</TextBlock>
</StatusBarItem>
<StatusBarItem Grid.Column="1" Background="Black">
<controls:UpdatableProgressBar x:Name="Volume"
Width="150"
Height="15"
HorizontalAlignment="Stretch"
Background="#252525"
BorderBrush="#252525"
Foreground="LightBlue"
Maximum="100"
Minimum="0">
</controls:UpdatableProgressBar>
</StatusBarItem>
<Separator Grid.Column="2"/>
<StatusBarItem Grid.Column="3" Background="Black">
<TextBlock Text="{Binding AnnotationCount}" />
</StatusBarItem>
</StatusBar>
</Grid> </Grid>
</Window> </Window>
+186 -4
View File
@@ -1,39 +1,203 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MessageBox = System.Windows.MessageBox;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public partial class DatasetExplorer public partial class DatasetExplorer
{ {
private readonly Config _config; private readonly Config _config;
private readonly ILogger<DatasetExplorer> _logger;
public ObservableCollection<ThumbnailDto> ThumbnailsDtos { get; set; } = new(); public ObservableCollection<ThumbnailDto> ThumbnailsDtos { get; set; } = new();
private ObservableCollection<AnnotationClass> AllAnnotationClasses { get; set; } = new();
public DatasetExplorer(Config config) private int _tempSelectedClassIdx = 0;
private readonly string _thumbnailsCacheFile;
private IConfigRepository _configRepository;
private readonly FormState _formState;
private static Dictionary<string, List<int>> LabelsCache { get; set; } = new();
public string CurrentImage { get; set; }
public DatasetExplorer(Config config, ILogger<DatasetExplorer> logger, IConfigRepository configRepository, FormState formState)
{ {
_config = config; _config = config;
_logger = logger;
_configRepository = configRepository;
_formState = formState;
_thumbnailsCacheFile = Path.Combine(config.ThumbnailsDirectory, Config.ThumbnailsCacheFile);
if (File.Exists(_thumbnailsCacheFile))
{
var cache = JsonConvert.DeserializeObject<Dictionary<string, List<int>>>(File.ReadAllText(_thumbnailsCacheFile));
LabelsCache = cache ?? new Dictionary<string, List<int>>();
}
InitializeComponent(); InitializeComponent();
DataContext = this; DataContext = this;
Loaded += async (sender, args) => await LoadThumbnails(); Loaded += (_, _) =>
{
AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
new List<AnnotationClass> { new(-1, "All") }
.Concat(_config.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.SelectionChanged += async (_, _) =>
{
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
await SelectClass(selectedClass);
};
LvClasses.SelectedIndex = 0;
SizeChanged += async (_, _) => await SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings();
StateChanged += async (_, _) => await SaveUserSettings();
};
Closing += (sender, args) => Closing += (sender, args) =>
{ {
args.Cancel = true; args.Cancel = true;
Visibility = Visibility.Hidden; Visibility = Visibility.Hidden;
}; };
ThumbnailsView.KeyDown += (sender, args) =>
{
switch (args.Key)
{
case Key.Delete:
DeleteAnnotations();
break;
case Key.Enter:
EditAnnotation();
break;
}
};
ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation();
ExplorerEditor.KeyDown += (_, args) =>
{
var key = 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)
return;
LvClasses.SelectedIndex = keyNumber.Value;
};
ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentImage!)!.Value;
} }
private async Task LoadThumbnails() private async Task EditAnnotation()
{
if (ThumbnailsView.SelectedItem == null)
return;
var dto = (ThumbnailsView.SelectedItem as ThumbnailDto)!;
ExplorerEditor.Background = new ImageBrush
{
ImageSource = new BitmapImage(new Uri(dto.ImagePath))
};
CurrentImage = dto.ImagePath;
Switcher.SelectedIndex = 1;
LvClasses.SelectedIndex = 1;
var time = _formState.GetTime(CurrentImage)!.Value;
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{
var annClass = _config.AnnotationClasses[ann.ClassNumber];
var annInfo = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
Dispatcher.Invoke(() => ExplorerEditor.CreateAnnotation(annClass, time, annInfo));
}
Switcher.SelectionChanged += (_, args) =>
{
//From Explorer to Editor
if (Switcher.SelectedIndex == 1)
{
_tempSelectedClassIdx = LvClasses.SelectedIndex;
LvClasses.ItemsSource = _config.AnnotationClasses;
}
else
{
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();
await ThrottleExt.Throttle(() =>
{
_configRepository.Save(_config);
return Task.CompletedTask;
}, TimeSpan.FromSeconds(5));
}
private void DeleteAnnotations()
{
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
var selected = ThumbnailsView.SelectedItems.Count;
for (var i = 0; i < selected; i++)
{
var dto = (ThumbnailsView.SelectedItems[0] as ThumbnailDto)!;
File.Delete(dto.ImagePath);
File.Delete(dto.LabelPath);
File.Delete(dto.ThumbnailPath);
ThumbnailsDtos.Remove(dto);
}
}
private async Task ReloadThumbnails()
{ {
if (!Directory.Exists(_config.ThumbnailsDirectory)) if (!Directory.Exists(_config.ThumbnailsDirectory))
return; return;
ThumbnailsDtos.Clear();
var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg"); var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg");
var thumbNum = 0;
foreach (var thumbnail in thumbnails) foreach (var thumbnail in thumbnails)
{
await AddThumbnail(thumbnail);
if (thumbNum % 1000 == 0)
await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache));
thumbNum++;
}
await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache));
}
private async Task AddThumbnail(string thumbnail)
{ {
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length]; var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length];
var imageName = Path.Combine(_config.ImagesDirectory, name); var imageName = Path.Combine(_config.ImagesDirectory, name);
@@ -44,12 +208,30 @@ public partial class DatasetExplorer
break; break;
} }
var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt");
if (!LabelsCache.TryGetValue(name, out var classes))
{
if (!File.Exists(labelPath))
{
_logger.LogError($"No label {labelPath} found ! Image {(!File.Exists(imageName) ? "not exists!" : "exists.")}");
return;
}
var labels = await YoloLabel.ReadFromFile(labelPath);
classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
LabelsCache.Add(name, classes);
}
if (classes.Contains(ExplorerEditor.CurrentAnnClass.Id) || ExplorerEditor.CurrentAnnClass.Id == -1)
{
ThumbnailsDtos.Add(new ThumbnailDto ThumbnailsDtos.Add(new ThumbnailDto
{ {
ThumbnailPath = thumbnail, ThumbnailPath = thumbnail,
ImagePath = imageName, ImagePath = imageName,
LabelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"), LabelPath = labelPath
}); });
} }
} }
} }
@@ -7,7 +7,9 @@ public static class ColorExtensions
public static Color ToColor(this int id) public static Color ToColor(this int id)
{ {
var index = id % ColorValues.Length; var index = id % ColorValues.Length;
var hex = $"#40{ColorValues[index]}"; var hex = index == -1
? "#40DDDDDD"
: $"#40{ColorValues[index]}";
var color =(Color)ColorConverter.ConvertFromString(hex); var color =(Color)ColorConverter.ConvertFromString(hex);
return color; return color;
} }
@@ -0,0 +1,15 @@
using System.Windows;
using Azaion.Annotator.DTO;
namespace Azaion.Annotator.Extensions;
public static class WindowExtensions
{
public static WindowConfig GetConfig(this Window window) =>
new()
{
WindowSize = new Size(window.Width, window.Height),
WindowLocation = new Point(window.Left, window.Top),
FullScreen = window.WindowState == WindowState.Maximized
};
}
+11 -2
View File
@@ -42,9 +42,12 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
try try
{ {
var bitmap = await GenerateThumbnail(img); var bitmap = await GenerateThumbnail(img);
if (bitmap != null)
{
var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg"); var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg");
bitmap.Save(thumbnailName, ImageFormat.Jpeg); bitmap.Save(thumbnailName, ImageFormat.Jpeg);
} }
}
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, $"Failed to generate thumbnail for {img.Name}"); logger.LogError(e, $"Failed to generate thumbnail for {img.Name}");
@@ -54,7 +57,7 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
} }
} }
private async Task<Bitmap> GenerateThumbnail(FileInfo img) private async Task<Bitmap?> GenerateThumbnail(FileInfo img)
{ {
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;
@@ -62,7 +65,7 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
var imgName = Path.GetFileNameWithoutExtension(img.Name); var imgName = Path.GetFileNameWithoutExtension(img.Name);
var labelName = Path.Combine(config.LabelsDirectory, $"{imgName}.txt"); var labelName = Path.Combine(config.LabelsDirectory, $"{imgName}.txt");
var originalImage = Image.FromFile(img.FullName); var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(img.FullName)));
var bitmap = new Bitmap(width, height); var bitmap = new Bitmap(width, height);
@@ -72,6 +75,12 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
g.InterpolationMode = InterpolationMode.Default; g.InterpolationMode = InterpolationMode.Default;
var size = new Size(originalImage.Width, originalImage.Height); 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.");
return null;
}
var labels = (await YoloLabel.ReadFromFile(labelName)) var labels = (await YoloLabel.ReadFromFile(labelName))
.Select(x => new CanvasLabel(x, size, size)) .Select(x => new CanvasLabel(x, size, size))
.ToList(); .ToList();
+4 -44
View File
@@ -160,51 +160,11 @@
</GridView> </GridView>
</ListView.View> </ListView.View>
</ListView> </ListView>
<DataGrid x:Name="LvClasses" <controls:AnnotationClasses
x:Name="LvClasses"
Grid.Column="0" Grid.Column="0"
Grid.Row="4" Grid.Row="4">
Background="Black" </controls:AnnotationClasses>
RowBackground="#252525"
Foreground="White"
RowHeaderWidth="0"
Padding="2 0 0 0"
AutoGenerateColumns="False"
SelectionMode="Single"
CellStyle="{DynamicResource DataGridCellStyle1}"
IsReadOnly="True"
CanUserResizeRows="False"
CanUserResizeColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn
Width="50"
Header="Клавіша"
CanUserSort="False">
<DataGridTemplateColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter>
</Style>
</DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Path=ColorBrush}">
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
Header="Назва"
Binding="{Binding Path=Name}"
CanUserSort="False">
<DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter>
</Style>
</DataGridTextColumn.HeaderStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<GridSplitter <GridSplitter
Background="DarkGray" Background="DarkGray"
ResizeDirection="Columns" ResizeDirection="Columns"
+23 -10
View File
@@ -30,7 +30,7 @@ public partial class MainWindow
private readonly IGalleryManager _galleryManager; private readonly IGalleryManager _galleryManager;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new(); private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100); private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
@@ -66,6 +66,15 @@ public partial class MainWindow
VideoView.Loaded += VideoView_Loaded; VideoView.Loaded += VideoView_Loaded;
Closed += OnFormClosed; Closed += OnFormClosed;
if (!Directory.Exists(_config.LabelsDirectory))
Directory.CreateDirectory(_config.LabelsDirectory);
if (!Directory.Exists(_config.ImagesDirectory))
Directory.CreateDirectory(_config.ImagesDirectory);
if (!Directory.Exists(_config.ResultsDirectory))
Directory.CreateDirectory(_config.ResultsDirectory);
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
} }
private void VideoView_Loaded(object sender, RoutedEventArgs e) private void VideoView_Loaded(object sender, RoutedEventArgs e)
@@ -84,16 +93,22 @@ public partial class MainWindow
_suspendLayout = true; _suspendLayout = true;
Left = _config.WindowLocation.X; Left = _config.MainWindowConfig.WindowLocation.X;
Top = _config.WindowLocation.Y; Top = _config.MainWindowConfig.WindowLocation.Y;
Width = _config.MainWindowConfig.WindowSize.Width;
Height = _config.MainWindowConfig.WindowSize.Height;
Width = _config.WindowSize.Width; _datasetExplorer.Left = _config.MainWindowConfig.WindowLocation.X;
Height = _config.WindowSize.Height; _datasetExplorer.Top = _config.DatasetExplorerConfig.WindowLocation.Y;
_datasetExplorer.Width = _config.DatasetExplorerConfig.WindowSize.Width;
_datasetExplorer.Height = _config.DatasetExplorerConfig.WindowSize.Height;
if (_config.DatasetExplorerConfig.FullScreen)
_datasetExplorer.WindowState = WindowState.Maximized;
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_config.LeftPanelWidth); MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_config.LeftPanelWidth);
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_config.RightPanelWidth); MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_config.RightPanelWidth);
if (_config.FullScreen) if (_config.MainWindowConfig.FullScreen)
WindowState = WindowState.Maximized; WindowState = WindowState.Maximized;
_suspendLayout = false; _suspendLayout = false;
@@ -190,9 +205,8 @@ public partial class MainWindow
_config.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _config.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
_config.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; _config.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
_config.WindowSize = new Size(Width, Height);
_config.WindowLocation = new Point(Left, Top); _config.MainWindowConfig = this.GetConfig();
_config.FullScreen = WindowState == WindowState.Maximized;
await ThrottleExt.Throttle(() => await ThrottleExt.Throttle(() =>
{ {
_configRepository.Save(_config); _configRepository.Save(_config);
@@ -303,7 +317,6 @@ public partial class MainWindow
_mediaPlayer.Stop(); _mediaPlayer.Stop();
_mediaPlayer.Dispose(); _mediaPlayer.Dispose();
_libVLC.Dispose(); _libVLC.Dispose();
_config.AnnotationClasses = AnnotationClasses.ToList();
_configRepository.Save(_config); _configRepository.Save(_config);
Application.Current.Shutdown(); Application.Current.Shutdown();
} }
+2 -11
View File
@@ -69,8 +69,6 @@ public class PlayerControlHandler :
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}");
var key = notification.Args.Key; var key = notification.Args.Key;
var keyNumber = (int?)null; var keyNumber = (int?)null;
@@ -79,7 +77,7 @@ public class PlayerControlHandler :
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((AnnotationClass)_mainWindow.LvClasses.Items[keyNumber.Value]);
if (_keysControlEnumDict.TryGetValue(key, out var value)) if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value); await ControlPlayback(value);
@@ -234,16 +232,9 @@ public class PlayerControlHandler :
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()));
if (!Directory.Exists(_config.LabelsDirectory)) await YoloLabel.WriteToFile(currentAnns, Path.Combine(_config.LabelsDirectory, $"{fName}.txt"));
Directory.CreateDirectory(_config.LabelsDirectory);
if (!Directory.Exists(_config.ImagesDirectory))
Directory.CreateDirectory(_config.ImagesDirectory);
if (!Directory.Exists(_config.ResultsDirectory))
Directory.CreateDirectory(_config.ResultsDirectory);
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);
+10 -2
View File
@@ -3,6 +3,7 @@
"LabelsDirectory": "E:\\labels", "LabelsDirectory": "E:\\labels",
"ImagesDirectory": "E:\\images", "ImagesDirectory": "E:\\images",
"ResultsDirectory": "E:\\results", "ResultsDirectory": "E:\\results",
"UnknownImages": "E:\\unknown",
"ThumbnailsDirectory": "E:\\thumbnails", "ThumbnailsDirectory": "E:\\thumbnails",
"AnnotationClasses": [ "AnnotationClasses": [
{ "Id": 0, "Name": "Броньована техніка", "Color": "#40FF0000" }, { "Id": 0, "Name": "Броньована техніка", "Color": "#40FF0000" },
@@ -16,13 +17,20 @@
{ "Id": 8, "Name": "Танк з захистом", "Color": "#40008000" }, { "Id": 8, "Name": "Танк з захистом", "Color": "#40008000" },
{ "Id": 9, "Name": "Дим", "Color": "#40000080" } { "Id": 9, "Name": "Дим", "Color": "#40000080" }
], ],
"MainWindowConfig": {
"WindowSize": "1920,1080", "WindowSize": "1920,1080",
"WindowLocation": "200,121", "WindowLocation": "50,50",
"FullScreen": true
},
"DatasetExplorerConfig": {
"WindowSize": "1920,1080",
"WindowLocation": "50,50",
"FullScreen": true
},
"ThumbnailConfig": { "ThumbnailConfig": {
"Size": "480,270", "Size": "480,270",
"Border": 10 "Border": 10
}, },
"FullScreen": true,
"LeftPanelWidth": 300, "LeftPanelWidth": 300,
"RightPanelWidth": 300, "RightPanelWidth": 300,
"ShowHelpOnStart": false, "ShowHelpOnStart": false,