add results pane

differentiate videos which already has annotations
This commit is contained in:
Oleksandr Bezdieniezhnykh
2024-07-20 13:50:10 +03:00
parent 288a34e992
commit 83e3532de2
11 changed files with 189 additions and 87 deletions
@@ -11,6 +11,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.8.0" /> <PackageReference Include="xunit" Version="2.8.0" />
</ItemGroup> </ItemGroup>
+32
View File
@@ -0,0 +1,32 @@
using FluentAssertions;
using Xunit;
using IntervalTree;
namespace Azaion.Annotator.Test;
public class IntervalTreeTest
{
[Theory]
[MemberData(nameof(IntervalTreeTestQueryTestData))]
public void IntervalTreeTestQuery(int second, string[] expected)
{
var mainTree = new IntervalTree<TimeSpan, string>
{
{ TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3), "res01" },
{ TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(4), "res02" },
{ TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(7), "res04" },
{ TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(8), "res05" },
};
var result = mainTree.Query(TimeSpan.FromSeconds(second)).ToArray();
result.Should().Equal(expected);
}
public static IEnumerable<object[]> IntervalTreeTestQueryTestData()
{
yield return [1, new[] {"res01"}];
yield return [5, new[] {"res04", "res05"}];
yield return [9, new string[] {}];
}
}
+1
View File
@@ -17,6 +17,7 @@
<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="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="RangeTree" Version="3.0.1" />
<PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" /> <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
+12 -4
View File
@@ -313,7 +313,7 @@ public class CanvasEditor : Canvas
#endregion #endregion
public void RemoveAnnotations(IEnumerable<AnnotationControl> listToRemove) private void RemoveAnnotations(IEnumerable<AnnotationControl> listToRemove)
{ {
foreach (var ann in listToRemove) foreach (var ann in listToRemove)
{ {
@@ -321,8 +321,15 @@ public class CanvasEditor : Canvas
CurrentAnns.Remove(ann); CurrentAnns.Remove(ann);
} }
} }
public void RemoveAllAnns() => RemoveAnnotations(CurrentAnns);
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected)); public void RemoveAllAnns()
{
foreach (var ann in CurrentAnns)
Children.Remove(ann);
CurrentAnns.Clear();
}
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList());
private void ClearSelections() private void ClearSelections()
{ {
@@ -332,6 +339,7 @@ public class CanvasEditor : Canvas
public void ClearExpiredAnnotations(TimeSpan time) public void ClearExpiredAnnotations(TimeSpan time)
{ {
RemoveAnnotations(CurrentAnns.Where(x => time - x.Time > _viewThreshold)); var expiredAnns = CurrentAnns.Where(x => Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds).ToList();
RemoveAnnotations(expiredAnns);
} }
} }
+48 -5
View File
@@ -1,18 +1,18 @@
using Newtonsoft.Json; using System.Windows.Media;
using Newtonsoft.Json;
namespace Azaion.Annotator.DTO; namespace Azaion.Annotator.DTO;
public class AnnotationResult public class AnnotationResult
{ {
private readonly Config _config = null!;
[JsonProperty(PropertyName = "f")] [JsonProperty(PropertyName = "f")]
public string Image { get; set; } = null!; public string Image { get; set; } = null!;
[JsonProperty(PropertyName = "t")] [JsonProperty(PropertyName = "t")]
public TimeSpan Time { get; set; } public TimeSpan Time { get; set; }
[JsonIgnore]
public string TimeStr => $"{Time:h\\:mm\\:ss}";
[JsonProperty(PropertyName = "p")] [JsonProperty(PropertyName = "p")]
public double Percentage { get; set; } public double Percentage { get; set; }
@@ -20,10 +20,53 @@ public class AnnotationResult
public double Lon { get; set; } public double Lon { get; set; }
public List<YoloLabel> Labels { get; set; } = new(); public List<YoloLabel> Labels { get; set; } = new();
#region For Display in the grid
[JsonIgnore]
//For XAML Form
public string TimeStr => $"{Time:h\\:mm\\:ss}";
[JsonIgnore]
//For XAML Form
public string ClassName
{
get
{
if (Labels.Count == 0)
return "";
var groups = Labels.Select(x => x.ClassNumber).GroupBy(x => x).ToList();
return groups.Count > 1
? string.Join(",", groups.Select(x => x.Key + 1))
: _config.AnnotationClassesDict[groups.FirstOrDefault().Key].Name;
}
}
[JsonIgnore]
//For XAML Form
public Color ClassColor
{
get
{
var defaultColor = (Color)ColorConverter.ConvertFromString("#404040");
if (Labels.Count == 0)
return defaultColor;
var groups = Labels.Select(x => x.ClassNumber).GroupBy(x => x).ToList();
return groups.Count > 1
? defaultColor
: _config.AnnotationClassesDict[groups.FirstOrDefault().Key].Color;
}
}
#endregion
public AnnotationResult() { } public AnnotationResult() { }
public AnnotationResult(TimeSpan time, string timeName, List<YoloLabel> labels) public AnnotationResult(TimeSpan time, string timeName, List<YoloLabel> labels, Config config)
{ {
_config = config;
Labels = labels; Labels = labels;
Time = time; Time = time;
Image = $"{timeName}.jpg"; Image = $"{timeName}.jpg";
+4
View File
@@ -27,6 +27,10 @@ public class Config
public string ResultsDirectory { get; set; } = DEFAULT_RESULTS_DIR; public string ResultsDirectory { get; set; } = DEFAULT_RESULTS_DIR;
public List<AnnotationClass> AnnotationClasses { 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 Size WindowSize { get; set; }
public Point WindowLocation { get; set; } public Point WindowLocation { get; set; }
public bool ShowHelpOnStart { get; set; } public bool ShowHelpOnStart { get; set; }
+3 -2
View File
@@ -1,4 +1,5 @@
using System.Globalization; using System.Collections.ObjectModel;
using System.Globalization;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
@@ -13,7 +14,7 @@ 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 ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}"; public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}";
+4 -5
View File
@@ -38,8 +38,8 @@ public class CanvasLabel : Label
ClassNumber = label.ClassNumber; ClassNumber = label.ClassNumber;
var left = X - Width / 2; var left = label.CenterX - label.Width / 2;
var top = Y - Height / 2; var top = label.CenterY - label.Height / 2;
if (videoAr > canvasAr) //100% width if (videoAr > canvasAr) //100% width
{ {
@@ -89,7 +89,6 @@ public class YoloLabel : Label
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size videoSize) public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size videoSize)
{ {
var cw = canvasSize.Width; var cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
var canvasAr = cw / ch; var canvasAr = cw / ch;
@@ -117,8 +116,8 @@ public class YoloLabel : Label
Width = canvasLabel.Width / realWidth; Width = canvasLabel.Width / realWidth;
} }
CenterX = left + canvasLabel.Width / 2.0; CenterX = left + Width / 2.0;
CenterY = top + canvasLabel.Height / 2.0; CenterY = top + Height / 2.0;
} }
public static YoloLabel? Parse(string s) public static YoloLabel? Parse(string s)
+16 -2
View File
@@ -207,7 +207,7 @@
CanUserResizeColumns="False"> CanUserResizeColumns="False">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn <DataGridTextColumn
Width="120" Width="60"
Header="Кадр" Header="Кадр"
CanUserSort="False" CanUserSort="False"
Binding="{Binding Path=TimeStr}"> Binding="{Binding Path=TimeStr}">
@@ -220,15 +220,29 @@
<DataGridTextColumn <DataGridTextColumn
Width="*" Width="*"
Header="Клас" Header="Клас"
Binding="{Binding Path=Name}" Binding="{Binding Path=ClassName}"
CanUserSort="False"> CanUserSort="False">
<DataGridTextColumn.HeaderStyle> <DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader"> <Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter> <Setter Property="Background" Value="#252525"></Setter>
</Style> </Style>
</DataGridTextColumn.HeaderStyle> </DataGridTextColumn.HeaderStyle>
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{Binding Path=ClassColor}"></SolidColorBrush>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn> </DataGridTextColumn>
</DataGrid.Columns> </DataGrid.Columns>
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDoubleClick" Handler="DgAnnotationsRowClick"></EventSetter>
</Style>
</DataGrid.ItemContainerStyle>
</DataGrid> </DataGrid>
<controls:UpdatableProgressBar x:Name="VideoSlider" <controls:UpdatableProgressBar x:Name="VideoSlider"
+60 -55
View File
@@ -1,6 +1,8 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
@@ -9,6 +11,7 @@ using Microsoft.WindowsAPICodePack.Dialogs;
using Newtonsoft.Json; using Newtonsoft.Json;
using Point = System.Windows.Point; using Point = System.Windows.Point;
using Size = System.Windows.Size; using Size = System.Windows.Size;
using IntervalTree;
namespace Azaion.Annotator; namespace Azaion.Annotator;
@@ -25,8 +28,11 @@ public partial class MainWindow
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new(); public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
public Dictionary<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new(); private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
public Dictionary<TimeSpan, List<AnnotationResult>> AnnotationResults { get; set; } = new(); private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(300);
public Dictionary<TimeSpan, List<YoloLabel>> AnnotationsDict { get; set; } = new();
private IntervalTree<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new();
public MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer, public MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer,
IMediator mediator, IMediator mediator,
@@ -111,20 +117,7 @@ public partial class MainWindow
_mediaPlayer.PositionChanged += (o, args) => _mediaPlayer.PositionChanged += (o, args) =>
{ {
Dispatcher.Invoke(() => VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum); ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
Dispatcher.Invoke(() => StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}");
var time = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
if (!Annotations.TryGetValue(time, out var annotations))
return;
foreach (var ann in 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) => VideoSlider.ValueChanged += (value, newValue) =>
@@ -147,59 +140,58 @@ public partial class MainWindow
Editor.FormState = _formState; Editor.FormState = _formState;
Editor.Mediator = _mediator; Editor.Mediator = _mediator;
}
public void LoadExistingAnnotations()
{
Annotations = LoadAnnotations();
_formState.AnnotationResults = LoadAnnotationResults();
DgAnnotations.ItemsSource = _formState.AnnotationResults; DgAnnotations.ItemsSource = _formState.AnnotationResults;
} }
private Dictionary<TimeSpan, List<YoloLabel>> LoadAnnotations() private void ShowTimeAnnotations(TimeSpan time)
{ {
Dispatcher.Invoke(() => VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum);
Dispatcher.Invoke(() => StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}");
Dispatcher.Invoke(() => Editor.ClearExpiredAnnotations(time));
var annotations = Annotations.Query(time).SelectMany(x => x).ToList();
foreach (var ann in annotations)
{
var annClass = _config.AnnotationClasses[ann.ClassNumber];
var annInfo = new CanvasLabel(ann, Editor.RenderSize, _formState.CurrentVideoSize);
Dispatcher.Invoke(() => Editor.CreateAnnotation(annClass, time, annInfo));
}
}
public void ReloadAnnotations()
{
_formState.AnnotationResults.Clear();
AnnotationsDict.Clear();
Annotations.Clear();
var labelDir = new DirectoryInfo(_config.LabelsDirectory); var labelDir = new DirectoryInfo(_config.LabelsDirectory);
if (!labelDir.Exists) if (!labelDir.Exists)
return new Dictionary<TimeSpan, List<YoloLabel>>(); return;
var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_*"); var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_*");
return labelFiles.Select(x => foreach (var file in labelFiles)
{ {
var name = Path.GetFileNameWithoutExtension(x.Name); var name = Path.GetFileNameWithoutExtension(file.Name);
return new var time = _formState.GetTime(name)!.Value;
{
Name = x.FullName, var str = File.ReadAllText(file.FullName);
Time = _formState.GetTime(name) var annotations = str.Split(Environment.NewLine).Select(YoloLabel.Parse)
}; .Where(ann => ann != null)
}).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(); .ToList();
})!;
}
private List<AnnotationResult> LoadAnnotationResults() AddAnnotation(time, annotations!);
{
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;
} }
public async Task AddAnnotation(TimeSpan time, List<YoloLabel> annotations)
{
var fName = _formState.GetTimeName(time);
AnnotationsDict.Add(time, annotations!);
Annotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotations);
_formState.AnnotationResults.Add(new AnnotationResult(time, fName, annotations, _config));
await File.WriteAllTextAsync($"{_config.ResultsDirectory}/{fName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults));
}
private void ReloadFiles() private void ReloadFiles()
{ {
@@ -288,4 +280,17 @@ public partial class MainWindow
_helpWindow.Show(); _helpWindow.Show();
_helpWindow.Activate(); _helpWindow.Activate();
} }
private void DgAnnotationsRowClick(object sender, MouseButtonEventArgs e)
{
DgAnnotations.MouseDoubleClick += (sender, args) =>
{
Editor.RemoveAllAnns();
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow;
var res = (AnnotationResult)dgRow!.Item;
_mediaPlayer.SetPause(true);
_mediaPlayer.Time = (long)res.Time.TotalMilliseconds;// + 250;
ShowTimeAnnotations(res.Time);
};
}
} }
+2 -8
View File
@@ -4,7 +4,6 @@ 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;
@@ -179,7 +178,7 @@ public class PlayerControlHandler(LibVLC libVLC, MediaPlayer mediaPlayer, MainWi
var fileInfo = (VideoFileInfo)mainWindow.LvFiles.SelectedItem; var fileInfo = (VideoFileInfo)mainWindow.LvFiles.SelectedItem;
formState.CurrentFile = fileInfo.Name; formState.CurrentFile = fileInfo.Name;
mainWindow.LoadExistingAnnotations(); mainWindow.ReloadAnnotations();
mediaPlayer.Stop(); mediaPlayer.Stop();
mediaPlayer.Play(new Media(libVLC, fileInfo.Path)); mediaPlayer.Play(new Media(libVLC, fileInfo.Path));
@@ -206,15 +205,10 @@ public class PlayerControlHandler(LibVLC libVLC, MediaPlayer mediaPlayer, MainWi
Directory.CreateDirectory(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[time] = currentAnns; await mainWindow.AddAnnotation(time, currentAnns);
mainWindow.Editor.RemoveAllAnns(); mainWindow.Editor.RemoveAllAnns();
mediaPlayer.Play(); mediaPlayer.Play();
} }