Add annotationResult and put them into json

This commit is contained in:
Oleksandr Bezdieniezhnykh
2024-07-16 14:06:05 +03:00
parent ea8d0e686a
commit 71006a2462
16 changed files with 445 additions and 252 deletions
+4 -4
View File
@@ -5,16 +5,16 @@ namespace Azaion.Annotator.Test;
public class DictTest public class DictTest
{ {
public Dictionary<string, List<AnnotationInfo>> Annotations = new(); public Dictionary<string, List<YoloLabel>> Annotations = new();
[Fact] [Fact]
public void DictAddTest() public void DictAddTest()
{ {
Annotations["sd"] = [new AnnotationInfo(1, 2, 2, 2, 2)]; Annotations["sd"] = [new YoloLabel(1, 2, 2, 2, 2)];
Annotations["sd"] = Annotations["sd"] =
[ [
new AnnotationInfo(1, 2, 2, 2, 2), new YoloLabel(1, 2, 2, 2, 2),
new AnnotationInfo(0, 1, 3, 2, 1) new YoloLabel(0, 1, 3, 2, 1)
]; ];
} }
} }
+16 -2
View File
@@ -4,19 +4,33 @@ using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading; using System.Windows.Threading;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public partial class App : Application public partial class App : Application
{ {
private readonly ServiceProvider _serviceProvider; private readonly ServiceProvider _serviceProvider;
private IMediator _mediator; private readonly IMediator _mediator;
public App() public App()
{ {
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Warning()
.WriteTo.Console()
.WriteTo.File(
path: "Logs/log.txt",
rollingInterval: RollingInterval.Day)
.CreateLogger();
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton<MainWindow>(); services.AddSingleton<MainWindow>();
services.AddSingleton<HelpWindow>(); services.AddSingleton<HelpWindow>();
@@ -37,7 +51,7 @@ public partial class App : Application
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{ {
File.AppendAllText("logs.txt", e.Exception.Message); Log.Logger.Error(e.Exception, e.Exception.Message);
e.Handled = true; e.Handled = true;
} }
+6
View File
@@ -15,7 +15,13 @@
<PackageReference Include="LibVLCSharp.WPF" Version="3.8.2" /> <PackageReference Include="LibVLCSharp.WPF" Version="3.8.2" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.20" /> <PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.20" />
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" /> <PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
</ItemGroup> </ItemGroup>
@@ -14,6 +14,7 @@ public class AnnotationControl : Border
private readonly Grid _grid; private readonly Grid _grid;
private readonly TextBlock _classNameLabel; private readonly TextBlock _classNameLabel;
public TimeSpan Time { get; set; }
private AnnotationClass _annotationClass = null!; private AnnotationClass _annotationClass = null!;
public AnnotationClass AnnotationClass public AnnotationClass AnnotationClass
@@ -40,8 +41,9 @@ public class AnnotationControl : Border
} }
} }
public AnnotationControl(AnnotationClass annotationClass, Action<object, MouseButtonEventArgs> resizeStart) public AnnotationControl(AnnotationClass annotationClass, TimeSpan time, Action<object, MouseButtonEventArgs> resizeStart)
{ {
Time = time;
_resizeStart = resizeStart; _resizeStart = resizeStart;
_classNameLabel = new TextBlock _classNameLabel = new TextBlock
{ {
@@ -104,5 +106,5 @@ public class AnnotationControl : Border
return rect; return rect;
} }
public AnnotationInfo Info => new(AnnotationClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height); public CanvasLabel Info => new(AnnotationClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
} }
+21 -18
View File
@@ -1,6 +1,5 @@
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Shapes; using System.Windows.Shapes;
@@ -26,6 +25,7 @@ public class CanvasEditor : Canvas
private AnnotationControl _curAnn = null!; private AnnotationControl _curAnn = null!;
private const int MIN_SIZE = 20; private const int MIN_SIZE = 20;
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
public FormState FormState { get; set; } = null!; public FormState FormState { get; set; } = null!;
public IMediator Mediator { get; set; } = null!; public IMediator Mediator { get; set; } = null!;
@@ -284,29 +284,27 @@ public class CanvasEditor : Canvas
if (width < MIN_SIZE || height < MIN_SIZE) if (width < MIN_SIZE || height < MIN_SIZE)
return; return;
var annotationControl = new AnnotationControl(CurrentAnnClass, AnnotationResizeStart) var mainWindow = (MainWindow)((Window)((Grid)Parent).Parent).Owner;
var time = TimeSpan.FromMilliseconds(mainWindow.VideoView.MediaPlayer.Time);
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
{ {
Width = width, Width = width,
Height = height Height = height,
}; X = Math.Min(endPos.X, _newAnnotationStartPos.X),
annotationControl.MouseDown += AnnotationPositionStart; Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y)
SetLeft(annotationControl, Math.Min(endPos.X, _newAnnotationStartPos.X)); });
SetTop(annotationControl, Math.Min(endPos.Y, _newAnnotationStartPos.Y));
Children.Add(annotationControl);
CurrentAnns.Add(annotationControl);
_newAnnotationRect.Fill = new SolidColorBrush(CurrentAnnClass.Color);
} }
public AnnotationControl CreateAnnotation(AnnotationClass annClass, AnnotationInfo info) public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan time, CanvasLabel canvasLabel)
{ {
var annotationControl = new AnnotationControl(annClass, AnnotationResizeStart) var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart)
{ {
Width = info.Width, Width = canvasLabel.Width,
Height = info.Height Height = canvasLabel.Height
}; };
annotationControl.MouseDown += AnnotationPositionStart; annotationControl.MouseDown += AnnotationPositionStart;
SetLeft(annotationControl, info.X ); SetLeft(annotationControl, canvasLabel.X );
SetTop(annotationControl, info.Y); SetTop(annotationControl, canvasLabel.Y);
Children.Add(annotationControl); Children.Add(annotationControl);
CurrentAnns.Add(annotationControl); CurrentAnns.Add(annotationControl);
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color); _newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
@@ -323,12 +321,17 @@ public class CanvasEditor : Canvas
CurrentAnns.Remove(ann); CurrentAnns.Remove(ann);
} }
} }
public void RemoveAllAnns() => RemoveAnnotations(CurrentAnns.ToList()); public void RemoveAllAnns() => RemoveAnnotations(CurrentAnns);
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList()); public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected));
private void ClearSelections() private void ClearSelections()
{ {
foreach (var ann in CurrentAnns) foreach (var ann in CurrentAnns)
ann.IsSelected = false; ann.IsSelected = false;
} }
public void ClearExpiredAnnotations(TimeSpan time)
{
RemoveAnnotations(CurrentAnns.Where(x => time - x.Time > _viewThreshold));
}
} }
-121
View File
@@ -1,121 +0,0 @@
using System.Globalization;
using System.Windows;
namespace Azaion.Annotator.DTO;
public class AnnotationInfo
{
public int ClassNumber { get; set; }
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public AnnotationInfo() { }
public AnnotationInfo(int classNumber, double x, double y, double width, double height)
{
ClassNumber = classNumber;
X = x;
Y = y;
Width = width;
Height = height;
}
public override string ToString() => $"{ClassNumber} {X:F5} {Y:F5} {Width:F5} {Height:F5}".Replace(',', '.');
public AnnotationInfo ToLabelCoordinates(Size canvasSize, Size videoSize)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAR = cw / ch;
var videoAR = videoSize.Width / videoSize.Height;
var annInfo = new AnnotationInfo { ClassNumber = this.ClassNumber };
double left, top;
if (videoAR > canvasAR) //100% width
{
left = X / cw;
annInfo.Width = Width / cw;
var realHeight = cw / videoAR; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
top = (Y - blackStripHeight) / realHeight;
annInfo.Height = Height / realHeight;
}
else //100% height
{
top = Y / ch;
annInfo.Height = Height / ch;
var realWidth = ch * videoAR; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
left = (X - blackStripWidth) / realWidth;
annInfo.Width = Width / realWidth;
}
annInfo.X = left + annInfo.Width / 2.0;
annInfo.Y = top + annInfo.Height / 2.0;
return annInfo;
}
public AnnotationInfo ToCanvasCoordinates(Size canvasSize, Size videoSize)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAR = cw / ch;
var videoAR = videoSize.Width / videoSize.Height;
var annInfo = new AnnotationInfo { ClassNumber = this.ClassNumber };
double left = X - Width / 2;
double top = Y - Height / 2;
if (videoAR > canvasAR) //100% width
{
var realHeight = cw / videoAR; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
annInfo.X = left * cw;
annInfo.Y = top * realHeight + blackStripHeight;
annInfo.Width = Width * cw;
annInfo.Height = Height * realHeight;
}
else //100% height
{
var realWidth = ch * videoAR; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
annInfo.X = left * realWidth + blackStripWidth;
annInfo.Y = top * ch;
annInfo.Width = Width * realWidth;
annInfo.Height = Height * ch;
}
return annInfo;
}
public static AnnotationInfo? Parse(string? s)
{
if (s == null || string.IsNullOrEmpty(s))
return null;
var strs = s.Replace(',','.').Split(' ');
if (strs.Length != 5)
return null;
try
{
var res = new AnnotationInfo
{
ClassNumber = int.Parse(strs[0], CultureInfo.InvariantCulture),
X = double.Parse(strs[1], CultureInfo.InvariantCulture),
Y = double.Parse(strs[2], CultureInfo.InvariantCulture),
Width = double.Parse(strs[3], CultureInfo.InvariantCulture),
Height = double.Parse(strs[4], CultureInfo.InvariantCulture)
};
return res;
}
catch (Exception)
{
return null;
}
}
}
+28
View File
@@ -0,0 +1,28 @@
using Newtonsoft.Json;
namespace Azaion.Annotator.DTO;
public class AnnotationResult
{
[JsonProperty(PropertyName = "f")]
public string Image { get; set; } = null!;
[JsonProperty(PropertyName = "t")]
public TimeSpan Time { get; set; }
[JsonProperty(PropertyName = "p")]
public double Percentage { get; set; }
public double Lat { get; set; }
public double Lon { get; set; }
public List<YoloLabel> Labels { get; set; } = new();
public AnnotationResult() { }
public AnnotationResult(TimeSpan time, string timeName, List<YoloLabel> labels)
{
Labels = labels;
Time = time;
Image = $"{timeName}.jpg";
Percentage = 100;
}
}
+5
View File
@@ -10,9 +10,13 @@ namespace Azaion.Annotator.DTO;
public class Config public class Config
{ {
private const string CONFIG_PATH = "config.json"; private const string CONFIG_PATH = "config.json";
private const string DEFAULT_VIDEO_DIR = "video"; private const string DEFAULT_VIDEO_DIR = "video";
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 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);
@@ -20,6 +24,7 @@ public class Config
public string VideosDirectory { get; set; } = DEFAULT_VIDEO_DIR; public string VideosDirectory { get; set; } = DEFAULT_VIDEO_DIR;
public string LabelsDirectory { get; set; } = DEFAULT_LABELS_DIR; public string LabelsDirectory { get; set; } = DEFAULT_LABELS_DIR;
public string ImagesDirectory { get; set; } = DEFAULT_IMAGES_DIR; public string ImagesDirectory { get; set; } = DEFAULT_IMAGES_DIR;
public string ResultsDirectory { get; set; } = DEFAULT_RESULTS_DIR;
public List<AnnotationClass> AnnotationClasses { get; set; } = []; public List<AnnotationClass> AnnotationClasses { get; set; } = [];
public Size WindowSize { get; set; } public Size WindowSize { get; set; }
+22 -1
View File
@@ -1,4 +1,5 @@
using System.IO; using System.Globalization;
using System.IO;
using System.Windows; using System.Windows;
namespace Azaion.Annotator.DTO; namespace Azaion.Annotator.DTO;
@@ -12,6 +13,26 @@ public class FormState
public string VideoName => Path.GetFileNameWithoutExtension(CurrentFile).Replace(" ", ""); public string VideoName => Path.GetFileNameWithoutExtension(CurrentFile).Replace(" ", "");
public TimeSpan CurrentVideoLength { get; set; } public TimeSpan CurrentVideoLength { get; set; }
public int CurrentVolume { get; set; } = 100; public int CurrentVolume { get; set; } = 100;
public List<AnnotationResult> AnnotationResults { get; set; } = [];
public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}"; public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}";
public TimeSpan? GetTime(string name)
{
var timeStr = name.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr))
return null;
try
{
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
return new TimeSpan(
days: 0,
hours: int.Parse(timeStr[0..1]),
minutes: int.Parse(timeStr[1..3]),
seconds: int.Parse(timeStr[3..5]),
milliseconds: int.Parse(timeStr[5..6]) * 100);
}
catch (Exception e) { return null; }
}
} }
+153
View File
@@ -0,0 +1,153 @@
using System.Globalization;
using System.Windows;
using Newtonsoft.Json;
namespace Azaion.Annotator.DTO;
public abstract class Label
{
[JsonProperty(PropertyName = "cl")]
public int ClassNumber { get; set; }
public Label(){}
public Label(int classNumber) { ClassNumber = classNumber; }
}
public class CanvasLabel : Label
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public CanvasLabel() { }
public CanvasLabel(int classNumber, double x, double y, double width, double height) : base(classNumber)
{
X = x;
Y = y;
Width = width;
Height = height;
}
public CanvasLabel(YoloLabel label, Size canvasSize, Size videoSize)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAr = cw / ch;
var videoAr = videoSize.Width / videoSize.Height;
ClassNumber = label.ClassNumber;
var left = X - Width / 2;
var top = Y - Height / 2;
if (videoAr > canvasAr) //100% width
{
var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
X = left * cw;
Y = top * realHeight + blackStripHeight;
Width = label.Width * cw;
Height = label.Height * realHeight;
}
else //100% height
{
var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
X = left * realWidth + blackStripWidth;
Y = top * ch;
Width = label.Width * realWidth;
Height = label.Height * ch;
}
}
}
public class YoloLabel : Label
{
[JsonProperty(PropertyName = "x")]
public double CenterX { get; set; }
[JsonProperty(PropertyName = "y")]
public double CenterY { get; set; }
[JsonProperty(PropertyName = "w")]
public double Width { get; set; }
[JsonProperty(PropertyName = "h")]
public double Height { get; set; }
public YoloLabel() { }
public YoloLabel(int classNumber, double centerX, double centerY, double width, double height) : base(classNumber)
{
CenterX = centerX;
CenterY = centerY;
Width = width;
Height = height;
}
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size videoSize)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAr = cw / ch;
var videoAr = videoSize.Width / videoSize.Height;
ClassNumber = canvasLabel.ClassNumber;
double left, top;
if (videoAr > canvasAr) //100% width
{
left = canvasLabel.X / cw;
Width = canvasLabel.Width / cw;
var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
top = (canvasLabel.Y - blackStripHeight) / realHeight;
Height = canvasLabel.Height / realHeight;
}
else //100% height
{
top = canvasLabel.Y / ch;
Height = canvasLabel.Height / ch;
var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
left = (canvasLabel.X - blackStripWidth) / realWidth;
Width = canvasLabel.Width / realWidth;
}
CenterX = left + canvasLabel.Width / 2.0;
CenterY = top + canvasLabel.Height / 2.0;
}
public static YoloLabel? Parse(string s)
{
if (string.IsNullOrEmpty(s))
return null;
var strings = s.Replace(',','.').Split(' ');
if (strings.Length != 5)
return null;
try
{
var res = new YoloLabel
{
ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture),
CenterX = double.Parse(strings[1], CultureInfo.InvariantCulture),
CenterY = double.Parse(strings[2], CultureInfo.InvariantCulture),
Width = double.Parse(strings[3], CultureInfo.InvariantCulture),
Height = double.Parse(strings[4], CultureInfo.InvariantCulture)
};
return res;
}
catch (Exception)
{
return null;
}
}
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
}
+49 -1
View File
@@ -62,6 +62,7 @@
<ColumnDefinition Width="250" /> <ColumnDefinition Width="250" />
<ColumnDefinition Width="4"/> <ColumnDefinition Width="4"/>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Menu Grid.Row="0" <Menu Grid.Row="0"
@@ -179,10 +180,56 @@
x:Name="VideoView"> x:Name="VideoView">
<controls:CanvasEditor x:Name="Editor" Background="#01000000" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" /> <controls:CanvasEditor x:Name="Editor" Background="#01000000" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
</wpf:VideoView> </wpf:VideoView>
<DataGrid x:Name="DgAnnotations"
Grid.Column="3"
Grid.Row="1"
Grid.RowSpan="3"
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="120"
Header="Кадр"
CanUserSort="False">
<DataGridTemplateColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter>
</Style>
</DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock>
</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>
<controls:UpdatableProgressBar x:Name="VideoSlider" <controls:UpdatableProgressBar x:Name="VideoSlider"
Grid.Column="0" Grid.Column="0"
Grid.Row="4" Grid.Row="4"
Grid.ColumnSpan="3" Grid.ColumnSpan="4"
Background="#252525" Background="#252525"
Foreground="LightBlue"> Foreground="LightBlue">
@@ -373,6 +420,7 @@
<StatusBar <StatusBar
Grid.Row="5" Grid.Row="5"
Grid.Column="2" Grid.Column="2"
Grid.ColumnSpan="2"
Background="#252525" Background="#252525"
Foreground="White" Foreground="White"
> >
+53 -40
View File
@@ -1,13 +1,12 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Drawing;
using System.IO; using System.IO;
using System.Reflection;
using System.Windows; using System.Windows;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
using Newtonsoft.Json;
using Point = System.Windows.Point; using Point = System.Windows.Point;
using Size = System.Windows.Size; using Size = System.Windows.Size;
@@ -22,12 +21,12 @@ public partial class MainWindow
private readonly Config _config; private readonly Config _config;
private readonly HelpWindow _helpWindow; private readonly HelpWindow _helpWindow;
private readonly TimeSpan _annotationTime = TimeSpan.FromSeconds(1);
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new(); public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
public Dictionary<string, List<AnnotationInfo>> Annotations { get; set; } = new(); public Dictionary<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new();
public Dictionary<TimeSpan, List<AnnotationResult>> AnnotationResults { get; set; } = new();
public MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer, public MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer,
IMediator mediator, IMediator mediator,
@@ -115,38 +114,17 @@ public partial class MainWindow
Dispatcher.Invoke(() => VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum); Dispatcher.Invoke(() => VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum);
Dispatcher.Invoke(() => StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"); Dispatcher.Invoke(() => StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}");
var curTime = _formState.GetTimeName(TimeSpan.FromMilliseconds(_mediaPlayer.Time)); var time = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
if (!Annotations.TryGetValue(curTime, out var annotationInfos)) if (!Annotations.TryGetValue(time, out var annotations))
return; return;
var annotations = annotationInfos.Select(info => foreach (var ann in annotations)
{ {
var annClass = _config.AnnotationClasses[info.ClassNumber]; var annClass = _config.AnnotationClasses[ann.ClassNumber];
var annInfo = info.ToCanvasCoordinates(Editor.RenderSize, _formState.CurrentVideoSize); var annInfo = new CanvasLabel(ann, Editor.RenderSize, _formState.CurrentVideoSize);
var annotation = Dispatcher.Invoke(() => Editor.CreateAnnotation(annClass, annInfo)); Dispatcher.Invoke(() => Editor.CreateAnnotation(annClass, time, annInfo));
return annotation; }
}).ToList(); Dispatcher.Invoke(() => Editor.ClearExpiredAnnotations(time));
//remove annotations: either in 1 sec, either earlier if there is next annotation in a dictionary
var timeStr = curTime.Split("_").LastOrDefault();
if (!int.TryParse(timeStr, out var time))
return;
var ts = TimeSpan.FromMilliseconds(time * 100);
var timeSpanRemove = Enumerable.Range(1, (int)_annotationTime.TotalMilliseconds / 100 - 1)
.Select(x =>
{
var timeNext = TimeSpan.FromMilliseconds(x * 100);
var fName = _formState.GetTimeName(ts.Add(timeNext));
return Annotations.ContainsKey(fName) ? timeNext : (TimeSpan?)null;
}).FirstOrDefault(x => x != null) ?? _annotationTime;
_ = Task.Run(async () =>
{
await Task.Delay(timeSpanRemove);
Dispatcher.Invoke(() => Editor.RemoveAnnotations(annotations));
});
}; };
VideoSlider.ValueChanged += (value, newValue) => VideoSlider.ValueChanged += (value, newValue) =>
@@ -173,20 +151,55 @@ public partial class MainWindow
public void LoadExistingAnnotations() public void LoadExistingAnnotations()
{ {
var dirInfo = new DirectoryInfo(_config.LabelsDirectory); Annotations = LoadAnnotations();
if (!dirInfo.Exists) _formState.AnnotationResults = LoadAnnotationResults();
return; }
var files = dirInfo.GetFiles($"{_formState.VideoName}_*"); private Dictionary<TimeSpan, List<YoloLabel>> LoadAnnotations()
Annotations = files.ToDictionary(f => Path.GetFileNameWithoutExtension(f.Name), f => {
var labelDir = new DirectoryInfo(_config.LabelsDirectory);
if (!labelDir.Exists)
return new Dictionary<TimeSpan, List<YoloLabel>>();
var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_*");
return labelFiles.Select(x =>
{ {
var str = File.ReadAllText(f.FullName); var name = Path.GetFileNameWithoutExtension(x.Name);
return str.Split(Environment.NewLine).Select(AnnotationInfo.Parse) return new
{
Name = x.FullName,
Time = _formState.GetTime(name)
};
}).ToDictionary(f => f.Time!.Value, f =>
{
var str = File.ReadAllText(f.Name);
return str.Split(Environment.NewLine).Select(YoloLabel.Parse)
.Where(x => x != null) .Where(x => x != null)
.ToList(); .ToList();
})!; })!;
} }
private List<AnnotationResult> LoadAnnotationResults()
{
var resultDir = new DirectoryInfo(_config!.ResultsDirectory);
if (!resultDir.Exists)
Directory.CreateDirectory(_config!.ResultsDirectory);
var resultsFile = resultDir.GetFiles($"{_formState!.CurrentFile}*").FirstOrDefault();
List<AnnotationResult> results;
if (resultsFile == null)
{
results = Annotations.Select(anns => new AnnotationResult(anns.Key, _formState.GetTimeName(anns.Key), anns.Value))
.ToList();
File.WriteAllText($"{_config.ResultsDirectory}/{_formState.VideoName}.json", JsonConvert.SerializeObject(results, Formatting.Indented));
}
else
results = JsonConvert.DeserializeObject<List<AnnotationResult>>(File.ReadAllText(File.ReadAllText(resultsFile.FullName)))!;
return results;
}
private void ReloadFiles() private void ReloadFiles()
{ {
var dir = new DirectoryInfo(_config.VideosDirectory); var dir = new DirectoryInfo(_config.VideosDirectory);
+67 -52
View File
@@ -4,6 +4,7 @@ using System.Windows.Input;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Newtonsoft.Json;
namespace Azaion.Annotator; namespace Azaion.Annotator;
@@ -98,56 +99,64 @@ public class PlayerControlHandler(LibVLC libVLC, MediaPlayer mediaPlayer, MainWi
private async Task ControlPlayback(PlaybackControlEnum controlEnum) private async Task ControlPlayback(PlaybackControlEnum controlEnum)
{ {
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl); try
var step = isCtrlPressed ? LARGE_STEP : STEP;
switch (controlEnum)
{ {
case PlaybackControlEnum.Play: var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
Play(); var step = isCtrlPressed ? LARGE_STEP : STEP;
break;
case PlaybackControlEnum.Pause: switch (controlEnum)
mediaPlayer.Pause(); {
if (!mediaPlayer.IsPlaying) case PlaybackControlEnum.Play:
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]); Play();
break; break;
case PlaybackControlEnum.Stop: case PlaybackControlEnum.Pause:
mediaPlayer.Stop(); mediaPlayer.Pause();
break; if (!mediaPlayer.IsPlaying)
case PlaybackControlEnum.PreviousFrame: mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
mediaPlayer.SetPause(true); break;
mediaPlayer.Time -= step; case PlaybackControlEnum.Stop:
mainWindow.VideoSlider.Value = mediaPlayer.Position * 100; mediaPlayer.Stop();
break; break;
case PlaybackControlEnum.NextFrame: 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.SaveAnnotations: case PlaybackControlEnum.NextFrame:
await SaveAnnotations(); mediaPlayer.SetPause(true);
break; mediaPlayer.Time += step;
case PlaybackControlEnum.RemoveSelectedAnns: mainWindow.VideoSlider.Value = mediaPlayer.Position * 100;
mainWindow.Editor.RemoveSelectedAnns(); break;
break; case PlaybackControlEnum.SaveAnnotations:
case PlaybackControlEnum.RemoveAllAnns: await SaveAnnotations();
mainWindow.Editor.RemoveAllAnns(); break;
break; case PlaybackControlEnum.RemoveSelectedAnns:
case PlaybackControlEnum.TurnOnVolume: mainWindow.Editor.RemoveSelectedAnns();
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed; break;
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible; case PlaybackControlEnum.RemoveAllAnns:
mediaPlayer.Volume = formState.CurrentVolume; mainWindow.Editor.RemoveAllAnns();
break; break;
case PlaybackControlEnum.TurnOffVolume: case PlaybackControlEnum.TurnOnVolume:
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed; mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible; mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
formState.CurrentVolume = mediaPlayer.Volume; mediaPlayer.Volume = formState.CurrentVolume;
mediaPlayer.Volume = 0; break;
break; case PlaybackControlEnum.TurnOffVolume:
case PlaybackControlEnum.None: mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
break; mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
default: formState.CurrentVolume = mediaPlayer.Volume;
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null); mediaPlayer.Volume = 0;
break;
case PlaybackControlEnum.None:
break;
default:
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null);
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
} }
} }
@@ -182,9 +191,10 @@ public class PlayerControlHandler(LibVLC libVLC, MediaPlayer mediaPlayer, MainWi
if (string.IsNullOrEmpty(formState.CurrentFile)) if (string.IsNullOrEmpty(formState.CurrentFile))
return; return;
var fName = formState.GetTimeName(TimeSpan.FromMilliseconds(mediaPlayer.Time)); var time = TimeSpan.FromMilliseconds(mediaPlayer.Time);
var fName = formState.GetTimeName(time);
var currentAnns = mainWindow.Editor.CurrentAnns var currentAnns = mainWindow.Editor.CurrentAnns
.Select(x => x.Info.ToLabelCoordinates(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()));
@@ -192,14 +202,19 @@ public class PlayerControlHandler(LibVLC libVLC, MediaPlayer mediaPlayer, MainWi
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))
Directory.CreateDirectory(config.ResultsDirectory);
await File.WriteAllTextAsync($"{config.LabelsDirectory}/{fName}.txt", labels); await File.WriteAllTextAsync($"{config.LabelsDirectory}/{fName}.txt", labels);
formState.AnnotationResults.Add(new AnnotationResult(time, fName, currentAnns));
await File.WriteAllTextAsync($"{config.ResultsDirectory}/{fName}.json", JsonConvert.SerializeObject(formState.AnnotationResults));
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);
mediaPlayer.TakeSnapshot(0, $"{config.ImagesDirectory}/{fName}.jpg", RESULT_WIDTH, resultHeight); mediaPlayer.TakeSnapshot(0, $"{config.ImagesDirectory}/{fName}.jpg", RESULT_WIDTH, resultHeight);
mainWindow.Annotations[fName] = currentAnns; mainWindow.Annotations[time] = currentAnns;
mainWindow.Editor.RemoveAllAnns(); mainWindow.Editor.RemoveAllAnns();
mediaPlayer.Play(); mediaPlayer.Play();
} }
+4 -3
View File
@@ -1,7 +1,8 @@
{ {
"VideosDirectory": "D:\\dev\\azaion\\ai\\ai-data", "VideosDirectory": "E:\\Azaion3\\Videos",
"LabelsDirectory": "D:\\dev\\azaion\\ai\\ai-data\\Azaion.Annotator\\labels", "LabelsDirectory": "E:\\labels",
"ImagesDirectory": "D:\\dev\\azaion\\ai\\ai-data\\Azaion.Annotator\\images", "ImagesDirectory": "E:\\images",
"ResultsDirectory": "E:\\results",
"AnnotationClasses": [ "AnnotationClasses": [
{ {
"Id": 0, "Id": 0,
+5
View File
@@ -0,0 +1,5 @@
гармата D20 D30 діацинт мста
макет
активність живої сили
вигорівша трава від пострілів
ящики