mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 09:56:31 +00:00
add editor, fix some bugs
WIP
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<None Update="config.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</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();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace Azaion.Annotator.Controls;
|
||||
public class CanvasEditor : Canvas
|
||||
{
|
||||
private Point _lastPos;
|
||||
public SelectionState SelectionState { get; set; } = SelectionState.None;
|
||||
|
||||
private readonly Rectangle _newAnnotationRect;
|
||||
private Point _newAnnotationStartPos;
|
||||
@@ -30,6 +31,19 @@ public class CanvasEditor : Canvas
|
||||
public FormState FormState { 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!;
|
||||
public AnnotationClass CurrentAnnClass
|
||||
{
|
||||
@@ -84,11 +98,7 @@ public class CanvasEditor : Canvas
|
||||
Stroke = new SolidColorBrush(Colors.Gray),
|
||||
Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)),
|
||||
};
|
||||
Loaded += Init;
|
||||
}
|
||||
|
||||
private void Init(object sender, RoutedEventArgs e)
|
||||
{
|
||||
KeyDown += (_, args) =>
|
||||
{
|
||||
Console.WriteLine($"pressed {args.Key}");
|
||||
@@ -98,15 +108,21 @@ public class CanvasEditor : Canvas
|
||||
MouseUp += CanvasMouseUp;
|
||||
SizeChanged += CanvasResized;
|
||||
Cursor = Cursors.Cross;
|
||||
_horizontalLine.X1 = 0;
|
||||
_horizontalLine.X2 = ActualWidth;
|
||||
_verticalLine.Y1 = 0;
|
||||
_verticalLine.Y2 = ActualHeight;
|
||||
|
||||
Children.Add(_newAnnotationRect);
|
||||
Children.Add(_horizontalLine);
|
||||
Children.Add(_verticalLine);
|
||||
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)
|
||||
@@ -125,22 +141,22 @@ public class CanvasEditor : Canvas
|
||||
|
||||
if (e.LeftButton != MouseButtonState.Pressed)
|
||||
return;
|
||||
if (FormState.SelectionState == SelectionState.NewAnnCreating)
|
||||
if (SelectionState == SelectionState.NewAnnCreating)
|
||||
NewAnnotationCreatingMove(sender, e);
|
||||
|
||||
if (FormState.SelectionState == SelectionState.AnnResizing)
|
||||
if (SelectionState == SelectionState.AnnResizing)
|
||||
AnnotationResizeMove(sender, e);
|
||||
|
||||
if (FormState.SelectionState == SelectionState.AnnMoving)
|
||||
if (SelectionState == SelectionState.AnnMoving)
|
||||
AnnotationPositionMove(sender, e);
|
||||
}
|
||||
|
||||
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (FormState.SelectionState == SelectionState.NewAnnCreating)
|
||||
if (SelectionState == SelectionState.NewAnnCreating)
|
||||
CreateAnnotation(e.GetPosition(this));
|
||||
|
||||
FormState.SelectionState = SelectionState.None;
|
||||
SelectionState = SelectionState.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
@@ -154,7 +170,7 @@ public class CanvasEditor : Canvas
|
||||
|
||||
private void AnnotationResizeStart(object sender, MouseEventArgs e)
|
||||
{
|
||||
FormState.SelectionState = SelectionState.AnnResizing;
|
||||
SelectionState = SelectionState.AnnResizing;
|
||||
_lastPos = e.GetPosition(this);
|
||||
_curRec = (Rectangle)sender;
|
||||
_curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent;
|
||||
@@ -163,7 +179,7 @@ public class CanvasEditor : Canvas
|
||||
|
||||
private void AnnotationResizeMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (FormState.SelectionState != SelectionState.AnnResizing)
|
||||
if (SelectionState != SelectionState.AnnResizing)
|
||||
return;
|
||||
|
||||
var currentPos = e.GetPosition(this);
|
||||
@@ -224,13 +240,13 @@ public class CanvasEditor : Canvas
|
||||
|
||||
_curAnn.IsSelected = true;
|
||||
|
||||
FormState.SelectionState = SelectionState.AnnMoving;
|
||||
SelectionState = SelectionState.AnnMoving;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void AnnotationPositionMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (FormState.SelectionState != SelectionState.AnnMoving)
|
||||
if (SelectionState != SelectionState.AnnMoving)
|
||||
return;
|
||||
|
||||
var currentPos = e.GetPosition(this);
|
||||
@@ -255,12 +271,12 @@ public class CanvasEditor : Canvas
|
||||
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
|
||||
_newAnnotationRect.MouseMove += NewAnnotationCreatingMove;
|
||||
|
||||
FormState.SelectionState = SelectionState.NewAnnCreating;
|
||||
SelectionState = SelectionState.NewAnnCreating;
|
||||
}
|
||||
|
||||
private void NewAnnotationCreatingMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (FormState.SelectionState != SelectionState.NewAnnCreating)
|
||||
if (SelectionState != SelectionState.NewAnnCreating)
|
||||
return;
|
||||
|
||||
var currentPos = e.GetPosition(this);
|
||||
@@ -284,8 +300,7 @@ public class CanvasEditor : Canvas
|
||||
if (width < MIN_SIZE || height < MIN_SIZE)
|
||||
return;
|
||||
|
||||
var mainWindow = (MainWindow)((Window)((Grid)Parent).Parent).Owner;
|
||||
var time = TimeSpan.FromMilliseconds(mainWindow.VideoView.MediaPlayer.Time);
|
||||
var time = GetTimeFunc();
|
||||
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
|
||||
{
|
||||
Width = width,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Size = System.Windows.Size;
|
||||
@@ -11,21 +10,22 @@ namespace Azaion.Annotator.DTO;
|
||||
public class Config
|
||||
{
|
||||
public const string ThumbnailPrefix = "_thumb";
|
||||
public const string ThumbnailsCacheFile = "thumbnails.cache";
|
||||
|
||||
public string VideosDirectory { get; set; }
|
||||
public string LabelsDirectory { get; set; }
|
||||
public string ImagesDirectory { get; set; }
|
||||
public string ResultsDirectory { get; set; }
|
||||
public string ThumbnailsDirectory { get; set; }
|
||||
public string UnknownImages { get; set; }
|
||||
|
||||
public List<AnnotationClass> AnnotationClasses { get; set; } = [];
|
||||
|
||||
private Dictionary<int, AnnotationClass>? _annotationClassesDict;
|
||||
public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
|
||||
|
||||
public Size WindowSize { get; set; }
|
||||
public Point WindowLocation { get; set; }
|
||||
public bool FullScreen { get; set; }
|
||||
public WindowConfig MainWindowConfig { get; set; }
|
||||
public WindowConfig DatasetExplorerConfig { get; set; }
|
||||
|
||||
public double LeftPanelWidth { get; set; }
|
||||
public double RightPanelWidth { get; set; }
|
||||
@@ -38,6 +38,13 @@ public class Config
|
||||
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 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_RESULTS_DIR = "results";
|
||||
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 Point DefaultWindowLocation = new(100, 100);
|
||||
@@ -82,9 +90,18 @@ public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfi
|
||||
ImagesDirectory = Path.Combine(exePath, DEFAULT_IMAGES_DIR),
|
||||
ResultsDirectory = Path.Combine(exePath, DEFAULT_RESULTS_DIR),
|
||||
ThumbnailsDirectory = Path.Combine(exePath, DEFAULT_THUMBNAILS_DIR),
|
||||
UnknownImages = Path.Combine(exePath, DEFAULT_UNKNOWN_IMG_DIR),
|
||||
|
||||
WindowLocation = DefaultWindowLocation,
|
||||
MainWindowConfig = new WindowConfig
|
||||
{
|
||||
WindowSize = DefaultWindowSize,
|
||||
WindowLocation = DefaultWindowLocation
|
||||
},
|
||||
DatasetExplorerConfig = new WindowConfig
|
||||
{
|
||||
WindowSize = DefaultWindowSize,
|
||||
WindowLocation = DefaultWindowLocation
|
||||
},
|
||||
ShowHelpOnStart = true,
|
||||
|
||||
VideoFormats = DefaultVideoFormats,
|
||||
|
||||
@@ -6,8 +6,6 @@ namespace Azaion.Annotator.DTO;
|
||||
|
||||
public class FormState
|
||||
{
|
||||
public SelectionState SelectionState { get; set; } = SelectionState.None;
|
||||
|
||||
public MediaFileInfo? CurrentMedia { get; set; }
|
||||
public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name)
|
||||
? ""
|
||||
|
||||
@@ -7,11 +7,16 @@ namespace Azaion.Annotator.DTO;
|
||||
|
||||
public abstract class Label
|
||||
{
|
||||
[JsonProperty(PropertyName = "cl")]
|
||||
public int ClassNumber { get; set; }
|
||||
[JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; }
|
||||
|
||||
public Label(){}
|
||||
public Label(int classNumber) { ClassNumber = classNumber; }
|
||||
public Label()
|
||||
{
|
||||
}
|
||||
|
||||
public Label(int classNumber)
|
||||
{
|
||||
ClassNumber = classNumber;
|
||||
}
|
||||
}
|
||||
|
||||
public class CanvasLabel : Label
|
||||
@@ -21,7 +26,10 @@ public class CanvasLabel : Label
|
||||
public double Width { 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)
|
||||
{
|
||||
X = x;
|
||||
@@ -67,19 +75,18 @@ public class CanvasLabel : Label
|
||||
|
||||
public class YoloLabel : Label
|
||||
{
|
||||
[JsonProperty(PropertyName = "x")]
|
||||
public double CenterX { get; set; }
|
||||
[JsonProperty(PropertyName = "x")] public double CenterX { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "y")]
|
||||
public double CenterY { get; set; }
|
||||
[JsonProperty(PropertyName = "y")] public double CenterY { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "w")]
|
||||
public double Width { get; set; }
|
||||
[JsonProperty(PropertyName = "w")] public double Width { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "h")]
|
||||
public double Height { get; set; }
|
||||
[JsonProperty(PropertyName = "h")] public double Height { get; set; }
|
||||
|
||||
public YoloLabel()
|
||||
{
|
||||
}
|
||||
|
||||
public YoloLabel() { }
|
||||
public YoloLabel(int classNumber, double centerX, double centerY, double width, double height) : base(classNumber)
|
||||
{
|
||||
CenterX = centerX;
|
||||
@@ -158,6 +165,11 @@ public class YoloLabel : Label
|
||||
.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(',', '.');
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
|
||||
xmlns:local="clr-namespace:Azaion.Annotator"
|
||||
xmlns:dto="clr-namespace:Azaion.Annotator.DTO"
|
||||
xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
|
||||
mc:Ignorable="d"
|
||||
Title="Браузер анотацій" Height="900" Width="1200">
|
||||
|
||||
@@ -28,16 +29,82 @@
|
||||
<ColumnDefinition Width="150" />
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition Width="200" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<vwp:GridView
|
||||
<controls:AnnotationClasses
|
||||
x:Name="LvClasses"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0">
|
||||
</controls:AnnotationClasses>
|
||||
|
||||
<TabControl
|
||||
Name="Switcher"
|
||||
Grid.Column="2"
|
||||
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"
|
||||
ItemsSource="{Binding ThumbnailsDtos, Mode=OneWay}"
|
||||
ItemTemplate="{StaticResource ThumbnailTemplate}">
|
||||
|
||||
ItemTemplate="{StaticResource ThumbnailTemplate}"
|
||||
>
|
||||
</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>
|
||||
</Window>
|
||||
|
||||
@@ -1,39 +1,203 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public partial class DatasetExplorer
|
||||
{
|
||||
private readonly Config _config;
|
||||
private readonly ILogger<DatasetExplorer> _logger;
|
||||
|
||||
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;
|
||||
_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();
|
||||
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) =>
|
||||
{
|
||||
args.Cancel = true;
|
||||
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))
|
||||
return;
|
||||
|
||||
ThumbnailsDtos.Clear();
|
||||
var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg");
|
||||
|
||||
var thumbNum = 0;
|
||||
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 imageName = Path.Combine(_config.ImagesDirectory, name);
|
||||
@@ -44,12 +208,30 @@ public partial class DatasetExplorer
|
||||
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
|
||||
{
|
||||
ThumbnailPath = thumbnail,
|
||||
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)
|
||||
{
|
||||
var index = id % ColorValues.Length;
|
||||
var hex = $"#40{ColorValues[index]}";
|
||||
var hex = index == -1
|
||||
? "#40DDDDDD"
|
||||
: $"#40{ColorValues[index]}";
|
||||
var color =(Color)ColorConverter.ConvertFromString(hex);
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -42,9 +42,12 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
|
||||
try
|
||||
{
|
||||
var bitmap = await GenerateThumbnail(img);
|
||||
if (bitmap != null)
|
||||
{
|
||||
var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg");
|
||||
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
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 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 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);
|
||||
|
||||
@@ -72,6 +75,12 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
|
||||
g.InterpolationMode = InterpolationMode.Default;
|
||||
|
||||
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))
|
||||
.Select(x => new CanvasLabel(x, size, size))
|
||||
.ToList();
|
||||
|
||||
@@ -160,51 +160,11 @@
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
<DataGrid x:Name="LvClasses"
|
||||
<controls:AnnotationClasses
|
||||
x:Name="LvClasses"
|
||||
Grid.Column="0"
|
||||
Grid.Row="4"
|
||||
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>
|
||||
Grid.Row="4">
|
||||
</controls:AnnotationClasses>
|
||||
<GridSplitter
|
||||
Background="DarkGray"
|
||||
ResizeDirection="Columns"
|
||||
|
||||
@@ -30,7 +30,7 @@ public partial class MainWindow
|
||||
private readonly IGalleryManager _galleryManager;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
|
||||
private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
|
||||
private bool _suspendLayout;
|
||||
|
||||
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
|
||||
@@ -66,6 +66,15 @@ public partial class MainWindow
|
||||
|
||||
VideoView.Loaded += VideoView_Loaded;
|
||||
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)
|
||||
@@ -84,16 +93,22 @@ public partial class MainWindow
|
||||
|
||||
_suspendLayout = true;
|
||||
|
||||
Left = _config.WindowLocation.X;
|
||||
Top = _config.WindowLocation.Y;
|
||||
Left = _config.MainWindowConfig.WindowLocation.X;
|
||||
Top = _config.MainWindowConfig.WindowLocation.Y;
|
||||
Width = _config.MainWindowConfig.WindowSize.Width;
|
||||
Height = _config.MainWindowConfig.WindowSize.Height;
|
||||
|
||||
Width = _config.WindowSize.Width;
|
||||
Height = _config.WindowSize.Height;
|
||||
_datasetExplorer.Left = _config.MainWindowConfig.WindowLocation.X;
|
||||
_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.LastOrDefault()!.Width = new GridLength(_config.RightPanelWidth);
|
||||
|
||||
if (_config.FullScreen)
|
||||
if (_config.MainWindowConfig.FullScreen)
|
||||
WindowState = WindowState.Maximized;
|
||||
|
||||
_suspendLayout = false;
|
||||
@@ -190,9 +205,8 @@ public partial class MainWindow
|
||||
|
||||
_config.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
||||
_config.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
||||
_config.WindowSize = new Size(Width, Height);
|
||||
_config.WindowLocation = new Point(Left, Top);
|
||||
_config.FullScreen = WindowState == WindowState.Maximized;
|
||||
|
||||
_config.MainWindowConfig = this.GetConfig();
|
||||
await ThrottleExt.Throttle(() =>
|
||||
{
|
||||
_configRepository.Save(_config);
|
||||
@@ -303,7 +317,6 @@ public partial class MainWindow
|
||||
_mediaPlayer.Stop();
|
||||
_mediaPlayer.Dispose();
|
||||
_libVLC.Dispose();
|
||||
_config.AnnotationClasses = AnnotationClasses.ToList();
|
||||
_configRepository.Save(_config);
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
@@ -69,8 +69,6 @@ public class PlayerControlHandler :
|
||||
|
||||
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 keyNumber = (int?)null;
|
||||
|
||||
@@ -79,7 +77,7 @@ public class PlayerControlHandler :
|
||||
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
|
||||
keyNumber = key - Key.NumPad1;
|
||||
if (keyNumber.HasValue)
|
||||
SelectClass(_mainWindow.AnnotationClasses[keyNumber.Value]);
|
||||
SelectClass((AnnotationClass)_mainWindow.LvClasses.Items[keyNumber.Value]);
|
||||
|
||||
if (_keysControlEnumDict.TryGetValue(key, out var value))
|
||||
await ControlPlayback(value);
|
||||
@@ -234,16 +232,9 @@ public class PlayerControlHandler :
|
||||
var currentAnns = _mainWindow.Editor.CurrentAnns
|
||||
.Select(x => new YoloLabel(x.Info, _mainWindow.Editor.RenderSize, _formState.CurrentVideoSize))
|
||||
.ToList();
|
||||
var labels = string.Join(Environment.NewLine, currentAnns.Select(x => x.ToString()));
|
||||
|
||||
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);
|
||||
await YoloLabel.WriteToFile(currentAnns, Path.Combine(_config.LabelsDirectory, $"{fName}.txt"));
|
||||
|
||||
await File.WriteAllTextAsync(Path.Combine(_config.LabelsDirectory, $"{fName}.txt"), labels);
|
||||
var resultHeight = (uint)Math.Round(RESULT_WIDTH / _formState.CurrentVideoSize.Width * _formState.CurrentVideoSize.Height);
|
||||
|
||||
await _mainWindow.AddAnnotation(time, currentAnns);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"LabelsDirectory": "E:\\labels",
|
||||
"ImagesDirectory": "E:\\images",
|
||||
"ResultsDirectory": "E:\\results",
|
||||
"UnknownImages": "E:\\unknown",
|
||||
"ThumbnailsDirectory": "E:\\thumbnails",
|
||||
"AnnotationClasses": [
|
||||
{ "Id": 0, "Name": "Броньована техніка", "Color": "#40FF0000" },
|
||||
@@ -16,13 +17,20 @@
|
||||
{ "Id": 8, "Name": "Танк з захистом", "Color": "#40008000" },
|
||||
{ "Id": 9, "Name": "Дим", "Color": "#40000080" }
|
||||
],
|
||||
"MainWindowConfig": {
|
||||
"WindowSize": "1920,1080",
|
||||
"WindowLocation": "200,121",
|
||||
"WindowLocation": "50,50",
|
||||
"FullScreen": true
|
||||
},
|
||||
"DatasetExplorerConfig": {
|
||||
"WindowSize": "1920,1080",
|
||||
"WindowLocation": "50,50",
|
||||
"FullScreen": true
|
||||
},
|
||||
"ThumbnailConfig": {
|
||||
"Size": "480,270",
|
||||
"Border": 10
|
||||
},
|
||||
"FullScreen": true,
|
||||
"LeftPanelWidth": 300,
|
||||
"RightPanelWidth": 300,
|
||||
"ShowHelpOnStart": false,
|
||||
|
||||
Reference in New Issue
Block a user