mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 11:06:30 +00:00
add Annotator
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
.idea
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
.vs
|
||||||
|
*.DotSettings*
|
||||||
|
*.user
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Azaion.Annotator\Azaion.Annotator.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.8.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Azaion.Annotator.DTO;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Test;
|
||||||
|
|
||||||
|
public class DictTest
|
||||||
|
{
|
||||||
|
public Dictionary<string, List<AnnotationInfo>> Annotations = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DictAddTest()
|
||||||
|
{
|
||||||
|
Annotations["sd"] = [new AnnotationInfo(1, 2, 2, 2, 2)];
|
||||||
|
Annotations["sd"] =
|
||||||
|
[
|
||||||
|
new AnnotationInfo(1, 2, 2, 2, 2),
|
||||||
|
new AnnotationInfo(0, 1, 3, 2, 1)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator", "Azaion.Annotator\Azaion.Annotator.csproj", "{8E0809AF-2920-4267-B14D-84BAB334A46F}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator.Test", "Azaion.Annotator.Test\Azaion.Annotator.Test.csproj", "{85359558-FB59-4542-A597-FD9E1B04C8E7}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{8E0809AF-2920-4267-B14D-84BAB334A46F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8E0809AF-2920-4267-B14D-84BAB334A46F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8E0809AF-2920-4267-B14D-84BAB334A46F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8E0809AF-2920-4267-B14D-84BAB334A46F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{85359558-FB59-4542-A597-FD9E1B04C8E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{85359558-FB59-4542-A597-FD9E1B04C8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{85359558-FB59-4542-A597-FD9E1B04C8E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{85359558-FB59-4542-A597-FD9E1B04C8E7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<Application x:Class="Azaion.Annotator.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Azaion.Annotator.DTO;
|
||||||
|
using LibVLCSharp.Shared;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private readonly ServiceProvider _serviceProvider;
|
||||||
|
private IMediator _mediator;
|
||||||
|
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<MainWindow>();
|
||||||
|
services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
|
||||||
|
services.AddSingleton<LibVLC>(_ => new LibVLC());
|
||||||
|
services.AddSingleton<FormState>();
|
||||||
|
services.AddSingleton<MediaPlayer>(sp =>
|
||||||
|
{
|
||||||
|
var libVLC = sp.GetRequiredService<LibVLC>();
|
||||||
|
return new MediaPlayer(libVLC);
|
||||||
|
});
|
||||||
|
services.AddSingleton<Config>(_ => Config.Read());
|
||||||
|
services.AddSingleton<PlayerControlHandler>();
|
||||||
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
_mediator = _serviceProvider.GetService<IMediator>()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.KeyDownEvent, new RoutedEventHandler(GlobalClick));
|
||||||
|
_serviceProvider.GetRequiredService<MainWindow>().Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GlobalClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var args = (KeyEventArgs)e;
|
||||||
|
|
||||||
|
_mediator.Publish(new KeyEvent(sender, args));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
[assembly: ThemeInfo(
|
||||||
|
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// or application resource dictionaries)
|
||||||
|
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// app, or any theme specific resource dictionaries)
|
||||||
|
)]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="libc.translation" Version="7.1.1" />
|
||||||
|
<PackageReference Include="LibVLCSharp" Version="3.8.2" />
|
||||||
|
<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="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.20" />
|
||||||
|
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using Azaion.Annotator.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Controls;
|
||||||
|
|
||||||
|
public class AnnotationControl : Border
|
||||||
|
{
|
||||||
|
private readonly Action<object, MouseButtonEventArgs> _resizeStart;
|
||||||
|
private const double RESIZE_RECT_SIZE = 10;
|
||||||
|
|
||||||
|
private readonly Grid _grid;
|
||||||
|
private readonly TextBlock _classNameLabel;
|
||||||
|
|
||||||
|
private AnnotationClass _annotationClass;
|
||||||
|
public AnnotationClass AnnotationClass
|
||||||
|
{
|
||||||
|
get => _annotationClass;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_grid.Background = value.ColorBrush;
|
||||||
|
_classNameLabel.Text = value.Name;
|
||||||
|
_annotationClass = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Rectangle _selectionFrame;
|
||||||
|
|
||||||
|
private bool _isSelected;
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => _isSelected;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectionFrame.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
_isSelected = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnnotationControl(AnnotationClass annotationClass, Action<object, MouseButtonEventArgs> resizeStart)
|
||||||
|
{
|
||||||
|
_resizeStart = resizeStart;
|
||||||
|
_classNameLabel = new TextBlock
|
||||||
|
{
|
||||||
|
Text = annotationClass.Name,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(0, 15, 0, 0),
|
||||||
|
Cursor = Cursors.Arrow
|
||||||
|
};
|
||||||
|
_selectionFrame = new Rectangle
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
|
Stroke = new SolidColorBrush(Colors.Black),
|
||||||
|
StrokeThickness = 3,
|
||||||
|
Visibility = Visibility.Collapsed
|
||||||
|
};
|
||||||
|
|
||||||
|
_grid = new Grid
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
_selectionFrame,
|
||||||
|
_classNameLabel,
|
||||||
|
AddRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
|
||||||
|
AddRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
|
||||||
|
AddRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
|
||||||
|
AddRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
|
||||||
|
AddRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
|
||||||
|
AddRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
|
||||||
|
AddRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
|
||||||
|
AddRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
|
||||||
|
},
|
||||||
|
|
||||||
|
Background = new SolidColorBrush(annotationClass.Color)
|
||||||
|
};
|
||||||
|
Child = _grid;
|
||||||
|
Cursor = Cursors.SizeAll;
|
||||||
|
AnnotationClass = annotationClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
//small corners
|
||||||
|
private Rectangle AddRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs)
|
||||||
|
{
|
||||||
|
var rect = new Rectangle() // small rectangles at the corners and sides
|
||||||
|
{
|
||||||
|
HorizontalAlignment = ha,
|
||||||
|
VerticalAlignment = va,
|
||||||
|
Width = RESIZE_RECT_SIZE,
|
||||||
|
Height = RESIZE_RECT_SIZE,
|
||||||
|
Stroke = new SolidColorBrush(Color.FromRgb(10, 10, 10)), // small rectangles color
|
||||||
|
Fill = new SolidColorBrush(Color.FromArgb(1, 255, 255, 255)),
|
||||||
|
Cursor = crs,
|
||||||
|
Name = name,
|
||||||
|
};
|
||||||
|
rect.MouseDown += (sender, args) => _resizeStart(sender, args);
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnnotationInfo Info => new(AnnotationClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
|
||||||
|
}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using Azaion.Annotator.DTO;
|
||||||
|
using MediatR;
|
||||||
|
using Color = System.Windows.Media.Color;
|
||||||
|
using Rectangle = System.Windows.Shapes.Rectangle;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Controls;
|
||||||
|
|
||||||
|
public class CanvasEditor : Canvas
|
||||||
|
{
|
||||||
|
private Point _lastPos;
|
||||||
|
|
||||||
|
private readonly Rectangle _newAnnotationRect;
|
||||||
|
private Point _newAnnotationStartPos;
|
||||||
|
|
||||||
|
private readonly Line _horizontalLine;
|
||||||
|
private readonly Line _verticalLine;
|
||||||
|
|
||||||
|
private Rectangle _curRec;
|
||||||
|
private AnnotationControl _curAnn;
|
||||||
|
|
||||||
|
private const int MIN_SIZE = 20;
|
||||||
|
|
||||||
|
public FormState FormState { get; set; }
|
||||||
|
public IMediator Mediator { get; set; }
|
||||||
|
|
||||||
|
private AnnotationClass _currentAnnClass;
|
||||||
|
public AnnotationClass CurrentAnnClass
|
||||||
|
{
|
||||||
|
get => _currentAnnClass;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_verticalLine.Stroke = value.ColorBrush;
|
||||||
|
_verticalLine.Fill = value.ColorBrush;
|
||||||
|
_horizontalLine.Stroke = value.ColorBrush;
|
||||||
|
_horizontalLine.Fill = value.ColorBrush;
|
||||||
|
_newAnnotationRect.Stroke = value.ColorBrush;
|
||||||
|
_newAnnotationRect.Fill = value.ColorBrush;
|
||||||
|
_currentAnnClass = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly List<AnnotationControl> CurrentAnns = new();
|
||||||
|
|
||||||
|
public CanvasEditor()
|
||||||
|
{
|
||||||
|
_horizontalLine = new Line
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
Stroke = new SolidColorBrush(Colors.Blue),
|
||||||
|
Fill = new SolidColorBrush(Colors.Blue),
|
||||||
|
StrokeDashArray = [5],
|
||||||
|
StrokeThickness = 2
|
||||||
|
};
|
||||||
|
_verticalLine = new Line
|
||||||
|
{
|
||||||
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
|
Stroke = new SolidColorBrush(Colors.Blue),
|
||||||
|
Fill = new SolidColorBrush(Colors.Blue),
|
||||||
|
StrokeDashArray = [5],
|
||||||
|
StrokeThickness = 2
|
||||||
|
};
|
||||||
|
_newAnnotationRect = new Rectangle
|
||||||
|
{
|
||||||
|
Name = "selector",
|
||||||
|
Height = 0,
|
||||||
|
Width = 0,
|
||||||
|
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}");
|
||||||
|
};
|
||||||
|
MouseDown += CanvasMouseDown;
|
||||||
|
MouseMove += CanvasMouseMove;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
ClearSelections();
|
||||||
|
NewAnnotationStart(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CanvasMouseMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
var pos = e.GetPosition(this);
|
||||||
|
_horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y;
|
||||||
|
_verticalLine.X1 = _verticalLine.X2 = pos.X;
|
||||||
|
|
||||||
|
if (e.LeftButton != MouseButtonState.Pressed)
|
||||||
|
return;
|
||||||
|
if (FormState.SelectionState == SelectionState.NewAnnCreating)
|
||||||
|
NewAnnotationCreatingMove(sender, e);
|
||||||
|
|
||||||
|
if (FormState.SelectionState == SelectionState.AnnResizing)
|
||||||
|
AnnotationResizeMove(sender, e);
|
||||||
|
|
||||||
|
if (FormState.SelectionState == SelectionState.AnnMoving)
|
||||||
|
AnnotationPositionMove(sender, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (FormState.SelectionState == SelectionState.NewAnnCreating)
|
||||||
|
CreateAnnotation(e.GetPosition(this));
|
||||||
|
|
||||||
|
FormState.SelectionState = SelectionState.None;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CanvasResized(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_horizontalLine.X2 = e.NewSize.Width;
|
||||||
|
_verticalLine.Y2 = e.NewSize.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Annotation Resizing & Moving
|
||||||
|
|
||||||
|
private void AnnotationResizeStart(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
FormState.SelectionState = SelectionState.AnnResizing;
|
||||||
|
_lastPos = e.GetPosition(this);
|
||||||
|
_curRec = (Rectangle)sender;
|
||||||
|
_curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnnotationResizeMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (FormState.SelectionState != SelectionState.AnnResizing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var currentPos = e.GetPosition(this);
|
||||||
|
|
||||||
|
var x = GetLeft(_curAnn);
|
||||||
|
var y = GetTop(_curAnn);
|
||||||
|
var offsetX = currentPos.X - _lastPos.X;
|
||||||
|
var offsetY = currentPos.Y - _lastPos.Y;
|
||||||
|
switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment)
|
||||||
|
{
|
||||||
|
case (HorizontalAlignment.Left, VerticalAlignment.Top):
|
||||||
|
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
|
||||||
|
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
|
||||||
|
SetLeft(_curAnn, x + offsetX);
|
||||||
|
SetTop(_curAnn, y + offsetY);
|
||||||
|
break;
|
||||||
|
case (HorizontalAlignment.Center, VerticalAlignment.Top):
|
||||||
|
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
|
||||||
|
SetTop(_curAnn, y + offsetY);
|
||||||
|
break;
|
||||||
|
case (HorizontalAlignment.Right, VerticalAlignment.Top):
|
||||||
|
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
|
||||||
|
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY);
|
||||||
|
SetTop(_curAnn, y + offsetY);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case (HorizontalAlignment.Left, VerticalAlignment.Center):
|
||||||
|
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
|
||||||
|
SetLeft(_curAnn, x + offsetX);
|
||||||
|
break;
|
||||||
|
case (HorizontalAlignment.Right, VerticalAlignment.Center):
|
||||||
|
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case (HorizontalAlignment.Left, VerticalAlignment.Bottom):
|
||||||
|
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX);
|
||||||
|
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
|
||||||
|
SetLeft(_curAnn, x + offsetX);
|
||||||
|
break;
|
||||||
|
case (HorizontalAlignment.Center, VerticalAlignment.Bottom):
|
||||||
|
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
|
||||||
|
break;
|
||||||
|
case (HorizontalAlignment.Right, VerticalAlignment.Bottom):
|
||||||
|
_curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX);
|
||||||
|
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_lastPos = currentPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnnotationPositionStart(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
_lastPos = e.GetPosition(this);
|
||||||
|
_curAnn = (AnnotationControl)sender;
|
||||||
|
|
||||||
|
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
|
||||||
|
ClearSelections();
|
||||||
|
|
||||||
|
_curAnn.IsSelected = true;
|
||||||
|
|
||||||
|
FormState.SelectionState = SelectionState.AnnMoving;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnnotationPositionMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (FormState.SelectionState != SelectionState.AnnMoving)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var currentPos = e.GetPosition(this);
|
||||||
|
var offsetX = currentPos.X - _lastPos.X;
|
||||||
|
var offsetY = currentPos.Y - _lastPos.Y;
|
||||||
|
|
||||||
|
SetLeft(_curAnn, GetLeft(_curAnn) + offsetX);
|
||||||
|
SetTop(_curAnn, GetTop(_curAnn) + offsetY);
|
||||||
|
_lastPos = currentPos;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region NewAnnotation
|
||||||
|
|
||||||
|
private void NewAnnotationStart(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
_newAnnotationStartPos = e.GetPosition(this);
|
||||||
|
|
||||||
|
SetLeft(_newAnnotationRect, _newAnnotationStartPos.X);
|
||||||
|
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
|
||||||
|
_newAnnotationRect.MouseMove += NewAnnotationCreatingMove;
|
||||||
|
|
||||||
|
FormState.SelectionState = SelectionState.NewAnnCreating;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NewAnnotationCreatingMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (FormState.SelectionState != SelectionState.NewAnnCreating)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var currentPos = e.GetPosition(this);
|
||||||
|
var diff = currentPos - _newAnnotationStartPos;
|
||||||
|
|
||||||
|
_newAnnotationRect.Height = Math.Abs(diff.Y);
|
||||||
|
_newAnnotationRect.Width = Math.Abs(diff.X);
|
||||||
|
|
||||||
|
if (diff.X < 0)
|
||||||
|
SetLeft(_newAnnotationRect, currentPos.X);
|
||||||
|
if (diff.Y < 0)
|
||||||
|
SetTop(_newAnnotationRect, currentPos.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateAnnotation(Point endPos)
|
||||||
|
{
|
||||||
|
_newAnnotationRect.Width = 0;
|
||||||
|
_newAnnotationRect.Height = 0;
|
||||||
|
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
|
||||||
|
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
|
||||||
|
if (width < MIN_SIZE || height < MIN_SIZE)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var annotationControl = new AnnotationControl(CurrentAnnClass, AnnotationResizeStart)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnnotationControl CreateAnnotation(AnnotationClass annClass, AnnotationInfo info)
|
||||||
|
{
|
||||||
|
var annotationControl = new AnnotationControl(annClass, AnnotationResizeStart)
|
||||||
|
{
|
||||||
|
Width = info.Width,
|
||||||
|
Height = info.Height
|
||||||
|
};
|
||||||
|
annotationControl.MouseDown += AnnotationPositionStart;
|
||||||
|
SetLeft(annotationControl, info.X );
|
||||||
|
SetTop(annotationControl, info.Y);
|
||||||
|
Children.Add(annotationControl);
|
||||||
|
CurrentAnns.Add(annotationControl);
|
||||||
|
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
|
||||||
|
return annotationControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void RemoveAnnotations(IEnumerable<AnnotationControl> listToRemove)
|
||||||
|
{
|
||||||
|
foreach (var ann in listToRemove)
|
||||||
|
{
|
||||||
|
Children.Remove(ann);
|
||||||
|
CurrentAnns.Remove(ann);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void RemoveAllAnns() => RemoveAnnotations(CurrentAnns.ToList());
|
||||||
|
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList());
|
||||||
|
|
||||||
|
private void ClearSelections()
|
||||||
|
{
|
||||||
|
foreach (var ann in CurrentAnns)
|
||||||
|
ann.IsSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Controls
|
||||||
|
{
|
||||||
|
public class UpdatableProgressBar : ProgressBar
|
||||||
|
{
|
||||||
|
public delegate void ValueChange(double oldValue, double newValue);
|
||||||
|
|
||||||
|
public event ValueChange? ValueChanged;
|
||||||
|
|
||||||
|
public UpdatableProgressBar() : base()
|
||||||
|
{
|
||||||
|
MouseDown += OnMouseDown;
|
||||||
|
MouseMove += OnMouseMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double SetProgressBarValue(double mousePos)
|
||||||
|
{
|
||||||
|
Value = Minimum;
|
||||||
|
var pbValue = mousePos / ActualWidth * Maximum;
|
||||||
|
ValueChanged?.Invoke(Value, pbValue);
|
||||||
|
return pbValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
Value = SetProgressBarValue(e.GetPosition(this).X);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.LeftButton == MouseButtonState.Pressed)
|
||||||
|
Value = SetProgressBarValue(e.GetPosition(this).X);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.DTO;
|
||||||
|
|
||||||
|
public class AnnClassSelectedEvent(AnnotationClass annotationClass) : INotification
|
||||||
|
{
|
||||||
|
public AnnotationClass AnnotationClass { get; } = annotationClass;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Windows.Media;
|
||||||
|
using Azaion.Annotator.Extensions;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.DTO;
|
||||||
|
|
||||||
|
public class AnnotationClass(int id, string name = "")
|
||||||
|
{
|
||||||
|
public int Id { get; set; } = id;
|
||||||
|
|
||||||
|
public string Name { get; set; } = name;
|
||||||
|
public Color Color { get; set; } = id.ToColor();
|
||||||
|
|
||||||
|
public int ClassNumber => Id + 1;
|
||||||
|
public SolidColorBrush ColorBrush => new(Color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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}";
|
||||||
|
|
||||||
|
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 = annInfo.X - annInfo.Width * 2;
|
||||||
|
double top = annInfo.Y - annInfo.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.Split(' ');
|
||||||
|
if (strs.Length != 5)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new AnnotationInfo
|
||||||
|
{
|
||||||
|
ClassNumber = int.Parse(strs[0]),
|
||||||
|
X = double.Parse(strs[1]),
|
||||||
|
Y = double.Parse(strs[2]),
|
||||||
|
Width = double.Parse(strs[3]),
|
||||||
|
Height = double.Parse(strs[4])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Size = System.Windows.Size;
|
||||||
|
using Point = System.Windows.Point;
|
||||||
|
|
||||||
|
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 static readonly Size DefaultWindowSize = new(1280, 720);
|
||||||
|
private static readonly Point DefaultWindowLocation = new(100, 100);
|
||||||
|
|
||||||
|
|
||||||
|
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 List<AnnotationClass> AnnotationClasses { get; set; } = [];
|
||||||
|
public Size WindowSize { get; set; }
|
||||||
|
public Point WindowLocation { get; set; }
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
File.WriteAllText(CONFIG_PATH, JsonConvert.SerializeObject(this, Formatting.Indented), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Config Read()
|
||||||
|
{
|
||||||
|
if (!File.Exists(CONFIG_PATH))
|
||||||
|
{
|
||||||
|
var exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||||
|
return new Config
|
||||||
|
{
|
||||||
|
VideosDirectory = Path.Combine(exePath, DEFAULT_VIDEO_DIR),
|
||||||
|
LabelsDirectory = Path.Combine(exePath, DEFAULT_LABELS_DIR),
|
||||||
|
ImagesDirectory = Path.Combine(exePath, DEFAULT_IMAGES_DIR),
|
||||||
|
WindowLocation = DefaultWindowLocation,
|
||||||
|
WindowSize = DefaultWindowSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var str = File.ReadAllText(CONFIG_PATH);
|
||||||
|
return JsonConvert.DeserializeObject<Config>(str) ?? new Config();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return new Config();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.DTO;
|
||||||
|
|
||||||
|
public class FormState
|
||||||
|
{
|
||||||
|
public SelectionState SelectionState { get; set; } = SelectionState.None;
|
||||||
|
public string CurrentFile { get; set; } = null!;
|
||||||
|
public Size CurrentVideoSize { get; set; }
|
||||||
|
|
||||||
|
public string VideoName => Path.GetFileNameWithoutExtension(CurrentFile).Replace(" ", "");
|
||||||
|
public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.DTO;
|
||||||
|
|
||||||
|
public class KeyEvent(object sender, KeyEventArgs args) : INotification
|
||||||
|
{
|
||||||
|
public object Sender { get; set; } = sender;
|
||||||
|
public KeyEventArgs Args { get; set; } = args;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Azaion.Annotator.DTO;
|
||||||
|
|
||||||
|
public enum SelectionState
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
NewAnnCreating = 1,
|
||||||
|
AnnResizing = 2,
|
||||||
|
AnnMoving = 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Azaion.Annotator.DTO;
|
||||||
|
|
||||||
|
public class VideoFileInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
public string Path { get; set; } = null!;
|
||||||
|
public TimeSpan Duration { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Extensions;
|
||||||
|
|
||||||
|
public static class CanvasExtensions
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty PercentPositionProperty =
|
||||||
|
DependencyProperty.RegisterAttached("PercentPosition", typeof(Point), typeof(CanvasExtensions),
|
||||||
|
new PropertyMetadata(new Point(0, 0), OnPercentPositionChanged));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty PercentSizeProperty =
|
||||||
|
DependencyProperty.RegisterAttached("PercentSize", typeof(Point), typeof(CanvasExtensions),
|
||||||
|
new PropertyMetadata(new Point(0, 0), OnPercentSizeChanged));
|
||||||
|
|
||||||
|
private static void OnPercentSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetPercentPosition(DependencyObject obj, Point value) => obj.SetValue(PercentPositionProperty, value);
|
||||||
|
|
||||||
|
private static void OnPercentPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!(d is FrameworkElement element)) return;
|
||||||
|
if (!(VisualTreeHelper.GetParent(element) is Canvas canvas)) return;
|
||||||
|
|
||||||
|
canvas.SizeChanged += (s, arg) =>
|
||||||
|
{
|
||||||
|
var percentPosition = (Point)element.GetValue(PercentPositionProperty);
|
||||||
|
var xPosition = percentPosition.X * canvas.ActualWidth - element.ActualWidth / 2;
|
||||||
|
var yPosition = percentPosition.Y * canvas.ActualHeight - element.ActualHeight / 2;
|
||||||
|
Canvas.SetLeft(element, xPosition);
|
||||||
|
Canvas.SetTop(element, yPosition);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Extensions;
|
||||||
|
|
||||||
|
public static class ColorExtensions
|
||||||
|
{
|
||||||
|
public static Color ToColor(this int id)
|
||||||
|
{
|
||||||
|
var index = id % ColorValues.Length;
|
||||||
|
return ToColor($"#{ColorValues[index]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color ToColor(string hex)
|
||||||
|
{
|
||||||
|
var color = (Color)ColorConverter.ConvertFromString(hex);
|
||||||
|
color.A = 128;
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly string[] ColorValues =
|
||||||
|
[
|
||||||
|
"FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", "000000",
|
||||||
|
"800000", "008000", "000080", "808000", "800080", "008080", "808080",
|
||||||
|
"C00000", "00C000", "0000C0", "C0C000", "C000C0", "00C0C0", "C0C0C0",
|
||||||
|
"400000", "004000", "000040", "404000", "400040", "004040", "404040",
|
||||||
|
"200000", "002000", "000020", "202000", "200020", "002020", "202020",
|
||||||
|
"600000", "006000", "000060", "606000", "600060", "006060", "606060",
|
||||||
|
"A00000", "00A000", "0000A0", "A0A000", "A000A0", "00A0A0", "A0A0A0",
|
||||||
|
"E00000", "00E000", "0000E0", "E0E000", "E000E0", "00E0E0", "E0E0E0"
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Extensions;
|
||||||
|
|
||||||
|
public static class DataGridExtensions
|
||||||
|
{
|
||||||
|
public static DataGridCell? GetCell(this DataGrid grid, int rowIndex, int columnIndex = 0)
|
||||||
|
{
|
||||||
|
var row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromIndex(rowIndex);
|
||||||
|
if (row == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var presenter = FindVisualChild<DataGridCellsPresenter>(row);
|
||||||
|
if (presenter == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
||||||
|
if (cell != null) return cell;
|
||||||
|
|
||||||
|
// now try to bring into view and retrieve the cell
|
||||||
|
grid.ScrollIntoView(row, grid.Columns[columnIndex]);
|
||||||
|
cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? dependencyObj) where T : DependencyObject
|
||||||
|
{
|
||||||
|
if (dependencyObj == null)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObj); i++)
|
||||||
|
{
|
||||||
|
var child = VisualTreeHelper.GetChild(dependencyObj, i);
|
||||||
|
if (child is T dependencyObject)
|
||||||
|
{
|
||||||
|
yield return dependencyObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (T childOfChild in FindVisualChildren<T>(child))
|
||||||
|
{
|
||||||
|
yield return childOfChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TChildItem? FindVisualChild<TChildItem>(DependencyObject? obj) where TChildItem : DependencyObject =>
|
||||||
|
FindVisualChildren<TChildItem>(obj).FirstOrDefault();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Extensions;
|
||||||
|
|
||||||
|
public static class DirectoryInfoExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<FileInfo> GetFiles(this DirectoryInfo dir, params string[] searchExtensions) =>
|
||||||
|
dir.GetFiles().Where(f => searchExtensions.Any(s => f.Name.Contains(s, StringComparison.CurrentCultureIgnoreCase))).ToList();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator;
|
||||||
|
|
||||||
|
public static class SynchronizeInvokeExtensions
|
||||||
|
{
|
||||||
|
public static void InvokeEx<T>(this T t, Action<T> action) where T : ISynchronizeInvoke
|
||||||
|
{
|
||||||
|
if (t.InvokeRequired)
|
||||||
|
t.Invoke(action, [t]);
|
||||||
|
else
|
||||||
|
action(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<Window x:Class="Azaion.Annotator.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF" xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="MainWindow" Height="450" Width="1100">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="DataGridCellStyle1" TargetType="{x:Type DataGridCell}">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="{x:Type DataGridCell}">
|
||||||
|
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Background" Value="SteelBlue"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static DataGrid.FocusBorderBrushKey}}"/>
|
||||||
|
</Trigger>
|
||||||
|
<MultiTrigger>
|
||||||
|
<MultiTrigger.Conditions>
|
||||||
|
<Condition Property="IsSelected" Value="true"/>
|
||||||
|
<Condition Property="Selector.IsSelectionActive" Value="false"/>
|
||||||
|
</MultiTrigger.Conditions>
|
||||||
|
<Setter Property="Background" Value="SteelBlue"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
|
||||||
|
</MultiTrigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="false">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid ShowGridLines="False"
|
||||||
|
Background="Black"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="28"></RowDefinition>
|
||||||
|
<RowDefinition Height="28"></RowDefinition>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="28"></RowDefinition>
|
||||||
|
<RowDefinition Height="32"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250" />
|
||||||
|
<ColumnDefinition Width="4"/>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Menu Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="3"
|
||||||
|
Background="Black">
|
||||||
|
<MenuItem Header="File" Foreground="#FFBDBCBC" Margin="0,3,0,0">
|
||||||
|
<MenuItem x:Name="OpenFolderItem"
|
||||||
|
Foreground="Black"
|
||||||
|
IsEnabled="True" Header="Open Folder..." Click="MenuItem_OnClick"/>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
<Grid
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="220" />
|
||||||
|
<ColumnDefinition Width="30"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Margin="1"
|
||||||
|
x:Name="TbFolder"></TextBox>
|
||||||
|
<Button
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="1"
|
||||||
|
Click="OpenFolderButtonClick">
|
||||||
|
. . .
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ListView Grid.Row="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Name="LvFiles"
|
||||||
|
Background="Black"
|
||||||
|
SelectedItem="{Binding Path=SelectedVideo}" Foreground="#FFA4AFCC"
|
||||||
|
>
|
||||||
|
<ListView.View>
|
||||||
|
<GridView>
|
||||||
|
<GridViewColumn Width="Auto"
|
||||||
|
Header="Файл"
|
||||||
|
DisplayMemberBinding="{Binding Path=Name}"/>
|
||||||
|
<GridViewColumn Width="Auto"
|
||||||
|
Header="Тривалість"
|
||||||
|
DisplayMemberBinding="{Binding Path=Duration}"/>
|
||||||
|
</GridView>
|
||||||
|
</ListView.View>
|
||||||
|
</ListView>
|
||||||
|
<DataGrid x:Name="LvClasses"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="3"
|
||||||
|
Background="Black"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
SelectionMode="Single"
|
||||||
|
CellStyle="{DynamicResource DataGridCellStyle1}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
CanUserResizeRows="False"
|
||||||
|
CanUserResizeColumns="False">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTemplateColumn
|
||||||
|
Width="60"
|
||||||
|
Header="Клавіша"
|
||||||
|
CanUserSort="False">
|
||||||
|
<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"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
<GridSplitter
|
||||||
|
Background="DarkGray"
|
||||||
|
ResizeDirection="Columns"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="1"
|
||||||
|
ResizeBehavior="PreviousAndNext"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
/>
|
||||||
|
<wpf:VideoView
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.RowSpan="3"
|
||||||
|
x:Name="VideoView">
|
||||||
|
<controls:CanvasEditor x:Name="Editor" Background="#01000000" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
|
||||||
|
</wpf:VideoView>
|
||||||
|
<controls:UpdatableProgressBar x:Name="videoSlider"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="4"
|
||||||
|
Grid.ColumnSpan="3">
|
||||||
|
|
||||||
|
</controls:UpdatableProgressBar>
|
||||||
|
<Grid
|
||||||
|
Grid.Row="5"
|
||||||
|
Grid.Column="0"
|
||||||
|
Background="Black"
|
||||||
|
>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="28" />
|
||||||
|
<ColumnDefinition Width="28"/>
|
||||||
|
<ColumnDefinition Width="28"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Button Grid.Column="0" Padding="5" Background="Black" BorderBrush="Black">
|
||||||
|
<Path Stretch="Fill" Fill="LightGray" Data="m295.84 146.049-256-144c-4.96-2.784-11.008-2.72-15.904.128-4.928
|
||||||
|
2.88-7.936 8.128-7.936 13.824v288c0 5.696 3.008 10.944 7.936 13.824 2.496 1.44 5.28 2.176 8.064 2.176 2.688
|
||||||
|
0 5.408-.672 7.84-2.048l256-144c5.024-2.848 8.16-8.16 8.16-13.952s-3.136-11.104-8.16-13.952z" />
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="1" Padding="2" Width="25" Height="25" Background="Black" BorderBrush="Black">
|
||||||
|
<Image>
|
||||||
|
<Image.Source>
|
||||||
|
<DrawingImage>
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="F1 M320,320z M0,0z M112,0L16,0C7.168,0,0,7.168,0,16L0,304C0,312.832,7.168,320,16,320L112,320C120.832,320,128,312.832,128,304L128,16C128,7.168,120.832,0,112,0z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="F1 M320,320z M0,0z M304,0L208,0C199.168,0,192,7.168,192,16L192,304C192,312.832,199.168,320,208,320L304,320C312.832,320,320,312.832,320,304L320,16C320,7.168,312.832,0,304,0z" />
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</Image.Source>
|
||||||
|
</Image>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="2" Padding="2" Width="25" Height="25" Background="Black" BorderBrush="Black">
|
||||||
|
<Path Stretch="Fill" Fill="LightGray" Data="m288 0h-256c-17.632 0-32 14.368-32 32v256c0 17.632 14.368 32 32 32h256c17.632
|
||||||
|
0 32-14.368 32-32v-256c0-17.632-14.368-32-32-32z" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<StatusBar
|
||||||
|
Grid.Row="5"
|
||||||
|
Grid.Column="2"
|
||||||
|
>
|
||||||
|
<StatusBarItem>
|
||||||
|
|
||||||
|
</StatusBarItem>
|
||||||
|
<StatusBarItem>
|
||||||
|
<TextBlock x:Name="Help" Text="{Binding Path=CurrentHelp}"></TextBlock>
|
||||||
|
</StatusBarItem>
|
||||||
|
<StatusBarItem>
|
||||||
|
<TextBlock x:Name="Status"></TextBlock>
|
||||||
|
</StatusBarItem>
|
||||||
|
</StatusBar>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
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;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator;
|
||||||
|
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private readonly LibVLC _libVLC;
|
||||||
|
private readonly MediaPlayer _mediaPlayer;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
private readonly FormState _formState;
|
||||||
|
|
||||||
|
private readonly Config _config;
|
||||||
|
private readonly TimeSpan _annotationTime = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; }
|
||||||
|
private bool _suspendLayout;
|
||||||
|
|
||||||
|
public Dictionary<string, List<AnnotationInfo>> Annotations { get; set; } = new();
|
||||||
|
|
||||||
|
public string CurrentHelp { get; set; }
|
||||||
|
|
||||||
|
public MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer,
|
||||||
|
IMediator mediator,
|
||||||
|
FormState formState,
|
||||||
|
Config config)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_libVLC = libVLC;
|
||||||
|
_mediaPlayer = mediaPlayer;
|
||||||
|
_mediator = mediator;
|
||||||
|
_formState = formState;
|
||||||
|
_config = config;
|
||||||
|
|
||||||
|
VideoView.Loaded += VideoView_Loaded;
|
||||||
|
Closed += OnFormClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VideoView_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Core.Initialize();
|
||||||
|
InitControls();
|
||||||
|
|
||||||
|
_suspendLayout = true;
|
||||||
|
Left = _config.WindowLocation.X;
|
||||||
|
Top = _config.WindowLocation.Y;
|
||||||
|
Width = _config.WindowSize.Width;
|
||||||
|
Height = _config.WindowSize.Height;
|
||||||
|
_suspendLayout = false;
|
||||||
|
|
||||||
|
ReloadFiles();
|
||||||
|
if (_config.AnnotationClasses.Count == 0)
|
||||||
|
_config.AnnotationClasses.Add(new AnnotationClass(0));
|
||||||
|
|
||||||
|
AnnotationClasses = new ObservableCollection<AnnotationClass>(_config.AnnotationClasses);
|
||||||
|
LvClasses.ItemsSource = AnnotationClasses;
|
||||||
|
LvClasses.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitControls()
|
||||||
|
{
|
||||||
|
VideoView.MediaPlayer = _mediaPlayer;
|
||||||
|
|
||||||
|
_mediaPlayer.Playing += (sender, args) =>
|
||||||
|
{
|
||||||
|
uint vw = 0, vh = 0;
|
||||||
|
_mediaPlayer.Size(0, ref vw, ref vh);
|
||||||
|
_formState.CurrentVideoSize = new Size(vw, vh);
|
||||||
|
};
|
||||||
|
|
||||||
|
LvFiles.MouseDoubleClick += async (_, _) =>
|
||||||
|
{
|
||||||
|
Play((VideoFileInfo)LvFiles.SelectedItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
LvClasses.SelectionChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
|
||||||
|
Editor.CurrentAnnClass = selectedClass;
|
||||||
|
_mediator.Publish(new AnnClassSelectedEvent(selectedClass));
|
||||||
|
};
|
||||||
|
|
||||||
|
_mediaPlayer.PositionChanged += (o, args) =>
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() => videoSlider.Value = _mediaPlayer.Position * videoSlider.Maximum);
|
||||||
|
var curTime = _formState.GetTimeName(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
|
||||||
|
if (!Annotations.TryGetValue(curTime, out var annotationInfos))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var annotations = annotationInfos.Select(info =>
|
||||||
|
{
|
||||||
|
var annClass = _config.AnnotationClasses[info.ClassNumber];
|
||||||
|
var annInfo = info.ToCanvasCoordinates(Editor.RenderSize, _formState.CurrentVideoSize);
|
||||||
|
return Editor.CreateAnnotation(annClass, annInfo);
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
//remove annotations: either in 1 sec, either earlier if there is next annotation in a dictionary
|
||||||
|
var strs = curTime.Split("_");
|
||||||
|
var timeStr = strs.LastOrDefault();
|
||||||
|
var ts = TimeSpan.ParseExact(timeStr, "hmmssf", CultureInfo.InvariantCulture);
|
||||||
|
var timeSpanRemove = Enumerable.Range(0, (int)_annotationTime.TotalMilliseconds / 100)
|
||||||
|
.Select(x =>
|
||||||
|
{
|
||||||
|
var time = TimeSpan.FromMilliseconds(x * 100);
|
||||||
|
var fName = _formState.GetTimeName(ts.Add(time));
|
||||||
|
return Annotations.ContainsKey(fName) ? time : (TimeSpan?)null;
|
||||||
|
}).FirstOrDefault(x => x != null) ?? _annotationTime;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(timeSpanRemove);
|
||||||
|
Dispatcher.Invoke(() => Editor.RemoveAnnotations(annotations));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
videoSlider.ValueChanged += (value, newValue) =>
|
||||||
|
_mediaPlayer.Position = (float)(newValue / videoSlider.Maximum);
|
||||||
|
|
||||||
|
videoSlider.KeyDown += (sender, args) => _mediator.Publish(new KeyEvent(sender, args));
|
||||||
|
|
||||||
|
SizeChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!_suspendLayout)
|
||||||
|
_config.WindowSize = args.NewSize;
|
||||||
|
};
|
||||||
|
LocationChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (!_suspendLayout)
|
||||||
|
_config.WindowLocation = new Point(Left, Top);
|
||||||
|
};
|
||||||
|
|
||||||
|
Editor.FormState = _formState;
|
||||||
|
Editor.Mediator = _mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Play(VideoFileInfo videoFileInfo)
|
||||||
|
{
|
||||||
|
if (LvFiles.SelectedItem == null)
|
||||||
|
return;
|
||||||
|
var fileInfo = (VideoFileInfo)LvFiles.SelectedItem;
|
||||||
|
|
||||||
|
_formState.CurrentFile = fileInfo.Name;
|
||||||
|
LoadExistingAnnotations();
|
||||||
|
|
||||||
|
_mediaPlayer.Stop();
|
||||||
|
_mediaPlayer.Play(new Media(_libVLC, fileInfo.Path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadExistingAnnotations()
|
||||||
|
{
|
||||||
|
var dirInfo = new DirectoryInfo(_config.LabelsDirectory);
|
||||||
|
if (!dirInfo.Exists)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var files = dirInfo.GetFiles($"{_formState.VideoName}_*");
|
||||||
|
Annotations = files.ToDictionary(f => Path.GetFileNameWithoutExtension(f.Name), f =>
|
||||||
|
{
|
||||||
|
var str = File.ReadAllText(f.FullName);
|
||||||
|
return str.Split(Environment.NewLine).Select(AnnotationInfo.Parse).ToList();
|
||||||
|
})!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReloadFiles()
|
||||||
|
{
|
||||||
|
var dir = new DirectoryInfo(_config.VideosDirectory);
|
||||||
|
if (!dir.Exists)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var files = dir.GetFiles("mp4", "mov").Select(x =>
|
||||||
|
{
|
||||||
|
_mediaPlayer.Media = new Media(_libVLC, x.FullName);
|
||||||
|
return new VideoFileInfo
|
||||||
|
{
|
||||||
|
Name = x.Name,
|
||||||
|
Path = x.FullName,
|
||||||
|
Duration = TimeSpan.FromMilliseconds(_mediaPlayer.Media.Duration)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
LvFiles.ItemsSource = new ObservableCollection<VideoFileInfo>(files);
|
||||||
|
TbFolder.Text = _config.VideosDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFormClosed(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_mediaPlayer.Stop();
|
||||||
|
_mediaPlayer.Dispose();
|
||||||
|
_libVLC.Dispose();
|
||||||
|
_config.AnnotationClasses = AnnotationClasses.ToList();
|
||||||
|
_config.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// private void AddClassBtnClick(object sender, RoutedEventArgs e)
|
||||||
|
// {
|
||||||
|
// LvClasses.IsReadOnly = false;
|
||||||
|
// AnnotationClasses.Add(new AnnotationClass(AnnotationClasses.Count));
|
||||||
|
// LvClasses.SelectedIndex = AnnotationClasses.Count - 1;
|
||||||
|
// }
|
||||||
|
private void MenuItem_OnClick(object sender, RoutedEventArgs e) => OpenFolder();
|
||||||
|
private void OpenFolderButtonClick(object sender, RoutedEventArgs e) => OpenFolder();
|
||||||
|
private void OpenFolder()
|
||||||
|
{
|
||||||
|
var dlg = new CommonOpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Open Video folder",
|
||||||
|
IsFolderPicker = true,
|
||||||
|
InitialDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() != CommonFileDialogResult.Ok)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_config.VideosDirectory = dlg.FileName;
|
||||||
|
ReloadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Azaion.Annotator.DTO;
|
||||||
|
using LibVLCSharp.Shared;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator;
|
||||||
|
|
||||||
|
public class PlayerControlHandler:
|
||||||
|
INotificationHandler<KeyEvent>,
|
||||||
|
INotificationHandler<AnnClassSelectedEvent>
|
||||||
|
{
|
||||||
|
private const int STEP = 20;
|
||||||
|
private const int LARGE_STEP = 2000;
|
||||||
|
|
||||||
|
private static readonly string[] CatchSenders = ["ForegroundWindow", "ScrollViewer"];
|
||||||
|
private MediaPlayer mediaPlayer;
|
||||||
|
private readonly MainWindow _mainWindow;
|
||||||
|
private readonly FormState _formState;
|
||||||
|
private readonly Config _config;
|
||||||
|
|
||||||
|
public PlayerControlHandler(MediaPlayer mediaPlayer1, MainWindow mainWindow, FormState formState, Config config)
|
||||||
|
{
|
||||||
|
mediaPlayer = mediaPlayer1;
|
||||||
|
_mainWindow = mainWindow;
|
||||||
|
_formState = formState;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) =>
|
||||||
|
SelectClass(notification.AnnotationClass);
|
||||||
|
|
||||||
|
private void SelectClass(AnnotationClass annClass)
|
||||||
|
{
|
||||||
|
_mainWindow.Editor.CurrentAnnClass = annClass;
|
||||||
|
foreach (var ann in _mainWindow.Editor.CurrentAnns.Where(x => x.IsSelected))
|
||||||
|
ann.AnnotationClass = annClass;
|
||||||
|
_mainWindow.LvClasses.SelectedIndex = annClass.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(KeyEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
//Console.WriteLine($"Time: {DateTime.UtcNow:hh:mm:ss.fff}. Sender {notification.Sender.GetType().Name} Key {notification.Args.Key}");
|
||||||
|
if (!CatchSenders.Contains(notification.Sender.GetType().Name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var isCtrlPressed = notification.Args.KeyboardDevice.IsKeyDown(Key.LeftCtrl) ||
|
||||||
|
notification.Args.KeyboardDevice.IsKeyDown(Key.RightCtrl);
|
||||||
|
var step = isCtrlPressed ? STEP : LARGE_STEP;
|
||||||
|
var key = notification.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)
|
||||||
|
SelectClass(_mainWindow.AnnotationClasses[keyNumber.Value]);
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case Key.Space:
|
||||||
|
mediaPlayer.Pause();
|
||||||
|
break;
|
||||||
|
case Key.Left:
|
||||||
|
mediaPlayer.SetPause(true);
|
||||||
|
mediaPlayer.Time -= step;
|
||||||
|
break;
|
||||||
|
case Key.Right:
|
||||||
|
mediaPlayer.SetPause(true);
|
||||||
|
mediaPlayer.Time += step;
|
||||||
|
break;
|
||||||
|
case Key.Enter:
|
||||||
|
if (string.IsNullOrEmpty(_formState.CurrentFile))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var fName = _formState.GetTimeName(TimeSpan.FromMilliseconds(mediaPlayer.Time));
|
||||||
|
var currentAnns = _mainWindow.Editor.CurrentAnns
|
||||||
|
.Select(x => x.Info.ToLabelCoordinates(_mainWindow.Editor.RenderSize, _formState.CurrentVideoSize))
|
||||||
|
.ToList();
|
||||||
|
var labels = string.Join(Environment.NewLine, currentAnns.Select(x => x.ToString()));
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync($"{_config.LabelsDirectory}/{fName}.txt", labels, cancellationToken);
|
||||||
|
mediaPlayer.TakeSnapshot(0, $"{_config.ImagesDirectory}/{fName}.jpg", 0, 0);
|
||||||
|
|
||||||
|
_mainWindow.Annotations[fName] = currentAnns;
|
||||||
|
_mainWindow.Editor.RemoveAllAnns();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.Delete:
|
||||||
|
_mainWindow.Editor.RemoveSelectedAnns();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"LookupDirectory": "video",
|
||||||
|
"LabelsDirectory": "labels",
|
||||||
|
"ImagesDirectory": "images",
|
||||||
|
"AnnotationClasses": [
|
||||||
|
{
|
||||||
|
"Id": 0,
|
||||||
|
"Name": "Броньована техніка",
|
||||||
|
"Color": "#80FF0000",
|
||||||
|
"ColorBrush": "#80FF0000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 1,
|
||||||
|
"Name": "Вантажівка",
|
||||||
|
"Color": "#8000FF00",
|
||||||
|
"ColorBrush": "#8000FF00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 2,
|
||||||
|
"Name": "Машина легкова",
|
||||||
|
"Color": "#800000FF",
|
||||||
|
"ColorBrush": "#800000FF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 3,
|
||||||
|
"Name": "Артилерія",
|
||||||
|
"Color": "#80FFFF00",
|
||||||
|
"ColorBrush": "#80FFFF00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 4,
|
||||||
|
"Name": "Тінь від техніки",
|
||||||
|
"Color": "#80FF00FF",
|
||||||
|
"ColorBrush": "#80FF00FF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 5,
|
||||||
|
"Name": "Окопи",
|
||||||
|
"Color": "#8000FFFF",
|
||||||
|
"ColorBrush": "#8000FFFF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 6,
|
||||||
|
"Name": "Військовий",
|
||||||
|
"Color": "#80000000",
|
||||||
|
"ColorBrush": "#80000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 7,
|
||||||
|
"Name": "Накати",
|
||||||
|
"Color": "#80800000",
|
||||||
|
"ColorBrush": "#80800000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 8,
|
||||||
|
"Name": "Саркофаг",
|
||||||
|
"Color": "#80008000",
|
||||||
|
"ColorBrush": "#80008000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": 9,
|
||||||
|
"Name": "Дим",
|
||||||
|
"Color": "#80000080",
|
||||||
|
"ColorBrush": "#80000080"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"WindowSize": "1920,1080",
|
||||||
|
"WindowLocation": "200,121"
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"en": {
|
||||||
|
"File": "File",
|
||||||
|
"Duration": "Duration",
|
||||||
|
"Key": "Key",
|
||||||
|
"ClassName": "Class Name",
|
||||||
|
"HelpOpen": "Open file from the list",
|
||||||
|
"HelpPause": "Press Space to pause video and "
|
||||||
|
},
|
||||||
|
"ua": {
|
||||||
|
"File": "Файл",
|
||||||
|
"Duration": "Тривалість",
|
||||||
|
"Key": "Клавіша",
|
||||||
|
"ClassName": "Назва класу"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user