rework to Azaion.Suite

This commit is contained in:
Alex Bezdieniezhnykh
2024-11-21 13:41:32 +02:00
parent 2cf69f4e4e
commit 5a592e9dbf
76 changed files with 1739 additions and 882 deletions
+16
View File
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
+73
View File
@@ -0,0 +1,73 @@
using System.Windows;
using Azaion.Common.DTO;
namespace Azaion.Common;
public class Constants
{
#region DefaultConfig
public const string CONFIG_PATH = "config.json";
public const string DEFAULT_VIDEO_DIR = "video";
public const string DEFAULT_LABELS_DIR = "labels";
public const string DEFAULT_IMAGES_DIR = "images";
public const string DEFAULT_RESULTS_DIR = "results";
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
public const int DEFAULT_THUMBNAIL_BORDER = 10;
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
public const double TRACKING_PROBABILITY_INCREASE = 15;
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
public static readonly Size DefaultWindowSize = new(1280, 720);
public static readonly Point DefaultWindowLocation = new(100, 100);
public static readonly Size DefaultThumbnailSize = new(240, 135);
#endregion
#region Thumbnails
public const string THUMBNAIL_PREFIX = "_thumb";
public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache";
#endregion
public static readonly List<AnnotationClass> DefaultAnnotationClasses =
[
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" },
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" },
new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" },
new() { Id = 3, Name = "Артилерія", ShortName = "Арта" },
new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" },
new() { Id = 5, Name = "Окопи", ShortName = "Окопи" },
new() { Id = 6, Name = "Військовий", ShortName = "Військов" },
new() { Id = 7, Name = "Накати", ShortName = "Накати" },
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" },
new() { Id = 9, Name = "Дим", ShortName = "Дим" },
new() { Id = 10, Name = "Літак", ShortName = "Літак" }
];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
public static TimeSpan? GetTime(string imagePath)
{
var timeStr = imagePath.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
return null;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!int.TryParse(timeStr[0..1], out var hours))
return null;
if (!int.TryParse(timeStr[1..3], out var minutes))
return null;
if (!int.TryParse(timeStr[3..5], out var seconds))
return null;
if (!int.TryParse(timeStr[5..6], out var milliseconds))
return null;
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
}
}
@@ -0,0 +1,49 @@
<DataGrid x:Class="Azaion.Common.Controls.AnnotationClasses"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
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="50"
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>
@@ -0,0 +1,9 @@
namespace Azaion.Common.Controls;
public partial class AnnotationClasses
{
public AnnotationClasses()
{
InitializeComponent();
}
}
+122
View File
@@ -0,0 +1,122 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Azaion.Common.DTO;
using Label = System.Windows.Controls.Label;
namespace Azaion.Common.Controls;
public class AnnotationControl : Border
{
private readonly Action<object, MouseButtonEventArgs> _resizeStart;
private const double RESIZE_RECT_SIZE = 9;
private readonly Grid _grid;
private readonly TextBlock _classNameLabel;
private readonly Label _probabilityLabel;
public TimeSpan? Time { get; set; }
private AnnotationClass _annotationClass = null!;
public AnnotationClass AnnotationClass
{
get => _annotationClass;
set
{
_grid.Background = value.ColorBrush;
_probabilityLabel.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, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null)
{
Time = time;
_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.SizeAll
};
_probabilityLabel = new Label
{
Content = probability.HasValue ? $"{probability.Value:F0}%" : string.Empty,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, -32, 0, 0),
FontSize = 16,
Visibility = Visibility.Visible
};
_selectionFrame = new Rectangle
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Stroke = new SolidColorBrush(Colors.Black),
StrokeThickness = 2,
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)
}
};
if (probability.HasValue)
_grid.Children.Add(_probabilityLabel);
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.FromArgb(230, 40, 40, 40)), // 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 CanvasLabel Info => new(AnnotationClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
}
+363
View File
@@ -0,0 +1,363 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Azaion.Annotator.DTO;
using Azaion.Common.DTO;
using MediatR;
using Color = System.Windows.Media.Color;
using Rectangle = System.Windows.Shapes.Rectangle;
namespace Azaion.Common.Controls;
public class CanvasEditor : Canvas
{
private Point _lastPos;
public SelectionState SelectionState { get; set; } = SelectionState.None;
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;
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
public IMediator Mediator { get; set; } = null!;
public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register(
nameof(GetTimeFunc),
typeof(Func<TimeSpan?>),
typeof(CanvasEditor),
new PropertyMetadata(null));
public Func<TimeSpan?> GetTimeFunc
{
get => (Func<TimeSpan?>)GetValue(GetTimeFuncProp);
set => SetValue(GetTimeFuncProp, value);
}
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)),
};
KeyDown += (_, args) =>
{
Console.WriteLine($"pressed {args.Key}");
};
MouseDown += CanvasMouseDown;
MouseMove += CanvasMouseMove;
MouseUp += CanvasMouseUp;
SizeChanged += CanvasResized;
Cursor = Cursors.Cross;
Children.Add(_newAnnotationRect);
Children.Add(_horizontalLine);
Children.Add(_verticalLine);
Children.Add(_classNameHint);
Loaded += Init;
}
private void Init(object sender, RoutedEventArgs e)
{
_horizontalLine.X1 = 0;
_horizontalLine.X2 = ActualWidth;
_verticalLine.Y1 = 0;
_verticalLine.Y2 = ActualHeight;
}
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 (SelectionState == SelectionState.NewAnnCreating)
NewAnnotationCreatingMove(sender, e);
if (SelectionState == SelectionState.AnnResizing)
AnnotationResizeMove(sender, e);
if (SelectionState == SelectionState.AnnMoving)
AnnotationPositionMove(sender, e);
}
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{
if (SelectionState == SelectionState.NewAnnCreating)
CreateAnnotation(e.GetPosition(this));
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)
{
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 (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;
SelectionState = SelectionState.AnnMoving;
e.Handled = true;
}
private void AnnotationPositionMove(object sender, MouseEventArgs e)
{
if (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;
SelectionState = SelectionState.NewAnnCreating;
}
private void NewAnnotationCreatingMove(object sender, MouseEventArgs e)
{
if (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 time = GetTimeFunc();
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
{
Width = width,
Height = height,
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y)
});
}
public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
{
var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
{
Width = canvasLabel.Width,
Height = canvasLabel.Height
};
annotationControl.MouseDown += AnnotationPositionStart;
SetLeft(annotationControl, canvasLabel.X );
SetTop(annotationControl, canvasLabel.Y);
Children.Add(annotationControl);
CurrentAnns.Add(annotationControl);
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
return annotationControl;
}
#endregion
private void RemoveAnnotations(IEnumerable<AnnotationControl> listToRemove)
{
foreach (var ann in listToRemove)
{
Children.Remove(ann);
CurrentAnns.Remove(ann);
}
}
public void RemoveAllAnns()
{
foreach (var ann in CurrentAnns)
Children.Remove(ann);
CurrentAnns.Clear();
}
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList());
private void ClearSelections()
{
foreach (var ann in CurrentAnns)
ann.IsSelected = false;
}
public void ClearExpiredAnnotations(TimeSpan time)
{
var expiredAnns = CurrentAnns.Where(x =>
x.Time.HasValue &&
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
.ToList();
RemoveAnnotations(expiredAnns);
}
}
@@ -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);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
using System.Windows.Media;
using Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public class AnnotationClass
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string ShortName { get; set; } = null!;
[JsonIgnore]
public Color Color => Id.ToColor();
[JsonIgnore]
public int ClassNumber => Id + 1;
[JsonIgnore]
public SolidColorBrush ColorBrush => new(Color);
}
@@ -0,0 +1,10 @@
namespace Azaion.Annotator.DTO;
public class AIRecognitionConfig
{
public string AIModelPath { get; set; } = null!;
public double FrameRecognitionSeconds { get; set; }
public double TrackingDistanceConfidence { get; set; }
public double TrackingProbabilityIncrease { get; set; }
public double TrackingIntersectionThreshold { get; set; }
}
@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config;
public class AnnotationConfig
{
public List<AnnotationClass> AnnotationClasses { get; set; } = null!;
[JsonIgnore]
private Dictionary<int, AnnotationClass>? _annotationClassesDict;
[JsonIgnore]
public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
public int? LastSelectedExplorerClass { get; set; }
public List<string> VideoFormats { get; set; } = null!;
public List<string> ImageFormats { get; set; } = null!;
}
+13
View File
@@ -0,0 +1,13 @@
namespace Azaion.Suite.Services.DTO;
public class ApiConfig
{
public string Url { get; set; } = null!;
public int RetryCount {get;set;}
public double TimeoutSeconds { get; set; }
}
public class LocalFilesConfig
{
public string DllPath { get; set; } = null!;
}
+91
View File
@@ -0,0 +1,91 @@
using System.IO;
using System.Text;
using Azaion.Annotator.DTO;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config;
public class AppConfig
{
public ApiConfig ApiConfig { get; set; } = null!;
public AnnotationConfig AnnotationConfig { get; set; } = null!;
public WindowConfig WindowConfig { get; set; } = null!;
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
}
public interface IConfigUpdater
{
void CheckConfig();
void Save(AppConfig config);
}
public class ConfigUpdater : IConfigUpdater
{
public void CheckConfig()
{
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH);
if (File.Exists(configFilePath))
return;
var appConfig = new AppConfig
{
AnnotationConfig = new AnnotationConfig
{
AnnotationClasses = Constants.DefaultAnnotationClasses,
VideoFormats = Constants.DefaultVideoFormats,
ImageFormats = Constants.DefaultImageFormats,
},
WindowConfig = new WindowConfig
{
WindowSize = Constants.DefaultWindowSize,
WindowLocation = Constants.DefaultWindowLocation,
ShowHelpOnStart = true,
FullScreen = true,
LeftPanelWidth = 250,
RightPanelWidth = 250,
},
DirectoriesConfig = new DirectoriesConfig
{
VideosDirectory = Constants.DEFAULT_VIDEO_DIR,
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR
},
ThumbnailConfig = new ThumbnailConfig
{
Size = Constants.DefaultThumbnailSize,
Border = Constants.DEFAULT_THUMBNAIL_BORDER
},
AIRecognitionConfig = new AIRecognitionConfig
{
AIModelPath = "azaion.onnx",
FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD
}
};
Save(appConfig);
}
public void Save(AppConfig config)
{
File.WriteAllText(Constants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
}
}
@@ -0,0 +1,10 @@
namespace Azaion.Common.DTO;
public class DirectoriesConfig
{
public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!;
public string ImagesDirectory { get; set; } = null!;
public string ResultsDirectory { get; set; } = null!;
public string ThumbnailsDirectory { get; set; } = null!;
}
@@ -0,0 +1,9 @@
using System.Windows;
namespace Azaion.Common.DTO.Config;
public class ThumbnailConfig
{
public Size Size { get; set; }
public int Border { get; set; }
}
+14
View File
@@ -0,0 +1,14 @@
using System.Windows;
namespace Azaion.Common.DTO.Config;
public class WindowConfig
{
public Size WindowSize { get; set; }
public Point WindowLocation { get; set; }
public bool FullScreen { get; set; }
public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; }
public bool ShowHelpOnStart { get; set; }
}
+8
View File
@@ -0,0 +1,8 @@
using MediatR;
namespace Azaion.Common.DTO;
public class ImageCreatedEvent(string imagePath) : INotification
{
public string ImagePath { get; } = imagePath;
}
+11
View File
@@ -0,0 +1,11 @@
using System.Windows.Input;
using MediatR;
namespace Azaion.Common.DTO;
public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification
{
public object Sender { get; set; } = sender;
public KeyEventArgs Args { get; set; } = args;
public WindowEnum WindowEnum { get; } = windowEnum;
}
+189
View File
@@ -0,0 +1,189 @@
using System.Drawing;
using System.Globalization;
using System.IO;
using Newtonsoft.Json;
using Size = System.Windows.Size;
namespace Azaion.Common.DTO;
public abstract class Label
{
[JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; }
protected Label()
{
}
protected Label(int classNumber)
{
ClassNumber = classNumber;
}
}
public class CanvasLabel : Label
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double? Probability { get; }
public CanvasLabel()
{
}
public CanvasLabel(int classNumber, double x, double y, double width, double height, double? probability = null) : base(classNumber)
{
X = x;
Y = y;
Width = width;
Height = height;
Probability = probability;
}
public CanvasLabel(YoloLabel label, Size canvasSize, Size videoSize, double? probability = null)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAr = cw / ch;
var videoAr = videoSize.Width / videoSize.Height;
ClassNumber = label.ClassNumber;
var left = label.CenterX - label.Width / 2;
var top = label.CenterY - label.Height / 2;
if (videoAr > canvasAr) //100% width
{
var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
X = left * cw;
Y = top * realHeight + blackStripHeight;
Width = label.Width * cw;
Height = label.Height * realHeight;
}
else //100% height
{
var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
X = left * realWidth + blackStripWidth;
Y = top * ch;
Width = label.Width * realWidth;
Height = label.Height * ch;
}
Probability = probability;
}
}
public class YoloLabel : Label
{
[JsonProperty(PropertyName = "x")] public double CenterX { get; set; }
[JsonProperty(PropertyName = "y")] public double CenterY { get; set; }
[JsonProperty(PropertyName = "w")] public double Width { get; set; }
[JsonProperty(PropertyName = "h")] public double Height { get; set; }
public YoloLabel()
{
}
public YoloLabel(int classNumber, double centerX, double centerY, double width, double height) : base(classNumber)
{
CenterX = centerX;
CenterY = centerY;
Width = width;
Height = height;
}
public RectangleF ToRectangle() =>
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size videoSize)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAr = cw / ch;
var videoAr = videoSize.Width / videoSize.Height;
ClassNumber = canvasLabel.ClassNumber;
double left, top;
if (videoAr > canvasAr) //100% width
{
left = canvasLabel.X / cw;
Width = canvasLabel.Width / cw;
var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
top = (canvasLabel.Y - blackStripHeight) / realHeight;
Height = canvasLabel.Height / realHeight;
}
else //100% height
{
top = canvasLabel.Y / ch;
Height = canvasLabel.Height / ch;
var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
left = (canvasLabel.X - blackStripWidth) / realWidth;
Width = canvasLabel.Width / realWidth;
}
CenterX = left + Width / 2.0;
CenterY = top + Height / 2.0;
}
public static YoloLabel? Parse(string s)
{
if (string.IsNullOrEmpty(s))
return null;
var strings = s.Replace(',', '.').Split(' ');
if (strings.Length != 5)
throw new Exception("Wrong labels format!");
var res = new YoloLabel
{
ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture),
CenterX = double.Parse(strings[1], CultureInfo.InvariantCulture),
CenterY = double.Parse(strings[2], CultureInfo.InvariantCulture),
Width = double.Parse(strings[3], CultureInfo.InvariantCulture),
Height = double.Parse(strings[4], CultureInfo.InvariantCulture)
};
return res;
}
public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken = default)
{
var str = await File.ReadAllTextAsync(filename, cancellationToken);
return str.Split('\n')
.Select(Parse)
.Where(ann => ann != null)
.ToList()!;
}
public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename, CancellationToken cancellationToken = default)
{
var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
await File.WriteAllTextAsync(filename, labelsStr, cancellationToken);
}
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
}
public class Detection : YoloLabel
{
public Detection(YoloLabel label, double? probability = null)
{
ClassNumber = label.ClassNumber;
CenterX = label.CenterX;
CenterY = label.CenterY;
Height = label.Height;
Width = label.Width;
Probability = probability;
}
public double? Probability { get; set; }
}
+10
View File
@@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace Azaion.Common.DTO;
public class LabelInfo
{
[JsonProperty("c")] public List<int> Classes { get; set; } = null!;
[JsonProperty("d")] public DateTime ImageDateTime { get; set; }
}
+19
View File
@@ -0,0 +1,19 @@
namespace Azaion.Common.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,
Previous = 11,
Next = 12,
Close = 13
}
+9
View File
@@ -0,0 +1,9 @@
namespace Azaion.Annotator.DTO;
public enum SelectionState
{
None = 0,
NewAnnCreating = 1,
AnnResizing = 2,
AnnMoving = 3
}
+8
View File
@@ -0,0 +1,8 @@
namespace Azaion.Common.DTO;
public enum WindowEnum
{
None = 0,
Annotator = 10,
DatasetExplorer = 20
}
@@ -0,0 +1,28 @@
using System.Windows.Media;
namespace Azaion.Common.Extensions;
public static class ColorExtensions
{
public static Color ToColor(this int id)
{
var index = id % ColorValues.Length;
var hex = index == -1
? "#40DDDDDD"
: $"#40{ColorValues[index]}";
var color =(Color)ColorConverter.ConvertFromString(hex);
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,12 @@
using Newtonsoft.Json.Converters;
namespace Azaion.Common.Extensions;
public class DenseDateTimeConverter : IsoDateTimeConverter
{
public DenseDateTimeConverter()
{
DateTimeFormat = "yy-MM-dd HH:mm:ss";
}
}
@@ -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();
}
+67
View File
@@ -0,0 +1,67 @@
namespace Azaion.Annotator.Extensions;
public class ParallelOptions
{
public int ProgressUpdateInterval { get; set; } = 100;
public Func<int, Task> ProgressFn { get; set; } = null!;
public double CpuUtilPercent { get; set; } = 100;
}
public class ParallelExt
{
public static async Task ForEachAsync<T>(ICollection<T> source,
Func<T, CancellationToken, ValueTask> processFn,
ParallelOptions? parallelOptions = null,
CancellationToken cancellationToken = default)
{
parallelOptions ??= new ParallelOptions
{
CpuUtilPercent = 100,
ProgressUpdateInterval = 100,
ProgressFn = i =>
{
Console.WriteLine($"Processed {i} item by Task {Task.CurrentId}%");
return Task.CompletedTask;
}
};
var threadsCount = (int)(Environment.ProcessorCount * parallelOptions.CpuUtilPercent / 100.0);
var processedCount = 0;
var chunkSize = Math.Max(1, (int)(source.Count / (decimal)threadsCount));
var chunks = source.Chunk(chunkSize).ToList();
if (chunks.Count > threadsCount)
{
chunks[^2] = chunks[^2].Concat(chunks.Last()).ToArray();
chunks.RemoveAt(chunks.Count - 1);
}
var progressUpdateLock = new SemaphoreSlim(1);
var tasks = new List<Task>();
foreach (var chunk in chunks)
{
tasks.Add(await Task.Factory.StartNew(async () =>
{
foreach (var item in chunk)
{
await processFn(item, cancellationToken);
Interlocked.Increment(ref processedCount);
if (processedCount % parallelOptions.ProgressUpdateInterval == 0 && parallelOptions.ProgressFn != null)
_ = Task.Run(async () =>
{
await progressUpdateLock.WaitAsync(cancellationToken);
try
{
await parallelOptions.ProgressFn(processedCount);
}
finally
{
progressUpdateLock.Release();
}
}, cancellationToken);
}
}, TaskCreationOptions.LongRunning));
}
await Task.WhenAll(tasks);
}
}
@@ -0,0 +1,19 @@
namespace Azaion.Annotator.Extensions;
public static class ThrottleExt
{
private static bool _throttleOn;
public static async Task Throttle(Func<Task> func, TimeSpan? throttleTime = null)
{
if (_throttleOn)
return;
_throttleOn = true;
await func();
_ = Task.Run(async () =>
{
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500));
_throttleOn = false;
});
}
}
@@ -0,0 +1,20 @@
using System.Windows;
using Azaion.Common.DTO;
namespace Azaion.Common.Extensions;
public static class WindowExtensions
{
public static WindowEnum GetParentWindow(this FrameworkElement? element)
{
while (element != null && element is not Window)
{
element = element.Parent as FrameworkElement;
}
if (element is not Window)
return WindowEnum.None;
return WindowEnum.Annotator;
}
}