mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 09:56:31 +00:00
Add annotationResult and put them into json
This commit is contained in:
@@ -5,16 +5,16 @@ namespace Azaion.Annotator.Test;
|
||||
|
||||
public class DictTest
|
||||
{
|
||||
public Dictionary<string, List<AnnotationInfo>> Annotations = new();
|
||||
public Dictionary<string, List<YoloLabel>> Annotations = new();
|
||||
|
||||
[Fact]
|
||||
public void DictAddTest()
|
||||
{
|
||||
Annotations["sd"] = [new AnnotationInfo(1, 2, 2, 2, 2)];
|
||||
Annotations["sd"] = [new YoloLabel(1, 2, 2, 2, 2)];
|
||||
Annotations["sd"] =
|
||||
[
|
||||
new AnnotationInfo(1, 2, 2, 2, 2),
|
||||
new AnnotationInfo(0, 1, 3, 2, 1)
|
||||
new YoloLabel(1, 2, 2, 2, 2),
|
||||
new YoloLabel(0, 1, 3, 2, 1)
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,33 @@ using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using LibVLCSharp.Shared;
|
||||
using MediatR;
|
||||
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;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private readonly ServiceProvider _serviceProvider;
|
||||
private IMediator _mediator;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
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();
|
||||
services.AddSingleton<MainWindow>();
|
||||
services.AddSingleton<HelpWindow>();
|
||||
@@ -37,7 +51,7 @@ public partial class App : Application
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
<PackageReference Include="LibVLCSharp.WPF" Version="3.8.2" />
|
||||
<PackageReference Include="MediatR" Version="12.2.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="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="WindowsAPICodePack" Version="7.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -14,6 +14,7 @@ public class AnnotationControl : Border
|
||||
|
||||
private readonly Grid _grid;
|
||||
private readonly TextBlock _classNameLabel;
|
||||
public TimeSpan Time { get; set; }
|
||||
|
||||
private AnnotationClass _annotationClass = null!;
|
||||
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;
|
||||
_classNameLabel = new TextBlock
|
||||
{
|
||||
@@ -104,5 +106,5 @@ public class AnnotationControl : Border
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
@@ -26,6 +25,7 @@ public class CanvasEditor : Canvas
|
||||
private AnnotationControl _curAnn = null!;
|
||||
|
||||
private const int MIN_SIZE = 20;
|
||||
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
|
||||
|
||||
public FormState FormState { get; set; } = null!;
|
||||
public IMediator Mediator { get; set; } = null!;
|
||||
@@ -284,29 +284,27 @@ public class CanvasEditor : Canvas
|
||||
if (width < MIN_SIZE || height < MIN_SIZE)
|
||||
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,
|
||||
Height = height
|
||||
};
|
||||
annotationControl.MouseDown += AnnotationPositionStart;
|
||||
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);
|
||||
Height = height,
|
||||
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
|
||||
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y)
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
Height = info.Height
|
||||
Width = canvasLabel.Width,
|
||||
Height = canvasLabel.Height
|
||||
};
|
||||
annotationControl.MouseDown += AnnotationPositionStart;
|
||||
SetLeft(annotationControl, info.X );
|
||||
SetTop(annotationControl, info.Y);
|
||||
SetLeft(annotationControl, canvasLabel.X );
|
||||
SetTop(annotationControl, canvasLabel.Y);
|
||||
Children.Add(annotationControl);
|
||||
CurrentAnns.Add(annotationControl);
|
||||
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
|
||||
@@ -323,12 +321,17 @@ public class CanvasEditor : Canvas
|
||||
CurrentAnns.Remove(ann);
|
||||
}
|
||||
}
|
||||
public void RemoveAllAnns() => RemoveAnnotations(CurrentAnns.ToList());
|
||||
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList());
|
||||
public void RemoveAllAnns() => RemoveAnnotations(CurrentAnns);
|
||||
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected));
|
||||
|
||||
private void ClearSelections()
|
||||
{
|
||||
foreach (var ann in CurrentAnns)
|
||||
ann.IsSelected = false;
|
||||
}
|
||||
|
||||
public void ClearExpiredAnnotations(TimeSpan time)
|
||||
{
|
||||
RemoveAnnotations(CurrentAnns.Where(x => time - x.Time > _viewThreshold));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,13 @@ namespace Azaion.Annotator.DTO;
|
||||
public class Config
|
||||
{
|
||||
private const string CONFIG_PATH = "config.json";
|
||||
|
||||
private const string DEFAULT_VIDEO_DIR = "video";
|
||||
|
||||
private const string DEFAULT_LABELS_DIR = "labels";
|
||||
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 Point DefaultWindowLocation = new(100, 100);
|
||||
|
||||
@@ -20,6 +24,7 @@ public class Config
|
||||
public string VideosDirectory { get; set; } = DEFAULT_VIDEO_DIR;
|
||||
public string LabelsDirectory { get; set; } = DEFAULT_LABELS_DIR;
|
||||
public string ImagesDirectory { get; set; } = DEFAULT_IMAGES_DIR;
|
||||
public string ResultsDirectory { get; set; } = DEFAULT_RESULTS_DIR;
|
||||
|
||||
public List<AnnotationClass> AnnotationClasses { get; set; } = [];
|
||||
public Size WindowSize { get; set; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
|
||||
namespace Azaion.Annotator.DTO;
|
||||
@@ -12,6 +13,26 @@ public class FormState
|
||||
public string VideoName => Path.GetFileNameWithoutExtension(CurrentFile).Replace(" ", "");
|
||||
public TimeSpan CurrentVideoLength { get; set; }
|
||||
public int CurrentVolume { get; set; } = 100;
|
||||
public List<AnnotationResult> AnnotationResults { get; set; } = [];
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(',', '.');
|
||||
|
||||
}
|
||||
@@ -62,6 +62,7 @@
|
||||
<ColumnDefinition Width="250" />
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="200" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Menu Grid.Row="0"
|
||||
@@ -179,10 +180,56 @@
|
||||
x:Name="VideoView">
|
||||
<controls:CanvasEditor x:Name="Editor" Background="#01000000" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
|
||||
</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"
|
||||
Grid.Column="0"
|
||||
Grid.Row="4"
|
||||
Grid.ColumnSpan="3"
|
||||
Grid.ColumnSpan="4"
|
||||
Background="#252525"
|
||||
Foreground="LightBlue">
|
||||
|
||||
@@ -373,6 +420,7 @@
|
||||
<StatusBar
|
||||
Grid.Row="5"
|
||||
Grid.Column="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Background="#252525"
|
||||
Foreground="White"
|
||||
>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using LibVLCSharp.Shared;
|
||||
using MediatR;
|
||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||
using Newtonsoft.Json;
|
||||
using Point = System.Windows.Point;
|
||||
using Size = System.Windows.Size;
|
||||
|
||||
@@ -22,12 +21,12 @@ public partial class MainWindow
|
||||
|
||||
private readonly Config _config;
|
||||
private readonly HelpWindow _helpWindow;
|
||||
private readonly TimeSpan _annotationTime = TimeSpan.FromSeconds(1);
|
||||
|
||||
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
|
||||
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,
|
||||
IMediator mediator,
|
||||
@@ -115,38 +114,17 @@ public partial class MainWindow
|
||||
Dispatcher.Invoke(() => VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum);
|
||||
Dispatcher.Invoke(() => StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}");
|
||||
|
||||
var curTime = _formState.GetTimeName(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
|
||||
if (!Annotations.TryGetValue(curTime, out var annotationInfos))
|
||||
var time = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
|
||||
if (!Annotations.TryGetValue(time, out var annotations))
|
||||
return;
|
||||
|
||||
var annotations = annotationInfos.Select(info =>
|
||||
foreach (var ann in annotations)
|
||||
{
|
||||
var annClass = _config.AnnotationClasses[info.ClassNumber];
|
||||
var annInfo = info.ToCanvasCoordinates(Editor.RenderSize, _formState.CurrentVideoSize);
|
||||
var annotation = Dispatcher.Invoke(() => Editor.CreateAnnotation(annClass, annInfo));
|
||||
return annotation;
|
||||
}).ToList();
|
||||
|
||||
//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));
|
||||
});
|
||||
|
||||
var annClass = _config.AnnotationClasses[ann.ClassNumber];
|
||||
var annInfo = new CanvasLabel(ann, Editor.RenderSize, _formState.CurrentVideoSize);
|
||||
Dispatcher.Invoke(() => Editor.CreateAnnotation(annClass, time, annInfo));
|
||||
}
|
||||
Dispatcher.Invoke(() => Editor.ClearExpiredAnnotations(time));
|
||||
};
|
||||
|
||||
VideoSlider.ValueChanged += (value, newValue) =>
|
||||
@@ -173,20 +151,55 @@ public partial class MainWindow
|
||||
|
||||
public void LoadExistingAnnotations()
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(_config.LabelsDirectory);
|
||||
if (!dirInfo.Exists)
|
||||
return;
|
||||
Annotations = LoadAnnotations();
|
||||
_formState.AnnotationResults = LoadAnnotationResults();
|
||||
}
|
||||
|
||||
var files = dirInfo.GetFiles($"{_formState.VideoName}_*");
|
||||
Annotations = files.ToDictionary(f => Path.GetFileNameWithoutExtension(f.Name), f =>
|
||||
private Dictionary<TimeSpan, List<YoloLabel>> LoadAnnotations()
|
||||
{
|
||||
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);
|
||||
return str.Split(Environment.NewLine).Select(AnnotationInfo.Parse)
|
||||
var name = Path.GetFileNameWithoutExtension(x.Name);
|
||||
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)
|
||||
.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()
|
||||
{
|
||||
var dir = new DirectoryInfo(_config.VideosDirectory);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Windows.Input;
|
||||
using Azaion.Annotator.DTO;
|
||||
using LibVLCSharp.Shared;
|
||||
using MediatR;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
@@ -98,56 +99,64 @@ public class PlayerControlHandler(LibVLC libVLC, MediaPlayer mediaPlayer, MainWi
|
||||
|
||||
private async Task ControlPlayback(PlaybackControlEnum controlEnum)
|
||||
{
|
||||
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
|
||||
var step = isCtrlPressed ? LARGE_STEP : STEP;
|
||||
|
||||
switch (controlEnum)
|
||||
try
|
||||
{
|
||||
case PlaybackControlEnum.Play:
|
||||
Play();
|
||||
break;
|
||||
case PlaybackControlEnum.Pause:
|
||||
mediaPlayer.Pause();
|
||||
if (!mediaPlayer.IsPlaying)
|
||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
|
||||
break;
|
||||
case PlaybackControlEnum.Stop:
|
||||
mediaPlayer.Stop();
|
||||
break;
|
||||
case PlaybackControlEnum.PreviousFrame:
|
||||
mediaPlayer.SetPause(true);
|
||||
mediaPlayer.Time -= step;
|
||||
mainWindow.VideoSlider.Value = mediaPlayer.Position * 100;
|
||||
break;
|
||||
case PlaybackControlEnum.NextFrame:
|
||||
mediaPlayer.SetPause(true);
|
||||
mediaPlayer.Time += step;
|
||||
mainWindow.VideoSlider.Value = mediaPlayer.Position * 100;
|
||||
break;
|
||||
case PlaybackControlEnum.SaveAnnotations:
|
||||
await SaveAnnotations();
|
||||
break;
|
||||
case PlaybackControlEnum.RemoveSelectedAnns:
|
||||
mainWindow.Editor.RemoveSelectedAnns();
|
||||
break;
|
||||
case PlaybackControlEnum.RemoveAllAnns:
|
||||
mainWindow.Editor.RemoveAllAnns();
|
||||
break;
|
||||
case PlaybackControlEnum.TurnOnVolume:
|
||||
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
|
||||
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
|
||||
mediaPlayer.Volume = formState.CurrentVolume;
|
||||
break;
|
||||
case PlaybackControlEnum.TurnOffVolume:
|
||||
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
|
||||
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
|
||||
formState.CurrentVolume = mediaPlayer.Volume;
|
||||
mediaPlayer.Volume = 0;
|
||||
break;
|
||||
case PlaybackControlEnum.None:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(controlEnum), controlEnum, null);
|
||||
var isCtrlPressed = Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl);
|
||||
var step = isCtrlPressed ? LARGE_STEP : STEP;
|
||||
|
||||
switch (controlEnum)
|
||||
{
|
||||
case PlaybackControlEnum.Play:
|
||||
Play();
|
||||
break;
|
||||
case PlaybackControlEnum.Pause:
|
||||
mediaPlayer.Pause();
|
||||
if (!mediaPlayer.IsPlaying)
|
||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
|
||||
break;
|
||||
case PlaybackControlEnum.Stop:
|
||||
mediaPlayer.Stop();
|
||||
break;
|
||||
case PlaybackControlEnum.PreviousFrame:
|
||||
mediaPlayer.SetPause(true);
|
||||
mediaPlayer.Time -= step;
|
||||
mainWindow.VideoSlider.Value = mediaPlayer.Position * 100;
|
||||
break;
|
||||
case PlaybackControlEnum.NextFrame:
|
||||
mediaPlayer.SetPause(true);
|
||||
mediaPlayer.Time += step;
|
||||
mainWindow.VideoSlider.Value = mediaPlayer.Position * 100;
|
||||
break;
|
||||
case PlaybackControlEnum.SaveAnnotations:
|
||||
await SaveAnnotations();
|
||||
break;
|
||||
case PlaybackControlEnum.RemoveSelectedAnns:
|
||||
mainWindow.Editor.RemoveSelectedAnns();
|
||||
break;
|
||||
case PlaybackControlEnum.RemoveAllAnns:
|
||||
mainWindow.Editor.RemoveAllAnns();
|
||||
break;
|
||||
case PlaybackControlEnum.TurnOnVolume:
|
||||
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Collapsed;
|
||||
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Visible;
|
||||
mediaPlayer.Volume = formState.CurrentVolume;
|
||||
break;
|
||||
case PlaybackControlEnum.TurnOffVolume:
|
||||
mainWindow.TurnOffVolumeBtn.Visibility = Visibility.Collapsed;
|
||||
mainWindow.TurnOnVolumeBtn.Visibility = Visibility.Visible;
|
||||
formState.CurrentVolume = mediaPlayer.Volume;
|
||||
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))
|
||||
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
|
||||
.Select(x => x.Info.ToLabelCoordinates(mainWindow.Editor.RenderSize, formState.CurrentVideoSize))
|
||||
.Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.CurrentVideoSize))
|
||||
.ToList();
|
||||
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);
|
||||
if (!Directory.Exists(config.ImagesDirectory))
|
||||
Directory.CreateDirectory(config.ImagesDirectory);
|
||||
if (!Directory.Exists(config.ResultsDirectory))
|
||||
Directory.CreateDirectory(config.ResultsDirectory);
|
||||
|
||||
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);
|
||||
|
||||
mediaPlayer.TakeSnapshot(0, $"{config.ImagesDirectory}/{fName}.jpg", RESULT_WIDTH, resultHeight);
|
||||
|
||||
mainWindow.Annotations[fName] = currentAnns;
|
||||
mainWindow.Annotations[time] = currentAnns;
|
||||
mainWindow.Editor.RemoveAllAnns();
|
||||
mediaPlayer.Play();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"VideosDirectory": "D:\\dev\\azaion\\ai\\ai-data",
|
||||
"LabelsDirectory": "D:\\dev\\azaion\\ai\\ai-data\\Azaion.Annotator\\labels",
|
||||
"ImagesDirectory": "D:\\dev\\azaion\\ai\\ai-data\\Azaion.Annotator\\images",
|
||||
"VideosDirectory": "E:\\Azaion3\\Videos",
|
||||
"LabelsDirectory": "E:\\labels",
|
||||
"ImagesDirectory": "E:\\images",
|
||||
"ResultsDirectory": "E:\\results",
|
||||
"AnnotationClasses": [
|
||||
{
|
||||
"Id": 0,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
гармата D20 D30 діацинт мста
|
||||
макет
|
||||
активність живої сили
|
||||
вигорівша трава від пострілів
|
||||
ящики
|
||||
Reference in New Issue
Block a user