mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 23:46:30 +00:00
move Windows app to Windows folder, create folder for Web, create simplest web api service
This commit is contained in:
@@ -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,56 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
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.AddSingleton<HelpWindow>();
|
||||
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>()!;
|
||||
DispatcherUnhandledException += OnDispatcherUnhandledException;
|
||||
}
|
||||
|
||||
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
File.AppendAllText("logs.txt", e.Exception.Message);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
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,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ApplicationIcon>logo.ico</ApplicationIcon>
|
||||
</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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="logo.ico" />
|
||||
<Resource Include="logo.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Resource>
|
||||
<None Update="config.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,108 @@
|
||||
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 = null!;
|
||||
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),
|
||||
FontSize = 14,
|
||||
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,334 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
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 readonly TextBlock _classNameHint;
|
||||
|
||||
private Rectangle _curRec = new();
|
||||
private AnnotationControl _curAnn = null!;
|
||||
|
||||
private const int MIN_SIZE = 20;
|
||||
|
||||
public FormState FormState { get; set; } = null!;
|
||||
public IMediator Mediator { get; set; } = null!;
|
||||
|
||||
private AnnotationClass _currentAnnClass = null!;
|
||||
public AnnotationClass CurrentAnnClass
|
||||
{
|
||||
get => _currentAnnClass;
|
||||
set
|
||||
{
|
||||
_verticalLine.Stroke = value.ColorBrush;
|
||||
_verticalLine.Fill = value.ColorBrush;
|
||||
_horizontalLine.Stroke = value.ColorBrush;
|
||||
_horizontalLine.Fill = value.ColorBrush;
|
||||
_classNameHint.Text = value.Name;
|
||||
|
||||
_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
|
||||
};
|
||||
_classNameHint = new TextBlock
|
||||
{
|
||||
Text = CurrentAnnClass?.Name ?? "asd",
|
||||
Foreground = new SolidColorBrush(Colors.Black),
|
||||
Cursor = Cursors.Arrow,
|
||||
FontSize = 16,
|
||||
FontWeight = FontWeights.Bold
|
||||
};
|
||||
_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);
|
||||
Children.Add(_classNameHint);
|
||||
}
|
||||
|
||||
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;
|
||||
SetLeft(_classNameHint, pos.X + 10);
|
||||
SetTop(_classNameHint, pos.Y - 30);
|
||||
|
||||
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 new 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,121 @@
|
||||
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,61 @@
|
||||
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 bool ShowHelpOnStart { get; set; }
|
||||
|
||||
public void Save()
|
||||
{
|
||||
File.WriteAllText(CONFIG_PATH, JsonConvert.SerializeObject(this, Formatting.Indented), Encoding.UTF8);
|
||||
}
|
||||
|
||||
public static Config Read()
|
||||
{
|
||||
string configFilePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), CONFIG_PATH);
|
||||
|
||||
if (!File.Exists(configFilePath))
|
||||
{
|
||||
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
|
||||
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,
|
||||
ShowHelpOnStart = true
|
||||
};
|
||||
}
|
||||
try
|
||||
{
|
||||
var str = File.ReadAllText(CONFIG_PATH);
|
||||
return JsonConvert.DeserializeObject<Config>(str) ?? new Config();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new Config();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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 TimeSpan CurrentVideoLength { get; set; }
|
||||
public int CurrentVolume { get; set; } = 100;
|
||||
|
||||
public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
|
||||
public class PlaybackControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
|
||||
{
|
||||
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
|
||||
}
|
||||
|
||||
public class VolumeChangedEvent(int volume) : INotification
|
||||
{
|
||||
public int Volume { get; set; } = volume;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Azaion.Annotator.DTO;
|
||||
|
||||
public enum PlaybackControlEnum
|
||||
{
|
||||
None = 0,
|
||||
Play = 1,
|
||||
Pause = 2,
|
||||
Stop = 3,
|
||||
PreviousFrame = 4,
|
||||
NextFrame = 5,
|
||||
SaveAnnotations = 6,
|
||||
RemoveSelectedAnns = 7,
|
||||
RemoveAllAnns = 8,
|
||||
TurnOffVolume = 9,
|
||||
TurnOnVolume = 10
|
||||
}
|
||||
@@ -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,10 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
|
||||
public static class DirectoryInfoExtensions
|
||||
{
|
||||
public static IEnumerable<FileInfo> GetFiles(this DirectoryInfo dir, params string[] searchExtensions) =>
|
||||
dir.GetFiles("*.*", SearchOption.AllDirectories)
|
||||
.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,24 @@
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public enum HelpTextEnum
|
||||
{
|
||||
None = 0,
|
||||
Initial = 1,
|
||||
PlayVideo = 2,
|
||||
PauseForAnnotations = 3,
|
||||
AnnotationHelp = 4
|
||||
}
|
||||
|
||||
public class HelpTexts
|
||||
{
|
||||
public static Dictionary<HelpTextEnum, string> HelpTextsDict = new()
|
||||
{
|
||||
{ HelpTextEnum.None, "" },
|
||||
{ HelpTextEnum.Initial, "Натисніть Файл - Відкрити папку... та виберіть папку з вашими відео для анотації" },
|
||||
{ HelpTextEnum.PlayVideo, "В списку відео виберіть потрібне та [подвійний клік] чи [Eнтер] на ньому - запустіть його на перегляд" },
|
||||
{ HelpTextEnum.PauseForAnnotations, "В потрібному місці відео де є один з об'єктів для анотації зупиніть його [Пробіл] або кн. на панелі" },
|
||||
{ HelpTextEnum.AnnotationHelp, "Клавішами [1] - [9] або мишкою оберіть потрібний клас та виділіть, тобто зробіть анотації всіх необхідних об'єктів. " +
|
||||
"Непотрібні анотації можна виділити (через [Ctrl] декілька) та [Del] видалити. [Eнтер] для збереження і перегляду далі" }
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<Window x:Class="Azaion.Annotator.HelpWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Azaion.Annotator"
|
||||
mc:Ignorable="d"
|
||||
Title="Як анотувати: прочитайте будь ласка, це важливо" Height="700" Width="800"
|
||||
ResizeMode="NoResize"
|
||||
Topmost="True"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" TextWrapping="Wrap" FontSize="18" >
|
||||
Анотація - це виділений на кадрі відео об'єкт з якимось класом (Броньована техніка, вантажівка, тощо)
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="1" TextWrapping="Wrap" FontSize="18" >
|
||||
1. Анотації мусять містити об'єкти найкращої чіткості та якості. Сильно розмазані чи задимлені об'єкти не підходять
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="2" TextWrapping="Wrap" FontSize="18" >
|
||||
2. Чим більше ракурсів одного і того самого об'єкту - тим краще. Наприклад, якщо на відео об'єкт малий,
|
||||
а далі на нього наводиться камера, то треба анотувати як малий об'єкт, так і великий. Якщо об'єкт статичний і ракурс не змінюється,
|
||||
достатньо одної анотації, а якщо рухається, і видно об'єкт з різних боків - то треба пару, по 1 на ракурс
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="3" TextWrapping="Wrap" FontSize="18" >
|
||||
3. Анотація об'єктів з формою що суттєво відрізняється від прямокутника. Наприклад, якщо танк має довге дуло, саме дуло не треба виділяти,
|
||||
оскільки попадає в анотацію дуже багато зайвого. Те ж саме з окопами - якщо окопи займають візуально багато місця,
|
||||
і в квадрат буде попадати багато зайвого, то краще зробити пару малих анотацій саме окопів
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="4" TextWrapping="Wrap" FontSize="18" >
|
||||
4. Будь-які існуючі позначки на відео, OSD і інше не мусять бути в анотаціях. Анотація мусить мати лише конкретний об'єкт без ліній на ньому
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="5" TextWrapping="Wrap" FontSize="18" >
|
||||
5. До кожного відео мусить бути 2-3 пустих фоток без об'єктів і без анотацій, для гарнішого навчання. (Просто натиснути [Ентер] на парі різних кадрів без нічого)
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="6" TextWrapping="Wrap" FontSize="18" >
|
||||
6. Об'єкти одного класу мусять бути візуально схожими, тоді як об'єкти різних класів мусять візуально відрізнятися.
|
||||
Оскільки це не є каталог військової техніки, а програма для створення датасету для навчання нейронної мережі,
|
||||
то принципи обрання класів мусять підпорядковуватись візуальній схожості для кращого розпізнавання, а не чіткій військовій класифікації.
|
||||
Наприклад, артилерія - це переважно міномети, тобто візуально це труба з чимось на основі.
|
||||
Тоді будь яка самохідна артилерія на гусеницях, хоч вона являє собою артилерію, мусить бути анотована як "Броньована техніка", оскільки візуально
|
||||
вона значно більш схожа на танк ніж на міномет.
|
||||
</TextBlock>
|
||||
<CheckBox Grid.Row="7" x:Name="CbShowHelp" Margin="10" Checked="CbShowHelp_OnChecked" Unchecked="CbShowHelp_OnUnchecked">
|
||||
Показувати при запуску
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Windows;
|
||||
using Azaion.Annotator.DTO;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public partial class HelpWindow : Window
|
||||
{
|
||||
private readonly Config _config;
|
||||
|
||||
public HelpWindow(Config config)
|
||||
{
|
||||
_config = config;
|
||||
Loaded += (_, _) => CbShowHelp.IsChecked = _config.ShowHelpOnStart;
|
||||
InitializeComponent();
|
||||
}
|
||||
private void Close(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _config.ShowHelpOnStart = true;
|
||||
private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _config.ShowHelpOnStart = false;
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
<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="Azaion Annotator" 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="Файл" Foreground="#FFBDBCBC" Margin="0,3,0,0">
|
||||
<MenuItem x:Name="OpenFolderItem"
|
||||
Foreground="Black"
|
||||
IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0">
|
||||
<MenuItem x:Name="OpenHelpWindow"
|
||||
Foreground="Black"
|
||||
IsEnabled="True" Header="Як анотувати" Click="OpenHelpWindowClick"/>
|
||||
</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"
|
||||
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="60"
|
||||
Header="Клавіша"
|
||||
CanUserSort="False">
|
||||
<DataGridTemplateColumn.HeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#252525"></Setter>
|
||||
</Style>
|
||||
</DataGridTemplateColumn.HeaderStyle>
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="{Binding Path=ColorBrush}">
|
||||
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
Header="Назва"
|
||||
Binding="{Binding Path=Name}"
|
||||
CanUserSort="False">
|
||||
<DataGridTextColumn.HeaderStyle>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#252525"></Setter>
|
||||
</Style>
|
||||
</DataGridTextColumn.HeaderStyle>
|
||||
</DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<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"
|
||||
Background="#252525"
|
||||
Foreground="LightBlue">
|
||||
|
||||
</controls:UpdatableProgressBar>
|
||||
<Grid
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Background="Black"
|
||||
>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="28"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black"
|
||||
Click="PlayClick">
|
||||
<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" ToolTip="Пауза/Відновити. Клавіша: [Пробіл]" Background="Black" BorderBrush="Black"
|
||||
Click="PauseClick">
|
||||
<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" ToolTip="Зупинити перегляд" Background="Black" BorderBrush="Black"
|
||||
Click="StopClick">
|
||||
<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>
|
||||
<Button Grid.Column="3" Padding="2" Width="25" Height="25" ToolTip="На 1 кадр назад. +[Ctrl] на 5 секунд назад. Клавіша: [Вліво]" Background="Black" BorderBrush="Black"
|
||||
Click="PreviousFrameClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m23.026 4.99579v22.00155c.00075.77029-.83285 1.25227-1.49993.86724l-19.05188-11.00078c-.66693-.38492-.66693-1.34761
|
||||
0-1.73254l19.05188-11.00078c.62227-.35929 1.49993.0539 1.49993.86531z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m29.026 4h-2c-.554 0-1 .446-1 1v22c0 .554.446 1 1 1h2c.554 0 1-.446 1-1v-22c0-.554-.446-1-1-1z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="4" Padding="2" Width="25" Height="25" ToolTip="На 1 кадр вперед. +[Ctrl] на 5 секунд вперед. Клавіша: [Вправо]" Background="Black" BorderBrush="Black"
|
||||
Click="NextFrameClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m8.974 4.99579v22.00155c-.00075.77029.83285 1.25227 1.49993.86724l19.05188-11.00078c.66693-.38492.66693-1.34761
|
||||
0-1.73254l-19.05188-11.00078c-.62227-.35929-1.49993.0539-1.49993.86531z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m2.974 4h2c.554 0 1 .446 1 1v22c0 .554-.446 1-1 1h-2c-.554 0-1-.446-1-1v-22c0-.554.446-1 1-1z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="5" Padding="2" Width="25" Height="25" ToolTip="Зберегти анотації та продовжити. Клавіша: [Ентер]" Background="Black" BorderBrush="Black"
|
||||
Click="SaveAnnotationsClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m30.71 7.29-6-6a1 1 0 0 0 -.71-.29h-2v8a2 2 0 0 1 -2 2h-8a2 2 0 0
|
||||
1 -2-2v-8h-6a3 3 0 0 0 -3 3v24a3 3 0 0 0 3 3h2v-9a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v9h2a3 3 0 0 0 3-3v-20a1 1 0 0 0 -.29-.71z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m12 1h8v8h-8z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m23 21h-14a1 1 0 0 0 -1 1v9h16v-9a1 1 0 0 0 -1-1z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button Grid.Column="6" Padding="2" Width="25" Height="25" ToolTip="Видалити обрані анотації. Клавіша: [Del]" Background="Black" BorderBrush="Black"
|
||||
Click="RemoveSelectedClick">
|
||||
<Path Stretch="Fill" Fill="LightGray" Data="M395.439,368.206h18.158v45.395h-45.395v-18.158h27.236V368.206z M109.956,413.601h64.569v-18.158h-64.569V413.601z
|
||||
M239.082,413.601h64.558v-18.158h-64.558V413.601z M18.161,368.206H0.003v45.395h45.395v-18.158H18.161V368.206z M18.161,239.079
|
||||
H0.003v64.562h18.158V239.079z M18.161,109.958H0.003v64.563h18.158V109.958z M0.003,45.395h18.158V18.158h27.237V0H0.003V45.395z
|
||||
M174.519,0h-64.563v18.158h64.563V0z M303.64,0h-64.558v18.158h64.558V0z M368.203,0v18.158h27.236v27.237h18.158V0H368.203z
|
||||
M395.439,303.642h18.158v-64.562h-18.158V303.642z M395.439,174.521h18.158v-64.563h-18.158V174.521z M325.45,93.187
|
||||
c-11.467-11.464-30.051-11.464-41.518,0l-77.135,77.129l-77.129-77.129c-11.476-11.464-30.056-11.464-41.521,0
|
||||
c-11.476,11.47-11.476,30.062,0,41.532l77.118,77.123l-77.124,77.124c-11.476,11.479-11.476,30.062,0,41.529
|
||||
c5.73,5.733,13.243,8.605,20.762,8.605c7.516,0,15.028-2.872,20.765-8.605l77.129-77.124l77.129,77.124
|
||||
c5.728,5.733,13.246,8.605,20.765,8.605c7.513,0,15.025-2.872,20.759-8.605c11.479-11.467,11.479-30.062,0-41.529l-77.124-77.124
|
||||
l77.124-77.123C336.923,123.243,336.923,104.656,325.45,93.187z" />
|
||||
</Button>
|
||||
<Button Grid.Column="7" Padding="2" Width="25" Height="25" ToolTip="Видалити всі аннотації. Клавіша: [X]" Background="Black" BorderBrush="Black"
|
||||
Click="RemoveAllClick">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m66.1455 13.1562c2.2083-4.26338 7.4546-5.92939 11.718-3.72109 4.2702 2.21179
|
||||
5.9335 7.47029 3.7121 11.73549l-8.9288 17.1434c-.3573.6862-.8001 1.3124-1.312 1.8677 2.44 3.6128 3.1963 8.2582 1.6501
|
||||
12.6558-.3523 1.002-.7242 2.0466-1.1108 3.1145-.1645.4546-.6923.659-1.1208.4351l-28.8106-15.0558c-.4666-.2438-.5746-.8639-.2219-1.2547.7171-.7943
|
||||
1.4152-1.5917 2.0855-2.3761 3.1513-3.6881 7.8213-5.7743 12.5381-5.6197.0534-.1099.1097-.2193.1689-.3283z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m37.7187 44.9911c-.3028-.1582-.6723-.1062-.9226.1263-1.7734 1.6478-3.5427
|
||||
3.0861-5.1934 4.1101-5.5739 3.4578-10.1819 4.704-13.0435 5.1463-1.6736.2587-3.032 1.3362-3.6937 2.7335-.6912 1.4595-.6391
|
||||
3.3721.7041 4.8522 1.48 1.6309 3.6724 3.7893 6.8345 6.3861.1854.1523.4298.2121.665.1649 2.2119-.4446 4.5148-.8643
|
||||
6.5245-1.9149.5849-.3058 1.4606-.8505 2.5588-1.7923 1.0935-.9379 2.7579-.8372 3.7175.2247.9595 1.062.8509 2.6831-.2426
|
||||
3.621-1.3886 1.1908-2.596 1.965-3.5534 2.4655-.7833.4094-1.603.7495-2.4399 1.0396-.6358.2203-.7846 1.0771-.2325 1.4619
|
||||
1.5928 1.1099 3.3299 2.2689 5.223 3.4729.9682.6158 1.9229 1.1946 2.8588 1.7383.2671.1552.6002.141.8515-.0387 1.351-.9664
|
||||
2.5145-1.9362 3.463-2.8261 2.1458-2.013 3.9974-4.231 5.4947-6.7819.7286-1.2414 2.3312-1.6783 3.5794-.9757s1.6693 2.2785.9406
|
||||
3.52c-1.7525 2.9859-3.9213 5.6002-6.4356 7.9591-.4351.4082-.9081.8302-1.4172 1.2601-.4505.3805-.3701 1.1048.1642 1.3543 3.184
|
||||
1.4867 5.8634 2.4904 7.7071 3.1131 2.6745.9033 5.5327-.1298 7.0673-2.4281 1.9401-2.9057 5.3476-8.3855 8.2732-15.0533.7591-1.7301
|
||||
1.5313-3.6163 2.2883-5.5494.1485-.3793-.0133-.8092-.3743-.9978z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m22.9737 37.9072c2.0802 0 3.7666-1.6864 3.7666-3.7667 0-2.0802-1.6864-3.7666-3.7666-3.7666-2.0803
|
||||
0-3.7667 1.6864-3.7667 3.7666 0 2.0803 1.6864 3.7667 3.7667 3.7667z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m12.7198 49.4854c2.0802 0 3.7666-1.6864 3.7666-3.7667 0-2.0802-1.6864-3.7666-3.7666-3.7666-2.0803
|
||||
0-3.76667 1.6864-3.76668 3.7666 0 2.0803 1.68638 3.7667 3.76668 3.7667z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="TurnOffVolumeBtn"
|
||||
Visibility="Visible"
|
||||
Grid.Column="8" Padding="2" Width="25"
|
||||
Height="25"
|
||||
ToolTip="Виключити звук. Клавіша: [M]" Background="Black" BorderBrush="Black"
|
||||
Click="TurnOffVolume">
|
||||
<Path Stretch="Fill" Fill="LightGray" Data="m9.383 3.07602c.18269.07574.33881.20395.44863.36842.10983.16447.16837.35781.16837
|
||||
.55558v11.99998c-.00004.1978-.05871.3911-.1686.5555-.10988.1644-.26605.2925-.44875.3682s-.38373.0955-.57768.0569-.37212-.1338-.51197-.2736l-3.707
|
||||
-3.707h-2.586c-.26522 0-.51957-.1053-.70711-.2929-.18753-.1875-.29289-.4419-.29289-.7071v-3.99998c0-.26522.10536-.51957.29289-.70711.18754-.18754
|
||||
.44189-.29289.70711-.29289h2.586l3.707-3.707c.13985-.13994.31805-.23524.51208-.27387.19402-.03863.39515-.01884.57792.05687zm5.274-.147c.1875-.18747
|
||||
.4418-.29279.707-.29279s.5195.10532.707.29279c.9298.92765 1.6672 2.02985 2.1699 3.24331.5026 1.21345.7606 2.51425.7591 3.82767.0015 1.3135-.2565
|
||||
2.6143-.7591 3.8277-.5027 1.2135-1.2401 2.3157-2.1699 3.2433-.1886.1822-.4412.283-.7034.2807s-.513-.1075-.6984-.2929-.2906-.4362-.2929-.6984
|
||||
.0985-.5148.2807-.7034c.7441-.7419 1.3342-1.6237 1.7363-2.5945.4022-.9709.6083-2.0117.6067-3.0625 0-2.20998-.894-4.20798-2.343-5.65698-.1875
|
||||
-.18753-.2928-.44184-.2928-.707 0-.26517.1053-.51948.2928-.707zm-2.829 2.828c.0929-.09298.2032-.16674.3246-.21706.1214-.05033.2515-.07623.3829
|
||||
-.07623s.2615.0259.3829.07623c.1214.05032.2317.12408.3246.21706.5579.55666 1.0003 1.21806 1.3018 1.94621.3015.72814.4562 1.50868.4552 2.29677.001
|
||||
.7881-.1537 1.5686-.4553 2.2968-.3015.7281-.7439 1.3895-1.3017 1.9462-.1876.1877-.4421.2931-.7075.2931s-.5199-.1054-.7075-.2931c-.1876-.1876
|
||||
-.2931-.4421-.2931-.7075 0-.2653.1055-.5198.2931-.7075.3722-.3708.6673-.8116.8685-1.2969.2011-.4854.3043-1.0057.3035-1.5311.0008-.52537-.1023
|
||||
-1.04572-.3035-1.53107-.2011-.48536-.4963-.92612-.8685-1.29691-.093-.09288-.1667-.20316-.2171-.32456-.0503-.1214-.0762-.25153-.0762-.38294
|
||||
0-.13142.0259-.26155.0762-.38294.0504-.1214.1241-.23169.2171-.32456z" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="TurnOnVolumeBtn"
|
||||
Visibility="Collapsed"
|
||||
Grid.Column="8" Padding="2" Width="25"
|
||||
Height="25"
|
||||
ToolTip="Включити звук. Клавіша: [M]" Background="Black" BorderBrush="Black"
|
||||
Click="TurnOnVolume">
|
||||
<Image>
|
||||
<Image.Source>
|
||||
<DrawingImage>
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m9.38268 3.07615c.37368.15478.61732.51942.61732.92388v11.99997c0
|
||||
.4045-.24364.7691-.61732.9239-.37367.1548-.80379.0692-1.08979-.2168l-3.7071-3.7071h-2.58579c-.55228
|
||||
0-1-.4477-1-1v-3.99997c0-.55229.44772-1 1-1h2.58579l3.7071-3.70711c.286-.286.71612-.37155 1.08979-.21677z" />
|
||||
<GeometryDrawing Brush="LightGray" Geometry="m12.2929 7.29289c.3905-.39052 1.0237-.39052 1.4142 0l1.2929
|
||||
1.2929 1.2929-1.2929c.3905-.39052 1.0237-.39052 1.4142 0 .3905.39053.3905 1.02369 0 1.41422l-1.2929 1.29289
|
||||
1.2929 1.2929c.3905.3905.3905 1.0237 0 1.4142s-1.0237.3905-1.4142 0l-1.2929-1.2929-1.2929
|
||||
1.2929c-.3905.3905-1.0237.3905-1.4142 0s-.3905-1.0237 0-1.4142l1.2929-1.2929-1.2929-1.29289c-.3905-.39053-.3905-1.02369
|
||||
0-1.41422z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
</Image.Source>
|
||||
</Image>
|
||||
</Button>
|
||||
</Grid>
|
||||
<StatusBar
|
||||
Grid.Row="5"
|
||||
Grid.Column="2"
|
||||
Background="#252525"
|
||||
Foreground="White"
|
||||
>
|
||||
<StatusBar.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
</Grid>
|
||||
</ItemsPanelTemplate>
|
||||
</StatusBar.ItemsPanel>
|
||||
<StatusBarItem Grid.Column="0" Background="Black">
|
||||
<controls:UpdatableProgressBar x:Name="Volume"
|
||||
Width="70"
|
||||
Height="15"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="#252525"
|
||||
BorderBrush="#252525"
|
||||
Foreground="LightBlue"
|
||||
Maximum="100"
|
||||
Minimum="0">
|
||||
</controls:UpdatableProgressBar>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem Grid.Column="1">
|
||||
<TextBlock Margin="3 0 0 0" x:Name="StatusClock" FontSize="16" Text="00:00 / 00:00"></TextBlock>
|
||||
</StatusBarItem>
|
||||
<Separator Grid.Column="2" />
|
||||
<StatusBarItem Grid.Column="3">
|
||||
<TextBlock Margin="3 0 0 0" x:Name="StatusHelp" FontSize="12" ></TextBlock>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem Grid.Column="4">
|
||||
<TextBlock x:Name="Status"></TextBlock>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,270 @@
|
||||
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 Point = System.Windows.Point;
|
||||
using Size = System.Windows.Size;
|
||||
|
||||
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 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 MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer,
|
||||
IMediator mediator,
|
||||
FormState formState,
|
||||
Config config,
|
||||
HelpWindow helpWindow)
|
||||
{
|
||||
InitializeComponent();
|
||||
_libVLC = libVLC;
|
||||
_mediaPlayer = mediaPlayer;
|
||||
_mediator = mediator;
|
||||
_formState = formState;
|
||||
_config = config;
|
||||
_helpWindow = helpWindow;
|
||||
|
||||
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;
|
||||
|
||||
if (LvFiles.Items.IsEmpty)
|
||||
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
|
||||
|
||||
if (_config.ShowHelpOnStart)
|
||||
_helpWindow.Show();
|
||||
}
|
||||
|
||||
public void BlinkHelp(string helpText, int times = 2)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
for (int i = 0; i < times; i++)
|
||||
{
|
||||
Dispatcher.Invoke(() => StatusHelp.Text = helpText);
|
||||
await Task.Delay(200);
|
||||
Dispatcher.Invoke(() => StatusHelp.Text = "");
|
||||
await Task.Delay(200);
|
||||
}
|
||||
Dispatcher.Invoke(() => StatusHelp.Text = helpText);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
|
||||
};
|
||||
|
||||
LvFiles.MouseDoubleClick += async (_, _) => await _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Play));
|
||||
|
||||
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);
|
||||
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))
|
||||
return;
|
||||
|
||||
var annotations = annotationInfos.Select(info =>
|
||||
{
|
||||
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));
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
VideoSlider.ValueChanged += (value, newValue) =>
|
||||
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
|
||||
|
||||
VideoSlider.KeyDown += (sender, args) => _mediator.Publish(new KeyEvent(sender, args));
|
||||
|
||||
Volume.ValueChanged += (_, newValue) => _mediator.Publish(new VolumeChangedEvent((int)newValue));
|
||||
|
||||
SizeChanged += (sender, args) =>
|
||||
{
|
||||
if (!_suspendLayout)
|
||||
_config.WindowSize = args.NewSize;
|
||||
};
|
||||
LocationChanged += (_, _) =>
|
||||
{
|
||||
if (!_suspendLayout)
|
||||
_config.WindowLocation = new Point(Left, Top);
|
||||
};
|
||||
|
||||
Editor.FormState = _formState;
|
||||
Editor.Mediator = _mediator;
|
||||
}
|
||||
|
||||
public 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)
|
||||
.Where(x => x != null)
|
||||
.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)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
LvFiles.ItemsSource = new ObservableCollection<VideoFileInfo>(files);
|
||||
TbFolder.Text = _config.VideosDirectory;
|
||||
|
||||
BlinkHelp(files.Count == 0
|
||||
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
|
||||
: HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]);
|
||||
}
|
||||
|
||||
private void OnFormClosed(object? sender, EventArgs e)
|
||||
{
|
||||
_mediaPlayer.Stop();
|
||||
_mediaPlayer.Dispose();
|
||||
_libVLC.Dispose();
|
||||
_config.AnnotationClasses = AnnotationClasses.ToList();
|
||||
_config.Save();
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
// private void AddClassBtnClick(object sender, RoutedEventArgs e)
|
||||
// {
|
||||
// LvClasses.IsReadOnly = false;
|
||||
// AnnotationClasses.Add(new AnnotationClass(AnnotationClasses.Count));
|
||||
// LvClasses.SelectedIndex = AnnotationClasses.Count - 1;
|
||||
// }
|
||||
private void OpenFolderItemClick(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(AppDomain.CurrentDomain.BaseDirectory)
|
||||
};
|
||||
if (dlg.ShowDialog() != CommonFileDialogResult.Ok)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrEmpty(dlg.FileName))
|
||||
_config.VideosDirectory = dlg.FileName;
|
||||
|
||||
ReloadFiles();
|
||||
}
|
||||
|
||||
private void PlayClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Play));
|
||||
private void PauseClick(object sender, RoutedEventArgs e)=> _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Pause));
|
||||
private void StopClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Stop));
|
||||
|
||||
private void PreviousFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.PreviousFrame));
|
||||
private void NextFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.NextFrame));
|
||||
|
||||
private void SaveAnnotationsClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.SaveAnnotations));
|
||||
|
||||
private void RemoveSelectedClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.RemoveSelectedAnns));
|
||||
private void RemoveAllClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.RemoveAllAnns));
|
||||
private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOffVolume));
|
||||
private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOnVolume));
|
||||
|
||||
private void OpenHelpWindowClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_helpWindow.Show();
|
||||
_helpWindow.Activate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Azaion.Annotator.DTO;
|
||||
using LibVLCSharp.Shared;
|
||||
using MediatR;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
|
||||
public class PlayerControlHandler(LibVLC libVLC, MediaPlayer mediaPlayer, MainWindow mainWindow, FormState formState, Config config) :
|
||||
INotificationHandler<KeyEvent>,
|
||||
INotificationHandler<AnnClassSelectedEvent>,
|
||||
INotificationHandler<PlaybackControlEvent>,
|
||||
INotificationHandler<VolumeChangedEvent>
|
||||
{
|
||||
private const int STEP = 20;
|
||||
private const int LARGE_STEP = 5000;
|
||||
private const int RESULT_WIDTH = 1280;
|
||||
|
||||
private static readonly string[] CatchSenders = ["ForegroundWindow", "ScrollViewer", "VideoView"];
|
||||
|
||||
private readonly Dictionary<Key, PlaybackControlEnum> KeysControlEnumDict = new()
|
||||
{
|
||||
{ Key.Space, PlaybackControlEnum.Pause },
|
||||
{ Key.Left, PlaybackControlEnum.PreviousFrame },
|
||||
{ Key.Right, PlaybackControlEnum.NextFrame },
|
||||
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
|
||||
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
|
||||
{ Key.X, PlaybackControlEnum.RemoveAllAnns }
|
||||
};
|
||||
|
||||
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
SelectClass(notification.AnnotationClass);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
if (!CatchSenders.Contains(notification.Sender.GetType().Name))
|
||||
return;
|
||||
|
||||
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]);
|
||||
|
||||
if (KeysControlEnumDict.TryGetValue(key, out var value))
|
||||
await ControlPlayback(value);
|
||||
|
||||
await VolumeControl(key);
|
||||
}
|
||||
|
||||
private async Task VolumeControl(Key key)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case Key.VolumeMute when mediaPlayer.Volume == 0:
|
||||
await ControlPlayback(PlaybackControlEnum.TurnOnVolume);
|
||||
break;
|
||||
case Key.VolumeMute:
|
||||
await ControlPlayback(PlaybackControlEnum.TurnOffVolume);
|
||||
break;
|
||||
case Key.Up:
|
||||
case Key.VolumeUp:
|
||||
var vUp = Math.Min(100, mediaPlayer.Volume + 5);
|
||||
ChangeVolume(vUp);
|
||||
mainWindow.Volume.Value = vUp;
|
||||
break;
|
||||
case Key.Down:
|
||||
case Key.VolumeDown:
|
||||
var vDown = Math.Max(0, mediaPlayer.Volume - 5);
|
||||
ChangeVolume(vDown);
|
||||
mainWindow.Volume.Value = vDown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await ControlPlayback(notification.PlaybackControl);
|
||||
mainWindow.VideoView.Focus();
|
||||
}
|
||||
|
||||
private async Task ControlPlayback(PlaybackControlEnum controlEnum)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ChangeVolume(notification.Volume);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ChangeVolume(int volume)
|
||||
{
|
||||
formState.CurrentVolume = volume;
|
||||
mediaPlayer.Volume = volume;
|
||||
}
|
||||
|
||||
private void Play()
|
||||
{
|
||||
if (mainWindow.LvFiles.SelectedItem == null)
|
||||
return;
|
||||
var fileInfo = (VideoFileInfo)mainWindow.LvFiles.SelectedItem;
|
||||
|
||||
formState.CurrentFile = fileInfo.Name;
|
||||
mainWindow.LoadExistingAnnotations();
|
||||
|
||||
mediaPlayer.Stop();
|
||||
mediaPlayer.Play(new Media(libVLC, fileInfo.Path));
|
||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
|
||||
}
|
||||
|
||||
private async Task SaveAnnotations()
|
||||
{
|
||||
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()));
|
||||
|
||||
if (!Directory.Exists(config.LabelsDirectory))
|
||||
Directory.CreateDirectory(config.LabelsDirectory);
|
||||
if (!Directory.Exists(config.ImagesDirectory))
|
||||
Directory.CreateDirectory(config.ImagesDirectory);
|
||||
|
||||
await File.WriteAllTextAsync($"{config.LabelsDirectory}/{fName}.txt", labels);
|
||||
|
||||
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.Editor.RemoveAllAnns();
|
||||
mediaPlayer.Play();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -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