move Windows app to Windows folder, create folder for Web, create simplest web api service

This commit is contained in:
Alex Bezdieniezhnykh
2024-07-11 19:40:17 +03:00
parent ea8d0e686a
commit 32c92fedf2
41 changed files with 138 additions and 0 deletions
+7
View File
@@ -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>
+56
View File
@@ -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));
}
}
+10
View File
@@ -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;
}
}
}
+61
View File
@@ -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();
}
}
}
+17
View File
@@ -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);
}
}
+24
View File
@@ -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нтер] для збереження і перегляду далі" }
};
}
+58
View File
@@ -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;
}
+420
View File
@@ -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>
+270
View File
@@ -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();
}
}
+69
View File
@@ -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": "Назва класу"
}
}